+ :platform: Mac, Unix, Windows
+ :synopsis: Monitor SimPy simulations.
+.. moduleauthor:: Ontje Lünsdorf
+.. moduleauthor:: Stefan Scherfke
+This module contains the proposal for a new approach for monitoring SimPy
+ """This class can monitor the values of a given set of variables.
+ Each variable is described by a *name* and a *collector function*. The
+ list of all variable values is accessible as attribute of a monitor
+ instance as well as via an index (defined by the order you passed them to
+ ``__init__``). Besides *(name, func)* tuples you can also pass nested
+ lists of those tuples, which is helpful for some convenience functions.
+ All collectors will be called (in the same order as they were specified)
+ each time a ``Monitor`` instance is called. The collector functions may
+ either grab the desired values by themselves or let them be passed
+ manually with each monitor call.
+ Here is an example how this works:
+ >>> class Spam(object):
+ >>> m = Monitor(('a', lambda: getattr(spam, 'a')), ('b', lambda x: x))
+ >>> m.a == m, m.b == m
+ >>> m # Monitor is a tuple with nested lists by itself.
+ >>> m.a, m.b # You can also access it's elements by their name.
+ In this example, ``spam`` is the object to be monitored. The monitor is
+ configured to observe two variables named “a” and “b”. The collector for
+ “a” automatically retrieves it’s value via ``getattr()`` while the value
+ for “b” needs to be passed to the monitor manually as keyword argument.
+ For these common cases, there are the shortcuts :func:`get` and
+ Note that names for data series need to be unique. ``Monitor``
+ instanciation will raise a ``ValueError`` if there's a duplicate name.
+ def __new__(cls, *args):
+ # We need to override __new__ instead of __init__ because tuples are
+ # immutable. This means it's data is set during the allocation phase in
+ raise AttributeError('Nothing to be monitored.')
+ # Parse arguments using a helper function. This method will populate the
+ names, collectors, series = , , 
+ """Method for parsing collector descriptions.
+ ``arg`` can be ('name', func) or (('name1', func),
+ raise TypeError('%s must be an instance of '
+ '"collections.Iterable", but %s is not.' %
+ # Check if this is a collector description. These look like this:
+ # ('name', func). Otherwise treat arg as a nested list of collector
+ if len(arg) == 2 and isinstance(arg, basestring) and \
+ 'There\'s already a series named "%s".' % arg)
+ # This is a nested list of collector descriptions. Recurse.
+ # Allocate the tuple instance and set the empty series lists as data.
+ instance = tuple.__new__(cls, series)
+ # Set the names and collectors of each series.
+ instance.__names = tuple(names)
+ instance.__collectors = tuple(collectors)
+ # Link attributes to series.
+ for name, data in zip(names, series):
+ setattr(instance, name, data)
+ def __call__(self, **kwargs):
+ for series, name, col in zip(self, self.__names, self.__collectors):
+ """Helper function for :func:`get`. Returns a function which will return
+ ``attr`` of ``obj`` upon calling.
+ return lambda: getattr(obj, attr)
+def get(obj, *attributes):
+ """This is a shortcut that creates lambda functions for several attributes
+ of an object. All functions will automatically get the attribute’s value
+ each time they are called.
+ The results of get can be directly passed into a monitor.
+ >>> from collections import namedtuple
+ >>> spam = namedtuple('Spam', ['a', 'b'])(1, 2)
+ >>> get(spam, 'a', 'b')
+ (('a', lambda: getattr(obj, attr)), ('b', lambda: getattr(obj, attr)))
+ >>> m = Monitor(get(spam, 'a', 'b'))
+ >>> m() # Collect values.
+ return tuple((name, __get(obj, name)) for name in attributes)
+ """This a simple shortcut for a function like ``lambda x: x`` if you want
+ to pass variable values manually to a monitor:
+ >>> m = Monitor(('a', manual))
+if __name__ == '__main__':
+ import matplotlib.pyplot as plt
+ from numpy import array, float64
+ from SimPy.Simulation import Simulation, Process, hold
+ """A simple example process that illustrates the usage of the new
+ monitoring approach."""
+ def __init__(self, sim):
+ Process.__init__(self, sim=sim)
+ # Create a monitor that will collect:
+ # time: A list with timestampes from sim.now()
+ # diff: A value passed manually
+ # square: Square of self.c
+ self.monitor = Monitor(
+ ('square', lambda: self.c ** 2)
+ self.a += random.random()
+ self.b += random.randint(1, 2)
+ self.c += random.randint(2, 4)
+ sim.activate(proc, proc.run())
+ # Just a test and a simple example how to acces the monitored values:
+ assert proc.monitor == proc.monitor.a
+ print zip(*proc.monitor)
+ # NumPy helps you with the statistics and other calculations.
+ # Note: specification of dtype gives you a massive speed-up!
+ a = array(proc.monitor.a, dtype=float64)
+ # NumPy: average, std. deviation, variance
+ print 'a stats:', a.mean(), a.std(), a.var()
+ # This one creates a multi-dimensional array (see the output)
+ np_mon = array(proc.monitor, dtype=float64)
+ print 't, a, b, diff, square:\n', np_mon
+ # Get the average of all monitored proc.b
+ print 'b stats:', np_mon.mean()
+ # Get the std. deviation of all monitored proc.c
+ print 'c stats:', np_mon.std()
+ # Matplotlib plots your data:
+ # Either directly from a monitor ...
+ plt.plot(proc.monitor.time, proc.monitor.a, 'b')
+ # ... or the NumPy array
+ plt.plot(np_mon, np_mon, 'r')