# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# -*- coding: utf-8 -*-
import math
import brainstate
import saiunit as u
import numpy as np
from brainstate.typing import ArrayLike
from .static_synapse import static_synapse
__all__ = [
'tsodyks_synapse',
]
_UNSET = object()
class tsodyks_synapse(static_synapse):
r"""NEST-compatible ``tsodyks_synapse`` connection model.
``tsodyks_synapse`` implements the short-term plasticity (STP) model of
Tsodyks, Uziel, and Markram (2000), exhibiting both synaptic depression
and facilitation. This synapse tracks three dynamic state variables—recovered
resources ``x``, active resources ``y``, and utilization ``u``—that evolve
between presynaptic spikes and are updated upon spike arrival.
The model replicates NEST ``models/tsodyks_synapse.h`` exactly, including
propagator computation, update ordering, and event timing semantics. Delay
scheduling and receiver delivery inherit from :class:`static_synapse`.
**1. Mathematical Model**
State Variables
---------------
- ``x``: Fraction of resources in the recovered (available) state
- ``y``: Fraction of resources in the active (released) state
- ``u``: Utilization (instantaneous release probability)
- ``z = 1 - x - y``: Fraction of resources in the inactive (unavailable) state
**Constraint:** :math:`x + y + z = 1` at all times; ``x, y, z`` are non-negative.
**Continuous-time dynamics (between spikes):**
.. math::
\frac{du}{dt} = -\frac{u}{\tau_{\mathrm{fac}}}
\frac{dy}{dt} = -\frac{y}{\tau_{\mathrm{psc}}}
\frac{dz}{dt} = -\frac{z}{\tau_{\mathrm{rec}}}
x = 1 - y - z
where:
- :math:`\tau_{\mathrm{fac}}` -- Facilitation time constant (ms). If :math:`\tau_{\mathrm{fac}} = 0`, facilitation is disabled and :math:`u` decays instantly to zero.
- :math:`\tau_{\mathrm{psc}}` -- Time constant of synaptic current decay (ms).
- :math:`\tau_{\mathrm{rec}}` -- Recovery time constant for inactive resources (ms).
**Upon presynaptic spike at time** :math:`t_s`:
Let :math:`h = t_s - t_{\mathrm{last}}` be the inter-spike interval since the last presynaptic spike.
**Step 1: Propagate state from** :math:`t_{\mathrm{last}}` **to** :math:`t_s`:
.. math::
u \leftarrow u \cdot P_{uu}
x \leftarrow x + P_{xy} \, y - P_{zz} \, z
y \leftarrow y \cdot P_{yy}
where the exact propagators are:
.. math::
P_{uu} = \begin{cases}
0, & \tau_{\mathrm{fac}} = 0 \\
e^{-h/\tau_{\mathrm{fac}}}, & \tau_{\mathrm{fac}} > 0
\end{cases}
P_{yy} = e^{-h/\tau_{\mathrm{psc}}}
P_{zz} = e^{-h/\tau_{\mathrm{rec}}} - 1
P_{xy} = \frac{P_{zz} \, \tau_{\mathrm{rec}} - (P_{yy} - 1) \, \tau_{\mathrm{psc}}}
{\tau_{\mathrm{psc}} - \tau_{\mathrm{rec}}}
**Step 2: Spike-triggered facilitation**:
.. math::
u \leftarrow u + U (1 - u)
where :math:`U` is the baseline utilization increment parameter.
**Step 3: Resource release**:
.. math::
\Delta y = u \cdot x
**Step 4: Update resource fractions**:
.. math::
x \leftarrow x - \Delta y
y \leftarrow y + \Delta y
**Step 5: Effective weight delivered to postsynaptic neuron**:
.. math::
w_{\mathrm{eff}} = \Delta y \cdot w
where :math:`w` is the baseline synaptic weight.
**2. Update Ordering and NEST Compatibility**
This implementation preserves the exact update sequence from NEST
``models/tsodyks_synapse.h::send()``:
1. Compute propagators :math:`P_{uu}, P_{yy}, P_{zz}, P_{xy}` from inter-spike interval :math:`h`
2. Propagate utilization: ``u *= P_uu``
3. Propagate recovered resources: ``x += P_xy * y - P_zz * z``
4. Propagate active resources: ``y *= P_yy``
5. Facilitation jump: ``u += U * (1 - u)``
6. Compute release: ``delta_y = u * x``
7. Update resources: ``x -= delta_y``, ``y += delta_y``
8. Schedule weighted event: ``w_eff = delta_y * weight``
**3. Event Timing Semantics**
NEST evaluates this model using spike time stamps (on-grid times) and ignores
precise sub-step offsets. This implementation follows the same convention:
- Presynaptic spike detected at simulation step ``n``
- Spike time stamp: :math:`t_{\mathrm{spike}} = t_n + dt`
- Inter-spike interval: :math:`h = t_{\mathrm{spike}} - t_{\mathrm{lastspike}}`
- Delivery time: :math:`t_{\mathrm{delivery}} = t_{\mathrm{spike}} + \mathrm{delay}`
**4. Stability Constraints and Computational Implications**
**Parameter Constraints:**
- :math:`\tau_{\mathrm{psc}} > 0` (strictly positive)
- :math:`\tau_{\mathrm{fac}} \geq 0` (zero disables facilitation)
- :math:`\tau_{\mathrm{rec}} > 0` (strictly positive)
- :math:`U \in [0, 1]`
- :math:`x, y, u \in [0, 1]`
- :math:`x + y \leq 1` (ensures :math:`z \geq 0`)
Numerical Considerations
------------------------
- Propagators :math:`P_{uu}, P_{yy}, P_{zz}` are computed using ``math.exp()``
and ``math.expm1()`` for numerical stability.
- The cross-propagator :math:`P_{xy}` involves division by :math:`\tau_{\mathrm{psc}} - \tau_{\mathrm{rec}}`.
If these time constants are nearly equal, numerical precision may degrade.
NEST does not provide a singular fallback; users should avoid
:math:`\tau_{\mathrm{psc}} \approx \tau_{\mathrm{rec}}`.
- All state variables are stored as Python floats (``float64`` precision).
- Per-call cost is :math:`O(1)` (scalar operations only).
**Behavioral Regimes:**
- **Depression-dominated** (:math:`\tau_{\mathrm{fac}} = 0`, :math:`U > 0`):
Repeated spikes deplete ``x``, reducing ``delta_y`` over time.
- **Facilitation-dominated** (:math:`\tau_{\mathrm{fac}} > 0`, large :math:`U`):
Utilization ``u`` grows with repeated spikes, increasing release.
- **Mixed dynamics**: Both effects coexist, yielding complex short-term plasticity.
Parameters
----------
weight : ArrayLike, optional
Baseline synaptic weight :math:`w` (dimensionless or with receiver-specific units).
Scalar float or array-like. Default: ``1.0``.
delay : ArrayLike, optional
Synaptic transmission delay :math:`d` in milliseconds. Must be ``> 0``.
Quantized to integer time steps per :class:`static_synapse` conventions.
Scalar with ``saiunit`` time dimension or dimensionless value interpreted
as milliseconds. Default: ``1.0 * u.ms``.
receptor_type : int, optional
Postsynaptic receptor port identifier (non-negative integer). Routes events
to labeled input channels on the receiver neuron. Default: ``0``.
tau_psc : ArrayLike, optional
Time constant of synaptic current decay :math:`\tau_{\mathrm{psc}}` in milliseconds.
Must be ``> 0``. Scalar with ``saiunit`` time dimension or dimensionless
value interpreted as milliseconds. Default: ``3.0 * u.ms``.
tau_fac : ArrayLike, optional
Facilitation time constant :math:`\tau_{\mathrm{fac}}` in milliseconds.
Must be ``>= 0``. Set to ``0.0 * u.ms`` to disable facilitation.
Scalar with ``saiunit`` time dimension or dimensionless value interpreted
as milliseconds. Default: ``0.0 * u.ms``.
tau_rec : ArrayLike, optional
Recovery (depression) time constant :math:`\tau_{\mathrm{rec}}` in milliseconds.
Must be ``> 0``. Scalar with ``saiunit`` time dimension or dimensionless
value interpreted as milliseconds. Default: ``800.0 * u.ms``.
U : ArrayLike, optional
Baseline utilization increment parameter :math:`U` (dimensionless).
Must be in ``[0, 1]``. Determines the magnitude of facilitation per spike.
Scalar float. Default: ``0.5``.
x : ArrayLike, optional
Initial fraction of recovered resources (dimensionless). Must be in ``[0, 1]``.
Together with ``y``, must satisfy ``x + y <= 1``. Scalar float. Default: ``1.0``.
y : ArrayLike, optional
Initial fraction of active resources (dimensionless). Must be in ``[0, 1]``.
Together with ``x``, must satisfy ``x + y <= 1``. Scalar float. Default: ``0.0``.
u : ArrayLike, optional
Initial utilization value (dimensionless). Must be in ``[0, 1]``.
Scalar float. Default: ``0.0``.
post : object, optional
Default postsynaptic receiver neuron. If provided, this neuron will be the
target for all :meth:`send` calls unless overridden by the ``post`` argument
in :meth:`send` or :meth:`update`. Default: ``None``.
name : str, optional
Unique identifier for this synapse instance. Used for debugging and logging.
Default: ``None`` (auto-generated).
Parameter Mapping
-----------------
========================= =================== =========================================
NEST Parameter brainpy.state Description
========================= =================== =========================================
``weight`` ``weight`` Baseline synaptic weight
``delay`` ``delay`` Synaptic delay (ms)
``receptor_type`` ``receptor_type`` Postsynaptic receptor port
``tau_psc`` ``tau_psc`` Synaptic current time constant (ms)
``tau_fac`` ``tau_fac`` Facilitation time constant (ms)
``tau_rec`` ``tau_rec`` Recovery time constant (ms)
``U`` ``U`` Utilization increment parameter
``x`` ``x`` Recovered resources state variable
``y`` ``y`` Active resources state variable
``u`` ``u`` Utilization state variable
========================= =================== =========================================
Attributes
----------
x : float
Current fraction of recovered resources. Mutable state variable.
y : float
Current fraction of active resources. Mutable state variable.
u : float
Current utilization value. Mutable state variable.
t_lastspike : float
Time stamp of the last presynaptic spike (ms). Used to compute inter-spike
intervals for propagator calculations.
Notes
-----
- **Event Type**: This model transmits ``'spike'`` events only. Other event
types (``'rate'``, ``'current'``, ``'conductance'``) are not supported.
- **State Variables**: ``x``, ``y``, and ``u`` are mutable per-connection states.
They can be inspected via :meth:`get` and modified via :meth:`set`.
- **Initialization**: Calling ``init_state()`` resets the internal event queue
and restores ``x``, ``y``, ``u`` to their initial values (``self._x0``,
``self._y0``, ``self._u0``). It also resets ``t_lastspike`` to ``0.0``.
- **Scalar-Only**: All parameters and state variables are scalar floats. This
model does not support vectorized per-connection parameters.
- **No Precise Timing**: Unlike some NEST models with ``_ps`` variants, this
implementation uses on-grid spike stamps and does not track sub-step offsets.
See Also
--------
tsodyks_synapse_hom : Homogeneous variant with shared state across all connections.
tsodyks2_synapse : Alternative Tsodyks model with different parameterization.
static_synapse : Base class for non-plastic synaptic connections.
References
----------
.. [1] Tsodyks M, Uziel A, Markram H (2000). Synchrony generation in recurrent
networks with frequency-dependent synapses. Journal of Neuroscience,
20(RC50):1-5.
.. [2] NEST source code: ``models/tsodyks_synapse.h`` and ``models/tsodyks_synapse.cpp``
(https://github.com/nest/nest-simulator)
.. [3] Tsodyks M, Markram H (1997). The neural code between neocortical pyramidal
neurons depends on neurotransmitter release probability. PNAS, 94(2):719-723.
Examples
--------
**1. Depression-dominated synapse (excitatory with depletion):**
.. code-block:: python
>>> import brainpy.state as bp
>>> import saiunit as u
>>> syn = bp.nest.tsodyks_synapse(
... weight=1.0,
... delay=1.5 * u.ms,
... tau_psc=5.0 * u.ms,
... tau_fac=0.0 * u.ms, # no facilitation
... tau_rec=800.0 * u.ms,
... U=0.5,
... x=1.0,
... y=0.0,
... u=0.0
... )
**2. Facilitation-dominated synapse (inhibitory with strengthening):**
.. code-block:: python
>>> syn = bp.nest.tsodyks_synapse(
... weight=-2.0, # inhibitory
... delay=1.0 * u.ms,
... tau_psc=3.0 * u.ms,
... tau_fac=200.0 * u.ms, # strong facilitation
... tau_rec=800.0 * u.ms,
... U=0.15,
... x=1.0,
... y=0.0,
... u=0.0
... )
**3. Simulating short-term plasticity:**
.. code-block:: python
>>> import brainstate as bst
>>> with bst.environ.context(dt=0.1 * u.ms):
... syn.init_all_states()
... # Simulate spike train at 50 Hz
... spike_times = [0.0, 20.0, 40.0, 60.0, 80.0] # ms
... for t_spike in spike_times:
... # Advance simulation to spike time
... # ... (step simulation forward)
... syn.send(multiplicity=1.0)
... print(f"t={t_spike:.1f} ms: u={syn.u:.3f}, x={syn.x:.3f}, y={syn.y:.3f}")
**4. Inspecting and modifying state:**
.. code-block:: python
>>> params = syn.get()
>>> print(params['x'], params['y'], params['u'])
1.0 0.0 0.0
>>> syn.set(x=0.8, y=0.1, u=0.3)
>>> print(syn.x, syn.y, syn.u)
0.8 0.1 0.3
**5. Multi-receptor connection:**
.. code-block:: python
>>> syn_ex = bp.nest.tsodyks_synapse(
... weight=1.0, delay=1.0 * u.ms, receptor_type=0, U=0.5
... )
>>> syn_in = bp.nest.tsodyks_synapse(
... weight=-1.0, delay=1.0 * u.ms, receptor_type=1, U=0.25
... )
>>> # Excitatory and inhibitory inputs routed to different receptor ports
"""
__module__ = 'brainpy.state'
def __init__(
self,
weight: ArrayLike = 1.0,
delay: ArrayLike = 1.0 * u.ms,
receptor_type: int = 0,
tau_psc: ArrayLike = 3.0 * u.ms,
tau_fac: ArrayLike = 0.0 * u.ms,
tau_rec: ArrayLike = 800.0 * u.ms,
U: ArrayLike = 0.5,
x: ArrayLike = 1.0,
y: ArrayLike = 0.0,
u: ArrayLike = 0.0,
post=None,
name: str | None = None,
):
super().__init__(
weight=weight,
delay=delay,
receptor_type=receptor_type,
post=post,
event_type='spike',
name=name,
)
self.tau_psc = self._to_scalar_time_ms(tau_psc, name='tau_psc')
self.tau_fac = self._to_scalar_time_ms(tau_fac, name='tau_fac')
self.tau_rec = self._to_scalar_time_ms(tau_rec, name='tau_rec')
self.U = self._to_scalar_unit_interval(U, name='U')
x0 = self._to_scalar_float(x, name='x')
y0 = self._to_scalar_float(y, name='y')
u0 = self._to_scalar_unit_interval(u, name='u')
self._validate_tau_psc(self.tau_psc)
self._validate_tau_fac(self.tau_fac)
self._validate_tau_rec(self.tau_rec)
self._validate_xy_sum(x0, y0)
self._x0 = float(x0)
self._y0 = float(y0)
self._u0 = float(u0)
self.x = float(self._x0)
self.y = float(self._y0)
self.u = float(self._u0)
self.t_lastspike = 0.0
@staticmethod
def _to_scalar_float(value: ArrayLike, *, name: str) -> float:
dftype = brainstate.environ.dftype()
arr = np.asarray(u.math.asarray(value, dtype=dftype), dtype=dftype)
if arr.size != 1:
raise ValueError(f'{name} must be scalar.')
return float(arr.reshape(()))
@staticmethod
def _to_scalar_unit_interval(value: ArrayLike, *, name: str) -> float:
v = tsodyks_synapse._to_scalar_float(value, name=name)
if v < 0.0 or v > 1.0:
raise ValueError(f"'{name}' must be in [0,1].")
return float(v)
@staticmethod
def _validate_tau_psc(value: float):
if value <= 0.0:
raise ValueError("'tau_psc' must be > 0.")
@staticmethod
def _validate_tau_fac(value: float):
if value < 0.0:
raise ValueError("'tau_fac' must be >= 0.")
@staticmethod
def _validate_tau_rec(value: float):
if value <= 0.0:
raise ValueError("'tau_rec' must be > 0.")
@staticmethod
def _validate_xy_sum(x: float, y: float):
if x + y > 1.0:
raise ValueError('x + y must be <= 1.0.')
[docs]
def init_state(self, batch_size: int = None, **kwargs):
r"""Initialize or reset all state variables to their configured initial values.
Resets the internal event delivery queue (via ``super().init_state()``) and
restores the short-term plasticity state variables ``x``, ``y``, ``u`` to
their initial values (``self._x0``, ``self._y0``, ``self._u0``). Also resets
the last spike time stamp to ``0.0``.
Parameters
----------
batch_size : int, optional
Ignored. This scalar synapse model does not support batching.
**kwargs
Additional keyword arguments. Ignored.
Notes
-----
- This method is typically called once at the start of a simulation or when
resetting the network state.
- After calling this method, the synapse behaves as if no presynaptic spikes
have occurred yet (``t_lastspike = 0.0``).
"""
del batch_size, kwargs
super().init_state()
self.x = float(self._x0)
self.y = float(self._y0)
self.u = float(self._u0)
self.t_lastspike = 0.0
[docs]
def get(self) -> dict:
r"""Return current public parameters and mutable state variables.
Retrieves all NEST-visible synapse parameters, including the baseline weight,
delay, receptor type (from ``super().get()``), time constants, utilization
parameter, and current state variables ``x``, ``y``, ``u``.
Returns
-------
dict
Dictionary with the following keys:
- ``'weight'`` (float): Baseline synaptic weight
- ``'delay'`` (float): Synaptic delay in ms
- ``'receptor_type'`` (int): Postsynaptic receptor port
- ``'tau_psc'`` (float): Synaptic current time constant in ms
- ``'tau_fac'`` (float): Facilitation time constant in ms
- ``'tau_rec'`` (float): Recovery time constant in ms
- ``'U'`` (float): Utilization increment parameter
- ``'x'`` (float): Current recovered resources
- ``'y'`` (float): Current active resources
- ``'u'`` (float): Current utilization value
- ``'synapse_model'`` (str): Model identifier (``'tsodyks_synapse'``)
Notes
-----
- The returned dictionary reflects the *current* state at the time of the call.
State variables ``x``, ``y``, ``u`` evolve during simulation.
- This method is compatible with NEST's ``GetStatus()`` semantics.
Examples
--------
.. code-block:: python
>>> syn = bp.nest.tsodyks_synapse(U=0.5, tau_rec=800.0 * u.ms)
>>> params = syn.get()
>>> print(params['U'], params['tau_rec'], params['x'])
0.5 800.0 1.0
"""
params = super().get()
params['tau_psc'] = float(self.tau_psc)
params['tau_fac'] = float(self.tau_fac)
params['tau_rec'] = float(self.tau_rec)
params['U'] = float(self.U)
params['x'] = float(self.x)
params['y'] = float(self.y)
params['u'] = float(self.u)
params['synapse_model'] = 'tsodyks_synapse'
return params
[docs]
def set(
self,
*,
weight: ArrayLike | object = _UNSET,
delay: ArrayLike | object = _UNSET,
receptor_type: ArrayLike | object = _UNSET,
tau_psc: ArrayLike | object = _UNSET,
tau_fac: ArrayLike | object = _UNSET,
tau_rec: ArrayLike | object = _UNSET,
U: ArrayLike | object = _UNSET,
x: ArrayLike | object = _UNSET,
y: ArrayLike | object = _UNSET,
u: ArrayLike | object = _UNSET,
post: object = _UNSET,
):
r"""Set NEST-style public parameters and state variables.
Updates synapse parameters and/or state variables. Only parameters explicitly
provided (not ``_UNSET``) are modified. All changes are validated before
application. If ``x`` or ``y`` are updated, their sum is checked to ensure
``x + y <= 1``.
Updating state variables ``x``, ``y``, or ``u`` also updates their initial
values (``self._x0``, ``self._y0``, ``self._u0``), so subsequent calls to
``init_state()`` will restore to the newly set values.
Parameters
----------
weight : ArrayLike, optional
New baseline synaptic weight. If not provided, weight is unchanged.
delay : ArrayLike, optional
New synaptic delay in ms. Must be ``> 0``. If not provided, delay is unchanged.
receptor_type : int, optional
New postsynaptic receptor port. Must be a non-negative integer. If not
provided, receptor type is unchanged.
tau_psc : ArrayLike, optional
New synaptic current time constant in ms. Must be ``> 0``. If not provided,
``tau_psc`` is unchanged.
tau_fac : ArrayLike, optional
New facilitation time constant in ms. Must be ``>= 0``. If not provided,
``tau_fac`` is unchanged.
tau_rec : ArrayLike, optional
New recovery time constant in ms. Must be ``> 0``. If not provided,
``tau_rec`` is unchanged.
U : ArrayLike, optional
New utilization increment parameter. Must be in ``[0, 1]``. If not provided,
``U`` is unchanged.
x : ArrayLike, optional
New recovered resources value. Must be in ``[0, 1]``. Together with ``y``,
must satisfy ``x + y <= 1``. If not provided, ``x`` is unchanged.
y : ArrayLike, optional
New active resources value. Must be in ``[0, 1]``. Together with ``x``,
must satisfy ``x + y <= 1``. If not provided, ``y`` is unchanged.
u : ArrayLike, optional
New utilization value. Must be in ``[0, 1]``. If not provided, ``u`` is unchanged.
post : object, optional
New default postsynaptic receiver neuron. If not provided, receiver is unchanged.
Raises
------
ValueError
If any parameter violates its constraint (e.g., ``tau_psc <= 0``, ``U`` out
of range, ``x + y > 1``).
Notes
-----
- This method is compatible with NEST's ``SetStatus()`` semantics.
- Parameter validation occurs *before* any state is modified. If validation
fails, no changes are applied.
- Updating state variables mid-simulation can produce non-physical dynamics.
Use with caution outside of initialization or testing contexts.
Examples
--------
.. code-block:: python
>>> syn = bp.nest.tsodyks_synapse(U=0.5)
>>> syn.set(U=0.8, tau_rec=600.0 * u.ms)
>>> print(syn.U, syn.tau_rec)
0.8 600.0
>>> syn.set(x=0.7, y=0.2)
>>> print(syn.x, syn.y)
0.7 0.2
>>> syn.set(x=0.8, y=0.3) # doctest: +SKIP
ValueError: x + y must be <= 1.0.
"""
new_tau_psc = (
self.tau_psc
if tau_psc is _UNSET
else self._to_scalar_time_ms(tau_psc, name='tau_psc')
)
new_tau_fac = (
self.tau_fac
if tau_fac is _UNSET
else self._to_scalar_time_ms(tau_fac, name='tau_fac')
)
new_tau_rec = (
self.tau_rec
if tau_rec is _UNSET
else self._to_scalar_time_ms(tau_rec, name='tau_rec')
)
new_U = self.U if U is _UNSET else self._to_scalar_unit_interval(U, name='U')
new_x = self.x if x is _UNSET else self._to_scalar_float(x, name='x')
new_y = self.y if y is _UNSET else self._to_scalar_float(y, name='y')
new_u = self.u if u is _UNSET else self._to_scalar_unit_interval(u, name='u')
self._validate_tau_psc(float(new_tau_psc))
self._validate_tau_fac(float(new_tau_fac))
self._validate_tau_rec(float(new_tau_rec))
self._validate_xy_sum(float(new_x), float(new_y))
super_kwargs = {}
if weight is not _UNSET:
super_kwargs['weight'] = weight
if delay is not _UNSET:
super_kwargs['delay'] = delay
if receptor_type is not _UNSET:
super_kwargs['receptor_type'] = receptor_type
if post is not _UNSET:
super_kwargs['post'] = post
if super_kwargs:
super().set(**super_kwargs)
self.tau_psc = float(new_tau_psc)
self.tau_fac = float(new_tau_fac)
self.tau_rec = float(new_tau_rec)
self.U = float(new_U)
self.x = float(new_x)
self.y = float(new_y)
self.u = float(new_u)
self._x0 = float(self.x)
self._y0 = float(self.y)
self._u0 = float(self.u)
[docs]
def send(
self,
multiplicity: ArrayLike = 1.0,
*,
post=None,
receptor_type: ArrayLike | None = None,
) -> bool:
r"""Schedule one outgoing event with NEST ``tsodyks_synapse`` short-term plasticity dynamics.
Processes a presynaptic spike event by:
1. Propagating state variables ``u``, ``x``, ``y`` from the last spike time to the current spike time.
2. Applying spike-triggered facilitation to ``u``.
3. Computing the released resource fraction ``delta_y = u * x``.
4. Updating resource fractions ``x`` and ``y``.
5. Scheduling a weighted event (``delta_y * weight * multiplicity``) for delivery to the postsynaptic neuron.
The update ordering exactly matches NEST ``models/tsodyks_synapse.h::send()``.
Parameters
----------
multiplicity : ArrayLike, optional
Presynaptic spike count (typically ``1.0`` for a single spike or ``0.0`` for
no spike). Can be a float representing spike rate or an integer spike count.
Default: ``1.0``.
post : object, optional
Postsynaptic receiver neuron. If ``None``, uses the default receiver
specified at construction (``self.post``). Default: ``None``.
receptor_type : ArrayLike, optional
Receptor port to target on the postsynaptic neuron. If ``None``, uses
``self.receptor_type``. Default: ``None``.
Returns
-------
bool
``True`` if an event was scheduled (i.e., ``multiplicity`` is non-zero),
``False`` otherwise.
Notes
-----
- **Event Timing**: The spike time stamp is computed as ``current_time + dt``
(on-grid time). Inter-spike interval ``h`` is the difference between the
current spike stamp and ``self.t_lastspike``.
- **State Update**: State variables ``u``, ``x``, ``y`` are updated *in place*
and persist across calls. ``t_lastspike`` is updated to the current spike stamp.
- **Zero Multiplicity**: If ``multiplicity`` is zero or negligible, no event
is scheduled and state variables are *not* updated. Returns ``False``.
- **Effective Weight**: The delivered payload is ``delta_y * weight * multiplicity``,
where ``delta_y`` is the released resource fraction computed from current state.
Warnings
--------
- If ``tau_psc`` and ``tau_rec`` are numerically close, the propagator ``P_xy``
may suffer from floating-point cancellation. Users should avoid configurations
where ``abs(tau_psc - tau_rec) < 1e-6``.
Examples
--------
.. code-block:: python
>>> import brainstate as bst
>>> with bst.environ.context(dt=0.1 * u.ms):
... syn = bp.nest.tsodyks_synapse(weight=1.0, U=0.5, tau_rec=800.0 * u.ms)
... syn.init_all_states()
... # First spike
... success = syn.send(multiplicity=1.0)
... print(f"Scheduled: {success}, u={syn.u:.3f}, x={syn.x:.3f}")
Scheduled: True, u=0.500, x=0.500
... # Second spike 20 ms later (simulate time advancement)
... success = syn.send(multiplicity=1.0)
... print(f"Scheduled: {success}, u={syn.u:.3f}, x={syn.x:.3f}")
Scheduled: True, u=0.750, x=0.125
"""
if not self._is_nonzero(multiplicity):
return False
dt_ms = self._refresh_delay_if_needed()
current_step = self._curr_step(dt_ms)
# NEST uses the spike stamp and ignores precise sub-step offsets.
t_spike = self._current_time_ms() + dt_ms
h = float(t_spike - self.t_lastspike)
puu = 0.0 if self.tau_fac == 0.0 else math.exp(-h / self.tau_fac)
pyy = math.exp(-h / self.tau_psc)
pzz = math.expm1(-h / self.tau_rec)
pxy = (pzz * self.tau_rec - (pyy - 1.0) * self.tau_psc) / (self.tau_psc - self.tau_rec)
z = 1.0 - self.x - self.y
# Keep ordering identical to NEST models/tsodyks_synapse.h::send.
self.u *= puu
self.x += pxy * self.y - pzz * z
self.y *= pyy
self.u += self.U * (1.0 - self.u)
delta_y_tsp = self.u * self.x
self.x -= delta_y_tsp
self.y += delta_y_tsp
weighted_payload = multiplicity * (delta_y_tsp * self.weight)
receiver = self._resolve_receiver(post)
rport = self.receptor_type if receptor_type is None else self._to_receptor_type(receptor_type)
delivery_step = int(current_step + int(self._delay_steps))
self._queue[delivery_step].append((receiver, weighted_payload, int(rport), 'spike'))
self.t_lastspike = float(t_spike)
return True
[docs]
def update(
self,
pre_spike: ArrayLike = 0.0,
*,
post=None,
receptor_type: ArrayLike | None = None,
) -> int:
r"""Deliver due events and process current-step presynaptic input.
This method performs two tasks per simulation time step:
1. **Deliver pending events**: Dequeue and dispatch all events scheduled for
delivery at the current simulation time step (via ``_deliver_due_events()``).
2. **Process presynaptic input**: Collect current-step and delta inputs
(via ``sum_current_inputs()`` and ``sum_delta_inputs()``), then schedule
a new event if total input is non-zero (via ``send()``).
This is the main per-step entry point for synapse dynamics when integrated
into a simulation loop.
Parameters
----------
pre_spike : ArrayLike, optional
Presynaptic spike count or rate for the current simulation step. Typically
``0.0`` (no spike) or ``1.0`` (spike). This value is accumulated with any
other inputs registered via ``add_current_input()`` or ``add_delta_input()``.
Default: ``0.0``.
post : object, optional
Postsynaptic receiver neuron. If ``None``, uses the default receiver
specified at construction. Default: ``None``.
receptor_type : ArrayLike, optional
Receptor port to target. If ``None``, uses ``self.receptor_type``. Default: ``None``.
Returns
-------
int
Number of events delivered during this step (from the delivery queue).
Notes
-----
- **Execution Order**: Delivery precedes scheduling. Events scheduled at step
``n`` are delivered at step ``n + delay_steps``.
- **Input Accumulation**: ``pre_spike`` is summed with any inputs registered
via the ``current_inputs`` and ``delta_inputs`` dictionaries (inherited from
:class:`Dynamics`). The total determines whether a new event is scheduled.
- **Typical Usage**: Call this method once per simulation time step in a network
update loop, after presynaptic neuron spike detection.
Examples
--------
.. code-block:: python
>>> import brainstate as bst
>>> with bst.environ.context(dt=0.1 * u.ms):
... syn = bp.nest.tsodyks_synapse(weight=1.0, delay=1.0 * u.ms, U=0.5)
... syn.init_all_states()
... # Simulate one step with a presynaptic spike
... delivered_count = syn.update(pre_spike=1.0)
... print(f"Delivered: {delivered_count}, State: u={syn.u:.3f}, x={syn.x:.3f}")
Delivered: 0, State: u=0.500, x=0.500
... # Advance simulation by delay steps (typically via outer loop)
... # ... (advance time by 1.0 ms)
... delivered_count = syn.update(pre_spike=0.0)
... print(f"Delivered: {delivered_count}")
Delivered: 1
"""
dt_ms = self._refresh_delay_if_needed()
step = self._curr_step(dt_ms)
delivered = self._deliver_due_events(step)
total = self.sum_current_inputs(pre_spike)
total = self.sum_delta_inputs(total)
if self._is_nonzero(total):
self.send(total, post=post, receptor_type=receptor_type)
return delivered