Source code for brainpy_state._nest.rate_connection_delayed

from typing import Any

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

from ._base import NESTSynapse

# 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 -*-

__all__ = [
    'rate_connection_delayed',
]


class rate_connection_delayed(NESTSynapse):
    r"""NEST-compatible ``rate_connection_delayed`` connection model.

    Implements connection-level semantics for delayed rate connections following NEST's
    ``rate_connection_delayed`` model. This class represents a synapse that transmits
    rate signals with a configurable delay, supporting secondary event propagation
    for rate-based network simulations.

    Unlike ``rate_connection_instantaneous``, this model enforces a minimum delay of
    one simulation step and supports delayed secondary events through coefficient arrays.

    Parameters
    ----------
    weight : float or array-like, optional
        Connection gain/strength applied to transmitted rate signals. Must be scalar.
        Default: ``1.0``.
    delay_steps : int or array-like, optional
        Transmission delay in discrete simulation steps. Must be integer-valued and
        ``>= 1``. Default: ``1``.
    name : str or None, optional
        Optional name identifier for this connection instance. Default: ``None``.

    Attributes
    ----------
    weight : float
        Connection gain (validated scalar).
    delay_steps : int
        Delay in simulation steps (validated ``>= 1``).
    name : str or None
        Instance name.
    HAS_DELAY : bool
        Class attribute, always ``True`` (this model enforces delay).
    SUPPORTS_WFR : bool
        Class attribute, always ``False`` (waveform relaxation not supported).

    Parameter Mapping
    -----------------
    The following table maps NEST parameters to this implementation:

    ================================  ====================  ========================================
    NEST Parameter                    brainpy.state         Notes
    ================================  ====================  ========================================
    ``weight``                        ``weight``            Connection gain (scalar float)
    ``delay``                         ``delay_steps``       Delay in steps (integer ``>= 1``)
    ``has_delay``                     ``HAS_DELAY``         Always ``True`` (class attribute)
    ``supports_wfr``                  ``SUPPORTS_WFR``      Always ``False`` (class attribute)
    ================================  ====================  ========================================

    Mathematical Description
    ------------------------
    **1. Connection Model**

    A ``rate_connection_delayed`` synapse transmits a rate signal :math:`r_\text{pre}(t)`
    from a presynaptic neuron to a postsynaptic neuron with delay :math:`d` and weight
    :math:`w`:

    .. math::

       r_\text{post}(t) = w \cdot r_\text{pre}(t - d)

    In discrete-time simulation with step size :math:`\Delta t`, the delay is quantized
    to an integer number of steps:

    .. math::

       d_\text{steps} = \left\lceil \frac{d}{\Delta t} \right\rceil, \quad d_\text{steps} \geq 1

    **2. Secondary Event Handling**

    NEST rate neurons support secondary events for implicit integration schemes. A
    secondary event carries a coefficient array :math:`\{c_0, c_1, \ldots, c_{n-1}\}`
    representing contributions at multiple time lags.

    The receiver maps each coefficient :math:`c_i` to a target buffer slot:

    .. math::

       \text{target\_slot} = (d_\text{steps} - d_\text{min}) + i

    where :math:`d_\text{min}` is the minimum delay in the network. This ensures that
    coefficients are applied to the correct future time steps.

    **3. Event Payload Structure**

    A delayed rate event contains:

    - ``rate``: Scalar rate value or coefficient array
    - ``weight``: Connection weight
    - ``delay_steps``: Integer delay
    - ``multiplicity``: Optional scaling factor for probabilistic connections

    The method :meth:`coeffarray_to_step_events` expands a coefficient array into
    individual per-step events, each with the appropriate delay offset.

    Implementation Notes
    --------------------
    **Delay Validation**

    This model enforces ``delay_steps >= 1`` to comply with NEST semantics. Attempting
    to set ``delay_steps = 0`` raises a ``ValueError``. For instantaneous (zero-delay)
    connections, use ``rate_connection_instantaneous`` instead.

    **Unit Handling**

    All parameters accept ``saiunit.Quantity`` objects or plain numeric values. If a
    ``Quantity`` is provided, its mantissa is extracted. Internally, values are stored
    as dimensionless floats or integers.

    **Compatibility with NEST**

    - NEST stores delays in time units (ms), while this implementation uses discrete
      steps to match BrainPy's event system.
    - The ``set_status`` method accepts both ``delay`` and ``delay_steps`` as aliases
      for backward compatibility.
    - Secondary event expansion follows NEST's buffer indexing logic exactly.

    Raises
    ------
    ValueError
        If ``weight`` or ``delay_steps`` is not scalar, or if ``delay_steps < 1``.
    ValueError
        If ``delay`` and ``delay_steps`` are both provided in ``set_status`` with
        conflicting values.
    ValueError
        If coefficient array is empty or delay is less than ``min_delay_steps`` in
        ``coeffarray_to_step_events``.

    See Also
    --------
    rate_connection_instantaneous : Zero-delay rate connection model (NEST equivalent)
    rate_neuron_ipn : Input rate neuron (delayed event receiver)
    rate_neuron_opn : Output rate neuron (delayed event receiver)

    References
    ----------
    .. [1] Hahne, J., et al. (2015). "A unified framework for spiking and rate-based
           neural networks." Frontiers in Neuroinformatics, 9, 22.
    .. [2] NEST Simulator documentation: Rate neuron models.
           https://nest-simulator.readthedocs.io/en/stable/models/rate_connection_delayed.html
    .. [3] NEST source: ``models/rate_connection_delayed.{h,cpp}``.
    .. [4] NEST delayed-rate receiver handling: ``models/rate_neuron_ipn_impl.h``
           and ``models/rate_neuron_opn_impl.h``.

    Examples
    --------
    **Basic Usage**

    Create a delayed connection with weight 2.0 and 3-step delay:

    .. code-block:: python

       >>> import brainpy.state as bst
       >>> conn = bst.rate_connection_delayed(weight=2.0, delay_steps=3)
       >>> conn.get_status()
       {'weight': 2.0, 'delay_steps': 3, 'delay': 3, 'has_delay': True, 'supports_wfr': False}

    **Creating Rate Events**

    Transmit a rate signal with the connection's parameters:

    .. code-block:: python

       >>> event = conn.to_rate_event(rate=5.0, multiplicity=1.0)
       >>> event
       {'rate': 5.0, 'weight': 2.0, 'delay_steps': 3, 'multiplicity': 1.0}

    **Secondary Event Expansion**

    Map a coefficient array to per-step events (e.g., for implicit integration):

    .. code-block:: python

       >>> coeffs = [0.5, 1.0, 0.3]  # Three lag coefficients
       >>> events = conn.coeffarray_to_step_events(coeffs, min_delay_steps=1)
       >>> len(events)
       3
       >>> events[0]
       {'rate': 0.5, 'weight': 2.0, 'delay_steps': 2, 'multiplicity': 1.0}
       >>> events[1]
       {'rate': 1.0, 'weight': 2.0, 'delay_steps': 3, 'multiplicity': 1.0}
       >>> events[2]
       {'rate': 0.3, 'weight': 2.0, 'delay_steps': 4, 'multiplicity': 1.0}

    **Dynamic Parameter Updates**

    Update connection parameters at runtime:

    .. code-block:: python

       >>> conn.set_status(weight=1.5, delay_steps=5)
       >>> conn.get('weight')
       1.5
       >>> conn.get('delay_steps')
       5

    **Using with Rate Neurons**

    Typical usage in a rate-based network (conceptual example):

    .. code-block:: python

       >>> import brainpy.state as bst
       >>> import saiunit as u
       >>> # Create rate neurons (assuming rate_neuron_ipn exists)
       >>> pre = bst.rate_neuron_ipn(size=10, tau=10.0*u.ms)
       >>> post = bst.rate_neuron_ipn(size=5, tau=10.0*u.ms)
       >>> # Define delayed connections
       >>> conns = [bst.rate_connection_delayed(weight=w, delay_steps=d)
       ...          for w, d in zip([0.8, 1.2, 0.5], [2, 3, 1])]
       >>> # Transmit events during simulation
       >>> for conn in conns:
       ...     event = conn.to_rate_event(pre.rate, multiplicity=1.0)
       ...     # post.handle_delayed_event(event)  # Receiver-side logic
    """

    __module__ = 'brainpy.state'

    HAS_DELAY = True
    SUPPORTS_WFR = False

    def __init__(
        self,
        weight: ArrayLike = 1.0,
        delay_steps: ArrayLike = 1,
        name: str | None = None,
    ):
        super().__init__(in_size=1, name=name)
        self.weight = self._to_float_scalar(weight, name='weight')
        self.delay_steps = self._validate_delay_steps(delay_steps)

    @property
    def properties(self) -> dict[str, Any]:
        r"""Return connection model properties.

        Returns
        -------
        dict
            Dictionary with keys:

            - ``'has_delay'`` (bool): Always ``True`` for this model.
            - ``'supports_wfr'`` (bool): Always ``False`` (waveform relaxation not supported).
        """
        return {
            'has_delay': self.HAS_DELAY,
            'supports_wfr': self.SUPPORTS_WFR,
        }

[docs] def get_status(self) -> dict[str, Any]: r"""Retrieve all connection parameters as a dictionary. Follows NEST's ``GetStatus`` API convention. Returns ------- dict Dictionary with keys: - ``'weight'`` (float): Connection gain. - ``'delay_steps'`` (int): Delay in simulation steps. - ``'delay'`` (int): Alias for ``delay_steps`` (NEST compatibility). - ``'has_delay'`` (bool): Always ``True``. - ``'supports_wfr'`` (bool): Always ``False``. Examples -------- .. code-block:: python >>> conn = rate_connection_delayed(weight=1.5, delay_steps=2) >>> status = conn.get_status() >>> status['weight'] 1.5 >>> status['delay'] 2 """ return { 'weight': float(self.weight), 'delay_steps': int(self.delay_steps), 'delay': int(self.delay_steps), 'has_delay': self.HAS_DELAY, 'supports_wfr': self.SUPPORTS_WFR, }
[docs] def set_status(self, status: dict[str, Any] | None = None, **kwargs): r"""Update connection parameters from a dictionary or keyword arguments. Follows NEST's ``SetStatus`` API convention. Accepts both ``delay`` and ``delay_steps`` as aliases (if both provided, they must match). Parameters ---------- status : dict or None, optional Dictionary of parameters to update. Keys: ``'weight'``, ``'delay'``, ``'delay_steps'``. **kwargs Alternative parameter specification as keyword arguments. Merged with ``status`` (keyword args take precedence). Raises ------ ValueError If ``delay`` and ``delay_steps`` are both provided with conflicting values. ValueError If any parameter fails validation (e.g., non-scalar, delay < 1). Examples -------- .. code-block:: python >>> conn = rate_connection_delayed(weight=1.0, delay_steps=1) >>> conn.set_status({'weight': 2.5, 'delay_steps': 4}) >>> conn.get('weight') 2.5 >>> conn.set_status(weight=3.0) # Keyword argument style >>> conn.get('weight') 3.0 """ updates = {} if status is not None: updates.update(status) updates.update(kwargs) if 'weight' in updates: self.set_weight(updates['weight']) has_delay = 'delay' in updates has_delay_steps = 'delay_steps' in updates if has_delay and has_delay_steps: d = self._to_int_scalar(updates['delay'], name='delay') ds = self._to_int_scalar(updates['delay_steps'], name='delay_steps') if d != ds: raise ValueError('delay and delay_steps must be identical when both are provided.') self.set_delay_steps(ds) elif has_delay_steps: self.set_delay_steps(updates['delay_steps']) elif has_delay: self.set_delay(updates['delay'])
[docs] def get(self, key: str = 'status'): r"""Retrieve a specific parameter or full status dictionary. Parameters ---------- key : str, optional Parameter name to retrieve. Special value ``'status'`` returns full status dictionary. Supported keys: ``'status'``, ``'weight'``, ``'delay'``, ``'delay_steps'``, ``'has_delay'``, ``'supports_wfr'``. Default: ``'status'``. Returns ------- dict or scalar If ``key == 'status'``, returns full status dictionary. Otherwise, returns the requested parameter value. Raises ------ KeyError If ``key`` is not a recognized parameter name. Examples -------- .. code-block:: python >>> conn = rate_connection_delayed(weight=2.0, delay_steps=3) >>> conn.get('weight') 2.0 >>> conn.get('delay_steps') 3 >>> conn.get('status') {'weight': 2.0, 'delay_steps': 3, ...} """ if key == 'status': return self.get_status() status = self.get_status() if key in status: return status[key] raise KeyError(f'Unsupported key "{key}" for rate_connection_delayed.get().')
[docs] def set_weight(self, weight: ArrayLike): r"""Update the connection weight. Parameters ---------- weight : float or array-like New connection gain. Must be scalar. Accepts ``saiunit.Quantity`` (mantissa will be extracted). Raises ------ ValueError If ``weight`` is not scalar. Examples -------- .. code-block:: python >>> conn = rate_connection_delayed() >>> conn.set_weight(2.5) >>> conn.get('weight') 2.5 """ self.weight = self._to_float_scalar(weight, name='weight')
[docs] def set_delay(self, delay: ArrayLike): r"""Update the connection delay (alias for ``set_delay_steps``). Parameters ---------- delay : int or array-like New delay in simulation steps. Must be integer-valued scalar ``>= 1``. Accepts ``saiunit.Quantity`` (mantissa will be extracted). Raises ------ ValueError If ``delay`` is not scalar, not integer-valued, or ``< 1``. Examples -------- .. code-block:: python >>> conn = rate_connection_delayed() >>> conn.set_delay(5) >>> conn.get('delay_steps') 5 """ self.delay_steps = self._validate_delay_steps(delay, name='delay')
[docs] def set_delay_steps(self, delay_steps: ArrayLike): r"""Update the connection delay in simulation steps. Parameters ---------- delay_steps : int or array-like New delay in simulation steps. Must be integer-valued scalar ``>= 1``. Accepts ``saiunit.Quantity`` (mantissa will be extracted). Raises ------ ValueError If ``delay_steps`` is not scalar, not integer-valued, or ``< 1``. Examples -------- .. code-block:: python >>> conn = rate_connection_delayed() >>> conn.set_delay_steps(3) >>> conn.get('delay_steps') 3 """ self.delay_steps = self._validate_delay_steps(delay_steps, name='delay_steps')
[docs] def prepare_secondary_event(self, coeffarray: ArrayLike) -> dict[str, Any]: r"""Create a delayed secondary-event payload. Secondary events are used in implicit integration schemes for rate neurons. The coefficient array represents contributions at multiple time lags. Parameters ---------- coeffarray : array-like 1D array of coefficients, shape ``(n,)``, representing rate contributions at ``n`` consecutive time lags. Must be non-empty. Accepts ``saiunit.Quantity`` (mantissa will be extracted). Returns ------- dict Event payload with keys: - ``'coeffarray'`` (ndarray): Validated coefficient array, shape ``(n,)``, dtype ``float64``. - ``'weight'`` (float): Connection weight. - ``'delay_steps'`` (int): Connection delay in steps. Raises ------ ValueError If ``coeffarray`` is empty or cannot be converted to a 1D array. See Also -------- coeffarray_to_step_events : Expand coefficient array to per-step events. Examples -------- .. code-block:: python >>> conn = rate_connection_delayed(weight=2.0, delay_steps=3) >>> event = conn.prepare_secondary_event([0.5, 1.0, 0.3]) >>> event['coeffarray'] array([0.5, 1. , 0.3]) >>> event['weight'] 2.0 """ return { 'coeffarray': self._to_coeff_array(coeffarray), 'weight': float(self.weight), 'delay_steps': int(self.delay_steps), }
[docs] def to_rate_event( self, rate: ArrayLike, multiplicity: ArrayLike = 1.0, delay_steps: ArrayLike | None = None, ) -> dict[str, Any]: r"""Create a delayed-rate event payload for step-based simulation APIs. Constructs an event dictionary that can be passed to rate neuron receivers to transmit a rate signal with the connection's weight and delay. Parameters ---------- rate : float or array-like Rate value to transmit. Can be scalar (single rate) or array (multiple rates). Accepts ``saiunit.Quantity`` (mantissa will be extracted). multiplicity : float or array-like, optional Scaling factor for stochastic or probabilistic connections. Must be scalar. Default: ``1.0``. delay_steps : int or array-like or None, optional Override delay for this event. Must be integer-valued scalar ``>= 1``. If ``None``, uses the connection's ``self.delay_steps``. Default: ``None``. Returns ------- dict Event payload with keys: - ``'rate'`` (float or ndarray): Rate value (scalar or array). - ``'weight'`` (float): Connection weight. - ``'delay_steps'`` (int): Delay in steps. - ``'multiplicity'`` (float): Scaling factor. Raises ------ ValueError If ``multiplicity`` or ``delay_steps`` is not scalar, or if ``delay_steps < 1``. Examples -------- .. code-block:: python >>> conn = rate_connection_delayed(weight=2.0, delay_steps=3) >>> event = conn.to_rate_event(rate=5.0) >>> event {'rate': 5.0, 'weight': 2.0, 'delay_steps': 3, 'multiplicity': 1.0} Override delay for a specific event: .. code-block:: python >>> event = conn.to_rate_event(rate=5.0, delay_steps=5) >>> event['delay_steps'] 5 """ d = self.delay_steps if delay_steps is None else self._validate_delay_steps(delay_steps, name='delay_steps') return { 'rate': self._to_rate_value(rate), 'weight': float(self.weight), 'delay_steps': int(d), 'multiplicity': self._to_float_scalar(multiplicity, name='multiplicity'), }
[docs] def coeffarray_to_step_events( self, coeffarray: ArrayLike, min_delay_steps: ArrayLike = 1, multiplicity: ArrayLike = 1.0, ) -> list[dict[str, Any]]: r"""Map lag-indexed coefficient array to per-step delayed-rate events. Expands a coefficient array representing multiple time lags into individual per-step rate events, following NEST's delayed-rate receiver buffer indexing. Each coefficient :math:`c_i` is mapped to an event with delay offset :math:`(d - d_{\text{min}}) + i`, where :math:`d` is the connection delay and :math:`d_{\text{min}}` is the minimum network delay. This method is used to distribute secondary event coefficients across future time steps for implicit integration schemes. Parameters ---------- coeffarray : array-like 1D array of coefficients, shape ``(n,)``. Each element represents a rate contribution at a successive time lag. Must be non-empty. Accepts ``saiunit.Quantity`` (mantissa will be extracted). min_delay_steps : int or array-like, optional Minimum delay in the network (in simulation steps). Used to compute the base delay offset. Must be integer-valued scalar ``>= 1``. Default: ``1``. multiplicity : float or array-like, optional Scaling factor for all events. Must be scalar. Default: ``1.0``. Returns ------- list of dict List of event dictionaries, one per coefficient. Each event has keys: - ``'rate'`` (float): Coefficient value. - ``'weight'`` (float): Connection weight. - ``'delay_steps'`` (int): Computed delay = ``(self.delay_steps - min_delay_steps) + i``. - ``'multiplicity'`` (float): Scaling factor. Length of list equals ``len(coeffarray)``. Raises ------ ValueError If ``coeffarray`` is empty. ValueError If ``self.delay_steps < min_delay_steps`` (would produce negative delay). ValueError If ``min_delay_steps`` or ``multiplicity`` is not scalar, or if ``min_delay_steps < 1``. Notes ----- **NEST Buffer Indexing** NEST rate neurons with delayed connections use a ring buffer indexed by: .. math:: \text{buffer\_slot} = (d - d_{\text{min}}) + i where :math:`d` is the connection delay, :math:`d_{\text{min}}` is the minimum delay, and :math:`i` is the coefficient index. This ensures that coefficients are applied to the correct future time steps during implicit integration. **Example Calculation** Suppose ``self.delay_steps = 5``, ``min_delay_steps = 2``, and ``coeffarray = [0.5, 1.0, 0.3]``: - Coefficient 0 (``0.5``): ``delay_steps = (5 - 2) + 0 = 3`` - Coefficient 1 (``1.0``): ``delay_steps = (5 - 2) + 1 = 4`` - Coefficient 2 (``0.3``): ``delay_steps = (5 - 2) + 2 = 5`` See Also -------- prepare_secondary_event : Create secondary event with coefficient array. to_rate_event : Create single-rate event. Examples -------- Basic usage with 3 coefficients: .. code-block:: python >>> conn = rate_connection_delayed(weight=2.0, delay_steps=5) >>> coeffs = [0.5, 1.0, 0.3] >>> events = conn.coeffarray_to_step_events(coeffs, min_delay_steps=2) >>> len(events) 3 >>> events[0] {'rate': 0.5, 'weight': 2.0, 'delay_steps': 3, 'multiplicity': 1.0} >>> events[1] {'rate': 1.0, 'weight': 2.0, 'delay_steps': 4, 'multiplicity': 1.0} >>> events[2] {'rate': 0.3, 'weight': 2.0, 'delay_steps': 5, 'multiplicity': 1.0} Error when connection delay is less than minimum delay: .. code-block:: python >>> conn = rate_connection_delayed(weight=1.0, delay_steps=1) >>> conn.coeffarray_to_step_events([0.5, 1.0], min_delay_steps=3) Traceback (most recent call last): ... ValueError: delay_steps must be >= min_delay_steps. Using with multiplicity scaling: .. code-block:: python >>> conn = rate_connection_delayed(weight=1.0, delay_steps=3) >>> events = conn.coeffarray_to_step_events([0.5, 1.0], min_delay_steps=1, multiplicity=0.8) >>> events[0]['multiplicity'] 0.8 """ coeff = self._to_coeff_array(coeffarray) min_delay = self._validate_delay_steps(min_delay_steps, name='min_delay_steps') base_delay = int(self.delay_steps - min_delay) if base_delay < 0: raise ValueError('delay_steps must be >= min_delay_steps.') mult = self._to_float_scalar(multiplicity, name='multiplicity') events = [] for i, c in enumerate(coeff): events.append( { 'rate': float(c), 'weight': float(self.weight), 'delay_steps': int(base_delay + i), 'multiplicity': float(mult), } ) return events
@staticmethod def _to_coeff_array(value: ArrayLike) -> np.ndarray: r"""Convert input to a validated 1D float64 coefficient array. Parameters ---------- value : array-like Input value. Accepts ``saiunit.Quantity`` (mantissa extracted). Returns ------- ndarray 1D array with dtype ``float64``. Raises ------ ValueError If array is empty after conversion. """ if isinstance(value, u.Quantity): value = u.get_mantissa(value) dftype = brainstate.environ.dftype() arr = np.asarray(u.math.asarray(value), dtype=dftype).reshape(-1) if arr.size == 0: raise ValueError('Coefficient array must not be empty.') return arr @staticmethod def _to_rate_value(value: ArrayLike): r"""Convert input to a rate value (scalar float or array). Parameters ---------- value : array-like Input value. Accepts ``saiunit.Quantity`` (mantissa extracted). Returns ------- float or ndarray If input has exactly one element, returns scalar float. Otherwise, returns array with dtype ``float64``. """ if isinstance(value, u.Quantity): value = u.get_mantissa(value) dftype = brainstate.environ.dftype() arr = np.asarray(u.math.asarray(value), dtype=dftype) if arr.size == 1: return float(arr.reshape(-1)[0]) return arr @staticmethod def _to_float_scalar(value: ArrayLike, name: str) -> float: r"""Convert input to a validated scalar float. Parameters ---------- value : array-like Input value. Accepts ``saiunit.Quantity`` (mantissa extracted). name : str Parameter name for error messages. Returns ------- float Scalar float value. Raises ------ ValueError If input is not scalar (size != 1). """ if isinstance(value, u.Quantity): value = u.get_mantissa(value) dftype = brainstate.environ.dftype() arr = np.asarray(u.math.asarray(value), dtype=dftype).reshape(-1) if arr.size != 1: raise ValueError(f'{name} must be scalar.') return float(arr[0]) @staticmethod def _to_int_scalar(value: ArrayLike, name: str) -> int: r"""Convert input to a validated integer scalar. Parameters ---------- value : array-like Input value. Accepts ``saiunit.Quantity`` (mantissa extracted). name : str Parameter name for error messages. Returns ------- int Integer value (rounded from float if within tolerance ``1e-12``). Raises ------ ValueError If input is not scalar, not finite, or not integer-valued (``|value - round(value)| > 1e-12``). """ if isinstance(value, u.Quantity): value = u.get_mantissa(value) dftype = brainstate.environ.dftype() arr = np.asarray(u.math.asarray(value), dtype=dftype).reshape(-1) if arr.size != 1: raise ValueError(f'{name} must be scalar.') v = float(arr[0]) if not np.isfinite(v): raise ValueError(f'{name} must be finite.') vr = int(round(v)) if abs(v - vr) > 1e-12: raise ValueError(f'{name} must be integer-valued.') return vr def _validate_delay_steps(self, delay_steps: ArrayLike, name: str = 'delay_steps') -> int: r"""Validate and convert delay parameter to integer >= 1. Parameters ---------- delay_steps : array-like Delay value in simulation steps. Accepts ``saiunit.Quantity`` (mantissa extracted). name : str, optional Parameter name for error messages. Default: ``'delay_steps'``. Returns ------- int Validated integer delay value. Raises ------ ValueError If delay is not scalar, not integer-valued, or ``< 1``. """ d = self._to_int_scalar(delay_steps, name=name) if d < 1: raise ValueError(f'{name} must be >= 1.') return d