stdp_facetshw_synapse_hom#
- class brainpy.state.stdp_facetshw_synapse_hom(weight=1.0, delay=Quantity(1., "ms"), receptor_type=0, tau_plus=Quantity(20., "ms"), tau_minus_stdp=Quantity(20., "ms"), Wmax=100.0, weight_per_lut_entry=<object object>, no_synapses=0, synapses_per_driver=50, driver_readout_time=15.0, readout_cycle_duration=<object object>, lookuptable_0=(2, 3, 4, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 15), lookuptable_1=(0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13), lookuptable_2=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), configbit_0=(0, 0, 1, 0), configbit_1=(0, 1, 0, 0), reset_pattern=(1, 1, 1, 1, 1, 1), a_causal=0.0, a_acausal=0.0, a_thresh_th=21.835, a_thresh_tl=21.835, init_flag=False, synapse_id=0, next_readout_time=0.0, post=None, name=None)#
NEST-compatible
stdp_facetshw_synapse_homconnection model.Implements hardware-constrained spike-timing dependent plasticity (STDP) designed for the BrainScaleS / FACETS neuromorphic hardware platform. This model features 4-bit discrete weight representation, periodic controller-driven weight updates via look-up tables, reduced symmetric nearest-neighbor spike pairing, and configurable capacitor reset patterns. All plasticity parameters are model-level (homogeneous across all synapses).
1. Mathematical Formulation
The model maintains two exponentially decaying accumulator traces that encode pre-post spike timing relationships:
Causal accumulator (potentiation):
\[a_{\text{causal}}(t) = \sum_{t_{\text{post}} < t_{\text{pre,last}}} \exp\left(\frac{t_{\text{post}} - t_{\text{pre,last}}}{\tau_+}\right)\]Updated when a postsynaptic spike occurs before the last presynaptic spike (only the first post-spike in each interval contributes).
Acausal accumulator (depression):
\[a_{\text{acausal}}(t) = \sum_{t_{\text{post}} > t_{\text{pre}}} \exp\left(\frac{t_{\text{post}} - t_{\text{pre}}}{\tau_-}\right)\]Updated when a postsynaptic spike occurs after the current presynaptic spike (only the last post-spike in each interval contributes).
2. Hardware-Constrained Weight Update
Weight updates are not instantaneous but occur at periodic readout cycles mimicking hardware controller limitations:
Continuous to discrete conversion:
\[w_{\text{discrete}} = \text{round}\left(\frac{w_{\text{continuous}}}{w_{\text{per\_entry}}}\right)\]where \(w_{\text{per\_entry}} = W_{\max} / 15\) (4-bit representation: 0–15).
Comparator evaluation: Two evaluation functions produce binary decisions:
\[E_k(a_c, a_a) = \left(\frac{a_{\text{tl}} + c_{k,2} a_c + c_{k,1} a_a}{1 + c_{k,2} + c_{k,1}} > \frac{a_{\text{th}} + c_{k,0} a_c + c_{k,3} a_a}{1 + c_{k,0} + c_{k,3}}\right)\]where \(k \in \{0, 1\}\), \(c_{k,\cdot}\) are configuration bits, \(a_c = a_{\text{causal}}\), \(a_a = a_{\text{acausal}}\), \(a_{\text{th}} = a_{\text{thresh\_th}}\), and \(a_{\text{tl}} = a_{\text{thresh\_tl}}\).
LUT selection and weight update:
If \((E_0, E_1) = (1, 0)\) – apply
lookuptable_0[w_discrete]to get \(w_{\text{discrete}}'\)If \((E_0, E_1) = (0, 1)\) – apply
lookuptable_1[w_discrete]to get \(w_{\text{discrete}}'\)If \((E_0, E_1) = (1, 1)\) – apply
lookuptable_2[w_discrete]to get \(w_{\text{discrete}}'\)If \((E_0, E_1) = (0, 0)\) – no weight change
Capacitor reset: After LUT application, accumulators may be reset to zero based on 6-bit
reset_pattern(pairs of causal/acausal reset bits for each LUT).Discrete to continuous conversion:
\[w_{\text{continuous}}' = w_{\text{discrete}}' \times w_{\text{per\_entry}}\]
3. Readout Cycle Scheduling
Weight updates occur only when \(t_{\text{spike}} > t_{\text{next\_readout}}\), where \(t_{\text{next\_readout}}\) advances in fixed intervals:
\[T_{\text{cycle}} = \left\lceil\frac{N_{\text{synapses}}}{N_{\text{synapses/driver}}}\right\rceil \times T_{\text{driver\_readout}}\]Each synapse is assigned a unique
synapse_idupon first activation, determining its initial readout time offset within the cycle.4. Event Ordering and Timing
For a presynaptic spike at time \(t_{\text{pre}}\) with dendritic delay \(d\):
Initialize controller state (on first spike): - Assign
synapse_idfrom global counterno_synapses- Incrementno_synapses, recalculate cycle duration - Set initialnext_readout_timebased on synapse_idCheck readout window: if \(t_{\text{pre}} > t_{\text{next\_readout}}\): - Convert \(w\) to 4-bit discrete representation - Evaluate comparator functions \(E_0\) and \(E_1\) - Apply selected LUT and reset pattern - Advance \(t_{\text{next\_readout}}\) until \(t_{\text{pre}} \leq t_{\text{next\_readout}}\) - Convert updated discrete weight back to continuous value
Spike pairing: query postsynaptic history in \((t_{\text{last}} - d, t_{\text{pre}} - d]\). If history non-empty, update \(a_{\text{causal}}\) using first post-spike timestamp and update \(a_{\text{acausal}}\) using last post-spike timestamp.
Event transmission: schedule spike event with current weight at \(t_{\text{pre}} + d\)
Update state: set \(t_{\text{lastspike}} = t_{\text{pre}}\)
5. Computational Constraints
Uses on-grid spike timestamps (ignores sub-step offsets)
Reduced spike pairing (first/last only) minimizes computational cost
Discrete 4-bit weight representation matches hardware constraints
Periodic updates simulate asynchronous hardware readout cycles
- Parameters:
weight (
floatorarray-like, default:1.0) – Initial continuous synaptic weight (dimensionless). Converted to 4-bit discrete representation during readout cycles.delay (
Quantity[time], default:1.0 * u.ms) – Synaptic transmission delay. Affects timing of post-synaptic event delivery and the temporal window for spike pairing (dendritic delay \(d\) in equations above).receptor_type (
int, default:0) – Target receptor port identifier on the postsynaptic neuron. Used for routing events to specific synaptic channels (e.g., AMPA vs GABA receptors).tau_plus (
Quantity[time], default:20.0 * u.ms) – Time constant \(\tau_+\) for the causal (potentiation) accumulator. Must be positive. Controls decay rate of timing information for pre-before-post pairings.tau_minus_stdp (
Quantity[time], default:20.0 * u.ms) – Time constant \(\tau_-\) for the acausal (depression) accumulator. Must be positive. Controls decay rate of timing information for post-before-pre pairings.Wmax (
float, default:100.0) – Maximum biological weight. Defines the upper bound of the continuous weight range and is used to computeweight_per_lut_entryif not explicitly provided. Typically set to match the biological range of synaptic efficacy.weight_per_lut_entry (
float, optional) – Conversion factor between LUT index (0–15) and continuous weight. If not provided, automatically computed as \(W_{\max} / 15\). This quantization step determines the granularity of weight representation.no_synapses (
int, default:0) – Global synapse counter used by the simulated hardware controller. Automatically incremented when new synapses are initialized. Affects readout cycle scheduling.synapses_per_driver (
int, default:50) – Number of synapses updated per driver readout cycle. Must be positive. Models hardware bandwidth constraints: larger values reduce cycle duration but may be hardware-infeasible.driver_readout_time (
float, default:15.0) – Processing time (ms) required for one driver row readout. Must be positive. Combined withsynapses_per_driver, determines the total readout cycle duration.readout_cycle_duration (
float, optional) – Total duration (ms) of one complete readout cycle. If not provided, automatically computed as \(\lceil N_{\text{synapses}} / N_{\text{synapses/driver}} \rceil \times T_{\text{driver\_readout}}\). Explicitly setting this overrides the automatic calculation.lookuptable_0 (
array-likeof16 ints, default:(2,3,4,4,5,6,7,8,9,10,11,12,13,14,14,15)) – Look-up table applied when comparator evaluation yields \((E_0, E_1) = (1, 0)\). Each entry must be an integer in [0, 15]. Index by current discrete weight, returns new discrete weight. Default implements potentiation (weight increase).lookuptable_1 (
array-likeof16 ints, default:(0,0,1,2,3,4,5,6,7,8,9,10,10,11,12,13)) – Look-up table applied when comparator evaluation yields \((E_0, E_1) = (0, 1)\). Default implements depression (weight decrease).lookuptable_2 (
array-likeof16 ints, default:(0,1,2,...,15)) – Look-up table applied when comparator evaluation yields \((E_0, E_1) = (1, 1)\). Default is identity (no change). Can be configured for combined potentiation/depression.configbit_0 (
array-likeof4 ints, default:(0,0,1,0)) – Configuration bits \([c_{0,0}, c_{0,1}, c_{0,2}, c_{0,3}]\) for comparator function \(E_0\). Determines which accumulator values influence the evaluation threshold.configbit_1 (
array-likeof4 ints, default:(0,1,0,0)) – Configuration bits \([c_{1,0}, c_{1,1}, c_{1,2}, c_{1,3}]\) for comparator function \(E_1\). Default configuration creates asymmetry between \(E_0\) and \(E_1\).reset_pattern (
array-likeof6 ints, default:(1,1,1,1,1,1)) – Six reset bits controlling accumulator resets after LUT application:[causal_reset_0, acausal_reset_0, causal_reset_1, acausal_reset_1, causal_reset_2, acausal_reset_2]. Bit value 1 means reset to zero, 0 means preserve accumulator value.a_causal (
float, default:0.0) – Initial value of the causal (potentiation) accumulator. Typically starts at zero and accumulates over simulation time based on spike timing.a_acausal (
float, default:0.0) – Initial value of the acausal (depression) accumulator.a_thresh_th (
float, default:21.835) – Upper comparator threshold \(a_{\text{th}}\) used in evaluation functions. Controls sensitivity to accumulator values. Default matches NEST hardware parameters.a_thresh_tl (
float, default:21.835) – Lower comparator threshold \(a_{\text{tl}}\) used in evaluation functions.init_flag (
bool, default:False) – Internal flag tracking whether the synapse has been initialized (assigned a synapse_id). Set to True automatically upon first presynaptic spike.synapse_id (
int, default:0) – Unique identifier assigned to this synapse by the controller. Determines initial readout time offset. Automatically assigned on first spike if init_flag is False.next_readout_time (
float, default:0.0) – Timestamp (ms) of the next scheduled readout cycle for this synapse. Advances in steps ofreadout_cycle_durationafter each readout window.post (
Dynamics, optional) – Default postsynaptic target object. Can be overridden per-call insend()andupdate()methods.name (
str, optional) – Unique identifier for this synapse instance. Used for debugging and logging.
Parameter Mapping
Correspondence between NEST C++ implementation and this Python implementation:
NEST Parameter
brainpy.state Parameter
Notes
weightweightContinuous synaptic weight
delaydelayTransmission delay (ms)
tau_plustau_plusCausal time constant (ms)
tau_minus_stdptau_minus_stdpAcausal time constant (ms)
WmaxWmaxMaximum weight
no_synapsesno_synapsesGlobal synapse counter
synapses_per_driversynapses_per_driverSynapses per readout cycle
driver_readout_timedriver_readout_timeDriver processing time (ms)
readout_cycle_durationreadout_cycle_durationFull cycle duration (ms)
lookuptable_0lookuptable_0LUT for (1,0) evaluation
lookuptable_1lookuptable_1LUT for (0,1) evaluation
lookuptable_2lookuptable_2LUT for (1,1) evaluation
configbit_0configbit_04-bit config for \(E_0\)
configbit_1configbit_14-bit config for \(E_1\)
reset_patternreset_pattern6-bit accumulator reset pattern
a_causala_causalCausal accumulator state
a_acausala_acausalAcausal accumulator state
a_thresh_tha_thresh_thUpper comparator threshold
a_thresh_tla_thresh_tlLower comparator threshold
- discrete_weight#
Current 4-bit discrete weight representation (0–15). Updated during readout cycles by applying the selected look-up table.
- Type:
- t_lastspike#
Timestamp (ms) of the most recent presynaptic spike. Used to determine the temporal window for querying postsynaptic spike history.
- Type:
- Raises:
If
tau_plusortau_minus_stdpis non-positive - Ifsynapses_per_driveris non-positive - Ifdriver_readout_timeis non-positive - If look-up table entries are outside [0, 15] range - If look-up tables have mismatched sizes (must all be length 16) - Ifconfigbit_0orconfigbit_1do not have exactly 4 entries - Ifreset_patterndoes not have exactly 6 entries - Ifreadout_cycle_durationis zero or negative during active readout scheduling - If common properties (e.g.,tau_plus,Wmax, LUTs) are included in per-synapse connection specifications
Notes
Design Rationale:
This model replicates the behavior of NEST’s
stdp_facetshw_synapse_homimplementation, which was designed to simulate the BrainScaleS neuromorphic hardware platform. Key constraints:4-bit weight quantization: Hardware synapses use 4-bit weight storage (16 discrete levels), requiring explicit continuous↔discrete conversion.
Periodic controller updates: Hardware cannot update all synapses simultaneously; instead, a controller sequentially reads/writes synapse rows at fixed intervals.
Reduced spike pairing: Full all-to-all spike pairing is computationally expensive; using only first/last post-spikes per interval provides sufficient plasticity information.
Homogeneous parameters: All plasticity parameters (time constants, LUTs, thresholds) are model-level, not per-synapse, matching hardware constraints.
Differences from Standard STDP:
Standard STDP uses immediate weight updates on every spike pair. This model defers updates to periodic readout cycles.
Standard STDP uses continuous weight values. This model quantizes weights to 4-bit discrete values during updates.
Standard STDP uses all-to-all spike pairing. This model uses only first/last post-spikes.
Common-Property Restrictions:
Unlike NEST’s per-synapse models (e.g.,
stdp_synapse), this homogeneous variant does not allow time constants, LUTs, or configuration bits to be set per-connection. Attempting to pass these insyn_specdictionaries will raiseValueError. To customize these parameters:Set them at model creation:
stdp_facetshw_synapse_hom(tau_plus=15*u.ms, ...)Or use
set()method on the model instance:model.set(tau_plus=15*u.ms)
Simulation Performance:
This model is more computationally efficient than full all-to-all STDP due to:
Reduced spike pairing (O(1) instead of O(n_post_spikes) per pre-spike)
Infrequent weight updates (only at readout cycles, not every spike)
Simplified LUT-based weight changes (no exponential calculations)
However, tracking postsynaptic spike history requires memory proportional to post-spike count.
Examples
1. Basic Usage with Default Hardware Parameters
>>> import brainpy.state as bp >>> import saiunit as u >>> >>> # Create synapse with default FACETS hardware parameters >>> syn = bp.stdp_facetshw_synapse_hom( ... weight=1.0, ... delay=1.0 * u.ms, ... tau_plus=20.0 * u.ms, ... tau_minus_stdp=20.0 * u.ms, ... Wmax=100.0, ... ) >>> >>> # Initialize state >>> syn.init_state() >>> >>> # Simulate presynaptic spike at t=10 ms >>> # (post-spike must be recorded separately) >>> syn.record_post_spike(1.0, t_spike_ms=8.0) # post-before-pre >>> syn.send(1.0) # process pre-spike >>> >>> # Check updated state >>> params = syn.get() >>> print(f"Weight: {params['weight']:.3f}") >>> print(f"Causal accumulator: {params['a_causal']:.3f}")
2. Custom Look-Up Tables for Asymmetric Learning
>>> # Create LUTs with strong potentiation, weak depression >>> lut_pot = [3, 5, 7, 9, 11, 12, 13, 14, 14, 15, 15, 15, 15, 15, 15, 15] # strong LTP >>> lut_dep = [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] # weak LTD >>> >>> syn = bp.stdp_facetshw_synapse_hom( ... weight=50.0, ... Wmax=100.0, ... lookuptable_0=lut_pot, ... lookuptable_1=lut_dep, ... configbit_0=(0, 0, 1, 0), # causal-dominant for potentiation ... configbit_1=(0, 1, 0, 0), # acausal-dominant for depression ... )
3. Controller Timing Configuration
>>> # Simulate hardware with 100 synapses, 25 per driver, 10ms readout time >>> syn = bp.stdp_facetshw_synapse_hom( ... weight=10.0, ... no_synapses=100, ... synapses_per_driver=25, ... driver_readout_time=10.0, ... ) >>> >>> # Readout cycle duration computed automatically: >>> # ceil(100 / 25) * 10ms = 4 * 10ms = 40ms >>> print(f"Cycle duration: {syn.readout_cycle_duration} ms")
4. Selective Accumulator Resets
>>> # Only reset causal accumulator after potentiation (LUT 0) >>> # Preserve both accumulators for depression (LUT 1) >>> syn = bp.stdp_facetshw_synapse_hom( ... weight=50.0, ... reset_pattern=(1, 0, 0, 0, 1, 1), # reset pattern for [LUT0_causal, LUT0_acausal, ... # LUT1_causal, LUT1_acausal, ... # LUT2_causal, LUT2_acausal] ... )
See also
stdp_synapseStandard all-to-all pair-based STDP without hardware constraints
stdp_triplet_synapseTriplet STDP rule with additional pre-post-pre and post-pre-post terms
static_synapseBase class for static (non-plastic) synaptic connections
References
- check_synapse_params(syn_spec)[source]#
Validate synapse specification for connection-time parameter assignment.
Ensures that model-level (common) properties are not included in per-synapse connection specifications. The
_hom(homogeneous) variant requires all plasticity parameters to be set at the model level, not per-connection, matching NEST’s hardware-constrained design.- Parameters:
syn_spec (
dictorNone) – Synapse specification dictionary passed during connection setup. Should only contain per-synapse properties likeweight,delay, andreceptor_type. Must NOT contain plasticity parameters (time constants, LUTs, configuration bits, etc.).- Raises:
ValueError – If any of the following model-level parameters appear in
syn_spec:'tau_plus','tau_minus_stdp','Wmax','weight_per_lut_entry','no_synapses','synapses_per_driver','driver_readout_time','readout_cycle_duration','lookuptable_0','lookuptable_1','lookuptable_2','configbit_0','configbit_1','reset_pattern'.
Notes
This restriction enforces the homogeneous design constraint: all synapses using this model share the same plasticity parameters, matching the BrainScaleS hardware architecture where plasticity settings are global, not per-synapse.
To customize plasticity parameters:
Set them at model instantiation:
stdp_facetshw_synapse_hom(tau_plus=15*u.ms, ...)Or update via
set()method:model.set(tau_plus=15*u.ms)
Per-synapse properties (
weight,delay,receptor_type) CAN be specified insyn_specas they represent individual connection strengths, not shared plasticity rules.Examples
>>> import brainpy.state as bp >>> >>> syn = bp.stdp_facetshw_synapse_hom(tau_plus=20.0) >>> >>> # Valid: per-synapse weight/delay specification >>> syn.check_synapse_params({'weight': 50.0, 'delay': 1.5}) >>> # No error raised >>> >>> # Invalid: attempting to set model-level parameter per-synapse >>> try: ... syn.check_synapse_params({'tau_plus': 15.0}) ... except ValueError as e: ... print(e) tau_plus cannot be specified in connect-time synapse parameters for stdp_facetshw_synapse_hom; set common properties on the model itself (for example via CopyModel()/SetDefaults()).
- get()[source]#
Return current public parameters and mutable state.
Retrieves all user-accessible parameters and state variables in NEST-compatible format. Includes inherited static synapse properties (
weight,delay,receptor_type), plasticity parameters (time constants, LUTs, thresholds), and controller state (synapse_id, readout timing, accumulators).- Returns:
params – Dictionary mapping parameter names (str) to current values. Keys include:
'weight'(float): Current continuous synaptic weight'delay'(float): Transmission delay in ms'receptor_type'(int): Target receptor port'tau_plus'(float): Causal time constant (ms)'tau_minus_stdp'(float): Acausal time constant (ms)'Wmax'(float): Maximum weight for LUT conversion'weight_per_lut_entry'(float): Weight quantization step'no_synapses'(int): Global synapse counter'synapses_per_driver'(int): Synapses per readout cycle'driver_readout_time'(float): Driver processing time (ms)'readout_cycle_duration'(float): Full cycle duration (ms)'lookuptable_0'(list[int]): LUT for (1,0) evaluation (16 entries)'lookuptable_1'(list[int]): LUT for (0,1) evaluation (16 entries)'lookuptable_2'(list[int]): LUT for (1,1) evaluation (16 entries)'configbit_0'(list[int]): Comparator config for E_0 (4 entries)'configbit_1'(list[int]): Comparator config for E_1 (4 entries)'reset_pattern'(list[int]): Accumulator reset bits (6 entries)'a_causal'(float): Current causal accumulator value'a_acausal'(float): Current acausal accumulator value'a_thresh_th'(float): Upper comparator threshold'a_thresh_tl'(float): Lower comparator threshold'init_flag'(bool): Initialization status'synapse_id'(int): Assigned synapse identifier'next_readout_time'(float): Next scheduled readout (ms)'synapse_model'(str): Model name (‘stdp_facetshw_synapse_hom’)
- Return type:
Notes
This method provides a snapshot of the current state at the time of invocation. State variables like
a_causal,a_acausal, andweightmay change during subsequent spike processing.Examples
>>> import brainpy.state as bp >>> import saiunit as u >>> >>> syn = bp.stdp_facetshw_synapse_hom( ... weight=50.0, ... tau_plus=15.0 * u.ms, ... Wmax=100.0, ... ) >>> syn.init_state() >>> >>> # Get initial state >>> params = syn.get() >>> print(f"Weight: {params['weight']}") Weight: 50.0 >>> print(f"Tau plus: {params['tau_plus']} ms") Tau plus: 15.0 ms >>> print(f"Synapse model: {params['synapse_model']}") Synapse model: stdp_facetshw_synapse_hom
- init_state(batch_size=None, **kwargs)[source]#
Initialize or reset synapse state (event queue).
Clears the internal event queue and resets the delivery counter. Should be called before starting a new simulation or when resetting network state.
- Parameters:
batch_size (
int, optional) – Batch size for state initialization (ignored; static synapses are scalar).**kwargs – Additional initialization arguments (ignored).
Notes
The event queue is a per-synapse structure mapping future time steps to lists of pending events:
{step: [(receiver, value, receptor, type), ...]}.Delivery counter generates unique input keys for the receiver’s input dicts.
Unlike stateful synapses (e.g., with facilitation/depression), static synapses have no evolving state variables—only the transient event queue.
- record_post_spike(multiplicity=1.0, *, t_spike_ms=None)[source]#
Record postsynaptic spikes into internal history buffer.
Registers postsynaptic spike events for subsequent spike-timing calculations during presynaptic spike processing. Recorded spikes are stored in an internal buffer and queried when
send()is called to compute accumulator updates based on pre-post spike timing.- Parameters:
multiplicity (
floatorarray-like, default:1.0) – Number of postsynaptic spikes to record at the given timestamp. Must be non-negative. Formultiplicity=0, no spikes are recorded (no-op). Formultiplicity>1, multiple identical spike timestamps are added to the history buffer.t_spike_ms (
floatorarray-like, optional) – Explicit spike timestamp in milliseconds. If not provided, defaults to current simulation time plus delay:current_time_ms + delay. This allows recording spikes at arbitrary past or future times for offline analysis or replay scenarios.
- Returns:
count – Number of spike events actually recorded (equals
multiplicityif non-negative, otherwise 0).- Return type:
- Raises:
ValueError – If
multiplicityis negative.
Notes
The history buffer is unbounded and grows with every recorded spike. For long simulations with high-frequency postsynaptic activity, consider periodically clearing old spikes that fall outside the relevant temporal window (not implemented in this version).
Spike timestamps are stored as floating-point values with millisecond precision. Spikes are matched to presynaptic events using a small tolerance (
_STDP_EPS = 1e-6 ms) to account for floating-point rounding errors.This method is typically called automatically by the postsynaptic neuron’s spike generation mechanism, but can also be invoked manually for testing or replay purposes.
Examples
>>> import brainpy.state as bp >>> import saiunit as u >>> >>> syn = bp.stdp_facetshw_synapse_hom(weight=10.0, delay=1.0 * u.ms) >>> syn.init_state() >>> >>> # Record single post-spike at t=5.0 ms >>> count = syn.record_post_spike(1.0, t_spike_ms=5.0) >>> print(f"Recorded {count} spike(s)") Recorded 1 spike(s) >>> >>> # Record burst of 3 post-spikes at t=10.0 ms >>> count = syn.record_post_spike(3.0, t_spike_ms=10.0) >>> print(f"Recorded {count} spike(s)") Recorded 3 spike(s) >>> >>> # No-op: record zero spikes >>> count = syn.record_post_spike(0.0, t_spike_ms=15.0) >>> print(f"Recorded {count} spike(s)") Recorded 0 spike(s)
- send(multiplicity=1.0, *, post=None, receptor_type=None)[source]#
Process presynaptic spike and schedule outgoing synaptic event.
Implements the full NEST
stdp_facetshw_synapse_hom::sendprotocol: controller initialization, readout-cycle-based weight update via look-up tables, nearest-neighbor spike pairing with postsynaptic history, and event scheduling with current weight. This is the core plasticity update method triggered by each presynaptic spike.Processing Steps (matching NEST order):
Controller initialization (first spike only): - Assign unique
synapse_idfrom global counterno_synapses- Incrementno_synapsesand recomputereadout_cycle_duration- Calculate initialnext_readout_timebased on synapse ID and driver readout schedule - Setinit_flag = TrueReadout-based weight update (if current time exceeds
next_readout_time): - Convert continuousweightto 4-bit discrete representation via rounding - Evaluate two comparator functionsE_0andE_1from accumulators and thresholds - Select LUT based on evaluation bits:(1, 0)– applylookuptable_0(typically potentiation)(0, 1)– applylookuptable_1(typically depression)(1, 1)– applylookuptable_2(typically no change or combined rule)(0, 0)– no weight update
Reset accumulators to zero according to selected LUT’s reset bits in
reset_patternAdvance
next_readout_timebyreadout_cycle_durationuntil it exceeds current timeConvert updated discrete weight back to continuous value
Spike pairing (if postsynaptic history exists): - Query postsynaptic spikes in interval \((t_{\text{last}} - d, t_{\text{pre}} - d]\) where \(d\) is dendritic delay. - Update
a_causalusing first post-spike in interval (pre-before-post timing). - Updatea_acausalusing last post-spike in interval (post-before-pre timing).Event scheduling: - Schedule spike event with weighted payload
multiplicity * weight- Deliver to targetpostat receptor portreceptor_type- Delivery time: current time + delayState update: - Record current spike timestamp in
t_lastspikefor next iteration
- Parameters:
multiplicity (
floatorarray-like, default:1.0) – Presynaptic spike count or weight. Typical value is 1.0 for single spike. Formultiplicity=0, no event is generated (early return). The weighted payload sent to the postsynaptic target ismultiplicity * weight.post (
Dynamics, optional) – Target postsynaptic object. If not provided, uses the defaultself.postset at initialization or viaset(). Must implementadd_current_input()or equivalent input reception method.receptor_type (
int, optional) – Target receptor port on postsynaptic neuron. If not provided, usesself.receptor_type. Allows routing to different synaptic channels (e.g., 0 for AMPA, 1 for GABA).
- Returns:
sent –
Trueif an event was scheduled (multiplicity != 0),Falseotherwise.- Return type:
- Raises:
If
readout_cycle_durationis zero or negative when attempting to advancenext_readout_time(indicates invalid controller configuration) - If discrete weight index falls outside LUT bounds [0, 15] during LUT lookup (indicates weight or Wmax misconfiguration)
Notes
Timing Semantics:
All spike timestamps are on-grid (fixed time steps). Sub-step precise timing is not used.
Dendritic delay \(d\) affects both event delivery and the temporal window for querying postsynaptic history. The history query interval is delay-adjusted: spikes are matched at times \((t_{\text{last}} - d, t_{\text{pre}} - d]\) rather than \((t_{\text{last}}, t_{\text{pre}}]\), accounting for the fact that postsynaptic spikes reach the synapse earlier than they occur at the soma.
Accumulator Updates:
a_causalgrows when post-spikes occur before the last pre-spike (pre-before-post pairing → potentiation signal)a_acausalgrows when post-spikes occur after the current pre-spike (post-before-pre pairing → depression signal)Only the first post-spike updates
a_causal(oldest timing)Only the last post-spike updates
a_acausal(most recent timing)This reduced pairing strategy minimizes computational cost while preserving essential timing information
Readout Cycle Behavior:
Weight updates are not immediate but deferred to periodic readout cycles
Multiple presynaptic spikes can occur between readouts; only the last spike triggers the update check
If simulation time jumps significantly (e.g., after long pauses),
next_readout_timeadvances in multiple steps to catch upThis models hardware constraints where a controller sequentially updates synapse arrays
Examples
>>> import brainpy.state as bp >>> import saiunit as u >>> >>> # Create synapse and postsynaptic target >>> syn = bp.stdp_facetshw_synapse_hom( ... weight=10.0, ... delay=1.0 * u.ms, ... tau_plus=20.0 * u.ms, ... Wmax=100.0, ... ) >>> syn.init_state() >>> >>> # Record post-spike before pre-spike (depression scenario) >>> syn.record_post_spike(1.0, t_spike_ms=8.0) >>> >>> # Process pre-spike at t=10 ms >>> sent = syn.send(1.0) # multiplicity=1.0 >>> print(f"Event sent: {sent}") Event sent: True >>> >>> # Check accumulator update >>> params = syn.get() >>> print(f"Acausal accumulator: {params['a_acausal']:.4f}") Acausal accumulator: 0.9048 >>> >>> # No event for zero multiplicity >>> sent = syn.send(0.0) >>> print(f"Event sent: {sent}") Event sent: False
- set(*, weight=<object object>, delay=<object object>, receptor_type=<object object>, tau_plus=<object object>, tau_minus_stdp=<object object>, Wmax=<object object>, weight_per_lut_entry=<object object>, no_synapses=<object object>, synapses_per_driver=<object object>, driver_readout_time=<object object>, readout_cycle_duration=<object object>, lookuptable_0=<object object>, lookuptable_1=<object object>, lookuptable_2=<object object>, configbit_0=<object object>, configbit_1=<object object>, reset_pattern=<object object>, a_causal=<object object>, a_acausal=<object object>, a_thresh_th=<object object>, a_thresh_tl=<object object>, init_flag=<object object>, synapse_id=<object object>, next_readout_time=<object object>, post=<object object>)[source]#
Update public parameters and mutable state variables.
Provides NEST-compatible interface for modifying synapse properties and state after instantiation. All parameters are optional; only provided arguments are updated. Performs validation and automatically recomputes dependent values (e.g.,
readout_cycle_durationwhen driver parameters change,weight_per_lut_entrywhenWmaxchanges).- Parameters:
weight (
floatorarray-like, optional) – New continuous synaptic weight (dimensionless).delay (
Quantity[time]orarray-like, optional) – New transmission delay. Must include units (e.g.,1.5 * u.ms).receptor_type (
int, optional) – New receptor port identifier.tau_plus (
Quantity[time]orarray-like, optional) – New causal time constant. Must be positive.tau_minus_stdp (
Quantity[time]orarray-like, optional) – New acausal time constant. Must be positive.Wmax (
floatorarray-like, optional) – New maximum weight. Automatically recomputesweight_per_lut_entryunlessweight_per_lut_entryis also explicitly provided.weight_per_lut_entry (
floatorarray-like, optional) – New weight quantization step. Overrides automatic computation fromWmax.no_synapses (
intorarray-like, optional) – New global synapse count. Automatically recomputesreadout_cycle_duration.synapses_per_driver (
intorarray-like, optional) – New synapses-per-driver count. Must be positive. Automatically recomputesreadout_cycle_duration.driver_readout_time (
floatorarray-like, optional) – New driver processing time (ms). Must be positive. Automatically recomputesreadout_cycle_duration.readout_cycle_duration (
floatorarray-like, optional) – New cycle duration (ms). If provided, overrides automatic computation.lookuptable_0 (
array-likeof16 ints, optional) – New LUT for (1,0) evaluation. All entries must be in [0, 15].lookuptable_1 (
array-likeof16 ints, optional) – New LUT for (0,1) evaluation. All entries must be in [0, 15].lookuptable_2 (
array-likeof16 ints, optional) – New LUT for (1,1) evaluation. All entries must be in [0, 15].configbit_0 (
array-likeof4 ints, optional) – New comparator configuration for E_0.configbit_1 (
array-likeof4 ints, optional) – New comparator configuration for E_1.reset_pattern (
array-likeof6 ints, optional) – New accumulator reset pattern (6 binary flags).a_causal (
floatorarray-like, optional) – New causal accumulator value.a_acausal (
floatorarray-like, optional) – New acausal accumulator value.a_thresh_th (
floatorarray-like, optional) – New upper comparator threshold.a_thresh_tl (
floatorarray-like, optional) – New lower comparator threshold.init_flag (
boolorarray-like, optional) – New initialization status (True = initialized, False = uninitialized).synapse_id (
intorarray-like, optional) – New synapse identifier.next_readout_time (
floatorarray-like, optional) – New next scheduled readout time (ms).post (
Dynamics, optional) – New default postsynaptic target object.
- Raises:
If
tau_plusortau_minus_stdpis non-positive - Ifsynapses_per_driveris non-positive - Ifdriver_readout_timeis non-positive - If LUT entries are outside [0, 15] range - If LUT sizes are mismatched (must all be 16 entries) - Ifconfigbit_0orconfigbit_1do not have exactly 4 entries - Ifreset_patterndoes not have exactly 6 entries - If any scalar parameter is non-finite (NaN or ±inf)
Notes
Automatic Recomputation Logic:
When
Wmaxis updated (andweight_per_lut_entryis not):weight_per_lut_entry = Wmax / 15When any of
no_synapses,synapses_per_driver, ordriver_readout_timeis updated (andreadout_cycle_durationis not):readout_cycle_duration = ceil(no_synapses / synapses_per_driver) * driver_readout_timeExplicit
weight_per_lut_entryorreadout_cycle_durationarguments override automatic computation.
State Persistence:
All updated values are also saved to internal
_*0attributes (e.g.,_a_causal0,_tau_plus0) to ensure consistent state restoration wheninit_state()is called.Examples
>>> import brainpy.state as bp >>> import saiunit as u >>> >>> syn = bp.stdp_facetshw_synapse_hom( ... weight=10.0, ... tau_plus=20.0 * u.ms, ... Wmax=100.0, ... ) >>> syn.init_state() >>> >>> # Update time constants >>> syn.set(tau_plus=15.0 * u.ms, tau_minus_stdp=25.0 * u.ms) >>> >>> # Update weight (triggers discrete conversion on next readout) >>> syn.set(weight=50.0) >>> >>> # Reset accumulators to zero >>> syn.set(a_causal=0.0, a_acausal=0.0) >>> >>> # Change maximum weight (automatically updates weight_per_lut_entry) >>> syn.set(Wmax=200.0) >>> print(syn.weight_per_lut_entry) # Now 200.0 / 15 = 13.333... 13.333333333333334
- update(pre_spike=0.0, *, post_spike=0.0, post=None, receptor_type=None)[source]#
Advance synapse state by one time step.
Orchestrates the full update cycle: delivers due events from the internal queue, records postsynaptic spikes into history, aggregates presynaptic input from all sources, and triggers plasticity updates via
send(). This method should be called once per simulation time step to maintain consistent event timing.Update Sequence:
Event delivery: Pop and deliver all events scheduled for the current time step from the internal event queue. Delivered events invoke
add_current_input()or equivalent methods on target postsynaptic objects.Post-spike recording: If
post_spike > 0, record the specified number of postsynaptic spikes at the current time (plus delay) into the internal history buffer. These spikes will be queried by subsequent presynaptic spikes for plasticity calculations.Pre-spike aggregation: Sum presynaptic input from: - Explicit
pre_spikeargument - Current inputs registered viaadd_current_input()(from projections/other sources) - Delta inputs registered viaadd_delta_input()(spike-like events)Plasticity processing: If aggregated pre-spike count is non-zero, invoke
send()to perform readout-based weight update, spike pairing, and event scheduling.
- Parameters:
pre_spike (
floatorarray-like, default:0.0) – Explicit presynaptic spike count for this time step. Typically 0.0 or 1.0. Can be a fractional value for rate-based approximations. This value is added to any inputs accumulated viaadd_current_input()/add_delta_input().post_spike (
floatorarray-like, default:0.0) – Postsynaptic spike count for this time step. Must be non-negative. Used for recording spike history for plasticity calculations. Common values: 0.0 (no spike) or 1.0 (spike).post (
Dynamics, optional) – Target postsynaptic object for event delivery. If not provided, usesself.post.receptor_type (
int, optional) – Target receptor port. If not provided, usesself.receptor_type.
- Returns:
delivered – Number of events delivered to postsynaptic targets during this time step. Equals the number of events that were scheduled for delivery at the current time. Can be 0 if no events were due, or ≥1 if delayed events arrived.
- Return type:
Notes
Integration with BrainPy Projections:
When used within a
Projectionor similar container, presynaptic spikes from multiple sources are typically accumulated viaadd_current_input()calls. Thepre_spikeargument provides an additional explicit input, useful for:Manual spike injection in testing/debugging
Direct neuron-to-synapse connections without projections
Feedforward external stimulation
The total presynaptic drive is:
total_pre = pre_spike + sum(current_inputs) + sum(delta_inputs)Timing Precision:
All spike times are on-grid (rounded to simulation time steps)
post_spiketimestamp iscurrent_time + delay(same convention assend())Event delivery respects the internal event queue’s scheduling, accounting for synaptic delays set via
delayparameter
State Consistency:
This method does not clear the postsynaptic history buffer. For long simulations, history grows unbounded unless manually cleared (not implemented in current version). Future enhancements may add automatic history pruning for spikes older than
tau_plus + tau_minus.Performance Considerations:
Event delivery is O(n_events_due) per call
Post-spike recording is O(post_spike) per call
Pre-spike processing triggers full
send()logic, including LUT lookups and history queries, which is O(1) per call (reduced pairing strategy)
Examples
>>> import brainpy.state as bp >>> import saiunit as u >>> import brainstate as bst >>> >>> # Setup simulation context >>> with bst.environ.context(dt=0.1 * u.ms): ... syn = bp.stdp_facetshw_synapse_hom( ... weight=10.0, ... delay=1.0 * u.ms, ... tau_plus=20.0 * u.ms, ... ) ... syn.init_state() ... ... # Time step 0: no activity ... delivered = syn.update(pre_spike=0.0, post_spike=0.0) ... print(f"Step 0: delivered {delivered} events") ... ... # Time step 1: post-spike only ... delivered = syn.update(pre_spike=0.0, post_spike=1.0) ... print(f"Step 1: delivered {delivered} events") ... ... # Time step 2: pre-spike (triggers plasticity) ... delivered = syn.update(pre_spike=1.0, post_spike=0.0) ... print(f"Step 2: delivered {delivered} events") ... ... # Check accumulator update from spike pairing ... params = syn.get() ... print(f"Causal accumulator: {params['a_causal']:.4f}") Step 0: delivered 0 events Step 1: delivered 0 events Step 2: delivered 0 events Causal accumulator: 0.9048