jonke_synapse#

class brainpy.state.jonke_synapse(weight=1.0, delay=1.0, delay_steps=1, Kplus=0.0, t_last_spike_ms=0.0, alpha=1.0, beta=0.0, lambda_=0.01, mu_plus=0.0, mu_minus=0.0, tau_plus=20.0, Wmax=100.0, name=None)#

NEST-compatible jonke_synapse connection model with weight-dependent STDP.

Implements spike-timing-dependent plasticity with exponential weight dependence and additive offsets, following NEST’s jonke_synapse semantics. The model applies multiplicative weight factors \(\exp(\mu w)\) to both facilitation and depression branches, producing nonlinear weight dynamics that can stabilize synaptic strengths or implement homeostatic control.

1. Mathematical Formulation

The plasticity rule operates on synaptic weight \(w(t)\) using presynaptic trace \(K_+(t)\) (with time constant \(\tau_+\)) and postsynaptic trace \(K_-(t)\):

\[\begin{split}\frac{dK_+}{dt} &= -\frac{K_+}{\tau_+} + \sum_f \delta(t - t_f^{\text{pre}}) \\ \frac{dK_-}{dt} &= -\frac{K_-}{\tau_-} + \sum_j \delta(t - t_j^{\text{post}})\end{split}\]

Weight-dependent plasticity kernels:

\[\begin{split}\Phi_+(w) &= \exp(\mu_+ w) \\ \Phi_-(w) &= \exp(\mu_- w)\end{split}\]

Update rules (applied at spike times):

\[\begin{split}\Delta w_+ &= \lambda \left( \Phi_+(w) K_+ - \beta \right) \quad \text{(facilitation)} \\ \Delta w_- &= \lambda \left( -\alpha \Phi_-(w) K_- - \beta \right) \quad \text{(depression)}\end{split}\]

The weight is hard-bounded to \([0, W_{\max}]\) after each update.

2. Temporal Dynamics

At each presynaptic spike at time \(t\):

  1. History lookup: Read postsynaptic spikes in \((t_{\text{last}} - d,\; t - d]\)

  2. Facilitation pass: For each post-spike \(t_j\) in history:

    \[ \begin{align}\begin{aligned}K_+^{\text{eff}} = K_+(t_{\text{last}}) \exp\left(\frac{t_{\text{last}} - (t_j + d)}{\tau_+}\right)\\w \leftarrow w + \lambda \left( \Phi_+(w) K_+^{\text{eff}} - \beta \right)\end{aligned}\end{align} \]
  3. Depression: Using postsynaptic \(K_-(t - d)\):

    \[w \leftarrow w + \lambda \left( -\alpha \Phi_-(w) K_-(t-d) - \beta \right)\]
  4. Event emission: Spike delivered with updated weight

  5. Trace update:

    \[K_+ \leftarrow K_+ \exp\left(\frac{t_{\text{last}} - t}{\tau_+}\right) + 1\]
  6. State update: \(t_{\text{last}} \leftarrow t\)

3. Design Considerations

  • Exponential weight dependence: \(\mu_+ > 0\) implements soft upper bound (larger weights resist growth); \(\mu_+ < 0\) enables runaway potentiation. Similarly for \(\mu_-\) and depression.

  • Additive offset :math:`beta`: Shifts both update branches uniformly. Positive \(\beta\) biases toward depression, negative toward potentiation. Can implement heterosynaptic competition.

  • Asymmetric depression scaling: \(\alpha\) allows independent control of depression amplitude relative to potentiation.

  • Numerical stability: For large \(|\mu w|\), \(\exp(\mu w)\) may overflow/underflow. Consider \(\mu\) values keeping \(|\mu W_{\max}| < 10\).

4. Computational Properties

  • Time complexity: \(O(N_{\text{post-spikes}})\) per presynaptic spike, where \(N_{\text{post-spikes}}\) is the count in delay window.

  • Dendritic delay semantics: History lookups use \(t - d\) (compensating for backpropagation time). Event delivery uses delay_steps (axonal propagation).

  • Precision: Sub-grid spike timing (offset component in NEST) is ignored; all updates use grid-aligned times only.

Parameters:
  • weight (float or array-like, default 1.0) – Initial synaptic efficacy (dimensionless or pA/mV). Must be finite. Updated during plasticity and bounded to \([0, W_{\max}]\).

  • delay (float or array-like, default 1.0) – Dendritic delay in milliseconds for history lookups and depression timing (\(d\) in equations). Must be positive.

  • delay_steps (int or array-like, default 1) – Axonal event delivery delay in simulation time steps. Must be ≥ 1. Typically set to match delay quantized to grid resolution.

  • Kplus (float or array-like, default 0.0) – Initial presynaptic trace value \(K_+(0)\). Must be non-negative. Evolves according to \(\tau_+\) dynamics.

  • t_last_spike_ms (float or array-like, default 0.0) – Timestamp of last presynaptic spike in milliseconds. Used for trace decay computation between spikes.

  • alpha (float or array-like, default 1.0) – Depression amplitude scaling factor. \(\alpha = 1\) gives symmetric update magnitudes (when \(\mu_+ = \mu_- = 0, \beta = 0\)).

  • beta (float or array-like, default 0.0) – Additive offset applied to both update branches (dimensionless). Positive values bias toward depression. Enables heterosynaptic effects.

  • lambda (float or array-like, default 0.01) – Learning rate \(\lambda\). Controls plasticity time scale. Set to 0 to disable learning. Typical values: \(10^{-4}\) to \(10^{-1}\).

  • mu_plus (float or array-like, default 0.0) – Facilitation weight dependence exponent \(\mu_+\). Positive values create soft upper bound. Units: inverse of weight units.

  • mu_minus (float or array-like, default 0.0) – Depression weight dependence exponent \(\mu_-\). Positive values accelerate depression at high weights.

  • tau_plus (float or array-like, default 20.0) – Presynaptic trace time constant in milliseconds \(\tau_+\). Controls potentiation temporal window. Typical range: 10–40 ms.

  • Wmax (float or array-like, default 100.0) – Hard upper weight bound \(W_{\max}\). Weights exceeding this after updates are clipped. Lower bound is always 0.

  • name (str or None, optional) – Instance identifier for debugging and logging.

Parameter Mapping

NEST Parameter

brainpy.state Parameter

Units / Notes

weight

weight

dimensionless

delay

delay

ms

delay_steps

delay_steps

steps

Kplus

Kplus

dimensionless

t_lastspike

t_last_spike_ms

ms

alpha

alpha

dimensionless

beta

beta

dimensionless

lambda

lambda_

dimensionless

mu_plus

mu_plus

1/weight

mu_minus

mu_minus

1/weight

tau_plus

tau_plus

ms

Wmax

Wmax

same as weight

Raises:

ValueError

  • If Kplus < 0 (violates trace non-negativity). - If delay <= 0 (non-physical delay). - If delay_steps < 1 (invalid event scheduling). - If any parameter is non-finite (NaN or ±inf). - If scalar parameters have size ≠ 1.

Notes

  • Target interface requirements: The postsynaptic target object passed to send() must implement:

    • get_history(t1, t2) -> iterable: Returns postsynaptic spike times in \((t_1, t_2]\). Each entry is an object/dict/tuple with time accessible via .t_, .t, ['t_'], ['t'], or first element.

    • get_K_value(t) -> float or get_k_value(t) -> float: Returns depression trace \(K_-(t)\) at time t.

  • NEST compatibility: Reproduces behavior of nest-simulator/models/jonke_synapse.cpp including parameter validation, update ordering, and spike event payload structure.

  • Sub-grid timing: Unlike NEST’s precise spike timing mode, this implementation uses grid-aligned times only (ignoring offset components).

  • Homeostatic interpretation: With \(\mu_+ > 0\) and appropriate \(\beta\), the model can implement sliding threshold mechanisms that stabilize weight distributions.

Examples

Basic STDP with linear weight dependence:

>>> import brainpy.state as bp
>>> # Create synapse with standard STDP parameters
>>> syn = bp.jonke_synapse(
...     weight=5.0,
...     delay=1.0,
...     lambda_=0.01,
...     tau_plus=20.0,
...     alpha=1.0,
...     beta=0.0,
...     mu_plus=0.0,
...     mu_minus=0.0,
...     Wmax=10.0
... )
>>> syn.get_status()['weight']
5.0

Exponential weight dependence (soft bounds):

>>> # Mu_plus > 0 creates resistance to potentiation at high weights
>>> syn_bounded = bp.jonke_synapse(
...     weight=1.0,
...     lambda_=0.01,
...     mu_plus=0.1,
...     mu_minus=0.05,
...     Wmax=20.0
... )
>>> # At w=10: Phi_+(10) = exp(0.1*10) = 2.72 (potentiation enhanced)
>>> # At w=0: Phi_+(0) = 1.0 (baseline)

Heterosynaptic plasticity via beta offset:

>>> # Positive beta biases toward depression
>>> syn_hetero = bp.jonke_synapse(
...     weight=5.0,
...     lambda_=0.005,
...     beta=0.05,
...     alpha=1.2
... )
>>> # All weights slowly decay even without post-spikes (beta term)

Simulate spike-pair interaction:

>>> class MockTarget:
...     def get_history(self, t1, t2):
...         # Return single post-spike at t=15 ms
...         if t1 < 15.0 <= t2:
...             return [{'t_': 15.0}]
...         return []
...     def get_k_value(self, t):
...         # Kminus trace at depression check time
...         return 0.8
>>>
>>> target = MockTarget()
>>> syn = bp.jonke_synapse(weight=5.0, lambda_=0.01, tau_plus=20.0)
>>>
>>> # Pre-spike at t=10 ms (pre-before-post → no facilitation yet)
>>> event1 = syn.send(t_spike_ms=10.0, target=target)
>>> print(f"Weight after pre@10: {event1['weight']:.3f}")
Weight after pre@10: 4.992
>>>
>>> # Pre-spike at t=20 ms (post@15 in history → facilitation)
>>> event2 = syn.send(t_spike_ms=20.0, target=target)
>>> print(f"Weight after pre@20: {event2['weight']:.3f}")
Weight after pre@20: 5.034

See also

stdp_synapse

Classical pair-based STDP without weight dependence.

stdp_triplet_synapse

Triplet STDP rule with better experimental fit.

vogels_sprekeler_synapse

Inhibitory STDP for E/I balance.

References

get(key='status')[source]#

Retrieve parameter or full status dictionary by key (NEST Get compatible).

Parameters:

key (str, default 'status') – Parameter name or 'status' for full dictionary. Valid keys include: 'weight', 'delay', 'Kplus', 'lambda', 'tau_plus', etc.

Returns:

  • If key='status': full dictionary from get_status().

  • Otherwise: scalar value of the requested parameter.

Return type:

Any

Raises:

KeyError – If key is not 'status' and not found in status dictionary.

Examples

>>> syn = bp.jonke_synapse(weight=7.0, tau_plus=25.0)
>>> syn.get('weight')
7.0
>>> syn.get('tau_plus')
25.0
>>> syn.get('status')['lambda']
0.01
get_status()[source]#

Retrieve complete synapse state snapshot (NEST GetStatus compatible).

Returns current values of all parameters, state variables, and model capabilities. Output format matches NEST’s GetStatus dictionary structure.

Returns:

Dictionary containing:

  • weight (float): Current synaptic efficacy.

  • delay (float): Dendritic delay in ms.

  • delay_steps (int): Event delivery delay in steps.

  • Kplus (float): Current presynaptic trace value.

  • t_last_spike_ms (float): Last presynaptic spike time in ms.

  • alpha (float): Depression scaling factor.

  • beta (float): Additive offset.

  • lambda (float): Learning rate (key name uses NEST convention).

  • mu_plus (float): Facilitation weight exponent.

  • mu_minus (float): Depression weight exponent.

  • tau_plus (float): Presynaptic trace time constant in ms.

  • Wmax (float): Maximum weight bound.

  • size_of (int): Memory footprint in bytes.

  • Capability flags (has_delay, is_primary, etc.).

Return type:

dict[str, Any]

Examples

>>> syn = bp.jonke_synapse(weight=3.5, lambda_=0.02, tau_plus=15.0)
>>> status = syn.get_status()
>>> print(status['weight'], status['lambda'], status['tau_plus'])
3.5 0.02 15.0
property properties: dict[str, Any]#

NEST synapse model capability flags.

Returns:

Dictionary with boolean capability flags:

  • has_delay: Supports delayed spike delivery (always True).

  • is_primary: Primary connection type for spike transmission (always True).

  • supports_hpc: Compatible with NEST’s high-performance computing mode (True).

  • supports_lbl: Supports label-based connectivity (True).

  • supports_wfr: Supports waveform relaxation method (always False for plasticity models).

Return type:

dict[str, Any]

send(t_spike_ms, target, receptor_type=0, multiplicity=1.0, delay=None, delay_steps=None)[source]#

Process presynaptic spike with plasticity and return spike event payload.

Implements the full NEST jonke_synapse::send() protocol:

  1. Retrieve postsynaptic spike history in delay-compensated window

  2. Apply facilitation for each postsynaptic spike in history

  3. Apply depression using current postsynaptic trace

  4. Update presynaptic trace and timestamp

  5. Return spike event dictionary with updated weight

This is the core method for spike-driven plasticity computation.

Parameters:
  • t_spike_ms (float or array-like) – Current presynaptic spike time in milliseconds. Must be scalar and ≥ t_last_spike_ms (non-decreasing spike times assumed).

  • target (object) –

    Postsynaptic neuron or recorder object. Must implement:

    • get_history(t1, t2) -> iterable: Postsynaptic spike times in (t1, t2].

    • get_K_value(t) -> float or get_k_value(t) -> float: Depression trace \(K_-(t)\).

  • receptor_type (int or array-like, default 0) – Postsynaptic receptor channel identifier (e.g., 0=AMPA, 1=NMDA, 2=GABA_A). Passed through to event payload without modification.

  • multiplicity (float or array-like, default 1.0) – Spike event amplitude multiplier. Must be non-negative. Scales effective weight in postsynaptic neuron. Typical use: probabilistic synapses or multi-vesicle release.

  • delay (float or array-like or None, optional) – Override dendritic delay for this spike (in ms). If None, uses self.delay. Affects history lookup window: \((t_{\text{last}} - d,\; t - d]\).

  • delay_steps (int or array-like or None, optional) – Override event delivery delay (in steps). If None, uses self.delay_steps. Determines when postsynaptic neuron receives the spike.

Returns:

Spike event payload dictionary containing:

  • weight (float): Updated synaptic efficacy after plasticity.

  • delay (float): Dendritic delay used (ms).

  • delay_steps (int): Event delivery delay (steps).

  • receptor_type (int): Postsynaptic receptor channel.

  • multiplicity (float): Spike amplitude multiplier.

  • t_spike_ms (float): Presynaptic spike time (ms).

  • Kminus (float): Postsynaptic trace value at depression check time.

  • Kplus_pre (float): Presynaptic trace before update.

  • Kplus_post (float): Presynaptic trace after update.

Return type:

dict[str, Any]

Raises:
  • ValueError

    • If t_spike_ms, receptor_type, or multiplicity are non-scalar. - If multiplicity < 0. - If delay <= 0 or delay_steps < 1 (when overriding defaults).

  • AttributeError – If target does not implement required get_history() or get_K_value() methods.

  • TypeError – If history entries do not expose time via supported interface (see Notes).

Notes

  • History entry format: Each entry from target.get_history(t1, t2) must be:

    • Object with .t_ or .t attribute, OR

    • Dictionary with 't_' or 't' key, OR

    • Tuple/list where first element is time (float).

  • Delay semantics: History lookup uses \(t - d\) to account for backpropagation delay. Event delivery uses delay_steps for forward propagation.

  • State mutation: Updates self.weight, self.Kplus, and self.t_last_spike_ms in place. Not thread-safe without external synchronization.

  • Causality: If \(t_{\text{spike}} < t_{\text{last}}\), trace decay may produce negative exponential argument (mathematically valid but may indicate simulation error).

Examples

Basic spike transmission:

>>> class PostNeuron:
...     def get_history(self, t1, t2):
...         return []  # No post-spikes
...     def get_K_value(self, t):
...         return 0.5  # Constant depression trace
>>>
>>> syn = bp.jonke_synapse(weight=5.0, lambda_=0.01, beta=0.02)
>>> target = PostNeuron()
>>> event = syn.send(t_spike_ms=10.0, target=target)
>>>
>>> print(f"Weight: {event['weight']:.3f}")
Weight: 4.980
>>> # Depression applied: dw = 0.01 * (-1.0 * 1.0 * 0.5 - 0.02) = -0.007
>>> # Weight bounded: max(0, 5.0 - 0.007) = 4.993 (approx, with exp factors)

Spike-pair potentiation:

>>> class PostNeuron:
...     def get_history(self, t1, t2):
...         # Post-spike at t=12 ms (between t1 and t2)
...         if t1 < 12.0 <= t2:
...             return [{'t_': 12.0}]
...         return []
...     def get_K_value(self, t):
...         return 0.0  # No depression trace yet
>>>
>>> syn = bp.jonke_synapse(weight=5.0, lambda_=0.01, tau_plus=20.0)
>>> target = PostNeuron()
>>>
>>> # First pre-spike at t=10 ms (before post@12)
>>> event1 = syn.send(t_spike_ms=10.0, target=target)
>>> print(f"Weight after pre@10: {event1['weight']:.3f}, Kplus: {event1['Kplus_post']:.3f}")
Weight after pre@10: 5.000, Kplus: 1.000
>>>
>>> # Second pre-spike at t=15 ms (post@12 now in history)
>>> event2 = syn.send(t_spike_ms=15.0, target=target)
>>> print(f"Weight after pre@15: {event2['weight']:.3f}")
Weight after pre@15: 5.009
>>> # Facilitation from Kplus(10) decayed to t=12: exp((10-13)/20) ≈ 0.861

Override delay per spike:

>>> event = syn.send(
...     t_spike_ms=20.0,
...     target=target,
...     delay=2.5,
...     delay_steps=3
... )
>>> print(event['delay'], event['delay_steps'])
2.5 3

See also

to_spike_event

Alias for send() (NEST naming compatibility).

simulate_pre_spike_train

Process multiple spikes in sequence.

set_delay(delay)[source]#

Update dendritic delay (convenience method).

Parameters:

delay (float or array-like) – New delay in milliseconds. Must be positive and finite.

Raises:

ValueError – If delay ≤ 0, non-scalar, or non-finite.

set_delay_steps(delay_steps)[source]#

Update event delivery delay in steps (convenience method).

Parameters:

delay_steps (int or array-like) – New delay in simulation time steps. Must be ≥ 1.

Raises:

ValueError – If delay_steps < 1, non-integer, or non-finite.

set_status(status=None, **kwargs)[source]#

Update synapse parameters and state (NEST SetStatus compatible).

Modifies any subset of parameters. Unspecified keys retain current values. Validates all updates before applying (atomic operation). Accepts both dictionary argument and keyword arguments (merged with kwargs taking precedence).

Parameters:
  • status (dict[str, Any] or None, optional) – Dictionary of parameter updates. Keys match those in get_status(). If None, only kwargs are processed.

  • **kwargs – Additional parameter updates as keyword arguments. Merged with status dict. If both status['key'] and key=value are provided for the same parameter, kwargs takes precedence.

Raises:

ValueError

  • If Kplus is set to negative value. - If delay <= 0 or delay_steps < 1. - If both lambda and lambda_ are provided with different values. - If any scalar parameter is non-finite or has size ≠ 1.

Notes

  • The learning rate can be specified as either lambda (NEST convention) or lambda_ (Python identifier). Both refer to the same internal state.

  • Validation occurs after all updates are collected, ensuring atomic updates (all succeed or all fail).

  • Setting lambda=0 disables plasticity without affecting trace dynamics.

Examples

>>> syn = bp.jonke_synapse(weight=5.0, lambda_=0.01)
>>> syn.set_status({'weight': 8.0, 'lambda': 0.005})
>>> syn.get_status()['weight']
8.0
>>> syn.get_status()['lambda']
0.005

Keyword argument syntax:

>>> syn.set_status(Wmax=50.0, mu_plus=0.1)
>>> syn.get_status()['Wmax']
50.0

Disable learning:

>>> syn.set_status(lambda_=0.0)
>>> # Synapse now transmits spikes but does not update weight
set_weight(weight)[source]#

Update synaptic weight (convenience method).

Parameters:

weight (float or array-like) – New synaptic efficacy value. Must be scalar and finite.

Raises:

ValueError – If weight is non-scalar or non-finite.

simulate_pre_spike_train(pre_spike_times_ms, target, receptor_type=0, multiplicity=1.0, delay=None, delay_steps=None)[source]#

Process sequence of presynaptic spikes and track weight evolution.

Convenience method for simulating complete spike train interactions. Sequentially calls send() for each spike time, maintaining plasticity state across spikes. Useful for analyzing STDP curves, weight trajectories, or protocol responses.

Parameters:
  • pre_spike_times_ms (array-like) – Presynaptic spike times in milliseconds. Shape: (n_spikes,) or any shape (will be flattened). Times need not be sorted but should be non-decreasing for physically meaningful trace dynamics.

  • target (object) – Postsynaptic target with required interface (same as send()).

  • receptor_type (int or array-like, default 0) – Receptor channel identifier (constant for all spikes).

  • multiplicity (float or array-like, default 1.0) – Spike amplitude multiplier (constant for all spikes).

  • delay (float or array-like or None, optional) – Override dendritic delay for all spikes (ms). If None, uses self.delay.

  • delay_steps (int or array-like or None, optional) – Override event delivery delay for all spikes (steps).

Returns:

Event payloads for each spike, in order. Length equals len(pre_spike_times_ms). Each dictionary has structure documented in send().

Return type:

list[dict[str, Any]]

Notes

  • State evolution: Synapse state (weight, Kplus, t_last_spike_ms) evolves across the sequence. Final state reflects cumulative plasticity from all spikes.

  • Performance: For large spike trains (>10⁴ spikes), consider batching or vectorized implementations if available.

  • Non-sorted times: If times are unsorted, trace decay may produce unexpected results (negative exponential arguments). Always verify input ordering.

Examples

STDP pairing protocol (pre-post and post-pre pairs):

>>> class PostNeuron:
...     def __init__(self):
...         self.post_spikes = [15.0, 35.0]  # Post-spikes at t=15, 35
...     def get_history(self, t1, t2):
...         return [{'t_': t} for t in self.post_spikes if t1 < t <= t2]
...     def get_K_value(self, t):
...         return 1.0 if t >= 15.0 else 0.0
>>>
>>> syn = bp.jonke_synapse(weight=5.0, lambda_=0.01, tau_plus=20.0)
>>> target = PostNeuron()
>>>
>>> # Pre-spikes at t=[10, 20, 30, 40] ms
>>> events = syn.simulate_pre_spike_train(
...     pre_spike_times_ms=[10.0, 20.0, 30.0, 40.0],
...     target=target
... )
>>>
>>> # Track weight evolution
>>> for i, evt in enumerate(events):
...     print(f"Spike {i}: t={evt['t_spike_ms']:.1f} ms, weight={evt['weight']:.4f}")
Spike 0: t=10.0 ms, weight=5.0000
Spike 1: t=20.0 ms, weight=5.0085
Spike 2: t=30.0 ms, weight=5.0112
Spike 3: t=40.0 ms, weight=5.0098

Extract weight trajectory:

>>> weights = [evt['weight'] for evt in events]
>>> pre_traces = [evt['Kplus_post'] for evt in events]
>>> print(weights)
[5.0, 5.0085, 5.0112, 5.0098]

Frequency-dependent plasticity:

>>> # High-frequency pre-spikes (10 Hz for 1 sec)
>>> times = np.arange(0, 1000, 100)  # t=0, 100, 200, ..., 900 ms
>>> events = syn.simulate_pre_spike_train(times, target)
>>> print(f"Initial weight: {events[0]['weight']:.3f}")
>>> print(f"Final weight: {events[-1]['weight']:.3f}")

See also

send

Single spike processing (core method).

to_spike_event(t_spike_ms, target, receptor_type=0, multiplicity=1.0, delay=None, delay_steps=None)[source]#

Alias for send() method (NEST naming compatibility).

Identical functionality to send(). Provided for API consistency with NEST’s Connection::to_spike_event() naming convention.

Parameters:
  • t_spike_ms (float or array-like) – Presynaptic spike time in milliseconds.

  • target (object) – Postsynaptic target with required interface.

  • receptor_type (int or array-like, default 0) – Receptor channel identifier.

  • multiplicity (float or array-like, default 1.0) – Spike amplitude multiplier.

  • delay (float or array-like or None, optional) – Override dendritic delay (ms).

  • delay_steps (int or array-like or None, optional) – Override event delivery delay (steps).

Returns:

Spike event payload (see send() for structure).

Return type:

dict[str, Any]

See also

send

Primary spike processing method (full documentation).