Source code for brainpy_state._nest_synapse.sic_connection

from typing import Any

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

from brainpy_state._nest_base.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 is the NEST-parity connection *spec* for ``sic_connection``
    (``models/sic_connection.{h,cpp}``): a one-way astrocyte->neuron edge carrying the
    slow inward current. It stores the scalar ``weight`` (a unitless multiplier of the
    astrocyte's per-step ``SIC``) and the integer ``delay_steps``, validates the
    sender/receiver model pair, and is consumed by :class:`~brainpy_state.Simulator`,
    which builds the routing — an ``as_current``
    :class:`~brainpy_state._nest_network.event_proj.EventProjection` that reads the
    astrocyte's emission holder and deposits ``weight·SIC`` into the neuron's labelled
    ``'I_SIC'`` current channel each step.

    **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. Substrate delivery**

    On the :class:`~brainpy_state.Simulator` substrate the astrocyte emits its per-step
    graded ``SIC`` continuously (seam-(H) emission). The Simulator reads that emission
    holder and deposits

    .. math::

       I_{\text{SIC}} = w \cdot \mathrm{SIC}[n-1]

    into the neuron's labelled ``'I_SIC'`` current channel, which the neuron reads (and
    pops) **before** its ``I_stim`` read so the device current and the SIC current never
    collide. The edge is one-way (a NEST ``SICEvent`` has no back-channel).
    ``delay_steps=1`` (the NEST default / minimum delay) rides the substrate's intrinsic
    one-step pipeline latency; a larger ``delay_steps`` adds ``(delay_steps - 1)``
    buffered steps. There is **no** host-side event queue: the SIC current is a
    State-backed channel, so the whole simulation lowers into one compiled ``for_loop``.

    Parameters
    ----------
    weight : float, array-like, optional
        Unitless weight multiplying the astrocyte's per-step ``SIC``. 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 is the connection *spec* only. The per-step ``SIC`` is generated by
      the source model (``astrocyte_lr_1994``) and the membrane integration by the
      target (``aeif_cond_alpha_astro``); the :class:`~brainpy_state.Simulator` builds
      the routing between them.
    - Connection objects are stateless; all state (membrane conductances, SIC time
      courses) is maintained by source and target neuron models.

    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

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

    **Wire it on a Simulator (astrocyte -> neuron):**

    .. code-block:: python

        >>> import brainunit as u
        >>> sim = bp.Simulator(dt=0.1 * u.ms)
        >>> astro = sim.create(bp.astrocyte_lr_1994, 1)
        >>> neuron = sim.create(bp.aeif_cond_alpha_astro, 1)
        >>> proj = sim.connect(astro, neuron, synapse=bp.sic_connection(weight=0.5))

    **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
@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 brainunit 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 brainunit 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 brainunit 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 brainunit 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