1 """Mobile Security & Privacy Simulator core"""
2
3 import sys
4 import random
5 import math
6 import time
7 import datetime
8 import struct
9 from heapq import heappush, heappop
10 from struct import pack, unpack
11 import logging
12
13 import socket
14
15 from SimPy import SimulationRT
16 from SimPy.SimulationRT import hold, passivate
17
18 from locations import PersonWakeUp
19 import group
20 import collide
21 import monitors
22 from geo import utils
23 from geo import osm
24
25 __author__ = "B. Henne, F. Ludwig, P. Tute"
26 __contact__ = "henne@dcsec.uni-hannover.de"
27 __copyright__ = "(c) 2010-2012, DCSec, Leibniz Universitaet Hannover, Germany"
28 __license__ = "GPLv3"
32 """A MOSP Simulation.
33
34 Extends SimPy's SimulationRT (Simulation with Real Time Synchronization)
35 to store MOSP specific data and implement specific methods.
36 @author: F. Ludwig
37 @author: P. Tute
38 @author: B. Henne"""
39
40 - def __init__(self, geo, start_timestamp=None, rel_speed=None, seed=1, allow_dup=False):
41 """Initialize the MOSP Simulation.
42
43 @param geo: geo model for simulation, a mosp.geo.osm.OSMModel extending the mops.collide.World
44 @param start_timestamp: time.time timestamp when simulation starts - used to calc DateTime of simlation out of simulation ticks.
45 @param rel_speed: (SimPy) ratio simulation time over wallclock time; example: rel_speed=200 executes 200 units of simulation time in about one second
46 @param seed: seed for simulation random generator
47 @param allow_dup: allow duplicates? only one or multiple Simulations can be startet at once
48 """
49 SimulationRT.SimulationRT.__init__(self)
50 assert allow_dup or osm.GLOBAL_SIM is None
51 osm.GLOBAL_SIM = self
52 self.initialize()
53 self.geo = geo
54 self.monitors = []
55 self.rel_speed = rel_speed if rel_speed else 1
56 self.start_timestamp = self.start_timestamp if start_timestamp else time.time()
57 self.random = random.Random(seed)
58 self.next_person_id = 0
59 self.persons = group.PersonGroup()
60 self.removed_persons = {}
61 self.person_alarm_clock = PersonWakeUp('Omni-present person wake up alarm', self)
62 self.messages = []
63 group.Message.sim = self
64 geo.initialize(self)
65
67 """Add a Monitor to Simulation to produce any kind of output.
68 @param monitor_cls: the monitor class from mops.monitors
69 @param tick: new monitor will observe every tick tick
70 @param kwargs: keyword parameters for monitor class instantiation
71 @return: reference to new, added monitor instance
72 """
73 mon = monitor_cls('mon'+str(len(self.monitors)), self, tick, kwargs)
74 self.monitors.append(mon)
75 return mon
76
77 - def add_persons(self, pers_cls, n=1, monitor=None, args=None):
78 """Add a Person to Simulation.
79 @param pers_cls: the person class (inherited from mosp.core.Person)
80 @param n: the number of new, added instances of pers_cls
81 @param monitor: (list of) monitor(s) the person(s) shall be observed by
82 @param args: dictionary of arguments for pers_cls instantiation
83 """
84 if not args:
85 args = {}
86 for i in xrange(n):
87 seed = self.random.randrange(2**24)
88 pers = pers_cls(self.next_person_id, self, random.Random(seed), **args)
89 self.next_person_id += 1
90 if monitor is not None:
91 if isinstance(monitor, monitors.EmptyMonitor):
92
93 monitor.append(pers)
94 elif hasattr(monitor,'__iter__'):
95
96 for mon in monitor:
97 mon.append(pers)
98 self.activate(pers, pers.go(), 0)
99 self.persons.add(pers)
100
102 """Find a person by its ID.
103
104 @param: the ID of the person
105 @return: the person or None, if the ID was not found
106
107 """
108 result = self.persons.filter(p_id=id)
109 if len(result) == 1:
110 return result.pop()
111 else:
112 return None
113
115 """Remove a person from the simulation.
116
117 The person will be saved in self.removed_persons for later reference.
118 @todo: try using Process.cancel() from SimPy (is broken atm and may stay broken...try again with new go)
119 @todo: maybe remove person from monitors
120 @param person: the person to be removed. The person must have been added and must be active.
121 """
122 self.removed_persons[person.p_id] = person
123 person.stop_actions(True)
124 person.remove_from_sim = True
125 self.persons.remove(person)
126
128 """Add a previously removed person to the simulation again.
129
130 @param id: id of the removed person
131 @type id: int
132 @param changes: changes to be made when reinserting the person
133 @type changes: dict with pairs of 'variable_name': value (variable_name in a string)
134 """
135 if id not in self.removed_persons:
136 print 'Tried to re-add unknown person...ignored.'
137 print id, type(id)
138 print self.removed_persons
139 return
140 person = self.removed_persons[id]
141 person.__dict__.update(changes)
142 person.current_way.persons.append(person)
143 person.current_coords = person.current_coords_impl
144 for a in person._stopped_actions_for_removal:
145 a.start()
146 person._stopped_actions_for_removal = []
147 self.persons.add(person)
148
149 person._terminated = False
150 person._nextTime = None
151 self.activate(person, person.go())
152 person.readd_actions()
153
154 - def run(self, until, real_time, monitor=True):
155 """Runs Simulation after setup.
156
157 @param until: simulation runs until this tick
158 @param real_time: run in real-time? or as fast as possible
159 @param monitor: start defined monitors?
160 """
161 if monitor:
162 if len(self.monitors) == 0:
163 raise monitors.NoSimulationMonitorDefinedException('at mosp.Simulation.run()')
164 for mon in self.monitors:
165 mon.init()
166
167
168 self.activate(self.person_alarm_clock, self.person_alarm_clock.serve(), 0)
169
170
171 self.rtstart = self.wallclock()
172 self.rtset(self.rel_speed)
173
174 last_event_time = 0
175 avg_delta = 0
176 while self._timestamps and not self._stop:
177 next_event_time = self.peek()
178
179
180 if real_time and self.rel_speed == 1:
181 now = time.time()
182 sim_now = self.start_timestamp + self.now()
183 delta = now - sim_now
184
185
186
187
188
189 if delta < 0:
190 time.sleep(-delta)
191
192 if last_event_time != next_event_time:
193 pass
194
195 last_event_time = next_event_time
196 if next_event_time > until:
197 break
198
199
200
201
202 if real_time and self.rel_speed != 1:
203 delay = (
204 next_event_time / self.rel_speed -
205 (self.wallclock() - self.rtstart)
206 )
207 if delay > 0: time.sleep(delay)
208
209
210 while self.messages and self.messages[0].time < next_event_time:
211
212 heappop(self.messages)()
213
214 self.step()
215
216
217
218
219 for m in self.monitors:
220 m.end()
221 if not self._stop and self._timestamps:
222 self._t = until
223 return 'SimPy: Normal exit'
224 else:
225 return 'SimPy: No activities scheduled'
226
228 """Returns Simulation information for pickling using the pickle module."""
229 state = self.__dict__
230
231 del state['sim']
232 return state
233
236 """Simulation DateTime extension.
237 @author: F. Ludwig"""
238 @classmethod
240 """Returns a DateTime object representing simulation time.
241 @return: DateTime object with time as sim.start_timestamp+tick/sim_rel_speed"""
242 ts = sim.start_timestamp + float(tick) / sim.rel_speed
243 dt = self.fromtimestamp(ts)
244 dt.sim = sim
245 dt.tick = tick
246 return dt
247
250 """Base class for Person actions. Every action is an own SimPy Process.
251 @author: F. Ludwig"""
253 """Person executes action (self.func) every self.every ticks."""
254 while True:
255 yield hold, self, self.every
256 answer = self.func(self.pers)
257 if answer:
258 self.every = answer
259
260
261 -def action(every, start=True):
262 """MOSP action decorator executes an action defined by an method regularly using a PersonAction(SimPy Process).
263 @param every: action is executed every every ticks
264 @param start: start action immediately?
265 @author: F. Ludwig"""
266 def re(func):
267 class PersonAction(PersonActionBase):
268 """Encapsulates a person's action executes regularly."""
269 def __init__(self, pers, sim):
270 """Inits the PersonAction. Starts it if requested."""
271 name = pers.name + '_action_' + str(id(self))
272 SimulationRT.Process.__init__(self, name=name, sim=sim)
273 self.every = every
274 self.func = func
275 self.pers = pers
276 if start:
277 self.start()
278
279 def start(self, at='undefined', delay='undefined'):
280 """Starts the action.
281 @param at: start tick
282 @param delay: start action with delay of ticks"""
283 self.sim.activate(self, self.go(), at=at, delay=delay)
284
285 def stop(self):
286 """Stops an action."""
287 self.pers.cancel(self)
288 pass
289
290
291 func.action = PersonAction
292 return func
293 return re
294
295 -def start_action(action, at='undefined', delay='undefined'):
296 """Starts an action that has not been started (or stopped?) before.
297 @author: F. Ludwig"""
298 action.im_self._actions[action.im_func].start(at=at, delay=delay)
299
301 """Stops an action.
302 @author: B. Henne"""
303 action.im_self._actions[action.im_func].stop()
304
306 """Return the distance of two coordinate pairs (four values).
307 @author: B. Henne"""
308 return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
309
311 """Return the distance of two coordinate pairs (two pairs).
312 @param n1: List/2-tuple [x, y]
313 @param n2: List/2-tuple [x, y]
314 @author: B. Henne"""
315 return math.sqrt((n2[0] - n1[0])**2 + (n2[1] - n1[1])**2)
316
317
318 -class Person(SimulationRT.Process, collide.Point):
319 """A basic simulated Person."""
320 - def __init__(self, id, sim, random, speed=1.4, **kwargs):
321 """Initializes the Person.
322 @param id: unique id of a person
323 @param sim: reference to superordinate mosp.core.Simulation
324 @param random: random object for Person - use superordinate mosp.core.Simulation.random
325 @param speed: basic walking speed of person in meter/tick
326 @param kwargs: additonal keyword arguments intended for inheriting classes
327 @author: F. Ludwig
328 @author: P. Tute
329 @author: B. Henne
330 """
331 SimulationRT.Process.__init__(self, name='p' + str(id), sim=sim)
332
333
334
335 self._p_speed = speed
336 self._random = random
337 self._start_time = sim.now()
338 self._duration = 0
339 self._location_offset = [0, 0]
340 self._actions = {}
341 self._stopped_actions = []
342 self._stopped_actions_for_removal = []
343 self._messages = []
344
345
346 self.last_node = self.get_random_way_node(include_exits=False)
347 self.next_node = self.last_node
348 self.start_node = None
349 self.dest_node = self.next_node
350 self.current_way = self._random.choice(self.next_node.ways.values())
351 self.current_way.persons.append(self)
352 self.road_orthogonal_offset = self.current_way.width
353 self.current_coords = self.current_coords_impl
354 self.last_coord = [0, 0]
355 self.target_coord = [0, 0]
356 self.last_coord = [self.last_node.x, self.last_node.y]
357 self.target_coord = [self.next_node.x, self.next_node.y]
358
359
360 self.need_next_target = False
361 self.passivate = False
362 self.passivate_with_stop_actions = False
363 self.stop_all_actions = False
364 self.remove_from_sim = False
365
366
367
368
369 self.p_id = id
370 self.p_color = 0
371 self.p_color_rgba = (0.1, 0.1, 1.0, 0.9)
372
373
374 for name, obj in self.__class__.__dict__.items():
375 if hasattr(obj, 'action') and issubclass(obj.action, PersonActionBase):
376 self._actions[obj] = obj.action(self, sim)
377
379 """Returns Person information for pickling using the pickle module."""
380 re = self.__dict__
381 del re['sim']
382
383
384 return re
385
387 """Returns a random way_node from sim.way_nodes.
388
389 Formerly known as get_random_node(). Todo: push to geo model at mosp.geo!
390 @param include_exits: if True, also nodes marked as border nodes are regarded"""
391 if not include_exits:
392 possible_targets = [n for n in self.sim.geo.way_nodes if 'border' not in n.tags]
393 else:
394 possible_targets = self.sim.geo.way_nodes
395 return self._random.choice(possible_targets)
396
398 """Calculates the current position from remaining time
399 using last_coord and target_coord to implement road width"""
400 if self._duration == 0:
401 return self.target_coord
402 last = self.last_coord
403 target = self.target_coord
404 completed = (float(self.sim.now() - self._start_time) / self._duration)
405 return last[0] + (target[0] - last[0])* completed, last[1] + (target[1] - last[1])* completed
406
407
408
409
410
411
412
413
414
415
416
417 @property
419 """Should not be used. In most cases x and y are needed the same time.
420 So properties x and y duplicate the number of calls of current_coords()."""
421 return self.current_coords()[0]
422
423 @property
425 """Should not be used. In most cases x and y are needed the same time.
426 So properties x and y duplicate the number of calls of current_coords()."""
427 return self.current_coords()[1]
428
430 """Checks if this person collides with the given circle.
431 Overwrites point.collide_circle for performance optimization:
432 call current_coords only once per x-y-pair"""
433 current_coords = self.current_coords()
434 if current_coords is None:
435
436 return False
437 selfx, selfy = current_coords
438 return math.sqrt((selfx - x)**2 + (selfy - y)**2) <= radius
439
441 """Checks if this person collides with the given rectangle.
442 Overwrites point.collide_rectangle for performance optimization:
443 call current_coords only once per x-y-pair"""
444 selfx, selfy = self.current_coords()
445 return (x_min <= selfx <= x_max and
446 y_min <= selfy <= y_max)
447
449 """property speed: movement speed of the Person."""
450 return self._p_speed
451
453 """Sets movement speed of the Person."""
454 self._p_speed = speed
455 self.interrupt(self)
456
457 p_speed = property(get_speed, set_speed)
458
460 return self.__class__.__name__
461
462 p_agenttype = property(_get_classname)
463
465 """Return all the person's properties (variables and properties p_*) as a dictionary."""
466 properties = {}
467 for v in self.__dict__.iteritems():
468 if v[0].startswith('p_'):
469 properties[v[0]] = v[1]
470
471
472
473
474
475
476
477
478
479
480
481
482 for attr_name in dir(self.__class__):
483 attr = getattr(self.__class__, attr_name)
484 if isinstance(attr, property) and attr_name.startswith('p_'):
485 properties[attr_name] = attr.__get__(self)
486 return properties
487
489 """Finds a new target to move to. Sets next_node, maybe dest_node. Must be overwritten with an implementation."""
490 return
491
493 """Calculates new target coordinates based on next_node coordinates
494 plus orthogonal to road offset defined by self.road_orthogonal_offset.
495 Examples of different values for self.road_orthogonal_offset:
496 - [0,0]: No road width
497 - [0,5]: random offset 0-5m to the right of the road in movement direction.
498 Walking on a two way single lane road, walking in the right side.
499 - [3,5]: random offset 3-5m to the right of the road in movement direction.
500 Walking on the right sidewalk, road width 3m, sidewalk width 2m.
501 - [-2,2]: random offset of 0-2m to the left and the right of the way.
502 Walking on both 2m width lanes, the own and the opposite direction's lane in both directions
503 Same as walking on a 6m width street in both directions, no distinct lanes."""
504 target = self.next_node
505 last = self.last_node
506 if target == last:
507 return [target.x, target.y]
508 r = self._random.uniform(*self.road_orthogonal_offset)
509
510 offset_angle = (last.ways[target].directions[target]-90)/180*math.pi
511 return [target.x-r*math.cos(offset_angle), target.y-r*math.sin(offset_angle)]
512
514 """The main-loop of every person.
515
516 The way a person behaves can be modified by implementing own versions of
517 Person.handle_interrupts
518 Person.think,
519 Person.receive and
520 Person.next_target.
521 @author: P. Tute
522
523 """
524 sleep = 1
525 if self.last_node == self.next_node:
526 self._duration = 0
527 else:
528 self._duration = self.calculate_duration()
529
530 while True:
531 yield hold, self, max(sleep, 1)
532
533 for m in [m for m in self._messages if self.sim.now() >= m[0]]:
534 if self.receive(m[1], m[2]):
535 self._messages.remove(m)
536 if self.interrupted():
537 for callb, args, kwargs in self._messages:
538 callb(*args, **kwargs)
539 self._messages.remove((callb, args, kwargs))
540 self.handle_interrupts()
541 self.last_coord = self.current_coords()
542
543 self._duration = self.calculate_duration()
544 sleep = self.think()
545
546 if self.stop_all_actions:
547 self.stop_all_actions = False
548 self.stop_actions()
549
550 if self.remove_from_sim:
551 self.remove_from_sim = False
552 curr = self.current_coords()
553 self.last_stop_coords = [ curr[0] + self._location_offset[0], curr[1] + self._location_offset[1] ]
554 self.current_coords = lambda: self.last_stop_coords
555 self.current_way.persons.remove(self)
556 return
557
558 if self.stop_all_actions:
559 self.stop_all_actions = False
560 self.stop_actions()
561
562 if self.passivate:
563 self._duration = 0
564 curr = self.current_coords()
565 self.last_stop_coords = [ curr[0] + self._location_offset[0], curr[1] + self._location_offset[1] ]
566 self.current_coords = lambda: self.last_stop_coords
567 if self.passivate_with_stop_actions:
568 self.stop_actions()
569 yield passivate, self
570
571 if self.need_next_target:
572 self.next_target()
573 self.need_next_target = False
574 self.last_coord = self.target_coord
575 self.target_coord = self.next_target_coord()
576 if self.next_node is not self.last_node:
577 self.current_way.persons.remove(self)
578 self.current_way = self.last_node.ways[self.next_node]
579 self.current_way.persons.append(self)
580 self.road_orthogonal_offset = self.current_way.width
581
582
583 self.target_coord = self.next_target_coord()
584 self._duration = self.calculate_duration()
585 self._start_time = self.sim.now()
586
587 self._duration = max(self.calculate_duration(),
588 1)
589 if sleep < 1:
590 sleep = self._duration
591 else:
592 sleep = min(sleep, self._duration)
593
594
596 """Calculate the time needed for walking to the next node."""
597 return int(math.ceil(distN2N(self.last_coord, self.target_coord) / self.p_speed))
598
599 - def reactivate(self, at = 'undefined', delay = 'undefined', prior = False):
600 """Reactivates passivated person and optionally restarts stopped actions."""
601 self.passivate = False
602 self._location_offset = [0, 0]
603 self.current_coords = self.current_coords_impl
604 self.sim.reactivate(self, at, delay, prior)
605 if self.passivate_with_stop_actions:
606 self.restart_actions()
607
608 - def pause_movement(self, duration, location_offset_xy=0, deactivateActions=False):
609 """Stops movement of person. Currently only works with passivating at next_node.
610 Currently cannot be used to stop on a way like after infect!
611 @param duration: pause duration
612 @param location_offset_xy: optional random offset to be added to current location when stopping.
613 @param deactivateActions: deactive actions while pausing?"""
614 self.passivate = True
615 self._location_offset = [ self._random.randint(0, int(location_offset_xy)), self._random.randint(0, int(location_offset_xy)) ]
616 self.passivate_with_stop_actions = deactivateActions
617
618 self.sim.person_alarm_clock.interact(self, duration=duration-1)
619
621 """Actions of the person when arriving at a node. To be overwritten with an implementation.
622
623 This method is executes when the Person arrives at a node.
624 @param node: is the next_node the Person arrives at"""
625 pass
626
628 """Think about what to do next.
629
630 This method can include all logic of the person (what to do, where to go etc.).
631 Decisions could be made by using flags for example.
632 This is where a new self.dest_node and self.start_node should be set if necessary.
633 self.next_node should not be set here. This should be done in self.next_target.
634 @return: time until next wakeup (int, ticks), returning a negative number or 0 will cause self.go() to find a time
635 @note: This method provides only the most basic functionality. Overwrite (and if necessary call) it to implement own behaviour.
636 """
637 if self._start_time + self._duration <= self.sim.now():
638
639 self.next_node.on_visit(self)
640 self.act_at_node(self.next_node)
641 self.need_next_target = True
642 return -1
643
645 """Find out, what caused an interrupt and act accordingly.
646
647 This method is called whenever a person is interrupted.
648 This is the place to implement own reactions to interrupts. Calling methods that were send via a send() call is done BEFORE this method is called. Handling pause, stop, removal and change of movement speed is done automatically AFTER this method is called. Removing the corresponding flag (setting it to False) in this method allows for handling these things on your own.
649 @note: This method does nothing per default. Implement it to react to interrupts. If you do not want or need to react to interrupts, ignore this method.
650 """
651 pass
652
653 - def send(self, receiver, message, earliest_arrival=None, interrupt=False):
654 """Send a message to another Person.
655
656 Person.receive() must be implemented for anything to actually happen. Without implementation of that method, messages will simply be removed at earliest_arrival.
657 Messages are received, whenever the receiver wakes up, before interrupts are handled or think() is called.
658
659 @param receiver: a Person to receive the message
660 @type receiver: mosp.Person or group.PersonGroup
661 @param message: any type of message to be delivered. Implement Person.receive() accordingly.
662 @param earliest_arrival: the earliest tick to receive the message. Default is the next tick (self.sim.now() + 1).
663 @param interrupt: interrupt the receiver, after this message was queued.
664
665 """
666 if earliest_arrival == None:
667 earliest_arrival = self.sim.now() + 1
668 if type(receiver) == group.PersonGroup:
669 for rec in receiver:
670 rec._messages.append([earliest_arrival, message, self])
671 if interrupt:
672 self.interrupt(rec)
673 else:
674 receiver._messages.append([earliest_arrival, message, self])
675 if interrupt:
676 self.interrupt(rec)
677
678 - def receive(self, message, sender):
679 """Receive a message and handle it.
680
681 Removal from the message queue and earliest arrival time are handled automatically.
682
683 @param message: a message from the persons message queue.
684 @param sender: the sender of the message.
685 @return: True if the message should be removed from the queue, False else. It will then be delivered again in the next cycle.
686
687 """
688 return True
689
691 """Start an action."""
692 self._actions[action].start()
693
695 """Stop an action."""
696 self._actions[action].stop()
697
699 """Stop all actions of the Person.
700
701 For all actions if the Person: call stop and store as stopped action
702 @param removal: used to signal removal of the person. Actions are then stored in seperate list.
703 """
704 for a in self._actions.values():
705 if a.active():
706 if removal:
707 self._stopped_actions_for_removal.append(a)
708 else:
709 self._stopped_actions.append(a)
710 a.stop()
711
713 """Sets stop_all_actions und interrupts."""
714 self.stop_all_actions = True
715 self.interrupt(self)
716
718 """Restarts all actions that where stopped via stop_actions."""
719 for a in self._stopped_actions:
720 a.start()
721 self._stopped_actions = []
722
723 - def get_near(self, dist, self_included=True):
724 """Returns Persons near this Person.
725 @param dist: lookup radius
726 @param self_included: if True, this Person itself is included in resulting PersonGroup
727 @return: PersonGroup containing all Persons in distance
728 @rtype: mosp.group.PersonGroup"""
729 current_coords = self.current_coords()
730 re = group.PersonGroup()
731 if current_coords is None:
732
733 return re
734 x, y = current_coords
735 for element in self.sim.geo.collide_circle(x, y, dist):
736 if isinstance(element, Person):
737 if element.collide_circle(x, y, dist):
738 if self_included or element != self:
739 re.add(element)
740 else:
741 for person in element.persons:
742 if person.collide_circle(x, y, dist):
743 if self_included or person != self:
744 re.add(person)
745 return re
746
748 """Do things, when person is readded via Simulation.readd().
749
750 This does nothing by default. It could be implemented by a simulation, if needed."""
751 pass
752