Commit 2ca6158e authored by Stefan Scherfke's avatar Stefan Scherfke

Moved all events from simpy.core to simpy.events and adjusted the tests and docs.

parent 587eedfb
......@@ -11,6 +11,7 @@ components.
simpy
simpy.core
simpy.events
simpy.monitoring
simpy.resources
simpy.resources.base
......
......@@ -4,56 +4,57 @@
.. automodule:: simpy.core
Environments
============
.. autoclass:: BaseEnvironment
:members:
.. autoclass:: Environment
:members:
:inherited-members:
.. autoattribute:: now
.. autoattribute:: active_process
Events
======
.. method:: start(generator)
.. autoclass:: Event(env, value=PENDING, name=None)
:members:
Start a new process from *generator*. Alias of :meth:`process`.
.. autoclass:: Process
:members:
.. method:: process(generator)
.. autoclass:: Timeout
:members:
Return a new :class:`~simpy.events.Process` instance for *generator*.
.. autoclass:: Condition
:members:
.. method:: timeout(delay, value=None)
.. autoclass:: AllOf
:members:
Return a new :class:`~simpy.events.Timeout` event with a *delay* and,
optionally, a *value*.
.. autoclass:: AnyOf
:members:
.. autoclass:: Initialize
:members:
.. method:: suspend()
Alias of :meth:`event()`. Yielding this event suspends a process until
another process triggers the event.
.. method:: event()
Return a new :class:`~simpy.events.Event` instance.
.. method:: all_of(events)
Miscellaneous (Interrupt and constants)
=======================================
Return a new :class:`~simpy.events.AllOf` condition for a list of
*events*.
.. method:: any_of(events)
Return a new :class:`~simpy.events.AnyOf` condition for a list of
*events*.
.. automethod:: exit
.. automethod:: schedule
.. automethod:: peek
.. automethod:: step
.. automethod:: run
.. autoclass:: BoundClass
:members:
.. autoclass:: EmptySchedule
.. autoclass:: Interrupt
:members: cause
.. autodata:: Infinity
.. autodata:: PENDING
.. autodata:: HIGH_PRIORITY
.. autodata:: DEFAULT_PRIORITY
.. autodata:: LOW_PRIORITY
=====================================
``simpy.events`` --- Core event types
=====================================
.. automodule:: simpy.events
.. autoclass:: Event
:members:
.. autoclass:: Process
:members:
.. autoclass:: Timeout
:members:
.. autoclass:: Condition
:members:
.. autoclass:: AllOf
:members:
.. autoclass:: AnyOf
:members:
.. autoclass:: Initialize
:members:
.. autoclass:: Interrupt
:members: cause
.. autodata:: PENDING
.. autodata:: HIGH_PRIORITY
.. autodata:: DEFAULT_PRIORITY
.. autodata:: LOW_PRIORITY
......@@ -3,4 +3,5 @@ SimPy home
==========
.. _templates/index.html contains the content for this page.
..
_templates/index.html contains the content for this page.
......@@ -22,12 +22,12 @@ 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 :class:`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 :class:`Timeout`
and all other events can be created by calling the appropriate method of the
:class:`Environment` that the process lives in (:meth:`Environment.timeout()`
for example).
An important event type is the :class:`~simpy.events.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 :class:`~simpy.events.Timeout` and all other events can be created by calling
the appropriate method of the :class:`Environment` that the process lives in
(:meth:`Environment.timeout()` for example).
Our First Process
......@@ -59,10 +59,10 @@ 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 :attr:`Environment.now` property). It then calls the
:meth:`Environment.timeout()` factory function to create a :class:`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.
:meth:`Environment.timeout()` factory function to create
a :class:`~simpy.events.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::
......@@ -86,9 +86,9 @@ a *process generator* that needs to be started and added to the environment via
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 :class:`Process` returned by :meth:`~Environment.start()` can be used for
process interactions (we will cover that in the next section, so we will ignore
it for now).
The :class:`~simpy.events.Process` returned by :meth:`~Environment.start()` 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 meth:`Environment.simulate()` and
passing the environment as well as an end time to it.
......
......@@ -4,20 +4,20 @@ Process Interaction
.. currentmodule:: simpy.core
The :class:`Process` instance that is returned by :meth:`Environment.start()`
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.
The :class:`~simpy.events.Process` instance that is returned by
:meth:`Environment.start()` 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 :class:`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.
As it happens, a SimPy :class:`~simpy.events.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
......@@ -30,8 +30,9 @@ Therefore, we refactor our car to be a class with two process methods:
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 :class:`Process` instance that :meth:`Environment.start()`
returns, the ``run`` process starts waiting for it to finish::
yielding the :class:`~simpy.events.Process` instance that
:meth:`Environment.start()` returns, the ``run`` process starts waiting for it
to finish::
>>> class Car(object):
... def __init__(self, env):
......@@ -79,7 +80,7 @@ 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
:meth:`~Process.interrupt()` method::
:meth:`~simpy.events.Process.interrupt()` method::
>>> def driver(env, car):
... yield env.timeout(3)
......@@ -88,10 +89,10 @@ SimPy allows you to interrupt a running process by calling its
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 :exc:`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)::
Interrupts are thrown into process functions as :exc:`~simpy.events.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):
......@@ -138,8 +139,8 @@ 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
:doc:`../topical_guides/index` or the :class:`Process` API reference for more
details.
:doc:`../topical_guides/index` or the :class:`~simpy.events.Process` API
reference for more details.
In the :doc:`next section <shared_resources>` we will cover the basic usage of
shared resources.
......@@ -22,8 +22,9 @@ simulation (``SimPy.Simulation``), a real-time simulation
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 :class:`~simpy.core.Process` and the SimPy keywords
anymore). In most use cases you will now only need to import :mod:`simpy`.
don't need direct access to :class:`~simpy.events.Process` and the SimPy
keywords anymore). In most use cases you will now only need to import
:mod:`simpy`.
**SimPy 2**
......@@ -176,7 +177,7 @@ additional parameters (at least ``self``). These keywords had to be import from
to a function that generated the according event.
SimPy 3 directly exposes these event-generating functions via the
:class:`~simpy.core.Environment`, :class:`~simpy.core.Process` or resource
:class:`~simpy.core.Environment`, :class:`~simpy.events.Process` or resource
types, depending on were they make most sense. You don't need to import
something separately anymore.
......@@ -240,8 +241,8 @@ 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 :meth:`~simpy.core.Process.interrupt()` on the victim
process. You can optionally pass a cause. An :exc:`~simpy.core.Interrupt` is
In SimPy 3, you call :meth:`~simpy.events.Process.interrupt()` on the victim
process. You can optionally pass a cause. An :exc:`~simpy.events.Interrupt` is
then thrown into the victim process, which has to handle the interrupt via
``try: ... except Interrupt: ...``.
......
......@@ -12,9 +12,9 @@ Core classes and functions
- :class:`Environment`: SimPy's central class. It contains
the simulation's state and lets the PEMs interact with it (i.e.,
schedule events).
- :class:`Process`: This class represents a process function while
it is executed in an environment. An instance of it is returned by
:meth:`Environment.start()`. It inherits :class:`Event`.
.. currentmodule:: simpy.events
- :class:`Interrupt`: This exception is thrown into a process if it gets
interrupted by another one.
......@@ -66,7 +66,8 @@ Other
.. - :func:`test`: Run the test suite on the installed copy of Simpy.
"""
from simpy.core import Environment, Interrupt, Process
from simpy.core import Environment
from simpy.events import Interrupt
from simpy.resources.resource import (
Resource, PriorityResource, PreemptiveResource)
from simpy.resources.container import Container
......@@ -75,7 +76,8 @@ from simpy.resources.store import Store, FilterStore
__all__ = [
'test',
'Environment', 'Interrupt', 'Process',
'Environment',
'Interrupt',
'Resource', 'PriorityResource', 'PreemptiveResource',
'Container',
'Store', 'FilterStore',
......
This diff is collapsed.
"""
This *events* module contains the various event type used by the SimPy core.
The base class for all events is :class:`Event`. Though it can be directly
used, there are several specialized subclasses of it:
- :class:`Timeout`: is scheduled with a certain delay and lets processes hold
their state for a certain amount of time.
- :class:`Initialize`: Initializes a new :class:`Process`.
- :class:`Process`: Processes are also modeled as an event so other processes
can wait until another one finishes.
- :class:`Condition`: Events can be concatenated with ``|`` an ``&`` to either
wait until one or both of the events are triggered.
- :class:`AllOf`: Special case of :class:`Condition`; wait until a list of
events has been triggered.
- :class:`AnyOf`: Special case of :class:`Condition`; wait until one of a list
of events has been triggered.
This module also defines the :exc:`Interrupt` exception.
"""
from inspect import isgenerator
from simpy._compat import PY2
if PY2:
import sys
PENDING = object() #: Unique object to identify pending values of events
HIGH_PRIORITY = 0 #: Priority of interrupts and Initialize events
DEFAULT_PRIORITY = 1 #: Default priority used by events
LOW_PRIORITY = 2 #: Priority of timeouts
class Event(object):
"""Base class for all events.
Every event is bound to an environment *env* (see
:class:`~simpy.core.BaseEnvironment`) and has an optional *value*.
An event has a list of :attr:`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 :class:`Condition` event is generated that lets you wait for both or one
of them.
"""
def __init__(self, env):
self.env = env
"""The :class:`~simpy.core.Environment` the event lives in."""
self.callbacks = []
"""List of functions that are called when the event is
processed."""
self._value = PENDING
def __repr__(self):
"""Return the description of the event (see :meth:`_desc`) with the id
of the event."""
return '<%s object at 0x%x>' % (self._desc(), id(self))
def _desc(self):
"""Return a string *Event()*."""
return '%s()' % self.__class__.__name__
@property
def triggered(self):
"""Becomes ``True`` if the event has been triggered and its callbacks
are about to be invoked."""
return self._value is not PENDING
@property
def processed(self):
"""Becomes ``True`` if the event has been processed (e.g., its
callbacks have been invoked)."""
return self.callbacks is None
@property
def value(self):
"""The value of the event if it is available.
The value is available when the event has been triggered.
Raise a :exc:`RuntimeError` if the value is not yet available.
"""
if self._value is PENDING:
raise RuntimeError('Value of %s is not yet available' % self)
return self._value
def trigger(self, event):
"""Triggers the event with the state and value of the provided *event*.
This method can be used directly as a callback function.
"""
self.ok = event.ok
self._value = event._value
self.env.schedule(self, DEFAULT_PRIORITY)
def succeed(self, 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 :exc:`RuntimeError` if this event has already been scheduled.
"""
if self._value is not PENDING:
raise RuntimeError('%s has already been triggered' % self)
self.ok = True
self._value = value
self.env.schedule(self, DEFAULT_PRIORITY)
return self
def fail(self, 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 :exc:`ValueError` if ``exception`` is not an :exc:`Exception`.
Raise a :exc:`RuntimeError` if this event has already been scheduled.
"""
if self._value is not PENDING:
raise RuntimeError('%s has already been triggered' % self)
if not isinstance(exception, Exception):
raise ValueError('%s is not an exception.' % exception)
self.ok = False
self._value = exception
self.env.schedule(self, DEFAULT_PRIORITY)
return self
def __and__(self, other):
"""Return ``True`` if this event and *other* are triggered."""
return Condition(self.env, Condition.all_events, [self, other])
def __or__(self, other):
"""Return ``True`` if this event or *other is triggered, or both."""
return Condition(self.env, Condition.any_events, [self, other])
class Timeout(Event):
"""An :class:`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 :class:`Event`) no *success()* or *fail()* method.
"""
def __init__(self, env, delay, value=None):
if delay < 0:
raise ValueError('Negative delay %s' % delay)
# NOTE: The following initialization code is inlined from
# Event.__init__() for performance reasons.
self.env = env
self.callbacks = []
self._value = value
# NOTE: The succeed() call is inlined for performance reasons.
self._delay = delay
self.ok = True
env.schedule(self, LOW_PRIORITY, delay)
def _desc(self):
"""Return a string *Timeout(delay[, value=value])*."""
return '%s(%s%s)' % (self.__class__.__name__, self._delay,
'' if self._value is None else
(', value=%s' % self._value))
class Initialize(Event):
"""Initializes a process. Only used internally by :class:`Process`."""
def __init__(self, env, process):
# NOTE: The following initialization code is inlined from
# Event.__init__() for performance reasons.
self.env = env
self.callbacks = [process._resume]
self._value = None
# Note: The succeed() call is inlined for performance reasons.
self.ok = True
env.schedule(self, HIGH_PRIORITY)
class Process(Event):
"""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 :class:`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
:meth:`simpy.core.Environment.start()`.
"""
def __init__(self, env, generator):
if not isgenerator(generator):
raise ValueError('%s is not a generator.' % generator)
# NOTE: The following initialization code is inlined from
# Event.__init__() for performance reasons.
self.env = env
self.callbacks = []
self._value = PENDING
self._generator = generator
# Schedule the start of the execution of the process.
self._target = Initialize(env, self)
def _desc(self):
"""Return a string *Process(process_func_name)*."""
return '%s(%s)' % (self.__class__.__name__, self._generator.__name__)
@property
def target(self):
"""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.
"""
return self._target
@property
def is_alive(self):
"""``True`` until the process generator exits."""
return self._value is PENDING
def interrupt(self, 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 :exc:`RuntimeError` in these
cases.
"""
if self._value is not PENDING:
raise RuntimeError('%s has terminated and cannot be interrupted.' %
self)
if self is self.env.active_process:
raise RuntimeError('A process is not allowed to interrupt itself.')
# Schedule interrupt event
# NOTE: The succeed() call is inline for performance reasons and to
# set a HIGH_PRIORITY.
event = self.env.event()
event._value = Interrupt(cause)
event.ok = False
# Interrupts do not cause the simulation to crash.
event.defused = True
event.callbacks.append(self._resume)
self.env.schedule(event, HIGH_PRIORITY)
def _resume(self, event):
"""Resume the execution of the process.
Send the result of the event the process was waiting for into the
process generator and retrieve a new event from it. Register this
method as callback for that event.
If the process generator exits or raises an exception, terminate this
process. Also schedule this process to notify all registered callbacks,
that the process terminated.
"""
# Ignore dead processes. Multiple concurrently scheduled interrupts
# cause this situation. If the process dies while handling the first
# one, the remaining interrupts must be discarded.
if self._value is not PENDING:
return
# If the current target (e.g. an interrupt) isn't the one the process
# expects, remove it from the original events joiners list.
if self._target is not event:
self._target.callbacks.remove(self._resume)