Stefan Scherfke

SimPy: Events

Posted on Saturday, April 05, 2014

It’s been a while since the last SimPy
guide, but I’ve been quite busy with developing and Open Sourcing mosaik
2.

“What’s SimPy again?”SimPy is
a process-based discrete-event simulation framework based on standard Python.
Its event dispatcher is based on Python’s generators and can also be used for
asynchronous networking or to
implement multi-agent systems (with both, simulated and real communication).

This is the set of basic events. Events are extensible and resources, for
example, define additional events. In this guide, we’ll focus on the events in
the simpy.events module.

Event basics

SimPy events are very similar – if not identical — to deferreds, futures or
promises. Instances of the class Event
are used to describe any kind of events. Events can be in one of the following
states. An event

might happen (not triggered),

is going to happen (triggered) or

has happened (processed).

They traverse these states exactly once in that order. Events are also tightly
bound to time and time causes events to advance their state.

Initially, events are not triggered and just objects in memory.

If an event gets triggered, it is scheduled at a given time and inserted into
SimPy’s event queue. The property Event.triggered
becomes True.

As long as the event is not processed, you can add callbacks to an event.
Callbacks are callables that accept an event as parameter and are stored in the
Event.callbacks list.

An event becomes processed when SimPy pops it from the event queue and calls
all of its callbacks. It is now no longer possible to add callbacks. The
property Event.processed
becomes True.

Events also have a value. The value can be set before or when the event is
triggered and can be retrieved via Event.value
or, within a process, by yielding the event (value = yield event).

Adding callbacks to an event

“What? Callbacks? I’ve never seen no callbacks!”, you might think if you have
worked your way through the tutorial.

That’s on purpose. The most common way to add a callback to an event is
yielding it from your process function (yield event). This will add the
process’ _resume() method as a callback. That’s how your process gets resumed
when it yielded an event.

However, you can add any callable object (function) to the list of callbacks
as long as it accepts an event instance as its single parameter:

>>>importsimpy>>>>>>defmy_callback(event):...print('Called back from',event)...>>>env=simpy.Environment()>>>event=env.event()>>>event.callbacks.append(my_callback)>>>event.callbacks[<functionmy_callbackat0x...>]

If an event has been processed, all of its Event.callbacks
have been executed and the attribute is set to None. This is to prevent you
from adding more callbacks – these would of course never get called because the
event has already happened.

Processes are smart about this, though. If you yield a processed event,
_resume() will immediately resume your process with the value of the event
(because there is nothing to wait for).

Triggering events

When events are triggered, they can either succeed or fail. For example, if
an event is to be triggered at the end of a computation and everything works
out fine, the event will succeed. If an exceptions occurs during that
computation, the event will fail.

To trigger an event and mark it as successful, you can use
Event.succeed(value=None). You can optionally pass a value to it (e.g.,
the results of a computation).

To trigger an event and mark it as failed, call Event.fail(exception)
and pass an Exception instance to it (e.g., the exception you caught
during your failed computation).

There is also a generic way to trigger an event: Event.trigger(event).
This will take the value and outcome (success or failure) of the event passed
to it.

All three methods return the event instance they are bound to. This allows you
to do things like yield Event(env).succeed().

Example usages for Event

The simple mechanics outlined above provide a great flexibility in the way
events (even the basic Event)
can be used.

One example for this is that events can be shared. They can be created by a
process or outside of the context of a process. They can be passed to other
processes and chained:

This can also be used like the passivate / reactivate known from SimPy 2.
The pupils passivate when class begins and are reactivated when the bell rings.

Let time pass by: the Timeout

To actually let time pass in a simulation, there is the timeout event.
A timeout has two parameters: a delay and an optional value:
Timeout(delay, value=None). It triggers itself during its creation and
schedules itself at now + delay. Thus, the succeed() and fail()
methods cannot be called again and you have to pass the event value to it when
you create the timeout.

The delay can be any kind of number, usually an int or float as long as it
supports comparison and addition.

Processes are events, too

SimPy processes (as created by Process
or env.process()) have the nice property of being events, too.

That means, that a process can yield another process. It will then be resumed
when the other process ends. The event’s value will be the return value of that process:

The example above will only work in Python >= 3.3. As a workaround for older
Python versions, you can use env.exit(23) with the same effect.

When a process is created, it schedules an Initialize
event which will start the execution of the process when triggered. You usually
won’t have to deal with this type of event.

If you don’t want a process to start immediately but after a certain delay, you
can use simpy.util.start_delayed().
This method returns a helper process that uses a timeout before actually
starting a process.

Waiting for for multiple events at once

Sometimes, you want to wait for more than one event at the same time. For
example, you may want to wait for a resource, but not for an unlimited amount
of time. Or you may want to wait until all of a number of events
have happened.

Both take a list of events as an argument and are triggered if at least one
of them is triggered or all of them:

>>>fromsimpy.eventsimportAnyOf,AllOf,Event>>>events=[Event(env)foriinrange(3)]>>>a=AnyOf(env,events)# Triggers if at least one of "events" is triggered.>>>b=AllOf(env,events)# Triggers if all each of "events" is triggered.

The value of a condition event is a dictionary with an entry for every
triggered event. In the case of AllOf, the size of that dictionary will
always be the same as the length of the event list. The value dict of AnyOf
will have at least one entry. In both cases, the event instances are used as
keys and the event values will be the values.

As a shorthand for AllOf and AnyOf, you can also use the logical
operators & (and) and | (or):

>>>deftest_condition(env):...t1,t2=env.timeout(1,value='spam'),env.timeout(2,value='eggs')...ret=yieldt1|t2...assertret=={t1:'spam'}......t1,t2=env.timeout(1,value='spam'),env.timeout(2,value='eggs')...ret=yieldt1&t2...assertret=={t1:'spam',t2:'eggs'}......# You can also concatenate & and |...e1,e2,e3=[env.timeout(i)foriinrange(3)]...yield(e1|e2)&e3...assertall(e.triggeredforein[e1,e2,e3])...>>>proc=env.process(test_condition(env))>>>env.run()

That’s it. I’ve already started with the next guide – about process interaction
– so hopefully it won’t take to long until publish it this time.