# 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 -*-
from collections.abc import Mapping
from brainstate.typing import ArrayLike
from .static_synapse import static_synapse
__all__ = [
'static_synapse_hom_w',
]
class static_synapse_hom_w(static_synapse):
r"""NEST-compatible ``static_synapse_hom_w`` with shared homogeneous weight.
``static_synapse_hom_w`` implements a static (non-plastic) synaptic connection
with a single shared weight parameter across all connections instantiated from
this model. This design reduces memory overhead for networks with many synapses
that share identical weights, mirroring NEST's ``CommonPropertiesHomW`` template.
The model inherits all event transmission, delay scheduling, and receptor routing
semantics from :class:`static_synapse`, but enforces a critical constraint: the
weight parameter is a **model-level property** rather than a per-connection
attribute. Attempting to set individual connection weights raises a ``ValueError``.
**1. Mathematical Model**
The event transmission equation is identical to :class:`static_synapse`:
.. math::
\text{output}(t + d) = w_{\text{common}} \cdot \text{input}(t)
where :math:`w_{\text{common}}` is the single shared weight value. All connections
using this model apply the same weight scaling factor.
**2. Homogeneous Weight Semantics**
**Model-level weight management:**
- **Initialization**: The ``weight`` parameter passed to ``__init__`` becomes the
shared weight for all connections.
- **Runtime modification**: Calling ``set(weight=...)`` updates the common weight,
affecting **all** connections immediately.
- **Per-connection restriction**: ``set_weight(weight)`` raises ``ValueError`` to
prevent accidental per-connection weight assignment.
- **Connection specification**: Providing ``weight`` in synapse specification dicts
(e.g., NEST ``syn_spec``) is forbidden; checked by ``check_synapse_params``.
This constraint ensures that:
1. Memory usage scales with number of unique weight values (one float), not
number of connections.
2. Weight modifications propagate instantly to all connections.
3. The API prevents accidental violations of homogeneity.
**3. Event Processing Pipeline**
NEST ``static_synapse_hom_w`` event transmission follows this sequence
(from ``models/static_synapse_hom_w.h:send()``):
1. **Weight retrieval**: ``e.set_weight(cp.get_weight())`` — fetch common weight
from ``CommonPropertiesHomW``
2. **Delay assignment**: ``e.set_delay_steps(get_delay_steps())``
3. **Receiver resolution**: ``e.set_receiver(*get_target(tid))``
4. **Receptor port**: ``e.set_rport(get_rport())``
5. **Event delivery**: ``e()`` — trigger receiver's event handler
This implementation preserves the same ordering by inheriting the
:meth:`static_synapse.send` and :meth:`static_synapse.update` methods unchanged.
The weight value is accessed via ``self.weight`` during event scheduling.
**4. Use Cases and Design Rationale**
**When to use ``static_synapse_hom_w``:**
- Large-scale networks with uniform connection strengths within populations
- Memory-constrained environments (embedded systems, large-scale simulations)
- Networks where connection weights are controlled algorithmically (e.g., all
inhibitory→excitatory synapses have weight -0.5)
- Rapid exploration of weight parameter space (single-value updates)
**When to use ``static_synapse`` instead:**
- Heterogeneous connection strengths (e.g., distance-dependent weights)
- Per-connection weight modifications (e.g., normalization, homeostatic scaling)
- Connection pruning or weight initialization from learned patterns
**Memory trade-offs:**
- ``static_synapse``: :math:`O(N)` weight storage for :math:`N` connections
- ``static_synapse_hom_w``: :math:`O(1)` weight storage (one shared value)
For a network with :math:`10^6` connections, this reduces weight memory from
~4 MB (float32) to 4 bytes.
**5. NEST Compatibility Notes**
This implementation replicates NEST behavior with minor API differences:
**Matching NEST semantics:**
- ``GetStatus()`` returns ``synapse_model: 'static_synapse_hom_w'``
- ``set_weight()`` raises error: "individual weights cannot be set"
- Weight modification only via model-level ``CopyModel()`` equivalent
- Event ordering matches NEST ``Connection::send()`` pipeline
**brainpy.state API adaptations:**
- NEST uses ``CopyModel()`` to create weight variants; brainpy.state uses
``set(weight=...)`` on the model instance
- NEST checks weight specification at connection creation; brainpy.state checks
in ``check_synapse_params()`` (typically called by projection classes)
- ``delay`` remains per-connection (NEST allows heterogeneous delays even with
homogeneous weights)
Parameters
----------
weight : float, array-like, or Quantity, optional
Common synaptic weight shared across all connections using this model.
Scalar value, dimensionless or with units (e.g., ``0.5*u.nS`` for
conductance-based, ``100*u.pA`` for current-based).
Default: ``1.0`` (dimensionless).
delay : float, array-like, or Quantity, optional
Synaptic transmission delay. Can be connection-specific despite homogeneous
weight. Must be positive scalar with time units (recommended: ``saiunit.ms``).
Will be discretized to integer time steps according to simulation resolution.
Default: ``1.0 * u.ms``.
receptor_type : int, optional
Receptor port identifier on the postsynaptic neuron. Non-negative integer
specifying which input channel receives events. Can be connection-specific.
Default: ``0`` (primary receptor port).
post : Dynamics, optional
Default postsynaptic receiver object. If provided, :meth:`send` and
:meth:`update` will target this receiver unless overridden.
Default: ``None`` (must provide receiver explicitly in method calls).
event_type : str, optional
Type of event to transmit. Determines delivery method and receiver handling.
Must be one of: ``'spike'``, ``'rate'``, ``'current'``, ``'conductance'``,
``'double_data'``, ``'data_logging'``.
Default: ``'spike'`` (binary spike events).
name : str, optional
Unique identifier for this synapse model instance.
Default: auto-generated.
Parameter Mapping
NEST ``static_synapse_hom_w`` parameters map to this implementation as follows:
======================= ==================== ========================================
NEST Parameter brainpy.state Param Notes
======================= ==================== ========================================
``weight`` (common) ``weight`` Scalar, model-level (not per-connection)
``delay`` ``delay`` Per-connection, converted to ms
``receptor_type`` ``receptor_type`` Per-connection, integer ≥ 0
(connection target) ``post`` Explicit receiver object
(event class) ``event_type`` String identifier for event routing
======================= ==================== ========================================
Attributes
----------
weight : float or Quantity
Current common synaptic weight (read/write via :meth:`set`, NOT :meth:`set_weight`).
delay : float
Effective transmission delay in milliseconds (quantized to time steps).
Inherited from :class:`static_synapse`.
receptor_type : int
Current receptor port identifier. Inherited from :class:`static_synapse`.
post : Dynamics or None
Default postsynaptic receiver. Inherited from :class:`static_synapse`.
event_type : str
Current event transmission type. Inherited from :class:`static_synapse`.
Raises
------
ValueError
If ``set_weight(weight)`` is called (per-connection weight assignment forbidden).
ValueError
If ``check_synapse_params(syn_spec)`` receives ``weight`` in ``syn_spec`` dict.
See Also
--------
static_synapse : Base static synapse with per-connection weights
tsodyks_synapse : Short-term plasticity extension (also has homogeneous variant)
Notes
-----
**Design pattern for large-scale networks:**
When building networks with multiple weight classes (e.g., strong/weak synapses),
create separate ``static_synapse_hom_w`` instances rather than one instance with
heterogeneous weights:
.. code-block:: python
# Good: Two model instances, two weight values
strong_syn = static_synapse_hom_w(weight=2.0, delay=1.0*u.ms)
weak_syn = static_synapse_hom_w(weight=0.5, delay=1.0*u.ms)
# Bad: Would require per-connection weights → use static_synapse instead
# mixed_syn = static_synapse(weight=weights_array, delay=1.0*u.ms)
**Thread safety and concurrency:**
Not thread-safe. Concurrent calls to ``set(weight=...)`` from multiple threads
will cause race conditions. Use thread-local synapse instances or external locking.
**Performance characteristics:**
- Memory: :math:`O(1)` for weight storage regardless of connection count
- Computation: Identical to ``static_synapse`` (weight is a scalar lookup)
- Weight update latency: :math:`O(1)` to modify, affects all connections instantly
References
----------
.. [1] NEST source code: ``models/static_synapse_hom_w.h``
https://github.com/nest/nest-simulator
.. [2] NEST source code: ``nestkernel/common_properties_hom_w.h``
Template for homogeneous connection properties
https://github.com/nest/nest-simulator
.. [3] Morrison, A., Straube, S., Plesser, H. E., & Diesmann, M. (2007).
Exact subthreshold integration with continuous spike times in discrete-time
neural network simulations. *Neural Computation*, 19(1), 47-79.
https://doi.org/10.1162/neco.2007.19.1.47
Examples
--------
**Basic usage with homogeneous weight:**
.. code-block:: python
>>> import brainpy.state as bs
>>> import saiunit as u
>>> import brainstate
>>> with brainstate.environ.context(dt=0.1 * u.ms):
... # Create postsynaptic population
... post_neurons = bs.LIF(100, V_rest=-65*u.mV, V_th=-50*u.mV, tau=20*u.ms)
...
... # Create synapse with common weight
... syn = bs.static_synapse_hom_w(
... weight=0.5*u.nS, # Shared across all connections
... delay=1.5*u.ms,
... receptor_type=0,
... post=post_neurons[0],
... )
...
... # All connections use weight=0.5
... syn.send(multiplicity=1.0) # Delivers 0.5*1.0 = 0.5 to receiver
**Modifying common weight at runtime:**
.. code-block:: python
>>> with brainstate.environ.context(dt=0.1*u.ms):
... syn = bs.static_synapse_hom_w(weight=1.0, delay=1.0*u.ms)
...
... # Update common weight (affects all connections)
... syn.set(weight=2.0)
... assert syn.weight == 2.0
...
... # Verify via GetStatus
... params = syn.get()
... assert params['synapse_model'] == 'static_synapse_hom_w'
... assert params['weight'] == 2.0
**Forbidden per-connection weight assignment:**
.. code-block:: python
>>> syn = bs.static_synapse_hom_w(weight=1.0)
>>> try:
... syn.set_weight(2.5) # Attempt per-connection weight change
... except ValueError as e:
... print(f"Error: {e}")
Error: Setting of individual weights is not possible! The common weights can be changed via CopyModel().
**Checking synapse specifications (projection API):**
.. code-block:: python
>>> syn = bs.static_synapse_hom_w(weight=1.0)
>>>
>>> # Valid: delay and receptor_type can be per-connection
>>> syn.check_synapse_params({'delay': 2.0*u.ms, 'receptor_type': 1})
>>>
>>> # Invalid: weight cannot be specified per-connection
>>> try:
... syn.check_synapse_params({'weight': 2.0})
... except ValueError as e:
... print(f"Error: {e}")
Error: Weight cannot be specified since it needs to be equal for all connections when static_synapse_hom_w is used.
**Memory-efficient large-scale network:**
.. code-block:: python
>>> # 10^6 excitatory connections, all weight=0.8
>>> exc_syn = bs.static_synapse_hom_w(weight=0.8*u.nS, delay=1.0*u.ms)
>>> # Memory: ~4 bytes for weight vs ~4 MB for per-connection weights
>>>
>>> # 10^6 inhibitory connections, all weight=-0.4
>>> inh_syn = bs.static_synapse_hom_w(weight=-0.4*u.nS, delay=1.2*u.ms)
>>> # Total memory: ~8 bytes for two weight values
**Global weight scaling (homeostatic regulation):**
.. code-block:: python
>>> # Simulate 1000 steps with homeostatic scaling
>>> syn = bs.static_synapse_hom_w(weight=1.0, post=neuron)
>>> for step in range(1000):
... delivered = syn.update(pre_spike=spike_train[step])
...
... # Every 100 steps, reduce weight by 1%
... if step % 100 == 0:
... current_weight = syn.weight
... syn.set(weight=current_weight * 0.99)
**Comparing with per-connection weights:**
.. code-block:: python
>>> # Homogeneous weights: efficient for uniform connections
>>> hom_syn = bs.static_synapse_hom_w(weight=1.0)
>>> print(f"Weight storage: {hom_syn.weight}") # Single scalar
>>>
>>> # Heterogeneous weights: use static_synapse instead
>>> het_syn = bs.static_synapse(weight=np.random.uniform(0.5, 1.5, 1000))
>>> # (Note: static_synapse API may differ; this is conceptual)
**Multi-receptor configuration with shared weights:**
.. code-block:: python
>>> with brainstate.environ.context(dt=0.1*u.ms):
... target = bs.LIF(1, V_rest=-65*u.mV, V_th=-50*u.mV, tau=20*u.ms)
...
... # Excitatory inputs on receptor 0 (all weight=0.8)
... exc_syn = bs.static_synapse_hom_w(
... weight=0.8*u.nS,
... delay=1.0*u.ms,
... receptor_type=0,
... post=target,
... )
...
... # Inhibitory inputs on receptor 1 (all weight=-0.4)
... inh_syn = bs.static_synapse_hom_w(
... weight=-0.4*u.nS,
... delay=1.2*u.ms,
... receptor_type=1,
... post=target,
... )
...
... # Multiple connections can share each synapse model
... for pre_idx in range(100):
... exc_syn.send(multiplicity=1.0) # All use weight=0.8
... for pre_idx in range(50):
... inh_syn.send(multiplicity=1.0) # All use weight=-0.4
"""
__module__ = 'brainpy.state'
[docs]
def get(self) -> dict:
r"""Retrieve current synapse parameters (NEST ``GetStatus`` equivalent).
Returns a dictionary of all public synapse parameters, identical to
:meth:`static_synapse.get` but with ``synapse_model`` set to
``'static_synapse_hom_w'`` for NEST compatibility.
Returns
-------
dict
Dictionary with keys:
- ``'weight'`` : float — Current common synaptic weight (shared across connections)
- ``'delay'`` : float — Effective delay in milliseconds (quantized)
- ``'delay_steps'`` : int — Delay in simulation time steps
- ``'receptor_type'`` : int — Receptor port identifier
- ``'event_type'`` : str — Event transmission type
- ``'synapse_model'`` : str — Always ``'static_synapse_hom_w'`` (distinguishes from base class)
Notes
-----
The returned ``weight`` value is the **common model-level weight**, not a
per-connection value. All connections using this synapse model share this
single weight parameter.
Examples
--------
.. code-block:: python
>>> import brainpy.state as bs
>>> import saiunit as u
>>> import brainstate
>>> with brainstate.environ.context(dt=0.1*u.ms):
... syn = bs.static_synapse_hom_w(weight=1.5, delay=2.0*u.ms, receptor_type=1)
... params = syn.get()
... print(params['synapse_model'])
static_synapse_hom_w
>>> assert params['weight'] == 1.5
>>> assert params['synapse_model'] == 'static_synapse_hom_w'
See Also
--------
static_synapse.get : Base class parameter retrieval
set : Update synapse parameters
"""
params = super().get()
params['synapse_model'] = 'static_synapse_hom_w'
return params
[docs]
def set_weight(self, weight: ArrayLike):
r"""Reject per-connection weight assignment (NEST compatibility constraint).
This method is intentionally disabled for ``static_synapse_hom_w`` to enforce
the homogeneous weight constraint. NEST prevents per-connection weight
modifications for ``static_synapse_hom_w`` connections because the weight is
a model-level property shared across all connections, not a per-connection
attribute.
Parameters
----------
weight : float, array-like, or Quantity
Attempted per-connection weight value (always rejected).
Raises
------
ValueError
Always raised with message indicating weights must be changed at the
model level via :meth:`set` (equivalent to NEST ``CopyModel()``).
Notes
-----
**NEST API correspondence:**
In NEST, attempting to set individual weights on ``static_synapse_hom_w``
connections fails with error:
.. code-block:: text
BadProperty: Setting of individual weights is not possible!
The common weights can be changed via CopyModel().
This implementation replicates that behavior by raising ``ValueError`` with
an identical error message.
**Correct weight modification approach:**
To change the common weight, use :meth:`set` instead:
.. code-block:: python
syn.set(weight=new_value) # Correct: updates model-level weight
This updates the shared weight for **all** connections using this synapse model.
**Design rationale:**
The restriction exists to prevent accidental violations of weight homogeneity.
If per-connection weights are needed, use :class:`static_synapse` instead.
See Also
--------
set : Update common synapse weight (correct method to use)
check_synapse_params : Connection-level weight specification validator
Examples
--------
**Attempting per-connection weight modification:**
.. code-block:: python
>>> import brainpy.state as bs
>>> syn = bs.static_synapse_hom_w(weight=1.0)
>>>
>>> # This raises ValueError
>>> try:
... syn.set_weight(2.5)
... except ValueError as e:
... print(str(e))
Setting of individual weights is not possible! The common weights can be changed via CopyModel().
**Correct weight modification:**
.. code-block:: python
>>> # Use set() to update common weight
>>> syn.set(weight=2.5)
>>> assert syn.weight == 2.5 # All connections now use weight=2.5
**NEST script translation:**
NEST code:
.. code-block:: python
# NEST: Create model with homogeneous weight
nest.CopyModel("static_synapse_hom_w", "exc_syn", {"weight": 1.5})
nest.Connect(pre, post, syn_spec="exc_syn")
# NEST: Modify common weight
nest.SetDefaults("exc_syn", {"weight": 2.0})
brainpy.state equivalent:
.. code-block:: python
# brainpy.state: Create synapse with homogeneous weight
exc_syn = bs.static_synapse_hom_w(weight=1.5)
# brainpy.state: Modify common weight
exc_syn.set(weight=2.0)
"""
del weight
raise ValueError(
'Setting of individual weights is not possible! The common weights '
'can be changed via CopyModel().'
)
[docs]
def check_synapse_params(self, syn_spec: Mapping[str, object] | None):
r"""Validate synapse specification parameters for NEST compatibility.
Checks that connection-level synapse specifications do not attempt to set
per-connection weights, which violates the homogeneous weight constraint.
This method is typically called by projection/connection classes during
network construction to enforce model semantics.
Parameters
----------
syn_spec : Mapping[str, object] or None
Synapse specification dictionary containing connection-level parameters.
Common keys include ``'delay'``, ``'receptor_type'``, etc.
If ``None``, validation is skipped (no parameters to check).
Raises
------
ValueError
If ``syn_spec`` contains a ``'weight'`` key, indicating an attempt to
specify per-connection weights.
Notes
-----
**Allowed per-connection parameters:**
The following parameters CAN be specified per-connection even with homogeneous
weights:
- ``delay`` : Each connection can have a different transmission delay
- ``receptor_type`` : Each connection can target a different receptor port
- Other NEST connection parameters (e.g., ``label``, ``synapse_model``)
**Forbidden per-connection parameters:**
- ``weight`` : Must be uniform across all connections using this model
**NEST API correspondence:**
In NEST, attempting to specify weights in the synapse specification for
``static_synapse_hom_w`` connections raises:
.. code-block:: text
BadProperty: Weight cannot be specified since it needs to be
equal for all connections when static_synapse_hom_w is used.
This method replicates that validation logic.
**When this method is called:**
Projection classes (e.g., ``AlignPostProj``, ``DeltaProj``) should call this
during connection setup:
.. code-block:: python
syn_model = static_synapse_hom_w(weight=1.0)
syn_model.check_synapse_params(user_provided_syn_spec)
# Proceed with connection creation if no exception raised
**Design rationale:**
Early validation prevents runtime errors when users accidentally mix
homogeneous-weight models with heterogeneous connection specifications,
providing clear error messages at network construction time.
See Also
--------
set_weight : Per-connection weight setter (also raises error)
set : Correct method for updating common weight
Examples
--------
**Valid synapse specification (no weight):**
.. code-block:: python
>>> import brainpy.state as bs
>>> syn = bs.static_synapse_hom_w(weight=1.0)
>>>
>>> # OK: delay and receptor_type can vary per-connection
>>> syn.check_synapse_params({
... 'delay': 2.0,
... 'receptor_type': 1,
... })
>>> # No exception raised
**Invalid synapse specification (contains weight):**
.. code-block:: python
>>> # Error: weight cannot be specified per-connection
>>> try:
... syn.check_synapse_params({'weight': 2.0})
... except ValueError as e:
... print(str(e))
Weight cannot be specified since it needs to be equal for all connections when static_synapse_hom_w is used.
**Null specification (skipped validation):**
.. code-block:: python
>>> # OK: None means no parameters to validate
>>> syn.check_synapse_params(None)
**Usage in projection class:**
.. code-block:: python
>>> class MyProjection:
... def __init__(self, syn_model, syn_spec):
... # Validate synapse specification early
... syn_model.check_synapse_params(syn_spec)
... # ... proceed with connection creation ...
>>>
>>> proj = MyProjection(
... syn_model=bs.static_synapse_hom_w(weight=1.0),
... syn_spec={'delay': 1.5}, # Valid
... )
**NEST script translation:**
NEST code:
.. code-block:: python
# NEST: This would raise error during Connect()
nest.Connect(pre, post,
syn_spec={"synapse_model": "static_synapse_hom_w",
"weight": 2.0}) # ERROR
brainpy.state equivalent:
.. code-block:: python
# brainpy.state: Raises error during validation
syn = bs.static_synapse_hom_w(weight=1.0)
syn.check_synapse_params({"weight": 2.0}) # Raises ValueError
"""
if syn_spec is None:
return
if 'weight' in syn_spec:
raise ValueError(
'Weight cannot be specified since it needs to be equal for all '
'connections when static_synapse_hom_w is used.'
)