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.
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 getvariableSpike 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)
names2-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!.
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 efficacypresynaptic(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 6Plasticity 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.429173For 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.
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.
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.
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.
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.
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.
SNNModels.clear_records! — Method
clear_records!(objs::AbstractArray)Call clear_records! on each element of objs.
SNNModels.clear_records! — Method
clear_records!(obj, sym::Symbol)Delete the recorded data of the key sym of obj only, keeping it monitored.
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.
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.
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.
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).
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 counterT.tt[1], cast to Float32.
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.
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.
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.
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, τ = τ).
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.
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: anAbstractPopulation,AbstractConnectionorAbstractStimulus. For a vector or a NamedTuple (e.g.model.pop) the call is repeated on each element, and the default sampling rate is then200Hzinstead of1000Hz.keys: field names ofobj(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 ismax(1, round(1 / (sr * dt)))steps, sosrlarger than1/dtrecords every step. Ignored for:fire.variables = :none: name of a nested field holding the variables, e.g.:STPVarsor:LTPVarsof aSpikingSynapse, or:synvars. The keysymis then read fromobj.<variables>.<sym>and stored asSymbol(variables, "_", sym), e.g.:STPVars_u. The positional formmonitor!(obj, keys, :STPVars)is equivalent. Variables of a plasticity rule are recorded at their value at the sampling time, read throughplasticity_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 aplasticity_valuemethod is anArgumentError.monitor_time: capacity hint in ms used to size the buffers (default: the duration of thesim!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:firebuffers (default20Hz). A plain number (any value above1kHz) is anArgumentError.verbose = false: warn about keys already monitored or not found.
Storage
:fireis stored as a list of(time, neuron)events; read it withspiketimes(obj),firing_rate, orrecord(obj, :fire; interval).Float32scalars,Vector{Float32}(also with indices), andMatrix{Float32}/Array{Float32,3}without indices are stored in a dense array with time as the last dimension; read withgetvariable(raw samples) orrecord(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 = 0when 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)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.
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 byfiring_rate(p, interval; interpolate, kwargs...);interval(anAbstractRange, ms) is required.sym == :spiketimesor:spikes: returnsspiketimes(p)(ignoresrange).- any other
sym: withinterpolate = true(default) an interpolation object over time, as returned byinterpolated_record, resampled onintervalif given; withinterpolate = falsethe raw samplesgetvariable(p, sym)(andr = interval). variables: name of the variable group used inmonitor!(e.g.:STPVars); the key read isSymbol(variables, "_", sym). Equivalent to passing:STPVars_udirectly.range = truereturns(v, r)instead ofv.
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)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.
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.
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.
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.
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.