Dynamics: Process dynamics over networks

class epydemic.Dynamics(p, g=None)

Bases: NetworkExperiment

An abstract simulation framework for running a process over a network.

This is the abstract base class for implementing different kinds of network process dynamics as computational experiments suitable for running under epyc. Sub-classes provide synchronous and stochastic (Gillespie) simulation dynamics.

The dynamics runs a network process provided as a Process object. It is provided with a network generator that is called to generate a new experimental network instance for each run. The generator can be any iterator but will typically be an instance of NetworkGenerator.

The dynamics also provides the interface for Event taps, allowing external code to tap-into the changes the experiment makes to the network. Sub-classes need to insert calls to this interface as appropriate, notably around the main body of the simulation and at each significant change.

Parameters:
  • p (Process) – network process to run

  • g (Union[Graph, NetworkGenerator, None]) – (optional) prototype network or network generator

In use

A Dynamics object is an instance of NetworkExperiment which is turn an epyc experiment, and hence can be run either stand-alone or as part of a larger planned experimental protocol. Each simulation is parameterised by a dict providing the parameters used to condition the simulation, typically providing event probabilities for the various events that may happen.

Note

Simulations don’t use Dynamics objects directly, but instead use a sub-class. See Simulation dynamics for an explanation of the differences between approaches.

In stand-alone mode, a simulation is run by calling the run() method (inherited from epyc.Experiment), passing a dict of parameters. The network dynamics then performs a single simulation according to the following process:

This decomposition is very flexible. At its simplest, a dynamics takes a fixed prototype network as a parameter to its construction and copies it for every run. More complex use cases supply an instance of NetworkGenerator that samples from a class of random networks defined by the experimental parameters.

Note

In versions of epydemic prior to 1.13.1 the working network was discarded as part of tear-down, making it inaccessible once the experimental run had ended.

Starting with version 1.13.1, this behaviour was changed so that the working network is retained until the next experiment is performed, whereupon it is discarded and a new working network is created.

This change simplifies using epydemic at a small scale, since one can directly see the network that exists after an experimental run without having to explicitly save it. It makes no difference beyond this.

Note the division of labour. A Dynamics object provides the scheduling for events, which are themselves specified and defined in a Process object. There is seldom any need to interact directly with a Dynamics object other than through its execution interface.

Attributes

Dynamics.TIME: Final[str] = 'epydemic.monitor.time'

Metadata element holding the logical simulation end-time.

Dynamics.EVENTS: Final[str] = 'epydemic.monitor.events'

Metadata element holding the number of events that happened.

Configuring the simulation

A Dynamics object runs the process it describes over a network. It also maintains the simulation time as the simulation progresses.

Dynamics.process()

Return the network process being run.

Return type:

Process

Returns:

the process

Dynamics.currentSimulationTime()

Return the current simulation time.

Return type:

float

Returns:

the current time

Dynamics.setCurrentSimulationTime(t)

Set the current simulation time. This should only be used by sub-classes when running the simulation: doing so in any other context risks damaging the simulation.

Parameters:

t (float) – the new simualtion time

Running the experiment

A simulation takes the form of an epyc experiment which has set-up, execution, and tear-down phases.

Dynamics.setUp(params)

Set up the experiment for a run. This performs the inherited actions, then builds the network process that the dynamics is to run.

Params params:

the experimental parameters

Dynamics.tearDown()

At the end of each experiment, throw away any posted by un-executed events.

Dynamics.experimentalResults()

Report the process’ experimental results. This simply calls through to the Process.results() method of the process being simulated.

Return type:

Dict[str, Any]

Returns:

the results of the process

Loci

Loci for stochastic events are craeted by Process instances.

Dynamics.addLocus(p, n, l=None)

Add a named locus associated with the given process.

Parameters:
  • p (Process) – the process

  • n (str) – the locus name

  • l (Optional[Locus]) – the locus (defaults to a simple set-based locus)

Return type:

Locus

Returns:

the locus

Dynamics.locus(n)

Retrieve a locus by name.

Parameters:

n (str) – the locus name

Return type:

Locus

Returns:

the locus

Dynamics.loci()

Return all the loci in the simulation.

Return type:

Dict[str, Locus]

Returns:

a dict from names to loci

Dynamics.lociForProcess(p)

Return all the loci defined for a specific process.

Parameters:

p (Process) – the process

Return type:

Dict[str, Locus]

Returns:

a dict from names to loci

Stochastic event distributions

The stochastic events are defined and managed in the Process class. The dynamics only cares about their distributions.

Dynamics.perElementEventDistribution(t)

Return the distribution of of all processes’ per-element events at the given time.

Note that this method returns the probability of events, not their expected rates, for which use perElementEventRateDistribution().

Parameters:

t (float) – the simulation time

Return type:

List[Tuple[Locus, float, Callable[[float, Union[Any, Tuple[Any, Any]]], None], str]]

Returns:

a list of (locus, probability, event function) triples

Dynamics.perElementEventRateDistribution(t)

Return the rates of per-element events for all processes at the given time.

Note that this is method returns event rates, not their probabilities as returned by perElementEventDistribution(). The rate is simply an event’s probability multiplied by the size of the locus on which it occurs, giving the expected number of events occurring from that locus in unit time.

Parameters:

t (float) – the simulation time

Return type:

List[Tuple[Locus, float, Callable[[float, Union[Any, Tuple[Any, Any]]], None], str]]

Returns:

a list of (locus, rate, event function) triples

Dynamics.fixedRateEventDistribution(t)

Return the distribution of fixed-rate events for all processes at the given time.

Parameters:

t (float) – the simulation time

Return type:

List[Tuple[Locus, float, Callable[[float, Union[Any, Tuple[Any, Any]]], None], str]]

Returns:

a list of (locus, probability, event function) triples

Dynamics.eventRateDistribution(t)

Return the event distribution, a sequence of (l, r, f, n) tuples where l is the locus where the event occurs, r is the rate at which an event occurs, f is the event function called to make it happen, and n is the event name (which may be None).

Note the distinction between a rate and a probability: the former can be obtained from the latter simply by multiplying the event probability by the number of times it’s possible in the current network, which for per-element events is the population of nodes or edges in a given state.

It is perfectly fine for an event to have a zero rate. The process is assumed to have reached equilibrium if all events have zero rates.

Parameters:

t (float) – current time

Return type:

List[Tuple[Locus, float, Callable[[float, Union[Any, Tuple[Any, Any]]], None], str]]

Returns:

a list of (locus, rate, event function, event name) tuples

Posted events

A Dynamics object also maintains a queue of posted event.

Dynamics.postEvent(t, p, e, ef, name=None)

Post an event that calls the event function at time t. A unique id it returned that can be used to remove the event before it fires using unpostEvent().

The optional name is used in conjunction with event taps when calling eventFired().

Parameters:
  • t (float) – the current time

  • p (Process) – the process originating the event

  • e (Union[Any, Tuple[Any, Any]]) – the element (node or edge) on which the event occurs

  • ef (Callable[[float, Union[Any, Tuple[Any, Any]]], None]) – the event function

  • name (Optional[str]) – (optional) meaningful name of the event

Return type:

int

Returns:

the event id

Dynamics.postRepeatingEvent(t, dt, p, e, ef, name=None)

Post an event that starts at time t and re-occurs at interval dt. Repeating events can’t be removed once posted.

The optional name is used in conjunction with event taps when calling eventFired().

Parameters:
  • t (float) – the start time

  • dt (float) – the interval

  • p (Process) – the process originating the event

  • e (Union[Any, Tuple[Any, Any]]) – the element (node or edge) on which the event occurs

  • ef (Callable[[float, Union[Any, Tuple[Any, Any]]], None]) – the element function

  • name (Optional[str]) – (optional) meaningful name of the event

This queue is then accessed to extract the events that need to be fired up to a given simulation time.

Dynamics.nextPendingEvent()

Return the next posted event, or None if there are no events.

Return type:

Optional[Tuple[float, int, Process, Optional[Callable[[], None]], Union[Any, Tuple[Any, Any]], str]]

Returns:

the next posted event or None

Dynamics.nextPendingEventTime()

Return the simulation time for the next pending posted event, without affecting the event queue.

Return type:

Optional[float]

Returns:

the simulation time or None

Dynamics.nextPendingEventBefore(t)

Return the next pending event to occur at or before time t.

Parameters:

t (float) – the current time

Return type:

Optional[Tuple[float, int, Process, Optional[Callable[[], None]], Union[Any, Tuple[Any, Any]], str]]

Returns:

a posted event or None

Dynamics.pendingEventTime(id)

Return the time for which the given event is posted for. This will raise a KeyError if the event isn’t in the queue, thrtough having fired or being un-posted.

Poram id:

the event

Return type:

float

Returns:

the event’s posted simulation time

Dynamics.runPendingEvents(t)

Retrieve and fire any pending events at time t. This handles the case where firing an event posts another event that needs to be run before other already-posted events coming before time t: in other words, it ensures that the simulation order is respected.

Parameters:

t (float) – the current time

Return type:

int

Returns:

the number of events fired

Posted events can be un-posted at any time before they fire. (Repeating events can’t be un-posted once posted.)

Dynamics.unpostEvent(id, fatal=True)

Un-post a posted event. This is only legal before the event has fired, and will normally raise a KeyError if called on one that’s not queued: set fatal to False to avoid this.

Parameters:
  • id (int) – the event id

  • fatal (bool) – whether to raise KeyError for missing events (defaults to True)

Return type:

Optional[float]

Returns:

the simulation time at which the event would have fired, or None

Event taps

Whenever the network changes, there is an opportunity for the experiment to log it or take some other action. We refer to this as the event tap, as it captures the entire stream of events regardless of how they are defined. See Event taps for a discussion.

To use the event tap interface, you need to override these methods (they all have empty defaults) and ensure that they’re called from the right places.

Dynamics.eventFired(t, p, name, e)

Respond to the occurrance of the given event. The method is passed the simulation time, originating process, event name, and the element affected – and isn’t passed the event function, which is used elsewhere.

This method is called in the past tense, after the event function has been run. This lets the effects of the event be observed.

The event name is simply the optional name that was given to the event when it was declared using addEventPerElement() or addFixedRateEvent(). It will be None if no name was provided.

The default does nothing. It can be overridden by sub-classes to provide event-level logging or other functions.

Parameters:
  • t (float) – the simulation time

  • p (Process) – the process firing the event

  • name (str) – the event name

  • e (Union[Any, Tuple[Any, Any]]) – the element

There are three other methods that are called within the core of the experiment to set up and manage the event tap.

Dynamics.initialiseEventTaps()

Initialise the event tap sub-system, which allows external code access to the event stream of the simulation as it runs.

The default does nothing.

Dynamics.simulationStarted(params)

Called when the simulation has been configured and set up, any processes built, and is ready to run.

The default does nothing.

Parameters:

params (Dict[str, Any]) – the experimental parameters

Dynamics.simulationEnded(res)

Called when the simulation has stopped, immediately before tear-down.

The default does nothing.

Parameters:

res (Union[Dict[str, Any], List[Dict[str, Any]]]) – the experimental results

This is simply an interface definition: all the default implementations are empty. To use the event taps you need to override these methods for every experiment. The methods should be called as follows:

  • NetworkExperiment.initialiseEventTaps(): Early in the construction process, to allow any data structures to be created.

  • NetworkExperiment.simulationStarted(): After any set-up and immediately before the work of the experiment (simulation) starts, so that the overridden method gets to see the experiment right before execution.

  • NetworkExperiment.eventFired(): Immediately after each “event”, however defined, to that the overridden method gets to see the effect that the method had.

  • NetworkExperiment.simulationEnded(): After the last event and before any tear-down, so that the overridden method gets to see the final result of the simulation.

For example, in StochasticDynamics, NetworkExperiment.initialiseEventTaps() is called from the constructor of Dynamics; NetworkExperiment.simulationStarted() is called before the first stochastic event is drawn; NetworkExperiment.eventFired() is called immediately after each event function has been called, for both stochastic and posted events; and NetworkExperiment.simulationEnded() is called after the last stochastic event has been fired.