Documentation for SimPy¶
Contents:
SimPy home¶
SimPy in 10 Minutes¶
In this section, you’ll learn the basics of SimPy in just a few minutes. Afterwards, you will be able to implement a simple simulation using SimPy and you’ll be able to make an educated decision if SimPy is what you need. We’ll also give you some hints on how to proceed to implement more complex simulations.
Installation¶
SimPy is implemented in pure Python and has no dependencies. SimPy runs on Python 2 (>= 2.7) and Python 3 (>= 3.2). PyPy is also supported. If you have pip installed, just type
$ pip install simpy
and you are done.
Alternatively, you can download SimPy and install it manually. Extract the archive, open a terminal window where you extracted SimPy and type:
$ python setup.py install
You can now optionally run SimPy’s tests to see if everything works fine. You need pytest and mock for this:
$ python -c "import simpy; simpy.test()"
Upgrading from SimPy 2¶
If you are already familiar with SimPy 2, please read the Guide Porting from SimPy 2 to 3.
What’s Next¶
Now that you’ve installed SimPy, you probably want to simulate something. The next section will introduce you to SimPy’s basic concepts.
Basic Concepts¶
SimPy is a discrete-event simulation library. The behavior of active components (like vehicles, customers or messages) is modeled by processes. All processes live in an environment. They interact with the environment and with each other via events.
Processes are described by simple Python generators. You can call them process function or process method, depending on whether it is a normal function or method of a class. During their life time, they create events and yield them in order to wait for them to be triggered.
When a process yields an event, the process gets suspended. SimPy resumes the process, when the event occurs (we say that the event is triggered). Multiple processes can wait for the same event. SimPy resumes them in the same order in which they yielded that event.
An important event type is the Timeout. Events of this type are triggered after a certain amount of (simulated) time has passed. They allow a process to sleep (or hold its state) for the given time. A Timeout and all other events can be created by calling the appropriate method of the Environment that the process lives in (Environment.timeout() for example).
Our First Process¶
Our first example will be a car process. The car will alternately drive and park for a while. When it starts driving (or parking), it will print the current simulation time.
So let’s start:
>>> def car(env):
... while True:
... print('Start parking at %d' % env.now)
... parking_duration = 5
... yield env.timeout(parking_duration)
...
... print('Start driving at %d' % env.now)
... trip_duration = 2
... yield env.timeout(trip_duration)
Our car process requires a reference to an Environment (env) in order to create new events. The car‘s behaviour is described in an infinite loop. Remember, this function is a generator. Though it will never terminate, it will pass the control flow back to the simulation once a yield statement is reached. Once the yielded event is triggered (“it occurs”), the simulation will resume the function at this statement.
As I said before, our car switches between the states parking and driving. It announces its new state by printing a message and the current simulation time (as returned by the Environment.now property). It then calls the Environment.timeout() factory function to create a Timeout event. This event describes the point in time the car is done parking (or driving, respectively). By yielding the event, it signals the simulation that it wants to wait for the event to occur.
Now that the behaviour of our car has been modelled, lets create an instance of it and see how it behaves:
>>> import simpy
>>> env = simpy.Environment()
>>> env.process(car(env))
<Process(car) object at 0x...>
>>> env.run(until=15)
Start parking at 0
Start driving at 5
Start parking at 7
Start driving at 12
Start parking at 14
The first thing we need to do is to create an instance of Environment. This instance is passed into our car process function. Calling it creates a process generator that needs to be started and added to the environment via Environment.process().
Note, that at this time, none of the code of our process function is being executed. It’s execution is merely scheduled at the current simulation time.
The Process returned by process() can be used for process interactions (we will cover that in the next section, so we will ignore it for now).
Finally, we start the simulation by calling run() and passing an end time to it.
What’s Next?¶
You should now be familiar with Simpy’s terminology and basic concepts. In the next section, we will cover process interaction.
Process Interaction¶
The Process instance that is returned by Environment.process() can be utilized for process interactions. The two most common examples for this are to wait for another process to finish and to interrupt another process while it is waiting for an event.
Waiting for a Process¶
As it happens, a SimPy Process can be used like an event (technically, a process actually is an event). If you yield it, you are resumed once the process has finished. Imagine a car-wash simulation where cars enter the car-wash and wait for the washing process to finish. Or an airport simulation where passengers have to wait until a security check finishes.
Lets assume that the car from our last example magically became an electric vehicle. Electric vehicles usually take a lot of time charing their batteries after a trip. They have to wait until their battery is charged before they can start driving again.
We can model this with an additional charge() process for our car. Therefore, we refactor our car to be a class with two process methods: run() (which is the original car() process function) and charge().
The run process is automatically started when Car is instantiated. A new charge process is started every time the vehicle starts parking. By yielding the Process instance that Environment.process() returns, the run process starts waiting for it to finish:
>>> class Car(object):
... def __init__(self, env):
... self.env = env
... # Start the run process everytime an instance is created.
... self.proc = env.process(self.run())
...
... def run(self):
... while True:
... print('Start parking and charging at %d' % env.now)
... charge_duration = 5
... # We yield the process that process() returns
... # to wait for it to finish
... yield env.process(self.charge(charge_duration))
...
... # The charge process has finished and
... # we can start driving again.
... print('Start driving at %d' % env.now)
... trip_duration = 2
... yield env.timeout(trip_duration)
...
... def charge(self, duration):
... yield self.env.timeout(duration)
Starting the simulation is straight forward again: We create an environment, one (or more) cars and finally call meth:~Environment.simulate().
>>> import simpy
>>> env = simpy.Environment()
>>> car = Car(env)
>>> env.run(until=15)
Start parking and charging at 0
Start driving at 5
Start parking and charging at 7
Start driving at 12
Start parking and charging at 14
Interrupting Another Process¶
Imagine, you don’t want to wait until your electric vehicle is fully charged but want to interrupt the charging process and just start driving instead.
SimPy allows you to interrupt a running process by calling its interrupt() method:
>>> def driver(env, car):
... yield env.timeout(3)
... car.action.interrupt()
The driver process has a reference to the car’s run process. After waiting for 3 time steps, it interrupts that process.
Interrupts are thrown into process functions as Interrupt exceptions that can (should) be handled by the interrupted process. The process can than decide what to do next (e.g., continuing to wait for the original event or yielding a new event):
>>> class Car(object):
... def __init__(self, env):
... self.env = env
... self.action = env.process(self.run())
...
... def run(self):
... while True:
... print('Start parking and charging at %d' % env.now)
... charge_duration = 5
... # We may get interrupted while charging the battery
... try:
... yield env.process(self.charge(charge_duration))
... except simpy.Interrupt:
... # When we received an interrupt, we stop charing and
... # switch to the "driving" state
... print('Was interrupted. Hope, the battery is full enough ...')
...
... print('Start driving at %d' % env.now)
... trip_duration = 2
... yield env.timeout(trip_duration)
...
... def charge(self, duration):
... yield self.env.timeout(duration)
When you compare the output of this simulation with the previous example, you’ll notice that the car now starts driving at time 3 instead of 5:
>>> env = simpy.Environment()
>>> car = Car(env)
>>> env.process(driver(env, car))
<Process(driver) object at 0x...>
>>> env.run(until=15)
Start parking and charging at 0
Was interrupted. Hope, the battery is full enough ...
Start driving at 3
Start parking and charging at 5
Start driving at 10
Start parking and charging at 12
What’s Next¶
We just demonstrated two basic methods for process interactions—waiting for a process and interrupting a process. Take a look at the Topical Guides or the Process API reference for more details.
In the next section we will cover the basic usage of shared resources.
How to Proceed¶
If you are not certain yet if SimPy fulfills your requirements or if you want to see more features in action, you should take a look at the various examples we provide.
If you are looking for a more detailed description of a certain aspect or feature of SimPy, the Topical Guides section might help you.
Finally, there is an API Reference that describes all functions and classes in full detail.
Topical Guides¶
This sections covers various aspects of SimPy more in-depth. It assumes that you have a basic understanding of SimPy’s capabilities and that you know what you are looking for.
SimPy basics¶
This guide describes the basic concepts of SimPy: How does it work? What are processes, events and the environment? What can I do with them?
How SimPy works¶
If you break SimPy down, it is just an asynchronous event dispatcher. You generate events and schedule them at a given simulation time. Events are sorted by priority, simulation time, and an increasing event id. An event also has a list of callbacks, which are executed when the event is triggered and processed by the event loop. Events may also have a return value.
The components involved in this are the Environment, events and the process functions that you write.
Process functions implement your simulation model, that is, they define the behavior of your simulation. They are plain Python generator functions that yield instances of Event.
The environment stores these events in its event list and keeps track of the current simulation time.
If a process function yields and event, SimPy adds the process to the event’s callbacks and suspends the process until the event is triggered and processed. When a process waiting for an event is resumed, it will also receive the event’s value.
Here is a very simple example that illustrates all this; the code is more verbose than it needs to be to make things extra clear. You find a compact version of it at the end of this section:
>>> import simpy
>>>
>>> def example(env):
... event = simpy.events.Timeout(env, delay=1, value=42)
... value = yield event
... print('now=%d, value=%d' % (env.now, value))
>>>
>>> env = simpy.Environment()
>>> example_gen = example(env)
>>> p = simpy.events.Process(env, example_gen)
>>>
>>> env.run()
now=1, value=42
The example() process function above first creates a Timeout event. It passes the environment, a delay, and a value to it. The Timeout schedules itself at now + delay (that’s why the environment is required); other event types usually schedule themselves at the current simulation time.
The process function then yields the event and thus gets suspended. It is resumed, when SimPy processes the Timeout event. The process function also receives the event’s value (42) – this is, however, optional, so yield event would have been okay if the you were not interested in the value or if the event had no value at all.
Finally, the process function prints the current simulation time (that is accessible via the environment’s now attribute) and the Timeout’s value.
If all required process functions are defined, you can instantiate all objects for your simulation. In most cases, you start by creating an instance of Environment, because you’ll need to pass it around a lot when creating everything else.
Starting a process function involves two things:
- You have to call the process function to create a generator object. (This will not execute any code of that function yet. Please read The Python yield keyword explained, to understand why this is the case.)
- You then create an instance of Process and pass the environment and the generator object to it. This will schedule an Initialize event at the current simulation time which starts the execution of the process function. The process instance is also an event that is triggered when the process function returns. The guide to events explains why this is handy.
Finally, you can start SimPy’s event loop. By default, it will run as long as there are events in the event list, but you can also let it stop earlier by providing an until argument (see Simulation control).
The following guides describe the environment and its interactions with events and process functions in more detail.
“Best practice” version of the example above¶
>>> import simpy
>>>
>>> def example(env):
... value = yield env.timeout(1, value=42)
... print('now=%d, value=%d' % (env.now, value))
>>>
>>> env = simpy.Environment()
>>> p = env.process(example(env))
>>> env.run()
now=1, value=42
Environments¶
A simulation environment manages the simulation time as well as the scheduling and processing of events. It also provides means to step through or execute the simulation.
The base class for all environments is BaseEnvironment. “Normal” simulations usually use its subclass Environment. For real-time simulations, SimPy provides a RealtimeEnvironment (more on that in realtime_simulations).
Simulation control¶
SimPy is very flexible in terms of simulation execution. You can run your simulation until there is no more event, until a certain simulation time is reached, or until a certain event is triggered. You can also step through the simulation event by event. Furthermore, you can mix these things as you like.
For example, you could run your simulation until an interesting event occurs. You could then step through the simulation event by event for a while; and finally run the simulation until there is no more event left and your processes all have terminated.
The most important method here is Environment.run():
If you call it without any argument (env.run()), it steps through the simulation until there is no more event left.
Warning
If your processes run forever (while True: yield env.timeout(1)), this method will never terminate (unless you kill your script by e.g., pressing Ctrl-C).
In most cases it is more advisable to stop your simulation when it reaches a certain simulation time. Therefore, you can pass the desired time via the until parameter, e.g.: env.run(until=10).
The simulation will then stop when the internal clock reaches 10 but will not process any events scheduled for time 10. This is similar to a new environment where the clock is 0 but (obviously) no events have yet been processed.
If you want to integrate your simulation in a GUI and want to draw a process bar, you can repeatedly call this function with increasing until values and update your progress bar after each call:
for i in range(100): env.run(until=i) progressbar.update(i)
Instead of passing a number to run(), you can also pass any event to it. run() will then return when the event has been processed.
Assuming that the current time is 0, env.run(until=env.timeout(5)) is equivalent to env.run(until=5).
You can also pass other types of events (remember, that a Process is an event, too):
>>> import simpy >>> >>> def my_proc(env): ... yield env.timeout(1) ... return 'Monty Python’s Flying Circus' >>> >>> env = simpy.Environment() >>> proc = env.process(my_proc(env)) >>> env.run(until=proc) 'Monty Python’s Flying Circus'
To step through the simulation event by event, the environment offers peek() and step().
peek() returns the time of the next scheduled event of infinity (float('inf')) of no more event is scheduled.
step() processes the next scheduled event. It raises an EmptySchedule exception if no event is available.
In a typical use case, you use these methods in a loop like:
until = 10
while env.peek() < until:
env.step()
State access¶
The environment allows you to get the current simulation time via the Environment.now property. The simulation time is a number without unit and is increased via Timeout events.
By default, now starts at 0, but you can pass an initial_time to the Environment to use something else.
Note
Although the simulation time is technically unitless, you can pretend that it is, for example, in seconds and use it like a timestamp returned by time.time() to calculate a date or the day of the week.
The property Environment.active_process is comparable to os.getpid() and is either None or pointing at the currently active Process. A process is active when its process function is being executed. It becomes inactive (or suspended) when it yields an event.
Thus, it makes only sense to access this property from within a process function or a function that is called by your process function:
>>> def subfunc(env):
... print(env.active_process) # will print "p1"
>>>
>>> def my_proc(env):
... while True:
... print(env.active_process) # will print "p1"
... subfunc(env)
... yield env.timeout(1)
>>>
>>> env = simpy.Environment()
>>> p1 = env.process(my_proc(env))
>>> env.active_process # None
>>> env.step()
<Process(my_proc) object at 0x...>
<Process(my_proc) object at 0x...>
>>> env.active_process # None
An exemplary use case for this is the resource system: If a process function calls request() to request a resource, the resource determines the requesting process via env.active_process. Take a look at the code to see how we do this :-).
Event creation¶
To create events, you normally have to import simpy.events, instantiate the event class and pass a reference to the environment to it. To reduce the amount of typing, the Environment provides some shortcuts for event creation. For example, Environment.event() is equivalent to simpy.events.Event(env).
Other shortcuts are:
More details on what the events do can be found in the guide to events.
Miscellaneous¶
Since Python 3.3, a generator function can have a return value:
def my_proc(env):
yield env.timeout(1)
return 42
In SimPy, this can be used to provide return values for processes that can be used by other processes:
def other_proc(env):
ret_val = yield env.process(my_proc(env))
assert ret_val == 42
Internally, Python passes the return value as parameter to the StopIteration exception that it raises when a generator is exhausted. So in Python 2.7 and 3.2 you could replace the return 42 with a raise StopIteration(42) to achieve the same result.
To keep your code more readable, the environment provides the method exit() to do exactly this:
def my_proc(env):
yield env.timeout(1)
env.exit(42) # Py2 equivalent to "return 42"
Porting from SimPy 2 to 3¶
Porting from SimPy 2 to SimPy 3 is not overly complicated. A lot of changes merely comprise copy/paste.
This guide describes the conceptual and API changes between both SimPy versions and shows you how to change your code for SimPy 3.
Imports¶
In SimPy 2, you had to decide at import-time whether you wanted to use a normal simulation (SimPy.Simulation), a real-time simulation (SimPy.SimulationRT) or something else. You usually had to import Simulation (or SimulationRT), Process and some of the SimPy keywords (hold or passivate, for example) from that package.
In SimPy 3, you usually need to import much less classes and modules (e.g., you don’t need direct access to Process and the SimPy keywords anymore). In most use cases you will now only need to import simpy.
SimPy 2
from Simpy.Simulation import Simulation, Process, hold
SimPy 3
import simpy
The Simulation* classes¶
SimPy 2 encapsulated the simulation state in a Simulation* class (e.g., Simulation, SimulationRT or SimulationTrace). This class also had a simulate() method that executed a normal simulation, a real-time simulation or something else (depending on the particular class).
There was a global Simulation instance that was automatically created when you imported SimPy. You could also instantiate it on your own to uses Simpy’s object-orient API. This led to some confusion and problems, because you had to pass the Simulation instance around when you were using the OO API but not if you were using the procedural API.
In SimPy 3, an Environment replaces Simulation and RealtimeEnvironment replaces SimulationRT. You always need to instantiate an environment. There’s no more global state.
To execute a simulation, you call the environment’s run() method.
SimPy 2
# Procedural API
from SimPy.Simulation import initialize, simulate
initialize()
# Start processes
simulate(until=10)
# Object-oriented API
from SimPy.Simulation import Simulation
sim = Simulation()
# Start processes
sim.simulate(until=10)
SimPy3
import simpy
env = simpy.Environment()
# Start processes
env.run(until=10)
Defining a Process¶
Processes had to inherit the Process base class in SimPy 2. Subclasses had to implement at least a so called Process Execution Method (PEM) and in most cases __init__(). Each process needed to know the Simulation instance it belonged to. This reference was passed implicitly in the procedural API and had to be passed explicitly in the object-oriented API. Apart from some internal problems, this made it quite cumbersome to define a simple process.
Processes were started by passing the Process and the generator returned by the PEM to either the global activate() function or the corresponding Simulation method.
A process in SimPy 3 can be defined by any Python generator function (no matter if it’s defined on module level or as an instance method). Hence, they are now just called process functions. They usually require a reference to the Environment to interact with, but this is completely optional.
Processes are can be started by creating a Process instance and passing the generator to it. The environment provides a shortcut for this: process().
SimPy 2
# Procedural API
from Simpy.Simulation import Process
class MyProcess(Process):
def __init__(self, another_param):
super().__init__()
self.another_param = another_param
def run(self):
"""Implement the process' behavior."""
initialize()
proc = Process('Spam')
activate(proc, proc.run())
# Object-oriented API
from SimPy.Simulation import Simulation, Process
class MyProcess(Process):
def __init__(self, sim, another_param):
super().__init__(sim=sim)
self.another_param = another_param
def run(self):
"""Implement the process' behaviour."""
sim = Simulation()
proc = Process(sim, 'Spam')
sim.activate(proc, proc.run())
SimPy 3
import simpy
def my_process(env, another_param):
"""Implement the process' behavior."""
env = simpy.Environment()
proc = env.process(my_process(env, 'Spam'))
SimPy Keywords (hold etc.)¶
In SimPy 2, processes created new events by yielding a SimPy Keyword and some additional parameters (at least self). These keywords had to be imported from SimPy.Simulation* if they were used. Internally, the keywords were mapped to a function that generated the according event.
In SimPy 3, you directly yield events. You can instantiate an event directly or use the shortcuts provided by Environment.
Generally, whenever a process yields an event, this process is suspended and resumed once the event has been triggered. To motivate this understanding, some of the events were renamed. For example, the hold keyword meant to wait until some time has passed. In terms of events this means that a timeout has happened. Therefore hold has been replaced by a Timeout event.
SimPy 2
yield hold, self, duration
yield passivate, self
yield request, self, resource
yield release, self, resource
yield waitevent, self, event
yield waitevent, self, [event_a, event_b, event_c]
yield queueevent, self, event_list
yield waituntil, self, cond_func
yield get, self, level, amount
yield put, self, level, amount
SimPy 3
from simpy.util import wait_for_any, wait_for_all
yield env.timeout(duration) # hold: renamed
yield env.event() # passivate: renamed
yield resource.request() # Request is now bound to class Resource
resource.release() # Release no longer needs to be yielded
yield event # waitevent: just yield the event
yield wait_for_any([event_a, event_b, event_c]) # waitevent
yield wait_for_all([event_a, event_b, event_c]) # This is new.
yield event_a | event_b # Wait for either a or b. This is new.
yield event_a & event_b # Wait for a and b. This is new.
# There is no direct equivalent for "queueevent"
yield env.process(cond_func(env)) # cond_func is now a process that
# terminates when the cond. is True
# (Yes, you can wait for processes now!)
yield container.get(amount) # Level is now called Container
yield container.put(amount)
Interrupts¶
In SimPy 2, interrupt() was a method of the interrupting process. The victim of the interrupt had to be passed as an argument.
The victim was not directly notified of the interrupt but had to check if the interrupted flag was set. It then had to reset the interrupt via interruptReset(). You could manually set the interruptCause attribute of the victim.
Explicitly checking for an interrupt is obviously error prone as it is too easy to be forgotten.
In SimPy 3, you call interrupt() on the victim process. You can optionally pass a cause. An Interrupt is then thrown into the victim process, which has to handle the interrupt via try: ... except Interrupt: ....
SimPy 2
class Interrupter(Process):
def __init__(self, victim):
super().__init__()
self.victim = victim
def run(self):
yield hold, self, 1
self.interrupt(self.victim_proc)
self.victim_proc.interruptCause = 'Spam'
class Victim(Process):
def run(self):
yield hold, self, 10
if self.interrupted:
cause = self.interruptCause
self.interruptReset()
SimPy 3
def interrupter(env, victim_proc):
yield env.timeout(1)
victim_proc.interrupt('Spam')
def victim(env):
try:
yield env.timeout(10)
except Interrupt as interrupt:
cause = interrupt.cause
Conclusion¶
This guide is by no means complete. If you run into problems, please have a look at the other guides, the examples or the API Reference. You are also very welcome to submit improvements. Just create a pull request at bitbucket.
Examples¶
All theory is grey. In this section, we present various practical examples that demonstrate how to uses SimPy’s features.
Here’s a list of examples grouped by features they demonstrate.
Condition events¶
Interrupts¶
Monitoring¶
Resources: Container¶
Resources: Preemptive Resource¶
Resources: Resource¶
Resources: Store¶
Waiting for other processes¶
All examples¶
Bank Renege¶
Covers:
- Resources: Resource
- Condition events
A counter with a random service time and customers who renege. Based on the program bank08.py from TheBank tutorial of SimPy 2. (KGM)
This example models a bank counter and customers arriving t random times. Each customer has a certain patience. It waits to get to the counter until she’s at the end of her tether. If she gets to the counter, she uses it for a while before releasing it.
New customers are created by the source process every few time steps.
"""
Bank renege example
Covers:
- Resources: Resource
- Condition events
Scenario:
A counter with a random service time and customers who renege. Based on the
program bank08.py from TheBank tutorial of SimPy 2. (KGM)
"""
import random
import simpy
RANDOM_SEED = 42
NEW_CUSTOMERS = 5 # Total number of customers
INTERVAL_CUSTOMERS = 10.0 # Generate new customers roughly every x seconds
MIN_PATIENCE = 1 # Min. customer patience
MAX_PATIENCE = 3 # Max. customer patience
def source(env, number, interval, counter):
"""Source generates customers randomly"""
for i in range(number):
c = customer(env, 'Customer%02d' % i, counter, time_in_bank=12.0)
env.process(c)
t = random.expovariate(1.0 / interval)
yield env.timeout(t)
def customer(env, name, counter, time_in_bank):
"""Customer arrives, is served and leaves."""
arrive = env.now
print('%7.4f %s: Here I am' % (arrive, name))
with counter.request() as req:
patience = random.uniform(MIN_PATIENCE, MAX_PATIENCE)
# Wait for the counter or abort at the end of our tether
results = yield req | env.timeout(patience)
wait = env.now - arrive
if req in results:
# We got to the counter
print('%7.4f %s: Waited %6.3f' % (env.now, name, wait))
tib = random.expovariate(1.0 / time_in_bank)
yield env.timeout(tib)
print('%7.4f %s: Finished' % (env.now, name))
else:
# We reneged
print('%7.4f %s: RENEGED after %6.3f' % (env.now, name, wait))
# Setup and start the simulation
print('Bank renege')
random.seed(RANDOM_SEED)
env = simpy.Environment()
# Start processes and run
counter = simpy.Resource(env, capacity=1)
env.process(source(env, NEW_CUSTOMERS, INTERVAL_CUSTOMERS, counter))
env.run()
The simulation’s output:
Bank renege
0.0000 Customer00: Here I am
0.0000 Customer00: Waited 0.000
3.8595 Customer00: Finished
10.2006 Customer01: Here I am
10.2006 Customer01: Waited 0.000
12.7265 Customer02: Here I am
13.9003 Customer02: RENEGED after 1.174
23.7507 Customer01: Finished
34.9993 Customer03: Here I am
34.9993 Customer03: Waited 0.000
37.9599 Customer03: Finished
40.4798 Customer04: Here I am
40.4798 Customer04: Waited 0.000
43.1401 Customer04: Finished
Carwash¶
Covers:
- Waiting for other processes
- Resources: Resource
The Carwash example is a simulation of a carwash with a limited number of machines and a number of cars that arrive at the carwash to get cleaned.
The carwash uses a Resource to model the limited number of washing machines. It also defines a process for washing a car.
When a car arrives at the carwash, it requests a machine. Once it got one, it starts the carwash’s wash processes and waits for it to finish. It finally releases the machine and leaves.
The cars are generated by a setup process. After creating an intial amount of cars it creates new car processes after a random time interval as long as the simulation continues.
"""
Carwasch example.
Covers:
- Waiting for other processes
- Resources: Resource
Scenario:
A carwash has a limited number of washing machines and defines
a washing processes that takes some (random) time.
Car processes arrive at the carwash at a random time. If one washing
machine is available, they start the washing process and wait for it
to finish. If not, they wait until they an use one.
"""
import random
import simpy
RANDOM_SEED = 42
NUM_MACHINES = 2 # Number of machines in the carwash
WASHTIME = 5 # Minutes it takes to clean a car
T_INTER = 7 # Create a car every ~7 minutes
SIM_TIME = 20 # Simulation time in minutes
class Carwash(object):
"""A carwash has a limited number of machines (``NUM_MACHINES``) to
clean cars in parallel.
Cars have to request one of the machines. When they got one, they
can start the washing processes and wait for it to finish (which
takes ``washtime`` minutes).
"""
def __init__(self, env, num_machines, washtime):
self.env = env
self.machine = simpy.Resource(env, num_machines)
self.washtime = washtime
def wash(self, car):
"""The washing processes. It takes a ``car`` processes and tries
to clean it."""
yield self.env.timeout(WASHTIME)
print("Carwashed removed %d%% of %s's dirt." %
(random.randint(50, 99), car))
def car(env, name, cw):
"""The car process (each car has a ``name``) arrives at the carwash
(``cw``) and requests a cleaning machine.
It then starts the washing process, waits for it to finish and
leaves to never come back ...
"""
print('%s arrives at the carwash at %.2f.' % (name, env.now))
with cw.machine.request() as request:
yield request
print('%s enters the carwash at %.2f.' % (name, env.now))
yield env.process(cw.wash(name))
print('%s leaves the carwash at %.2f.' % (name, env.now))
def setup(env, num_machines, washtime, t_inter):
"""Create a carwash, a number of initial cars and keep creating cars
approx. every ``t_inter`` minutes."""
# Create the carwash
carwash = Carwash(env, num_machines, washtime)
# Create 4 initial cars
for i in range(4):
env.process(car(env, 'Car %d' % i, carwash))
# Create more cars while the simulation is running
while True:
yield env.timeout(random.randint(t_inter-2, t_inter+2))
i += 1
env.process(car(env, 'Car %d' % i, carwash))
# Setup and start the simulation
print('Carwash')
print('Check out http://youtu.be/fXXmeP9TvBg while simulating ... ;-)')
random.seed(RANDOM_SEED) # This helps reproducing the results
# Create an environment and start the setup process
env = simpy.Environment()
env.process(setup(env, NUM_MACHINES, WASHTIME, T_INTER))
# Execute!
env.run(until=SIM_TIME)
The simulation’s output:
Carwash
Check out http://youtu.be/fXXmeP9TvBg while simulating ... ;-)
Car 0 arrives at the carwash at 0.00.
Car 1 arrives at the carwash at 0.00.
Car 2 arrives at the carwash at 0.00.
Car 3 arrives at the carwash at 0.00.
Car 0 enters the carwash at 0.00.
Car 1 enters the carwash at 0.00.
Car 4 arrives at the carwash at 5.00.
Carwashed removed 97% of Car 0's dirt.
Carwashed removed 67% of Car 1's dirt.
Car 0 leaves the carwash at 5.00.
Car 1 leaves the carwash at 5.00.
Car 2 enters the carwash at 5.00.
Car 3 enters the carwash at 5.00.
Car 5 arrives at the carwash at 10.00.
Carwashed removed 64% of Car 2's dirt.
Carwashed removed 58% of Car 3's dirt.
Car 2 leaves the carwash at 10.00.
Car 3 leaves the carwash at 10.00.
Car 4 enters the carwash at 10.00.
Car 5 enters the carwash at 10.00.
Carwashed removed 97% of Car 4's dirt.
Carwashed removed 56% of Car 5's dirt.
Car 4 leaves the carwash at 15.00.
Car 5 leaves the carwash at 15.00.
Car 6 arrives at the carwash at 16.00.
Car 6 enters the carwash at 16.00.
Machine Shop¶
Covers:
- Interrupts
- Resources: PreemptiveResource
This example comprises a workshop with n identical machines. A stream of jobs (enough to keep the machines busy) arrives. Each machine breaks down periodically. Repairs are carried out by one repairman. The repairman has other, less important tasks to perform, too. Broken machines preempt theses tasks. The repairman continues them when he is done with the machine repair. The workshop works continuously.
A machine has two processes: working implements the actual behaviour of the machine (producing parts). break_machine periodically interrupts the working process to simulate the machine failure.
The repairman’s other job is also a process (implemented by other_job). The repairman itself is a PreemptiveResource with a capacity of 1. The machine repairing has a priority of 1, while the other job has a priority of 2 (the smaller the number, the higher the priority).
"""
Machine shop example
Covers:
- Interrupts
- Resources: PreemptiveResource
Scenario:
A workshop has *n* identical machines. A stream of jobs (enough to
keep the machines busy) arrives. Each machine breaks down
periodically. Repairs are carried out by one repairman. The repairman
has other, less important tasks to perform, too. Broken machines
preempt theses tasks. The repairman continues them when he is done
with the machine repair. The workshop works continuously.
"""
import random
import simpy
RANDOM_SEED = 42
PT_MEAN = 10.0 # Avg. processing time in minutes
PT_SIGMA = 2.0 # Sigma of processing time
MTTF = 300.0 # Mean time to failure in minutes
BREAK_MEAN = 1 / MTTF # Param. for expovariate distribution
REPAIR_TIME = 30.0 # Time it takes to repair a machine in minutes
JOB_DURATION = 30.0 # Duration of other jobs in minutes
NUM_MACHINES = 10 # Number of machines in the machine shop
WEEKS = 4 # Simulation time in weeks
SIM_TIME = WEEKS * 7 * 24 * 60 # Simulation time in minutes
def time_per_part():
"""Return actual processing time for a concrete part."""
return random.normalvariate(PT_MEAN, PT_SIGMA)
def time_to_failure():
"""Return time until next failure for a machine."""
return random.expovariate(BREAK_MEAN)
class Machine(object):
"""A machine produces parts and my get broken every now and then.
If it breaks, it requests a *repairman* and continues the production
after the it is repaired.
A machine has a *name* and a numberof *parts_made* thus far.
"""
def __init__(self, env, name, repairman):
self.env = env
self.name = name
self.parts_made = 0
self.broken = False
# Start "working" and "break_machine" processes for this machine.
self.process = env.process(self.working(repairman))
env.process(self.break_machine())
def working(self, repairman):
"""Produce parts as long as the simulation runs.
While making a part, the machine may break multiple times.
Request a repairman when this happens.
"""
while True:
# Start making a new part
done_in = time_per_part()
while done_in:
try:
# Working on the part
start = self.env.now
yield self.env.timeout(done_in)
done_in = 0 # Set to 0 to exit while loop.
except simpy.Interrupt:
self.broken = True
done_in -= self.env.now - start # How much time left?
# Request a repairman. This will preempt its "other_job".
with repairman.request(priority=1) as req:
yield req
yield self.env.timeout(REPAIR_TIME)
self.broken = False
# Part is done.
self.parts_made += 1
def break_machine(self):
"""Break the machine every now and then."""
while True:
yield self.env.timeout(time_to_failure())
if not self.broken:
# Only break the machine if it is currently working.
self.process.interrupt()
def other_jobs(env, repairman):
"""The repairman's other (unimportant) job."""
while True:
# Start a new job
done_in = JOB_DURATION
while done_in:
# Retry the job until it is done.
# It's priority is lower than that of machine repairs.
with repairman.request(priority=2) as req:
yield req
try:
start = env.now
yield env.timeout(done_in)
done_in = 0
except simpy.Interrupt:
done_in -= env.now - start
# Setup and start the simulation
print('Machine shop')
random.seed(RANDOM_SEED) # This helps reproducing the results
# Create an environment and start the setup process
env = simpy.Environment()
repairman = simpy.PreemptiveResource(env, capacity=1)
machines = [Machine(env, 'Machine %d' % i, repairman)
for i in range(NUM_MACHINES)]
env.process(other_jobs(env, repairman))
# Execute!
env.run(until=SIM_TIME)
# Analyis/results
print('Machine shop results after %s weeks' % WEEKS)
for machine in machines:
print('%s made %d parts.' % (machine.name, machine.parts_made))
The simulation’s output:
Machine shop
Machine shop results after 4 weeks
Machine 0 made 3251 parts.
Machine 1 made 3273 parts.
Machine 2 made 3242 parts.
Machine 3 made 3343 parts.
Machine 4 made 3387 parts.
Machine 5 made 3244 parts.
Machine 6 made 3269 parts.
Machine 7 made 3185 parts.
Machine 8 made 3302 parts.
Machine 9 made 3279 parts.
Movie Renege¶
Covers:
- Resources: Resource
- Condition events
- Shared events
This examples models a movie theater with one ticket counter selling tickets for three movies (next show only). People arrive at random times and triy to buy a random number (1–6) tickets for a random movie. When a movie is sold out, all people waiting to buy a ticket for that movie renege (leave the queue).
The movie theater is just a container for all the related data (movies, the counter, tickets left, collected data, ...). The counter is a Resource with a capacity of one.
The moviegoer process starts waiting until either it’s his turn (it acquires the counter resource) or until the sold out signal is triggered. If the latter is the case it reneges (leaves the queue). If it gets to the counter, it tries to buy some tickets. This might not be successful, e.g. if the process tries to buy 5 tickets but only 3 are left. If less then two tickets are left after the ticket purchase, the sold out signal is triggered.
Moviegoers are generated by the customer arrivals process. It also chooses a movie and the number of tickets for the moviegoer.
"""
Movie renege example
Covers:
- Resources: Resource
- Condition events
- Shared events
Scenario:
A movie theatre has one ticket counter selling tickets for three
movies (next show only). When a movie is sold out, all people waiting
to buy tickets for that movie renege (leave queue).
"""
import collections
import random
import simpy
RANDOM_SEED = 42
TICKETS = 50 # Number of tickets per movie
SIM_TIME = 120 # Simulate until
def moviegoer(env, movie, num_tickets, theater):
"""A moviegoer tries to by a number of tickets (*num_tickets*) for
a certain *movie* in a *theater*.
If the movie becomes sold out, she leaves the theater. If she gets
to the counter, she tries to buy a number of tickets. If not enough
tickets are left, she argues with the teller and leaves.
If at most one ticket is left after the moviegoer bought her
tickets, the *sold out* event for this movie is triggered causing
all remaining moviegoers to leave.
"""
with theater.counter.request() as my_turn:
# Wait until its our turn or until the movie is sold out
result = yield my_turn | theater.sold_out[movie]
# Check if it's our turn of if movie is sold out
if my_turn not in result:
theater.num_renegers[movie] += 1
env.exit()
# Check if enough tickets left.
if theater.available[movie] < num_tickets:
# Moviegoer leaves after some discussion
yield env.timeout(0.5)
env.exit()
# Buy tickets
theater.available[movie] -= num_tickets
if theater.available[movie] < 2:
# Trigger the "sold out" event for the movie
theater.sold_out[movie].succeed()
theater.when_sold_out[movie] = env.now
theater.available[movie] = 0
yield env.timeout(1)
def customer_arrivals(env, theater):
"""Create new *moviegoers* until the sim time reaches 120."""
while True:
yield env.timeout(random.expovariate(1 / 0.5))
movie = random.choice(theater.movies)
num_tickets = random.randint(1, 6)
if theater.available[movie]:
env.process(moviegoer(env, movie, num_tickets, theater))
Theater = collections.namedtuple('Theater', 'counter, movies, available, '
'sold_out, when_sold_out, '
'num_renegers')
# Setup and start the simulation
print('Movie renege')
random.seed(RANDOM_SEED)
env = simpy.Environment()
# Create movie theater
counter = simpy.Resource(env, capacity=1)
movies = ['Python Unchained', 'Kill Process', 'Pulp Implementation']
available = {movie: TICKETS for movie in movies}
sold_out = {movie: env.event() for movie in movies}
when_sold_out = {movie: None for movie in movies}
num_renegers = {movie: 0 for movie in movies}
theater = Theater(counter, movies, available, sold_out, when_sold_out,
num_renegers)
# Start process and run
env.process(customer_arrivals(env, theater))
env.run(until=SIM_TIME)
# Analysis/results
for movie in movies:
if theater.sold_out[movie]:
print('Movie "%s" sold out %.1f minutes after ticket counter '
'opening.' % (movie, theater.when_sold_out[movie]))
print(' Number of people leaving queue when film sold out: %s' %
theater.num_renegers[movie])
The simulation’s output:
Movie renege
Movie "Python Unchained" sold out 38.0 minutes after ticket counter opening.
Number of people leaving queue when film sold out: 16
Movie "Kill Process" sold out 43.0 minutes after ticket counter opening.
Number of people leaving queue when film sold out: 5
Movie "Pulp Implementation" sold out 28.0 minutes after ticket counter opening.
Number of people leaving queue when film sold out: 5
Gas Station Refueling¶
Covers:
- Resources: Resource
- Resources: Container
- Waiting for other processes
This examples models a gas station and cars that arrive at the station for refueling.
The gas station has a limited number of fuel pumps and a fuel tank that is shared between the fuel pumps. The gas station is thus modeled as Resource. The shared fuel tank is modeled with a Container.
Vehicles arriving at the gas station first request a fuel pump from the station. Once they acquire one, they try to take the desired amount of fuel from the fuel pump. They leave when they are done.
The gas stations fuel level is reqularly monitored by gas station control. When the level drops below a certain threshold, a tank truck is called to refuel the gas station itself.
"""
Gas Station Refueling example
Covers:
- Resources: Resource
- Resources: Container
- Waiting for other processes
Scenario:
A gas station has a limited number of gas pumps that share a common
fuel reservoir. Cars randomly arrive at the gas station, request one
of the fuel pumps and start refueling from that reservoir.
A gas station control process observes the gas station's fuel level
and calls a tank truck for refueling if the station's level drops
below a threshold.
"""
import itertools
import random
import simpy
RANDOM_SEED = 42
GAS_STATION_SIZE = 200 # liters
THRESHOLD = 10 # Threshold for calling the tank truck (in %)
FUEL_TANK_SIZE = 50 # liters
FUEL_TANK_LEVEL = [5, 25] # Min/max levels of fuel tanks (in liters)
REFUELING_SPEED = 2 # liters / second
TANK_TRUCK_TIME = 300 # Seconds it takes the tank truck to arrive
T_INTER = [30, 300] # Create a car every [min, max] seconds
SIM_TIME = 1000 # Simulation time in seconds
def car(name, env, gas_station, fuel_pump):
"""A car arrives at the gas station for refueling.
It requests one of the gas station's fuel pumps and tries to get the
desired amount of gas from it. If the stations reservoir is
depleted, the car has to wait for the tank truck to arrive.
"""
fuel_tank_level = random.randint(*FUEL_TANK_LEVEL)
print('%s arriving at gas station at %.1f' % (name, env.now))
with gas_station.request() as req:
start = env.now
# Request one of the gas pumps
yield req
# Get the required amount of fuel
liters_required = FUEL_TANK_SIZE - fuel_tank_level
yield fuel_pump.get(liters_required)
# The "actual" refueling process takes some time
yield env.timeout(liters_required / REFUELING_SPEED)
print('%s finished refueling in %.1f seconds.' % (name,
env.now - start))
def gas_station_control(env, fuel_pump):
"""Periodically check the level of the *fuel_pump* and call the tank
truck if the level falls below a threshold."""
while True:
if fuel_pump.level / fuel_pump.capacity * 100 < THRESHOLD:
# We need to call the tank truck now!
print('Calling tank truck at %d' % env.now)
# Wait for the tank truck to arrive and refuel the station
yield env.process(tank_truck(env, fuel_pump))
yield env.timeout(10) # Check every 10 seconds
def tank_truck(env, fuel_pump):
"""Arrives at the gas station after a certain delay and refuels it."""
yield env.timeout(TANK_TRUCK_TIME)
print('Tank truck arriving at time %d' % env.now)
ammount = fuel_pump.capacity - fuel_pump.level
print('Tank truck refuelling %.1f liters.' % ammount)
yield fuel_pump.put(ammount)
def car_generator(env, gas_station, fuel_pump):
"""Generate new cars that arrive at the gas station."""
for i in itertools.count():
yield env.timeout(random.randint(*T_INTER))
env.process(car('Car %d' % i, env, gas_station, fuel_pump))
# Setup and start the simulation
print('Gas Station refuelling')
random.seed(RANDOM_SEED)
# Create environment and start processes
env = simpy.Environment()
gas_station = simpy.Resource(env, 2)
fuel_pump = simpy.Container(env, GAS_STATION_SIZE, init=GAS_STATION_SIZE)
env.process(gas_station_control(env, fuel_pump))
env.process(car_generator(env, gas_station, fuel_pump))
# Execute!
env.run(until=SIM_TIME)
The simulation’s output:
Gas Station refuelling
Car 0 arriving at gas station at 87.0
Car 0 finished refueling in 18.5 seconds.
Car 1 arriving at gas station at 129.0
Car 1 finished refueling in 19.0 seconds.
Car 2 arriving at gas station at 284.0
Car 2 finished refueling in 21.0 seconds.
Car 3 arriving at gas station at 385.0
Car 3 finished refueling in 13.5 seconds.
Car 4 arriving at gas station at 459.0
Calling tank truck at 460
Car 4 finished refueling in 22.0 seconds.
Car 5 arriving at gas station at 705.0
Car 6 arriving at gas station at 750.0
Tank truck arriving at time 760
Tank truck refuelling 188.0 liters.
Car 6 finished refueling in 29.0 seconds.
Car 5 finished refueling in 76.5 seconds.
Car 7 arriving at gas station at 891.0
Car 7 finished refueling in 13.0 seconds.
Process Communication¶
Covers:
- Resources: Store
This example shows how to interconnect simulation model elements together using “resources.Store” for one-to-one, and many-to-one asynchronous processes. For one-to-many a simple BroadCastPipe class is constructed from Store.
- When Useful:
When a consumer process does not always wait on a generating process and these processes run asynchronously. This example shows how to create a buffer and also tell is the consumer process was late yielding to the event from a generating process.
This is also useful when some information needs to be broadcast to many receiving processes
Finally, using pipes can simplify how processes are interconnected to each other in a simulation model.
- Example By:
- Keith Smith
"""
Process communication example
Covers:
- Resources: Store
Scenario:
This example shows how to interconnect simulation model elements
together using :class:`~simpy.resources.store.Store` for one-to-one,
and many-to-one asynchronous processes. For one-to-many a simple
BroadCastPipe class is constructed from Store.
When Useful:
When a consumer process does not always wait on a generating process
and these processes run asynchronously. This example shows how to
create a buffer and also tell is the consumer process was late
yielding to the event from a generating process.
This is also useful when some information needs to be broadcast to
many receiving processes
Finally, using pipes can simplify how processes are interconnected to
each other in a simulation model.
Example By:
Keith Smith
"""
import random
import simpy
RANDOM_SEED = 42
SIM_TIME = 100
class BroadcastPipe(object):
"""A Broadcast pipe that allows one process to send messages to many.
This construct is useful when message consumers are running at
different rates than message generators and provides an event
buffering to the consuming processes.
The parameters are used to create a new
:class:`~simpy.resources.store.Store` instance each time
:meth:`get_output_conn()` is called.
"""
def __init__(self, env, capacity=simpy.core.Infinity):
self.env = env
self.capacity = capacity
self.pipes = []
def put(self, value):
"""Broadcast a *value* to all receivers."""
if not self.pipes:
raise RuntimeError('There are no output pipes.')
events = [store.put(value) for store in self.pipes]
return self.env.all_of(events) # Condition event for all "events"
def get_output_conn(self):
"""Get a new output connection for this broadcast pipe.
The return value is a :class:`~simpy.resources.store.Store`.
"""
pipe = simpy.Store(self.env, capacity=self.capacity)
self.pipes.append(pipe)
return pipe
def message_generator(name, env, out_pipe):
"""A process which randomly generates messages."""
while True:
# wait for next transmission
yield env.timeout(random.randint(6, 10))
# messages are time stamped to later check if the consumer was
# late getting them. Note, using event.triggered to do this may
# result in failure due to FIFO nature of simulation yields.
# (i.e. if at the same env.now, message_generator puts a message
# in the pipe first and then message_consumer gets from pipe,
# the event.triggered will be True in the other order it will be
# False
msg = (env.now, '%s says hello at %d' % (name, env.now))
out_pipe.put(msg)
def message_consumer(name, env, in_pipe):
"""A process which consumes messages."""
while True:
# Get event for message pipe
msg = yield in_pipe.get()
if msg[0] < env.now:
# if message was already put into pipe, then
# message_consumer was late getting to it. Depending on what
# is being modeled this, may, or may not have some
# significance
print('LATE Getting Message: at time %d: %s received message: %s' %
(env.now, name, msg[1]))
else:
# message_consumer is synchronized with message_generator
print('at time %d: %s received message: %s.' %
(env.now, name, msg[1]))
# Process does some other work, which may result in missing messages
yield env.timeout(random.randint(4, 8))
# Setup and start the simulation
print('Process communication')
random.seed(RANDOM_SEED)
env = simpy.Environment()
# For one-to-one or many-to-one type pipes, use Store
pipe = simpy.Store(env)
env.process(message_generator('Generator A', env, pipe))
env.process(message_consumer('Consumer A', env, pipe))
print('\nOne-to-one pipe communication\n')
env.run(until=SIM_TIME)
# For one-to many use BroadcastPipe
# (Note: could also be used for one-to-one,many-to-one or many-to-many)
env = simpy.Environment()
bc_pipe = BroadcastPipe(env)
env.process(message_generator('Generator A', env, bc_pipe))
env.process(message_consumer('Consumer A', env, bc_pipe.get_output_conn()))
env.process(message_consumer('Consumer B', env, bc_pipe.get_output_conn()))
print('\nOne-to-many pipe communication\n')
env.run(until=SIM_TIME)
The simulation’s output:
Process communication
One-to-one pipe communication
at time 6: Consumer A received message: Generator A says hello at 6.
at time 12: Consumer A received message: Generator A says hello at 12.
at time 19: Consumer A received message: Generator A says hello at 19.
at time 26: Consumer A received message: Generator A says hello at 26.
at time 36: Consumer A received message: Generator A says hello at 36.
at time 46: Consumer A received message: Generator A says hello at 46.
at time 52: Consumer A received message: Generator A says hello at 52.
at time 58: Consumer A received message: Generator A says hello at 58.
LATE Getting Message: at time 66: Consumer A received message: Generator A says hello at 65
at time 75: Consumer A received message: Generator A says hello at 75.
at time 85: Consumer A received message: Generator A says hello at 85.
at time 95: Consumer A received message: Generator A says hello at 95.
One-to-many pipe communication
at time 10: Consumer A received message: Generator A says hello at 10.
at time 10: Consumer B received message: Generator A says hello at 10.
at time 18: Consumer A received message: Generator A says hello at 18.
at time 18: Consumer B received message: Generator A says hello at 18.
at time 27: Consumer A received message: Generator A says hello at 27.
at time 27: Consumer B received message: Generator A says hello at 27.
at time 34: Consumer A received message: Generator A says hello at 34.
at time 34: Consumer B received message: Generator A says hello at 34.
at time 40: Consumer A received message: Generator A says hello at 40.
LATE Getting Message: at time 41: Consumer B received message: Generator A says hello at 40
at time 46: Consumer A received message: Generator A says hello at 46.
LATE Getting Message: at time 47: Consumer B received message: Generator A says hello at 46
at time 56: Consumer A received message: Generator A says hello at 56.
at time 56: Consumer B received message: Generator A says hello at 56.
at time 65: Consumer A received message: Generator A says hello at 65.
at time 65: Consumer B received message: Generator A says hello at 65.
at time 74: Consumer A received message: Generator A says hello at 74.
at time 74: Consumer B received message: Generator A says hello at 74.
at time 82: Consumer A received message: Generator A says hello at 82.
at time 82: Consumer B received message: Generator A says hello at 82.
at time 92: Consumer A received message: Generator A says hello at 92.
at time 92: Consumer B received message: Generator A says hello at 92.
at time 98: Consumer B received message: Generator A says hello at 98.
at time 98: Consumer A received message: Generator A says hello at 98.
Event Latency¶
Covers:
- Resources: Store
This example shows how to separate the time delay of events between processes from the processes themselves.
- When Useful:
When modeling physical things such as cables, RF propagation, etc. it better encapsulation to keep this propagation mechanism outside of the sending and receiving processes.
Can also be used to interconnect processes sending messages
- Example by:
- Keith Smith
"""
Event Latency example
Covers:
- Resources: Store
Scenario:
This example shows how to separate the time delay of events between
processes from the processes themselves.
When Useful:
When modeling physical things such as cables, RF propagation, etc. it
better encapsulation to keep this propagation mechanism outside of the
sending and receiving processes.
Can also be used to interconnect processes sending messages
Example by:
Keith Smith
"""
import simpy
SIM_DURATION = 100
class Cable(object):
"""This class represents the propagation through a cable."""
def __init__(self, env, delay):
self.env = env
self.delay = delay
self.store = simpy.Store(env)
def latency(self, value):
yield self.env.timeout(self.delay)
self.store.put(value)
def put(self, value):
self.env.process(self.latency(value))
def get(self):
return self.store.get()
def sender(env, cable):
"""A process which randomly generates messages."""
while True:
# wait for next transmission
yield env.timeout(5)
cable.put('Sender sent this at %d' % env.now)
def receiver(env, cable):
"""A process which consumes messages."""
while True:
# Get event for message pipe
msg = yield cable.get()
print('Received this at %d while %s' % (env.now, msg))
# Setup and start the simulation
print('Event Latency')
env = simpy.Environment()
cable = Cable(env, 10)
env.process(sender(env, cable))
env.process(receiver(env, cable))
env.run(until=SIM_DURATION)
The simulation’s output:
Event Latency
Received this at 15 while Sender sent this at 5
Received this at 20 while Sender sent this at 10
Received this at 25 while Sender sent this at 15
Received this at 30 while Sender sent this at 20
Received this at 35 while Sender sent this at 25
Received this at 40 while Sender sent this at 30
Received this at 45 while Sender sent this at 35
Received this at 50 while Sender sent this at 40
Received this at 55 while Sender sent this at 45
Received this at 60 while Sender sent this at 50
Received this at 65 while Sender sent this at 55
Received this at 70 while Sender sent this at 60
Received this at 75 while Sender sent this at 65
Received this at 80 while Sender sent this at 70
Received this at 85 while Sender sent this at 75
Received this at 90 while Sender sent this at 80
Received this at 95 while Sender sent this at 85
You have ideas for better examples? Please send them to our mainling list or make a pull request on bitbucket.
API Reference¶
The API reference provides detailed descriptions of SimPy’s classes and functions. It should be helpful if you plan to extend Simpy with custom components.
simpy — The end user API¶
The simpy module provides SimPy’s end-user API. It aggregates Simpy’s most important classes and methods. This is purely for your convenience. You can of course also access everything (and more!) via their actual submodules.
Core classes and functions¶
- Environment: SimPy’s central class. It contains the simulation’s state and lets the PEMs interact with it (i.e., schedule events).
- Interrupt: This exception is thrown into a process if it gets interrupted by another one.
Resources¶
- Resource: Can be used by a limited number of processes at a time (e.g., a gas station with a limited number of fuel pumps).
- PriorityResource: Like Resource, but waiting processes are sorted by priority.
- PreemptiveResource: Version of Resource with preemption.
- Container: Models the production and consumption of a homogeneous, undifferentiated bulk. It may either be continuous (like water) or discrete (like apples).
- Store: Allows the production and consumption of discrete Python objects.
- FilterStore: Like Store, but items taken out of it can be filtered with a user-defined function.
Monitoring¶
[Not yet implemented]
simpy.core — SimPy’s core components¶
This module contains the implementation of SimPy’s core classes. The most important ones are directly importable via simpy.
- class simpy.core.BaseEnvironment¶
The abstract definition of an environment.
An implementation must at least provide the means to access the current time of the environment (see now) and to schedule (see schedule()) as well as execute (see step() and run()) events.
The class is meant to be subclassed for different execution environments. For example, SimPy defines a Environment for simulations with a virtual time and and a RealtimeEnvironment that schedules and executes events in real (e.g., wallclock) time.
- now¶
The current time of the environment.
- active_process¶
The currently active process of the environment.
- schedule(event, priority=1, delay=0)¶
Schedule an event with a given priority and a delay.
- step()¶
Process the next event.
- run(until=None)¶
Executes step() until the given criterion until is met.
- If it is None (which is the default) this method will return if there are no further events to be processed.
- If it is an Event the method will continue stepping until this event has been triggered and will return its value.
- If it can be converted to a number the method will continue stepping until the environment’s time reaches until.
- class simpy.core.Environment(initial_time=0)¶
Inherits BaseEnvironment and implements a simulation environment which simulates the passing of time by stepping from event to event.
You can provide an initial_time for the environment. By defaults, it starts at 0.
This class also provides aliases for common event types, for example process, timeout and event.
- now¶
The current simulation time.
- active_process¶
The currently active process of the environment.
- event()¶
Return a new Event instance. Yielding this event suspends a process until another process triggers the event.
- exit(value=None)¶
Convenience function provided for Python versions prior to 3.3. Stop the current process, optionally providing a value.
Note
From Python 3.3, you can use return value instead.
- schedule(event, priority=1, delay=0)¶
Schedule an event with a given priority and a delay.
- step()¶
Process the next event.
Raise an EmptySchedule if no further events are available.
- run(until=None)¶
Executes step() until the given criterion until is met.
- If it is None (which is the default) this method will return if there are no further events to be processed.
- If it is an Event the method will continue stepping until this event has been triggered and will return its value.
- If it can be converted to a number the method will continue stepping until the environment’s time reaches until.
- class simpy.core.BoundClass(cls)¶
Allows classes to behave like methods.
The __get__() descriptor is basically identical to function.__get__() and binds the first argument of the cls to the descriptor instance.
- static bind_early(instance)¶
Bind all BoundClass attributes of the instance’s class to the instance itself to increase performance.
- class simpy.core.EmptySchedule¶
Thrown by the Environment if there are no further events to be processed.
- simpy.core.Infinity = inf¶
Convenience alias for infinity
simpy.events — Core event types¶
This events module contains the various event type used by the SimPy core.
The base class for all events is Event. Though it can be directly used, there are several specialized subclasses of it:
- Timeout: is scheduled with a certain delay and lets processes hold their state for a certain amount of time.
- Initialize: Initializes a new Process.
- Process: Processes are also modeled as an event so other processes can wait until another one finishes.
- Condition: Events can be concatenated with | an & to either wait until one or both of the events are triggered.
- AllOf: Special case of Condition; wait until a list of events has been triggered.
- AnyOf: Special case of Condition; wait until one of a list of events has been triggered.
This module also defines the Interrupt exception.
- simpy.events.PENDING = object()¶
Unique object to identify pending values of events.
- simpy.events.URGENT = 0¶
Priority of interrupts and process initialization events.
- simpy.events.NORMAL = 1¶
Default priority used by events.
- class simpy.events.Event(env)¶
Base class for all events.
Every event is bound to an environment env (see BaseEnvironment) and has an optional value.
An event has a list of callbacks. A callback can be any callable that accepts a single argument which is the event instances the callback belongs to. This list is not exclusively for SimPy internals—you can also append custom callbacks. All callbacks are executed in the order that they were added when the event is processed.
This class also implements __and__() (&) and __or__() (|). If you concatenate two events using one of these operators, a Condition event is generated that lets you wait for both or one of them.
- env = None¶
The Environment the event lives in.
- callbacks = None¶
List of functions that are called when the event is processed.
- triggered¶
Becomes True if the event has been triggered and its callbacks are about to be invoked.
- processed¶
Becomes True if the event has been processed (e.g., its callbacks have been invoked).
- value¶
The value of the event if it is available.
The value is available when the event has been triggered.
Raise a AttributeError if the value is not yet available.
- trigger(event)¶
Triggers the event with the state and value of the provided event.
This method can be used directly as a callback function.
- succeed(value=None)¶
Schedule the event and mark it as successful. Return the event instance.
You can optionally pass an arbitrary value that will be sent into processes waiting for that event.
Raise a RuntimeError if this event has already been scheduled.
- fail(exception)¶
Schedule the event and mark it as failed. Return the event instance.
The exception will be thrown into processes waiting for that event.
Raise a ValueError if exception is not an Exception.
Raise a RuntimeError if this event has already been scheduled.
- class simpy.events.Timeout(env, delay, value=None)¶
An Event that is scheduled with a certain delay after its creation.
This event can be used by processes to wait (or hold their state) for delay time steps. It is immediately scheduled at env.now + delay and has thus (in contrast to Event) no success() or fail() method.
- class simpy.events.Initialize(env, process)¶
Initializes a process. Only used internally by Process.
- class simpy.events.Process(env, generator)¶
A Process is a wrapper for the process generator (that is returned by a process function) during its execution.
It also contains internal and external status information and is used for process interaction, e.g., for interrupts.
Process inherits Event. You can thus wait for the termination of a process by simply yielding it from your process function.
An instance of this class is returned by simpy.core.Environment.process().
- target¶
The event that the process is currently waiting for.
May be None if the process was just started or interrupted and did not yet yield a new event.
- is_alive¶
True until the process generator exits.
- interrupt(cause=None)¶
Interupt this process optionally providing a cause.
A process cannot be interrupted if it already terminated. A process can also not interrupt itself. Raise a RuntimeError in these cases.
- class simpy.events.Condition(env, evaluate, events)¶
A Condition Event groups several events and is triggered if a given condition (implemented by the evaluate function) becomes true.
The value of the condition is a dictionary that maps the input events to their respective values. It only contains entries for those events that occurred until the condition was met.
If one of the events fails, the condition also fails and forwards the exception of the failing event.
The evaluate function receives the list of target events and the dictionary with all values currently available. If it returns True, the condition is scheduled. The Condition.all_events() and Condition.any_events() functions are used to implement and (&) and or (|) for events.
Conditions events can be nested.
- static all_events(events, values)¶
A condition function that returns True if all events have been triggered.
- static any_events(events, values)¶
A condition function that returns True if at least one of events has been triggered.
- class simpy.events.AnyOf(env, events)¶
A Condition event that waits until the first of events is triggered.
- exception simpy.events.Interrupt(cause)¶
This exceptions is sent into a process if it was interrupted by another process (see Process.interrupt()).
cause may be none if no cause was explicitly passed to Process.interrupt().
An interrupt has a higher priority as a normal event. Thus, if a process has a normal event and an interrupt scheduled at the same time, the interrupt will always be thrown into the process first.
If a process is interrupted multiple times at the same time, all interrupts will be thrown into the process in the same order as they occurred.
- cause¶
The cause of the interrupt or None if no cause was provided.
simpy.monitoring — Monitor SimPy simulations¶
SimPy’s monitoring capabilities will be added in version 3.1.
simpy.resources — SimPy’s built-in resource types¶
SimPy defines three kinds of resources with one or more concrete resource types each:
- resource: Resources that can be used by a limited number of processes at a time (e.g., a gas station with a limited number of fuel pumps).
- container: Resources that model the production and consumption of a homogeneous, undifferentiated bulk. It may either be continuous (like water) or discrete (like apples).
- store: Resources that allow the production and consumption of discrete Python objects.
The base module defines the base classes that are used by all resource types.
simpy.resources.base — Base classes for all resources¶
This module contains the base classes for Simpy’s resource system.
BaseResource defines the abstract base resource. The request for putting something into or getting something out of a resource is modeled as an event that has to be yielded by the requesting process. Put and Get are the base event types for this.
- class simpy.resources.base.BaseResource(env)¶
This is the abstract base class for all SimPy resources.
All resources are bound to a specific Environment env.
You can put() something into the resources or get() something out of it. Both methods return an event that the requesting process has to yield.
If a put or get operation can be performed immediately (because the resource is not full (put) or not empty (get)), that event is triggered immediately.
If a resources is too full or too empty to perform a put or get request, the event is pushed to the put_queue or get_queue. An event is popped from one of these queues and triggered as soon as the corresponding operation is possible.
put() and get() only provide the user API and the general framework and should not be overridden in subclasses. The actual behavior for what happens when a put/get succeeds should rather be implemented in _do_put() and _do_get().
- PutQueue¶
The type to be used for the put_queue. This can either be a plain list (default) or a subclass of it.
alias of list
- GetQueue¶
The type to be used for the get_queue. This can either be a plain list (default) or a subclass of it.
alias of list
- put_queue = None¶
Queue/list of events waiting to get something out of the resource.
- get_queue = None¶
Queue/list of events waiting to put something into the resource.
- _do_put(event)¶
Actually perform the put operation.
This methods needs to be implemented by subclasses. It receives the put_event that is created at each request and doesn’t need to return anything.
- _trigger_put(get_event)¶
Trigger pending put events after a get event has been executed.
- _do_get(event)¶
Actually perform the get operation.
This methods needs to be implemented by subclasses. It receives the get_event that is created at each request and doesn’t need to return anything.
- _trigger_get(put_event)¶
Trigger pending get events after a put event has been executed.
- class simpy.resources.base.Put(resource)¶
The base class for all put events.
It receives the resource that created the event.
This event (and all of its subclasses) can act as context manager and can be used with the with statement to automatically cancel a put request if an exception or an simpy.events.Interrupt occurs:
with res.put(item) as request: yield request
It is not used directly by any resource, but rather sub-classed for each type.
- cancel(exc_type, exc_value, traceback)¶
Cancel the current put request.
This method has to be called if a process received an Interrupt or an exception while yielding this event and is not going to yield this event again.
If the event was created in a with statement, this method is called automatically.
- class simpy.resources.base.Get(resource)¶
The base class for all get events.
It receives the resource that created the event.
This event (and all of its subclasses) can act as context manager and can be used with the with statement to automatically cancel a get request if an exception or an simpy.events.Interrupt occurs:
with res.get() as request: yield request
It is not used directly by any resource, but rather sub-classed for each type.
- cancel(exc_type, exc_value, traceback)¶
Cancel the current get request.
This method has to be called if a process received an Interrupt or an exception while yielding this event and is not going to yield this event again.
If the event was created in a with statement, this method is called automatically.
simpy.resources.container — Container type resources¶
This module contains all Container like resources.
Containers model the production and consumption of a homogeneous, undifferentiated bulk. It may either be continuous (like water) or discrete (like apples).
For example, a gasoline station stores gas (petrol) in large tanks. Tankers increase, and refuelled cars decrease, the amount of gas in the station’s storage tanks.
- class simpy.resources.container.Container(env, capacity=inf, init=0)¶
Models the production and consumption of a homogeneous, undifferentiated bulk. It may either be continuous (like water) or discrete (like apples).
The env parameter is the Environment instance the container is bound to.
The capacity defines the size of the container and must be a positive number (> 0). By default, a container is of unlimited size. You can specify the initial level of the container via init. It must be >= 0 and is 0 by default.
Raise a ValueError if capacity <= 0, init < 0 or init > capacity.
- capacity¶
The maximum capacity of the container.
- level¶
The current level of the container (a number between 0 and capacity).
- put¶
Creates a new ContainerPut event.
alias of ContainerPut
- get¶
Creates a new ContainerGet event.
alias of ContainerGet
- class simpy.resources.container.ContainerPut(container, amount)¶
An event that puts amount into the container. The event is triggered as soon as there’s enough space in the container.
Raise a ValueError if amount <= 0.
- amount = None¶
The amount to be put into the container.
- class simpy.resources.container.ContainerGet(resource, amount)¶
An event that gets amount from the container. The event is triggered as soon as there’s enough content available in the container.
Raise a ValueError if amount <= 0.
- amount = None¶
The amount to be taken out of the container.
simpy.resources.resource – Resource type resources¶
This module contains all Resource like resources.
These resources can be used by a limited number of processes at a time (e.g., a gas station with a limited number of fuel pumps). Processes request these resources to become a user (or to own them) and have to release them once they are done (e.g., vehicles arrive at the gas station, use a fuel-pump, if one is available, and leave when they are done).
Requesting a resources is modeled as “putting a process’ token into the resources” and releasing a resources correspondingly as “getting a process’ token out of the resource”. Thus, calling request()/release() is equivalent to calling put()/get(). Note, that releasing a resource will always succeed immediately, no matter if a process is actually using a resource or not.
Beside Resource, there are a PriorityResource, were processes can define a request priority, and a PreemptiveResource whose resource users can be preempted by other processes with a higher priority.
- class simpy.resources.resource.Resource(env, capacity=1)¶
A resource has a limited number of slots that can be requested by a process.
If all slots are taken, requesters are put into a queue. If a process releases a slot, the next process is popped from the queue and gets one slot.
The env parameter is the Environment instance the resource is bound to.
The capacity defines the number of slots and must be a positive integer.
- queue = None¶
Queue/list of pending Request events that represent processes waiting to use the resource.
- capacity¶
Maximum capacity of the resource.
- count¶
Number of users currently using the resource.
- class simpy.resources.resource.PriorityResource(env, capacity=1)¶
This class works like Resource, but requests are sorted by priority.
The queue is kept sorted by priority in ascending order (a lower value for priority results in a higher priority), so more important request will get the resource earlier.
- PutQueue¶
The type to be used for the put_queue.
alias of SortedQueue
- request¶
Create a new PriorityRequest event.
alias of PriorityRequest
- class simpy.resources.resource.PreemptiveResource(env, capacity=1)¶
This resource mostly works like Resource, but users of the resource can be preempted by higher prioritized requests.
Furthermore, the queue of requests is also sorted by priority.
If a less important request is preempted, the process of that request will receive an Interrupt with a Preempted instance as cause.
- class simpy.resources.resource.Preempted(by, usage_since)¶
- by = None¶
The preempting simpy.events.Process.
- usage_since = None¶
The simulation time at which the preempted process started to use the resource.
- class simpy.resources.resource.Request(resource)¶
Request access on the resource. The event is triggered once access is granted.
If the maximum capacity of users is not reached, the requesting process obtains the resource immediately. If the maximum capacity is reached, the requesting process waits until another process releases the resource.
The request is automatically released when the request was created within a with statement.
- class simpy.resources.resource.Release(resource, request)¶
Releases the access privilege to resource granted by request. This event is triggered immediately.
If there’s another process waiting for the resource, resume it.
If the request was made in a with statement (e.g., with res.request() as req:), this method is automatically called when the with block is left.
- class simpy.resources.resource.PriorityRequest(resource, priority=0, preempt=True)¶
Request the resource with a given priority. If the resource supports preemption and preempted is true other processes with access to the resource may be preempted (see PreemptiveResource for details).
This event type inherits Request and adds some additional attributes needed by PriorityResource and PreemptiveResource
- priority = None¶
The priority of this request. A smaller number means higher priority.
- preempt = None¶
Indicates whether the request should preempt a resource user or not (this flag is not taken into account by PriorityResource).
- time = None¶
The time at which the request was made.
- key = None¶
Key for sorting events. Consists of the priority (lower value is more important) and the time at witch the request was made (earlier requests are more important).
- class simpy.resources.resource.SortedQueue(maxlen=None)¶
Queue that sorts events by their key attribute.
- maxlen = None¶
Maximum length of the queue.
- append(item)¶
Append item to the queue and keep the queue sorted.
Raise a RuntimeError if the queue is full.
simpy.resources.store — Store type resources¶
This module contains all Store like resources.
Stores model the production and consumption of concrete objects. The object type is, by default, not restricted. A single Store can even contain multiple types of objects.
Beside Store, there is a FilterStore that lets you use a custom function to filter the objects you get out of the store.
- class simpy.resources.store.Store(env, capacity=inf)¶
Models the production and consumption of concrete Python objects.
Items put into the store can be of any type. By default, they are put and retrieved from the store in a first-in first-out order.
The env parameter is the Environment instance the container is bound to.
The capacity defines the size of the Store and must be a positive number (> 0). By default, a Store is of unlimited size. A ValueError is raised if the value is negative.
- items = None¶
List of the items within the store.
- capacity¶
The maximum capacity of the store.
- class simpy.resources.store.FilterStore(env, capacity=inf)¶
The FilterStore subclasses Store and allows you to only get items that match a user-defined criteria.
This criteria is defined via a filter function that is passed to get(). get() only considers items for which this function returns True.
Note
In contrast to Store, processes trying to get an item from FilterStore won’t necessarily be processed in the same order that they made the request.
Example: The store is empty. Process 1 tries to get an item of type a, Process 2 an item of type b. Another process puts one item of type b into the store. Though Process 2 made his request after Process 1, it will receive that new item because Process 1 doesn’t want it.
- GetQueue¶
The type to be used for the get_queue.
alias of FilterQueue
- get¶
Create a new FilterStoreGet event.
alias of FilterStoreGet
- class simpy.resources.store.StorePut(resource, item)¶
Put item into the store if possible or wait until it is.
- item = None¶
The item to put into the store.
- class simpy.resources.store.StoreGet(resource)¶
Get an item from the store or wait until one is available.
- class simpy.resources.store.FilterStoreGet(resource, filter=lambda item: True)¶
Get an item from the store for which filter returns True. This event is triggered once such an event is available.
The default filter function returns True for all items, and thus this event exactly behaves like StoreGet.
- filter = None¶
The filter function to use.
- class simpy.resources.store.FilterQueue¶
The queue inherits list and modifies __getitem__() and __bool__() to appears to only contain events for which the store‘s item queue contains proper item.
- __getitem__(key)¶
Get the keyth event from all events that have an item available in the corresponding store’s item queue.
- __bool__()¶
Return True if the queue contains an event for which an item is available in the corresponding store’s item queue.
- __nonzero__()¶
Provided for backwards compatability: __bool__() is only used from Python 3 onwards.
simpy.rt — Real-time simulations¶
Provides an environment whose time passes according to the (scaled) real-time (aka wallclock time).
- class simpy.rt.RealtimeEnvironment(initial_time=0, factor=1.0, strict=True)¶
An Environment which uses the real (e.g. wallclock) time.
A time step will take factor seconds of real time (one second by default); e.g., if you step from 0 until 3 with factor=0.5, the simpy.core.BaseEnvironment.run() call will take at least 1.5 seconds.
If the processing of the events for a time step takes too long, a RuntimeError is raised in step(). You can disable this behavior by setting strict to False.
- factor = None¶
Scaling factor of the real-time.
- strict = None¶
Running mode of the environment. step() will raise a RuntimeError if this is set to True and the processing of events takes too long.
- step()¶
Waits until enough real-time has passed for the next event to happen.
The delay is scaled according to the real-time factor. If the events of a time step are processed too slowly for the given factor and if strict is enabled, a RuntimeError is raised.
simpy.util — Utility functions for SimPy¶
This modules contains various utility functions:
- start_delayed(): Start a process with a given delay.
- subscribe_at(): Receive an interrupt if an event occurs.
- simpy.util.start_delayed(env, generator, delay)¶
Return a helper process that starts another process for generator after a certain delay.
process() starts a process at the current simulation time. This helper allows you to start a process after a delay of delay simulation time units:
>>> from simpy import Environment >>> from simpy.util import start_delayed >>> def my_process(env, x): ... print('%s, %s' % (env.now, x)) ... yield env.timeout(1) ... >>> env = Environment() >>> proc = start_delayed(env, my_process(env, 3), 5) >>> env.run() 5, 3
Raise a ValueError if delay <= 0.
- simpy.util.subscribe_at(event)¶
Register at the event to receive an interrupt when it occurs.
The most common use case for this is to pass a Process to get notified when it terminates.
Raise a RuntimeError if event has already occurred.
About SimPy¶
This sections is all about the non-technical stuff. How did SimPy evolve? Who was responsible for it? And what the heck were they tinking when they made it?
SimPy History & Change Log¶
SimPy was originally based on ideas from Simula and Simscript but uses standard Python. It combines two previous packages, SiPy, in Simula-Style (Klaus Müller) and SimPy, in Simscript style (Tony Vignaux and Chang Chui).
SimPy was based on efficient implementation of co-routines using Python’s generators capability.
SimPy 3 introduced a completely new and easier-to-use API, but still relied on Python’s generators as they proved to work very well.
The package has been hosted on Sourceforge.net since September 15th, 2002. In June 2012, the project moved to Bitbucket.org.
3.0.2 – 2013-10-24¶
- [FIX] The default capacity for Container and FilterStore is now also inf.
3.0.1 – 2013-10-24¶
- [FIX] Documentation and default parameters of Store didn’t match. Its default capacity is now inf.
3.0 – 2013-10-11¶
SimPy 3 has been completely rewritten from scratch. Our main goals were to simplify the API and code base as well as making SimPy more flexible and extensible. Some of the most important changes are:
- Stronger focus on events. Processes yield event instances and are suspended until the event is triggered. An example for an event is a timeout (formerly known as hold), but even processes are now events, too (you can wait until a process terminates).
- Events can be combined with & (and) and | (or) to create condition events.
- Process can now be defined by any generator function. You don’t have to subclass Process anymore.
- No more global simulation state. Every simulation stores its state in an environment which is comparable to the old Simulation class.
- Improved resource system with newly added resource types.
- Removed plotting and GUI capabilities. Pyside and matplotlib are much better with this.
- Greatly improved test suite. Its cleaner, and the tests are shorter and more numerous.
- Completely overhauled documentation.
There is a guide for porting from SimPy 2 to SimPy 3. If you want to stick to SimPy 2 for a while, change your requirements to 'SimPy>=2.3,<3'.
All in all, SimPy has become a framework for asynchronous programming based on coroutines. It brings more than ten years of experience and scientific know-how in the field of event-discrete simulation to the world of asynchronous programming and should thus be a solid foundation for everything based on an event loop.
You can find information about older versions on the history page
2.3.1 – 2012-01-28¶
- [NEW] More improvements on the documentation.
- [FIX] Syntax error in tkconsole.py when installing on Py3.2.
- [FIX] Added mock to the dep. list in SimPy.test().
2.3 – 2011-12-24¶
- [NEW] Support for Python 3.2. Support for Python <= 2.5 has been dropped.
- [NEW] SimPy.test() method to run the tests on the installed version of SimPy.
- [NEW] Tutorials/examples were integrated into the test suite.
- [CHANGE] Even more code clean-up (e.g., removed prints throughout the code, removed if-main-blocks, ...).
- [CHANGE] Many documentation improvements.
2.2 – 2011-09-27¶
- [CHANGE] Restructured package layout to be conform to the Hitchhiker’s Guide to packaging
- [CHANGE] Tests have been ported to pytest.
- [CHANGE] Documentation improvements and clean-ups.
- [FIX] Fixed incorrect behavior of Store._put, thanks to Johannes Koomer for the fix.
2.1 – 2010-06-03¶
- [NEW] A function step has been added to the API. When called, it executes the next scheduled event. (step is actually a method of Simulation.)
- [NEW] Another new function is peek. It returns the time of the next event. By using peek and step together, one can easily write e.g. an interactive program to step through a simulation event by event.
- [NEW] A simple interactive debugger stepping.py has been added. It allows stepping through a simulation, with options to skip to a certain time, skip to the next event of a given process, or viewing the event list.
- [NEW] Versions of the Bank tutorials (documents and programs) using the advanced- [NEW] object-oriented API have been added.
- [NEW] A new document describes tools for gaining insight into and debugging SimPy models.
- [CHANGE] Major re-structuring of SimPy code, resulting in much less SimPy code – great for the maintainers.
- [CHANGE] Checks have been added which test whether entities belong to the same Simulation instance.
- [CHANGE] The Monitor and Tally methods timeAverage and timeVariance now calculate only with the observed time-series. No value is assumed for the period prior to the first observation.
- [CHANGE] Changed class Lister so that circular references between objects no longer lead to stack overflow and crash.
- [FIX] Functions allEventNotices and allEventTimes are working again.
- [FIX] Error messages for methods in SimPy.Lib work again.
2.0.1 – 2009-04-06¶
- [NEW] Tests for real time behavior (testRT_Behavior.py and testRT_Behavior_OO.py in folder SimPy).
- [FIX] Repaired a number of coding errors in several models in the SimPyModels folder.
- [FIX] Repaired SimulationRT.py bug introduced by recoding for the OO API.
- [FIX] Repaired errors in sample programs in documents:
- Simulation with SimPy - In Depth Manual
- SimPy’s Object Oriented API Manual
- Simulation With Real Time Synchronization Manual
- SimPlot Manual
- Publication-quality Plot Production With Matplotlib Manual
2.0.0 – 2009-01-26¶
This is a major release with changes to the SimPy application programming interface (API) and the formatting of the documentation.
API changes¶
In addition to its existing API, SimPy now also has an object oriented API. The additional API
- allows running SimPy in parallel on multiple processors or multi-core CPUs,
- supports better structuring of SimPy programs,
- allows subclassing of class Simulation and thus provides users with the capability of creating new simulation modes/libraries like SimulationTrace, and
- reduces the total amount of SimPy code, thereby making it easier to maintain.
Note that the OO API is in addition to the old API. SimPy 2.0 is fully backward compatible.
Documentation format changes¶
SimPy’s documentation has been restructured and processed by the Sphinx documentation generation tool. This has generated one coherent, well structured document which can be easily browsed. A seach capability is included.
March 2008: Version 1.9.1¶
This is a bug-fix release which cures the following bugs:
- Excessive production of circular garbage, due to a circular reference between Process instances and event notices. This led to large memory requirements.
- Runtime error for preempts of proceeses holding multiple Resource objects.
It also adds a Short Manual, describing only the basic facilities of SimPy.
December 2007: Version 1.9¶
This is a major release with added functionality/new user API calls and bug fixes.
Major changes¶
- The event list handling has been changed to improve the runtime performance of large SimPy models (models with thousands of processes). The use of dictionaries for timestamps has been stopped. Thanks are due to Prof. Norm Matloff and a team of his students who did a study on improving SimPy performance. This was one of their recommendations. Thanks, Norm and guys! Furthermore, in version 1.9 the ‘heapq’ sorting package replaces ‘bisect’. Finally, cancelling events no longer removes them, but rather marks them. When their event time comes, they are ignored. This was Tony Vignaux’ idea!
- The Manual has been edited and given an easier-to-read layout.
- The Bank2 tutorial has been extended by models which use more advanced SimPy commands/constructs.
Bug fixes¶
- The tracing of ‘activate’ statements has been enabled.
Additions¶
- A method returning the time-weighted variance of observations has been added to classes Monitor and Tally.
- A shortcut activation method called “start” has been added to class Process.
January 2007: Version 1.8¶
Major Changes¶
- SimPy 1.8 and future releases will not run under the obsolete Python 2.2 version. They require Python 2.3 or later.
- The Manual has been thoroughly edited, restructured and rewritten. It is now also provided in PDF format.
- The Cheatsheet has been totally rewritten in a tabular format. It is provided in both XLS (MS Excel spreadsheet) and PDF format.
- The version of SimPy.Simulation(RT/Trace/Step) is now accessible by the variable ‘version’.
- The __str__ method of Histogram was changed to return a table format.
Bug fixes¶
- Repaired a bug in yield waituntil runtime code.
- Introduced check for capacity parameter of a Level or a Store being a number > 0.
- Added code so that self.eventsFired gets set correctly after an event fires in a compound yield get/put with a waitevent clause (reneging case).
- Repaired a bug in prettyprinting of Store objects.
Additions¶
- New compound yield statements support time-out or event-based reneging in get and put operations on Store and Level instances.
- yield get on a Store instance can now have a filter function.
- All Monitor and Tally instances are automatically registered in list allMonitors and allTallies, respectively.
- The new function startCollection allows activation of Monitors and Tallies at a specified time.
- A printHistogram method was added to Tally and Monitor which generates a table-form histogram.
- In SimPy.SimulationRT: A function for allowing changing the ratio wall clock time to simulation time has been added.
June 2006: Version 1.7.1¶
This is a maintenance release. The API has not been changed/added to.
- Repair of a bug in the _get methods of Store and Level which could lead to synchronization problems (blocking of producer processes, despite space being available in the buffer).
- Repair of Level __init__ method to allow initialBuffered to be of either float or int type.
- Addition of type test for Level get parameter ‘nrToGet’ to limit it to positive int or float.
- To improve pretty-printed output of ‘Level’ objects, changed attribute ‘_nrBuffered’ to ‘nrBuffered’ (synonym for ‘amount’ property).
- To improve pretty-printed output of ‘Store’ objects, added attribute ‘buffered’ (which refers to ‘_theBuffer’ attribute).
February 2006: Version 1.7¶
This is a major release.
- Addition of an abstract class Buffer, with two sub-classes Store and Level Buffers are used for modelling inter-process synchronization in producer/ consumer and multi-process cooperation scenarios.
- Addition of two new yield statements:
- yield put for putting items into a buffer, and
- yield get for getting items from a buffer.
- The Manual has undergone a major re-write/edit.
- All scripts have been restructured for compatibility with IronPython 1 beta2. This was doen by moving all import statements to the beginning of the scripts. After the removal of the first (shebang) line, all scripts (with the exception of plotting and GUI scripts) can run successfully under this new Python implementation.
September 2005: Version 1.6.1¶
This is a minor release.
- Addition of Tally data collection class as alternative to Monitor. It is intended for collecting very large data sets more efficiently in storage space and time than Monitor.
- Change of Resource to work with Tally (new Resource API is backwards-compatible with 1.6).
- Addition of function setHistogram to class Monitor for initializing histograms.
- New function allEventNotices() for debugging/teaching purposes. It returns a prettyprinted string with event times and names of process instances.
- Addition of function allEventTimes (returns event times of all scheduled events).
15 June 2005: Version 1.6¶
- Addition of two compound yield statement forms to support the modelling of processes reneging from resource queues.
- Addition of two test/demo files showing the use of the new reneging statements.
- Addition of test for prior simulation initialization in method activate().
- Repair of bug in monitoring thw waitQ of a resource when preemption occurs.
- Major restructuring/editing to Manual and Cheatsheet.
1 February 2005: Version 1.5.1¶
MAJOR LICENSE CHANGE:
Starting with this version 1.5.1, SimPy is being release under the GNU Lesser General Public License (LGPL), instead of the GNU GPL. This change has been made to encourage commercial firms to use SimPy in for-profit work.
Minor re-release
No additional/changed functionality
Includes unit test file’MonitorTest.py’ which had been accidentally deleted from 1.5
Provides updated version of ‘Bank.html’ tutorial.
Provides an additional tutorial (‘Bank2.html’) which shows how to use the new synchronization constructs introduced in SimPy 1.5.
More logical, cleaner version numbering in files.
1 December 2004: Version 1.5¶
- No new functionality/API changes relative to 1.5 alpha
- Repaired bug related to waiting/queuing for multiple events
- SimulationRT: Improved synchronization with wallclock time on Unix/Linux
25 September 2004: Version 1.5alpha¶
New functionality/API additions
- SimEvents and signalling synchronization constructs, with ‘yield waitevent’ and ‘yield queueevent’ commands.
- A general “wait until” synchronization construct, with the ‘yield waituntil’ command.
No changes to 1.4.x API, i.e., existing code will work as before.
19 May 2004: Version 1.4.2¶
Sub-release to repair two bugs:
- The unittest for monitored Resource queues does not fail anymore.
- SimulationTrace now works correctly with “yield hold,self” form.
No functional or API changes
29 February 2004: Version 1.4.1¶
Sub-release to repair two bugs:
- The (optional) monitoring of the activeQ in Resource now works correctly.
- The “cellphone.py” example is now implemented correctly.
No functional or API changes
1 February 2004: Version 1.4¶
- Released on SourceForge.net
22 December 2003: Version 1.4 alpha¶
New functionality/API changes
- All classes in the SimPy API are now new style classes, i.e., they inherit from object either directly or indirectly.
- Module Monitor.py has been merged into module Simulation.py and all SimulationXXX.py modules. Import of Simulation or any SimulationXXX module now also imports Monitor.
- Some Monitor methods/attributes have changed. See Manual!
- Monitor now inherits from list.
- A class Histogram has been added to Simulation.py and all SimulationXXX.py modules.
- A module SimulationRT has been added which allows synchronization between simulated and wallclock time.
- A moduleSimulationStep which allows the execution of a simulation model event-by-event, with the facility to execute application code after each event.
- A Tk/Tkinter-based module SimGUI has been added which provides a SimPy GUI framework.
- A Tk/Tkinter-based module SimPlot has been added which provides for plot output from SimPy programs.
22 June 2003: Version 1.3¶
- No functional or API changes
- Reduction of sourcecode linelength in Simulation.py to <= 80 characters
June 2003: Version 1.3 alpha¶
Significantly improved performance
Significant increase in number of quasi-parallel processes SimPy can handle
New functionality/API changes:
- Addition of SimulationTrace, an event trace utility
- Addition of Lister, a prettyprinter for instance attributes
- No API changes
Internal changes:
- Implementation of a proposal by Simon Frost: storing the keys of the event set dictionary in a binary search tree using bisect. Thank you, Simon! SimPy 1.3 is dedicated to you!
Update of Manual to address tracing.
Update of Interfacing doc to address output visualization using Scientific Python gplt package.
29 April 2003: Version 1.2¶
No changes in API.
- Internal changes:
- Defined “True” and “False” in Simulation.py to support Python 2.2.
22 October 2002¶
- Re-release of 0.5 Beta on SourceForge.net to replace corrupted file __init__.py.
- No code changes whatever!
18 October 2002¶
Version 0.5 Beta-release, intended to get testing by application developers and system integrators in preparation of first full (production) release. Released on SourceForge.net on 20 October 2002.
More models
Documentation enhanced by a manual, a tutorial (“The Bank”) and installation instructions.
Major changes to the API:
Introduced ‘simulate(until=0)’ instead of ‘scheduler(till=0)’. Left ‘scheduler()’ in for backward compatibility, but marked as deprecated.
Added attribute “name” to class Process. Process constructor is now:
def __init__(self,name="a_process")
Backward compatible if keyword parameters used.
Changed Resource constructor to:
def __init__(self,capacity=1,name="a_resource",unitName="units")
Backward compatible if keyword parameters used.
27 September 2002¶
- Version 0.2 Alpha-release, intended to attract feedback from users
- Extended list of models
- Upodated documentation
17 September 2002¶
- Version 0.1.2 published on SourceForge; fully working, pre-alpha code
- Implements simulation, shared resources with queuing (FIFO), and monitors for data gathering/analysis.
- Contains basic documentation (cheatsheet) and simulation models for test and demonstration.
Acknowledgments¶
SimPy 2 has been primarily developed by Stefan Scherfke and Ontje Lünsdorf, starting from SimPy 1.9. Their work has resulted in a most elegant combination of an object oriented API with the existing API, maintaining full backward compatibility. It has been quite easy to integrate their product into the existing SimPy code and documentation environment.
Thanks, guys, for this great job! SimPy 2.0 is dedicated to you!
SimPy was originally created by Klaus Müller and Tony Vignaux. They pushed its development for several years and built the Simpy community. Without them, there would be no SimPy 3.
Thanks, guys, for this great job! SimPy 3.0 is dedicated to you!
The many contributions of the SimPy user and developer communities are of course also gratefully acknowledged.
Defense of Design¶
This document explains why SimPy is designed the way it is and how its design evolved over time.
Original Design of SimPy 1¶
SimPy 1 was heavily inspired by Simula 67 and Simscript. The basic entity of the framework was a process. A process described a temporal sequence of actions.
In SimPy 1, you implemented a process by sub-classing Process. The instance of such a subclass carried both, process and simulation internal information, whereat the latter wasn’t of any use to the process itself. The sequence of actions of the process was specified in a method of the subclass, called the process execution method (or PEM in short). A PEM interacted with the simulation by yielding one of several keywords defined in the simulation package.
The simulation itself was executed via module level functions. The simulation state was stored in the global scope. This made it very easy to implement and execute a simulation (despite from heaving to inherit from Process and instantianting the processes before starting their PEMs). However, having all simulation state global makes it hard to parallelize multiple simulations.
SimPy 1 also followed the “batteries included” approach, providing shared resources, monitoring, plotting, GUIs and multiple types of simulations (“normal”, real-time, manual stepping, with tracing).
The following code fragment shows how a simple simulation could be implemented in SimPy 1:
from SimPy.Simulation import Process, hold, initialize, activate, simulate
class MyProcess(Process):
def pem(self, repeat):
for i in range(repeat):
yield hold, self, 1
initialize()
proc = MyProcess()
activate(proc, proc.pem(3))
simulate(until=10)
sim = Simulation()
proc = MyProcess(sim=sim)
sim.activate(proc, proc.pem(3))
sim.simulate(until=10)
Changes in SimPy 2¶
Simpy 2 mostly sticked with Simpy 1’s design, but added an object orient API for the execution of simulations, allowing them to be executed in parallel. Since processes and the simulation state were so closely coupled, you now needed to pass the Simulation instance into your process to “bind” them to that instance. Additionally, you still had to activate the process. If you forgot to pass the simulation instance, the process would use a global instance thereby breaking your program. SimPy 2’s OO-API looked like this:
from SimPy.Simulation import Simulation, Process, hold
class MyProcess(Process):
def pem(self, repeat):
for i in range(repeat):
yield hold, self, 1
sim = Simulation()
proc = MyProcess(sim=sim)
sim.activate(proc, proc.pem(3))
sim.simulate(until=10)
Changes and Decisions in SimPy 3¶
The original goals for SimPy 3 were to simplify and PEP8-ify its API and to clean up and modularize its internals. We knew from the beginning that our goals would not be achievable without breaking backwards compatibility with SimPy 2. However, we didn’t expect the API changes to become as extensive as they ended up to be.
We also removed some of the included batteries, namely SimPy’s plotting and GUI capabilities, since dedicated libraries like matplotlib or PySide do a much better job here.
However, by far the most changes are—from the end user’s view—mostly syntactical. Thus, porting from 2 to 3 usually just means replacing a line of SimPy 2 code with its SimPy3 equivalent (e.g., replacing yield hold, self, 1 with yield env.timeout(1)).
In short, the most notable changes in SimPy 3 are:
- No more sub-classing of Process required. PEMs can even be simple module level functions.
- The simulation state is now stored in an Environment which can also be used by a PEM to interact with the simulation.
- PEMs now yield event objects. This implicates interesting new features and allows an easy extension with new event types.
These changes are causing the above example to now look like this:
from simpy import Environment, simulate
def pem(env, repeat):
for i in range(repeat):
yield env.timeout(i)
env = Environment()
env.process(pem(env, 7))
simulate(env, until=10)
The following sections describe these changes in detail:
No More Sub-classing of Process¶
In Simpy 3, every Python generator can be used as a PEM, no matter if it is a module level function or a method of an object. This reduces the amount of code required for simple processes. The Process class still exists, but you don’t need to instantiate it by yourself, though. More on that later.
Processes Live in an Environment¶
Process and simulation state are decoupled. An Environment holds the simulation state and serves as base API for processes to create new events. This allows you to implement advanced use cases by extending the Process or Environment class without affecting other components.
For the same reason, the simulate() method now is a module level function that takes an environment to simulate.
Stronger Focus on Events¶
In former versions, PEMs needed to yield one of SimPy’s built-in keywords (like hold) to interact with the simulation. These keywords had to be imported separately and were bound to some internal functions that were tightly integrated with the Simulation and Process making it very hard to extend SimPy with new functionality.
In Simpy 3, PEMs just need to yield events. There are various built-in event types, but you can also create custom ones by making a subclass of a BaseEvent. Most events are generated by factory methods of Environment. For example, Environment.timeout() creates a Timeout event that replaces the hold keyword.
The Process is now also an event. You can now yield another process and wait for it to finish. For example, think of a car-wash simulation were “washing” is a process that the car processes can wait for once they enter the washing station.
Creating Events via the Environment or Resources¶
The Environment and resources have methods to create new events, e.g. Environment.timeout() or Resource.request(). Each of these methods maps to a certain event type. It creates a new instance of it and returns it, e.g.:
def event(self):
return Event()
To simplify things, we wanted to use the event classes directly as methods:
class Environment(object)
event = Event
This was, unfortunately, not directly possible and we had to wrap the classes to behave like bound methods. Therefore, we introduced a BoundClass:
class BoundClass(object):
"""Allows classes to behave like methods. The ``__get__()`` descriptor
is basically identical to ``function.__get__()`` and binds the first
argument of the ``cls`` to the descriptor instance.
"""
def __init__(self, cls):
self.cls = cls
def __get__(self, obj, type=None):
if obj is None:
return self.cls
return types.MethodType(self.cls, obj)
class Environment(object):
event = BoundClass(Event)
These methods are called a lot, so we added the event classes as types.MethodType to the instance of Environment (or the resources, respectively):
class Environment(object):
def __init__(self):
self.event = types.MethodType(Event, self)
It turned out the the class attributes (the BoundClass instances) were now quite useless, so we removed them allthough it was actually the “right” way to to add classes as methods to another class.
Release Process¶
This process describes the steps to execute in order to release a new version of SimPy.
Preparations¶
Close all tickets for the next version.
Update the minium required versions of dependencies in setup.py. Update the exact version of all entries in requirements.txt.
Run tox from the project root. All tests for all supported versions must pass:
$ tox [...] ________ summary ________ py27: commands succeeded py32: commands succeeded py33: commands succeeded pypy: commands succeeded congratulations :)
Note
Tox will use the requirements.txt to setup the venvs, so make sure you’ve updated it!
Build the docs (HTML is enough). Make sure there are no errors and undefined references.
$ cd docs/ $ make clean html $ cd ..
Check if all authors are listed in AUTHORS.txt.
Update the change logs (CHANGES.txt and docs/about/history.rst). Only keep changes for the current major release in CHANGES.txt and reference the history page from there.
Commit all changes:
$ hg ci -m 'Updated change log for the upcoming release.'
Write a draft for the announcement mail with a list of changes, acknowledgements and installation instructions. Everyone in the team should agree with it.
Build and release¶
Test the release process. Build a source distribution and a wheel package and test them:
$ python setup.py sdist $ python setup.py bdist_wheel $ ls dist/ simpy-a.b.c-py2.py3-none-any.whl simpy-a.b.c.tar.gz
Try installing them:
$ rm -rf /tmp/simpy-sdist # ensure clean state if ran repeatedly $ virtualenv /tmp/simpy-sdist $ /tmp/simpy-sdist/bin/pip install pytest $ /tmp/simpy-sdist/bin/pip install --no-index dist/simpy-a.b.c.tar.gz $ /tmp/simpy-sdist/bin/python >>> import simpy >>> simpy.__version__ # doctest: +SKIP 'a.b.c' >>> simpy.test() # doctest: +SKIP
and
$ rm -rf /tmp/simpy-wheel # ensure clean state if ran repeatedly $ virtualenv /tmp/simpy-wheel $ /tmp/simpy-wheel/bin/pip install pytest $ /tmp/simpy-wheel/bin/pip install --use-wheel --no-index --find-links dist simpy $ /tmp/simpy-wheel/bin/python >>> import simpy # doctest: +SKIP >>> simpy.__version__ # doctest: +SKIP 'a.b.c' >>> simpy.test() # doctest: +SKIP
Create or check your accounts for the test server <https://testpypi.python.org/pypi> and PyPI. Update your ~/.pypirc with your current credentials:
[distutils] index-servers = pypi test [test] repository = https://testpypi.python.org/pypi username = <your test user name goes here> password = <your test password goes here> [pypi] repository = http://pypi.python.org/pypi username = <your production user name goes here> password = <your production password goes here>
Register SimPy with the test server and upload the distributions:
$ python setup.py register -r test $ python setup.py sdist upload -r test $ python setup.py bdist_wheel upload -r test
Check if the package is displayed correctly: https://testpypi.python.org/pypi/simpy
Test the installation again:
$ pip install -i https://testpypi.python.org/pypi simpy
Update the version number in simpy/__init__.py, commit and create a tag:
$ hg ci -m 'Bump version from a.b.c to x.y.z' $ hg tag x.y.z $ hg push ssh://hg@bitbucket.org/simpy/simpy
Finally upload the package to PyPI and test its installation one last time:
$ python setup.py register $ python setup.py sdist upload $ python setup.py bdist_wheel upload $ pip install simpy
Check if the package is displayed correctly: https://pypi.python.org/pypi/simpy
Activate the documentation build for the new version.
Post release¶
- Send the prepared email to the mailing list and post it on Google+.
- Update Wikipedia entries.
- Update Python Wiki
- Post something to Planet Python (e.g., via Stefan’s blog).