// neuron_if_sinexp.mbt — IF + single-exp synapse (LKD2014SingleExp.PV).
//
// Julia reference:
// - LKD2014SingleExp.PV in SNNUtils.jl/src/models/lkd2014.jl
// uses an IF neuron with τe=6ms, τi=2ms single-exp synapses.
// - See AdExSinExp (neuron_adex_sinexp.mbt) for the single-exp
// synapse update rule.
//
// Float32 contract: same as IF (every arithmetic uses `Float`).
///|
/// IFSinExpParameter — IF neuron parameters with single-exp synapse
/// time constants (τe, τi). Structurally identical to IFParameter;
/// kept as a separate type for documentation and to match Julia's
/// `IFSinExpParameter` named tuple in LKD2014SingleExp.
pub struct IFSinExpParameter {
c : Float
gl : Float
tm : Float
vt : Float
vr : Float
el : Float
r : Float
dt_slope : Float
a : Float
b : Float
tw : Float
// Single-exp synapse time constants (LKD2014SingleExp.PV defaults).
tau_e : Float
tau_i : Float
}
///|
/// Default IFSinExpParameter, matching Julia's `IFSinExpParameter()`
/// with τe=6, τi=2.
pub fn IFSinExpParameter::new() -> IFSinExpParameter {
let c : Float = -1.0F
let gl : Float = -1.0F
{ c, gl, tm: 20.0F, vt: -50.0F, vr: -60.0F, el: -62.0F, r: 0.05F,
dt_slope: 2.0F, a: 0.0F, b: 0.0F, tw: 0.0F,
tau_e: 6.0F, tau_i: 2.0F }
}
///|
/// IFSinExpParameter with LKD 2014 PV defaults (El=-62mV, Vr=-57.47mV,
/// Vt=-52mV, τm=20ms, τe=6, τi=2). Matches Julia's
/// `IFSinExpParameter(El=-62, Vr=-57.47, Vt=-52, τm=20, τi=2, τe=6)`.
pub fn IFSinExpParameter::lkd_pv() -> IFSinExpParameter {
let c : Float = -1.0F
let gl : Float = -1.0F
{ c, gl, tm: 20.0F, vt: -52.0F, vr: -57.47F, el: -62.0F, r: 0.05F,
dt_slope: 2.0F, a: 0.0F, b: 0.0F, tw: 0.0F,
tau_e: 6.0F, tau_i: 2.0F }
}
///|
/// IFSinExp neuron state — population of N integrate-and-fire neurons
/// with single-exp synaptic dynamics.
pub struct IFSinExp {
param : IFSinExpParameter
spike : PostSpike
n : Int
v : Array[Float]
w : Array[Float]
fire : Array[Bool]
tabs : Array[Int]
i : Array[Float]
syn_curr : Array[Float]
// Synapse state (single-exp — no separate rise conductances)
ge : Array[Float]
gi : Array[Float]
glu : Array[Float]
gaba : Array[Float]
gsyn_e : Array[Float]
gsyn_i : Array[Float]
// Records
fire_t : Array[Int]
fire_t_delta : Array[Int]
}
///|
/// Build an IFSinExp population of size n with `param` and random
/// initial state drawn from `rng`.
pub fn IFSinExp::new(
n : Int,
param : IFSinExpParameter,
rng : Xoshiro,
) -> IFSinExp {
let v : Array[Float] = Array::make(n, param.el)
let w : Array[Float] = Array::make(n, 0.0F)
let fire : Array[Bool] = Array::make(n, false)
let tabs : Array[Int] = Array::make(n, 0)
let i : Array[Float] = Array::make(n, 0.0F)
let syn_curr : Array[Float] = Array::make(n, 0.0F)
let ge : Array[Float] = Array::make(n, 0.0F)
let gi : Array[Float] = Array::make(n, 0.0F)
let glu : Array[Float] = Array::make(n, 0.0F)
let gaba : Array[Float] = Array::make(n, 0.0F)
let gsyn_e : Array[Float] = Array::make(n, 0.0F)
let gsyn_i : Array[Float] = Array::make(n, 0.0F)
let fire_t : Array[Int] = []
let fire_t_delta : Array[Int] = Array::make(n, 0)
// Random initial voltages via Box-Muller (small noise).
for k in 0..= vt, fire, reset v=vr, record fire_t.
pub fn ifsinexp_step(
pop : IFSinExp,
dt : Float,
) -> Unit {
let n = pop.n
let p = pop.param
let mut k = 0
while k < n {
pop.fire[k] = false
// Skip refractory
if pop.tabs[k] > 0 {
pop.tabs[k] = pop.tabs[k] - 1
} else {
// Membrane update (Euler).
pop.v[k] = pop.v[k] + dt * (pop.v[k] - p.el - p.r * pop.i[k]) / p.tm
// Fire?
if pop.v[k] >= p.vt {
pop.fire[k] = true
pop.v[k] = p.vr
pop.fire_t.push(k)
pop.fire_t_delta[k] = pop.fire_t_delta[k] + 1
pop.tabs[k] = pop.spike.tabs_const.to_int() // refractory ticks
}
}
// Single-exp synapse decay (matches Julia SingleExpSynapse update).
pop.ge[k] = pop.ge[k] + pop.glu[k]
pop.ge[k] = pop.ge[k] + dt * (-pop.ge[k] / p.tau_e)
pop.gi[k] = pop.gi[k] + pop.gaba[k]
pop.gi[k] = pop.gi[k] + dt * (-pop.gi[k] / p.tau_i)
pop.glu[k] = 0.0F
pop.gaba[k] = 0.0F
k = k + 1
}
let _ = dt
}