Source code for brainpy_state._nest.tsodyks_synapse_hom

# 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
from collections.abc import Mapping

import brainstate
import saiunit as u
import jax.numpy as jnp
import numpy as np
from brainstate.typing import ArrayLike

from .static_synapse_hom_w import static_synapse_hom_w

__all__ = [
    'tsodyks_synapse_hom',
]

_UNSET = object()


class tsodyks_synapse_hom(static_synapse_hom_w):
    r"""NEST-compatible ``tsodyks_synapse_hom`` connection model with homogeneous short-term plasticity.

    **1. Short Description**

    Synapse type with short-term plasticity using homogeneous parameters across all connections,
    implementing the Tsodyks-Markram model with depressing and facilitating dynamics.

    **2. Mathematical Description**

    ``tsodyks_synapse_hom`` mirrors NEST ``models/tsodyks_synapse_hom.h`` and implements the
    short-term plasticity model of Tsodyks, Uziel and Markram (2000) [2]_, with dynamic
    per-connection state variables:

    - ``x``: fraction of resources in recovered state (available for release)
    - ``y``: fraction of resources in active state (released but not yet decayed)
    - ``u``: utilization (release probability upon spike arrival)
    - ``z = 1 - x - y``: fraction of resources in inactive state (released and decayed, recovering)

    For a presynaptic spike at time :math:`t_s` with inter-spike interval
    :math:`h = t_s - t_{\mathrm{last}}`, NEST updates states in this exact order:

    **Step 1: Continuous-time propagation from** :math:`t_{\mathrm{last}}` **to** :math:`t_s`

    First compute propagation factors:

    .. 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}}} \\
        P_{xy} &= \frac{(P_{zz}-1)\tau_{\mathrm{rec}} - (P_{yy}-1)\tau_{\mathrm{psc}}}
                       {\tau_{\mathrm{psc}}-\tau_{\mathrm{rec}}} \\
        P_{xz} &= 1 - P_{zz}

    Then update state variables:

    .. math::
        u &\leftarrow u \cdot P_{uu} \\
        x &\leftarrow x + P_{xy} y + P_{xz} z \\
        y &\leftarrow y \cdot P_{yy}

    **Step 2: Spike-triggered facilitation**

    .. math::
        u \leftarrow u + U(1-u)

    where :math:`U` is the utilization increment parameter.

    **Step 3: Resource release calculation**

    .. math::
        \Delta y = u \cdot x

    This is the fraction of resources released by the current spike.

    **Step 4: State updates for resource depletion**

    .. math::
        x &\leftarrow x - \Delta y \\
        y &\leftarrow y + \Delta y

    **Step 5: Event amplitude computation**

    .. math::
        w_{\mathrm{eff}} = \Delta y \cdot w

    where :math:`w` is the common synaptic weight.

    This implementation preserves the exact NEST ordering in :meth:`send`. Delay
    scheduling and delivery follow :class:`static_synapse_hom_w`.

    **3. Homogeneous-Property Semantics**

    In NEST, ``weight``, ``U``, ``tau_psc``, ``tau_fac`` and ``tau_rec`` are common
    synapse-model properties (stored in ``TsodyksHomCommonProperties``), shared across
    all connections using this synapse type. In contrast, ``x``, ``y`` and ``u`` are
    per-connection state variables that evolve independently for each synapse.

    This implementation mirrors that behavior by:

    - Forbidding ``set_weight(...)`` calls (same as NEST)
    - Rejecting these common properties in connect-time synapse specs via
      :meth:`check_synapse_params`
    - Allowing model-level parameter updates via :meth:`set(...)`

    **4. Event Timing Semantics**

    As in NEST, this model uses spike timestamps (grid-aligned) and ignores precise
    sub-step offsets when computing plasticity updates. All spikes are treated as
    occurring at the beginning of their respective time step.

    Parameters
    ----------
    weight : float or array-like, optional
        Common synaptic weight (dimensionless). Shared across all connections. Must be scalar.
        Default: ``1.0``.
    delay : Quantity (time) or array-like, optional
        Synaptic transmission delay. Must be positive. Scalar or broadcastable to connection count.
        Default: ``1.0 * u.ms``.
    receptor_type : int, optional
        Target receptor port ID on the postsynaptic neuron. Default: ``0``.
    tau_psc : Quantity (time) or array-like, optional
        Time constant for postsynaptic current (active resource decay). Must be ``> 0`` and scalar.
        Corresponds to NEST's ``tau_psc`` parameter. Default: ``3.0 * u.ms``.
    tau_fac : Quantity (time) or array-like, optional
        Facilitation time constant (utilization decay). Must be ``>= 0`` and scalar.
        When ``tau_fac = 0``, facilitation is disabled (:math:`P_{uu} = 0`).
        Corresponds to NEST's ``tau_fac`` parameter. Default: ``0.0 * u.ms``.
    tau_rec : Quantity (time) or array-like, optional
        Recovery time constant (inactive → recovered transition). Must be ``> 0`` and scalar.
        Corresponds to NEST's ``tau_rec`` parameter. Default: ``800.0 * u.ms``.
    U : float or array-like, optional
        Utilization increment parameter. Must be in ``[0, 1]`` and scalar.
        Determines the spike-triggered increase in release probability.
        Corresponds to NEST's ``U`` parameter. Default: ``0.5``.
    x : float or array-like, optional
        Initial fraction of recovered resources. Must satisfy ``x + y <= 1`` and be scalar.
        Default: ``1.0`` (all resources initially available).
    y : float or array-like, optional
        Initial fraction of active resources. Must satisfy ``x + y <= 1`` and be scalar.
        Default: ``0.0`` (no resources initially active).
    u : float or array-like, optional
        Initial utilization state (release probability). Must be scalar.
        NEST stores this as mutable per-connection state without enforcing bounds on assignment.
        Default: ``0.0``.
    post : object, optional
        Default postsynaptic target object. Can be overridden in :meth:`send` and :meth:`update`.
    name : str, optional
        Descriptive name for this synapse model instance.

    Parameter Mapping
    -----------------

    ================  ==========================  ==========  ===========
    NEST Parameter    brainpy.state Parameter     Type        Scope
    ================  ==========================  ==========  ===========
    weight            weight                      float       Common
    delay             delay                       Quantity    Common
    receptor_type     receptor_type               int         Common
    tau_psc           tau_psc                     Quantity    Common
    tau_fac           tau_fac                     Quantity    Common
    tau_rec           tau_rec                     Quantity    Common
    U                 U                           float       Common
    x                 x                           float       State
    y                 y                           float       State
    u                 u                           float       State
    ================  ==========================  ==========  ===========

    Notes
    -----
    - **Event type**: This model transmits spike-like events only, not continuous currents.
    - **State initialization**: ``init_state()`` resets queue state and restores ``x``, ``y``, ``u``
      to their constructor-specified initial values.
    - **Last-spike timestamp**: ``t_lastspike`` starts at ``0.0`` ms (as in NEST). If ``x != 1``
      initially, the first-spike dynamics depend on this initial timestamp.
    - **Homogeneous constraints**: Common properties (``weight``, ``U``, ``tau_psc``, ``tau_fac``,
      ``tau_rec``) cannot be set per-connection via connect-time synapse specs. Use ``set()`` to
      modify them at the model level.
    - **Validation**: Constructor validates ``tau_psc > 0``, ``tau_fac >= 0``, ``tau_rec > 0``,
      ``U ∈ [0,1]``, and ``x + y <= 1``. ``set()`` re-validates on updates.
    - **Numerical stability**: When ``tau_psc ≈ tau_rec``, the :math:`P_{xy}` calculation may
      become numerically unstable. NEST uses the same formula without special handling.

    See Also
    --------
    tsodyks_synapse : Heterogeneous variant with per-connection parameters
    static_synapse_hom_w : Parent class for homogeneous static connections

    References
    ----------
    .. [1] NEST source: ``models/tsodyks_synapse_hom.h`` and
           ``models/tsodyks_synapse_hom.cpp``.
    .. [2] Tsodyks M, Uziel A, Markram H (2000). Synchrony generation in
           recurrent networks with frequency-dependent synapses.
           Journal of Neuroscience, 20:RC50.
           https://www.jneurosci.org/content/20/1/RC50

    Examples
    --------
    Create a depressing synapse (default parameters):

    .. code-block:: python

        >>> import brainpy.state as bs
        >>> import saiunit as u
        >>> syn = bs.tsodyks_synapse_hom(
        ...     weight=2.0,
        ...     delay=1.5 * u.ms,
        ...     U=0.5,
        ...     tau_rec=800.0 * u.ms,
        ...     tau_fac=0.0 * u.ms,  # no facilitation
        ... )

    Create a facilitating synapse:

    .. code-block:: python

        >>> syn_fac = bs.tsodyks_synapse_hom(
        ...     weight=1.5,
        ...     U=0.15,
        ...     tau_rec=200.0 * u.ms,
        ...     tau_fac=750.0 * u.ms,  # strong facilitation
        ...     tau_psc=5.0 * u.ms,
        ... )

    Update common properties (e.g., increase facilitation):

    .. code-block:: python

        >>> syn_fac.set(tau_fac=1000.0 * u.ms, U=0.2)

    Initialize state for simulation:

    .. code-block:: python

        >>> syn.init_state()
        >>> print(syn.x, syn.y, syn.u)  # (1.0, 0.0, 0.0)

    Retrieve current parameters and state:

    .. code-block:: python

        >>> params = syn.get()
        >>> print(params['synapse_model'])  # 'tsodyks_synapse_hom'
        >>> print(params['U'], params['tau_rec'])  # (0.5, 800.0)
    """

    __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_no_quotes(U, name='U')

        x0 = self._to_scalar_float(x, name='x')
        y0 = self._to_scalar_float(y, name='y')
        u0 = self._to_scalar_float(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_no_quotes(value: ArrayLike, *, name: str) -> float:
        v = tsodyks_synapse_hom._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"""Reset queue state and restore initial resource/utilization values. Resets the event delivery queue (inherited from parent) and restores the short-term plasticity state variables (``x``, ``y``, ``u``) to their constructor-specified initial values (``_x0``, ``_y0``, ``_u0``). Also resets the last-spike timestamp to ``0.0`` ms. Parameters ---------- batch_size : int, optional Ignored for scalar synapse models (retained for API compatibility). **kwargs : dict, optional Ignored (retained for API compatibility). Notes ----- - This method should be called before starting a new simulation to ensure consistent initial conditions. - After calling ``init_state()``, the synapse is in a "naive" state with ``t_lastspike = 0.0``, meaning the first spike will have inter-spike interval :math:`h = t_{\mathrm{spike}} - 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. Retrieves all public parameters (``weight``, ``delay``, ``receptor_type``, ``tau_psc``, ``tau_fac``, ``tau_rec``, ``U``) and current state variables (``x``, ``y``, ``u``) as a dictionary. Inherited parameters are retrieved via ``super().get()``. Returns ------- dict Dictionary with keys: - ``'weight'`` (float): Common synaptic weight. - ``'delay'`` (float): Synaptic delay in ms. - ``'receptor_type'`` (int): Receiver port ID. - ``'tau_psc'`` (float): PSC 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 resource fraction. - ``'y'`` (float): Current active resource fraction. - ``'u'`` (float): Current utilization state. - ``'synapse_model'`` (str): Always ``'tsodyks_synapse_hom'``. Notes ----- - All time constants are returned in milliseconds (converted from internal storage). - State variables (``x``, ``y``, ``u``) reflect the current simulation state, not initial values. - This method is compatible with NEST's ``GetStatus()`` semantics. """ 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_hom' return params
[docs] def check_synapse_params(self, syn_spec: Mapping[str, object] | None): r"""Reject common-property assignments in connect-time synapse specs. Validates that connect-time synapse specifications do not attempt to override common (homogeneous) properties that must be shared across all connections. This enforces NEST's homogeneous synapse semantics where ``weight``, ``U``, ``tau_psc``, ``tau_fac``, and ``tau_rec`` are model-level properties. Parameters ---------- syn_spec : dict or None Synapse specification dictionary passed during connection creation. If ``None``, validation is skipped (no parameters to check). Raises ------ ValueError If ``syn_spec`` contains any of the disallowed keys: ``'weight'``, ``'U'``, ``'tau_psc'``, ``'tau_rec'``, or ``'tau_fac'``. These must be set at the model level using :meth:`set` (or NEST's ``CopyModel``/``SetDefaults``). Notes ----- - This method is called internally during connection setup to prevent misuse. - In NEST, ``weight`` is a common property for ``tsodyks_synapse_hom`` but can vary per-connection in ``tsodyks_synapse``. This model follows the ``_hom`` variant's restrictions. - To modify common properties, use ``model.set(weight=..., U=..., ...)`` before or during simulation, not during connection creation. """ if syn_spec is None: return disallowed = ('weight', 'U', 'tau_psc', 'tau_rec', 'tau_fac') for key in disallowed: if key in syn_spec: raise ValueError( f'{key} cannot be specified in connect-time synapse parameters ' 'for tsodyks_synapse_hom; set common properties on the model ' 'itself (for example via CopyModel()/SetDefaults()).' )
[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 public parameters following NEST-style validation semantics. Updates model parameters and/or state variables with comprehensive validation. All parameters are optional; only specified values are updated. This method mirrors NEST's ``SetStatus()`` for homogeneous synapse models. Parameters ---------- weight : float or array-like, optional New common synaptic weight (dimensionless). Must be scalar. delay : Quantity (time) or array-like, optional New synaptic transmission delay. Must be positive. receptor_type : int, optional New target receptor port ID. tau_psc : Quantity (time) or array-like, optional New PSC time constant. Must be ``> 0`` and scalar. tau_fac : Quantity (time) or array-like, optional New facilitation time constant. Must be ``>= 0`` and scalar. tau_rec : Quantity (time) or array-like, optional New recovery time constant. Must be ``> 0`` and scalar. U : float or array-like, optional New utilization increment parameter. Must be in ``[0, 1]`` and scalar. x : float or array-like, optional New recovered resource fraction. Must satisfy ``x + y <= 1`` and be scalar. y : float or array-like, optional New active resource fraction. Must satisfy ``x + y <= 1`` and be scalar. u : float or array-like, optional New utilization state. Must be scalar. post : object, optional New default postsynaptic target object. Raises ------ ValueError If any parameter fails validation: - ``tau_psc <= 0`` - ``tau_fac < 0`` - ``tau_rec <= 0`` - ``U ∉ [0, 1]`` - ``x + y > 1`` - Non-scalar values for common properties Notes ----- - All validation is performed **before** any state is modified (atomic update). - When updating ``x`` or ``y``, the sum constraint is checked with the new values. - Updating ``x``, ``y``, or ``u`` also updates the internal initial-value cache (``_x0``, ``_y0``, ``_u0``), so subsequent ``init_state()`` calls will use the new values. - Time constants are validated after conversion to milliseconds. - This method does **not** reset ``t_lastspike`` or clear the event queue. Examples -------- Update facilitation parameters: .. code-block:: python >>> syn.set(tau_fac=500.0 * u.ms, U=0.3) Update state variables (e.g., after external perturbation): .. code-block:: python >>> syn.set(x=0.8, y=0.1, u=0.2) Update multiple parameters atomically: .. code-block:: python >>> syn.set( ... weight=2.5, ... delay=2.0 * u.ms, ... tau_rec=600.0 * u.ms, ... U=0.6, ... ) """ 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_no_quotes(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_float(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_hom`` dynamics. Computes short-term plasticity state updates following the Tsodyks-Markram model, calculates the effective synaptic weight, and schedules a delayed spike event for delivery to the postsynaptic target. **Algorithm:** 1. Compute inter-spike interval :math:`h = t_{\mathrm{spike}} - t_{\mathrm{lastspike}}` 2. Apply continuous-time propagation (exponential decay) to ``u``, ``x``, ``y`` 3. Apply spike-triggered facilitation: :math:`u \leftarrow u + U(1-u)` 4. Compute released resources: :math:`\Delta y = u \cdot x` 5. Update ``x`` and ``y`` for depletion/release 6. Compute effective weight: :math:`w_{\mathrm{eff}} = \Delta y \cdot w \cdot \text{multiplicity}` 7. Schedule delayed event in delivery queue 8. Update ``t_lastspike`` to current spike time Parameters ---------- multiplicity : float or array-like, optional Spike count or scaling factor. If zero, no event is scheduled and ``False`` is returned immediately. Default: ``1.0``. post : object, optional Target postsynaptic object. If ``None``, uses the default receiver stored in ``self.post``. receptor_type : int or array-like, optional Target receptor port ID. If ``None``, uses ``self.receptor_type``. Returns ------- bool ``True`` if an event was successfully scheduled, ``False`` if ``multiplicity`` was zero (no event sent). Notes ----- - **State updates**: This method mutates ``self.x``, ``self.y``, ``self.u``, and ``self.t_lastspike`` in-place. - **Ordering**: The update order exactly matches NEST's ``tsodyks_synapse_hom.h::send()``. - **Delay handling**: The event is scheduled for delivery at :math:`t_{\mathrm{current}} + \text{dt} + \text{delay}`, where ``dt`` is the simulation timestep. - **Queue structure**: Events are stored as ``(receiver, payload, port, 'spike')`` tuples in the internal delay queue. - **Numerical stability**: When ``tau_psc ≈ tau_rec``, the :math:`P_{xy}` calculation may lose precision (same limitation as NEST). See Also -------- update : Delivers queued events and schedules new ones from presynaptic input """ if not self._is_nonzero(multiplicity): return False dt_ms = self._refresh_delay_if_needed() current_step = self._curr_step(dt_ms) 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.exp(-h / self.tau_rec) pxy = ((pzz - 1.0) * self.tau_rec - (pyy - 1.0) * self.tau_psc) / ( self.tau_psc - self.tau_rec ) pxz = 1.0 - pzz z = 1.0 - self.x - self.y # Keep ordering identical to NEST models/tsodyks_synapse_hom.h::send. self.u *= puu self.x += pxy * self.y + pxz * 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 # NEST code sets receiver before weight and delay assignment. receiver = self._resolve_receiver(post) weighted_payload = multiplicity * (delta_y_tsp * self.weight) 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, then schedule current-step presynaptic input. This is the main simulation-step method, combining event delivery and spike transmission. It first delivers all queued events that are due for the current time step, then processes new presynaptic input (if any) by calling :meth:`send`. **Workflow:** 1. Refresh delay computation if needed (inherited from parent) 2. Deliver all events scheduled for the current simulation step 3. Sum presynaptic inputs from ``pre_spike``, ``current_inputs``, and ``delta_inputs`` 4. If total input is non-zero, call :meth:`send` to schedule a new event with short-term plasticity Parameters ---------- pre_spike : float or array-like, optional Presynaptic spike input for the current time step (dimensionless spike count or activation). Default: ``0.0`` (no input). post : object, optional Target postsynaptic object for new events. If ``None``, uses ``self.post``. receptor_type : int or array-like, optional Target receptor port ID for new events. If ``None``, uses ``self.receptor_type``. Returns ------- int Number of events delivered to postsynaptic targets during this time step. Notes ----- - **Event delivery**: Delivered events are removed from the internal queue after processing. - **Input summation**: The method sums: 1. Explicit ``pre_spike`` argument 2. Accumulated ``current_inputs`` (from projections) 3. Accumulated ``delta_inputs`` (from delta-function sources) - **Plasticity application**: Short-term plasticity state updates occur only when ``send()`` is called (i.e., when total input is non-zero). - **Typical usage**: Called once per simulation time step for each synapse model instance. Examples -------- Simulation loop with explicit presynaptic spike: .. code-block:: python >>> for t in range(1000): ... spike = 1.0 if t % 100 == 0 else 0.0 # spike every 100 steps ... delivered = syn.update(pre_spike=spike, post=target_neuron) Simulation with projection-driven input (no explicit spike): .. code-block:: python >>> # Projections accumulate input in syn.current_inputs >>> for t in range(1000): ... delivered = syn.update(post=target_neuron) """ 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