// AdExSinExp — Adaptive Exponential Integrate-and-Fire neuron with
// SINGLE-EXPONENTIAL synapses (instead of double-exponential).
//
// Julia reference:
// SNNModels.jl/src/populations/synapse/synapses/SingleExpSynapse.jl
// LKD2014SingleExp in SNNUtils.jl/src/models/lkd2014.jl uses this
// parameterisation (τe=6ms, τi=2ms).
//
// Float32 contract: every arithmetic uses `Float` (Float32); expf
// for the exponential term (matches Julia's exp(Float32, x)).
//
// Bit-exact ordering: same as AdEx (Brette-Gerstner 2005) for the
// neuron dynamics; the only difference is in `adex_sinexp_step_synapses`
// which does single-exponential ge/gi decay instead of double-exponential
// (no separate rise variables he/hi).
//
// Difference vs AdEx:
// - AdEx (double-exp): he/hi are rise conductances that buffer
// spikes from glu/gaba before being integrated into ge/gi.
// - AdExSinExp: no he/hi. glu is added directly to ge (and gaba to gi).
// Julia formula (SingleExpSynapse):
// ge[i] += glu[i]
// gi[i] += gaba[i]
// ge[i] += dt * (-ge[i] / τe)
// gi[i] += dt * (-gi[i] / τi)
// glu[i] = 0
// gaba[i] = 0
///|
/// AdExSinExpParameter — same as AdExParameter (the neuron model
/// is identical; only the synapse model differs). We keep it as a
/// separate type to make `param: AdExSinExpParameter` a useful
/// documentation marker and to match Julia's `AdExSinExpParameter`.
pub(all) struct AdExSinExpParameter {
c : Float
gl : Float
vt : Float
vr : Float
el : Float
tm : Float
r : Float
dt_slope : Float
tw : Float
a : Float
b : Float
}
///|
/// Default AdExSinExpParameter, matching Julia's `AdExSinExpParameter()`.
/// All values identical to AdExParameter defaults.
pub fn AdExSinExpParameter::new() -> AdExSinExpParameter {
let c : Float = 281.0F
let gl : Float = 40.0F
let tm : Float = 281.0F / 40.0F
let r : Float = 1.0F / 40.0F
{ c, gl, vt: -50.0F, vr: -70.6F, el: -70.6F, tm, r, dt_slope: 2.0F,
tw: 144.0F, a: 4.0F, b: 80.5F }
}
///|
/// AdExSinExpParameter with LKD 2014 defaults (El=-70mV, Vt=-52mV,
/// Vr=-60mV, At=10mV). Matches the LKD2014SingleExp AdEx config in
/// SNNUtils.jl/src/models/lkd2014.jl.
pub fn AdExSinExpParameter::lkd_adex() -> AdExSinExpParameter {
let c : Float = 300.0F
let gl : Float = 15.0F
let tm : Float = c / gl
let r : Float = 1.0F / gl
{ c, gl, vt: -52.0F, vr: -60.0F, el: -70.0F, tm, r, dt_slope: 2.0F,
tw: 144.0F, a: 4.0F, b: 80.5F }
}
///|
/// AdEx SinExp neuron state — a population of N adaptive exponential
/// LIF neurons with single-exponential synaptic dynamics.
pub struct AdExSinExp {
param : AdExSinExpParameter
spike : AdExPostSpike
n : Int
v : Array[Float]
w : Array[Float]
fire : Array[Bool]
threshold : Array[Float]
tabs : Array[Int]
i : Array[Float]
syn_curr : Array[Float]
// SinExp synapse state (no he/hi — single exponential).
ge : Array[Float]
gi : Array[Float]
glu : Array[Float]
gaba : Array[Float]
gsyn_e : Array[Float]
gsyn_i : Array[Float]
// Reversal potentials.
e_e : Float
e_i : Float
// SinExp time constants.
tau_e : Float
tau_i : Float
}
///|
/// Construct a new AdExSinExp population with `n` neurons, default
/// PostSpike (At=0mV, τA=10ms) and LKD-like defaults
/// (E_e=0mV, E_i=-75mV, τe=6ms, τi=2ms).
pub fn AdExSinExp::new(
n : Int,
param : AdExSinExpParameter,
rng : Xoshiro,
) -> AdExSinExp {
AdExSinExp::new_with_spike(n, param, AdExPostSpike::new(), rng)
}
///|
/// Construct a new AdExSinExp population with a custom PostSpike.
pub fn AdExSinExp::new_with_spike(
n : Int,
param : AdExSinExpParameter,
spike : AdExPostSpike,
rng : Xoshiro,
) -> AdExSinExp {
let v = Array::make(n, 0.0F)
let spread = param.vt - param.vr
for k in 0.. Unit {
let n = p.n
let tau_e = p.tau_e
let tau_i = p.tau_i
// Consume glu/gaba into ge/gi, then apply single-exponential decay.
for i in 0.. Unit {
let n = p.n
for i in 0.. Unit {
let n = p.n
let p_ = p.param
let tm = p_.tm
let vt = p_.vt
let vr = p_.vr
let el = p_.el
let r = p_.r
let dt_slope = p_.dt_slope
let tw = p_.tw
let a = p_.a
let b = p_.b
let at = p.spike.at
let tau_a = p.spike.tau_a
let tabs_const = p.spike.tabs_const
let tabs_steps : Int = (tabs_const / dt).to_int()
for i in 0.. 0 {
continue
}
// Adaptation current
p.w[i] = p.w[i] + dt * (a * (p.v[i] - el) - p.w[i]) / tw
// Membrane potential: leakage + exponential + synapses + adaptation + ext I
let exp_term = if dt_slope < 0.0F {
0.0F
} else {
dt_slope * expf((p.v[i] - p.threshold[i]) / dt_slope)
}
p.v[i] = p.v[i] +
dt *
(
-(p.v[i] - el) + exp_term - r * p.syn_curr[i] - r * p.w[i] +
r * p.i[i]
) / tm
// Threshold dynamics
p.threshold[i] = p.threshold[i] + dt * (vt - p.threshold[i]) / tau_a
// Spike detection
p.fire[i] = p.v[i] >= 0.0F
p.v[i] = if p.fire[i] { 20.0F } else { p.v[i] }
p.w[i] = if p.fire[i] { p.w[i] + b } else { p.w[i] }
p.threshold[i] = if p.fire[i] { p.threshold[i] + at } else { p.threshold[i] }
p.tabs[i] = if p.fire[i] { tabs_steps } else { p.tabs[i] }
}
()
}
// =========================================================================
// MonitorAdExSinExp — minimal monitor for AdExSinExp populations.
// Mirrors MonitorAdEx but for the SinExp variant. The Monitor struct in
// sim.mbt is hardcoded to IF (it stores `pop : IF`), so for AdExSinExp
// we provide a separate type. Same idea as MonitorAdEx for AdEx.
// =========================================================================
///|
/// A monitor for an AdExSinExp population. Records :v / :fire / :w.
pub struct MonitorAdExSinExp {
pop : AdExSinExp
sym : String
data : Array[Float]
times : Array[Float]
neuron : Int
}
///|
pub fn MonitorAdExSinExp::new_v(pop : AdExSinExp, neuron : Int) -> MonitorAdExSinExp {
{ pop, sym: "v", data: [], times: [], neuron }
}
///|
pub fn MonitorAdExSinExp::new_fire(pop : AdExSinExp, neuron : Int) -> MonitorAdExSinExp {
{ pop, sym: "fire", data: [], times: [], neuron }
}
///|
/// Take one snapshot, appending to `data` and `times`.
pub fn record_one_sinexp(m : MonitorAdExSinExp, t : Float) -> Unit {
let v = if m.sym == "v" {
m.pop.v[m.neuron]
} else if m.sym == "fire" {
if m.pop.fire[m.neuron] { 1.0F } else { 0.0F }
} else if m.sym == "w" {
m.pop.w[m.neuron]
} else {
0.0F
}
m.data.push(v)
m.times.push(t)
}