Source code for brainpy_state._nest.gap_junction

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


class gap_junction(NESTSynapse):
    r"""NEST-compatible ``gap_junction`` connection model for electrical synaptic coupling.

    This class implements electrical gap junction connections that transmit
    ``GapJunctionEvent`` data and contribute currents proportional to membrane
    voltage differences between coupled neurons. It follows the connection-level
    semantics of NEST's ``models/gap_junction.{h,cpp}`` implementation, including
    waveform-relaxation (WFR) support for multi-timestep integration.

    Parameters
    ----------
    weight : float or array-like, optional
        Gap junction conductance weight (unitless coupling strength).
        Must be a scalar value. Default: ``1.0``.
    name : str or None, optional
        Optional model instance name for identification. Default: ``None``.

    Attributes
    ----------
    name : str or None
        Model instance name.
    weight : float
        Gap junction conductance weight.
    sumj_g_ij : float
        Sum of incoming gap junction weights (runtime state).
    interpolation_coefficients : ndarray
        Buffer of summed interpolation coefficients from incoming events,
        shape ``(min_delay_steps * (interpolation_order + 1),)``.
    REQUIRES_SYMMETRIC : bool
        Class constant, always ``True`` for gap junctions.
    SUPPORTS_WFR : bool
        Class constant, always ``True`` for WFR compatibility.
    SUPPORTED_WFR_INTERPOLATION_ORDERS : tuple
        Supported interpolation orders: ``(0, 1, 3)``.

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

    .. list-table::
       :header-rows: 1

       * - NEST Name
         - brainpy.state Name
         - Description
       * - weight
         - weight
         - Gap junction conductance (nS)
       * - delay
         - *unsupported*
         - Delay not allowed for gap junctions

    Mathematical Formulation
    ------------------------

    *1. Gap Junction Current*

    The gap junction current for a post-synaptic neuron :math:`i` coupled to
    pre-synaptic neurons :math:`j` is given by:

    .. math::

        I_{\text{gap},i} = -\sum_j g_{ij} V_i + P_{\text{interp}}(t)

    where:

    - :math:`g_{ij}` is the gap junction conductance (weight) from neuron
      :math:`j` to :math:`i`
    - :math:`V_i` is the membrane potential of the post-synaptic neuron
    - :math:`P_{\text{interp}}(t)` is the interpolation polynomial constructed
      from pre-synaptic voltage contributions

    *2. Waveform Relaxation (WFR) Event Accumulation*

    During WFR cycles, incoming ``GapJunctionEvent`` objects update two quantities:

    .. math::

        \Sigma g_{ij} &\leftarrow \Sigma g_{ij} + w_{ij} \\
        c_k &\leftarrow c_k + w_{ij} \cdot a_k

    where:

    - :math:`w_{ij}` is the connection weight
    - :math:`a_k` are source neuron interpolation coefficients
    - :math:`c_k` are target-side summed coefficients for polynomial
      reconstruction

    *3. Interpolation Polynomials*

    The interpolation term :math:`P_{\text{interp}}(t)` depends on the WFR order:

    - **Order 0** (constant interpolation):

      .. math::

          P_{\text{interp}}(t) = c_0

    - **Order 1** (linear interpolation):

      .. math::

          P_{\text{interp}}(t) = c_0 + c_1 \cdot t

    - **Order 3** (cubic Hermite interpolation):

      .. math::

          P_{\text{interp}}(t) = c_0 + c_1 \cdot t + c_2 \cdot t^2 + c_3 \cdot t^3

    where :math:`t \in [0, 1]` is the normalized time within the current WFR
    substep.

    **Implementation Details**

    *1. Update Ordering (matching NEST)*

    The WFR cycle follows this sequence:

    1. **Begin WFR window**: Clear ``sumj_g_ij`` and interpolation coefficient
       buffer.
    2. **Event accumulation**: For each arriving secondary event, add weight and
       weighted coefficients.
    3. **ODE substeps**: Evaluate :math:`I_{\text{gap}}` from current voltage,
       lag, interpolation order, and normalized substep time.
    4. **End WFR window**: Reset runtime buffers for the next window.

    *2. Symmetric Connectivity Requirement*

    Gap junctions require symmetric connections: if neuron :math:`i` is coupled to
    neuron :math:`j` with weight :math:`w_{ij}`, then neuron :math:`j` must also be
    coupled to neuron :math:`i` with weight :math:`w_{ji}`. Asymmetric configurations
    will produce incorrect dynamics.

    *3. Delay Constraint*

    Unlike chemical synapses, gap junctions are instantaneous (or near-instantaneous)
    electrical connections. Setting a delay raises a ``ValueError``.

    *4. Coefficient Buffer Sizing*

    For WFR with ``min_delay_steps`` lags and interpolation order :math:`n`, the
    coefficient buffer size is:

    .. math::

        \text{buffer\_size} = \text{min\_delay\_steps} \times (n + 1)

    Examples
    --------
    **Basic gap junction setup:**

    .. code-block:: python

        >>> import brainpy_state as bp
        >>> import saiunit as u
        >>>
        >>> # Create gap junction with 10 nS conductance
        >>> gap = bp.nest.gap_junction(weight=10.0)
        >>>
        >>> # Query connection properties
        >>> gap.get_status()
        {'weight': 10.0, 'delay': None, 'requires_symmetric': True,
         'supports_wfr': True, 'supported_wfr_interpolation_orders': (0, 1, 3)}

    **WFR cycle simulation:**

    .. code-block:: python

        >>> import numpy as np
        >>>
        >>> # Initialize WFR with 5 delay steps and linear interpolation
        >>> gap.begin_wfr_cycle(min_delay_steps=5, interpolation_order=1)
        >>>
        >>> # Simulate incoming event with pre-synaptic coefficients
        >>> pre_coeffs = np.array([1.0, 0.5, 1.2, 0.8, 1.5, 0.3, 1.1, 0.9, 1.3, 0.7])
        >>> gap.handle_gap_event(coeffarray=pre_coeffs)
        >>>
        >>> # Evaluate gap current at lag 2, normalized time t=0.5
        >>> V_post = -65.0  # mV
        >>> I_gap = gap.evaluate_gap_current(V_m=V_post, lag=2, t=0.5)
        >>> print(f"Gap junction current: {I_gap:.2f}")
        Gap junction current: ...

    **Error: attempting to set delay:**

    .. code-block:: python

        >>> gap.set_delay(1.5)
        ValueError: gap_junction connection has no delay

    See Also
    --------
    hh_psc_alpha_gap : Hodgkin-Huxley neuron with gap junction support
    hh_cond_beta_gap_traub : Traub HH neuron with gap junction support

    Notes
    -----
    - **Delay is intentionally unsupported**: Matches NEST's constraint that gap
      junctions have no synaptic delay.
    - **Connection-level semantics only**: This class represents gap junction
      connection properties. Neuron-specific ODE dynamics (voltage interpolation,
      coefficient broadcasting) remain in the neuron model.
    - **Interpolation coefficient ownership**: The neuron model owns and updates
      interpolation coefficients based on its voltage history. This class only
      accumulates weighted coefficients from incoming events.
    - **Thread safety**: Not thread-safe. Runtime state (``sumj_g_ij``,
      ``interpolation_coefficients``) assumes single-threaded access within each
      WFR cycle.

    References
    ----------
    .. [1] NEST Simulator gap junction implementation:
           ``models/gap_junction.h`` and ``models/gap_junction.cpp``.
    .. [2] NEST gap current evaluation in neuron models:
           ``models/hh_psc_alpha_gap.cpp`` and ``models/hh_cond_beta_gap_traub.cpp``.
    .. [3] Hahne, J., et al. (2015). "A unified framework for spiking and gap-junction
           interactions in distributed neuronal network simulations."
           *Frontiers in Neuroinformatics*, 9:22. doi:10.3389/fninf.2015.00022
    """

    __module__ = 'brainpy.state'

    REQUIRES_SYMMETRIC = True
    SUPPORTS_WFR = True
    SUPPORTED_WFR_INTERPOLATION_ORDERS = (0, 1, 3)

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

        # Runtime state accumulated from incoming GapJunctionEvent payloads.
        self.sumj_g_ij: float = 0.0
        dftype = brainstate.environ.dftype()
        self.interpolation_coefficients = np.zeros((0,), dtype=dftype)
        self._interpolation_order = 0

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

        Returns
        -------
        dict[str, Any]
            Dictionary containing:
              - ``'requires_symmetric'`` (bool): Always ``True``.
              - ``'supports_wfr'`` (bool): Always ``True``.

        Examples
        --------
        .. code-block:: python

            >>> gap = bp.nest.gap_junction(weight=5.0)
            >>> gap.properties
            {'requires_symmetric': True, 'supports_wfr': True}
        """
        return {
            'requires_symmetric': self.REQUIRES_SYMMETRIC,
            'supports_wfr': self.SUPPORTS_WFR,
        }

[docs] def get_status(self) -> dict[str, Any]: r"""Retrieve the current connection status and parameters. Returns ------- dict[str, Any] Status dictionary containing: - ``'weight'`` (float): Current gap junction weight. - ``'delay'`` (None): Always ``None`` (delays not supported). - ``'requires_symmetric'`` (bool): Symmetric connectivity requirement. - ``'supports_wfr'`` (bool): WFR support flag. - ``'supported_wfr_interpolation_orders'`` (tuple): Tuple of supported interpolation orders ``(0, 1, 3)``. Examples -------- .. code-block:: python >>> gap = bp.nest.gap_junction(weight=2.5) >>> status = gap.get_status() >>> status['weight'] 2.5 >>> status['delay'] is None True See Also -------- set_status : Update connection parameters get : Retrieve specific status keys """ # Keep "delay" present (as None) for API parity with model status. return { 'weight': float(self.weight), 'delay': None, 'requires_symmetric': self.REQUIRES_SYMMETRIC, 'supports_wfr': self.SUPPORTS_WFR, 'supported_wfr_interpolation_orders': self.SUPPORTED_WFR_INTERPOLATION_ORDERS, }
[docs] def set_status(self, status: dict[str, Any] | None = None, **kwargs): r"""Update connection parameters from a status dictionary or keyword arguments. Parameters ---------- status : dict[str, Any] or None, optional Dictionary of parameter updates. Keys must be valid parameter names. **kwargs Additional parameter updates passed as keyword arguments. These override values in ``status`` if both are provided. Raises ------ ValueError If attempting to set ``'delay'`` (gap junctions have no delay). Examples -------- .. code-block:: python >>> gap = bp.nest.gap_junction(weight=1.0) >>> gap.set_status({'weight': 5.0}) >>> gap.get_status()['weight'] 5.0 >>> gap.set_status(weight=10.0) >>> gap.weight 10.0 **Error case:** .. code-block:: python >>> gap.set_status({'delay': 1.0}) ValueError: gap_junction connection has no delay See Also -------- get_status : Retrieve current status set_weight : Update weight parameter directly """ updates = {} if status is not None: updates.update(status) updates.update(kwargs) if 'delay' in updates: raise ValueError('gap_junction connection has no delay') if 'weight' in updates: self.set_weight(updates['weight'])
[docs] def get(self, key: str = 'status'): r"""Retrieve a specific status parameter or the full status dictionary. Parameters ---------- key : str, optional Parameter name to retrieve. Use ``'status'`` for the full dictionary. Default: ``'status'``. Returns ------- Any If ``key == 'status'``, returns the full status dictionary. Otherwise, returns the value associated with ``key``. Raises ------ KeyError If ``key`` is not a valid status parameter name. Examples -------- .. code-block:: python >>> gap = bp.nest.gap_junction(weight=3.5) >>> gap.get('weight') 3.5 >>> gap.get('requires_symmetric') True >>> gap.get() # defaults to 'status' {'weight': 3.5, 'delay': None, ...} See Also -------- get_status : Full status dictionary retrieval """ 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 gap_junction.get().')
[docs] def set_weight(self, weight: ArrayLike): r"""Update the gap junction conductance weight. Parameters ---------- weight : float or array-like New gap junction weight (must be scalar). Raises ------ ValueError If ``weight`` is not a scalar value. Examples -------- .. code-block:: python >>> gap = bp.nest.gap_junction(weight=1.0) >>> gap.set_weight(5.5) >>> gap.weight 5.5 See Also -------- set_status : Update multiple parameters at once """ self.weight = self._to_float_scalar(weight, name='weight')
[docs] def set_delay(self, _): r"""Attempt to set delay (always raises an error). Gap junctions are instantaneous electrical connections and do not support synaptic delays. This method exists for API compatibility with NEST but always raises a ``ValueError``. Parameters ---------- _ : Any Ignored (delay value). Raises ------ ValueError Always raised with message ``'gap_junction connection has no delay'``. Examples -------- .. code-block:: python >>> gap = bp.nest.gap_junction() >>> gap.set_delay(1.5) ValueError: gap_junction connection has no delay See Also -------- set_status : General parameter update method (also rejects delay) """ raise ValueError('gap_junction connection has no delay')
[docs] def begin_wfr_cycle(self, min_delay_steps: ArrayLike, interpolation_order: int = 0): r"""Initialize a new waveform-relaxation (WFR) cycle. Resets runtime state (``sumj_g_ij`` and ``interpolation_coefficients``) and allocates the coefficient buffer based on the number of delay steps and the interpolation order. Parameters ---------- min_delay_steps : int or array-like Minimum delay in discrete timesteps (must be > 0). Determines the number of lag slots in the interpolation coefficient buffer. interpolation_order : int, optional Interpolation polynomial order. Must be in ``{0, 1, 3}``: - ``0`` -- Constant interpolation (piecewise constant). - ``1`` -- Linear interpolation. - ``3`` -- Cubic Hermite interpolation. Default: ``0``. Raises ------ ValueError - If ``min_delay_steps <= 0``. - If ``interpolation_order`` is not in ``{0, 1, 3}``. Notes ----- The coefficient buffer size is ``min_delay_steps * (interpolation_order + 1)``. For example, with ``min_delay_steps=5`` and ``interpolation_order=1``, the buffer will have 10 elements (2 coefficients per lag). Examples -------- .. code-block:: python >>> gap = bp.nest.gap_junction(weight=2.0) >>> gap.begin_wfr_cycle(min_delay_steps=3, interpolation_order=1) >>> gap.interpolation_coefficients.shape (6,) # 3 lags * 2 coefficients per lag >>> gap.sumj_g_ij 0.0 See Also -------- reset_runtime_state : Clear runtime state without reallocating buffer handle_gap_event : Accumulate incoming gap junction events """ min_delay_steps = self._to_int_scalar(min_delay_steps, name='min_delay_steps') if min_delay_steps <= 0: raise ValueError('min_delay_steps must be > 0.') interpolation_order = self._validate_interpolation_order(interpolation_order) coeff_len = min_delay_steps * (interpolation_order + 1) self._interpolation_order = interpolation_order self.sumj_g_ij = 0.0 dftype = brainstate.environ.dftype() self.interpolation_coefficients = np.zeros((coeff_len,), dtype=dftype)
[docs] def reset_runtime_state(self): r"""Clear runtime state accumulated from gap junction events. Resets ``sumj_g_ij`` to ``0.0`` and zeros out the interpolation coefficient buffer without reallocating it. This method is typically called at the end of a WFR cycle to prepare for the next cycle. Examples -------- .. code-block:: python >>> gap = bp.nest.gap_junction(weight=2.0) >>> gap.begin_wfr_cycle(min_delay_steps=3, interpolation_order=0) >>> gap.handle_gap_event(coeffarray=np.array([1.0, 2.0, 3.0])) >>> gap.sumj_g_ij 2.0 >>> gap.reset_runtime_state() >>> gap.sumj_g_ij 0.0 >>> np.all(gap.interpolation_coefficients == 0.0) True See Also -------- begin_wfr_cycle : Initialize WFR cycle (resets and reallocates buffer) handle_gap_event : Accumulate gap junction events """ self.sumj_g_ij = 0.0 if self.interpolation_coefficients.size > 0: self.interpolation_coefficients.fill(0.0)
[docs] def prepare_secondary_event(self, coeffarray: ArrayLike) -> dict[str, Any]: r"""Prepare a secondary gap junction event payload for transmission. Packages the connection weight and pre-synaptic interpolation coefficients into a dictionary suitable for transmission as a ``GapJunctionEvent`` in a WFR simulation. Parameters ---------- coeffarray : array-like Pre-synaptic neuron's interpolation coefficients. Must be a non-empty 1D array. For interpolation order :math:`n` and ``min_delay_steps`` lags, the expected size is ``min_delay_steps * (n + 1)``. Returns ------- dict[str, Any] Event payload containing: - ``'weight'`` (float): Connection weight. - ``'coeffarray'`` (ndarray): 1D float64 array of interpolation coefficients. Raises ------ ValueError If ``coeffarray`` is empty or cannot be converted to a 1D array. Examples -------- .. code-block:: python >>> import numpy as np >>> gap = bp.nest.gap_junction(weight=3.0) >>> pre_coeffs = np.array([0.5, 1.0, 1.5]) >>> event = gap.prepare_secondary_event(pre_coeffs) >>> event['weight'] 3.0 >>> event['coeffarray'] array([0.5, 1. , 1.5]) See Also -------- handle_gap_event : Process incoming gap junction events """ coeff_np = self._to_coeff_array(coeffarray) return { 'weight': float(self.weight), 'coeffarray': coeff_np, }
[docs] def handle_gap_event(self, coeffarray: ArrayLike, weight: ArrayLike | None = None): r"""Process an incoming gap junction event and accumulate contributions. Updates runtime state by adding the event weight to ``sumj_g_ij`` and accumulating weighted interpolation coefficients. This method matches NEST's ``handle(GapJunctionEvent&)`` semantics. Parameters ---------- coeffarray : array-like Pre-synaptic neuron's interpolation coefficients. Must be a 1D array with size matching the current coefficient buffer size. weight : float, array-like, or None, optional Event-specific weight override. If ``None`` (default), uses the connection's stored ``self.weight``. Raises ------ ValueError - If ``coeffarray`` is empty. - If ``coeffarray`` size does not match the current buffer size. - If ``weight`` is provided but not a scalar. Notes ----- **Accumulation rule**: .. math:: \Sigma g_{ij} &\leftarrow \Sigma g_{ij} + w \\ c_k &\leftarrow c_k + w \cdot a_k where :math:`w` is the event weight and :math:`a_k` are the incoming coefficients. **Buffer initialization**: If the coefficient buffer is empty when the first event arrives, it is initialized to match the incoming array size. Examples -------- .. code-block:: python >>> import numpy as np >>> gap = bp.nest.gap_junction(weight=2.0) >>> gap.begin_wfr_cycle(min_delay_steps=3, interpolation_order=0) >>> pre_coeffs = np.array([1.0, 1.5, 2.0]) >>> gap.handle_gap_event(coeffarray=pre_coeffs) >>> gap.sumj_g_ij 2.0 >>> gap.interpolation_coefficients array([2. , 3. , 4. ]) # weight * coeffarray **Multiple events accumulate:** .. code-block:: python >>> gap.handle_gap_event(coeffarray=np.array([0.5, 0.5, 0.5])) >>> gap.sumj_g_ij 4.0 # 2.0 + 2.0 >>> gap.interpolation_coefficients array([3. , 4. , 5. ]) # previous + 2.0 * [0.5, 0.5, 0.5] See Also -------- prepare_secondary_event : Create event payload evaluate_gap_current : Compute gap junction current from accumulated events """ coeff_np = self._to_coeff_array(coeffarray) if self.interpolation_coefficients.size == 0: dftype = brainstate.environ.dftype() self.interpolation_coefficients = np.zeros_like(coeff_np, dtype=dftype) if coeff_np.size != self.interpolation_coefficients.size: raise ValueError( f'Coefficient size mismatch: got {coeff_np.size}, expected {self.interpolation_coefficients.size}.' ) w = self.weight if weight is None else self._to_float_scalar(weight, name='weight') self.sumj_g_ij += w # Matches NEST handle(GapJunctionEvent&): interpolation_coefficients += weight * coeffarray. self.interpolation_coefficients += w * coeff_np
[docs] def evaluate_gap_current( self, V_m: ArrayLike, lag: int, t: ArrayLike = 0.0, interpolation_order: int | None = None, ): r"""Evaluate the gap junction current at a specific WFR lag and substep time. Computes the gap junction current contribution using the accumulated weight sum and interpolation coefficients from incoming events. The current is computed as: .. math:: I_{\text{gap}} = -\Sigma g_{ij} \cdot V_m + P_{\text{interp}}(t) where :math:`P_{\text{interp}}(t)` is the interpolation polynomial evaluated at normalized time :math:`t \in [0, 1]` within the current substep. Parameters ---------- V_m : float or array-like Post-synaptic membrane potential (scalar or array). Units should match the voltage scale used in the neuron model (typically mV). lag : int WFR lag index (delay step offset). Must be in range ``[0, n_lags)``, where ``n_lags = buffer_size / (interpolation_order + 1)``. t : float or array-like, optional Normalized substep time in :math:`[0, 1]`. Represents the position within the current integration step. Default: ``0.0``. interpolation_order : int or None, optional Interpolation order override. If ``None``, uses the order set in ``begin_wfr_cycle()``. Must be in ``{0, 1, 3}``. Returns ------- float or ndarray Gap junction current :math:`I_{\text{gap}}`. Shape matches ``V_m``. Units depend on weight scaling (typically nA for conductance-based models). Raises ------ ValueError - If ``interpolation_coefficients`` buffer is empty (call ``begin_wfr_cycle()`` first). - If buffer size is incompatible with ``interpolation_order``. - If ``lag`` is out of bounds. - If ``interpolation_order`` is not in ``{0, 1, 3}``. Notes ----- **Interpolation formulas**: - **Order 0** (constant): .. math:: P_{\text{interp}}(t) = c_0 - **Order 1** (linear): .. math:: P_{\text{interp}}(t) = c_0 + c_1 \cdot t - **Order 3** (cubic Hermite): .. math:: P_{\text{interp}}(t) = c_0 + c_1 \cdot t + c_2 \cdot t^2 + c_3 \cdot t^3 **Coefficient indexing**: Coefficients are stored in a flat array with layout ``[lag0_c0, lag0_c1, ..., lag1_c0, lag1_c1, ...]``. For lag :math:`L` and order :math:`n`, coefficients start at index :math:`L \times (n + 1)`. Examples -------- **Constant interpolation (order 0):** .. code-block:: python >>> import numpy as np >>> gap = bp.nest.gap_junction(weight=2.0) >>> gap.begin_wfr_cycle(min_delay_steps=3, interpolation_order=0) >>> gap.handle_gap_event(coeffarray=np.array([10.0, 20.0, 30.0])) >>> V_post = -65.0 # mV >>> I_gap = gap.evaluate_gap_current(V_m=V_post, lag=1) >>> I_gap 170.0 # -2.0 * (-65.0) + 40.0 **Linear interpolation (order 1):** .. code-block:: python >>> gap2 = bp.nest.gap_junction(weight=1.5) >>> gap2.begin_wfr_cycle(min_delay_steps=2, interpolation_order=1) >>> gap2.handle_gap_event(coeffarray=np.array([5.0, 10.0, 15.0, 20.0])) >>> I_gap = gap2.evaluate_gap_current(V_m=-70.0, lag=0, t=0.5) >>> # I_gap = -1.5 * (-70.0) + (7.5 + 15.0 * 0.5) >>> I_gap 120.0 **Cubic interpolation (order 3):** .. code-block:: python >>> gap3 = bp.nest.gap_junction(weight=1.0) >>> gap3.begin_wfr_cycle(min_delay_steps=1, interpolation_order=3) >>> gap3.handle_gap_event(coeffarray=np.array([1.0, 2.0, 3.0, 4.0])) >>> I_gap = gap3.evaluate_gap_current(V_m=-60.0, lag=0, t=0.3) >>> # I_gap = -1.0 * (-60.0) + (1.0 + 2.0*0.3 + 3.0*0.09 + 4.0*0.027) >>> I_gap 62.438 See Also -------- begin_wfr_cycle : Initialize WFR cycle with interpolation order handle_gap_event : Accumulate gap junction events reset_runtime_state : Clear accumulated state """ if self.interpolation_coefficients.size == 0: raise ValueError('No interpolation coefficients available. Call begin_wfr_cycle() first.') order = self._interpolation_order if interpolation_order is None else interpolation_order order = self._validate_interpolation_order(order) n_per_lag = order + 1 if self.interpolation_coefficients.size % n_per_lag != 0: raise ValueError('Interpolation coefficient buffer size is incompatible with interpolation order.') lag = self._to_int_scalar(lag, name='lag') n_lags = self.interpolation_coefficients.size // n_per_lag if lag < 0 or lag >= n_lags: raise ValueError(f'lag {lag} is out of bounds for {n_lags} lag slots.') dftype = brainstate.environ.dftype() v = np.asarray(u.math.asarray(V_m), dtype=dftype) t = np.asarray(u.math.asarray(t), dtype=dftype) base = -self.sumj_g_ij * v if order == 0: return base + self.interpolation_coefficients[lag] if order == 1: i0 = lag * 2 return base + self.interpolation_coefficients[i0] + self.interpolation_coefficients[i0 + 1] * t i0 = lag * 4 t2 = t * t return ( base + self.interpolation_coefficients[i0] + self.interpolation_coefficients[i0 + 1] * t + self.interpolation_coefficients[i0 + 2] * t2 + self.interpolation_coefficients[i0 + 3] * t2 * t )
@classmethod def _validate_interpolation_order(cls, order: int) -> int: r"""Validate that the interpolation order is supported. Parameters ---------- order : int Interpolation order to validate. Returns ------- int Validated interpolation order. Raises ------ ValueError If ``order`` is not in ``{0, 1, 3}``. """ order = cls._to_int_scalar(order, name='interpolation_order') if order not in cls.SUPPORTED_WFR_INTERPOLATION_ORDERS: raise ValueError('Interpolation order must be 0, 1, or 3.') return order @staticmethod def _to_coeff_array(value: ArrayLike) -> np.ndarray: r"""Convert input to a 1D float64 coefficient array. Parameters ---------- value : array-like Input value (may be a saiunit Quantity, ndarray, or scalar). Returns ------- ndarray 1D float64 array with at least one element. Raises ------ ValueError If the resulting array is empty. """ 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 a scalar float value. Parameters ---------- value : array-like Input value (may be a saiunit Quantity, ndarray, or scalar). name : str Parameter name for error messages. Returns ------- float Scalar float value. Raises ------ ValueError If the input is not a 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 scalar integer value. Parameters ---------- value : array-like Input value (may be a saiunit Quantity, ndarray, or scalar). name : str Parameter name for error messages. Returns ------- int Scalar integer value (rounded if necessary). Raises ------ ValueError If the input is not a 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 int(arr[0])