2022-02-18 • Scale up N and duration
Contents
2022-02-18 • Scale up N and duration¶
Setup¶
# Pkg.resolve()
include("nb_init.jl")
[ Info: using Revise
[ Info: import Distributions
[ Info: import MyToolbox
[ Info: using VoltageToMap
using Parameters, ComponentArrays
@alias CVec = ComponentVector;
Parameters¶
Simulation duration¶
sim_duration = 1.2 * seconds;
Input spike trains¶
N_unconn = 100
N_exc = 800
# N_exc = 5200
N_inh = N_exc ÷ 4
200
N_conn = N_inh + N_exc
1000
N = N_conn + N_unconn
1100
input_spike_rate = LogNormal_with_mean(4Hz, √0.6) # See the previous notebook
LogNormal{Float64}(μ=1.0862943611198905, σ=0.7745966692414834)
Synapses¶
Reversal potential at excitatory and inhibitory synapses,
as in the report 2021-11-11__synaptic_conductance_ratio.pdf
:
E_exc = 0 * mV
E_inh = -65 * mV;
Synaptic conductances g
at t = 0
g_t0 = 0 * nS;
Exponential decay time constant of synaptic conductance, \(τ_{s}\) (s
for “synaptic”)
τ_s = 7 * ms;
Increase in synaptic conductance on a presynaptic spike
Δg_exc = 0.1 * nS
Δg_inh = 0.4 * nS;
Izhikevich neuron¶
Initial membrane potential v
and adaptation variable u
values
v_t0 = -80 * mV
u_t0 = 0 * pA;
Izhikevich’s neuron model parameters for a cortical regular spiking neuron:
cortical_RS = CVec(
C = 100 * pF,
k = 0.7 * (nS/mV), # steepness of dv/dt's parabola
vr = -60 * mV,
vt = -40 * mV,
a = 0.03 / ms, # 1 / time constant of `u`
b = -2 * nS, # how strongly `v` deviations from `vr` increase `u`.
v_peak = 35 * mV,
c = -50 * mV, # reset voltage.
d = 100 * pA, # `u` increase on spike. Free parameter.
);
Numerics¶
Whether to use a fixed (false
) or adaptive timestep (true
).
adaptive = true;
Timestep. If adaptive
, size of first time step.
dt = 0.1 * ms;
Minimum and maximum step sizes
dtmax = 0.5 * ms # solution unstable if not set
dtmin = 0.01 * ms; # don't spend too much time finding thr crossing or spike arrival
Error tolerances used for determining step size, if adaptive
.
The solver guarantees that the (estimated) difference between
the numerical solution and the true solution at any time step
is not larger than abstol + reltol * |y|
(where y
≈ the numerical solution at that time step).
abstol_v = 0.1 * mV
abstol_u = 0.1 * pA
abstol_g = 0.01 * nS;
reltol = 1e-3; # e.g. if true sol is -80 mV, then max error of 0.08 mV
reltol = 1; # only use abstol
tol_correction = 0.1;
(abstol_v, abstol_u, abstol_g) .* tol_correction
(1.0e-5, 1.0000000000000002e-14, 1.0000000000000002e-12)
For comparison, the default tolerances for ODEs in DifferentialEquations.jl are
reltol = 1e-2
abstol = 1e-6
.
IDs¶
Neuron, synapse & simulated variable IDs.
"""
idvec(A = 4, B = 2, …)
Build a `ComponentVector` (CVec) with the given group names and
as many elements per group as specified. Each element gets a
unique ID within the CVec, which is also its index in the CVec.
I.e. the above call yields `CVec(A = [1,2,3,4], B = [5,6])`.
Specify `nothing` as size for a scalar element. Example:
`idvec(A=nothing, B=1)` → `CVec(A=1, B=[2])`
"""
function idvec(; kw...)
cvec = CVec(; (name => _expand(val) for (name, val) in kw)...)
cvec .= 1:length(cvec)
return cvec
end;
temp = -1 # value does not matter; they get overwritten by UnitRange
_expand(val::Nothing) = temp
_expand(val::Integer) = fill(temp, val)
_expand(val::CVec) = val # allow nested idvecs
;
input_neurons = idvec(conn = idvec(exc = N_exc, inh = N_inh), unconn = N_unconn)
ComponentVector{Int64}(conn = (exc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 … 791, 792, 793, 794, 795, 796, 797, 798, 799, 800], inh = [801, 802, 803, 804, 805, 806, 807, 808, 809, 810 … 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000]), unconn = [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010 … 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100])
synapses = idvec(exc = N_exc, inh = N_inh)
ComponentVector{Int64}(exc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 … 791, 792, 793, 794, 795, 796, 797, 798, 799, 800], inh = [801, 802, 803, 804, 805, 806, 807, 808, 809, 810 … 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000])
simulated_vars = idvec(v = nothing, u = nothing, g = synapses)
ComponentVector{Int64}(v = 1, u = 2, g = (exc = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12 … 793, 794, 795, 796, 797, 798, 799, 800, 801, 802], inh = [803, 804, 805, 806, 807, 808, 809, 810, 811, 812 … 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002]))
Connections¶
postsynapses = Dict{Int, Vector{Int}}() # input_neuron_ID => [synapse_IDs...]
for (n, s) in zip(input_neurons.conn, synapses)
postsynapses[n] = [s]
end
for n in input_neurons.unconn
postsynapses[n] = []
end
Broadcast parameters¶
Δg = similar(synapses, Float64)
Δg.exc .= Δg_exc
Δg.inh .= Δg_inh;
E = similar(synapses, Float64)
E.exc .= E_exc
E.inh .= E_inh;
Initial conditions
vars_t0 = similar(simulated_vars, Float64)
vars_t0.v = v_t0
vars_t0.u = u_t0
vars_t0.g .= g_t0;
Maximum error
abstol = similar(simulated_vars, Float64)
abstol.v = abstol_v
abstol.u = abstol_u
abstol.g .= abstol_g
abstol = abstol .* tol_correction;
ISI distributions¶
Generate firing rates \(λ\) by sampling from the input spike rate distribution.
λ = similar(input_neurons, Float64)
λ .= rand(input_spike_rate, length(λ));
# showsome(λ)
Distributions.jl
uses an alternative Exp
parametrization, namely scale \(β\) = 1 / rate.
β = 1 ./ λ;
ISI_distributions = similar(input_neurons, Exponential{Float64})
ISI_distributions .= Exponential.(β);
Initialize spiking¶
Generate the first spike time for every input neuron by sampling once from its ISI distribution.
first_spike_times = rand.(ISI_distributions);
Sort these initial spike times by building a priority queue.
upcoming_input_spikes = PriorityQueue{Int, Float64}();
for (neuron, first_spike_time) in enumerate(first_spike_times)
enqueue!(upcoming_input_spikes, neuron => first_spike_time)
end
p
object¶
params = (;
E, Δg, τ_s,
izh = cortical_RS,
postsynapses,
ISI_distributions,
upcoming_input_spikes,
);
Differential equations¶
The derivative functions that define the differential equations.
Note that discontinuities are defined in the next section.
function f(D, vars, params, _)
@unpack v, u, g = vars
@unpack E, τ_s, izh = params
@unpack C, k, vr, vt, a, b = izh
I_s = 0.0
for (gi, Ei) in zip(g, E)
I_s += gi * (v - Ei)
end
D.v = (k * (v - vr) * (v - vt) - u - I_s) / C
D.u = a * (b * (v - vr) - u)
D.g .= .-g ./ τ_s
return nothing
end;
vars = vars_t0
D = similar(vars);
f(D, vars, params, nothing)
@time f(D, vars, params, nothing)
0.000018 seconds (3 allocations: 128 bytes)
function f2(D,vars,params)
@unpack v, u, g = vars
@unpack E, τ_s, izh = params
@unpack C, k, vr, vt, a, b = izh;
I_s = 0.0
for (gi, Ei) in zip(g, E)
I_s += gi * (v - Ei)
end
D.v = (k * (v - vr) * (v - vt) - u - I_s) / C
D.u = a * (b * (v - vr) - u)
for i in 1:length(g)
D.g[i] = -g[i] / τ_s
end
return nothing
end;
f2(D,vars,params)
@time f2(D,vars,params)
0.000010 seconds
nice. so D.g should be loop, for no alloc at all. Even @.
didn’t help
Events¶
"""
An `Event` encapsulates two functions that determine when and
how to introduce discontinuities in the differential equations:
- `distance` returns some distance to the next event.
An event occurs when this distance hits zero.
- `on_event!` is called at each event and may modify
the simulated variables and the parameter object.
Both functions take the parameters `(vars, params, t)`: the simulated
variables, the parameter object, and the current simulation time.
"""
struct Event
distance
on_event!
end;
Input spike generation (== arrival, because no transmission delay):
function time_to_next_input_spike(vars, params, t)
_, next_input_spike_time = peek(params.upcoming_input_spikes)
return t - next_input_spike_time
end;
t_ = 0.1s;
time_to_next_input_spike(vars, params, t_)
@time time_to_next_input_spike(vars, params, t_);
0.000007 seconds (1 allocation: 16 bytes)
The one alloc is for the return value. We hope this function gets inlined where it’s used (condition
).
function on_input_spike!(vars, params, t)
# Process the neuron that just fired.
# Start by removing it from the queue.
fired_neuron = dequeue!(params.upcoming_input_spikes)
# Generate a new spike time, and add it to the queue.
new_spike_time = t + rand(params.ISI_distributions[fired_neuron])
enqueue!(params.upcoming_input_spikes, fired_neuron => new_spike_time)
# Update the downstream synapses
# (number of these synapses in the N-to-1 case: 0 or 1).
for synapse in params.postsynapses[fired_neuron]
vars.g[synapse] += params.Δg[synapse]
end
end
input_spike = Event(time_to_next_input_spike, on_input_spike!);
on_input_spike!(vars, params, t_)
@time on_input_spike!(vars, params, t_);
0.000019 seconds
Nice! no allocs already
Spike threshold crossing of Izhikevich neuron
distance_to_v_peak(vars, params, _) = vars.v - params.izh.v_peak;
distance_to_v_peak(vars, params, t_)
@time distance_to_v_peak(vars, params, t_);
0.000005 seconds (1 allocation: 16 bytes)
same thang, return val.
function on_v_peak!(vars, params, _)
# The discontinuous LIF/Izhikevich/AdEx update
vars.v = params.izh.c
vars.u += params.izh.d
return nothing
end
spiking_threshold_crossing = Event(distance_to_spiking_threshold, on_spiking_threshold_crossing!);
on_v_peak!(vars, params, t_)
@time on_v_peak!(vars, params, t_);
0.000006 seconds
I added return nothing
so no alloc.
events = [input_spike, spiking_threshold_crossing];
diffeq.jl API¶
Set-up problem and solution in DifferentialEquations.jl’s API.
@withfeedback using OrdinaryDiffEq
[ Info: using OrdinaryDiffEq
prob = ODEProblem(f, vars_t0, float(sim_duration), params);
function condition(distance, vars, t, integrator)
for (i, event) in enumerate(events)
distance[i] = event.distance(vars, integrator.p, t)
end
end;
distance = zeros(2)
integrator = (p = params,);
condition(distance, vars, t_, integrator)
@time condition(distance, vars, t_, integrator);
0.000014 seconds (19 allocations: 624 bytes)
integrator = (;params, events);
function c2(distance, vars, t, int)
for i in 1:10 end # no alloc
# for i in 1:length(events) end # 3 allocs!! 100 byte
for i in 1:length(int.events) end # no alloc :) (thanks to no global)
for (i, event) in enumerate(int.events)
# distance[i] = event.distance(vars, int.params, t) # 8 allocs, 256 bytes
end
end;
c2(distance, vars, t_, integrator)
@time c2(distance, vars, t_, integrator);
0.000012 seconds (8 allocations: 256 bytes)
@time events[1].distance(vars, integrator.params, t_);
0.000018 seconds (5 allocations: 240 bytes)
@time events[1].distance(vars, params, t_);
0.000013 seconds (3 allocations: 80 bytes)
@time time_to_next_input_spike(vars, params, t_);
0.000007 seconds (1 allocation: 16 bytes)
function affect!(integrator, i)
events[i].on_event!(integrator.u, integrator.p, integrator.t)
end
callback = VectorContinuousCallback(condition, affect!, length(events));
solver = Tsit5()
Tsit5(stage_limiter! = trivial_limiter!, step_limiter! = trivial_limiter!, thread = static(false))
save_idxs = [simulated_vars.v, simulated_vars.u];
progress = true;
solve_() = solve(
prob, solver;
callback, save_idxs, progress,
adaptive, dt, dtmax, dtmin, abstol, reltol,
);
Solve¶
sol = @time solve_();
16.172403 seconds (24.21 M allocations: 1.162 GiB, 5.96% gc time, 88.36% compilation time)
sol = @time solve_();
12.388856 seconds (23.88 M allocations: 1.152 GiB, 4.50% gc time, 89.20% compilation time)
Plot¶
@withfeedback import PyPlot
using Sciplotlib
[ Info: import PyPlot
""" t = [200ms, 600ms] e.g. """
function Sciplotlib.plot(sol::ODESolution; t = nothing)
isnothing(t) && (t = sol.t[[1,end]])
izoom = first(t) .< sol.t .< last(t)
plot(
sol.t[izoom] / ms,
sol[1,izoom] / mV,
clip_on = false,
marker = ".", ms = 1.2, lw = 0.4,
# xlim = t, # haha lolwut, adding this causes fig to no longer display.
)
end;
plot(sol);