Source code for brainpy_state._nest.sic_connection

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__ = [
    'sic_connection',
]


class sic_connection(NESTSynapse):
    r"""NEST-compatible ``sic_connection`` synapse model for astrocyte-to-neuron slow inward current (SIC) coupling.

    This class implements connection-level semantics of NEST's ``sic_connection`` model
    (``models/sic_connection.{h,cpp}``), which transmits slow inward current coefficients
    from astrocytes to neurons via ``SICEvent`` secondary events. The connection stores
    a scalar weight that multiplies the SIC coefficient stream and supports delayed delivery.

    **1. Biological Context**

    Astrocytes can modulate neuronal excitability through release of gliotransmitters,
    triggering slow inward currents (SICs) in nearby neurons. These currents typically
    arise from extrasynaptic NMDA receptors activated by glutamate released from astrocytes,
    producing depolarizing currents that persist for hundreds of milliseconds to seconds.
    This connection model represents the functional coupling between astrocyte dynamics
    and neuronal membrane conductance.

    **2. Supported Model Pairings**

    In standard NEST model sets:

    - **Source models** (emit ``SICEvent``): ``astrocyte_lr_1994``
    - **Target models** (handle ``SICEvent``): ``aeif_cond_alpha_astro``

    Methods :meth:`supports_connection` and :meth:`check_connection` validate model
    compatibility at the model-name level.

    **3. Mathematical Formulation**

    The connection transmits a coefficient array :math:`c = [c_0, c_1, \ldots, c_{n-1}]`
    representing SIC amplitude values scheduled for future time steps. Each coefficient
    is scaled by the connection weight :math:`w` and delivered with delay :math:`d`:

    .. math::

       I_{\text{SIC}}(t + d + i) = w \cdot c_i, \qquad i = 0, 1, \ldots, n-1

    where :math:`t` is the current simulation time, :math:`d` is the connection delay,
    and :math:`i` indexes the coefficient array.

    **4. Delay Mapping and Buffer Indexing**

    NEST's target-side SIC handling in ``aeif_cond_alpha_astro::handle(SICEvent&)`` uses:

    .. math::

       \text{offset} = d - d_{\text{min}}, \qquad \text{slot}_i = \text{offset} + i

    where :math:`d` is the event delay in steps, :math:`d_{\text{min}}` is the minimum
    network delay, and :math:`i` is the coefficient index.

    Local step-based APIs in this package interpret ``delay_steps`` as a zero-indexed
    offset (``delay_steps - 1``). Therefore, for an absolute NEST delay :math:`d` and
    minimum delay :math:`d_{\text{min}}`, this class maps to local delay:

    .. math::

       d_{\text{local}} = (d - d_{\text{min}}) + 1

    This ensures proper alignment with NEST's buffer indexing. Methods
    :meth:`to_aeif_sic_event` and :meth:`coeffarray_to_step_events` apply this mapping.

    **5. Event Structure**

    The connection produces event dictionaries with:

    - ``coeffs`` (array or scalar): SIC coefficient value(s)
    - ``weight`` (float): Connection weight × multiplicity
    - ``delay_steps`` (int): Delay offset for buffer indexing
    - ``multiplicity`` (float): Event count (always 1.0 after weight multiplication)

    Parameters
    ----------
    weight : float, array-like, optional
        Synaptic weight multiplying SIC coefficients. Must be scalar. Default: ``1.0``.
    delay_steps : int, array-like, optional
        Absolute event delay in simulation steps (NEST-style). Must be scalar integer
        ``>= 1``. Default: ``1``.
    name : str, optional
        Model instance name for identification. Default: ``None``.

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

    ================  =============  ========  ===========================================
    Parameter         NEST Name      Unit      Description
    ================  =============  ========  ===========================================
    weight            weight         unitless  Coefficient scaling factor
    delay_steps       delay          steps     Absolute transmission delay
    ================  =============  ========  ===========================================

    Attributes
    ----------
    weight : float
        Validated scalar weight.
    delay_steps : int
        Validated delay in steps (≥ 1).
    name : str or None
        Optional instance identifier.
    HAS_DELAY : bool
        Class attribute, always ``True`` (supports delayed transmission).
    SUPPORTS_WFR : bool
        Class attribute, always ``False`` (waveform relaxation not supported).
    SUPPORTED_SOURCES : tuple of str
        Valid source model names (``'astrocyte_lr_1994'``).
    SUPPORTED_TARGETS : tuple of str
        Valid target model names (``'aeif_cond_alpha_astro'``).

    Raises
    ------
    ValueError
        If ``weight`` or ``delay_steps`` are not scalar, or if ``delay_steps < 1``.

    Notes
    -----
    - This class represents connection semantics only. SIC coefficient generation
      is handled by the source model (e.g., ``astrocyte_lr_1994``).
    - Connection objects are stateless; all state (membrane conductances, SIC time
      courses) is maintained by source and target neuron models.
    - The ``multiplicity`` parameter in event methods allows scaling weights for
      multi-synapse connections but is always normalized to 1.0 in final events.

    References
    ----------
    .. [1] NEST source code: ``models/sic_connection.h`` and ``models/sic_connection.cpp``.
    .. [2] NEST receiver logic: ``models/aeif_cond_alpha_astro.cpp``, ``handle(SICEvent&)`` method.
    .. [3] NEST test suite: ``testsuite/pytests/test_sic_connection.py``.
    .. [4] Li, Y. X., & Rinzel, J. (1994). Equations for InsP3 receptor-mediated
           [Ca2+]i oscillations derived from a detailed kinetic model: a Hodgkin-Huxley
           like formalism. *Journal of Theoretical Biology*, 166(4), 461-473.

    Examples
    --------
    **Basic connection setup:**

    .. code-block:: python

        >>> import brainpy.state as bp
        >>> conn = bp.sic_connection(weight=0.5, delay_steps=2)
        >>> conn.get_status()
        {'weight': 0.5, 'delay_steps': 2, 'delay': 2, ...}

    **Create SIC event for target neuron:**

    .. code-block:: python

        >>> coeffs = [0.1, 0.3, 0.5, 0.3, 0.1]  # Time-varying SIC profile
        >>> event = conn.to_aeif_sic_event(
        ...     coeffarray=coeffs,
        ...     min_delay_steps=1,
        ...     multiplicity=1.0
        ... )
        >>> event['weight']  # Scaled by connection weight
        0.5
        >>> event['delay_steps']  # Local delay: (2 - 1) + 1 = 2
        2

    **Decompose coefficient array into per-step events:**

    .. code-block:: python

        >>> events = conn.coeffarray_to_step_events(
        ...     coeffarray=[0.1, 0.2, 0.3],
        ...     min_delay_steps=1
        ... )
        >>> len(events)
        3
        >>> events[0]['delay_steps']  # First coefficient at delay 2
        2
        >>> events[2]['delay_steps']  # Third coefficient at delay 4
        4

    **Validate model compatibility:**

    .. code-block:: python

        >>> bp.sic_connection.supports_connection(
        ...     'astrocyte_lr_1994', 'aeif_cond_alpha_astro'
        ... )
        True
        >>> bp.sic_connection.check_connection(
        ...     'astrocyte_lr_1994', 'iaf_psc_alpha'
        ... )  # doctest: +SKIP
        ValueError: Unsupported sic_connection pair...
    """

    __module__ = 'brainpy.state'

    HAS_DELAY = True
    SUPPORTS_WFR = False
    SUPPORTED_SOURCES = ('astrocyte_lr_1994',)
    SUPPORTED_TARGETS = ('aeif_cond_alpha_astro',)

    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 capability flags and supported model types.

        Returns
        -------
        dict[str, Any]
            Dictionary with keys:

            - ``'has_delay'`` (bool): Always ``True`` for ``sic_connection``.
            - ``'supports_wfr'`` (bool): Always ``False`` (waveform relaxation not implemented).
            - ``'supported_sources'`` (tuple[str, ...]): Model names that can emit ``SICEvent``.
            - ``'supported_targets'`` (tuple[str, ...]): Model names that can receive ``SICEvent``.

        Notes
        -----
        This property provides introspection for connection compatibility checking and
        simulation infrastructure setup (e.g., delay buffer allocation).
        """
        return {
            'has_delay': self.HAS_DELAY,
            'supports_wfr': self.SUPPORTS_WFR,
            'supported_sources': self.SUPPORTED_SOURCES,
            'supported_targets': self.SUPPORTED_TARGETS,
        }

[docs] def get_status(self) -> dict[str, Any]: r"""Return complete connection state and metadata. Returns ------- dict[str, Any] Dictionary containing: - ``'weight'`` (float): Connection weight. - ``'delay_steps'`` (int): Delay in simulation steps. - ``'delay'`` (int): Alias for ``delay_steps`` (NEST compatibility). - ``'size_of'`` (int): Memory footprint in bytes (Python object overhead). - ``'has_delay'``, ``'supports_wfr'``, ``'supported_sources'``, ``'supported_targets'``: Connection properties (see :meth:`properties`). Notes ----- This method mirrors NEST's ``GetStatus`` API, providing a snapshot of all connection parameters and capabilities. The ``delay`` key is an alias for ``delay_steps`` to maintain NEST naming conventions. """ return { 'weight': float(self.weight), 'delay_steps': int(self.delay_steps), 'delay': int(self.delay_steps), 'size_of': int(self.__sizeof__()), 'has_delay': self.HAS_DELAY, 'supports_wfr': self.SUPPORTS_WFR, 'supported_sources': self.SUPPORTED_SOURCES, 'supported_targets': self.SUPPORTED_TARGETS, }
[docs] def set_status(self, status: dict[str, Any] | None = None, **kwargs): r"""Update connection parameters (NEST ``SetStatus`` API). Parameters ---------- status : dict[str, Any], optional Dictionary of parameters to update. Valid keys: ``'weight'``, ``'delay'``, ``'delay_steps'``. Default: ``None``. **kwargs Additional keyword arguments merged with ``status`` dict. Raises ------ ValueError If both ``'delay'`` and ``'delay_steps'`` are provided with different values, or if parameter values fail validation (non-scalar, delay < 1, etc.). Notes ----- - If both ``'delay'`` and ``'delay_steps'`` are present, they must be identical (they are aliases in NEST). - Validation is delegated to :meth:`set_weight` and :meth:`set_delay_steps`. - Unknown keys are silently ignored (NEST-compatible behavior). Examples -------- .. code-block:: python >>> conn = bp.sic_connection(weight=1.0, delay_steps=1) >>> conn.set_status(weight=2.5, delay=3) >>> conn.get_status()['weight'] 2.5 >>> conn.get_status()['delay_steps'] 3 """ 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 connection parameter or full status dictionary. Parameters ---------- key : str, optional Parameter name to retrieve. If ``'status'`` (default), returns full status dictionary from :meth:`get_status`. Otherwise, must be a valid status key (e.g., ``'weight'``, ``'delay'``, ``'delay_steps'``). Returns ------- Any If ``key == 'status'``: full status dictionary. Otherwise: value of the requested parameter. Raises ------ KeyError If ``key`` is not ``'status'`` and not present in the status dictionary. Examples -------- .. code-block:: python >>> conn = bp.sic_connection(weight=0.8, delay_steps=2) >>> conn.get('weight') 0.8 >>> conn.get('status')['delay'] 2 """ 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 sic_connection.get().')
[docs] def set_weight(self, weight: ArrayLike): r"""Update connection weight. Parameters ---------- weight : float or array-like New synaptic weight. Must be scalar after conversion. Raises ------ ValueError If ``weight`` is not scalar. Examples -------- .. code-block:: python >>> conn = bp.sic_connection() >>> conn.set_weight(2.5) >>> conn.weight 2.5 """ self.weight = self._to_float_scalar(weight, name='weight')
[docs] def set_delay(self, delay: ArrayLike): r"""Update connection delay (alias for :meth:`set_delay_steps`). Parameters ---------- delay : int or array-like New delay in simulation steps. Must be scalar integer ``>= 1``. Raises ------ ValueError If ``delay`` is not scalar integer or ``< 1``. """ self.delay_steps = self._validate_delay_steps(delay, name='delay')
[docs] def set_delay_steps(self, delay_steps: ArrayLike): r"""Update connection delay in simulation steps. Parameters ---------- delay_steps : int or array-like New delay in steps. Must be scalar integer ``>= 1``. Raises ------ ValueError If ``delay_steps`` is not scalar integer or ``< 1``. Examples -------- .. code-block:: python >>> conn = bp.sic_connection(delay_steps=1) >>> conn.set_delay_steps(5) >>> conn.delay_steps 5 """ self.delay_steps = self._validate_delay_steps(delay_steps, name='delay_steps')
@classmethod def _model_name(cls, model: Any) -> str: r"""Extract model name from string, class, or instance. Parameters ---------- model : Any Model identifier (string name, class, or instance). Returns ------- str Normalized model name for compatibility checking. Notes ----- Extraction order: 1. If ``model`` is string, return as-is. 2. If ``model`` has ``__name__`` attribute (class), return it. 3. If ``model`` has ``__class__.__name__`` (instance), return class name. 4. Fallback to ``str(model)``. """ if isinstance(model, str): return model if hasattr(model, '__name__'): return str(model.__name__) if hasattr(model, '__class__') and hasattr(model.__class__, '__name__'): return str(model.__class__.__name__) return str(model)
[docs] @classmethod def supports_connection(cls, source_model: Any, target_model: Any) -> bool: r"""Check if source-target model pair is compatible with ``sic_connection``. Parameters ---------- source_model : Any Source model (string name, class, or instance). Must be in ``SUPPORTED_SOURCES`` (``'astrocyte_lr_1994'``). target_model : Any Target model (string name, class, or instance). Must be in ``SUPPORTED_TARGETS`` (``'aeif_cond_alpha_astro'``). Returns ------- bool ``True`` if both models are in their respective supported lists, ``False`` otherwise. Examples -------- .. code-block:: python >>> bp.sic_connection.supports_connection( ... 'astrocyte_lr_1994', 'aeif_cond_alpha_astro' ... ) True >>> bp.sic_connection.supports_connection( ... 'iaf_psc_alpha', 'aeif_cond_alpha_astro' ... ) False """ src = cls._model_name(source_model) tgt = cls._model_name(target_model) return src in cls.SUPPORTED_SOURCES and tgt in cls.SUPPORTED_TARGETS
[docs] @classmethod def check_connection(cls, source_model: Any, target_model: Any) -> bool: r"""Validate source-target model pair and raise error if incompatible. Parameters ---------- source_model : Any Source model (string name, class, or instance). target_model : Any Target model (string name, class, or instance). Returns ------- bool Always returns ``True`` if validation passes. Raises ------ ValueError If the model pair is not supported (see :meth:`supports_connection`). Error message includes actual and expected model names. Examples -------- .. code-block:: python >>> bp.sic_connection.check_connection( ... 'astrocyte_lr_1994', 'aeif_cond_alpha_astro' ... ) True >>> bp.sic_connection.check_connection( ... 'iaf_psc_alpha', 'aeif_cond_alpha_astro' ... ) # doctest: +SKIP ValueError: Unsupported sic_connection pair... """ ok = cls.supports_connection(source_model, target_model) if not ok: src = cls._model_name(source_model) tgt = cls._model_name(target_model) raise ValueError( f'Unsupported sic_connection pair: source={src}, target={tgt}. ' f'Expected source in {cls.SUPPORTED_SOURCES} and target in {cls.SUPPORTED_TARGETS}.' ) return True
[docs] def prepare_secondary_event( self, coeffarray: ArrayLike, delay_steps: ArrayLike | None = None, ) -> dict[str, Any]: r"""Create a NEST-style ``SICEvent`` payload with coefficient array. Parameters ---------- coeffarray : array-like 1D array of SIC coefficients for future time steps. Must be non-empty. Shape: ``(n_steps,)``. delay_steps : int, array-like, optional Override connection delay. If ``None``, uses ``self.delay_steps``. Default: ``None``. Returns ------- dict[str, Any] NEST-compatible event dictionary: - ``'coeffarray'`` (ndarray): Validated float64 coefficient array, shape ``(n_steps,)``. - ``'weight'`` (float): Connection weight. - ``'delay_steps'`` (int): Absolute delay in simulation steps. Raises ------ ValueError If ``coeffarray`` is empty, or if ``delay_steps`` validation fails. Notes ----- This method produces raw NEST-style events. For local step-based consumption, use :meth:`to_aeif_sic_event` or :meth:`coeffarray_to_step_events` which apply delay mapping. Examples -------- .. code-block:: python >>> conn = bp.sic_connection(weight=0.5, delay_steps=2) >>> event = conn.prepare_secondary_event([0.1, 0.3, 0.5]) >>> event['coeffarray'] array([0.1, 0.3, 0.5]) >>> event['delay_steps'] 2 """ d = self.delay_steps if delay_steps is None else self._validate_delay_steps(delay_steps, name='delay_steps') return { 'coeffarray': self._to_coeff_array(coeffarray), 'weight': float(self.weight), 'delay_steps': int(d), }
[docs] def to_aeif_sic_event( self, coeffarray: ArrayLike, min_delay_steps: ArrayLike = 1, multiplicity: ArrayLike = 1.0, delay_steps: ArrayLike | None = None, ) -> dict[str, Any]: r"""Create SIC event payload consumable by ``aeif_cond_alpha_astro.update``. Applies delay mapping from NEST absolute delay to local step-based offset: ``local_delay = (delay - min_delay) + 1``. Parameters ---------- coeffarray : array-like 1D array of SIC coefficients. Shape: ``(n_steps,)``. min_delay_steps : int, array-like, optional Minimum network delay (NEST-style). Used for delay offset calculation. Default: ``1``. multiplicity : float, array-like, optional Scaling factor for connection weight (e.g., for multi-synapse connections). Default: ``1.0``. delay_steps : int, array-like, optional Override connection delay. If ``None``, uses ``self.delay_steps``. Default: ``None``. Returns ------- dict[str, Any] Local event dictionary: - ``'coeffs'`` (ndarray): Coefficient array, shape ``(n_steps,)``. - ``'weight'`` (float): Connection weight × multiplicity. - ``'delay_steps'`` (int): Local delay offset = ``(delay - min_delay) + 1``. - ``'multiplicity'`` (float): Always ``1.0`` (absorbed into weight). Raises ------ ValueError If ``coeffarray`` is empty, if ``delay_steps < min_delay_steps``, or if any parameter is not scalar where required. Notes ----- This method is the primary interface for integrating ``sic_connection`` events into step-based neuron update loops. The delay mapping ensures compatibility with NEST's buffer indexing semantics. Examples -------- .. code-block:: python >>> conn = bp.sic_connection(weight=0.5, delay_steps=3) >>> event = conn.to_aeif_sic_event( ... coeffarray=[0.1, 0.2], ... min_delay_steps=1, ... multiplicity=2.0 ... ) >>> event['weight'] # 0.5 * 2.0 1.0 >>> event['delay_steps'] # (3 - 1) + 1 3 """ coeff = self._to_coeff_array(coeffarray) d = self.delay_steps if delay_steps is None else self._validate_delay_steps(delay_steps, name='delay_steps') local_delay = self._to_local_delay_steps(d, min_delay_steps=min_delay_steps) mult = self._to_float_scalar(multiplicity, name='multiplicity') return { 'coeffs': coeff, 'weight': float(self.weight * mult), 'delay_steps': int(local_delay), 'multiplicity': 1.0, }
[docs] def to_sic_event( self, coeff: ArrayLike, min_delay_steps: ArrayLike = 1, multiplicity: ArrayLike = 1.0, delay_steps: ArrayLike | None = None, ) -> dict[str, Any]: r"""Create single-coefficient SIC event for local step-based APIs. This is an alias for :meth:`to_aeif_sic_event` supporting both single-value and array-valued coefficients. Parameters ---------- coeff : float, array-like SIC coefficient value(s). Can be scalar or array. min_delay_steps : int, array-like, optional Minimum network delay. Default: ``1``. multiplicity : float, array-like, optional Weight scaling factor. Default: ``1.0``. delay_steps : int, array-like, optional Override delay. Default: ``None`` (use ``self.delay_steps``). Returns ------- dict[str, Any] See :meth:`to_aeif_sic_event` return value. Examples -------- .. code-block:: python >>> conn = bp.sic_connection(weight=0.8, delay_steps=2) >>> event = conn.to_sic_event(coeff=0.5, min_delay_steps=1) >>> event['coeffs'] array([0.5]) >>> event['delay_steps'] 2 """ return self.to_aeif_sic_event( coeffarray=coeff, min_delay_steps=min_delay_steps, multiplicity=multiplicity, delay_steps=delay_steps, )
[docs] def coeffarray_to_step_events( self, coeffarray: ArrayLike, min_delay_steps: ArrayLike = 1, multiplicity: ArrayLike = 1.0, delay_steps: ArrayLike | None = None, ) -> list[dict[str, Any]]: r"""Map lag-indexed SIC coefficients to one event per future time step. Decomposes an ``n``-element coefficient array into ``n`` separate events, each scheduled for a distinct future step. Coefficient ``i`` is delivered at ``local_delay + i`` steps in the future. Parameters ---------- coeffarray : array-like 1D array of SIC coefficients, length ``n``. Coefficient ``i`` corresponds to time step ``current_time + delay + i``. min_delay_steps : int, array-like, optional Minimum network delay for delay mapping. Default: ``1``. multiplicity : float, array-like, optional Weight scaling factor. Default: ``1.0``. delay_steps : int, array-like, optional Override connection delay. Default: ``None`` (use ``self.delay_steps``). Returns ------- list[dict[str, Any]] List of ``n`` event dictionaries, one per coefficient. Each contains: - ``'coeffs'`` (float): Single coefficient value. - ``'weight'`` (float): Connection weight × multiplicity. - ``'delay_steps'`` (int): Local delay = ``base_delay + i`` for index ``i``. - ``'multiplicity'`` (float): Always ``1.0``. Raises ------ ValueError If ``coeffarray`` is empty or parameter validation fails. Notes ----- This method is useful for event-driven simulations where SIC coefficients need to be delivered as discrete time-stamped events rather than as a bundled array. The base delay is computed as ``(delay - min_delay) + 1``, and each coefficient adds its index to this base. Examples -------- .. code-block:: python >>> conn = bp.sic_connection(weight=0.5, delay_steps=2) >>> events = conn.coeffarray_to_step_events( ... coeffarray=[0.1, 0.3, 0.5], ... min_delay_steps=1 ... ) >>> len(events) 3 >>> events[0]['coeffs'] 0.1 >>> events[0]['delay_steps'] # (2 - 1) + 1 + 0 2 >>> events[2]['delay_steps'] # (2 - 1) + 1 + 2 4 """ coeff = self._to_coeff_array(coeffarray) d = self.delay_steps if delay_steps is None else self._validate_delay_steps(delay_steps, name='delay_steps') local_delay = self._to_local_delay_steps(d, min_delay_steps=min_delay_steps) mult = self._to_float_scalar(multiplicity, name='multiplicity') weight = float(self.weight * mult) events = [] for i, c in enumerate(coeff): events.append( { 'coeffs': float(c), 'weight': weight, 'delay_steps': int(local_delay + i), 'multiplicity': 1.0, } ) return events
@classmethod def _to_local_delay_steps( cls, delay_steps: ArrayLike, min_delay_steps: ArrayLike = 1, ) -> int: r"""Convert NEST absolute delay to local step-based offset. Implements the mapping: ``local_delay = (delay - min_delay) + 1``. Parameters ---------- delay_steps : int, array-like Absolute NEST-style delay in steps. min_delay_steps : int, array-like, optional Minimum network delay. Default: ``1``. Returns ------- int Local delay offset for buffer indexing. Raises ------ ValueError If ``delay_steps < min_delay_steps`` (violates causality). Notes ----- NEST uses absolute delays and computes buffer offsets as ``delay - min_delay``. Local step-based APIs use zero-indexed delays, so we add 1 to align with NEST's buffer indexing semantics where delay=1 corresponds to the next step. """ delay = cls._validate_delay_steps(delay_steps, name='delay_steps') min_delay = cls._validate_delay_steps(min_delay_steps, name='min_delay_steps') if delay < min_delay: raise ValueError('delay_steps must be >= min_delay_steps.') return int(delay - min_delay + 1) @staticmethod def _to_coeff_array(value: ArrayLike) -> np.ndarray: r"""Convert input to validated 1D float64 coefficient array. Parameters ---------- value : array-like Coefficient value(s). May be scalar, array, or saiunit Quantity. Returns ------- ndarray Validated 1D float64 array, shape ``(n,)`` where ``n >= 1``. Raises ------ ValueError If the result is empty (size 0). Notes ----- - Strips units from saiunit Quantity objects. - Flattens multi-dimensional inputs to 1D. - Scalars are converted to shape ``(1,)`` arrays. """ 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_float_scalar(value: ArrayLike, name: str) -> float: r"""Convert input to validated scalar float. Parameters ---------- value : array-like Input value (may be scalar, array, or saiunit Quantity). name : str Parameter name for error messages. Returns ------- float Validated scalar float64 value. Raises ------ ValueError If the input is not scalar (size != 1 after flattening). Notes ----- - Strips units from saiunit Quantity objects. - Flattens multi-dimensional inputs before checking size. """ 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 validated scalar integer. Parameters ---------- value : array-like Input value (may be scalar, array, or saiunit Quantity). name : str Parameter name for error messages. Returns ------- int Validated integer value (rounded if float is within tolerance). Raises ------ ValueError If the input is: - Not scalar (size != 1 after flattening). - Not finite (NaN or ±inf). - Not integer-valued (differs from nearest int by > 1e-12). Notes ----- - Strips units from saiunit Quantity objects. - Allows float inputs if they are integer-valued within 1e-12 tolerance. """ 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 @classmethod def _validate_delay_steps(cls, delay_steps: ArrayLike, name: str = 'delay_steps') -> int: r"""Validate delay parameter and ensure it is a positive integer. Parameters ---------- delay_steps : array-like Delay value in simulation steps. name : str, optional Parameter name for error messages. Default: ``'delay_steps'``. Returns ------- int Validated delay value (≥ 1). Raises ------ ValueError If ``delay_steps`` is not scalar, not integer-valued, or ``< 1``. Notes ----- Minimum delay of 1 step is required for causal event delivery in NEST semantics. """ d = cls._to_int_scalar(delay_steps, name=name) if d < 1: raise ValueError(f'{name} must be >= 1.') return d