Model extensions
After this page you can add a population, connection, stimulus or plasticity rule of your own. sim! and train! dispatch on the type of each component and of its param field (see the step in Running simulations), so a new model is a new type plus the methods the step calls; often a new parameter type and one method are enough.
Interface of each component
Populations (<: AbstractPopulation, parameter <: AbstractPopulationParameter)
- Required fields:
N,param,id,name,records::Dict(checked byvalidate_population_model;monitor!andrecord!store the recordings inrecords). A fieldfire::Vector{Bool}is needed to record spikes (monitor!(p, [:fire])) and to be the presynaptic population of aSpikingSynapse. Any other field (v,I, ...) can be recorded withmonitor!. integrate!(p, param, dt::AbstractFloat): advance the state by one step (dtarrives in the precision of the population,float_type(p)).synaptic_target(targets::Dict, post, sym::Symbol, target): return(g, v_post), the vector into which connections and stimuli add their input and the membrane potential used by voltage-dependent plasticity. Needed only if the population receivesSpikingSynapses or spiking stimuli; there is no generic fallback. Stimuli that write a current (CurrentStimulus) read the fieldsymdirectly.- Optional:
Population(param::MyParameter; kwargs...)so thatSNN.Population(; param, ...)builds the new type, andplasticity!/update_traces!methods; the generic fallbacks forAbstractPopulationParameterdo nothing.
Connections (<: AbstractConnection, parameter <: AbstractConnectionParameter)
- Required fields:
param,id,name,records::Dict(checked byvalidate_synapse_model). forward!(c, param, dt, T)or the two-argument formforward!(c, param): the generic four-argument method calls the two-argument one. This generic method, and the no-opupdate_traces!fallback, exist only for parameters that are subtypes ofAbstractConnectionParameter; with any other parameter typesim!andtrain!fail with aMethodError.plasticity!(c, param, dt, T::Time): called bytrain!for every connection; there is no generic fallback, so define it (it can returnnothing) if the model is ever run withtrain!.update_traces!has a no-op fallback.
Stimuli (<: AbstractStimulus, parameter <: AbstractStimulusParameter)
- Required fields:
param,id,name,records::Dict(checked byvalidate_stimulus_model). stimulate!(s, param, T::Time, dt).- Optional: a
Stimulus(param::MyParameter, post, sym; kwargs...)method.
Plasticity rules for SpikingSynapse (<: LTPParameter or <: STPParameter)
plasticityvariables(param, Npre, Npost): return the state of the rule, a subtype ofPlasticityVariableswith anactive::Vector{Bool}field (the rule runs only ifany(active)).plasticity!(c, param, vars, dt, T)(applied afterforward!) and, if the rule needs it,update_traces!(c, param, vars, dt, T)(applied beforeforward!). Long-term rules modifyc.W, short-term rules the efficacyc.ρ.
Connections read the spikes of a population from its fire vector once per step. A population that already knows which neurons fired can return its own list from spike_index (see the API below).
The existing types in SNNModels are the best templates: see the Model catalogue.
Adding a population model: current-based IF
The new types are defined inside SNNModels with @eval, which is equivalent to adding a file to SNNModels.jl/src/populations and gives access to the internal macros (@snn_kw) and types.
A docstring cannot be attached directly to an @snn_kw struct; write the docstring above the bare name of the type and define the struct after it, as below. @snn_kw generates a keyword constructor whose defaults may refer to earlier fields. Field types must be plain names: a parametric type written in place, such as Vector{Float32}, is not accepted by the macro (it fails with Cannot convert an object of type Expr to an object of type Symbol). Declare vector types as type parameters with a default (Neuron{VFT = Vector{Float32}}), as the library models do, or use the aliases VBT = Vector{Bool} and VIT = Vector{Int} defined in SNNModels.
using SpikingNeuralNetworks
using Distributions
SNN.@load_units
@eval SNN.SNNModels begin
"""
NeuronParameter
Parameters of a current-based leaky integrate-and-fire neuron.
"""
NeuronParameter
@snn_kw struct NeuronParameter <: AbstractPopulationParameter
R::Float32 = 1GΩ
El::Float32 = -70.6mV
Vt::Float32 = -50.4mV
Vr::Float32 = -70.6mV
τm::Float32 = 20ms
τe::Float32 = 10ms
τi::Float32 = 10ms
end
"""
Neuron
Population of current-based leaky integrate-and-fire neurons. `N`, `param`, `name`, `id`
and `records` are compulsory; `fire` is needed to record spikes and to project spikes.
"""
Neuron
@snn_kw struct Neuron{VFT = Vector{Float32}} <: AbstractPopulation
param::NeuronParameter = NeuronParameter()
N::Int = 10
name::String = "Neuron"
id::String = randstring(12)
v::VFT = fill(param.El, N)
ge::VFT = zeros(Float32, N)
gi::VFT = zeros(Float32, N)
fire::VBT = zeros(Bool, N)
I::VFT = zeros(Float32, N)
records::Dict = Dict()
end
# one forward Euler step; recordings are handled by the simulation loop
function integrate!(p::Neuron, param::NeuronParameter, dt::AbstractFloat)
@unpack N, v, ge, gi, fire, I = p
@unpack R, El, Vt, Vr, τm, τe, τi = param
@inbounds for i in 1:N
v[i] += dt / τm * (El - v[i] + R * (ge[i] - gi[i] + I[i]))
ge[i] -= dt * ge[i] / τe
gi[i] -= dt * gi[i] / τi
fire[i] = v[i] >= Vt
fire[i] && (v[i] = Vr)
end
end
# where SpikingSynapse and spiking stimuli deliver their input
function synaptic_target(targets::Dict, post::Neuron, sym::Symbol, target = nothing)
push!(targets, :sym => sym)
return getfield(post, sym), post.v
end
end
neuron = SNN.SNNModels.Neuron(N = 1)
current_param = SNN.CurrentNoise(neuron.N; I_base = 20pA, I_dist = Normal(0pA, 20pA))
current_stim = SNN.Stimulus(current_param, neuron, :I)
input = SNN.Stimulus(SNN.PoissonLayer(rate = 10Hz, N = 100), neuron, :ge; conn = (p = 1, μ = 1pA))
SNN.monitor!(neuron, [:v, :fire, :ge, :I], sr = 2kHz)
model = SNN.compose(; neuron, current_stim, input, silent = true)
SNN.sim!(; model, duration = 1000ms)
SNN.spiketimes(neuron)Adding a stimulus model: Poisson input with refractory period
Here a new parameter type, subtype of PoissonStimulusParameter, is enough: the existing PoissonStimulus container and its Stimulus constructor are reused, and only a stimulate! method for the new parameter is added. The stimulus adds μ to the target of each neuron when the neuron's Poisson source emits a spike, with an absolute refractory period ΔT between spikes.
using SpikingNeuralNetworks
SNN.@load_units
@eval SNN.SNNModels begin
@snn_kw struct PoissonRefractory{VFT = Vector{Float32}} <: PoissonStimulusParameter
rate::Float32 = 10Hz
ΔT::Float32 = 2ms # absolute refractory period
μ::Float32 = 1.0f0 # increment per spike
N::Int = 100 # size of the target population
last_spike::VFT = fill(-Inf32, N)
active::VBT = [true]
end
function stimulate!(p::PoissonStimulus, param::PoissonRefractory, time::Time, dt::AbstractFloat)
param.active[1] || return
@unpack rate, ΔT, μ, last_spike = param
t = get_time(time)
@inbounds for n in p.neurons
if t - last_spike[n] > ΔT && rand() < rate * dt
p.g[n] += μ
last_spike[n] = t
end
end
end
end
neuron = SNN.Identity(N = 1, name = "Identity Neuron")
stim = SNN.Stimulus(SNN.SNNModels.PoissonRefractory(N = 1, rate = 100Hz, ΔT = 20ms), neuron, :g)
SNN.monitor!(neuron, [:fire])
model = SNN.compose(; neuron, stim, silent = true)
SNN.sim!(; model, duration = 2000ms)
isi = diff(SNN.spiketimes(neuron)[1])
@assert minimum(isi) > 20msInternals: spike lists
SNNModels.SpikeIndex — Type
SpikeIndex(N)Ascending Int32 indices of the neurons that fired in the current step, derived from a population's fire flags so that forward!, the event-driven plasticity kernels and the :fire recorder scan the flags at most once per step. While active the list is scanned once (valid) and reused; otherwise every spike_list! call rescans fire.
Three kinds:
- scratch (
SpikeIndex(N)): one per connection, opened and closed around the connection's step byprepare_spikes!/release_spikes!. owned(owned_spike_index): the list of an integrate-and-fire population, shared by its connections and recorder; its window is opened and closed by the simulation loop (seespike_index).fed(fed_spike_index): written by its owner every step;fireis never scanned.
SNNModels.TouchList — Type
TouchList(N)Indices of the neurons whose input was written by list-aware stimuli since the last integrate!, in write order, possibly repeated. n counts the writes; n > length(idx) means overflow and the population falls back to its dense update.
SNNModels.attach_driver! — Method
attach_driver!(post::AbstractPopulation)Called by the constructor of a stimulus that logs the neurons it writes. Returns the TouchList in which the stimulus logs (_touch!) every neuron of post whose input it increments, or nothing if post has no sparse update (the default). post (Identity) uses its sparse update only while every writer of its input is such a driver.
SNNModels.fed_spike_index — Method
fed_spike_index(N)
owned_spike_index(N)SpikeIndex of capacity N that is fed by its owner, or owned by a population (see SpikeIndex).
SNNModels.spike_index — Method
spike_index(pop::AbstractPopulation)SpikeIndex used by a connection that reads the spikes of pop: a new scratch list by default, else the list pop keeps:
- the integrate-and-fire family (
IF,AdEx,ExtendedIF,HetRec,Tripod,BallAndStick,HH,IZ,MorrisLecar) owns one list (pop.spikes).sim!/train!open it afterintegrate!and close it at the end of the step; inside this window the first consumer scansfireand later consumers reuse the list. Contract: nothing may writepop.firebetweenintegrate!ofpopand the end of the step. Outside the window every call rescansfire. A failed step is a fatal error and the run is discarded: nothing closes the window after an exception (callSNNModels._retire_all_spikes!(P)before callingforward!,plasticity!orrecord!by hand); the next step publishes afresh. Identitydriven only by list-aware stimuli, and the Poisson populations in their sparse regime, return their fed list.
SNNModels.spike_list! — Method
spike_list!(s::SpikeIndex, fire)Indices (ascending, Int32) of the true entries of fire, as a view of the buffer of s. Not thread-safe on a shared (owned) list: call on the stepping thread, never inside a foreach_chunk body.