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 by validate_population_model; monitor! and record! store the recordings in records). A field fire::Vector{Bool} is needed to record spikes (monitor!(p, [:fire])) and to be the presynaptic population of a SpikingSynapse. Any other field (v, I, ...) can be recorded with monitor!.
  • integrate!(p, param, dt::AbstractFloat): advance the state by one step (dt arrives 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 receives SpikingSynapses or spiking stimuli; there is no generic fallback. Stimuli that write a current (CurrentStimulus) read the field sym directly.
  • Optional: Population(param::MyParameter; kwargs...) so that SNN.Population(; param, ...) builds the new type, and plasticity!/update_traces! methods; the generic fallbacks for AbstractPopulationParameter do nothing.

Connections (<: AbstractConnection, parameter <: AbstractConnectionParameter)

  • Required fields: param, id, name, records::Dict (checked by validate_synapse_model).
  • forward!(c, param, dt, T) or the two-argument form forward!(c, param): the generic four-argument method calls the two-argument one. This generic method, and the no-op update_traces! fallback, exist only for parameters that are subtypes of AbstractConnectionParameter; with any other parameter type sim! and train! fail with a MethodError.
  • plasticity!(c, param, dt, T::Time): called by train! for every connection; there is no generic fallback, so define it (it can return nothing) if the model is ever run with train!. update_traces! has a no-op fallback.

Stimuli (<: AbstractStimulus, parameter <: AbstractStimulusParameter)

  • Required fields: param, id, name, records::Dict (checked by validate_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 of PlasticityVariables with an active::Vector{Bool} field (the rule runs only if any(active)).
  • plasticity!(c, param, vars, dt, T) (applied after forward!) and, if the rule needs it, update_traces!(c, param, vars, dt, T) (applied before forward!). Long-term rules modify c.W, short-term rules the efficacy c.ρ.

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) > 20ms

Internals: 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 by prepare_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 (see spike_index).
  • fed (fed_spike_index): written by its owner every step; fire is never scanned.
source
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.

source
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.

source
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).

source
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 after integrate! and close it at the end of the step; inside this window the first consumer scans fire and later consumers reuse the list. Contract: nothing may write pop.fire between integrate! of pop and the end of the step. Outside the window every call rescans fire. A failed step is a fatal error and the run is discarded: nothing closes the window after an exception (call SNNModels._retire_all_spikes!(P) before calling forward!, plasticity! or record! by hand); the next step publishes afresh.
  • Identity driven only by list-aware stimuli, and the Poisson populations in their sparse regime, return their fed list.
source
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.

source