Skip to main content

sc_neurocore_engine/
network_runner.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — Network runner: high-performance Rust simulation backend
8
9//! High-performance network simulation backend.
10//!
11//! Replaces the Python per-neuron loop with Rayon-parallel Rust execution
12//! over CSR-stored projections and heterogeneous neuron populations.
13
14use rayon::prelude::*;
15
16use crate::neuron::*;
17use crate::neurons::*;
18
19// ── Interface wrappers for non-standard models ──────────────────────
20
21/// Wrapper: multi-input spiking model → single-input interface.
22/// Extra inputs default to 0/false when driven from the network runner.
23macro_rules! wrap_2arg_f64 {
24    ($name:ident, $inner:ty, $v:ident, $extra:expr) => {
25        #[derive(Clone, Debug)]
26        pub struct $name(pub $inner);
27        impl $name {
28            pub fn new() -> Self {
29                Self(<$inner>::new())
30            }
31            pub fn step(&mut self, current: f64) -> i32 {
32                self.0.step(current, $extra)
33            }
34            pub fn reset(&mut self) {
35                self.0.reset();
36            }
37            pub fn v(&self) -> f64 {
38                self.0.$v as f64
39            }
40        }
41        impl Default for $name {
42            fn default() -> Self {
43                Self::new()
44            }
45        }
46    };
47}
48
49/// Wrapper: 3-arg model → single-input interface.
50macro_rules! wrap_3arg {
51    ($name:ident, $inner:ty, $v:ident, $e2:expr, $e3:expr) => {
52        #[derive(Clone, Debug)]
53        pub struct $name(pub $inner);
54        impl $name {
55            pub fn new() -> Self {
56                Self(<$inner>::new())
57            }
58            pub fn step(&mut self, current: f64) -> i32 {
59                self.0.step(current, $e2, $e3)
60            }
61            pub fn reset(&mut self) {
62                self.0.reset();
63            }
64            pub fn v(&self) -> f64 {
65                self.0.$v as f64
66            }
67        }
68        impl Default for $name {
69            fn default() -> Self {
70                Self::new()
71            }
72        }
73    };
74}
75
76/// Wrapper: i32-input model → f64-input interface.
77macro_rules! wrap_i32_input {
78    ($name:ident, $inner:ty, $v:ident, $ctor:expr) => {
79        #[derive(Clone, Debug)]
80        pub struct $name(pub $inner);
81        impl $name {
82            pub fn new() -> Self {
83                Self($ctor)
84            }
85            pub fn step(&mut self, current: f64) -> i32 {
86                self.0.step(current as i32)
87            }
88            pub fn reset(&mut self) {
89                self.0.reset();
90            }
91            pub fn v(&self) -> f64 {
92                self.0.$v as f64
93            }
94        }
95        impl Default for $name {
96            fn default() -> Self {
97                Self::new()
98            }
99        }
100    };
101}
102
103/// Wrapper: f64-output (rate/graded) → i32 spike interface via thresholding.
104macro_rules! wrap_graded {
105    ($name:ident, $inner:ty, $v:ident, $threshold:expr) => {
106        #[derive(Clone, Debug)]
107        pub struct $name(pub $inner);
108        impl $name {
109            pub fn new() -> Self {
110                Self(<$inner>::new())
111            }
112            pub fn step(&mut self, current: f64) -> i32 {
113                let out = self.0.step(current);
114                if out > $threshold {
115                    1
116                } else {
117                    0
118                }
119            }
120            pub fn reset(&mut self) {
121                self.0.reset();
122            }
123            pub fn v(&self) -> f64 {
124                self.0.$v as f64
125            }
126        }
127        impl Default for $name {
128            fn default() -> Self {
129                Self::new()
130            }
131        }
132    };
133}
134
135// Multi-input spiking wrappers
136wrap_2arg_f64!(WrAlpha, AlphaNeuron, v, 0.0_f64);
137wrap_3arg!(WrCOBALIF, COBALIFNeuron, v, 0.0_f64, 0.0_f64);
138wrap_2arg_f64!(WrCompteWM, CompteWMNeuron, v, false);
139wrap_2arg_f64!(WrTsodyksMarkram, TsodyksMarkramNeuron, v, false);
140wrap_2arg_f64!(WrPinskyRinzel, PinskyRinzelNeuron, v_s, 0.0_f64);
141wrap_2arg_f64!(WrHayL5, HayL5PyramidalNeuron, v_s, 0.0_f64);
142wrap_2arg_f64!(WrTwoCompLIF, TwoCompartmentLIFNeuron, v_s, 0.0_f64);
143
144// Hardware integer-input wrappers
145wrap_i32_input!(WrLoihiCUBA, LoihiCUBANeuron, v, LoihiCUBANeuron::new());
146wrap_i32_input!(WrLoihi2, Loihi2Neuron, s1, Loihi2Neuron::new());
147wrap_i32_input!(WrSpiNNaker2, SpiNNaker2Neuron, v, SpiNNaker2Neuron::new());
148wrap_i32_input!(WrTrueNorth, TrueNorthNeuron, v, TrueNorthNeuron::new(256));
149wrap_i32_input!(
150    WrIntegerQIF,
151    IntegerQIFNeuron,
152    v,
153    IntegerQIFNeuron::new(1, 1000)
154);
155
156// Graded/rate output wrappers (threshold at 0.5 for rates, 0.0 for sensory)
157wrap_graded!(WrSigmoidRate, SigmoidRateNeuron, r, 0.5);
158wrap_graded!(WrThresholdLinear, ThresholdLinearRateNeuron, r, 0.5);
159wrap_graded!(WrAstrocyte, AstrocyteModel, ca, 0.1);
160wrap_graded!(WrInnerHairCell, InnerHairCell, v, 0.0);
161wrap_graded!(WrOuterHairCell, OuterHairCell, v, 0.0);
162wrap_graded!(WrRodPhotoreceptor, RodPhotoreceptor, v, 0.0);
163wrap_graded!(WrConePhotoreceptor, ConePhotoreceptor, v, 0.0);
164wrap_graded!(WrTasteReceptor, TasteReceptorCell, v, 0.0);
165
166// ── NeuronVariant ───────────────────────────────────────────────────
167
168/// Enum dispatch across all neuron models.
169///
170/// Models with non-standard signatures are wrapped via `Wr*` types
171/// to normalise to `step(f64) -> i32`.
172#[allow(clippy::large_enum_variant)]
173pub enum NeuronVariant {
174    // neuron.rs
175    Izhikevich(Izhikevich),
176    AdEx(AdExNeuron),
177    ExpIF(ExpIfNeuron),
178    Lapicque(LapicqueNeuron),
179    HomeostaticLif(HomeostaticLif),
180
181    // biophysical.rs
182    HodgkinHuxley(HodgkinHuxleyNeuron),
183    TraubMiles(TraubMilesNeuron),
184    WangBuzsaki(WangBuzsakiNeuron),
185    ConnorStevens(ConnorStevensNeuron),
186    DestexheThalamic(DestexheThalamicNeuron),
187    HuberBraun(HuberBraunNeuron),
188    GolombFS(GolombFSNeuron),
189    Pospischil(PospischilNeuron),
190    MainenSejnowski(MainenSejnowskiNeuron),
191    DeSchutterPurkinje(DeSchutterPurkinjeNeuron),
192    PlantR15(PlantR15Neuron),
193    Prescott(PrescottNeuron),
194    MihalasNiebur(MihalasNieburNeuron),
195    GLIF(GLIFNeuron),
196    GIFPopulation(GIFPopulationNeuron),
197    AvRonCardiac(AvRonCardiacNeuron),
198    DurstewitzDopamine(DurstewitzDopamineNeuron),
199    HillTononi(HillTononiNeuron),
200    BertramPhantom(BertramPhantomBurster),
201    Yamada(YamadaNeuron),
202
203    // simple_spiking.rs
204    FitzHughNagumo(FitzHughNagumoNeuron),
205    MorrisLecar(MorrisLecarNeuron),
206    HindmarshRose(HindmarshRoseNeuron),
207    ResonateAndFire(ResonateAndFireNeuron),
208    BalancedResonateAndFire(BalancedResonateAndFireNeuron),
209    FitzHughRinzel(FitzHughRinzelNeuron),
210    McKean(McKeanNeuron),
211    TermanWang(TermanWangOscillator),
212    GutkinErmentrout(GutkinErmentroutNeuron),
213    WilsonHR(WilsonHRNeuron),
214    Chay(ChayNeuron),
215    ChayKeizer(ChayKeizerNeuron),
216    ShermanRinzelKeizer(ShermanRinzelKeizerNeuron),
217    ButeraRespiratory(ButeraRespiratoryNeuron),
218    EPropALIF(EPropALIFNeuron),
219    SuperSpike(SuperSpikeNeuron),
220    LearnableNeuron(LearnableNeuronModel),
221    Pernarowski(PernarowskiNeuron),
222
223    // trivial.rs (simple IF variants)
224    QuadraticIF(QuadraticIFNeuron),
225    Theta(ThetaNeuron),
226    PerfectIntegrator(PerfectIntegratorNeuron),
227    GatedLIF(GatedLIFNeuron),
228    NonlinearLIF(NonlinearLIFNeuron),
229    SFA(SFANeuron),
230    MAT(MATNeuron),
231    KLIF(KLIFNeuron),
232    InhibitoryLIF(InhibitoryLIFNeuron),
233    ComplementaryLIF(ComplementaryLIFNeuron),
234    ParametricLIF(ParametricLIFNeuron),
235    NonResettingLIF(NonResettingLIFNeuron),
236    AdaptiveThresholdIF(AdaptiveThresholdIFNeuron),
237    SigmaDelta(SigmaDeltaNeuron),
238    EnergyLIF(EnergyLIFNeuron),
239    ClosedFormContinuous(ClosedFormContinuousNeuron),
240
241    // maps.rs
242    ChialvoMap(ChialvoMapNeuron),
243    RulkovMap(RulkovMapNeuron),
244    IbarzTanakaMap(IbarzTanakaMapNeuron),
245    MedvedevMap(MedvedevMapNeuron),
246    CazellesMap(CazellesMapNeuron),
247    CourageNekorkinMap(CourageNekorkinMapNeuron),
248    AiharaMap(AiharaMapNeuron),
249    KilincBhattMap(KilincBhattMapNeuron),
250    ErmentroutKopellMap(ErmentroutKopellMapNeuron),
251
252    // hardware.rs (f64 input subset)
253    BrainScaleSAdEx(BrainScaleSAdExNeuron),
254    SpiNNakerLIF(SpiNNakerLIFNeuron),
255    NeuroGrid(NeuroGridNeuron),
256    DPI(DPINeuron),
257    Akida(AkidaNeuron),
258    StochasticLIF(StochasticLIFNeuron),
259
260    // multi_compartment.rs (single-f64-input subset)
261    MarderSTG(MarderSTGNeuron),
262    RallCable(RallCableNeuron),
263    BoothRinzel(BoothRinzelNeuron),
264    Dendrify(DendrifyNeuron),
265
266    // rate.rs (spiking subset with step(f64)->i32)
267    LiquidTimeConstant(LiquidTimeConstantNeuron),
268    ParallelSpiking(ParallelSpikingNeuron),
269    FractionalLIF(FractionalLIFNeuron),
270
271    // special.rs (step(f64)->i32 subset)
272    StochasticIF(StochasticIFNeuron),
273    GalvesLocherbach(GalvesLocherbachNeuron),
274    SpikeResponse(SpikeResponseNeuron),
275    GLM(GLMNeuron),
276    Arcane(ArcaneNeuron),
277
278    // --- newly wired (step(f64)->i32 compatible) ---
279    // ai_optimized.rs
280    MultiTimescale(MultiTimescaleNeuron),
281    AttentionGated(AttentionGatedNeuron),
282    PredictiveCoding(PredictiveCodingNeuron),
283    SelfReferential(SelfReferentialNeuron),
284    CompositionalBinding(CompositionalBindingNeuron),
285    DifferentiableSurrogate(DifferentiableSurrogateNeuron),
286    ContinuousAttractor(ContinuousAttractorNeuron),
287    MetaPlastic(MetaPlasticNeuron),
288    // simple_spiking.rs
289    BendaHerz(BendaHerzNeuron),
290    BrunelWang(BrunelWangNeuron),
291    // special.rs
292    Poisson(PoissonNeuron),
293    InhomogeneousPoisson(InhomogeneousPoissonNeuron),
294    GammaRenewal(GammaRenewalNeuron),
295    EscapeRate(EscapeRateNeuron),
296    // interneurons.rs (step(f64)->i32)
297    PVFastSpiking(PVFastSpikingNeuron),
298    SST(SSTNeuron),
299    VIP(VIPNeuron),
300    Chandelier(ChandelierNeuron),
301    CerebellarBasket(CerebellarBasketNeuron),
302    Martinotti(MartinottiNeuron),
303
304    // motor.rs (step(f64)->i32)
305    AlphaMotor(AlphaMotorNeuron),
306    GammaMotor(GammaMotorNeuron),
307    UpperMotor(UpperMotorNeuron),
308    Renshaw(RenshawCell),
309    MotorUnitCell(MotorUnit),
310
311    // sensory.rs (spiking subset: step(f64)->i32)
312    RetinalGanglion(RetinalGanglionCell),
313    Merkel(MerkelCell),
314    Pacinian(PacinianCorpuscle),
315    NociceptorCell(Nociceptor),
316    OlfactoryReceptor(OlfactoryReceptorNeuron),
317
318    // cerebellar.rs (step(f64)->i32)
319    Granule(GranuleCell),
320    Golgi(GolgiCell),
321    Stellate(StellateCell),
322    Lugaro(LugaroCell),
323    UnipolarBrush(UnipolarBrushCell),
324    DCN(DCNNeuron),
325
326    // channels.rs (step(f64)->i32)
327    PersistentNa(PersistentNaNeuron),
328    Ih(IhNeuron),
329    TTypeCa(TTypeCaNeuron),
330    ATypeK(ATypeKNeuron),
331    BK(BKNeuron),
332    SK(SKNeuron),
333    NMDA(NMDANeuron),
334
335    // population.rs (step(f64)->i32)
336    MontbrioMPR(MontbrioMeanField),
337    Brunel(BrunelNetwork),
338    TUM(TUMNetwork),
339    ElBoustani(ElBoustaniNetwork),
340
341    // misc.rs (step(f64)->i32)
342    GradedSynapse(GradedSynapseNeuron),
343    GapJunction(GapJunctionNeuron),
344    FHAxon(FrankenhaeUserHuxleyAxon),
345    NodeOfRanvier(NodeOfRanvier),
346    MyelinAxon(MyelinatedAxon),
347    CardiacPurkinje(CardiacPurkinjeFibre),
348    SmoothMuscle(SmoothMuscleCell),
349    BetaCell(EndocrineBetaCell),
350
351    // Wrapped models: non-standard interfaces normalised to step(f64)->i32
352    // Multi-input spiking
353    WrAlphaCell(WrAlpha),
354    WrCOBALIFCell(WrCOBALIF),
355    WrCompteWMCell(WrCompteWM),
356    WrTsodyksMarkramCell(WrTsodyksMarkram),
357    WrPinskyRinzelCell(WrPinskyRinzel),
358    WrHayL5Cell(WrHayL5),
359    WrTwoCompLIFCell(WrTwoCompLIF),
360    // Hardware integer-input
361    WrLoihiCUBACell(WrLoihiCUBA),
362    WrLoihi2Cell(WrLoihi2),
363    WrSpiNNaker2Cell(WrSpiNNaker2),
364    WrTrueNorthCell(WrTrueNorth),
365    WrIntegerQIFCell(WrIntegerQIF),
366    // Graded/rate output
367    WrSigmoidRateCell(WrSigmoidRate),
368    WrThresholdLinearCell(WrThresholdLinear),
369    WrAstrocyteCell(WrAstrocyte),
370    WrInnerHairCellCell(WrInnerHairCell),
371    WrOuterHairCellCell(WrOuterHairCell),
372    WrRodPhotoreceptorCell(WrRodPhotoreceptor),
373    WrConePhotoreceptorCell(WrConePhotoreceptor),
374    WrTasteReceptorCell(WrTasteReceptor),
375}
376
377macro_rules! dispatch_step {
378    ($self:expr, $current:expr,
379     $($variant:ident),* $(,)?) => {
380        match $self {
381            $(NeuronVariant::$variant(n) => n.step($current),)*
382        }
383    };
384}
385
386macro_rules! dispatch_reset {
387    ($self:expr,
388     $($variant:ident),* $(,)?) => {
389        match $self {
390            $(NeuronVariant::$variant(n) => n.reset(),)*
391        }
392    };
393}
394
395/// All variant names in one place for the dispatch macros.
396macro_rules! all_variants {
397    ($mac:ident, $($args:tt)*) => {
398        $mac!($($args)*
399            Izhikevich, AdEx, ExpIF, Lapicque, HomeostaticLif,
400            HodgkinHuxley, TraubMiles, WangBuzsaki, ConnorStevens,
401            DestexheThalamic, HuberBraun, GolombFS,
402            Pospischil, MainenSejnowski, DeSchutterPurkinje,
403            PlantR15, Prescott, MihalasNiebur, GLIF, GIFPopulation,
404            AvRonCardiac, DurstewitzDopamine, HillTononi, BertramPhantom, Yamada, Akida, StochasticLIF,
405            FitzHughNagumo, MorrisLecar, HindmarshRose, ResonateAndFire, BalancedResonateAndFire,
406            FitzHughRinzel, McKean, TermanWang, GutkinErmentrout, WilsonHR,
407            Chay, ChayKeizer, ShermanRinzelKeizer, ButeraRespiratory,
408            EPropALIF, SuperSpike, LearnableNeuron, Pernarowski,
409            QuadraticIF, Theta, PerfectIntegrator, GatedLIF, NonlinearLIF,
410            SFA, MAT, KLIF, InhibitoryLIF, ComplementaryLIF, ParametricLIF,
411            NonResettingLIF, AdaptiveThresholdIF, SigmaDelta, EnergyLIF,
412            ClosedFormContinuous,
413            ChialvoMap, RulkovMap, IbarzTanakaMap, MedvedevMap,
414            CazellesMap, CourageNekorkinMap, AiharaMap, KilincBhattMap, ErmentroutKopellMap,
415            BrainScaleSAdEx, SpiNNakerLIF, NeuroGrid, DPI,
416            MarderSTG, RallCable, BoothRinzel, Dendrify,
417            LiquidTimeConstant, ParallelSpiking, FractionalLIF,
418            StochasticIF, GalvesLocherbach, SpikeResponse, GLM,
419            Arcane,
420            MultiTimescale, AttentionGated, PredictiveCoding,
421            SelfReferential, CompositionalBinding, DifferentiableSurrogate,
422            ContinuousAttractor, MetaPlastic,
423            BendaHerz, BrunelWang,
424            Poisson, InhomogeneousPoisson, GammaRenewal, EscapeRate,
425            PVFastSpiking, SST, VIP, Chandelier, CerebellarBasket, Martinotti,
426            AlphaMotor, GammaMotor, UpperMotor, Renshaw, MotorUnitCell,
427            RetinalGanglion, Merkel, Pacinian, NociceptorCell, OlfactoryReceptor,
428            Granule, Golgi, Stellate, Lugaro, UnipolarBrush, DCN,
429            PersistentNa, Ih, TTypeCa, ATypeK, BK, SK, NMDA,
430            MontbrioMPR, Brunel, TUM, ElBoustani,
431            GradedSynapse, GapJunction, FHAxon, NodeOfRanvier, MyelinAxon, CardiacPurkinje,
432            SmoothMuscle, BetaCell,
433            WrAlphaCell, WrCOBALIFCell, WrCompteWMCell, WrTsodyksMarkramCell,
434            WrPinskyRinzelCell, WrHayL5Cell, WrTwoCompLIFCell,
435            WrLoihiCUBACell, WrLoihi2Cell, WrSpiNNaker2Cell, WrTrueNorthCell, WrIntegerQIFCell,
436            WrSigmoidRateCell, WrThresholdLinearCell, WrAstrocyteCell,
437            WrInnerHairCellCell, WrOuterHairCellCell,
438            WrRodPhotoreceptorCell, WrConePhotoreceptorCell, WrTasteReceptorCell,
439        )
440    };
441}
442
443impl NeuronVariant {
444    pub fn step(&mut self, current: f64) -> i32 {
445        all_variants!(dispatch_step, self, current,)
446    }
447
448    pub fn reset(&mut self) {
449        all_variants!(dispatch_reset, self,)
450    }
451
452    pub fn soma_voltage(&self) -> f64 {
453        match self {
454            NeuronVariant::Izhikevich(n) => n.v,
455            NeuronVariant::AdEx(n) => n.v,
456            NeuronVariant::ExpIF(n) => n.v,
457            NeuronVariant::Lapicque(n) => n.v,
458            NeuronVariant::HomeostaticLif(n) => n.v,
459            NeuronVariant::HodgkinHuxley(n) => n.v,
460            NeuronVariant::TraubMiles(n) => n.v,
461            NeuronVariant::WangBuzsaki(n) => n.v,
462            NeuronVariant::ConnorStevens(n) => n.v,
463            NeuronVariant::DestexheThalamic(n) => n.v,
464            NeuronVariant::HuberBraun(n) => n.v,
465            NeuronVariant::GolombFS(n) => n.v,
466            NeuronVariant::Pospischil(n) => n.v,
467            NeuronVariant::MainenSejnowski(n) => n.vs,
468            NeuronVariant::DeSchutterPurkinje(n) => n.v,
469            NeuronVariant::PlantR15(n) => n.v,
470            NeuronVariant::Prescott(n) => n.v,
471            NeuronVariant::MihalasNiebur(n) => n.v,
472            NeuronVariant::GLIF(n) => n.v,
473            NeuronVariant::GIFPopulation(n) => n.v,
474            NeuronVariant::AvRonCardiac(n) => n.v,
475            NeuronVariant::DurstewitzDopamine(n) => n.v,
476            NeuronVariant::HillTononi(n) => n.v,
477            NeuronVariant::BertramPhantom(n) => n.v,
478            NeuronVariant::Yamada(n) => n.v,
479            NeuronVariant::FitzHughNagumo(n) => n.v,
480            NeuronVariant::MorrisLecar(n) => n.v,
481            NeuronVariant::HindmarshRose(n) => n.x,
482            NeuronVariant::ResonateAndFire(n) => n.x,
483            NeuronVariant::BalancedResonateAndFire(n) => n.x,
484            NeuronVariant::FitzHughRinzel(n) => n.v,
485            NeuronVariant::McKean(n) => n.v,
486            NeuronVariant::TermanWang(n) => n.v,
487            NeuronVariant::GutkinErmentrout(n) => n.v,
488            NeuronVariant::WilsonHR(n) => n.v,
489            NeuronVariant::Chay(n) => n.v,
490            NeuronVariant::ChayKeizer(n) => n.v,
491            NeuronVariant::ShermanRinzelKeizer(n) => n.v,
492            NeuronVariant::ButeraRespiratory(n) => n.v,
493            NeuronVariant::EPropALIF(n) => n.v,
494            NeuronVariant::SuperSpike(n) => n.v,
495            NeuronVariant::LearnableNeuron(n) => n.v,
496            NeuronVariant::Pernarowski(n) => n.v,
497            NeuronVariant::QuadraticIF(n) => n.v,
498            NeuronVariant::Theta(n) => n.theta,
499            NeuronVariant::PerfectIntegrator(n) => n.v,
500            NeuronVariant::GatedLIF(n) => n.v,
501            NeuronVariant::NonlinearLIF(n) => n.v,
502            NeuronVariant::SFA(n) => n.v,
503            NeuronVariant::MAT(n) => n.v,
504            NeuronVariant::KLIF(n) => n.v,
505            NeuronVariant::InhibitoryLIF(n) => n.v,
506            NeuronVariant::ComplementaryLIF(n) => n.v_pos,
507            NeuronVariant::ParametricLIF(n) => n.v,
508            NeuronVariant::NonResettingLIF(n) => n.v,
509            NeuronVariant::AdaptiveThresholdIF(n) => n.v,
510            NeuronVariant::SigmaDelta(n) => n.sigma,
511            NeuronVariant::EnergyLIF(n) => n.v,
512            NeuronVariant::ClosedFormContinuous(n) => n.x,
513            NeuronVariant::ChialvoMap(n) => n.x,
514            NeuronVariant::RulkovMap(n) => n.x,
515            NeuronVariant::IbarzTanakaMap(n) => n.x,
516            NeuronVariant::MedvedevMap(n) => n.x,
517            NeuronVariant::CazellesMap(n) => n.x,
518            NeuronVariant::CourageNekorkinMap(n) => n.x,
519            NeuronVariant::AiharaMap(n) => n.x,
520            NeuronVariant::KilincBhattMap(n) => n.x,
521            NeuronVariant::ErmentroutKopellMap(n) => n.theta,
522            NeuronVariant::BrainScaleSAdEx(n) => n.v,
523            NeuronVariant::SpiNNakerLIF(n) => n.v,
524            NeuronVariant::NeuroGrid(n) => n.v_s,
525            NeuronVariant::DPI(n) => n.i_mem,
526            NeuronVariant::MarderSTG(n) => n.v,
527            NeuronVariant::RallCable(n) => n.v.first().copied().unwrap_or(0.0),
528            NeuronVariant::BoothRinzel(n) => n.vs,
529            NeuronVariant::Dendrify(n) => n.v_s,
530            NeuronVariant::LiquidTimeConstant(n) => n.x,
531            NeuronVariant::ParallelSpiking(_) => 0.0,
532            NeuronVariant::FractionalLIF(n) => n.v,
533            NeuronVariant::StochasticIF(n) => n.v,
534            NeuronVariant::GalvesLocherbach(n) => n.v,
535            NeuronVariant::SpikeResponse(n) => n.v,
536            NeuronVariant::GLM(n) => n.mu,
537            NeuronVariant::Arcane(n) => n.v_fast,
538            // newly wired — default voltage field
539            NeuronVariant::MultiTimescale(n) => n.v_fast,
540            NeuronVariant::AttentionGated(n) => n.v,
541            NeuronVariant::PredictiveCoding(n) => n.v,
542            NeuronVariant::SelfReferential(n) => n.v,
543            NeuronVariant::CompositionalBinding(_) => 0.0,
544            NeuronVariant::DifferentiableSurrogate(n) => n.v,
545            NeuronVariant::ContinuousAttractor(_) => 0.0,
546            NeuronVariant::MetaPlastic(n) => n.v,
547            NeuronVariant::BendaHerz(n) => n.a,
548            NeuronVariant::BrunelWang(n) => n.v,
549            NeuronVariant::Poisson(_) => 0.0,
550            NeuronVariant::InhomogeneousPoisson(_) => 0.0,
551            NeuronVariant::GammaRenewal(_) => 0.0,
552            NeuronVariant::EscapeRate(n) => n.v,
553            NeuronVariant::Akida(n) => n.v as f64,
554            NeuronVariant::StochasticLIF(n) => n.v,
555            // interneurons
556            NeuronVariant::PVFastSpiking(n) => n.v,
557            NeuronVariant::SST(n) => n.v,
558            NeuronVariant::VIP(n) => n.v,
559            NeuronVariant::Chandelier(n) => n.v,
560            NeuronVariant::CerebellarBasket(n) => n.v,
561            NeuronVariant::Martinotti(n) => n.v,
562            // motor
563            NeuronVariant::AlphaMotor(n) => n.v,
564            NeuronVariant::GammaMotor(n) => n.v,
565            NeuronVariant::UpperMotor(n) => n.v,
566            NeuronVariant::Renshaw(n) => n.v,
567            NeuronVariant::MotorUnitCell(n) => n.v,
568            // sensory (spiking)
569            NeuronVariant::RetinalGanglion(n) => n.baseline, // GLM: no membrane V
570            NeuronVariant::Merkel(n) => n.v,
571            NeuronVariant::Pacinian(n) => n.v,
572            NeuronVariant::NociceptorCell(n) => n.v,
573            NeuronVariant::OlfactoryReceptor(n) => n.v,
574            // cerebellar
575            NeuronVariant::Granule(n) => n.v,
576            NeuronVariant::Golgi(n) => n.v,
577            NeuronVariant::Stellate(n) => n.v,
578            NeuronVariant::Lugaro(n) => n.v,
579            NeuronVariant::UnipolarBrush(n) => n.v,
580            NeuronVariant::DCN(n) => n.v,
581            // channels
582            NeuronVariant::PersistentNa(n) => n.v,
583            NeuronVariant::Ih(n) => n.v,
584            NeuronVariant::TTypeCa(n) => n.v,
585            NeuronVariant::ATypeK(n) => n.v,
586            NeuronVariant::BK(n) => n.v,
587            NeuronVariant::SK(n) => n.v,
588            NeuronVariant::NMDA(n) => n.v,
589            // population
590            NeuronVariant::MontbrioMPR(n) => n.v,
591            NeuronVariant::Brunel(n) => n.r_e,
592            NeuronVariant::TUM(n) => n.r,
593            NeuronVariant::ElBoustani(n) => n.r_e,
594            // misc
595            NeuronVariant::GradedSynapse(n) => n.v,
596            NeuronVariant::GapJunction(n) => n.v,
597            NeuronVariant::FHAxon(n) => n.v,
598            NeuronVariant::NodeOfRanvier(n) => n.v,
599            NeuronVariant::MyelinAxon(n) => n.v(),
600            NeuronVariant::CardiacPurkinje(n) => n.v,
601            NeuronVariant::SmoothMuscle(n) => n.v,
602            NeuronVariant::BetaCell(n) => n.v,
603            // Wrapped models — voltage via wrapper .v()
604            NeuronVariant::WrAlphaCell(n) => n.v(),
605            NeuronVariant::WrCOBALIFCell(n) => n.v(),
606            NeuronVariant::WrCompteWMCell(n) => n.v(),
607            NeuronVariant::WrTsodyksMarkramCell(n) => n.v(),
608            NeuronVariant::WrPinskyRinzelCell(n) => n.v(),
609            NeuronVariant::WrHayL5Cell(n) => n.v(),
610            NeuronVariant::WrTwoCompLIFCell(n) => n.v(),
611            NeuronVariant::WrLoihiCUBACell(n) => n.v(),
612            NeuronVariant::WrLoihi2Cell(n) => n.v(),
613            NeuronVariant::WrSpiNNaker2Cell(n) => n.v(),
614            NeuronVariant::WrTrueNorthCell(n) => n.v(),
615            NeuronVariant::WrIntegerQIFCell(n) => n.v(),
616            NeuronVariant::WrSigmoidRateCell(n) => n.v(),
617            NeuronVariant::WrThresholdLinearCell(n) => n.v(),
618            NeuronVariant::WrAstrocyteCell(n) => n.v(),
619            NeuronVariant::WrInnerHairCellCell(n) => n.v(),
620            NeuronVariant::WrOuterHairCellCell(n) => n.v(),
621            NeuronVariant::WrRodPhotoreceptorCell(n) => n.v(),
622            NeuronVariant::WrConePhotoreceptorCell(n) => n.v(),
623            NeuronVariant::WrTasteReceptorCell(n) => n.v(),
624        }
625    }
626}
627
628// ── PopulationRunner ────────────────────────────────────────────────
629
630pub struct PopulationRunner {
631    neurons: Vec<NeuronVariant>,
632    spikes: Vec<u8>,
633    currents: Vec<f64>,
634}
635
636const CHUNK_SIZE: usize = 256;
637
638impl PopulationRunner {
639    pub fn new(neurons: Vec<NeuronVariant>) -> Self {
640        let n = neurons.len();
641        Self {
642            neurons,
643            spikes: vec![0u8; n],
644            currents: vec![0.0; n],
645        }
646    }
647
648    pub fn len(&self) -> usize {
649        self.neurons.len()
650    }
651
652    pub fn is_empty(&self) -> bool {
653        self.neurons.is_empty()
654    }
655
656    pub fn step_all(&mut self) {
657        let neurons = &mut self.neurons;
658        let spikes = &mut self.spikes;
659        let currents = &self.currents;
660
661        neurons
662            .par_chunks_mut(CHUNK_SIZE)
663            .zip(spikes.par_chunks_mut(CHUNK_SIZE))
664            .zip(currents.par_chunks(CHUNK_SIZE))
665            .for_each(|((n_chunk, s_chunk), c_chunk)| {
666                for i in 0..n_chunk.len() {
667                    let fired = n_chunk[i].step(c_chunk[i]);
668                    s_chunk[i] = if fired != 0 { 1 } else { 0 };
669                }
670            });
671    }
672
673    pub fn reset_all(&mut self) {
674        for n in &mut self.neurons {
675            n.reset();
676        }
677        self.spikes.fill(0);
678        self.currents.fill(0.0);
679    }
680
681    pub fn reset_currents(&mut self) {
682        self.currents.fill(0.0);
683    }
684
685    pub fn set_currents(&mut self, currents: &[f64]) -> Result<(), String> {
686        if currents.len() != self.currents.len() {
687            return Err(format!(
688                "current vector length mismatch: got {}, expected {}",
689                currents.len(),
690                self.currents.len()
691            ));
692        }
693        self.currents.copy_from_slice(currents);
694        Ok(())
695    }
696
697    pub fn collect_voltages(&self) -> Vec<f64> {
698        self.neurons.iter().map(|n| n.soma_voltage()).collect()
699    }
700}
701
702// ── ProjectionRunner ────────────────────────────────────────────────
703
704/// CSR-stored synaptic projection with optional axonal delay.
705pub struct ProjectionRunner {
706    pub src_pop: usize,
707    pub tgt_pop: usize,
708    row_offsets: Vec<usize>,
709    col_indices: Vec<usize>,
710    values: Vec<f64>,
711    delay_steps: usize,
712    delay_buffer: Vec<Vec<u8>>,
713    buf_idx: usize,
714}
715
716impl ProjectionRunner {
717    pub fn new(
718        src_pop: usize,
719        tgt_pop: usize,
720        row_offsets: Vec<usize>,
721        col_indices: Vec<usize>,
722        values: Vec<f64>,
723        delay_steps: usize,
724    ) -> Self {
725        let n_delay = if delay_steps > 0 { delay_steps } else { 0 };
726        let n_src = if row_offsets.is_empty() {
727            0
728        } else {
729            row_offsets.len() - 1
730        };
731        let delay_buffer = if n_delay > 0 {
732            vec![vec![0u8; n_src]; n_delay]
733        } else {
734            Vec::new()
735        };
736        Self {
737            src_pop,
738            tgt_pop,
739            row_offsets,
740            col_indices,
741            values,
742            delay_steps: n_delay,
743            delay_buffer,
744            buf_idx: 0,
745        }
746    }
747
748    /// Scatter spikes through CSR connectivity into target current buffer.
749    pub fn propagate(&mut self, src_spikes: &[u8], tgt_currents: &mut [f64]) {
750        let spikes = if self.delay_steps > 0 {
751            let delayed = &self.delay_buffer[self.buf_idx];
752            let out: Vec<u8> = delayed.clone();
753            self.delay_buffer[self.buf_idx] = src_spikes.to_vec();
754            self.buf_idx = (self.buf_idx + 1) % self.delay_steps;
755            out
756        } else {
757            src_spikes.to_vec()
758        };
759
760        let n_src = self.row_offsets.len().saturating_sub(1);
761        for i in 0..n_src {
762            if spikes.get(i).copied().unwrap_or(0) == 0 {
763                continue;
764            }
765            let start = self.row_offsets[i];
766            let end = self.row_offsets[i + 1];
767            for k in start..end {
768                let j = self.col_indices[k];
769                if j < tgt_currents.len() {
770                    tgt_currents[j] += self.values[k];
771                }
772            }
773        }
774    }
775}
776
777// ── SimResults ──────────────────────────────────────────────────────
778
779pub struct SimResults {
780    pub spike_counts: Vec<usize>,
781    /// Per-population flat spike data: (neuron_id << 32) | timestep packed as u64.
782    /// Supports up to 2^32 neurons and 2^32 timesteps.
783    pub spike_data: Vec<Vec<u64>>,
784    pub voltages: Vec<Vec<f64>>,
785}
786
787// ── NetworkRunner ───────────────────────────────────────────────────
788
789pub struct NetworkRunner {
790    pub populations: Vec<PopulationRunner>,
791    pub projections: Vec<ProjectionRunner>,
792}
793
794impl NetworkRunner {
795    pub fn new() -> Self {
796        Self {
797            populations: Vec::new(),
798            projections: Vec::new(),
799        }
800    }
801
802    pub fn add_population(&mut self, pop: PopulationRunner) -> usize {
803        let idx = self.populations.len();
804        self.populations.push(pop);
805        idx
806    }
807
808    pub fn add_projection(&mut self, proj: ProjectionRunner) {
809        self.projections.push(proj);
810    }
811
812    pub fn step_population_with_currents(
813        &mut self,
814        pop_idx: usize,
815        currents: &[f64],
816    ) -> Result<(Vec<u8>, Vec<f64>), String> {
817        let pop = self
818            .populations
819            .get_mut(pop_idx)
820            .ok_or_else(|| format!("population index {pop_idx} out of range"))?;
821        pop.set_currents(currents)?;
822        pop.step_all();
823        Ok((pop.spikes.clone(), pop.collect_voltages()))
824    }
825
826    pub fn run(&mut self, n_steps: usize) -> SimResults {
827        let n_pops = self.populations.len();
828        let mut spike_counts = vec![0usize; n_pops];
829        let mut spike_data: Vec<Vec<u64>> = vec![Vec::new(); n_pops];
830
831        for t in 0..n_steps {
832            // Reset currents
833            for pop in &mut self.populations {
834                pop.reset_currents();
835            }
836
837            // Propagate spikes through projections
838            // Must borrow populations mutably for target currents, immutably for source spikes.
839            // Use index-based access to avoid aliasing issues.
840            for proj_idx in 0..self.projections.len() {
841                let src = self.projections[proj_idx].src_pop;
842                let tgt = self.projections[proj_idx].tgt_pop;
843                if src == tgt {
844                    // Self-projection: copy spikes, then propagate
845                    let spikes_copy = self.populations[src].spikes.clone();
846                    let currents = &mut self.populations[tgt].currents;
847                    self.projections[proj_idx].propagate(&spikes_copy, currents);
848                } else {
849                    // Split borrow via raw pointers (safe because src != tgt)
850                    let pops_ptr = self.populations.as_mut_ptr();
851                    let src_spikes = unsafe { &(*pops_ptr.add(src)).spikes };
852                    let tgt_currents = unsafe { &mut (*pops_ptr.add(tgt)).currents };
853                    self.projections[proj_idx].propagate(src_spikes, tgt_currents);
854                }
855            }
856
857            // Step all populations
858            for (pop_idx, pop) in self.populations.iter_mut().enumerate() {
859                pop.step_all();
860                for (nid, &spike) in pop.spikes.iter().enumerate() {
861                    if spike != 0 {
862                        spike_counts[pop_idx] += 1;
863                        spike_data[pop_idx].push(((nid as u64) << 32) | (t as u64));
864                    }
865                }
866            }
867        }
868
869        let voltages: Vec<Vec<f64>> = self
870            .populations
871            .iter()
872            .map(|p| p.collect_voltages())
873            .collect();
874
875        SimResults {
876            spike_counts,
877            spike_data,
878            voltages,
879        }
880    }
881}
882
883impl Default for NetworkRunner {
884    fn default() -> Self {
885        Self::new()
886    }
887}
888
889// ── Factory ─────────────────────────────────────────────────────────
890
891/// Create a population of `n` identical neurons by model name.
892pub fn create_population(model_name: &str, n: usize) -> Result<PopulationRunner, String> {
893    let neurons: Vec<NeuronVariant> = (0..n)
894        .map(|_| create_neuron(model_name))
895        .collect::<Result<_, _>>()?;
896    Ok(PopulationRunner::new(neurons))
897}
898
899pub fn create_neuron(name: &str) -> Result<NeuronVariant, String> {
900    match name {
901        "Izhikevich" => Ok(NeuronVariant::Izhikevich(Izhikevich::regular_spiking())),
902        "AdEx" | "AdExNeuron" => Ok(NeuronVariant::AdEx(AdExNeuron::new())),
903        "ExpIF" | "ExpIfNeuron" => Ok(NeuronVariant::ExpIF(ExpIfNeuron::new())),
904        "Lapicque" | "LapicqueNeuron" => Ok(NeuronVariant::Lapicque(LapicqueNeuron::new(
905            20.0, 1.0, 1.0, 1.0,
906        ))),
907        "HomeostaticLif" => Ok(NeuronVariant::HomeostaticLif(
908            HomeostaticLif::with_defaults(),
909        )),
910        "HodgkinHuxley" | "HodgkinHuxleyNeuron" => {
911            Ok(NeuronVariant::HodgkinHuxley(HodgkinHuxleyNeuron::new()))
912        }
913        "TraubMiles" | "TraubMilesNeuron" => Ok(NeuronVariant::TraubMiles(TraubMilesNeuron::new())),
914        "WangBuzsaki" | "WangBuzsakiNeuron" => {
915            Ok(NeuronVariant::WangBuzsaki(WangBuzsakiNeuron::new()))
916        }
917        "ConnorStevens" | "ConnorStevensNeuron" => {
918            Ok(NeuronVariant::ConnorStevens(ConnorStevensNeuron::new()))
919        }
920        "DestexheThalamic" | "DestexheThalamicNeuron" => Ok(NeuronVariant::DestexheThalamic(
921            DestexheThalamicNeuron::new(),
922        )),
923        "HuberBraun" | "HuberBraunNeuron" => Ok(NeuronVariant::HuberBraun(HuberBraunNeuron::new())),
924        "GolombFS" | "GolombFSNeuron" => Ok(NeuronVariant::GolombFS(GolombFSNeuron::new())),
925        "Pospischil" | "PospischilNeuron" => Ok(NeuronVariant::Pospischil(PospischilNeuron::new())),
926        "MainenSejnowski" | "MainenSejnowskiNeuron" => {
927            Ok(NeuronVariant::MainenSejnowski(MainenSejnowskiNeuron::new()))
928        }
929        "DeSchutterPurkinje" | "DeSchutterPurkinjeNeuron" => Ok(NeuronVariant::DeSchutterPurkinje(
930            DeSchutterPurkinjeNeuron::new(),
931        )),
932        "PlantR15" | "PlantR15Neuron" => Ok(NeuronVariant::PlantR15(PlantR15Neuron::new())),
933        "Prescott" | "PrescottNeuron" => Ok(NeuronVariant::Prescott(PrescottNeuron::new())),
934        "MihalasNiebur" | "MihalasNieburNeuron" => {
935            Ok(NeuronVariant::MihalasNiebur(MihalasNieburNeuron::new()))
936        }
937        "GLIF" | "GLIFNeuron" => Ok(NeuronVariant::GLIF(GLIFNeuron::new())),
938        "GIFPopulation" | "GIFPopulationNeuron" => {
939            Ok(NeuronVariant::GIFPopulation(GIFPopulationNeuron::new(42)))
940        }
941        "AvRonCardiac" | "AvRonCardiacNeuron" => {
942            Ok(NeuronVariant::AvRonCardiac(AvRonCardiacNeuron::new()))
943        }
944        "DurstewitzDopamine" | "DurstewitzDopamineNeuron" => Ok(NeuronVariant::DurstewitzDopamine(
945            DurstewitzDopamineNeuron::new(),
946        )),
947        "HillTononi" | "HillTononiNeuron" => Ok(NeuronVariant::HillTononi(HillTononiNeuron::new())),
948        "BertramPhantom" | "BertramPhantomBurster" => {
949            Ok(NeuronVariant::BertramPhantom(BertramPhantomBurster::new()))
950        }
951        "Yamada" | "YamadaNeuron" => Ok(NeuronVariant::Yamada(YamadaNeuron::new())),
952        "FitzHughNagumo" | "FitzHughNagumoNeuron" => {
953            Ok(NeuronVariant::FitzHughNagumo(FitzHughNagumoNeuron::new()))
954        }
955        "MorrisLecar" | "MorrisLecarNeuron" => {
956            Ok(NeuronVariant::MorrisLecar(MorrisLecarNeuron::new()))
957        }
958        "HindmarshRose" | "HindmarshRoseNeuron" => {
959            Ok(NeuronVariant::HindmarshRose(HindmarshRoseNeuron::new()))
960        }
961        "ResonateAndFire" | "ResonateAndFireNeuron" => {
962            Ok(NeuronVariant::ResonateAndFire(ResonateAndFireNeuron::new()))
963        }
964        "BalancedResonateAndFire" | "BalancedResonateAndFireNeuron" => Ok(
965            NeuronVariant::BalancedResonateAndFire(BalancedResonateAndFireNeuron::new()),
966        ),
967        "FitzHughRinzel" | "FitzHughRinzelNeuron" => {
968            Ok(NeuronVariant::FitzHughRinzel(FitzHughRinzelNeuron::new()))
969        }
970        "McKean" | "McKeanNeuron" => Ok(NeuronVariant::McKean(McKeanNeuron::new())),
971        "TermanWang" | "TermanWangOscillator" => {
972            Ok(NeuronVariant::TermanWang(TermanWangOscillator::new()))
973        }
974        "GutkinErmentrout" | "GutkinErmentroutNeuron" => Ok(NeuronVariant::GutkinErmentrout(
975            GutkinErmentroutNeuron::new(),
976        )),
977        "WilsonHR" | "WilsonHRNeuron" => Ok(NeuronVariant::WilsonHR(WilsonHRNeuron::new())),
978        "Chay" | "ChayNeuron" => Ok(NeuronVariant::Chay(ChayNeuron::new())),
979        "ChayKeizer" | "ChayKeizerNeuron" => Ok(NeuronVariant::ChayKeizer(ChayKeizerNeuron::new())),
980        "ShermanRinzelKeizer" | "ShermanRinzelKeizerNeuron" => Ok(
981            NeuronVariant::ShermanRinzelKeizer(ShermanRinzelKeizerNeuron::new()),
982        ),
983        "ButeraRespiratory" | "ButeraRespiratoryNeuron" => Ok(NeuronVariant::ButeraRespiratory(
984            ButeraRespiratoryNeuron::new(),
985        )),
986        "EPropALIF" | "EPropALIFNeuron" => Ok(NeuronVariant::EPropALIF(EPropALIFNeuron::default())),
987        "SuperSpike" | "SuperSpikeNeuron" => {
988            Ok(NeuronVariant::SuperSpike(SuperSpikeNeuron::default()))
989        }
990        "LearnableNeuron" | "LearnableNeuronModel" => {
991            Ok(NeuronVariant::LearnableNeuron(LearnableNeuronModel::new()))
992        }
993        "Pernarowski" | "PernarowskiNeuron" => {
994            Ok(NeuronVariant::Pernarowski(PernarowskiNeuron::new()))
995        }
996        "QuadraticIF" | "QuadraticIFNeuron" => {
997            Ok(NeuronVariant::QuadraticIF(QuadraticIFNeuron::default()))
998        }
999        "Theta" | "ThetaNeuron" => Ok(NeuronVariant::Theta(ThetaNeuron::default())),
1000        "PerfectIntegrator" | "PerfectIntegratorNeuron" => Ok(NeuronVariant::PerfectIntegrator(
1001            PerfectIntegratorNeuron::default(),
1002        )),
1003        "GatedLIF" | "GatedLIFNeuron" => Ok(NeuronVariant::GatedLIF(GatedLIFNeuron::default())),
1004        "NonlinearLIF" | "NonlinearLIFNeuron" => {
1005            Ok(NeuronVariant::NonlinearLIF(NonlinearLIFNeuron::new()))
1006        }
1007        "SFA" | "SFANeuron" => Ok(NeuronVariant::SFA(SFANeuron::new())),
1008        "MAT" | "MATNeuron" => Ok(NeuronVariant::MAT(MATNeuron::new())),
1009        "KLIF" | "KLIFNeuron" => Ok(NeuronVariant::KLIF(KLIFNeuron::default())),
1010        "InhibitoryLIF" | "InhibitoryLIFNeuron" => {
1011            Ok(NeuronVariant::InhibitoryLIF(InhibitoryLIFNeuron::default()))
1012        }
1013        "ComplementaryLIF" | "ComplementaryLIFNeuron" => Ok(NeuronVariant::ComplementaryLIF(
1014            ComplementaryLIFNeuron::default(),
1015        )),
1016        "ParametricLIF" | "ParametricLIFNeuron" => {
1017            Ok(NeuronVariant::ParametricLIF(ParametricLIFNeuron::default()))
1018        }
1019        "NonResettingLIF" | "NonResettingLIFNeuron" => {
1020            Ok(NeuronVariant::NonResettingLIF(NonResettingLIFNeuron::new()))
1021        }
1022        "AdaptiveThresholdIF" | "AdaptiveThresholdIFNeuron" => Ok(
1023            NeuronVariant::AdaptiveThresholdIF(AdaptiveThresholdIFNeuron::new()),
1024        ),
1025        "SigmaDelta" | "SigmaDeltaNeuron" => {
1026            Ok(NeuronVariant::SigmaDelta(SigmaDeltaNeuron::default()))
1027        }
1028        "EnergyLIF" | "EnergyLIFNeuron" => Ok(NeuronVariant::EnergyLIF(EnergyLIFNeuron::new())),
1029        "ClosedFormContinuous" | "ClosedFormContinuousNeuron" => Ok(
1030            NeuronVariant::ClosedFormContinuous(ClosedFormContinuousNeuron::new()),
1031        ),
1032        "ChialvoMap" | "ChialvoMapNeuron" => Ok(NeuronVariant::ChialvoMap(ChialvoMapNeuron::new())),
1033        "RulkovMap" | "RulkovMapNeuron" => Ok(NeuronVariant::RulkovMap(RulkovMapNeuron::new())),
1034        "IbarzTanakaMap" | "IbarzTanakaMapNeuron" => {
1035            Ok(NeuronVariant::IbarzTanakaMap(IbarzTanakaMapNeuron::new()))
1036        }
1037        "MedvedevMap" | "MedvedevMapNeuron" => {
1038            Ok(NeuronVariant::MedvedevMap(MedvedevMapNeuron::default()))
1039        }
1040        "CazellesMap" | "CazellesMapNeuron" => {
1041            Ok(NeuronVariant::CazellesMap(CazellesMapNeuron::new()))
1042        }
1043        "CourageNekorkinMap" | "CourageNekorkinMapNeuron" => Ok(NeuronVariant::CourageNekorkinMap(
1044            CourageNekorkinMapNeuron::new(),
1045        )),
1046        "AiharaMap" | "AiharaMapNeuron" => Ok(NeuronVariant::AiharaMap(AiharaMapNeuron::new())),
1047        "KilincBhattMap" | "KilincBhattMapNeuron" => {
1048            Ok(NeuronVariant::KilincBhattMap(KilincBhattMapNeuron::new()))
1049        }
1050        "ErmentroutKopellMap" | "ErmentroutKopellMapNeuron" => Ok(
1051            NeuronVariant::ErmentroutKopellMap(ErmentroutKopellMapNeuron::new()),
1052        ),
1053        "BrainScaleSAdEx" | "BrainScaleSAdExNeuron" => {
1054            Ok(NeuronVariant::BrainScaleSAdEx(BrainScaleSAdExNeuron::new()))
1055        }
1056        "SpiNNakerLIF" | "SpiNNakerLIFNeuron" => {
1057            Ok(NeuronVariant::SpiNNakerLIF(SpiNNakerLIFNeuron::new()))
1058        }
1059        "NeuroGrid" | "NeuroGridNeuron" => Ok(NeuronVariant::NeuroGrid(NeuroGridNeuron::new())),
1060        "DPI" | "DPINeuron" => Ok(NeuronVariant::DPI(DPINeuron::new())),
1061        "MarderSTG" | "MarderSTGNeuron" => Ok(NeuronVariant::MarderSTG(MarderSTGNeuron::new())),
1062        "RallCable" | "RallCableNeuron" => Ok(NeuronVariant::RallCable(RallCableNeuron::new(5))),
1063        "BoothRinzel" | "BoothRinzelNeuron" => {
1064            Ok(NeuronVariant::BoothRinzel(BoothRinzelNeuron::new()))
1065        }
1066        "Dendrify" | "DendrifyNeuron" => Ok(NeuronVariant::Dendrify(DendrifyNeuron::new())),
1067        "Akida" | "AkidaNeuron" => Ok(NeuronVariant::Akida(AkidaNeuron::new(100))),
1068        "StochasticLIF" | "StochasticLIFNeuron" => {
1069            Ok(NeuronVariant::StochasticLIF(StochasticLIFNeuron::new(42)))
1070        }
1071        "LiquidTimeConstant" | "LiquidTimeConstantNeuron" => Ok(NeuronVariant::LiquidTimeConstant(
1072            LiquidTimeConstantNeuron::new(),
1073        )),
1074        "ParallelSpiking" | "ParallelSpikingNeuron" => Ok(NeuronVariant::ParallelSpiking(
1075            ParallelSpikingNeuron::new(4, 0.5),
1076        )),
1077        "FractionalLIF" | "FractionalLIFNeuron" => Ok(NeuronVariant::FractionalLIF(
1078            FractionalLIFNeuron::new(0.8, 50),
1079        )),
1080        "StochasticIF" | "StochasticIFNeuron" => {
1081            Ok(NeuronVariant::StochasticIF(StochasticIFNeuron::new(42)))
1082        }
1083        "GalvesLocherbach" | "GalvesLocherbachNeuron" => Ok(NeuronVariant::GalvesLocherbach(
1084            GalvesLocherbachNeuron::new(42),
1085        )),
1086        "SpikeResponse" | "SpikeResponseNeuron" => {
1087            Ok(NeuronVariant::SpikeResponse(SpikeResponseNeuron::new()))
1088        }
1089        "GLM" | "GLMNeuron" => Ok(NeuronVariant::GLM(GLMNeuron::new(5, 10, 42))),
1090        "Arcane" | "ArcaneNeuron" => Ok(NeuronVariant::Arcane(ArcaneNeuron::new())),
1091        // newly wired
1092        "MultiTimescale" | "MultiTimescaleNeuron" => {
1093            Ok(NeuronVariant::MultiTimescale(MultiTimescaleNeuron::new()))
1094        }
1095        "AttentionGated" | "AttentionGatedNeuron" => {
1096            Ok(NeuronVariant::AttentionGated(AttentionGatedNeuron::new()))
1097        }
1098        "PredictiveCoding" | "PredictiveCodingNeuron" => Ok(NeuronVariant::PredictiveCoding(
1099            PredictiveCodingNeuron::new(),
1100        )),
1101        "SelfReferential" | "SelfReferentialNeuron" => {
1102            Ok(NeuronVariant::SelfReferential(SelfReferentialNeuron::new()))
1103        }
1104        "CompositionalBinding" | "CompositionalBindingNeuron" => Ok(
1105            NeuronVariant::CompositionalBinding(CompositionalBindingNeuron::new()),
1106        ),
1107        "DifferentiableSurrogate" | "DifferentiableSurrogateNeuron" => Ok(
1108            NeuronVariant::DifferentiableSurrogate(DifferentiableSurrogateNeuron::new()),
1109        ),
1110        "ContinuousAttractor" | "ContinuousAttractorNeuron" => Ok(
1111            NeuronVariant::ContinuousAttractor(ContinuousAttractorNeuron::new(8)),
1112        ),
1113        "MetaPlastic" | "MetaPlasticNeuron" => {
1114            Ok(NeuronVariant::MetaPlastic(MetaPlasticNeuron::new()))
1115        }
1116        "BendaHerz" | "BendaHerzNeuron" => Ok(NeuronVariant::BendaHerz(BendaHerzNeuron::new(42))),
1117        "BrunelWang" | "BrunelWangNeuron" => Ok(NeuronVariant::BrunelWang(BrunelWangNeuron::new())),
1118        "Poisson" | "PoissonNeuron" => {
1119            Ok(NeuronVariant::Poisson(PoissonNeuron::new(50.0, 1.0, 42)))
1120        }
1121        "InhomogeneousPoisson" | "InhomogeneousPoissonNeuron" => Ok(
1122            NeuronVariant::InhomogeneousPoisson(InhomogeneousPoissonNeuron::new(1.0, 42)),
1123        ),
1124        "GammaRenewal" | "GammaRenewalNeuron" => Ok(NeuronVariant::GammaRenewal(
1125            GammaRenewalNeuron::new(50.0, 3, 42),
1126        )),
1127        "EscapeRate" | "EscapeRateNeuron" => {
1128            Ok(NeuronVariant::EscapeRate(EscapeRateNeuron::new(42)))
1129        }
1130        // interneurons
1131        "PVFastSpiking" | "PVFastSpikingNeuron" => {
1132            Ok(NeuronVariant::PVFastSpiking(PVFastSpikingNeuron::new()))
1133        }
1134        "SST" | "SSTNeuron" => Ok(NeuronVariant::SST(SSTNeuron::new())),
1135        "VIP" | "VIPNeuron" => Ok(NeuronVariant::VIP(VIPNeuron::new())),
1136        "Chandelier" | "ChandelierNeuron" => Ok(NeuronVariant::Chandelier(ChandelierNeuron::new())),
1137        "CerebellarBasket" | "CerebellarBasketNeuron" => Ok(NeuronVariant::CerebellarBasket(
1138            CerebellarBasketNeuron::new(),
1139        )),
1140        "Martinotti" | "MartinottiNeuron" => Ok(NeuronVariant::Martinotti(MartinottiNeuron::new())),
1141        // motor
1142        "AlphaMotor" | "AlphaMotorNeuron" => Ok(NeuronVariant::AlphaMotor(AlphaMotorNeuron::new())),
1143        "GammaMotor" | "GammaMotorNeuron" => Ok(NeuronVariant::GammaMotor(GammaMotorNeuron::new())),
1144        "UpperMotor" | "UpperMotorNeuron" => Ok(NeuronVariant::UpperMotor(UpperMotorNeuron::new())),
1145        "Renshaw" | "RenshawCell" => Ok(NeuronVariant::Renshaw(RenshawCell::new())),
1146        "MotorUnit" => Ok(NeuronVariant::MotorUnitCell(MotorUnit::new())),
1147        // sensory (spiking)
1148        "RetinalGanglion" | "RetinalGanglionCell" => {
1149            Ok(NeuronVariant::RetinalGanglion(RetinalGanglionCell::new()))
1150        }
1151        "Merkel" | "MerkelCell" => Ok(NeuronVariant::Merkel(MerkelCell::new())),
1152        "Pacinian" | "PacinianCorpuscle" => Ok(NeuronVariant::Pacinian(PacinianCorpuscle::new())),
1153        "Nociceptor" => Ok(NeuronVariant::NociceptorCell(Nociceptor::new())),
1154        "OlfactoryReceptor" | "OlfactoryReceptorNeuron" => Ok(NeuronVariant::OlfactoryReceptor(
1155            OlfactoryReceptorNeuron::new(),
1156        )),
1157        // cerebellar
1158        "GranuleCell" | "Granule" => Ok(NeuronVariant::Granule(GranuleCell::new())),
1159        "GolgiCell" | "Golgi" => Ok(NeuronVariant::Golgi(GolgiCell::new())),
1160        "StellateCell" | "Stellate" => Ok(NeuronVariant::Stellate(StellateCell::new())),
1161        "LugaroCell" | "Lugaro" => Ok(NeuronVariant::Lugaro(LugaroCell::new())),
1162        "UnipolarBrushCell" | "UBC" => Ok(NeuronVariant::UnipolarBrush(UnipolarBrushCell::new())),
1163        "DCNNeuron" | "DCN" => Ok(NeuronVariant::DCN(DCNNeuron::new())),
1164        // channels
1165        "PersistentNa" | "PersistentNaNeuron" => {
1166            Ok(NeuronVariant::PersistentNa(PersistentNaNeuron::new()))
1167        }
1168        "Ih" | "IhNeuron" => Ok(NeuronVariant::Ih(IhNeuron::new())),
1169        "TTypeCa" | "TTypeCaNeuron" => Ok(NeuronVariant::TTypeCa(TTypeCaNeuron::new())),
1170        "ATypeK" | "ATypeKNeuron" => Ok(NeuronVariant::ATypeK(ATypeKNeuron::new())),
1171        "BK" | "BKNeuron" => Ok(NeuronVariant::BK(BKNeuron::new())),
1172        "SK" | "SKNeuron" => Ok(NeuronVariant::SK(SKNeuron::new())),
1173        "NMDA" | "NMDANeuron" => Ok(NeuronVariant::NMDA(NMDANeuron::new())),
1174        // population
1175        "MontbrioMeanField" | "MPR" => Ok(NeuronVariant::MontbrioMPR(MontbrioMeanField::new())),
1176        "BrunelNetwork" | "Brunel" => Ok(NeuronVariant::Brunel(BrunelNetwork::new())),
1177        "TUMNetwork" | "TUM" => Ok(NeuronVariant::TUM(TUMNetwork::new())),
1178        "ElBoustaniNetwork" | "ElBoustani" => {
1179            Ok(NeuronVariant::ElBoustani(ElBoustaniNetwork::new()))
1180        }
1181        // misc
1182        "GradedSynapseNeuron" | "GradedSynapse" => {
1183            Ok(NeuronVariant::GradedSynapse(GradedSynapseNeuron::new()))
1184        }
1185        "GapJunctionNeuron" | "GapJunction" => {
1186            Ok(NeuronVariant::GapJunction(GapJunctionNeuron::new()))
1187        }
1188        "FrankenhaeUserHuxleyAxon" | "FHAxon" => {
1189            Ok(NeuronVariant::FHAxon(FrankenhaeUserHuxleyAxon::new()))
1190        }
1191        "NodeOfRanvier" => Ok(NeuronVariant::NodeOfRanvier(NodeOfRanvier::new())),
1192        "MyelinatedAxon" | "MyelinAxon" => Ok(NeuronVariant::MyelinAxon(MyelinatedAxon::new())),
1193        "CardiacPurkinjeFibre" | "CardiacPurkinje" => {
1194            Ok(NeuronVariant::CardiacPurkinje(CardiacPurkinjeFibre::new()))
1195        }
1196        "SmoothMuscleCell" | "SmoothMuscle" => {
1197            Ok(NeuronVariant::SmoothMuscle(SmoothMuscleCell::new()))
1198        }
1199        "EndocrineBetaCell" | "BetaCell" => Ok(NeuronVariant::BetaCell(EndocrineBetaCell::new())),
1200        // Wrapped multi-input spiking
1201        "AlphaNeuron" | "Alpha" => Ok(NeuronVariant::WrAlphaCell(WrAlpha::new())),
1202        "COBALIFNeuron" | "COBALIF" => Ok(NeuronVariant::WrCOBALIFCell(WrCOBALIF::new())),
1203        "CompteWMNeuron" | "CompteWM" => Ok(NeuronVariant::WrCompteWMCell(WrCompteWM::new())),
1204        "TsodyksMarkramNeuron" | "TsodyksMarkram" => {
1205            Ok(NeuronVariant::WrTsodyksMarkramCell(WrTsodyksMarkram::new()))
1206        }
1207        "PinskyRinzelNeuron" | "PinskyRinzel" => {
1208            Ok(NeuronVariant::WrPinskyRinzelCell(WrPinskyRinzel::new()))
1209        }
1210        "HayL5PyramidalNeuron" | "HayL5" => Ok(NeuronVariant::WrHayL5Cell(WrHayL5::new())),
1211        "TwoCompartmentLIFNeuron" | "TwoCompLIF" => {
1212            Ok(NeuronVariant::WrTwoCompLIFCell(WrTwoCompLIF::new()))
1213        }
1214        // Wrapped hardware integer-input
1215        "LoihiCUBANeuron" | "LoihiCUBA" => Ok(NeuronVariant::WrLoihiCUBACell(WrLoihiCUBA::new())),
1216        "Loihi2Neuron" | "Loihi2" => Ok(NeuronVariant::WrLoihi2Cell(WrLoihi2::new())),
1217        "SpiNNaker2Neuron" | "SpiNNaker2" => {
1218            Ok(NeuronVariant::WrSpiNNaker2Cell(WrSpiNNaker2::new()))
1219        }
1220        "TrueNorthNeuron" | "TrueNorth" => Ok(NeuronVariant::WrTrueNorthCell(WrTrueNorth::new())),
1221        "IntegerQIFNeuron" | "IntegerQIF" => {
1222            Ok(NeuronVariant::WrIntegerQIFCell(WrIntegerQIF::new()))
1223        }
1224        // Wrapped graded/rate output
1225        "SigmoidRateNeuron" | "SigmoidRate" => {
1226            Ok(NeuronVariant::WrSigmoidRateCell(WrSigmoidRate::new()))
1227        }
1228        "ThresholdLinearRateNeuron" | "ThresholdLinearRate" => Ok(
1229            NeuronVariant::WrThresholdLinearCell(WrThresholdLinear::new()),
1230        ),
1231        "AstrocyteModel" | "Astrocyte" => Ok(NeuronVariant::WrAstrocyteCell(WrAstrocyte::new())),
1232        "InnerHairCell" | "IHC" => Ok(NeuronVariant::WrInnerHairCellCell(WrInnerHairCell::new())),
1233        "OuterHairCell" | "OHC" => Ok(NeuronVariant::WrOuterHairCellCell(WrOuterHairCell::new())),
1234        "RodPhotoreceptor" | "Rod" => Ok(NeuronVariant::WrRodPhotoreceptorCell(
1235            WrRodPhotoreceptor::new(),
1236        )),
1237        "ConePhotoreceptor" | "Cone" => Ok(NeuronVariant::WrConePhotoreceptorCell(
1238            WrConePhotoreceptor::new(),
1239        )),
1240        "TasteReceptorCell" | "TasteReceptor" => {
1241            Ok(NeuronVariant::WrTasteReceptorCell(WrTasteReceptor::new()))
1242        }
1243        _ => Err(format!("Unsupported model: '{name}'")),
1244    }
1245}
1246
1247/// List all supported model names.
1248pub fn supported_models() -> Vec<&'static str> {
1249    vec![
1250        "Izhikevich",
1251        "AdEx",
1252        "ExpIF",
1253        "Lapicque",
1254        "HomeostaticLif",
1255        "HodgkinHuxley",
1256        "TraubMiles",
1257        "WangBuzsaki",
1258        "ConnorStevens",
1259        "DestexheThalamic",
1260        "HuberBraun",
1261        "GolombFS",
1262        "Pospischil",
1263        "MainenSejnowski",
1264        "DeSchutterPurkinje",
1265        "PlantR15",
1266        "Prescott",
1267        "MihalasNiebur",
1268        "GLIF",
1269        "GIFPopulation",
1270        "AvRonCardiac",
1271        "DurstewitzDopamine",
1272        "HillTononi",
1273        "BertramPhantom",
1274        "Yamada",
1275        "FitzHughNagumo",
1276        "MorrisLecar",
1277        "HindmarshRose",
1278        "ResonateAndFire",
1279        "BalancedResonateAndFire",
1280        "FitzHughRinzel",
1281        "McKean",
1282        "TermanWang",
1283        "GutkinErmentrout",
1284        "WilsonHR",
1285        "Akida",
1286        "StochasticLIF",
1287        "Chay",
1288        "ChayKeizer",
1289        "ShermanRinzelKeizer",
1290        "ButeraRespiratory",
1291        "EPropALIF",
1292        "SuperSpike",
1293        "LearnableNeuron",
1294        "Pernarowski",
1295        "QuadraticIF",
1296        "Theta",
1297        "PerfectIntegrator",
1298        "GatedLIF",
1299        "NonlinearLIF",
1300        "SFA",
1301        "MAT",
1302        "KLIF",
1303        "InhibitoryLIF",
1304        "ComplementaryLIF",
1305        "ParametricLIF",
1306        "NonResettingLIF",
1307        "AdaptiveThresholdIF",
1308        "SigmaDelta",
1309        "EnergyLIF",
1310        "ClosedFormContinuous",
1311        "ChialvoMap",
1312        "RulkovMap",
1313        "IbarzTanakaMap",
1314        "MedvedevMap",
1315        "CazellesMap",
1316        "CourageNekorkinMap",
1317        "AiharaMap",
1318        "KilincBhattMap",
1319        "ErmentroutKopellMap",
1320        "BrainScaleSAdEx",
1321        "SpiNNakerLIF",
1322        "NeuroGrid",
1323        "DPI",
1324        "MarderSTG",
1325        "RallCable",
1326        "BoothRinzel",
1327        "Dendrify",
1328        "LiquidTimeConstant",
1329        "ParallelSpiking",
1330        "FractionalLIF",
1331        "StochasticIF",
1332        "GalvesLocherbach",
1333        "SpikeResponse",
1334        "GLM",
1335        "ArcaneNeuron",
1336        // advanced
1337        "MultiTimescale",
1338        "AttentionGated",
1339        "PredictiveCoding",
1340        "SelfReferential",
1341        "CompositionalBinding",
1342        "DifferentiableSurrogate",
1343        "ContinuousAttractor",
1344        "MetaPlastic",
1345        "BendaHerz",
1346        // point-process
1347        "Poisson",
1348        "InhomogeneousPoisson",
1349        "GammaRenewal",
1350        "EscapeRate",
1351        "BrunelWangNeuron",
1352        // interneurons
1353        "PVFastSpiking",
1354        "SST",
1355        "VIP",
1356        "Chandelier",
1357        "CerebellarBasket",
1358        "Martinotti",
1359        // motor
1360        "AlphaMotor",
1361        "GammaMotor",
1362        "UpperMotor",
1363        "Renshaw",
1364        "MotorUnit",
1365        // sensory (spiking)
1366        "RetinalGanglion",
1367        "Merkel",
1368        "Pacinian",
1369        "Nociceptor",
1370        "OlfactoryReceptor",
1371        // cerebellar
1372        "GranuleCell",
1373        "GolgiCell",
1374        "StellateCell",
1375        "LugaroCell",
1376        "UnipolarBrushCell",
1377        "DCNNeuron",
1378        // channels
1379        "PersistentNa",
1380        "Ih",
1381        "TTypeCa",
1382        "ATypeK",
1383        "BK",
1384        "SK",
1385        "NMDA",
1386        // population
1387        "MontbrioMeanField",
1388        "BrunelNetwork",
1389        "TUMNetwork",
1390        "ElBoustaniNetwork",
1391        // misc
1392        "GradedSynapseNeuron",
1393        "GapJunctionNeuron",
1394        "FrankenhaeUserHuxleyAxon",
1395        "NodeOfRanvier",
1396        "MyelinatedAxon",
1397        "CardiacPurkinjeFibre",
1398        "SmoothMuscleCell",
1399        "EndocrineBetaCell",
1400        // wrapped multi-input spiking
1401        "AlphaNeuron",
1402        "COBALIFNeuron",
1403        "CompteWMNeuron",
1404        "TsodyksMarkramNeuron",
1405        "PinskyRinzelNeuron",
1406        "HayL5PyramidalNeuron",
1407        "TwoCompartmentLIFNeuron",
1408        // wrapped hardware integer-input
1409        "LoihiCUBANeuron",
1410        "Loihi2Neuron",
1411        "SpiNNaker2Neuron",
1412        "TrueNorthNeuron",
1413        "IntegerQIFNeuron",
1414        // wrapped graded/rate output
1415        "SigmoidRateNeuron",
1416        "ThresholdLinearRateNeuron",
1417        "AstrocyteModel",
1418        "InnerHairCell",
1419        "OuterHairCell",
1420        "RodPhotoreceptor",
1421        "ConePhotoreceptor",
1422        "TasteReceptorCell",
1423    ]
1424}
1425
1426// ── Tests ───────────────────────────────────────────────────────────
1427
1428#[cfg(test)]
1429mod tests {
1430    use super::*;
1431
1432    #[test]
1433    fn izhikevich_population_spikes() {
1434        let mut pop = create_population("Izhikevich", 10).unwrap();
1435        let mut total_spikes = 0usize;
1436        for _ in 0..100 {
1437            pop.currents.fill(10.0);
1438            pop.step_all();
1439            total_spikes += pop.spikes.iter().filter(|&&s| s != 0).count();
1440        }
1441        assert!(
1442            total_spikes > 0,
1443            "10 Izhikevich neurons must spike with I=10"
1444        );
1445    }
1446
1447    #[test]
1448    fn projection_propagates_spikes() {
1449        let row_offsets = vec![0, 2, 4];
1450        let col_indices = vec![0, 1, 0, 1];
1451        let values = vec![5.0, 3.0, 2.0, 4.0];
1452
1453        let mut proj = ProjectionRunner::new(0, 1, row_offsets, col_indices, values, 0);
1454        let src_spikes = vec![1u8, 0];
1455        let mut tgt_currents = vec![0.0; 2];
1456        proj.propagate(&src_spikes, &mut tgt_currents);
1457
1458        assert!((tgt_currents[0] - 5.0).abs() < 1e-10);
1459        assert!((tgt_currents[1] - 3.0).abs() < 1e-10);
1460    }
1461
1462    #[test]
1463    fn single_population_step_accepts_external_currents() {
1464        let mut runner = NetworkRunner::new();
1465        let idx = runner.add_population(create_population("Lapicque", 3).unwrap());
1466
1467        let (spikes, voltages) = runner
1468            .step_population_with_currents(idx, &[1.0, 2.0, 3.0])
1469            .unwrap();
1470
1471        assert_eq!(spikes.len(), 3);
1472        assert_eq!(voltages.len(), 3);
1473        assert!(spikes.iter().all(|&s| s <= 1));
1474        assert!(voltages.iter().all(|v| v.is_finite()));
1475        assert!(runner
1476            .step_population_with_currents(idx, &[1.0, 2.0])
1477            .is_err());
1478        assert!(runner
1479            .step_population_with_currents(idx + 1, &[1.0, 2.0, 3.0])
1480            .is_err());
1481    }
1482
1483    #[test]
1484    fn all_to_all_network_100_steps() {
1485        let mut runner = NetworkRunner::new();
1486        let pop = create_population("Izhikevich", 4).unwrap();
1487        runner.add_population(pop);
1488
1489        // All-to-all CSR: 4 src -> 4 tgt
1490        let mut row_offsets = Vec::new();
1491        let mut col_indices = Vec::new();
1492        let mut values = Vec::new();
1493        let mut offset = 0;
1494        for _i in 0..4 {
1495            row_offsets.push(offset);
1496            for j in 0..4 {
1497                col_indices.push(j);
1498                values.push(2.0);
1499                offset += 1;
1500            }
1501        }
1502        row_offsets.push(offset);
1503
1504        let proj = ProjectionRunner::new(0, 0, row_offsets, col_indices, values, 0);
1505        runner.add_projection(proj);
1506
1507        // Inject external current by pre-filling
1508        for n in &mut runner.populations[0].neurons {
1509            if let NeuronVariant::Izhikevich(iz) = n {
1510                iz.v = -50.0;
1511            }
1512        }
1513
1514        let results = runner.run(100);
1515        assert_eq!(results.spike_counts.len(), 1);
1516        assert_eq!(results.voltages.len(), 1);
1517        assert_eq!(results.voltages[0].len(), 4);
1518    }
1519
1520    #[test]
1521    fn mixed_hh_adex_network() {
1522        let mut runner = NetworkRunner::new();
1523
1524        let hh_pop = create_population("HodgkinHuxley", 3).unwrap();
1525        let adex_pop = create_population("AdEx", 3).unwrap();
1526        let hh_idx = runner.add_population(hh_pop);
1527        let adex_idx = runner.add_population(adex_pop);
1528
1529        // HH -> AdEx projection
1530        let row_offsets = vec![0, 3, 6, 9];
1531        let col_indices = vec![0, 1, 2, 0, 1, 2, 0, 1, 2];
1532        let values = vec![100.0; 9];
1533        let proj = ProjectionRunner::new(hh_idx, adex_idx, row_offsets, col_indices, values, 0);
1534        runner.add_projection(proj);
1535
1536        // Drive HH with external current
1537        runner.populations[0].currents.fill(15.0);
1538
1539        let results = runner.run(50);
1540        assert_eq!(results.spike_counts.len(), 2);
1541        assert_eq!(results.voltages.len(), 2);
1542    }
1543
1544    #[test]
1545    fn large_network_performance() {
1546        let n = 1000;
1547        let mut pop = create_population("Izhikevich", n).unwrap();
1548        // Run 1000 steps with constant drive — should complete quickly
1549        for _ in 0..1000 {
1550            pop.currents.fill(10.0);
1551            pop.step_all();
1552        }
1553        let total: usize = pop.spikes.iter().map(|&s| s as usize).sum();
1554        // Sanity: spike count should be deterministic and nonzero after 1000 driven steps
1555        let _ = total;
1556        // Check voltages are finite
1557        let voltages = pop.collect_voltages();
1558        assert_eq!(voltages.len(), n);
1559        for v in &voltages {
1560            assert!(v.is_finite(), "voltage must be finite");
1561        }
1562    }
1563
1564    #[test]
1565    fn batch_simulate_single_neuron() {
1566        let mut neuron = create_neuron("AdEx").unwrap();
1567        let n_steps = 1000;
1568        let current = 500.0;
1569        let mut voltages = Vec::with_capacity(n_steps);
1570        let mut spikes = Vec::new();
1571        for t in 0..n_steps {
1572            let fired = neuron.step(current);
1573            voltages.push(neuron.soma_voltage());
1574            if fired != 0 {
1575                spikes.push(t);
1576            }
1577        }
1578        assert_eq!(voltages.len(), n_steps);
1579        assert!(voltages.iter().all(|v| v.is_finite()));
1580        assert!(!spikes.is_empty(), "AdEx with I=10 should spike");
1581    }
1582
1583    #[test]
1584    fn create_neuron_all_supported() {
1585        for name in supported_models() {
1586            let result = create_neuron(name);
1587            assert!(
1588                result.is_ok(),
1589                "create_neuron({name}) failed: {:?}",
1590                result.err()
1591            );
1592        }
1593    }
1594
1595    // ── Pipeline integration: interneurons ────────────────────────
1596
1597    #[test]
1598    fn interneuron_population_create_step_reset() {
1599        for name in &[
1600            "PVFastSpiking",
1601            "SST",
1602            "VIP",
1603            "Chandelier",
1604            "CerebellarBasket",
1605            "Martinotti",
1606        ] {
1607            let mut pop = create_population(name, 5).unwrap();
1608            pop.currents.fill(3.0);
1609            for _ in 0..100 {
1610                pop.step_all();
1611            }
1612            let voltages = pop.collect_voltages();
1613            assert_eq!(voltages.len(), 5, "{name}: voltage count mismatch");
1614            for v in &voltages {
1615                assert!(v.is_finite(), "{name}: non-finite voltage {v}");
1616            }
1617            pop.reset_all();
1618            let v_after_reset = pop.collect_voltages();
1619            for v in &v_after_reset {
1620                assert!(v.is_finite(), "{name}: non-finite after reset");
1621            }
1622        }
1623    }
1624
1625    #[test]
1626    fn interneuron_mixed_network() {
1627        let mut runner = NetworkRunner::new();
1628        let pv_pop = create_population("PVFastSpiking", 3).unwrap();
1629        let sst_pop = create_population("SST", 3).unwrap();
1630        let pv_idx = runner.add_population(pv_pop);
1631        let sst_idx = runner.add_population(sst_pop);
1632
1633        // PV → SST all-to-all projection
1634        let row_offsets = vec![0, 3, 6, 9];
1635        let col_indices = vec![0, 1, 2, 0, 1, 2, 0, 1, 2];
1636        let values = vec![1.0; 9];
1637        let proj = ProjectionRunner::new(pv_idx, sst_idx, row_offsets, col_indices, values, 0);
1638        runner.add_projection(proj);
1639
1640        runner.populations[0].currents.fill(3.0);
1641        let results = runner.run(50);
1642        assert_eq!(results.spike_counts.len(), 2);
1643        assert_eq!(results.voltages.len(), 2);
1644        for pop_voltages in &results.voltages {
1645            for v in pop_voltages {
1646                assert!(v.is_finite());
1647            }
1648        }
1649    }
1650
1651    // ── Pipeline integration: sensory spiking ─────────────────────
1652
1653    #[test]
1654    fn sensory_spiking_population_create_step() {
1655        for name in &[
1656            "RetinalGanglion",
1657            "Merkel",
1658            "Pacinian",
1659            "Nociceptor",
1660            "OlfactoryReceptor",
1661        ] {
1662            let mut pop = create_population(name, 5).unwrap();
1663            pop.currents.fill(20.0);
1664            for _ in 0..200 {
1665                pop.step_all();
1666            }
1667            let voltages = pop.collect_voltages();
1668            assert_eq!(voltages.len(), 5, "{name}: voltage count mismatch");
1669            for v in &voltages {
1670                assert!(v.is_finite(), "{name}: non-finite voltage {v}");
1671            }
1672        }
1673    }
1674
1675    // ── NaN/Inf edge-case tests ───────────────────────────────────
1676
1677    #[test]
1678    fn all_models_nan_input_stays_finite() {
1679        // Models must not propagate NaN — they should produce finite
1680        // (possibly wrong) output. This catches catastrophic numerical issues.
1681        let fragile_models = &[
1682            "PVFastSpiking",
1683            "SST",
1684            "VIP",
1685            "Chandelier",
1686            "CerebellarBasket",
1687            "Martinotti",
1688            "RetinalGanglion",
1689            "Merkel",
1690            "Pacinian",
1691            "Nociceptor",
1692            "OlfactoryReceptor",
1693        ];
1694        for name in fragile_models {
1695            let mut neuron = create_neuron(name).unwrap();
1696            // Feed 100 normal steps first to get into active regime
1697            for _ in 0..100 {
1698                neuron.step(2.0);
1699            }
1700            // Then feed NaN — voltage may go NaN but should not panic
1701            for _ in 0..10 {
1702                let _ = neuron.step(f64::NAN);
1703            }
1704            // Reset must restore finite state
1705            neuron.reset();
1706            let v = neuron.soma_voltage();
1707            assert!(
1708                v.is_finite(),
1709                "{name}: voltage not finite after reset from NaN: {v}"
1710            );
1711        }
1712    }
1713
1714    #[test]
1715    fn all_models_extreme_input_stays_finite() {
1716        let models = &[
1717            "PVFastSpiking",
1718            "SST",
1719            "VIP",
1720            "Chandelier",
1721            "CerebellarBasket",
1722            "Martinotti",
1723            "RetinalGanglion",
1724            "Merkel",
1725            "Pacinian",
1726            "Nociceptor",
1727            "OlfactoryReceptor",
1728        ];
1729        for name in models {
1730            let mut neuron = create_neuron(name).unwrap();
1731            // Large positive current
1732            for _ in 0..50 {
1733                neuron.step(1e6);
1734            }
1735            neuron.reset();
1736            let v = neuron.soma_voltage();
1737            assert!(
1738                v.is_finite(),
1739                "{name}: non-finite after large positive input"
1740            );
1741
1742            // Large negative current
1743            for _ in 0..50 {
1744                neuron.step(-1e6);
1745            }
1746            neuron.reset();
1747            let v = neuron.soma_voltage();
1748            assert!(
1749                v.is_finite(),
1750                "{name}: non-finite after large negative input"
1751            );
1752        }
1753    }
1754}