Recordings

After this page you can record any variable of a population, connection or stimulus, and read it back as raw samples, interpolated traces, spike times, firing rates or weight matrices.

The blocks of this page run in sequence on the network below (STP on EE, inhibitory STDP on IE).

<!– run: sequential –>

using SpikingNeuralNetworks
using Statistics
SNN.@load_units

E = SNN.AdEx(; N = 800, param = SNN.AdExParameter(; El = -50mV))
I = SNN.IF(; N = 200, param = SNN.IFParameter())
EE = SNN.SpikingSynapse(E, E, :he; conn = (μ = 2, p = 0.02), STPParam = SNN.MarkramSTPParameter())
EI = SNN.SpikingSynapse(E, I, :ge; conn = (μ = 30, p = 0.02))
IE = SNN.SpikingSynapse(I, E, :hi; conn = (μ = 50, p = 0.02), LTPParam = SNN.iSTDPRate(r = 5Hz))
II = SNN.SpikingSynapse(I, I, :gi; conn = (μ = 10, p = 0.02))
input_E = SNN.Stimulus(SNN.PoissonFixed(rate = 2kHz), E, :ge)
input_I = SNN.Stimulus(SNN.PoissonFixed(rate = 2kHz), I, :ge)
model = SNN.compose(; E, I, EE, EI, IE, II, input_E, input_I, silent = true)

Monitoring variables

monitor!(obj, keys; sr) registers the fields keys of obj for recording. obj can be a single component or a collection such as model.pop. The sampling rate sr defaults to 1000Hz for a single component and to 200Hz for a collection; the sampling period is max(1, round(1 / (sr * dt))) steps. :fire records every spike, independently of sr.

SNN.monitor!(E, [:ge, :gi], sr = 200Hz)
SNN.monitor!(model.pop, :v, sr = 200Hz)
SNN.monitor!(model.pop, :fire)
SNN.sim!(; model, duration = 2s)

Buffers are allocated before the run from its duration (or monitor_time) and grow when needed, with a one-time warning; monitor_rate (default 20Hz, written with units) sizes the spike buffer.

A tuple (key, indices) restricts the recording of a vector field to some neurons, e.g. SNN.monitor!(E, [(:v, 1:10)]). For :fire only the spikes of the listed neurons are recorded, with their original indices.

Note

It is not possible to pause a recording while keeping the variable monitored. To start again from scratch, use clear_records!(obj) (deletes the data, keeps the set-up) or clear_monitor!(obj) (removes the set-up as well).

Reading variables

getvariable(obj, key) returns the raw samples, with time as the last dimension (neurons x samples for a vector field). When the model time is 0 at the start, the first sample is taken at t = 0.

record(obj, key) returns the recording as an interpolation object over continuous time: it is evaluated with call syntax, v(i, t), at any time t (in ms) within the recorded range (i and t can be ranges), and record(obj, key, range = true) also returns the time axis r.

Samples are taken at the global steps that are multiples of the sampling period, and r runs from the first to the last sample taken.

v = SNN.getvariable(E, :v)                    # 800 x 401 samples (2 s at 200 Hz, plus t = 0)
v = SNN.record(E, :v)                         # interpolated
v(1, 1.5s)
v(1:10, 1.2s:15ms:1.9s)
v, r = SNN.record(E, :v, range = true)
size(v), r
v = SNN.record(E, :v, interpolate = false)    # same as getvariable

Spike times and firing rates

spiketimes(pop) returns a Spiketimes object, a vector with the spike times (ms) of each neuron. The keyword interval restricts it to a time window.

st = SNN.spiketimes(E)
length(st), length(st[1])
st = SNN.spiketimes(E; interval = 0:1ms:1s)

bin_spiketimes(pop; interval) counts the spikes in the bins defined by the range interval and returns (counts, r), with counts a neurons x bins matrix.

interval = 0:10ms:2s
bins, r = SNN.bin_spiketimes(E; interval)
size(bins)
(800, 201)

firing_rate(pop; interval) returns (rates, r): the firing rate of each neuron (Hz) sampled on interval, obtained by convolving the spike trains with a kernel (alpha function by default). With interpolate = true (default) rates is an interpolation object; pop_average = true averages over neurons. On a collection of populations it returns (rates, r, names), one entry per population.

fr, r = SNN.firing_rate(E; interval)
fr, r = SNN.firing_rate(E; interval, interpolate = false)
fr, r, names = SNN.firing_rate(model.pop; interval)
names
2-element Vector{String}:
 "AdEx"
 "IF"

The same quantities are available through record:

fr = SNN.record(E, :fire; interval)
fr, r = SNN.record(E, :fire; interval, range = true)
st = SNN.record(E, :spikes)

Synaptic variables

Fields of connections are monitored in the same way. :W and :ρ have one value per synapse. Plasticity variables need the name of their structure, as the keyword variables or a third argument: :STPVars (:u, :x of MarkramSTPParameter) or :LTPVars (e.g. :tpost of iSTDPRate). They are stored under Symbol(variables, "_", key), e.g. :STPVars_u, and recorded at their value at the sampling time: some rules store traces and weights lazily, so read them outside a recording with plasticity_value or trace_values (see Plasticity).

SNN.monitor!(EE, [:ρ], sr = 10Hz)
SNN.monitor!(IE, [:W], sr = 10Hz)
SNN.monitor!(IE, [:tpost]; sr = 10Hz, variables = :LTPVars)
SNN.monitor!(EE, [:x, :u], :STPVars; sr = 10Hz)
SNN.train!(; model, duration = 2s)

The first run of this page used sim!, which never changes weights or STP variables; this one uses train!.

Warning

Recording per-synapse variables can use a lot of memory in large networks; use a low sampling rate.

Weight matrices

Connectivity is stored in sparse form; matrix(c) returns the current weights as an N_post x N_pre sparse matrix, and matrix(c, sym) any other per-synapse field.

W = SNN.matrix(EE)            # current weights
ρ = SNN.matrix(EE, :ρ)        # current STP efficacy

presynaptic(c, i) returns the presynaptic neurons of postsynaptic neuron i, and postsynaptic(c, j) the postsynaptic neurons of presynaptic neuron j; both also accept a vector of neurons.

neuron = 1
Is = SNN.postsynaptic(EE, neuron)
mean(W[Is, neuron])           # mean weight of the outgoing connections of neuron 1
Js = SNN.presynaptic(EE, neuron)
mean(W[neuron, Js])           # mean weight of the incoming connections of neuron 1
Js_many = SNN.presynaptic(EE, 1:10)

A recorded per-synapse variable is a synapses x samples array; record interpolates it in time, and matrix_record(c, sym, t) rebuilds the sparse matrix at time t (a vector of times gives a 3-dimensional array). The times are those of the train! call above, from 2 s to 4 s.

W_IE, r = SNN.record(IE, :W, range = true)
W_IE(axes(W_IE, 1), 3.5s)                      # weights of all IE synapses at t = 3.5 s
W_mat = SNN.matrix_record(IE, :W, 3.5s)         # 800 x 200 sparse matrix
W_mat2 = SNN.matrix(IE, W_IE, 3.5s)             # same, from the interpolated record
all(W_mat .== W_mat2)
W_T = SNN.matrix_record(IE, :W, 3s:100ms:3.5s)  # 800 x 200 x 6

Plasticity variables

Plasticity variables are read with their compound key:

x = SNN.record(EE, :STPVars_x)
x(1, 3.14s)
x, r = SNN.record(EE, :STPVars_x, range = true)
x_raw = SNN.getvariable(EE, :STPVars_x)
tpost = SNN.record(IE, :LTPVars_tpost)
tpost(1:10, 2.4s:15ms:3.1s)
10×47 Matrix{Float64}:
 0.094882   0.208778   0.322673  …  0.00504026  0.00368064  0.00232101
 0.0102218  0.0752736  0.140325     0.0946511   0.115885    0.137118
 0.0508576  0.181333   0.311808     0.0314169   0.0229421   0.0144673
 0.373419   0.324968   0.276516     0.198887    0.246085    0.293283
 0.128957   0.216923   0.304889     0.108755    0.131368    0.153982
 0.500588   0.435636   0.370685  …  0.308373    0.38108     0.453787
 0.0        0.0        0.0          0.00683649  0.00499233  0.00314817
 0.0        0.0        0.0          0.460274    0.564752    0.66923
 0.285869   0.248778   0.211686     8.32246e-7  6.07746e-7  3.83245e-7
 0.0278206  0.16653    0.305239     0.450276    0.439724    0.429173
Tip

For plotting, see the functions of SNNPlots on the Visualization page, or use any plotting package directly on the interpolated records: <!– norun –>

using Plots
v, r = SNN.record(E, :v, range = true)
plot(r, v(1, r), label = "membrane potential of neuron 1")

Recording API

SNNModels._allocate_records! — Method
_allocate_records!(P, C, S, dt::Float32, duration::Float32)

Pre-allocate dense recording buffers and the flat :fire COO event list for every monitored component. Idempotent across chunked sim! calls: a key already allocated at the same dt is skipped; on dt mismatch the buffer is reallocated.

source
SNNModels._ensure_fire_capacity! — Method
_ensure_fire_capacity!(rec, meta, need)

Make room for need more spikes in the :fire COO buffers (called once per step, before the writes). If the countdown meta[:allocated][:fire] is short, the buffers grow to max(2 * capacity, written + need, 1) slots, keeping the written prefix, with one warning per recording.

source
SNNModels._fire_mask! — Function
_fire_mask!(meta, ind, N, owner = nothing)

Membership mask (length N) of the recorded subset ind, and whether ind is strictly increasing. Cached in meta until ind is another vector or N changes; rebuilt at every sim!/train! call (in-place edits of ind within a manual loop are not seen). Throws ArgumentError if an index is outside 1:N.

source
SNNModels.add_endtime! — Method
add_endtime!(model::NamedTuple)

For every monitored key of every component of model that has no end time, set records[:end_time][key] to the current model time. Used for records loaded from older files.

source
SNNModels.add_starttime! — Method
add_starttime!(model::NamedTuple)

For every monitored key of every component of model that has no start time, set records[:start_time][key] = 0. Used for records loaded from older files.

source
SNNModels.clear_monitor! — Method
clear_monitor!(obj)

Remove all monitoring from obj by deleting every entry in obj.records. Unlike clear_records!, which empties data but preserves the monitoring schema, this function removes the schema too — monitor! must be called again before the next sim.

source
SNNModels.clear_records! — Method
clear_records!(obj)

Delete the recorded data of obj (a component, or a model/NamedTuple/collection of components, recursing into groups) but keep the monitoring set-up: the next sim! records the same variables again, with new start and end times.

source
SNNModels.get_dt — Method
get_dt(T::Time)

Get the time step size.

Arguments

  • T::Time: The Time object.

Returns

  • Float32: The time step size.
source
SNNModels.get_interval — Method
get_interval(T::Time)

Time points dt:dt:get_time(T) (ms) of the steps simulated so far, in the precision of the clock.

source
SNNModels.get_measure_interval — Method
get_measure_interval(p, sym::Symbol, steps::Int)
get_measure_interval(p, sym::Symbol, step::AbstractFloat)

Time range from p.records[:start_time][sym] to p.records[:end_time][sym], with steps points or with step step (ms).

source
SNNModels.get_step — Method
get_step(T::Time)

Get the current simulation step counter (integer number of dt steps elapsed).

Arguments

  • T::Time: The Time object.

Returns

  • Float32: The step counter T.tt[1], cast to Float32.
source
SNNModels.get_time — Method
get_time(T::Time)
get_time(model::NamedTuple)

Current simulation time in ms (T.t[1], or model.time.t[1] for a model built with compose), in the precision of the clock.

source
SNNModels.getrecord — Method
getrecord(p, sym)

Return the raw storage p.records[sym] (for dense records the full pre-allocated buffer, including unwritten slots; prefer getvariable). Throws ArgumentError if sym is not recorded.

source
SNNModels.getvariable — Function
getvariable(obj, key::Symbol, id = nothing)

Raw recorded samples of key, without interpolation, with time as the last dimension (neurons x samples for a vector field). For dense records this is a view on the buffer restricted to the samples written so far; id selects an index (or indices) along the first dimension. Legacy records are concatenated into a new array.

source
SNNModels.interpolated_record — Function
interpolated_record(p, sym, τ = 20ms)

Return (y, r): the recording sym of p as an Interpolations object y with time as the last axis, and the range r of time points (ms) assigned to the samples. It is evaluated with call syntax, y(i, t) (indices or ranges), at any t within r (linear interpolation; singleton dimensions are not interpolated). Indexing with square brackets and a non-integer time does not work.

Samples are taken at the steps whose global step counter is a multiple of the sampling period (including t = 0 for a fresh model). r spans from the time of the first sample to the time of the last one (records[:start_time][sym] to records[:end_time][sym]) with as many points as samples, so it is the exact sample-time axis, also when monitoring starts after t = 0 or the simulated time is not a multiple of the sampling period.

For sym == :fire it returns firing_rate(p, τ = τ).

Changed after SNNModels 1.8.4

Up to 1.8.4 the start and end times were those of the first and last record! call, not of the first and last sample, so the axis was shifted or stretched by up to one sampling period (e.g. :W sampled at 10 Hz from 2 s to 4 s: true times 2100:100:4000 ms, axis 2000.125:105.26:4000); the τ argument was ignored for :fire.

source
SNNModels.monitor! — Method
monitor!(obj, keys; sr = 1000Hz, variables = :none, monitor_time = nothing, monitor_rate = nothing, verbose = false)
monitor!(objs::Union{Array,NamedTuple}, keys; sr = 200Hz, kwargs...)
monitor!(obj, keys, variables::Symbol; kwargs...)

Register variables of a population, connection or stimulus for recording during the next sim!/train! calls. keys is a Symbol, a (Symbol, indices) tuple, or a vector of them. Buffers are allocated just before the time loop starts (_allocate_records!), not here.

Arguments

  • obj: an AbstractPopulation, AbstractConnection or AbstractStimulus. For a vector or a NamedTuple (e.g. model.pop) the call is repeated on each element, and the default sampling rate is then 200Hz instead of 1000Hz.
  • keys: field names of obj (e.g. :v, :w, :W, :ρ, :fire). A tuple (sym, indices) records only the listed elements of a vector field.
  • sr = 1000Hz: sampling rate. The sampling period is max(1, round(1 / (sr * dt))) steps, so sr larger than 1/dt records every step. Ignored for :fire.
  • variables = :none: name of a nested field holding the variables, e.g. :STPVars or :LTPVars of a SpikingSynapse, or :synvars. The key sym is then read from obj.<variables>.<sym> and stored as Symbol(variables, "_", sym), e.g. :STPVars_u. The positional form monitor!(obj, keys, :STPVars) is equivalent. Variables of a plasticity rule are recorded at their value at the sampling time, read through plasticity_value (lazily decayed traces are not stored at that value; reading the field directly returns the stored value). Recording never changes the state. A lazily stored variable without a plasticity_value method is an ArgumentError.
  • monitor_time: capacity hint in ms used to size the buffers (default: the duration of the sim! call, at least 1 s). Recording beyond the capacity is still correct: the buffer is doubled with a one-time warning.
  • monitor_rate: expected maximum firing rate, with units (e.g. 50Hz), used to size the :fire buffers (default 20Hz). A plain number (any value above 1kHz) is an ArgumentError.
  • verbose = false: warn about keys already monitored or not found.

Storage

  • :fire is stored as a list of (time, neuron) events; read it with spiketimes(obj), firing_rate, or record(obj, :fire; interval).
  • Float32 scalars, Vector{Float32} (also with indices), and Matrix{Float32} / Array{Float32,3} without indices are stored in a dense array with time as the last dimension; read with getvariable (raw samples) or record (interpolated in time).
  • Other field types (e.g. matrices with indices, vectors of vectors) are stored as a vector of snapshots.
  • A first sample is taken at t = 0 when the model time is 0.

Already-monitored keys and missing fields are skipped silently (with a warning if verbose = true).

With (:fire, indices) only the spikes of the listed neurons are recorded (with their original neuron indices, so spiketimes still returns one entry per neuron of the population). An index outside 1:N throws an ArgumentError at the first step of the next sim!/train!.

Example

using SpikingNeuralNetworks
SNN.@load_units
E = SNN.IF(N = 100)
stim = SNN.Stimulus(SNN.PoissonFixed(rate = 2kHz), E, :ge)
SNN.monitor!(E, [:v, :fire]; sr = 2kHz)
SNN.monitor!(E, [(:w, [1, 2, 3])])
model = SNN.compose(; E, stim)
SNN.sim!(; model, duration = 200ms)
v = SNN.getvariable(E, :v)          # 100 x 401 samples
st = SNN.spiketimes(E)
source
SNNModels.record! — Method
record!(obj, T::Time)

Record all monitored variables of obj at the current time T. Dispatches to the dense or legacy path per meta[:mode][key]. Called every dt inside the sim loop.

source
SNNModels.record — Method
record(p, sym::Symbol; range = false, interval = nothing, interpolate = true, variables = nothing, kwargs...)
record(p, sym::Symbol, interval::AbstractRange; kwargs...)
record(pops, sym::Symbol; interval, interpolate = true, kwargs...)

Read a recording of a population, connection or stimulus p.

  • sym == :fire: firing rates computed by firing_rate(p, interval; interpolate, kwargs...); interval (an AbstractRange, ms) is required.
  • sym == :spiketimes or :spikes: returns spiketimes(p) (ignores range).
  • any other sym: with interpolate = true (default) an interpolation object over time, as returned by interpolated_record, resampled on interval if given; with interpolate = false the raw samples getvariable(p, sym) (and r = interval).
  • variables: name of the variable group used in monitor! (e.g. :STPVars); the key read is Symbol(variables, "_", sym). Equivalent to passing :STPVars_u directly.
  • range = true returns (v, r) instead of v.

The method on a collection pops stacks the interpolated recordings of all elements sampled on interval (required); elements that do not record sym contribute no rows.

Example

using SpikingNeuralNetworks
SNN.@load_units
E = SNN.IF(N = 50)
stim = SNN.Stimulus(SNN.PoissonFixed(rate = 2kHz), E, :ge)
SNN.monitor!(E, [:v, :fire])
model = SNN.compose(; E, stim)
SNN.sim!(; model, duration = 500ms)
v = SNN.record(E, :v)                   # interpolated: v(1, 100.5ms)
v, r = SNN.record(E, :v, range = true)  # r: time axis in ms
v_raw = SNN.record(E, :v, interpolate = false)
fr, r = SNN.record(E, :fire; interval = 0:10ms:500ms, range = true)
st = SNN.record(E, :spikes)
source
SNNModels.record_fire! — Method
record_fire!(fire, fire_rec, T, indices)

Legacy fire recorder. Appends the current timestep's spike set to the jagged :time / :neurons vectors in fire_rec. Used when meta[:mode][:fire] == :legacy. The dense path uses record_fire_dense! instead, which writes into pre-allocated COO buffers without allocating.

source
SNNModels.record_fire_dense! — Method
record_fire_dense!(obj, fire, records, meta, T, indices)

Append the spikes of this step of obj (time Float32(get_time(T)), neuron id) to the COO buffers records[:fire][:times_buf], [:neurons_buf], in ascending neuron order (from _fire_step_list). With indices[:fire] only the listed neurons are recorded; an unsorted or repeated index list is visited in its own order.

source
SNNModels.record_sym! — Method
record_sym!(my_record, obj, key, T, indices, sr)

Legacy variable recorder. Appends a snapshot of my_record to obj.records[key] at the rate set by sr. Gated by record_step. Used when meta[:mode][key] == :legacy. The dense path uses record_sym_dense! instead.

source
SNNModels.reset_time! — Method
reset_time!(T::Time)
reset_time!(model::NamedTuple)

Set the time to 0 and the step counter to 0 (for a model, its model.time). Recordings are not cleared; use clear_records! for that.

source
SNNModels.update_time! — Method
update_time!(T::Time, dt)
update_time!(T::Time, myT::Time)

Advance the clock by one step: T.t[1] += dt and the step counter T.tt[1] += 1. Called at the beginning of every sim!/train! step, before the stimuli. The second method copies time and step counter from myT into T.

source