Package topo :: Package base :: Module simulation
[hide private]
[frames] | no frames]

Source Code for Module topo.base.simulation

   1  """ 
   2  A general-purpose Simulation class for discrete events.   
   3   
   4  The Simulation object is the central object of a simulation.  It 
   5  handles the simulation clock time and maintains communication between 
   6  the components of the simulation. 
   7   
   8  A simulation is structured as a directed graph of event-processing 
   9  nodes called EventProcessors (EPs).  EventProcessors generate 
  10  data-carrying Events, which are routed through the graph to other 
  11  EventProcessors via delayed connections.  Most objects in the 
  12  simulation will be a subclass of EventProcessor, customized to provide 
  13  some specific behavior. 
  14   
  15  There are different subclasses of Event, each doing different types of 
  16  computation, and each can contain any arbitrary Python data.  A 
  17  simulation begins by giving each EventProcessor an opportunity to send 
  18  any initial events.  It then proceeds by processing and delivering 
  19  events to EventProcessors in time order.  After all events for the 
  20  current time are processed, the simulation gives each EventProcessor a 
  21  chance to do any final computation, after which simulation time skips 
  22  to the time of the earliest event remaining in the queue. 
  23   
  24   
  25  PORTS 
  26   
  27  All connections between EPs are tagged with a source port and a 
  28  destination port.  Ports are internal addresses that EPs can use to 
  29  distinguish between inputs and outputs.  A port specifier can be any 
  30  hashable Python object.  If not specified, the input and output ports 
  31  for a connection default to None. 
  32   
  33  src_ports distinguish different types of output that an EP may 
  34  produce. When sending output, the EP must call self.send_output() 
  35  separately for each port.  dest_ports distinguish different types of 
  36  input an EP may receive and process.  The EP is free to interpret the 
  37  input port designation on an incoming event in any way it chooses. 
  38   
  39  An example dest_port might be for an EP that receives 'ordinary' 
  40  neural activity on the default port, and receives a separate 
  41  modulatory signal that influences learning.  The originator of the 
  42  modulatory signal might have a connection to the EP with dest_port = 
  43  'Modulation'.  Multiple ports can be grouped by a dest EP by assuming 
  44  a convention that the keys are tuples, 
  45  e.g. ('JointNormalize','Group1'), ('JointNormalize','Group2'). 
  46   
  47  An example src_port use might be an EP that sends different events to 
  48  itself than it sends out to other EPs.  In this case the self 
  49  connections might have src_port = 'Recurrent', and probably also a 
  50  special dest_port. 
  51   
  52   
  53  $Id: simulation.py 11090 2010-06-22 11:09:01Z ceball $ 
  54  """ 
  55  __version__='$Revision: 11090 $' 
  56   
  57  import param 
  58  from param.parameterized import as_uninitialized,OptionalSingleton 
  59   
  60  from copy import copy, deepcopy 
  61   
  62  import bisect 
  63   
  64  # JABALERT: Are these used for anything? 
  65  SLEEP_EXCEPTION = "Sleep Exception" 
  66  STOP = "Simulation Stopped" 
  67   
  68  # CEBALERT: Is it dangerous to have 'Forever' implemented 
  69  # like this? To start with, min(Forever,1) gives 
  70  # FixedPoint('-1.00,2') i.e. Forever. 
  71  # Forever = FixedPoint(-1) 
  72  Forever = -1 
  73  # (Python 2.6 includes support for float('inf') on all platforms?) 
  74   
  75  # Default path to the current simulation, from main 
  76  # Only to be used by script_repr(), to allow it to generate 
  77  # a runnable script 
  78  simulation_path="topo.sim" 
79 80 81 82 83 -class EventProcessor(param.Parameterized):
84 """ 85 Base class for EventProcessors, i.e. objects that can accept and 86 handle events. This base class handles the basic mechanics of 87 connections and sending events, and stores both incoming and outgoing 88 connections. 89 90 The dest_ports attribute specifies which dest_ports are supported 91 by this class; subclasses can augment or change this list if they 92 wish. The special value dest_ports=None means to accept 93 connections to any dest_port, while dest_ports=[None,'Trigger'] 94 means that only connections to port None or port 'Trigger' are 95 accepted. 96 97 Similarly, the src_ports attribute specifies which src_ports will 98 be given output by this class. 99 """ 100 __abstract = True 101 102 dest_ports=[None] 103 104 src_ports=[None] 105 106
107 - def __init__(self,**params):
108 """ 109 Create an EventProcessor. 110 111 Note that just creating an EventProcessor does not mean it is 112 part of the simulation (i.e. it is not in the simulation's list 113 of EventProcessors, and it will not have its start() method called). 114 To add an EventProcessor e to a simulation s, simply do 115 s['name_of_e']=e. At this point, e's 'name' attribute will be set 116 to 'name_of_e'. 117 """ 118 super(EventProcessor,self).__init__(**params) 119 120 # A subclass could use another data stucture to optimize operations 121 # specific to itself, if it also overrides _dest_connect(). 122 self.in_connections = [] 123 self.out_connections = [] 124 125 self.simulation = None
126
127 - def _port_match(self,key,portlist):
128 """ 129 Returns True if the given key matches any port on the given list. 130 131 In the default implementation, a port is considered a match if 132 the port is == to the key, but subclasses of EventProcessor can 133 override this to provide weaker forms of matching. 134 """ 135 return key in portlist
136 137 # if extra parameters are required for an EP subclass, a 138 # dictionary could be added to Simulation.connect() to hold 139 # them, and passed on here
140 - def _src_connect(self,conn):
141 """ 142 Add the specified connection to the list of outgoing connections. 143 Should only be called from Simulation.connect(). 144 """ 145 146 if self.src_ports and not self._port_match(conn.src_port,self.src_ports): 147 raise ValueError("%s is not on the list of ports provided for outgoing connections for %s: %s." % 148 (str(conn.src_port), self.__class__, str(self.src_ports))) 149 150 # CB: outgoing connection must be uniquely named among others 151 # going to the same destination. 152 for existing_connection in self.out_connections: 153 if existing_connection.name==conn.name and existing_connection.dest==conn.dest: 154 raise ValueError('A connection out of an EventProcessor must have a unique name among connections to a particular destination; "%s" out of %s into %s already exists'%(conn.name,conn.dest,self.name)) 155 156 # CB: alternative: outgoing connection must have a unique name 157 ## for existing_connection in self.out_connections: 158 ## if existing_connection.name==conn.name: 159 ## raise ValueError('A connection out of an EventProcessor must have a unique name; "%s" out of %s already exists'%(conn.name,self.name)) 160 161 self.out_connections.append(conn)
162 163
164 - def _dest_connect(self,conn):
165 """ 166 Add the specified connection to the list of incoming connections. 167 Should only be called from Simulation.connect(). 168 """ 169 170 if self.dest_ports and not self._port_match(conn.dest_port,self.dest_ports): 171 raise ValueError("%s is not on the list of ports allowed for incoming connections for %s: %s." % 172 (str(conn.dest_port), self.__class__, str(self.dest_ports))) 173 174 for existing_connection in self.in_connections: 175 if existing_connection.name == conn.name: 176 raise ValueError('A connection into an EventProcessor must have a unique name; "%s" into %s already exists'%(conn.name,self.name)) 177 178 self.in_connections.append(conn)
179 180
181 - def start(self):
182 """ 183 Called by the simulation when the EventProcessor is added to 184 the simulation. 185 186 If an EventProcessor needs to have any code run when it is 187 added to the simulation, the code can be put into this method 188 in the subclass. 189 """ 190 pass
191 192 ### JABALERT: Should change send_output to accept a list of src_ports, not a single src_port.
193 - def send_output(self,src_port=None,data=None):
194 """ 195 Send some data out to all connections on the given src_port. 196 The data is deepcopied before it is sent out, to ensure that 197 future changes to the data are not reflected in events from 198 the past. 199 """ 200 201 out_conns_on_src_port = [conn for conn in self.out_connections 202 if self._port_match(conn.src_port,[src_port])] 203 204 data=deepcopy(data) 205 for conn in out_conns_on_src_port: 206 #self.verbose("Sending output on src_port %s via connection %s to %s" % (str(src_port), conn.name, conn.dest.name)) 207 e=EPConnectionEvent(conn.delay+self.simulation.time(),conn,data,deep_copy=False) 208 self.simulation.enqueue_event(e)
209 210
211 - def input_event(self,conn,data):
212 """ 213 Called by the simulation when an EPConnectionEvent is delivered; 214 the EventProcessor should process the data somehow. 215 """ 216 raise NotImplementedError
217 218
219 - def process_current_time(self):
220 """ 221 Called by the simulation before advancing the simulation 222 time. Allows the event processor to do any computation that 223 requires that all events for this time have been delivered. 224 Computations performed in this method should not generate any 225 events with a zero time delay, or else causality could be 226 violated. (By default, does nothing.) 227 """ 228 pass
229
230 - def script_repr(self,imports=[],prefix=" "):
231 """Generate a runnable command for creating this EventProcessor.""" 232 return simulation_path+"['"+self.name+"']="+\ 233 super(EventProcessor,self).script_repr(imports=imports,prefix=prefix)
234
235 236 237 238 -class EventProcessorParameter(param.Parameter):
239 """Parameter whose value can be any EventProcessor instance.""" 240
241 - def __init__(self,default=EventProcessor(),**params):
243
244 - def __set__(self,obj,val):
245 if not isinstance(val,EventProcessor): 246 raise ValueError("Parameter must be an EventProcessor.") 247 else: 248 super(EventProcessorParameter,self).__set__(obj,val)
249 250 251 from param import parameterized
252 253 -class EPConnection(param.Parameterized):
254 """ 255 EPConnection stores basic information for a connection between 256 two EventProcessors. 257 """ 258 259 ## JPALERT: This type-checking is redundant, since 260 ## Simulation.connect() only allows the user to create connections 261 ## between existing simulation objects, which must be EPs. Type 262 ## checking here means that it is impossible to ever instantiate 263 ## an EPConnection in any situation (including debugging) w/o 264 ## making src and dest be EPs. However, there is nothing I can 265 ## find that requires that the src or dest be EPs. While some 266 ## *subclasses* of EPConnection (such as Projection) do require 267 ## that their src and dest support the interfaces of some 268 ## *subclasses* of EventProcessor (e.g. Sheet.activity), there is 269 ## no reason that those objects have to be EPs, per se. IMO, 270 ## excessive type checking removes much of the power of using a 271 ## dynamic language like Python. 272 273 ## src = EventProcessorParameter(default=None,constant=True,precedence=0.10,doc= 274 ## """The EventProcessor from which messages originate.""") 275 276 ## dest = EventProcessorParameter(default=None,constant=True,precedence=0.11,doc= 277 ## """The EventProcessor to which messages are delivered.""") 278 279 src = param.Parameter(default=None,constant=True,precedence=0.10,doc= 280 """The EventProcessor from which messages originate.""") 281 282 dest = param.Parameter(default=None,constant=True,precedence=0.11,doc= 283 """The EventProcessor to which messages are delivered.""") 284 285 src_port = param.Parameter(default=None,precedence=0.20,doc= 286 """ 287 Identifier that can be used to distinguish different types of outgoing connections. 288 289 EventProcessors that generate only a single type of 290 outgoing event will typically use a src_port of None. However, 291 if multiple types of communication are meaningful, the 292 EventProcessor can accept other values for src_port. It is up 293 to the src EventProcessor to deliver appropriate data to each 294 port, and to declare what will be sent over that port. 295 """) 296 297 dest_port = param.Parameter(default=None,precedence=0.21,doc= 298 """ 299 Identifier that can be used to distinguish different types of incoming connections. 300 301 EventProcessors that accept only a single type of incoming 302 event will typically use a src_port of None. However, if 303 multiple types of communication are meaningful, the 304 EventProcessor can accept other values for dest_port. It is up 305 to the dest EventProcessor to process the data appropriately 306 for each port, and to define what is expected to be sent to 307 that port. 308 """) 309 310 delay = param.Number(default=0,doc= 311 """Simulation time between when each Event is generated by the src and when it is delivered to the dest.""") 312 313 private = param.Boolean(default=False,doc= 314 """Set to true if this connection is for internal use only, not to be manipulated by a user.""") 315 316 317 # CEBALERT: should be reimplemented. It's difficult to understand, 318 # and contains the same code twice. But it does work.
319 - def remove(self):
320 """ 321 Remove this connection from its src's list of out_connections and its 322 dest's list of in_connections. 323 """ 324 # remove from EPs that have this as in_connection 325 i = 0 326 to_del = [] 327 for in_conn in self.dest.in_connections: 328 if in_conn is self: 329 to_del.append(i) 330 i+=1 331 332 for i in to_del: 333 del self.dest.in_connections[i] 334 335 # remove from EPs that have this as out_connection 336 i = 0 337 to_del = [] 338 for out_conn in self.src.out_connections: 339 if out_conn is self: 340 to_del.append(i) 341 i+=1 342 343 for i in to_del: 344 del self.src.out_connections[i]
345 346
347 - def script_repr(self,imports=[],prefix=" "):
348 """Generate a runnable command for creating this connection.""" 349 350 if self.private: 351 return "" 352 353 settings=[] 354 for name,val in self.get_param_values(): 355 try: # There may be a better way to figure out which parameters specify classes 356 if issubclass(val,object): 357 rep=val.__name__ 358 # Generate import statement 359 cls = val.__name__ 360 mod = val.__module__ 361 imports.append("from %s import %s" % (mod,cls)) 362 except TypeError: 363 if name=="src" or name=="dest": 364 rep=None 365 else: 366 rep = parameterized.script_repr(val,imports,prefix,settings) 367 368 if rep is not None: 369 settings.append('%s=%s' % (name,rep)) 370 371 # add import statement 372 cls = self.__class__.__name__ 373 mod = self.__module__ 374 imports.append("from %s import %s" % (mod,cls)) 375 376 return simulation_path+".connect('"+self.src.name+"','"+self.dest.name+ \ 377 "',connection_type="+self.__class__.__name__+ \ 378 ",\n"+prefix+(",\n"+prefix).join(settings) + ")"
379
380 381 # CB: event is not a Parameterized because of a (small) performance hit. 382 -class Event(object):
383 """Hierarchy of classes for storing simulation events of various types.""" 384
385 - def __init__(self,time):
386 self.time = time
387
388 - def __call__(self,sim):
389 """ 390 Cause some computation to be performed, deliver a message, etc., 391 as appropriate for each subtype of Event. Should be passed the 392 simulation object, to allow access to .time() etc. 393 """ 394 raise NotImplementedError
395
396 - def __cmp__(self,ev):
397 """ 398 Implements event comparison by time, allowing sorting, 399 and queue maintenance using bisect module or minheap 400 implementations, if needed. 401 402 NOTE: identity comparisons should always be done using the 403 'is' operator, not '=='. 404 """ 405 if self.time > ev.time: 406 return 1 407 elif self.time < ev.time: 408 return -1 409 else: 410 return 0
411
412 413 -class EPConnectionEvent(Event):
414 """ 415 An Event for delivery to an EPConnection. 416 417 Provides access to a data field, which can be used for anything 418 the src wants to provide, and a link to the connection over which 419 it has arrived, so that the dest can determine what to do with the 420 data. 421 422 By default, the data is deepcopied before being added to this 423 instance for safety (e.g. so that future changes to data 424 structures do not affect messages arriving from the past). 425 However, if you can ensure that the copying is not 426 necessary (e.g. if you deepcoy before sending a set of 427 identical messages), then you can pass deep_copy=False 428 to avoid the copy. 429 """ 430
431 - def __init__(self,time,conn,data=None,deep_copy=True):
432 super(EPConnectionEvent,self).__init__(time) 433 assert isinstance(conn,EPConnection) 434 self.data = deepcopy(data) if deep_copy else data 435 self.conn = conn
436
437 - def __call__(self,sim):
438 self.conn.dest.input_event(self.conn,self.data)
439
440 - def __repr__(self):
441 return "EPConnectionEvent(time="+`self.time`+",conn="+`self.conn`+")"
442
443 444 -class CommandEvent(Event):
445 """An Event consisting of a command string to execute.""" 446
447 - def __init__(self,time,command_string):
448 """ 449 Add the event to the simulation. 450 451 Raises an exception if the command_string contains a syntax 452 error. 453 """ 454 self.command_string = command_string 455 self.__test() 456 super(CommandEvent,self).__init__(time)
457
458 - def __repr__(self):
459 return "CommandEvent(time="+`self.time`+", command_string='"+self.command_string+"')"
460 461
462 - def script_repr(self,imports=[],prefix=" "):
463 """Generate a runnable command for creating this CommandEvent.""" 464 return simulation_path+'.schedule_command('\ 465 +`self.time`+',"'+self.command_string+'")'
466 467
468 - def __call__(self,sim):
469 """ 470 exec's the command_string in __main__.__dict__. 471 472 Be sure that any required items will be present in 473 __main__.__dict__; in particular, consider what will be present 474 after the network is saved and restored. For instance, results of 475 scripts you have run, or imports they make---all currently 476 available in __main__.__dict__---will not be saved with the 477 network. 478 """ 479 # Presumably here to avoid importing __main__ into the rest of the file 480 import __main__ 481 482 param.Parameterized(name='CommandEvent').message("Running command %s" \ 483 % (self.command_string)) 484 try: 485 exec self.command_string in __main__.__dict__ 486 except: 487 print "Error in scheduled command:" 488 raise
489
490 - def __test(self):
491 """ 492 Check for SyntaxErrors in the command. 493 """ 494 try: 495 compile(self.command_string,"CommandString","single") 496 except SyntaxError: 497 print "Error in scheduled command:" 498 raise
499
500 501 -class FunctionEvent(Event):
502 """ 503 Event that executes a given function function(*args,**kw). 504 """
505 - def __init__(self,time,fn,*args,**kw):
506 super(FunctionEvent,self).__init__(time) 507 self.fn = fn 508 self.args = args 509 self.kw = kw
510
511 - def __call__(self,sim):
512 self.fn(*self.args,**self.kw)
513
514 - def __repr__(self):
515 return 'FunctionEvent(%s,%s,*%s,**%s)' % (`self.time`,`self.fn`,`self.args`,`self.kw`)
516
517 -class EventSequence(Event):
518 """ 519 Event that contains a sequence of other events to be scheduled and 520 executed. 521 522 The .time attributes of the events in the sequence are interpreted 523 as offsets relative to the start time of the sequence itself. 524 """
525 - def __init__(self,time,sequence):
526 super(EventSequence,self).__init__(time) 527 self.sequence = sequence
528
529 - def __call__(self,sim):
530 531 # Enqueue all the events in the sequence, offsetting their 532 # times from the current time 533 sched_time = sim.time() 534 for ev in self.sequence: 535 new_ev = copy(ev) 536 sched_time += ev.time 537 new_ev.time = sched_time 538 sim.enqueue_event(new_ev)
539
540 - def __repr__(self):
541 return 'EventSequence(%s,%s)' % (`self.time`,`self.sequence`)
542
543 544 545 -class PeriodicEventSequence(EventSequence):
546 """ 547 An EventSequence that reschedules itself periodically 548 549 Takes a period argument that determines how often the sequence 550 will be scheduled. If the length of the sequence is longer than 551 the period, then the length of the sequence will be used as the period. 552 """ 553 ## JPHACKALERT: This should really be refactored into a 554 ## PeriodicEvent class that periodically executes a single event, 555 ## then the user can construct a periodic sequence using a 556 ## combination of PeriodicEvent and EventSequence. This would 557 ## change the behavior if the sequence length is longer than the 558 ## period, but I'm not sure how important that is, and it might 559 ## actually be useful the other way. 560 561
562 - def __init__(self,time,period,sequence):
563 super(PeriodicEventSequence,self).__init__(time,sequence) 564 self.period = period
565
566 - def __call__(self,sim):
567 super(PeriodicEventSequence,self).__call__(sim) 568 569 # Find the timed length of the sequence 570 seq_length = sum(e.time for e in self.sequence) 571 572 if seq_length < self.period: 573 # If the sequence is shorter than the period, then reschedule 574 # the sequence to occur again after the period 575 self.time += self.period 576 else: 577 # If the sequence is longer than the period, then 578 # reschedule to start after the sequence ends. 579 self.time += seq_length 580 sim.enqueue_event(self)
581
582 - def __repr__(self):
583 return 'PeriodicEventSequence(%s,%s,%s)' % (`self.time`,`self.period`,`self.sequence`)
584 585 586 # CB: code that previously existed in various places now collected 587 # together. The original timing code was not properly tested, and the 588 # current code has not been tested either: needs writing cleanly and 589 # testing. This whole class is pretty difficult to follow. 590 # 591 ### JP: Is it possible that some or all of this can be more cleanly 592 ### implemented using PeriodicEvents? 593 594 import time 595 from math import floor
596 -class SomeTimer(param.Parameterized)