Source code for brainpy_state._nest.bernoulli_synapse

# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

# -*- coding: utf-8 -*-


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

from .static_synapse import static_synapse

__all__ = [
    'bernoulli_synapse',
]

_UNSET = object()


class bernoulli_synapse(static_synapse):
    r"""NEST-compatible ``bernoulli_synapse`` connection model.

    Short description
    -----------------

    Static synapse with stochastic transmission.

    Description
    -----------

    ``bernoulli_synapse`` mirrors NEST ``models/bernoulli_synapse.h``.
    The model is non-plastic and stores fixed parameters:

    - synaptic weight ``weight``,
    - synaptic delay ``delay``,
    - receiver port ``receptor_type``,
    - transmission probability ``p_transmit``.

    On each outgoing event, one Bernoulli trial is drawn and the event is
    transmitted only if the trial succeeds:

    .. math::

       \mathrm{send} \iff U < p_{\mathrm{transmit}}, \quad U \sim \mathrm{Uniform}(0, 1).

    A failed trial drops the event entirely.

    **1. Mathematical formulation**

    Stochastic transmission probability: Each spike arriving at the synapse
    triggers a Bernoulli trial with success probability :math:`p_{\mathrm{transmit}}`.
    The trial outcome determines whether the event is forwarded to the
    postsynaptic target. Formally:

    .. math::

       P(\text{transmit spike}) = p_{\mathrm{transmit}}

    The transmission decision for each spike is independent of previous trials.
    No state variables are maintained across spikes — the synapse is memoryless.

    **2. Implementation details**

    Random number generation uses NumPy's global RNG state (``np.random.random()``).
    The trial outcome is computed by comparing a uniform random variate
    :math:`U \sim \mathrm{Uniform}(0, 1)` to the threshold :math:`p_{\mathrm{transmit}}`:

    .. math::

       \text{transmit} = \begin{cases}
         \text{True}, & U < p_{\mathrm{transmit}} \\
         \text{False}, & \text{otherwise}
       \end{cases}

    If the trial fails, the spike is discarded before scheduling. If successful,
    the spike is forwarded using the parent :class:`static_synapse` delivery
    mechanism with the configured ``weight`` and ``delay``.

    **3. Event send ordering (NEST source equivalent)**

    NEST ``models/bernoulli_synapse.h`` performs:

    1. Draw Bernoulli decision from uniform random number and ``p_transmit``.
    2. If successful:
       ``e.set_weight(weight_)``
    3. ``e.set_delay_steps(get_delay_steps())``
    4. ``e.set_receiver(*get_target(tid))``
    5. ``e.set_rport(get_rport())``
    6. ``e()`` (deliver event)

    This implementation preserves the same semantics: stochastic transmission
    is decided before scheduling; when accepted, inherited
    :class:`static_synapse` scheduling applies weight, delay steps, receiver
    and receptor port in the same effective order.

    **4. Biological motivation**

    Synaptic transmission in real neural circuits is stochastic due to finite
    vesicle release probabilities and quantal failures. This model approximates
    such unreliable transmission using a simple Bernoulli process, which is
    computationally efficient and captures the essential statistical properties
    of transmission failures.

    Applications include:

    - Sparse networks with probabilistic connectivity (e.g., cortical column
      models, [2]_).
    - Networks with strong-sparse and weak-dense connectivity motifs [3]_.
    - Recurrent networks where transmission failures contribute to decorrelation
      and robustness [4]_.

    **5. Computational considerations**

    - Random number generation introduces non-determinism. For reproducible
      simulations, seed the NumPy RNG before initializing the network:
      ``np.random.seed(42)``.
    - Each spike invokes one call to ``np.random.random()``, adding negligible
      computational overhead compared to deterministic synapses.
    - The model is stateless and suitable for large-scale network simulations
      where memory footprint per synapse is a constraint.

    Parameters
    ----------
    weight : ArrayLike, optional
        Fixed synaptic weight (dimensionless or receiver-dependent units such as
        pA, nS, or mV). Must be scalar. Default: ``1.0``.
    delay : ArrayLike, optional
        Synaptic delay in ms. Must be scalar. Converted to integer simulation
        steps using round-to-nearest with midpoint-up (NEST convention).
        Default: ``1.0 * u.ms``.
    receptor_type : int, optional
        Receiver port/receptor id. Used to target specific synaptic receptor
        channels on the postsynaptic neuron. Default: ``0``.
    p_transmit : ArrayLike, optional
        Spike transmission probability in ``[0, 1]``. Must be scalar. Values
        outside this range raise ``ValueError``. Default: ``1.0``.
    post : object, optional
        Default receiver object (postsynaptic neuron or target module). If not
        provided, a receiver must be passed explicitly to :meth:`send` when
        scheduling events.
    event_type : str, optional
        Event transmission type. Inherited from :class:`static_synapse`. One of
        ``'spike'``, ``'rate'``, ``'current'``, ``'conductance'``,
        ``'double_data'``, ``'data_logging'``. Default: ``'spike'``.
    name : str, optional
        Object name for debugging and introspection.

    Raises
    ------
    ValueError
        If ``p_transmit`` is not scalar or is outside ``[0, 1]``.

    See Also
    --------
    static_synapse : Parent class implementing deterministic event scheduling.
    tsodyks_synapse : Short-term plasticity model with dynamic transmission.

    Notes
    -----
    - This model does not implement plasticity (no weight updates, no state
      evolution).
    - Random draws use NumPy's global RNG state. Use ``np.random.seed(...)``
      for reproducible test traces when needed.
    - The model is compatible with all event types supported by
      :class:`static_synapse`, though biological applications typically use
      ``event_type='spike'``.
    - Transmission failures are independent across spikes and across different
      synapse instances.

    Examples
    --------
    Create a stochastic synapse with 50% transmission probability:

    .. code-block:: python

       >>> import brainpy.state as bst
       >>> import saiunit as u
       >>> post_neuron = bst.LIF(1)
       >>> syn = bst.bernoulli_synapse(
       ...     weight=5.0,
       ...     delay=2.0 * u.ms,
       ...     p_transmit=0.5,
       ...     post=post_neuron,
       ... )

    Simulate stochastic transmission and observe dropped spikes:

    .. code-block:: python

       >>> import numpy as np
       >>> np.random.seed(42)  # for reproducibility
       >>> transmitted = [syn.send(multiplicity=1.0) for _ in range(10)]
       >>> transmitted
       [False, True, False, True, True, False, True, False, False, True]

    Query current parameters:

    .. code-block:: python

       >>> params = syn.get()
       >>> params['p_transmit']
       0.5

    Update transmission probability dynamically:

    .. code-block:: python

       >>> syn.set(p_transmit=0.8)
       >>> syn.p_transmit
       0.8

    References
    ----------
    .. [1] NEST source: ``models/bernoulli_synapse.h`` and
           ``models/bernoulli_synapse.cpp``.
    .. [2] Lefort S, Tomm C, Sarria J-C F, Petersen CCH (2009).
           The excitatory neuronal network of the C2 barrel column in mouse
           primary somatosensory cortex. Neuron, 61(2):301-316.
           DOI: https://doi.org/10.1016/j.neuron.2008.12.020
    .. [3] Teramae J, Tsubo Y, Fukai T (2012). Optimal spike-based
           communication in excitable networks with strong-sparse and
           weak-dense links. Scientific Reports 2, 485.
           DOI: https://doi.org/10.1038/srep00485
    .. [4] Omura Y, Carvalho MM, Inokuchi K, Fukai T (2015).
           A lognormal recurrent network model for burst generation during
           hippocampal sharp waves. Journal of Neuroscience, 35(43):14585-14601.
           DOI: https://doi.org/10.1523/JNEUROSCI.4944-14.2015
    """

    __module__ = 'brainpy.state'

    def __init__(
        self,
        weight: ArrayLike = 1.0,
        delay: ArrayLike = 1.0 * u.ms,
        receptor_type: int = 0,
        p_transmit: ArrayLike = 1.0,
        post=None,
        event_type: str = 'spike',
        name: str | None = None,
    ):
        super().__init__(
            weight=weight,
            delay=delay,
            receptor_type=receptor_type,
            post=post,
            event_type=event_type,
            name=name,
        )

        self.p_transmit = self._to_scalar_probability(p_transmit)
        self._validate_probability(self.p_transmit)

    @staticmethod
    def _to_scalar_probability(value: ArrayLike) -> float:
        r"""Convert input to scalar float for probability validation.

        Parameters
        ----------
        value : ArrayLike
            Input value to convert. Must be scalar-compatible (size 1).

        Returns
        -------
        float
            Scalar float value extracted from input.

        Raises
        ------
        ValueError
            If input is not scalar (size != 1).
        """
        dftype = brainstate.environ.dftype()
        arr = np.asarray(u.math.asarray(value, dtype=dftype), dtype=dftype)
        if arr.size != 1:
            raise ValueError('p_transmit must be scalar.')
        return float(arr.reshape(()))

    @staticmethod
    def _validate_probability(p_transmit: float):
        r"""Validate that transmission probability is in valid range.

        Parameters
        ----------
        p_transmit : float
            Transmission probability to validate.

        Raises
        ------
        ValueError
            If ``p_transmit`` is outside the interval ``[0, 1]``.
        """
        if p_transmit < 0.0 or p_transmit > 1.0:
            raise ValueError('Spike transmission probability must be in [0, 1].')

    @staticmethod
    def _sample_uniform() -> float:
        r"""Draw one uniform random variate in [0, 1).

        Uses NumPy's global RNG state (``np.random.random()``).

        Returns
        -------
        float
            Uniform random value in ``[0, 1)``.

        Notes
        -----
        This method is stateless and relies on NumPy's global random number
        generator. For reproducible results, seed the RNG before simulation:
        ``np.random.seed(42)``.
        """
        return float(np.random.random())

[docs] def get(self) -> dict: r"""Return current public parameters. Returns ------- dict Dictionary containing all connection parameters: - ``'weight'`` : float — synaptic weight. - ``'delay'`` : float — synaptic delay in ms. - ``'receptor_type'`` : int — receiver port id. - ``'p_transmit'`` : float — transmission probability in ``[0, 1]``. - ``'synapse_model'`` : str — model identifier (``'bernoulli_synapse'``). Examples -------- .. code-block:: python >>> syn = bst.bernoulli_synapse(weight=2.0, p_transmit=0.7) >>> params = syn.get() >>> params['p_transmit'] 0.7 """ params = super().get() params['p_transmit'] = float(self.p_transmit) params['synapse_model'] = 'bernoulli_synapse' return params
[docs] def set( self, *, weight: ArrayLike | object = _UNSET, delay: ArrayLike | object = _UNSET, receptor_type: ArrayLike | object = _UNSET, p_transmit: ArrayLike | object = _UNSET, post: object = _UNSET, event_type: str | object = _UNSET, ): r"""Set NEST-style public parameters. Updates one or more connection parameters. All parameters are optional; unspecified parameters retain their current values. Parameters ---------- weight : ArrayLike, optional New synaptic weight. Must be scalar. delay : ArrayLike, optional New synaptic delay in ms. Must be scalar. receptor_type : int, optional New receiver port/receptor id. p_transmit : ArrayLike, optional New transmission probability in ``[0, 1]``. Must be scalar. post : object, optional New default receiver object. event_type : str, optional New event transmission type (``'spike'``, ``'rate'``, ``'current'``, ``'conductance'``, ``'double_data'``, ``'data_logging'``). Raises ------ ValueError If ``p_transmit`` is provided and is not scalar or is outside ``[0, 1]``. Examples -------- .. code-block:: python >>> syn = bst.bernoulli_synapse(weight=1.0, p_transmit=0.5) >>> syn.set(p_transmit=0.8, weight=2.0) >>> syn.p_transmit 0.8 """ new_p = ( self.p_transmit if p_transmit is _UNSET else self._to_scalar_probability(p_transmit) ) self._validate_probability(new_p) super_kwargs = {} if weight is not _UNSET: super_kwargs['weight'] = weight if delay is not _UNSET: super_kwargs['delay'] = delay if receptor_type is not _UNSET: super_kwargs['receptor_type'] = receptor_type if post is not _UNSET: super_kwargs['post'] = post if event_type is not _UNSET: super_kwargs['event_type'] = event_type if super_kwargs: super().set(**super_kwargs) self.p_transmit = float(new_p)
[docs] def send( self, multiplicity: ArrayLike = 1.0, *, post=None, receptor_type: ArrayLike | None = None, event_type: str | None = None, ) -> bool: r"""Stochastically schedule one outgoing event. Performs a Bernoulli trial to decide whether to transmit the event. If the trial succeeds, the event is scheduled for delivery using the inherited :class:`static_synapse` mechanism. If the trial fails, the event is discarded. Parameters ---------- multiplicity : ArrayLike, optional Event payload value (spike count, rate, or current). If zero or negligible (< 1e-12), the event is dropped without drawing a random number. Default: ``1.0``. post : object, optional Receiver object (postsynaptic neuron). Overrides the default receiver set in :meth:`__init__` or :meth:`set`. If both are ``None``, scheduling will fail. receptor_type : int, optional Receiver port/receptor id. Overrides the default receptor type. event_type : str, optional Event transmission type. Overrides the default event type. Returns ------- bool ``True`` if the event was successfully transmitted and scheduled. ``False`` if the event was zero, or if the Bernoulli trial failed. Notes ----- - Zero-valued events (``multiplicity < 1e-12``) are dropped before the Bernoulli trial to avoid unnecessary random number generation. - Each call draws one uniform random variate from NumPy's global RNG. - The transmission decision is independent of previous calls. Examples -------- Simulate stochastic transmission with 50% probability: .. code-block:: python >>> import numpy as np >>> np.random.seed(42) >>> syn = bst.bernoulli_synapse(p_transmit=0.5, post=post_neuron) >>> results = [syn.send(multiplicity=1.0) for _ in range(10)] >>> results [False, True, False, True, True, False, True, False, False, True] >>> sum(results) # approximately 5 out of 10 5 Zero-valued events are dropped without drawing random numbers: .. code-block:: python >>> syn.send(multiplicity=0.0) False """ if not self._is_nonzero(multiplicity): return False send_event = self._sample_uniform() < self.p_transmit if not send_event: return False return super().send( multiplicity, post=post, receptor_type=receptor_type, event_type=event_type, )