PulseCoupledOscillator: The canonical synchronisation process

class epydemic.PulseCoupledOscillator

Bases: Process

A pulse-coupled oscillator synchronisation process. This follows closely on the definition provided by Mirollo and Strogatz [MS90], a model inspired by synchronising biological systems such as fireflies and human heart cells.

The basic model consists of a collection of \(N\) nodes, each equipped with a state and an oscillator. As the phase of each oscillator advances it advances the corresponding state variable. When the state reaches 1 the node is triggered and fires – for example by emitting a pulse of light – and resets its state and phase to 0. On observing such a pulse, each other node advances its own state by some amount \(\epsilon\). If this takes the node’s state above 1, then it too resets its phase and state to 0, and is henceforth synchronised with the node that triggered it; otherwise the node’s phase is advanced to match its new state.

Note that, in this model, a node does not flash when it becomes synchronised, but is will flash after the next period (unless it again synchronises to another node before it fires). Note also that all synchronised nodes will fire at the same time, so there will be a potentially large number of flashes happening at the same time. (Both these properties can be relaxed.)

In the original formulation [MS90] the oscillators sit in a complete network, so that every node observes flashes from every other node. This has the interesting property that, once synchronised, two nodes will stay synchronised, so the number of phases and the size of the largest set of synchronised are both monotonic (decreasing and increasing respectively). This isn’t necessarily true for other networks.

Parameters

PulseCoupledOscillator.PERIOD: Final[str] = 'epydemic.pulsecoupled.period'

Parameter name for the oscillator period.

PulseCoupledOscillator.B: Final[str] = 'epydemic.pulsecoupled.dissipation'

Paramater name for the dissipation constant.

PulseCoupledOscillator.COUPLING: Final[str] = 'epydemic.pulsecoupled.coupling'

Parameter name for firing coupling strength.

Note that, unlike many processes, these parameters all have defaults set in PulseCoupledOscillator.build().

Results

PulseCoupledOscillator.PHASES: Final[str] = 'epydemic.pulsecoupled.phases'

The final phases of all the nodes.

PulseCoupledOscillator.FIRING_TIMES: Final[str] = 'epydemic.pulsecouplepd.firingTimes'

A list of firing times.

PulseCoupledOscillator.FIRING_NODES: Final[str] = 'epydemic.pulsecoupled.firingNodes'

A list of nodes firing at these times.

State variables

The only state variable holds an internal identifier for the posted event that will fire the node. It’s highly unlikely this will be useful outside the mechanics of the simulation.

PulseCoupledOscillator.NODE_EVENT_ID: str = None

Identifier of the next firing event.

Core parts of the oscillator process

The model is initialised by giving a phase to every oscillator.

PulseCoupledOscillator.initialisePhases()

Initialise the phases of the oscillators at all the nodes to a random value on the range \([0.0, 1.0]\).

The behaviour of firing, obsevration of flashes, and synchronisation of one oscillator to another can all be overridden or extended.

PulseCoupledOscillator.fire(t, n)

Handle the firing of an oscillator, when its phase hits 1. This marks all neighbours for update and resets the phase of this node to 0.

This method can be overridden to provide extra behaviours on firing. Be sure to call the base method first.

Parameters:
  • t (float) – the simulation time

  • n (Any) – the node that is firing

PulseCoupledOscillator.cascade(t, n, m)

Cascade the firing of one node into updating the phases of its neighbours, which may then synchronise them.

Parameters:
  • t (float) – the simulation time

  • n (Any) – the node that fired

  • m (Any) – the node being updated as a result of n firing

PulseCoupledOscillator.synchronised(t, n, m)

What to do when a node becomes synchronised with another. The default sets its phase to 0, meaning it will fire at the end of its next cycle: it won’t fire now.

Parameters:
  • t (float) – the time

  • n (Any) – the node that fired

  • m (Any) – the node that is now newly-synchronised with n

Setup and initialisation

PulseCoupledOscillator.build(params)

Build the oscillator model.

Each parameter gets a default of 1 if not set explicitly.

Parameters:

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

PulseCoupledOscillator.setUp(params)

Initialise the oscillator phases.

Parameters:

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

PulseCoupledOscillator.results()

Add the final phases of all oscillators to the results.

Return type:

Dict[str, Any]

Returns:

the experimental results

Events

The process defines one event that occurs whenever a node fires.

PulseCoupledOscillator.fired(t, n)

The event called when a node’s oscillator hits a phase of 1.0. This triggers the node, firing it and incrementing the phases of all its neighbours.

We track all the nodes that are triggered as a result of this event to ensure that they are only triggered at most once in any cascade.

Parameters:
  • t (float) – the simulation time

  • n (Any) – the node that is firing

PulseCoupledOscillator.FIRED: Final[str] = 'epydemic.pulsecoupled.fired'

The name of the firing event.

Managing state and phase

The state and phase parts of the system are managed by a pair of functions that convert between them.

PulseCoupledOscillator.phaseToState(phi)

Convert a phase to the corresponding state (function \(f\) in [MS90]).

Parameters:

phi (float) – the phase

Return type:

float

Returns:

the state

PulseCoupledOscillator.stateToPhase(u)

Convert a state to the corresponding phase (function \(g\) in [MS90]).

Parameters:

u (float) – the state

Return type:

float

Returns:

the phase

These are combined to advance the phase of an oscillator according to flashes it observes.

PulseCoupledOscillator.bumpPhase(phi)

Advance the phase and state of a node after it has observed a firing (the “return map” function \(h\) in [MS90]). By default this increments the state by the value given in the COUPLING parameter.

Parameters:

phi (float) – the phase

Return type:

float

Returns:

the updated phase

The state and phase of an oscillator are managed using a small set of methods that manage the next firing time of an oscillator (if left to its own devices, without being updated).

PulseCoupledOscillator.getPhase(t, n, normalise=False)

Get the phase of a node. This is computed by working back from the next-scheduled firing time.

By default this method returns the “raw” phase, and so treats phases of 0.0 and 1.0 as being different. Calling with normalise = True will map any phase of 1.0 to 0.0. This makes it easier to find all the nodes with oscillators at the same phase: however, it’s sometimes important to differentiate the two values (such as when deciding if an oscillator should fire), so the default keeps the two phases distinct.

Parameters:
  • t (float) – the time

  • m – the node

  • normalise (bool) – (optional) if True, combine phases of 0.0 and 1.0 (defaults to False)

Return type:

float

Returns:

the current phase

PulseCoupledOscillator.setPhase(t, n, phi)

Set the phase of the given node. This re-sets the next firing time for the node.

Parameters:
  • t (float) – the time

  • n (Any) – the node

  • phi (float) – the phase

PulseCoupledOscillator.getState(t, n)

Get the state of the given node.

Parameters:
  • t (float) – the time

  • n (Any) – the node

Return type:

float

Returns:

the state of the node

PulseCoupledOscillator.setFiringTime(n, et)

Set the next firing time of a node, replacing the currently-scheduled event.

Parameters:
  • n (Any) – the node

  • et (float) – the next scheduled firing time

PulseCoupledOscillator.getFiringTime(n)

Get the next firing time of a node.

Parameters:

n (Any) – the node

Returns:

the next scheduled firing time

It is important to manage the phase carefully to avoid numerical instability caused by unrestricted use of floating-point operations. Phases are kept in the range \([0.0, 1.0]\) and held to a fixed numerical precision defined by PulseCoupledOscillator.PHASE_PRECISION. (See the implementation note on event times for a discussion of this.)

PulseCoupledOscillator.normalisePhase(phi)

Ensure the phase is normalised and quantised. This ensures that the phase is in the range \([0.0, 1.0]\) and stored to a precision set by PHASE_PRECISION to avoid problems with floating-point rounding.

Parameters:

phi (float) – the phase

Return type:

float

Returns:

the normalised phase

Tuning parameters

The precision with which phases are held can be changed if required – although the default is almost certainly adequate.

PulseCoupledOscillator.PHASE_PRECISION: int = 5

Number of places or precision for phases.