cont_delay_synapse#

class brainpy.state.cont_delay_synapse(weight=1.0, delay=Quantity(1., 'ms'), receptor_type=0, post=None, event_type='spike', name=None)#

NEST-compatible static synapse with continuous (off-grid) delays.

This synapse model extends static_synapse to support precise spike timing by decomposing transmission delays into integer delay steps and fractional sub-timestep offsets. It mirrors the NEST cont_delay_synapse model and implements the exact subthreshold integration method described in Morrison et al. (2007).

Parameters:
  • weight (ArrayLike, optional) – Synaptic weight multiplier applied to incoming spike events. Dimensionless scalar or array. Default: 1.0.

  • delay (ArrayLike, optional) – Total synaptic transmission delay. Must be >= dt (simulation timestep). Decomposed internally into integer steps and fractional offset. Unit: millisecond. Default: 1.0 * u.ms.

  • receptor_type (int, optional) – Target receptor port index on the postsynaptic neuron. Used to differentiate excitatory/inhibitory or multiple receptor types. Default: 0.

  • post (brainstate.nn.Module, optional) – Default postsynaptic target object. Must implement either handle_cont_delay_synapse_event(value, receptor_type, event_type, offset) or add_precise_spike_event(key, value, offset, label) for off-grid event delivery. On-grid events fall back to standard static_synapse delivery via add_current_input or add_delta_input. Default: None.

  • event_type (str, optional) – Event transmission mode. Supported values: 'spike' (discrete delta events), 'rate' (continuous rate signals), 'current' (arbitrary current injection). Default: 'spike'.

  • name (str, optional) – Unique identifier for this synapse instance. Used for debugging and event tracking. Default: None (auto-generated).

Mathematical Model

1. Delay Decomposition

Given a continuous delay \(d\) and simulation timestep \(\Delta t\), the model decomposes \(d\) into:

  • Integer delay steps: \(d_{\text{steps}} \in \mathbb{N}\)

  • Fractional offset: \(d_{\text{offset}} \in [0, \Delta t)\) (in ms)

such that the effective delay satisfies:

\[d_{\text{eff}} = d_{\text{steps}} \cdot \Delta t - d_{\text{offset}}\]

The decomposition algorithm follows NEST conventions:

  • Case 1: On-grid delay (\(d / \Delta t \in \mathbb{N}\)):

    \[\begin{split}d_{\text{steps}} &= \frac{d}{\Delta t} \\ d_{\text{offset}} &= 0\end{split}\]
  • Case 2: Off-grid delay (\(d / \Delta t \notin \mathbb{N}\)):

    \[\begin{split}d_{\text{steps}} &= \lfloor d / \Delta t \rfloor + 1 \\ d_{\text{offset}} &= \Delta t \cdot \left(1 - \text{frac}(d / \Delta t)\right)\end{split}\]

    where \(\text{frac}(x) = x - \lfloor x \rfloor\) is the fractional part.

Constraint: \(d \geq \Delta t\) must hold. Violations raise ValueError during initialization or timestep changes.

2. Event Scheduling with Source Offsets

When a presynaptic spike arrives with source offset \(o_{\text{src}}\) (measured from the right edge of the current timestep), the effective event offset becomes:

\[o_{\text{total}} = o_{\text{src}} + d_{\text{offset}}\]

Carry handling: If \(o_{\text{total}} \geq \Delta t\), the event “carries over” to the next timestep:

\[\begin{split}d_{\text{steps}}^{\text{adj}} &= d_{\text{steps}} - 1 \\ o_{\text{event}} &= o_{\text{total}} - \Delta t\end{split}\]

Otherwise:

\[\begin{split}d_{\text{steps}}^{\text{adj}} &= d_{\text{steps}} \\ o_{\text{event}} &= o_{\text{total}}\end{split}\]

The adjusted delay steps determine when the event is delivered, and \(o_{\text{event}}\) specifies the sub-timestep delivery time.

3. Event Delivery

Events are queued at timestep \(t_{\text{deliver}} = t_{\text{current}} + d_{\text{steps}}^{\text{adj}}\) and delivered with offset \(o_{\text{event}}\). The delivery mechanism depends on the receiver’s capabilities:

  • Off-grid delivery: Calls receiver.handle_cont_delay_synapse_event(value, receptor_type, event_type, offset) if available. The receiver integrates the event at precise time \(t_{\text{step}} - o_{\text{event}}\) (measured from right edge).

  • On-grid fallback: If \(o_{\text{event}} \approx 0\), uses standard static_synapse delivery (add_current_input or add_delta_input).

  • Precise spike API: For spike events, optionally calls receiver.add_precise_spike_event(key, value, offset, label) if handle_cont_delay_synapse_event is unavailable.

4. Weight Multiplication

Spike multiplicity is scaled by the synaptic weight:

\[\text{payload} = m \cdot w\]

where \(m\) is the incoming spike count and \(w\) is the weight.

Parameter Mapping

NEST Parameter

brainpy.state Parameter

Notes

weight

weight

Dimensionless multiplier

delay

delay

Total delay (ms)

delay_steps

_delay_steps (internal)

Integer steps (auto)

delay_offset

_delay_offset_ms (internal)

Fractional offset (auto)

receptor_type

receptor_type

Receptor port index

Internal state _delay_steps and _delay_offset_ms are computed automatically during initialization and when the simulation timestep changes. They are exposed via get() for inspection.

Computational Properties

  • Time complexity: \(O(1)\) per event (queue insertion/retrieval uses dict lookups).

  • Space complexity: \(O(E \cdot D)\) where \(E\) is the number of pending events and \(D\) is the maximum delay in timesteps.

  • Numerical precision: Offset arithmetic uses double precision. Offsets within \(10^{-15}\) ms of zero are treated as on-grid for numerical stability.

  • Exact integration: When combined with compatible neuron models (e.g., NEST’s *_ps variants), achieves machine-precision spike timing independent of timestep choice (within numerical limits).

Failure Modes

  • ValueError: Raised if delay < dt or dt <= 0 during initialization or _refresh_delay_cache.

  • TypeError: Raised if receiver does not implement off-grid event delivery API when required (offset > 0).

  • ValueError: Raised if source offsets violate 0 <= offset <= dt.

See also

static_synapse

Base class for fixed-weight synapses

iaf_psc_exp_ps

NEST neuron model with precise spike timing support

Notes

Event Format: The update method accepts precise spike events via the spike_events parameter in two formats:

  • Tuple format: (offset, multiplicity) where offset is in milliseconds.

  • Dict format: {'offset': value, 'multiplicity': value} for explicit labeling.

  • Sequence format: List of tuples or dicts for multiple events in one timestep.

Offset Convention: All offsets follow NEST’s precise-time convention: measured backward from the right edge of the current timestep. An offset of 0 means delivery at the end of the step; dt means delivery at the start. This matches the continuous-time interpretation where time increases left-to-right within the discrete step.

Timestep Changes: When the simulation timestep changes (e.g., via brainstate.environ.context(dt=...)), the model automatically recomputes _delay_steps and _delay_offset_ms to maintain the requested delay value. This may alter the effective delay within machine precision bounds.

Warnings: If delay is specified in NEST-style Connect calls (via check_synapse_params), a warning is issued because connect-time delays are rounded to integer timesteps. For precise delays, define them in the synapse model itself (e.g., via NEST’s CopyModel).

References

Examples

Basic usage with on-grid delay:

>>> import brainpy.state as bst
>>> import brainstate as bst
>>> import saiunit as u
>>> with bst.environ.context(dt=0.1 * u.ms):
...     syn = bst.nest.cont_delay_synapse(
...         weight=2.5,
...         delay=1.0 * u.ms,  # 10 steps at dt=0.1 ms
...     )
...     print(syn.get())
{'weight': 2.5, 'delay': 1.0, 'delay_offset': 0.0, 'receptor_type': 0,
 'synapse_model': 'cont_delay_synapse'}

Off-grid delay with fractional offset:

>>> with bst.environ.context(dt=0.1 * u.ms):
...     syn = bst.nest.cont_delay_synapse(
...         weight=1.0,
...         delay=1.23 * u.ms,  # Not a multiple of 0.1 ms
...     )
...     params = syn.get()
...     print(f"delay_steps: {syn._delay_steps}, "
...           f"delay_offset: {params['delay_offset']:.4f} ms")
delay_steps: 13, delay_offset: 0.0700 ms
# Effective delay: 13 * 0.1 - 0.07 = 1.23 ms

Sending events with source offsets:

>>> import jax.numpy as jnp
>>> with bst.environ.context(dt=0.1 * u.ms):
...     neuron = bst.nn.LIF(1, V_rest=-70 * u.mV)
...     syn = bst.nest.cont_delay_synapse(
...         weight=5.0,
...         delay=0.5 * u.ms,
...         post=neuron,
...     )
...     syn.init_all_states()
...     neuron.init_all_states()
...     # Send spike with 0.03 ms offset from step edge
...     syn.send(multiplicity=1.0, source_offset=0.03 * u.ms)

Processing multiple precise events per timestep:

>>> with bst.environ.context(dt=0.1 * u.ms):
...     syn = bst.nest.cont_delay_synapse(weight=1.0, delay=0.5 * u.ms)
...     syn.init_all_states()
...     # Pass list of (offset, multiplicity) tuples
...     events = [
...         (0.02 * u.ms, 1.0),  # Early spike
...         (0.08 * u.ms, 2.0),  # Late spike (double)
...     ]
...     delivered = syn.update(spike_events=events)

Checking delay decomposition for validation:

>>> with bst.environ.context(dt=0.1 * u.ms):
...     syn = bst.nest.cont_delay_synapse(delay=0.37 * u.ms)
...     syn.init_all_states()
...     params = syn.get()
...     d_eff = syn._delay_steps * 0.1 - params['delay_offset']
...     print(f"Requested: 0.37 ms, Effective: {d_eff:.2f} ms")
Requested: 0.37 ms, Effective: 0.37 ms
check_synapse_params(syn_spec)[source]#

Validate and warn about connect-time synapse specifications.

Issues a warning if delay is specified in connect-time synapse dictionaries (e.g., in NEST-style Connect calls), because such delays are rounded to integer multiples of the simulation timestep rather than using the continuous-delay decomposition.

Parameters:

syn_spec (dict or None) – Synapse specification dictionary, typically from NEST-style connection APIs. Expected keys: 'delay', 'weight', etc.

Warns:

UserWarning – If 'delay' key is present in syn_spec. The warning message advises defining delays in the synapse model definition (e.g., via NEST’s CopyModel) to preserve precise sub-timestep offsets.

Notes

This method mirrors NEST’s cont_delay_synapse behavior, which prints a similar warning when delays are supplied at connection time. To avoid rounding, instantiate the synapse with the desired delay parameter directly rather than passing it via connection specs.

get()[source]#

Return current public parameters including delay decomposition.

Returns:

Parameter dictionary with keys:

  • 'weight' : float — Synaptic weight multiplier.

  • 'delay' : float — Effective total delay in milliseconds (computed as \(d_{\text{steps}} \cdot \Delta t - d_{\text{offset}}\)).

  • 'delay_offset' : float — Fractional sub-timestep offset in milliseconds (range [0, dt)). Zero for on-grid delays.

  • 'receptor_type' : int — Target receptor port index.

  • 'synapse_model' : str — Always 'cont_delay_synapse'.

Return type:

dict

Notes

The delay_offset field is specific to this model and not present in the base static_synapse. Integer delay steps (_delay_steps) are not exposed but can be accessed via the internal attribute for debugging.

send(multiplicity=1.0, *, source_offset=Quantity(0., 'ms'), post=None, receptor_type=None, event_type=None)[source]#

Schedule an outgoing synaptic event with continuous-delay offset.

Implements the NEST cont_delay_synapse event scheduling algorithm: combines the source spike offset with the synapse’s fractional delay offset, handles carry-over if the sum exceeds the timestep, and queues the event for delivery at the appropriate future timestep.

Parameters:
  • multiplicity (ArrayLike, optional) – Spike count or event magnitude. Multiplied by self.weight to compute the delivered payload. Must be non-negative scalar or array. Default: 1.0 (single spike).

  • source_offset (ArrayLike, optional) – Sub-timestep offset of the source spike, measured backward from the right edge of the current timestep. Must satisfy 0 <= source_offset <= dt. Unit: millisecond. Default: 0.0 * u.ms (spike occurred at step boundary).

  • post (brainstate.nn.Module, optional) – Override the default postsynaptic target for this event. If None, uses self.post. Default: None.

  • receptor_type (ArrayLike, optional) – Override the default receptor port index for this event. If None, uses self.receptor_type. Must be integer-valued. Default: None.

  • event_type (str, optional) – Override the default event transmission type for this event. If None, uses self.event_type. Supported values: 'spike', 'rate', 'current'. Default: None.

Returns:

True if the event was scheduled (or delivered immediately), False if the event was skipped due to zero multiplicity.

Return type:

bool

Raises:
  • ValueError – If source_offset violates the constraint 0 <= source_offset <= dt.

  • TypeError – If the receiver does not implement the required off-grid event delivery API when the effective offset is non-zero.

Notes

Offset Arithmetic:

The effective event offset is computed as:

\[o_{\text{total}} = o_{\text{src}} + d_{\text{offset}}\]

If \(o_{\text{total}} \geq \Delta t\), a carry occurs:

  • Adjusted delay steps – \(d_{\text{steps}} - 1\)

  • Final event offset – \(o_{\text{total}} - \Delta t\)

Otherwise, no carry:

  • Adjusted delay steps – \(d_{\text{steps}}\)

  • Final event offset – \(o_{\text{total}}\)

Immediate Delivery: If the carry reduces delay steps to zero, the event is delivered immediately via _deliver_event_with_offset rather than being queued.

Queue Structure: Events are stored in self._queue (a defaultdict mapping delivery timestep to event list). Each queued entry is a tuple: (receiver, payload, receptor_type, event_type, offset).

Example:

>>> # Synapse with 1.23 ms delay (13 steps, 0.07 ms offset at dt=0.1)
>>> # Source spike at 0.05 ms offset
>>> # Total: 0.05 + 0.07 = 0.12 ms >= 0.1 ms → carry
>>> # Adjusted: 12 steps, 0.02 ms offset
>>> syn.send(multiplicity=1.0, source_offset=0.05 * u.ms)
True
update(pre_spike=0.0, *, spike_events=None, post=None, receptor_type=None, event_type=None)[source]#

Process one simulation timestep: deliver queued events and schedule new ones.

This method implements the standard synapse update cycle:

  1. Deliver due events: Retrieve and deliver all events scheduled for the current timestep from the internal queue.

  2. Schedule on-grid events: Sum presynaptic input from pre_spike and registered current/delta inputs, then schedule with zero offset.

  3. Schedule precise events: Process each event in spike_events with its specified sub-timestep offset.

Parameters:
  • pre_spike (ArrayLike, optional) – On-grid presynaptic spike count or rate. Treated as occurring at the right edge of the timestep (offset = 0). Scalar or array. Default: 0.0 (no on-grid input).

  • spike_events (list of tuples/dicts, tuple, dict, or None, optional) –

    Precise spike events with sub-timestep timing. Supported formats:

    • Single tuple: (offset, multiplicity)

    • Single dict: {'offset': value, 'multiplicity': value}

    • List of tuples/dicts: Multiple events in one step

    Each offset must satisfy 0 <= offset <= dt (in ms). Default: None (no precise events).

  • post (brainstate.nn.Module, optional) – Override the default postsynaptic target. Default: None (use self.post).

  • receptor_type (ArrayLike, optional) – Override the default receptor port index. Default: None (use self.receptor_type).

  • event_type (str, optional) – Override the default event type. Supported: 'spike', 'rate', 'current'. Default: None (use self.event_type).

Returns:

Number of events delivered during this timestep (from the queue). Does not include newly scheduled events.

Return type:

int

Raises:
  • ValueError – If any event offset in spike_events violates 0 <= offset <= dt.

  • ValueError – If event dicts are missing required keys 'offset' or 'multiplicity'.

Notes

Processing Order: The three-step sequence (deliver → on-grid → precise) ensures deterministic behavior when events from previous timesteps arrive simultaneously with new inputs. Queued events are always processed before new scheduling occurs.

Input Aggregation: On-grid inputs are summed across all sources:

  • Explicit pre_spike argument

  • Inputs registered via add_current_input(label, value)

  • Inputs registered via add_delta_input(label, value)

Event Format Examples:

# Single tuple
syn.update(spike_events=(0.05 * u.ms, 2.0))

# Single dict
syn.update(spike_events={'offset': 0.05 * u.ms, 'multiplicity': 2.0})

# Multiple events
syn.update(spike_events=[
    (0.02 * u.ms, 1.0),
    (0.08 * u.ms, 3.0),
])

Return Value: Only counts delivered events. Newly scheduled events (from pre_spike or spike_events) will be counted in future timesteps when they are delivered.

See also

send

Schedule a single event with offset