1
2
3 """SimulationRT 2.0 Provides synchronization of real time and SimPy simulation time.
4 Implements SimPy Processes, resources, and the backbone simulation scheduling
5 by coroutine calls.
6 Based on generators (Python 2.3 and later; not 3.0)
7
8 LICENSE:
9 Copyright (C) 2002, 2005, 2006, 2007, 2008 Klaus G. Muller, Tony Vignaux
10 mailto: kgmuller@xs4all.nl and Tony.Vignaux@vuw.ac.nz
11
12 This library is free software; you can redistribute it and / or
13 modify it under the terms of the GNU Lesser General Public
14 License as published by the Free Software Foundation; either
15 version 2.1 of the License, or (at your option) any later version.
16
17 This library is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 Lesser General Public License for more details.
21
22 You should have received a copy of the GNU Lesser General Public
23 License along with this library; if not, write to the Free Software
24 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 - 1307 USA
25 END OF LICENSE
26
27
28 **Change history:**
29 4 / 8/2003: - Experimental introduction of synchronization of simulation
30 time and real time (idea of Geoff Jarrad of CSIRO -- thanks,
31 Geoff!).
32 * Changes made to class EvlistRT, _nextev(), simulate()
33
34 Dec 11, 2003:
35 - Updated to Simulation 1.4alpha API
36
37 13 Dec 2003: Merged in Monitor and Histogram
38
39 27 Feb 2004: Repaired bug in activeQ monitor of class Resource. Now actMon
40 correctly records departures from activeQ.
41
42 19 May 2004: Added erroneously omitted Histogram class.
43
44 5 Sep 2004: Added SimEvents synchronization constructs
45
46 17 Sep 2004: Added waituntil synchronization construct
47
48 28 Sep 2004: Changed handling of real time -- now uses time.clock for Win32, and
49 time.time for all other OS (works better on Linux, Unix).
50
51 01 Dec 2004: SimPy version 1.5
52 Changes in this module: Repaired SimEvents bug re proc.eventsFired
53
54 12 Jan 2005: SimPy version 1.5.1
55 Changes in this module: Monitor objects now have a default name
56 'a_Monitor'
57
58 29 Mar 2005: Start SimPy 1.6: compound 'yield request' statements
59
60 05 Jun 2005: Fixed bug in _request method -- waitMon did not work properly in
61 preemption case
62
63 09 Jun 2005: Added test in 'activate' to see whether 'initialize()' was called first.
64
65 23 Aug 2005: - Added Tally data collection class
66 - Adjusted Resource to work with Tally
67 - Redid function allEventNotices() (returns prettyprinted string with event
68 times and names of process instances
69 - Added function allEventTimes (returns event times of all scheduled events)
70
71 16 Mar 2006: - Added Store and Level classes
72 - Added 'yield get' and 'yield put'
73
74 10 May 2006: - Repaired bug in Store._get method
75 - Repaired Level to allow initialBuffered have float value
76 - Added type test for Level get parameter 'nrToGet'
77
78 06 Jun 2006: - To improve pretty - printed output of 'Level' objects, changed attribute
79 _nrBuffered to nrBuffered (synonym for amount property)
80 - To improve pretty - printed output of 'Store' objects, added attribute
81 buffered (which refers to _theBuffer)
82
83 25 Aug 2006: - Start of version 1.8
84 - made 'version' public
85 - corrected condQ initialization bug
86
87 30 Sep 2006: - Introduced checks to ensure capacity of a Buffer > 0
88 - Removed from __future__ import (so Python 2.3 or later needed)
89
90 15 Oct 2006: - Added code to register all Monitors and all Tallies in variables
91 'allMonitors' and 'allTallies'
92 - Added function 'startCollection' to activate Monitors and Tallies at a
93 specified time (e.g. after warmup period)
94 - Moved all test / demo programs to after 'if __name__ == '__main__':'.
95
96 17 Oct 2006: - Added compound 'put' and 'get' statements for Level and Store.
97
98 18 Oct 2006: - Repaired bug: self.eventsFired now gets set after an event fires
99 in a compound yield get / put with a waitevent clause (reneging case).
100
101 21 Oct 2006: - Introduced Store 'yield get' with a filter function.
102
103 22 Oct 2006: - Repaired bug in prettyprinting of Store objects (the buffer
104 content==._theBuffer was not shown) by changing ._theBuffer
105 to .theBuffer.
106
107 04 Dec 2006: - Added printHistogram method to Tally and Monitor (generates
108 table - form histogram)
109
110 07 Dec 2006: - Changed the __str__ method of Histogram to print a table
111 (like printHistogram).
112
113 18 Dec 2006: - Added trace printing of Buffers' 'unitName' for yield get and put.
114
115 09 Jun 2007: - Cleaned out all uses of 'object' to prevent name clash.
116
117 18 Nov 2007: - Start of 1.9 development
118 - Added 'start' method (alternative to activate) to Process
119
120 22 Nov 2007: - Major change to event list handling to speed up larger models:
121 * Drop dictionary
122 * Replace bisect by heapq
123 * Mark cancelled event notices in unpost and skip them in
124 nextev (great idea of Tony Vignaux))
125
126 4 Dec 2007: - Added twVariance calculation for both Monitor and Tally (gav)
127
128 5 Dec 2007: - Changed name back to timeVariance (gav)
129
130 1 Mar 2008: - Start of 1.9.1 bugfix release
131 - Delete circular reference in Process instances when event
132 notice has been processed (caused much circular garbage)
133
134 2 Apr 2008: - Repair of wallclock synchronisation algorithm
135
136 10 Aug 2008: - Renamed __Evlist to EvlistRT and let it inherit from
137 Evlist (from Simulation.py) (Stefan Scherfke)
138 - New class SimulationRT contains the old method simulate
139 - Removed everything else and imported it from Simulation.py
140
141 19 Mar 2009: - Repair of wallclock synchronisation algorithm (again)
142
143 """
144 import time
145
146 from SimPy.Simulation import *
147
148
149 __TESTING = False
150 version = __version__ = '2.0 $Revision: 271 $ $Date: 2009-03-25 13:27:06 +0100 (Mi, 25 Mrz 2009) $'
151 if __TESTING:
152 print 'SimPy.SimulationRT %s' %__version__,
153 if __debug__:
154 print '__debug__ on'
155 else:
156 print
157
158
160 """Defines event list and operations on it"""
162
163
164 self.sim = sim
165 self.timestamps = []
166 self.sortpr = 0
167 self.real_time = False
168 self.rel_speed = 1
169 self.rtlast = self.sim.wallclock()
170 self.stlast = 0
171
173 """Retrieve next event from event list"""
174 noActiveNotice = True
175
176 while noActiveNotice:
177 if self.timestamps:
178
179 (_tnotice, p,nextEvent, cancelled) = hq.heappop(self.timestamps)
180 noActiveNotice = cancelled
181 else:
182 raise Simerror('No more events at time %s' % self.sim._t)
183 nextEvent._rec = None
184 _tsimold = self.sim._t
185 self.sim._t = _tnotice
186
187
188 if self.real_time:
189 delay = float(_tnotice-_tsimold)/self.rel_speed-self.sim.rtnow()+self.rtlast
190 if delay > 0:
191 time.sleep(delay)
192 self.rtlast = self.sim.wallclock()-self.sim.rtstart
193 self.stlast = self.sim._t
194 if self.sim._t > self.sim._endtime:
195 self.sim._t = self.sim._endtime
196 self.sim._stop = True
197 return (None,)
198 try:
199 resultTuple = nextEvent._nextpoint.next()
200 except StopIteration:
201 nextEvent._nextpoint = None
202 nextEvent._terminated = True
203 nextEvent._nextTime = None
204 resultTuple = None
205 return (resultTuple, nextEvent)
206
207
209
211 if sys.platform == 'win32':
212 self.wallclock = time.clock
213 else:
214 self.wallclock = time.time
215 self.rtstart = self.wallclock()
216 Simulation.__init__(self)
217 self.initialize()
218
224
226 return self.wallclock() - self.rtstart
227
228 - def rtset(self, rel_speed = 1):
229 """resets the the ratio simulation time over clock time(seconds).
230 """
231 if self._e is None:
232 raise FatalSimerror('Fatal SimPy error: Simulation not initialized')
233 self._e.rel_speed = float(rel_speed)
234
235 - def simulate(self, until = 0, real_time = False, rel_speed = 1):
236 """Schedules Processes / semi - coroutines until time 'until'"""
237
238 """Gets called once. Afterwards, co - routines (generators) return by
239 'yield' with a cargo:
240 yield hold, self, <delay>: schedules the 'self' process for activation
241 after < delay > time units.If <,delay > missing,
242 same as 'yield hold, self, 0'
243
244 yield passivate, self : makes the 'self' process wait to be re - activated
245
246 yield request, self,<Resource > [,<priority>]: request 1 unit from < Resource>
247 with < priority > pos integer (default = 0)
248
249 yield release, self,<Resource> : release 1 unit to < Resource>
250
251 yield waitevent, self,<SimEvent>|[<Evt1>,<Evt2>,<Evt3), . . . ]:
252 wait for one or more of several events
253
254
255 yield queueevent, self,<SimEvent>|[<Evt1>,<Evt2>,<Evt3), . . . ]:
256 queue for one or more of several events
257
258 yield waituntil, self, cond : wait for arbitrary condition
259
260 yield get, self,<buffer > [,<WhatToGet > [,<priority>]]
261 get < WhatToGet > items from buffer (default = 1);
262 <WhatToGet > can be a pos integer or a filter function
263 (Store only)
264
265 yield put, self,<buffer > [,<WhatToPut > [,priority]]
266 put < WhatToPut > items into buffer (default = 1);
267 <WhatToPut > can be a pos integer (Level) or a list of objects
268 (Store)
269
270 EXTENSIONS:
271 Request with timeout reneging:
272 yield (request, self,<Resource>),(hold, self,<patience>) :
273 requests 1 unit from < Resource>. If unit not acquired in time period
274 <patience>, self leaves waitQ (reneges).
275
276 Request with event - based reneging:
277 yield (request, self,<Resource>),(waitevent, self,<eventlist>):
278 requests 1 unit from < Resource>. If one of the events in < eventlist > occurs before unit
279 acquired, self leaves waitQ (reneges).
280
281 Get with timeout reneging (for Store and Level):
282 yield (get, self,<buffer>,nrToGet etc.),(hold, self,<patience>)
283 requests < nrToGet > items / units from < buffer>. If not acquired < nrToGet > in time period
284 <patience>, self leaves < buffer>.getQ (reneges).
285
286 Get with event - based reneging (for Store and Level):
287 yield (get, self,<buffer>,nrToGet etc.),(waitevent, self,<eventlist>)
288 requests < nrToGet > items / units from < buffer>. If not acquired < nrToGet > before one of
289 the events in < eventlist > occurs, self leaves < buffer>.getQ (reneges).
290
291
292
293 Event notices get posted in event - list by scheduler after 'yield' or by
294 'activate' / 'reactivate' functions.
295
296 if real_time == True, the simulation time and real (clock) time get
297 synchronized as much as possible. rel_speed is the ratio simulation time
298 over clock time(seconds). Example: rel_speed == 100: 100 simulation time units take
299 1 second clock time.
300
301 """
302 global _endtime, _e, _stop, _t, _wustep
303 self._stop = False
304
305 if self._e is None:
306 raise FatalSimerror('Simulation not initialized')
307 self._e.real_time = real_time
308 self._e.rel_speed = rel_speed
309
310
311 if self._e._isEmpty():
312 message = 'SimPy: No activities scheduled'
313 return message
314
315 self._endtime = until
316 message = 'SimPy: Normal exit'
317 dispatch={hold:holdfunc, request:requestfunc, release:releasefunc,
318 passivate:passivatefunc, waitevent:waitevfunc, queueevent:queueevfunc,
319 waituntil:waituntilfunc, get:getfunc, put:putfunc}
320 commandcodes = dispatch.keys()
321 commandwords={hold:'hold', request:'request', release:'release', passivate:'passivate',
322 waitevent:'waitevent', queueevent:'queueevent', waituntil:'waituntil',
323 get:'get', put:'put'}
324 nextev = self._e._nextev
325 while not self._stop and self._t <= self._endtime:
326 try:
327 a = nextev()
328 if not a[0] is None:
329
330 if type(a[0][0]) == tuple:
331
332 command = a[0][0][0]
333 else:
334 command = a[0][0]
335 if __debug__:
336 if not command in commandcodes:
337 raise FatalSimerror('Illegal command: yield %s'%command)
338 dispatch[command](a)
339 except FatalSimerror, error:
340 print 'SimPy: ' + error.value
341 sys.exit(1)
342 except Simerror, error:
343 message = 'SimPy: ' + error.value
344 self._stop = True
345 if self._wustep:
346 self._test()
347 self._stopWUStepping()
348 self._e = None
349 return message
350
351
352 Globals.sim = SimulationRT()
353
356
357 rtset = Globals.sim.rtset
358
359 -def simulate(until = 0, real_time = False, rel_speed = 1):
360 return Globals.sim.simulate(until = until, real_time = real_time, rel_speed = rel_speed)
361
362 wallclock = Globals.sim.wallclock
363
364
365 if __name__ == '__main__':
366 print 'SimPy.SimulationRT %s' %__version__
367
369 class Aa(Process):
370 sequIn = []
371 sequOut = []
372 def __init__(self, holdtime, name):
373 Process.__init__(self, name)
374 self.holdtime = holdtime
375
376 def life(self, priority):
377 for i in range(1):
378 Aa.sequIn.append(self.name)
379 print now(),rrr.name, 'waitQ:', len(rrr.waitQ),'activeQ:',\
380 len(rrr.activeQ)
381 print 'waitQ: ',[(k.name, k._priority[rrr]) for k in rrr.waitQ]
382 print 'activeQ: ',[(k.name, k._priority[rrr]) \
383 for k in rrr.activeQ]
384 assert rrr.n + len(rrr.activeQ) == rrr.capacity, \
385 'Inconsistent resource unit numbers'
386 print now(),self.name, 'requests 1 ', rrr.unitName
387 yield request, self, rrr, priority
388 print now(),self.name, 'has 1 ', rrr.unitName
389 print now(),rrr.name, 'waitQ:', len(rrr.waitQ),'activeQ:',\
390 len(rrr.activeQ)
391 print now(),rrr.name, 'waitQ:', len(rrr.waitQ),'activeQ:',\
392 len(rrr.activeQ)
393 assert rrr.n + len(rrr.activeQ) == rrr.capacity, \
394 'Inconsistent resource unit numbers'
395 yield hold, self, self.holdtime
396 print now(),self.name, 'gives up 1', rrr.unitName
397 yield release, self, rrr
398 Aa.sequOut.append(self.name)
399 print now(),self.name, 'has released 1 ', rrr.unitName
400 print 'waitQ: ',[(k.name, k._priority[rrr]) for k in rrr.waitQ]
401 print now(),rrr.name, 'waitQ:', len(rrr.waitQ),'activeQ:',\
402 len(rrr.activeQ)
403 assert rrr.n + len(rrr.activeQ) == rrr.capacity, \
404 'Inconsistent resource unit numbers'
405
406 class Observer(Process):
407 def __init__(self):
408 Process.__init__(self)
409
410 def observe(self, step, processes, res):
411 while now() < 11:
412 for i in processes:
413 print ' %s %s: act:%s, pass:%s, term: %s, interr:%s, qu:%s'\
414 %(now(),i.name, i.active(),i.passive(),i.terminated()\
415 ,i.interrupted(),i.queuing(res))
416 print
417 yield hold, self, step
418
419 print'\n+++test_demo output'
420 print '****First case == priority queue, resource service not preemptable'
421 initialize()
422 rrr = Resource(5, name = 'Parking', unitName = 'space(s)', qType = PriorityQ,
423 preemptable = 0)
424 procs = []
425 for i in range(10):
426 z = Aa(holdtime = i, name = 'Car ' + str(i))
427 procs.append(z)
428 activate(z, z.life(priority = i))
429 o = Observer()
430 activate(o, o.observe(1, procs, rrr))
431 a = simulate(until = 10000, real_time = True, rel_speed = 1)
432 print a
433 print 'Input sequence: ', Aa.sequIn
434 print 'Output sequence: ', Aa.sequOut
435
436 print '\n****Second case == priority queue, resource service preemptable'
437 initialize()
438 rrr = Resource(5, name = 'Parking', unitName = 'space(s)', qType = PriorityQ,
439 preemptable = 1)
440 procs = []
441 for i in range(10):
442 z = Aa(holdtime = i, name = 'Car ' + str(i))
443 procs.append(z)
444 activate(z, z.life(priority = i))
445 o = Observer()
446 activate(o, o.observe(1, procs, rrr))
447 Aa.sequIn = []
448 Aa.sequOut = []
449 a = simulate(until = 10000)
450 print a
451 print 'Input sequence: ', Aa.sequIn
452 print 'Output sequence: ', Aa.sequOut
453
455 class Bus(Process):
456 def __init__(self, name):
457 Process.__init__(self, name)
458
459 def operate(self, repairduration = 0):
460 print now(),rtnow(),'>> %s starts' % (self.name)
461 tripleft = 1000
462 while tripleft > 0:
463 yield hold, self, tripleft
464 if self.interrupted():
465 print 'interrupted by %s' %self.interruptCause.name
466 print '%s(%s): %s breaks down ' %(now(),rtnow(),self.name)
467 tripleft = self.interruptLeft
468 self.interruptReset()
469 print 'tripleft ', tripleft
470 reactivate(br, delay = repairduration)
471 yield hold, self, repairduration
472 print now(),rtnow(),' repaired'
473 else:
474 break
475 print now(),'<< %s done' % (self.name)
476
477 class Breakdown(Process):
478 def __init__(self, myBus):
479 Process.__init__(self, name = 'Breakdown ' + myBus.name)
480 self.bus = myBus
481
482 def breakBus(self, interval):
483
484 while True:
485 yield hold, self, interval
486 if self.bus.terminated(): break
487 self.interrupt(self.bus)
488
489 print'\n\n+++test_interrupt'
490 initialize()
491 b = Bus('Bus 1')
492 activate(b, b.operate(repairduration = 20))
493 br = Breakdown(b)
494 activate(br, br.breakBus(200))
495 print simulate(until = 4000, real_time = True, rel_speed = 200)
496
498 class Waiter(Process):
499 def waiting(self, theSignal):
500 while True:
501 yield waitevent, self, theSignal
502 print '%s: process \'%s\' continued after waiting for %s' % (now(),self.name, theSignal.name)
503 yield queueevent, self, theSignal
504 print '%s: process \'%s\' continued after queueing for %s' % (now(),self.name, theSignal.name)
505
506 class ORWaiter(Process):
507 def waiting(self, signals):
508 while True:
509 yield waitevent, self, signals
510 print now(),'one of %s signals occurred' % [x.name for x in signals]
511 print '\t%s (fired / param)'%[(x.name, x.signalparam) for x in self.eventsFired]
512 yield hold, self, 1
513
514 class Caller(Process):
515 def calling(self):
516 while True:
517 signal1.signal('wake up!')
518 print '%s: signal 1 has occurred'%now()
519 yield hold, self, 10
520 signal2.signal('and again')
521 signal2.signal('sig 2 again')
522 print '%s: signal1, signal2 have occurred'%now()
523 yield hold, self, 10
524 print'\n\n+++testSimEvents output'
525 initialize()
526 signal1 = SimEvent('signal 1')
527 signal2 = SimEvent('signal 2')
528 signal1.signal('startup1')
529 signal2.signal('startup2')
530 w1 = Waiter('waiting for signal 1')
531 activate(w1, w1.waiting(signal1))
532 w2 = Waiter('waiting for signal 2')
533 activate(w2, w2.waiting(signal2))
534 w3 = Waiter('also waiting for signal 2')
535 activate(w3, w3.waiting(signal2))
536 w4 = ORWaiter('waiting for either signal 1 or signal 2')
537 activate(w4, w4.waiting([signal1, signal2]),prior = True)
538 c = Caller('Caller')
539 activate(c, c.calling())
540 print simulate(until = 100)
541
543 """
544 Demo of waitUntil capability.
545
546 Scenario:
547 Three workers require sets of tools to do their jobs. Tools are shared, scarce
548 resources for which they compete.
549 """
550
551
552 class Worker(Process):
553 def __init__(self, name, heNeeds = []):
554 Process.__init__(self, name)
555 self.heNeeds = heNeeds
556 def work(self):
557
558 def workerNeeds():
559 for item in self.heNeeds:
560 if item.n == 0:
561 return False
562 return True
563
564 while now() < 8 * 60:
565 yield waituntil, self, workerNeeds
566 for item in self.heNeeds:
567 yield request, self, item
568 print '%s %s has %s and starts job' % (now(),self.name,
569 [x.name for x in self.heNeeds])
570 yield hold, self, random.uniform(10, 30)
571 for item in self.heNeeds:
572 yield release, self, item
573 yield hold, self, 2
574
575 print '\n+++\nwaituntil demo output'
576 initialize()
577 brush = Resource(capacity = 1, name = 'brush')
578 ladder = Resource(capacity = 2, name = 'ladder')
579 hammer = Resource(capacity = 1, name = 'hammer')
580 saw = Resource(capacity = 1, name = 'saw')
581 painter = Worker('painter',[brush, ladder])
582 activate(painter, painter.work())
583 roofer = Worker('roofer',[hammer, ladder, ladder])
584 activate(roofer, roofer.work())
585 treeguy = Worker('treeguy',[saw, ladder])
586 activate(treeguy, treeguy.work())
587 for who in (painter, roofer, treeguy):
588 print '%s needs %s for his job' % (who.name,[x.name for x in who.heNeeds])
589 print
590 print simulate(until = 9 * 60)
591 test_demo()
592
593 test_interrupt()
594
595
596