Skip to main content

sc_neurocore_engine/
pyo3_neurons.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 — PyO3 wrappers for all neuron models
8
9//! PyO3 wrappers for all neuron models.
10//!
11//! Each wrapper follows the same pattern:
12//!   #[pyclass(name = "Model")] struct Py<Model> { inner: neurons::<Model> }
13//!   #[pymethods] impl Py<Model> { #[new] fn new(...) -> Self; fn step(...); fn reset(&mut self); fn get_state(...) }
14
15use numpy::{IntoPyArray, PyArray1};
16use pyo3::prelude::*;
17use pyo3::types::PyDict;
18
19use crate::neuron;
20use crate::neurons;
21
22macro_rules! py_neuron_default {
23    ($pylit:literal, $pyname:ident, $rust:ty $(, state $sname:ident)*) => {
24        #[pyclass(name = $pylit, module = "sc_neurocore_engine.sc_neurocore_engine")]
25        #[derive(Clone)]
26        pub struct $pyname { inner: $rust }
27
28        #[pymethods]
29        impl $pyname {
30            #[new]
31            fn new() -> Self { Self { inner: <$rust>::default() } }
32
33            fn step(&mut self, current: f64) -> i32 { self.inner.step(current) }
34
35            fn reset(&mut self) { self.inner.reset(); }
36
37            fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
38                let d = PyDict::new(py);
39                $(d.set_item(stringify!($sname), self.inner.$sname)?;)*
40                Ok(d.into_any().unbind())
41            }
42        }
43    };
44}
45
46// ═══════════════════════════════════════════════════════════════════
47// ai_optimized.rs models
48// ═══════════════════════════════════════════════════════════════════
49
50py_neuron_default!("MultiTimescaleNeuron", PyMultiTimescaleNeuron, neurons::MultiTimescaleNeuron, state v_fast, state v_medium, state v_slow);
51py_neuron_default!("AttentionGatedNeuron", PyAttentionGatedNeuron, neurons::AttentionGatedNeuron, state v);
52py_neuron_default!("PredictiveCodingNeuron", PyPredictiveCodingNeuron, neurons::PredictiveCodingNeuron, state v, state pred);
53py_neuron_default!("SelfReferentialNeuron", PySelfReferentialNeuron, neurons::SelfReferentialNeuron, state v);
54py_neuron_default!("CompositionalBindingNeuron", PyCompositionalBindingNeuron, neurons::CompositionalBindingNeuron, state phi, state amplitude);
55py_neuron_default!("DifferentiableSurrogateNeuron", PyDifferentiableSurrogateNeuron, neurons::DifferentiableSurrogateNeuron, state v);
56py_neuron_default!("MetaPlasticNeuron", PyMetaPlasticNeuron, neurons::MetaPlasticNeuron, state v, state error_trace, state expected_reward);
57
58py_neuron_default!("ArcaneNeuron", PyArcaneNeuron, neurons::ArcaneNeuron, state v_fast, state v_work, state v_deep);
59
60// Gap models: AdaptiveThresholdMoENeuron (step returns i32 spike count, not binary)
61py_neuron_default!("RustAdaptiveThresholdMoENeuron", PyAdaptiveThresholdMoENeuron, neurons::AdaptiveThresholdMoENeuron, state v, state v_th);
62
63// Gap models: CochlearHairCell (step(displacement) -> i32)
64py_neuron_default!("RustCochlearHairCell", PyCochlearHairCell, neurons::CochlearHairCell, state v, state glutamate_release);
65
66// Gap models: HybridLinearAttentionNeuron (needs dim param)
67#[pyclass(
68    name = "RustHybridLinearAttentionNeuron",
69    module = "sc_neurocore_engine.sc_neurocore_engine"
70)]
71#[derive(Clone)]
72pub struct PyHybridLinearAttentionNeuron {
73    inner: neurons::HybridLinearAttentionNeuron,
74}
75
76#[pymethods]
77impl PyHybridLinearAttentionNeuron {
78    #[new]
79    #[pyo3(signature = (dim=16))]
80    fn new(dim: usize) -> Self {
81        Self {
82            inner: neurons::HybridLinearAttentionNeuron::new(dim),
83        }
84    }
85    fn step(&mut self, current: f64) -> i32 {
86        self.inner.step(current)
87    }
88    fn step_qkv(&mut self, query: f64, key: f64, value: f64) -> f64 {
89        self.inner.step_qkv(query, key, value)
90    }
91    fn reset(&mut self) {
92        self.inner.reset();
93    }
94    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
95        let d = PyDict::new(py);
96        d.set_item("v", self.inner.v)?;
97        Ok(d.into_any().unbind())
98    }
99}
100
101// Gap models: QuantumInspiredLIFNeuron (step_complex)
102#[pyclass(
103    name = "RustQuantumInspiredLIFNeuron",
104    module = "sc_neurocore_engine.sc_neurocore_engine"
105)]
106#[derive(Clone)]
107pub struct PyQuantumInspiredLIFNeuron {
108    inner: neurons::QuantumInspiredLIFNeuron,
109}
110
111#[pymethods]
112impl PyQuantumInspiredLIFNeuron {
113    #[new]
114    fn new() -> Self {
115        Self {
116            inner: neurons::QuantumInspiredLIFNeuron::new(),
117        }
118    }
119    fn step(&mut self, current: f64) -> i32 {
120        self.inner.step(current)
121    }
122    fn step_complex(&mut self, i_re: f64, i_im: f64) -> i32 {
123        self.inner.step_complex(i_re, i_im)
124    }
125    fn reset(&mut self) {
126        self.inner.reset();
127    }
128    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
129        let d = PyDict::new(py);
130        d.set_item("z_re", self.inner.z_re)?;
131        d.set_item("z_im", self.inner.z_im)?;
132        Ok(d.into_any().unbind())
133    }
134}
135
136// Gap models: DendriticNMDANeuron (step(i_soma, glutamate))
137#[pyclass(
138    name = "RustDendriticNMDANeuron",
139    module = "sc_neurocore_engine.sc_neurocore_engine"
140)]
141#[derive(Clone)]
142pub struct PyDendriticNMDANeuron {
143    inner: neurons::DendriticNMDANeuron,
144}
145
146#[pymethods]
147impl PyDendriticNMDANeuron {
148    #[new]
149    fn new() -> Self {
150        Self {
151            inner: neurons::DendriticNMDANeuron::new(),
152        }
153    }
154    fn step(&mut self, i_soma: f64, glutamate: f64) -> i32 {
155        self.inner.step(i_soma, glutamate)
156    }
157    fn reset(&mut self) {
158        self.inner.reset();
159    }
160    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
161        let d = PyDict::new(py);
162        d.set_item("v_soma", self.inner.v_soma)?;
163        d.set_item("v_dend", self.inner.v_dend)?;
164        Ok(d.into_any().unbind())
165    }
166}
167
168// Gap models: MulticompartmentMCNNeuron (step_compartments(x_b, x_a, I))
169#[pyclass(
170    name = "RustMulticompartmentMCNNeuron",
171    module = "sc_neurocore_engine.sc_neurocore_engine"
172)]
173#[derive(Clone)]
174pub struct PyMulticompartmentMCNNeuron {
175    inner: neurons::MulticompartmentMCNNeuron,
176}
177
178#[pymethods]
179impl PyMulticompartmentMCNNeuron {
180    #[new]
181    fn new() -> Self {
182        Self {
183            inner: neurons::MulticompartmentMCNNeuron::new(),
184        }
185    }
186    fn step(&mut self, current: f64) -> i32 {
187        self.inner.step(current)
188    }
189    fn step_compartments(&mut self, x_basal: f64, x_apical: f64, i_soma: f64) -> i32 {
190        self.inner.step_compartments(x_basal, x_apical, i_soma)
191    }
192    fn reset(&mut self) {
193        self.inner.reset();
194    }
195    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
196        let d = PyDict::new(py);
197        d.set_item("u", self.inner.u)?;
198        d.set_item("v_basal", self.inner.v_basal)?;
199        d.set_item("v_apical", self.inner.v_apical)?;
200        Ok(d.into_any().unbind())
201    }
202}
203
204// Gap models: AstrocyteLIFNeuron (step_with_pre(i_ext, pre_spike))
205#[pyclass(
206    name = "RustAstrocyteLIFNeuron",
207    module = "sc_neurocore_engine.sc_neurocore_engine"
208)]
209#[derive(Clone)]
210pub struct PyAstrocyteLIFNeuron {
211    inner: neurons::AstrocyteLIFNeuron,
212}
213
214#[pymethods]
215impl PyAstrocyteLIFNeuron {
216    #[new]
217    fn new() -> Self {
218        Self {
219            inner: neurons::AstrocyteLIFNeuron::new(),
220        }
221    }
222    fn step(&mut self, current: f64) -> i32 {
223        self.inner.step(current)
224    }
225    fn step_with_pre(&mut self, i_ext: f64, pre_spike: bool) -> i32 {
226        self.inner.step_with_pre(i_ext, pre_spike)
227    }
228    fn reset(&mut self) {
229        self.inner.reset();
230    }
231    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
232        let d = PyDict::new(py);
233        d.set_item("v", self.inner.v)?;
234        d.set_item("ca", self.inner.ca)?;
235        Ok(d.into_any().unbind())
236    }
237}
238
239// Gap models: DirectionSelectiveRGC (step_rf(intensity, surround))
240#[pyclass(
241    name = "RustDirectionSelectiveRGC",
242    module = "sc_neurocore_engine.sc_neurocore_engine"
243)]
244#[derive(Clone)]
245pub struct PyDirectionSelectiveRGC {
246    inner: neurons::DirectionSelectiveRGC,
247}
248
249#[pymethods]
250impl PyDirectionSelectiveRGC {
251    #[new]
252    #[pyo3(signature = (is_on=true))]
253    fn new(is_on: bool) -> Self {
254        Self {
255            inner: if is_on {
256                neurons::DirectionSelectiveRGC::new_on()
257            } else {
258                neurons::DirectionSelectiveRGC::new_off()
259            },
260        }
261    }
262    fn step(&mut self, current: f64) -> i32 {
263        self.inner.step(current)
264    }
265    fn step_rf(&mut self, intensity: f64, surround_mean: f64) -> i32 {
266        self.inner.step_rf(intensity, surround_mean)
267    }
268    fn reset(&mut self) {
269        self.inner.reset();
270    }
271    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
272        let d = PyDict::new(py);
273        d.set_item("v", self.inner.v)?;
274        d.set_item("is_on_centre", self.inner.is_on_centre)?;
275        Ok(d.into_any().unbind())
276    }
277}
278
279// Gap synapse models: TripletStdpSynapse
280#[pyclass(
281    name = "RustTripletStdpSynapse",
282    module = "sc_neurocore_engine.sc_neurocore_engine"
283)]
284#[derive(Clone)]
285pub struct PyTripletStdpSynapse {
286    inner: crate::synapses::TripletStdpSynapse,
287}
288
289#[pymethods]
290impl PyTripletStdpSynapse {
291    #[new]
292    #[pyo3(signature = (weight=0.5, w_min=0.0, w_max=1.0))]
293    fn new(weight: f64, w_min: f64, w_max: f64) -> Self {
294        Self {
295            inner: crate::synapses::TripletStdpSynapse::new(weight, w_min, w_max),
296        }
297    }
298    fn step(&mut self, pre_spike: bool, post_spike: bool) {
299        self.inner.step(pre_spike, post_spike);
300    }
301    #[getter]
302    fn weight(&self) -> f64 {
303        self.inner.weight
304    }
305    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
306        let d = PyDict::new(py);
307        d.set_item("weight", self.inner.weight)?;
308        d.set_item("r1", self.inner.r1)?;
309        d.set_item("o1", self.inner.o1)?;
310        d.set_item("r2", self.inner.r2)?;
311        d.set_item("o2", self.inner.o2)?;
312        Ok(d.into_any().unbind())
313    }
314}
315
316// Gap synapse models: ShortTermPlasticitySynapse
317#[pyclass(
318    name = "RustShortTermPlasticitySynapse",
319    module = "sc_neurocore_engine.sc_neurocore_engine"
320)]
321#[derive(Clone)]
322pub struct PyShortTermPlasticitySynapse {
323    inner: crate::synapses::ShortTermPlasticitySynapse,
324}
325
326#[pymethods]
327impl PyShortTermPlasticitySynapse {
328    #[new]
329    fn new() -> Self {
330        Self {
331            inner: crate::synapses::ShortTermPlasticitySynapse::new_depressing(),
332        }
333    }
334    #[staticmethod]
335    fn depressing() -> Self {
336        Self {
337            inner: crate::synapses::ShortTermPlasticitySynapse::new_depressing(),
338        }
339    }
340    #[staticmethod]
341    fn facilitating() -> Self {
342        Self {
343            inner: crate::synapses::ShortTermPlasticitySynapse::new_facilitating(),
344        }
345    }
346    fn step(&mut self, pre_spike: bool) -> f64 {
347        self.inner.step(pre_spike)
348    }
349    fn reset(&mut self) {
350        self.inner.reset();
351    }
352    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
353        let d = PyDict::new(py);
354        d.set_item("x", self.inner.x)?;
355        d.set_item("u", self.inner.u)?;
356        Ok(d.into_any().unbind())
357    }
358}
359
360// Gap synapse models: DopamineStdpSynapse
361#[pyclass(
362    name = "RustDopamineStdpSynapse",
363    module = "sc_neurocore_engine.sc_neurocore_engine"
364)]
365#[derive(Clone)]
366pub struct PyDopamineStdpSynapse {
367    inner: crate::synapses::DopamineStdpSynapse,
368}
369
370#[pymethods]
371impl PyDopamineStdpSynapse {
372    #[new]
373    #[pyo3(signature = (weight=0.5, w_min=0.0, w_max=1.0))]
374    fn new(weight: f64, w_min: f64, w_max: f64) -> Self {
375        Self {
376            inner: crate::synapses::DopamineStdpSynapse::new(weight, w_min, w_max),
377        }
378    }
379    fn step(&mut self, pre_spike: bool, post_spike: bool, reward: f64) {
380        self.inner.step(pre_spike, post_spike, reward);
381    }
382    fn reset(&mut self) {
383        self.inner.reset();
384    }
385    #[getter]
386    fn weight(&self) -> f64 {
387        self.inner.weight
388    }
389    #[getter]
390    fn dopamine(&self) -> f64 {
391        self.inner.dopamine
392    }
393    #[getter]
394    fn eligibility(&self) -> f64 {
395        self.inner.eligibility
396    }
397    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
398        let d = PyDict::new(py);
399        d.set_item("weight", self.inner.weight)?;
400        d.set_item("eligibility", self.inner.eligibility)?;
401        d.set_item("dopamine", self.inner.dopamine)?;
402        d.set_item("trace_pre", self.inner.trace_pre)?;
403        d.set_item("trace_post", self.inner.trace_post)?;
404        Ok(d.into_any().unbind())
405    }
406}
407
408// ContinuousAttractorNeuron needs n_units param
409#[pyclass(
410    name = "RustContinuousAttractorNeuron",
411    module = "sc_neurocore_engine.sc_neurocore_engine"
412)]
413#[derive(Clone)]
414pub struct PyContinuousAttractorNeuron {
415    inner: neurons::ContinuousAttractorNeuron,
416}
417
418#[pymethods]
419impl PyContinuousAttractorNeuron {
420    #[new]
421    #[pyo3(signature = (n_units=16))]
422    fn new(n_units: usize) -> Self {
423        Self {
424            inner: neurons::ContinuousAttractorNeuron::new(n_units),
425        }
426    }
427    fn step(&mut self, current: f64) -> i32 {
428        self.inner.step(current)
429    }
430    fn bump_position(&self) -> usize {
431        self.inner.bump_position()
432    }
433    fn reset(&mut self) {
434        self.inner.reset();
435    }
436    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
437        let d = PyDict::new(py);
438        d.set_item("u", self.inner.u.clone().into_pyarray(py))?;
439        Ok(d.into_any().unbind())
440    }
441}
442
443// ═══════════════════════════════════════════════════════════════════
444// trivial.rs models
445// ═══════════════════════════════════════════════════════════════════
446
447py_neuron_default!("QuadraticIFNeuron", PyQuadraticIFNeuron, neurons::QuadraticIFNeuron, state v);
448py_neuron_default!("ThetaNeuron", PyThetaNeuron, neurons::ThetaNeuron, state theta);
449py_neuron_default!("PerfectIntegratorNeuron", PyPerfectIntegratorNeuron, neurons::PerfectIntegratorNeuron, state v);
450py_neuron_default!("GatedLIFNeuron", PyGatedLIFNeuron, neurons::GatedLIFNeuron, state v);
451py_neuron_default!("NonlinearLIFNeuron", PyNonlinearLIFNeuron, neurons::NonlinearLIFNeuron, state v, state w);
452py_neuron_default!("SFANeuron", PySFANeuron, neurons::SFANeuron, state v, state g_sfa);
453py_neuron_default!("MATNeuron", PyMATNeuron, neurons::MATNeuron, state v, state theta1, state theta2);
454py_neuron_default!("KLIFNeuron", PyKLIFNeuron, neurons::KLIFNeuron, state v);
455py_neuron_default!("InhibitoryLIFNeuron", PyInhibitoryLIFNeuron, neurons::InhibitoryLIFNeuron, state v, state inh_trace);
456py_neuron_default!("ComplementaryLIFNeuron", PyComplementaryLIFNeuron, neurons::ComplementaryLIFNeuron, state v_pos, state v_neg);
457py_neuron_default!("ParametricLIFNeuron", PyParametricLIFNeuron, neurons::ParametricLIFNeuron, state v);
458py_neuron_default!("NonResettingLIFNeuron", PyNonResettingLIFNeuron, neurons::NonResettingLIFNeuron, state v, state theta);
459py_neuron_default!("AdaptiveThresholdIFNeuron", PyAdaptiveThresholdIFNeuron, neurons::AdaptiveThresholdIFNeuron, state v, state theta);
460py_neuron_default!("SigmaDeltaNeuron", PySigmaDeltaNeuron, neurons::SigmaDeltaNeuron, state sigma);
461py_neuron_default!("EnergyLIFNeuron", PyEnergyLIFNeuron, neurons::EnergyLIFNeuron, state v, state epsilon);
462py_neuron_default!("ClosedFormContinuousNeuron", PyClosedFormContinuousNeuron, neurons::ClosedFormContinuousNeuron, state x);
463
464#[pyclass(
465    name = "IntegerQIFNeuron",
466    module = "sc_neurocore_engine.sc_neurocore_engine"
467)]
468#[derive(Clone)]
469pub struct PyIntegerQIFNeuron {
470    inner: neurons::IntegerQIFNeuron,
471}
472
473#[pymethods]
474impl PyIntegerQIFNeuron {
475    #[new]
476    #[pyo3(signature = (k=6, v_threshold=1024))]
477    fn new(k: i32, v_threshold: i32) -> Self {
478        Self {
479            inner: neurons::IntegerQIFNeuron::new(k, v_threshold),
480        }
481    }
482    fn step(&mut self, current: i32) -> i32 {
483        self.inner.step(current)
484    }
485    fn reset(&mut self) {
486        self.inner.reset();
487    }
488    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
489        let d = PyDict::new(py);
490        d.set_item("v", self.inner.v)?;
491        Ok(d.into_any().unbind())
492    }
493}
494
495// EscapeRateNeuron needs seed
496#[pyclass(
497    name = "EscapeRateNeuron",
498    module = "sc_neurocore_engine.sc_neurocore_engine"
499)]
500#[derive(Clone)]
501pub struct PyEscapeRateNeuron {
502    inner: neurons::EscapeRateNeuron,
503}
504
505#[pymethods]
506impl PyEscapeRateNeuron {
507    #[new]
508    #[pyo3(signature = (seed=42))]
509    fn new(seed: u64) -> Self {
510        Self {
511            inner: neurons::EscapeRateNeuron::new(seed),
512        }
513    }
514    fn step(&mut self, current: f64) -> i32 {
515        self.inner.step(current)
516    }
517    fn reset(&mut self) {
518        self.inner.reset();
519    }
520    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
521        let d = PyDict::new(py);
522        d.set_item("v", self.inner.v)?;
523        Ok(d.into_any().unbind())
524    }
525}
526
527// ═══════════════════════════════════════════════════════════════════
528// simple_spiking.rs models
529// ═══════════════════════════════════════════════════════════════════
530
531py_neuron_default!("FitzHughNagumoNeuron", PyFitzHughNagumoNeuron, neurons::FitzHughNagumoNeuron, state v, state w);
532py_neuron_default!("MorrisLecarNeuron", PyMorrisLecarNeuron, neurons::MorrisLecarNeuron, state v, state w);
533py_neuron_default!("HindmarshRoseNeuron", PyHindmarshRoseNeuron, neurons::HindmarshRoseNeuron, state x, state y, state z);
534py_neuron_default!("ResonateAndFireNeuron", PyResonateAndFireNeuron, neurons::ResonateAndFireNeuron, state x, state y);
535py_neuron_default!("BalancedResonateAndFireNeuron", PyBalancedResonateAndFireNeuron, neurons::BalancedResonateAndFireNeuron, state x, state y, state q);
536py_neuron_default!("FitzHughRinzelNeuron", PyFitzHughRinzelNeuron, neurons::FitzHughRinzelNeuron, state v, state w, state y);
537py_neuron_default!("McKeanNeuron", PyMcKeanNeuron, neurons::McKeanNeuron, state v, state w);
538py_neuron_default!("TermanWangOscillator", PyTermanWangOscillator, neurons::TermanWangOscillator, state v, state w);
539py_neuron_default!("GutkinErmentroutNeuron", PyGutkinErmentroutNeuron, neurons::GutkinErmentroutNeuron, state v, state n);
540py_neuron_default!("WilsonHRNeuron", PyWilsonHRNeuron, neurons::WilsonHRNeuron, state v, state r);
541py_neuron_default!("ChayNeuron", PyChayNeuron, neurons::ChayNeuron, state v, state n, state ca);
542py_neuron_default!("ChayKeizerNeuron", PyChayKeizerNeuron, neurons::ChayKeizerNeuron, state v, state n, state ca);
543py_neuron_default!("ShermanRinzelKeizerNeuron", PyShermanRinzelKeizerNeuron, neurons::ShermanRinzelKeizerNeuron, state v, state n, state s);
544py_neuron_default!("ButeraRespiratoryNeuron", PyButeraRespiratoryNeuron, neurons::ButeraRespiratoryNeuron, state v, state n, state h_nap);
545py_neuron_default!("LearnableNeuronModel", PyLearnableNeuronModel, neurons::LearnableNeuronModel, state v);
546py_neuron_default!("PernarowskiNeuron", PyPernarowskiNeuron, neurons::PernarowskiNeuron, state v, state w, state z);
547
548// AlphaNeuron: step(exc, inh)
549#[pyclass(
550    name = "AlphaNeuron",
551    module = "sc_neurocore_engine.sc_neurocore_engine"
552)]
553#[derive(Clone)]
554pub struct PyAlphaNeuron {
555    inner: neurons::AlphaNeuron,
556}
557
558#[pymethods]
559impl PyAlphaNeuron {
560    #[new]
561    fn new() -> Self {
562        Self {
563            inner: neurons::AlphaNeuron::new(),
564        }
565    }
566    #[pyo3(signature = (exc_current, inh_current=0.0))]
567    fn step(&mut self, exc_current: f64, inh_current: f64) -> i32 {
568        self.inner.step(exc_current, inh_current)
569    }
570    fn reset(&mut self) {
571        self.inner.reset();
572    }
573    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
574        let d = PyDict::new(py);
575        d.set_item("v", self.inner.v)?;
576        d.set_item("i_exc", self.inner.i_exc)?;
577        d.set_item("i_inh", self.inner.i_inh)?;
578        Ok(d.into_any().unbind())
579    }
580}
581
582// COBALIFNeuron: step(current, delta_ge, delta_gi)
583#[pyclass(
584    name = "COBALIFNeuron",
585    module = "sc_neurocore_engine.sc_neurocore_engine"
586)]
587#[derive(Clone)]
588pub struct PyCOBALIFNeuron {
589    inner: neurons::COBALIFNeuron,
590}
591
592#[pymethods]
593impl PyCOBALIFNeuron {
594    #[new]
595    fn new() -> Self {
596        Self {
597            inner: neurons::COBALIFNeuron::new(),
598        }
599    }
600    #[pyo3(signature = (current, delta_ge=0.0, delta_gi=0.0))]
601    fn step(&mut self, current: f64, delta_ge: f64, delta_gi: f64) -> i32 {
602        self.inner.step(current, delta_ge, delta_gi)
603    }
604    fn reset(&mut self) {
605        self.inner.reset();
606    }
607    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
608        let d = PyDict::new(py);
609        d.set_item("v", self.inner.v)?;
610        d.set_item("g_e", self.inner.g_e)?;
611        d.set_item("g_i", self.inner.g_i)?;
612        Ok(d.into_any().unbind())
613    }
614}
615
616// EPropALIFNeuron: needs tau params
617#[pyclass(
618    name = "EPropALIFNeuron",
619    module = "sc_neurocore_engine.sc_neurocore_engine"
620)]
621#[derive(Clone)]
622pub struct PyEPropALIFNeuron {
623    inner: neurons::EPropALIFNeuron,
624}
625
626#[pymethods]
627impl PyEPropALIFNeuron {
628    #[new]
629    #[pyo3(signature = (tau_m=20.0, tau_a=200.0, dt=1.0))]
630    fn new(tau_m: f64, tau_a: f64, dt: f64) -> Self {
631        Self {
632            inner: neurons::EPropALIFNeuron::new(tau_m, tau_a, dt),
633        }
634    }
635    fn step(&mut self, current: f64) -> i32 {
636        self.inner.step(current)
637    }
638    fn reset(&mut self) {
639        self.inner.reset();
640    }
641    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
642        let d = PyDict::new(py);
643        d.set_item("v", self.inner.v)?;
644        d.set_item("a", self.inner.a)?;
645        d.set_item("e_trace", self.inner.e_trace)?;
646        Ok(d.into_any().unbind())
647    }
648}
649
650// SuperSpikeNeuron: needs tau params
651#[pyclass(
652    name = "SuperSpikeNeuron",
653    module = "sc_neurocore_engine.sc_neurocore_engine"
654)]
655#[derive(Clone)]
656pub struct PySuperSpikeNeuron {
657    inner: neurons::SuperSpikeNeuron,
658}
659
660#[pymethods]
661impl PySuperSpikeNeuron {
662    #[new]
663    #[pyo3(signature = (tau_m=10.0, tau_e=10.0, dt=1.0))]
664    fn new(tau_m: f64, tau_e: f64, dt: f64) -> Self {
665        Self {
666            inner: neurons::SuperSpikeNeuron::new(tau_m, tau_e, dt),
667        }
668    }
669    fn step(&mut self, current: f64) -> i32 {
670        self.inner.step(current)
671    }
672    fn reset(&mut self) {
673        self.inner.reset();
674    }
675    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
676        let d = PyDict::new(py);
677        d.set_item("v", self.inner.v)?;
678        d.set_item("trace", self.inner.trace)?;
679        Ok(d.into_any().unbind())
680    }
681}
682
683// BendaHerzNeuron: needs seed
684#[pyclass(
685    name = "BendaHerzNeuron",
686    module = "sc_neurocore_engine.sc_neurocore_engine"
687)]
688#[derive(Clone)]
689pub struct PyBendaHerzNeuron {
690    inner: neurons::BendaHerzNeuron,
691}
692
693#[pymethods]
694impl PyBendaHerzNeuron {
695    #[new]
696    #[pyo3(signature = (seed=42))]
697    fn new(seed: u64) -> Self {
698        Self {
699            inner: neurons::BendaHerzNeuron::new(seed),
700        }
701    }
702    fn step(&mut self, current: f64) -> i32 {
703        self.inner.step(current)
704    }
705    fn reset(&mut self) {
706        self.inner.reset();
707    }
708    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
709        let d = PyDict::new(py);
710        d.set_item("a", self.inner.a)?;
711        Ok(d.into_any().unbind())
712    }
713}
714
715py_neuron_default!("BrunelWangNeuron", PyBrunelWangNeuron, neurons::BrunelWangNeuron, state v, state ref_remaining);
716
717// ═══════════════════════════════════════════════════════════════════
718// maps.rs models
719// ═══════════════════════════════════════════════════════════════════
720
721py_neuron_default!("ChialvoMapNeuron", PyChialvoMapNeuron, neurons::ChialvoMapNeuron, state x, state y);
722py_neuron_default!("RulkovMapNeuron", PyRulkovMapNeuron, neurons::RulkovMapNeuron, state x, state y);
723py_neuron_default!("IbarzTanakaMapNeuron", PyIbarzTanakaMapNeuron, neurons::IbarzTanakaMapNeuron, state x, state y);
724py_neuron_default!("MedvedevMapNeuron", PyMedvedevMapNeuron, neurons::MedvedevMapNeuron, state x);
725py_neuron_default!("CazellesMapNeuron", PyCazellesMapNeuron, neurons::CazellesMapNeuron, state x, state y);
726py_neuron_default!("CourageNekorkinMapNeuron", PyCourageNekorkinMapNeuron, neurons::CourageNekorkinMapNeuron, state x, state y);
727py_neuron_default!("AiharaMapNeuron", PyAiharaMapNeuron, neurons::AiharaMapNeuron, state x, state y);
728py_neuron_default!("KilincBhattMapNeuron", PyKilincBhattMapNeuron, neurons::KilincBhattMapNeuron, state x, state theta);
729py_neuron_default!("ErmentroutKopellMapNeuron", PyErmentroutKopellMapNeuron, neurons::ErmentroutKopellMapNeuron, state theta);
730
731// ═══════════════════════════════════════════════════════════════════
732// biophysical.rs models
733// ═══════════════════════════════════════════════════════════════════
734
735py_neuron_default!("HodgkinHuxleyNeuron", PyHodgkinHuxleyNeuron, neurons::HodgkinHuxleyNeuron, state v, state m, state h, state n);
736py_neuron_default!("TraubMilesNeuron", PyTraubMilesNeuron, neurons::TraubMilesNeuron, state v, state m, state h, state n);
737py_neuron_default!("WangBuzsakiNeuron", PyWangBuzsakiNeuron, neurons::WangBuzsakiNeuron, state v, state h, state n);
738py_neuron_default!("ConnorStevensNeuron", PyConnorStevensNeuron, neurons::ConnorStevensNeuron, state v, state m, state h, state n, state a, state b);
739py_neuron_default!("DestexheThalamicNeuron", PyDestexheThalamicNeuron, neurons::DestexheThalamicNeuron, state v, state h_na, state n_k, state m_t, state h_t);
740py_neuron_default!("HuberBraunNeuron", PyHuberBraunNeuron, neurons::HuberBraunNeuron, state v, state a_sd, state a_sr);
741py_neuron_default!("GolombFSNeuron", PyGolombFSNeuron, neurons::GolombFSNeuron, state v, state h, state n, state p);
742py_neuron_default!("PospischilNeuron", PyPospischilNeuron, neurons::PospischilNeuron, state v, state m, state h, state n, state p);
743py_neuron_default!("MainenSejnowskiNeuron", PyMainenSejnowskiNeuron, neurons::MainenSejnowskiNeuron, state vs, state va, state m, state h, state n);
744py_neuron_default!("DeSchutterPurkinjeNeuron", PyDeSchutterPurkinjeNeuron, neurons::DeSchutterPurkinjeNeuron, state v, state h_na, state n_k, state m_cap, state h_cap, state q_kca, state ca);
745py_neuron_default!("PlantR15Neuron", PyPlantR15Neuron, neurons::PlantR15Neuron, state v, state m, state h, state n, state ca);
746py_neuron_default!("PrescottNeuron", PyPrescottNeuron, neurons::PrescottNeuron, state v, state w);
747py_neuron_default!("MihalasNieburNeuron", PyMihalasNieburNeuron, neurons::MihalasNieburNeuron, state v, state theta, state i1, state i2);
748py_neuron_default!("GLIFNeuron", PyGLIFNeuron, neurons::GLIFNeuron, state v, state theta, state i_asc1, state i_asc2);
749py_neuron_default!("AvRonCardiacNeuron", PyAvRonCardiacNeuron, neurons::AvRonCardiacNeuron, state v, state h, state n, state s);
750py_neuron_default!("DurstewitzDopamineNeuron", PyDurstewitzDopamineNeuron, neurons::DurstewitzDopamineNeuron, state v, state h_na, state n_k);
751py_neuron_default!("HillTononiNeuron", PyHillTononiNeuron, neurons::HillTononiNeuron, state v, state h_na, state n_k, state m_h, state h_t, state na_i);
752py_neuron_default!("BertramPhantomBurster", PyBertramPhantomBurster, neurons::BertramPhantomBurster, state v, state s1, state s2);
753py_neuron_default!("YamadaNeuron", PyYamadaNeuron, neurons::YamadaNeuron, state v, state n, state q);
754
755// GIFPopulationNeuron: needs seed
756#[pyclass(
757    name = "GIFPopulationNeuron",
758    module = "sc_neurocore_engine.sc_neurocore_engine"
759)]
760#[derive(Clone)]
761pub struct PyGIFPopulationNeuron {
762    inner: neurons::GIFPopulationNeuron,
763}
764
765#[pymethods]
766impl PyGIFPopulationNeuron {
767    #[new]
768    #[pyo3(signature = (seed=42))]
769    fn new(seed: u64) -> Self {
770        Self {
771            inner: neurons::GIFPopulationNeuron::new(seed),
772        }
773    }
774    fn step(&mut self, current: f64) -> i32 {
775        self.inner.step(current)
776    }
777    fn reset(&mut self) {
778        self.inner.reset();
779    }
780    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
781        let d = PyDict::new(py);
782        d.set_item("v", self.inner.v)?;
783        d.set_item("theta", self.inner.theta)?;
784        d.set_item("eta", self.inner.eta)?;
785        Ok(d.into_any().unbind())
786    }
787}
788
789// ═══════════════════════════════════════════════════════════════════
790// multi_compartment.rs models
791// ═══════════════════════════════════════════════════════════════════
792
793// PinskyRinzelNeuron: step(current_soma, current_dend)
794#[pyclass(
795    name = "PinskyRinzelNeuron",
796    module = "sc_neurocore_engine.sc_neurocore_engine"
797)]
798#[derive(Clone)]
799pub struct PyPinskyRinzelNeuron {
800    inner: neurons::PinskyRinzelNeuron,
801}
802
803#[pymethods]
804impl PyPinskyRinzelNeuron {
805    #[new]
806    fn new() -> Self {
807        Self {
808            inner: neurons::PinskyRinzelNeuron::new(),
809        }
810    }
811    #[pyo3(signature = (current_soma, current_dend=0.0))]
812    fn step(&mut self, current_soma: f64, current_dend: f64) -> i32 {
813        self.inner.step(current_soma, current_dend)
814    }
815    fn reset(&mut self) {
816        self.inner.reset();
817    }
818    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
819        let d = PyDict::new(py);
820        d.set_item("v_s", self.inner.v_s)?;
821        d.set_item("v_d", self.inner.v_d)?;
822        Ok(d.into_any().unbind())
823    }
824}
825
826// HayL5PyramidalNeuron: step(current_soma, current_tuft)
827#[pyclass(
828    name = "HayL5PyramidalNeuron",
829    module = "sc_neurocore_engine.sc_neurocore_engine"
830)]
831#[derive(Clone)]
832pub struct PyHayL5PyramidalNeuron {
833    inner: neurons::HayL5PyramidalNeuron,
834}
835
836#[pymethods]
837impl PyHayL5PyramidalNeuron {
838    #[new]
839    fn new() -> Self {
840        Self {
841            inner: neurons::HayL5PyramidalNeuron::new(),
842        }
843    }
844    #[pyo3(signature = (current_soma, current_tuft=0.0))]
845    fn step(&mut self, current_soma: f64, current_tuft: f64) -> i32 {
846        self.inner.step(current_soma, current_tuft)
847    }
848    fn reset(&mut self) {
849        self.inner.reset();
850    }
851    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
852        let d = PyDict::new(py);
853        d.set_item("v_s", self.inner.v_s)?;
854        d.set_item("v_t", self.inner.v_t)?;
855        d.set_item("v_a", self.inner.v_a)?;
856        Ok(d.into_any().unbind())
857    }
858}
859
860py_neuron_default!("MarderSTGNeuron", PyMarderSTGNeuron, neurons::MarderSTGNeuron, state v, state ca);
861py_neuron_default!("BoothRinzelNeuron", PyBoothRinzelNeuron, neurons::BoothRinzelNeuron, state vs, state vd, state ca);
862py_neuron_default!("DendrifyNeuron", PyDendrifyNeuron, neurons::DendrifyNeuron, state v_s, state v_d);
863
864// RallCableNeuron: variable compartments
865#[pyclass(
866    name = "RallCableNeuron",
867    module = "sc_neurocore_engine.sc_neurocore_engine"
868)]
869#[derive(Clone)]
870pub struct PyRallCableNeuron {
871    inner: neurons::RallCableNeuron,
872}
873
874#[pymethods]
875impl PyRallCableNeuron {
876    #[new]
877    #[pyo3(signature = (n_comp=5))]
878    fn new(n_comp: usize) -> Self {
879        Self {
880            inner: neurons::RallCableNeuron::new(n_comp),
881        }
882    }
883    fn step(&mut self, current: f64) -> i32 {
884        self.inner.step(current)
885    }
886    fn reset(&mut self) {
887        self.inner.reset();
888    }
889    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
890        let d = PyDict::new(py);
891        d.set_item("v", self.inner.v.clone())?;
892        Ok(d.into_any().unbind())
893    }
894}
895
896// TwoCompartmentLIFNeuron: step(i_soma, i_dend)
897#[pyclass(
898    name = "TwoCompartmentLIFNeuron",
899    module = "sc_neurocore_engine.sc_neurocore_engine"
900)]
901#[derive(Clone)]
902pub struct PyTwoCompartmentLIFNeuron {
903    inner: neurons::TwoCompartmentLIFNeuron,
904}
905
906#[pymethods]
907impl PyTwoCompartmentLIFNeuron {
908    #[new]
909    fn new() -> Self {
910        Self {
911            inner: neurons::TwoCompartmentLIFNeuron::new(),
912        }
913    }
914    #[pyo3(signature = (i_soma, i_dend=0.0))]
915    fn step(&mut self, i_soma: f64, i_dend: f64) -> i32 {
916        self.inner.step(i_soma, i_dend)
917    }
918    fn reset(&mut self) {
919        self.inner.reset();
920    }
921    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
922        let d = PyDict::new(py);
923        d.set_item("v_s", self.inner.v_s)?;
924        d.set_item("v_d", self.inner.v_d)?;
925        Ok(d.into_any().unbind())
926    }
927}
928
929// ═══════════════════════════════════════════════════════════════════
930// special.rs models (stochastic / population / neural mass)
931// ═══════════════════════════════════════════════════════════════════
932
933#[pyclass(
934    name = "PoissonNeuron",
935    module = "sc_neurocore_engine.sc_neurocore_engine"
936)]
937#[derive(Clone)]
938pub struct PyPoissonNeuron {
939    inner: neurons::PoissonNeuron,
940}
941
942#[pymethods]
943impl PyPoissonNeuron {
944    #[new]
945    #[pyo3(signature = (rate_hz=100.0, dt_ms=1.0, seed=42))]
946    fn new(rate_hz: f64, dt_ms: f64, seed: u64) -> Self {
947        Self {
948            inner: neurons::PoissonNeuron::new(rate_hz, dt_ms, seed),
949        }
950    }
951    #[pyo3(signature = (rate_override=-1.0))]
952    fn step(&mut self, rate_override: f64) -> i32 {
953        self.inner.step(rate_override)
954    }
955    fn reset(&mut self) {
956        self.inner.reset();
957    }
958}
959
960#[pyclass(
961    name = "InhomogeneousPoissonNeuron",
962    module = "sc_neurocore_engine.sc_neurocore_engine"
963)]
964#[derive(Clone)]
965pub struct PyInhomogeneousPoissonNeuron {
966    inner: neurons::InhomogeneousPoissonNeuron,
967}
968
969#[pymethods]
970impl PyInhomogeneousPoissonNeuron {
971    #[new]
972    #[pyo3(signature = (dt_ms=1.0, seed=42))]
973    fn new(dt_ms: f64, seed: u64) -> Self {
974        Self {
975            inner: neurons::InhomogeneousPoissonNeuron::new(dt_ms, seed),
976        }
977    }
978    fn step(&mut self, rate_hz: f64) -> i32 {
979        self.inner.step(rate_hz)
980    }
981    fn reset(&mut self) {
982        self.inner.reset();
983    }
984}
985
986#[pyclass(
987    name = "GammaRenewalNeuron",
988    module = "sc_neurocore_engine.sc_neurocore_engine"
989)]
990#[derive(Clone)]
991pub struct PyGammaRenewalNeuron {
992    inner: neurons::GammaRenewalNeuron,
993}
994
995#[pymethods]
996impl PyGammaRenewalNeuron {
997    #[new]
998    #[pyo3(signature = (rate_hz=50.0, shape_k=3, seed=42))]
999    fn new(rate_hz: f64, shape_k: u32, seed: u64) -> Self {
1000        Self {
1001            inner: neurons::GammaRenewalNeuron::new(rate_hz, shape_k, seed),
1002        }
1003    }
1004    #[pyo3(signature = (rate_override=-1.0))]
1005    fn step(&mut self, rate_override: f64) -> i32 {
1006        self.inner.step(rate_override)
1007    }
1008    fn reset(&mut self) {
1009        self.inner.reset();
1010    }
1011}
1012
1013#[pyclass(
1014    name = "StochasticIFNeuron",
1015    module = "sc_neurocore_engine.sc_neurocore_engine"
1016)]
1017#[derive(Clone)]
1018pub struct PyStochasticIFNeuron {
1019    inner: neurons::StochasticIFNeuron,
1020}
1021
1022#[pymethods]
1023impl PyStochasticIFNeuron {
1024    #[new]
1025    #[pyo3(signature = (seed=42))]
1026    fn new(seed: u64) -> Self {
1027        Self {
1028            inner: neurons::StochasticIFNeuron::new(seed),
1029        }
1030    }
1031    fn step(&mut self, current: f64) -> i32 {
1032        self.inner.step(current)
1033    }
1034    fn reset(&mut self) {
1035        self.inner.reset();
1036    }
1037    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1038        let d = PyDict::new(py);
1039        d.set_item("v", self.inner.v)?;
1040        Ok(d.into_any().unbind())
1041    }
1042}
1043
1044#[pyclass(
1045    name = "GalvesLocherbachNeuron",
1046    module = "sc_neurocore_engine.sc_neurocore_engine"
1047)]
1048#[derive(Clone)]
1049pub struct PyGalvesLocherbachNeuron {
1050    inner: neurons::GalvesLocherbachNeuron,
1051}
1052
1053#[pymethods]
1054impl PyGalvesLocherbachNeuron {
1055    #[new]
1056    #[pyo3(signature = (seed=42))]
1057    fn new(seed: u64) -> Self {
1058        Self {
1059            inner: neurons::GalvesLocherbachNeuron::new(seed),
1060        }
1061    }
1062    fn step(&mut self, weighted_input: f64) -> i32 {
1063        self.inner.step(weighted_input)
1064    }
1065    fn reset(&mut self) {
1066        self.inner.reset();
1067    }
1068    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1069        let d = PyDict::new(py);
1070        d.set_item("v", self.inner.v)?;
1071        Ok(d.into_any().unbind())
1072    }
1073}
1074
1075py_neuron_default!("SpikeResponseNeuron", PySpikeResponseNeuron, neurons::SpikeResponseNeuron, state v, state time_since_spike);
1076
1077// GLMNeuron: needs n_k, n_h, seed
1078#[pyclass(name = "GLMNeuron", module = "sc_neurocore_engine.sc_neurocore_engine")]
1079#[derive(Clone)]
1080pub struct PyGLMNeuron {
1081    inner: neurons::GLMNeuron,
1082}
1083
1084#[pymethods]
1085impl PyGLMNeuron {
1086    #[new]
1087    #[pyo3(signature = (n_k=10, n_h=20, seed=42))]
1088    fn new(n_k: usize, n_h: usize, seed: u64) -> Self {
1089        Self {
1090            inner: neurons::GLMNeuron::new(n_k, n_h, seed),
1091        }
1092    }
1093    fn step(&mut self, stimulus: f64) -> i32 {
1094        self.inner.step(stimulus)
1095    }
1096    fn reset(&mut self) {
1097        self.inner.reset();
1098    }
1099}
1100
1101// WilsonCowanUnit: step returns f64
1102#[pyclass(
1103    name = "WilsonCowanUnit",
1104    module = "sc_neurocore_engine.sc_neurocore_engine"
1105)]
1106#[derive(Clone)]
1107pub struct PyWilsonCowanUnit {
1108    inner: neurons::WilsonCowanUnit,
1109}
1110
1111#[pymethods]
1112impl PyWilsonCowanUnit {
1113    #[new]
1114    fn new() -> Self {
1115        Self {
1116            inner: neurons::WilsonCowanUnit::new(),
1117        }
1118    }
1119    #[pyo3(signature = (ext_input=0.0))]
1120    fn step(&mut self, ext_input: f64) -> f64 {
1121        self.inner.step(ext_input)
1122    }
1123    fn reset(&mut self) {
1124        self.inner.reset();
1125    }
1126    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1127        let d = PyDict::new(py);
1128        d.set_item("e", self.inner.e)?;
1129        d.set_item("i", self.inner.i)?;
1130        Ok(d.into_any().unbind())
1131    }
1132}
1133
1134// JansenRitUnit: step returns f64
1135#[pyclass(
1136    name = "JansenRitUnit",
1137    module = "sc_neurocore_engine.sc_neurocore_engine"
1138)]
1139#[derive(Clone)]
1140pub struct PyJansenRitUnit {
1141    inner: neurons::JansenRitUnit,
1142}
1143
1144#[pymethods]
1145impl PyJansenRitUnit {
1146    #[new]
1147    fn new() -> Self {
1148        Self {
1149            inner: neurons::JansenRitUnit::new(),
1150        }
1151    }
1152    #[pyo3(signature = (p_ext=220.0))]
1153    fn step(&mut self, p_ext: f64) -> f64 {
1154        self.inner.step(p_ext)
1155    }
1156    fn reset(&mut self) {
1157        self.inner.reset();
1158    }
1159    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1160        let d = PyDict::new(py);
1161        d.set_item("y", self.inner.y.to_vec())?;
1162        Ok(d.into_any().unbind())
1163    }
1164}
1165
1166// WongWangUnit: step(stim1, stim2) -> (f64, f64)
1167#[pyclass(
1168    name = "WongWangUnit",
1169    module = "sc_neurocore_engine.sc_neurocore_engine"
1170)]
1171#[derive(Clone)]
1172pub struct PyWongWangUnit {
1173    inner: neurons::WongWangUnit,
1174}
1175
1176#[pymethods]
1177impl PyWongWangUnit {
1178    #[new]
1179    #[pyo3(signature = (seed=42))]
1180    fn new(seed: u64) -> Self {
1181        Self {
1182            inner: neurons::WongWangUnit::new(seed),
1183        }
1184    }
1185    #[pyo3(signature = (stim1=0.0, stim2=0.0))]
1186    fn step(&mut self, stim1: f64, stim2: f64) -> (f64, f64) {
1187        self.inner.step(stim1, stim2)
1188    }
1189    fn reset(&mut self) {
1190        self.inner.reset();
1191    }
1192    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1193        let d = PyDict::new(py);
1194        d.set_item("s1", self.inner.s1)?;
1195        d.set_item("s2", self.inner.s2)?;
1196        Ok(d.into_any().unbind())
1197    }
1198}
1199
1200// ErmentroutKopellPopulation: step returns f64
1201#[pyclass(
1202    name = "ErmentroutKopellPopulation",
1203    module = "sc_neurocore_engine.sc_neurocore_engine"
1204)]
1205#[derive(Clone)]
1206pub struct PyErmentroutKopellPopulation {
1207    inner: neurons::ErmentroutKopellPopulation,
1208}
1209
1210#[pymethods]
1211impl PyErmentroutKopellPopulation {
1212    #[new]
1213    fn new() -> Self {
1214        Self {
1215            inner: neurons::ErmentroutKopellPopulation::new(),
1216        }
1217    }
1218    #[pyo3(signature = (ext_input=0.0))]
1219    fn step(&mut self, ext_input: f64) -> f64 {
1220        self.inner.step(ext_input)
1221    }
1222    fn reset(&mut self) {
1223        self.inner.reset();
1224    }
1225    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1226        let d = PyDict::new(py);
1227        d.set_item("r", self.inner.r)?;
1228        d.set_item("v", self.inner.v)?;
1229        Ok(d.into_any().unbind())
1230    }
1231}
1232
1233// WendlingNeuron: step returns f64
1234#[pyclass(
1235    name = "WendlingNeuron",
1236    module = "sc_neurocore_engine.sc_neurocore_engine"
1237)]
1238#[derive(Clone)]
1239pub struct PyWendlingNeuron {
1240    inner: neurons::WendlingNeuron,
1241}
1242
1243#[pymethods]
1244impl PyWendlingNeuron {
1245    #[new]
1246    fn new() -> Self {
1247        Self {
1248            inner: neurons::WendlingNeuron::new(),
1249        }
1250    }
1251    #[pyo3(signature = (p_ext=220.0))]
1252    fn step(&mut self, p_ext: f64) -> f64 {
1253        self.inner.step(p_ext)
1254    }
1255    fn reset(&mut self) {
1256        self.inner.reset();
1257    }
1258}
1259
1260// LarterBreakspearNeuron: step returns f64
1261#[pyclass(
1262    name = "LarterBreakspearNeuron",
1263    module = "sc_neurocore_engine.sc_neurocore_engine"
1264)]
1265#[derive(Clone)]
1266pub struct PyLarterBreakspearNeuron {
1267    inner: neurons::LarterBreakspearNeuron,
1268}
1269
1270#[pymethods]
1271impl PyLarterBreakspearNeuron {
1272    #[new]
1273    fn new() -> Self {
1274        Self {
1275            inner: neurons::LarterBreakspearNeuron::new(),
1276        }
1277    }
1278    #[pyo3(signature = (coupling=0.0))]
1279    fn step(&mut self, coupling: f64) -> f64 {
1280        self.inner.step(coupling)
1281    }
1282    fn reset(&mut self) {
1283        self.inner.reset();
1284    }
1285    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1286        let d = PyDict::new(py);
1287        d.set_item("v", self.inner.v)?;
1288        d.set_item("w", self.inner.w)?;
1289        d.set_item("z", self.inner.z)?;
1290        Ok(d.into_any().unbind())
1291    }
1292}
1293
1294// ═══════════════════════════════════════════════════════════════════
1295// hardware.rs models
1296// ═══════════════════════════════════════════════════════════════════
1297
1298#[pyclass(
1299    name = "LoihiCUBANeuron",
1300    module = "sc_neurocore_engine.sc_neurocore_engine"
1301)]
1302#[derive(Clone)]
1303pub struct PyLoihiCUBANeuron {
1304    inner: neurons::LoihiCUBANeuron,
1305}
1306
1307#[pymethods]
1308impl PyLoihiCUBANeuron {
1309    #[new]
1310    fn new() -> Self {
1311        Self {
1312            inner: neurons::LoihiCUBANeuron::new(),
1313        }
1314    }
1315    fn step(&mut self, weighted_input: i32) -> i32 {
1316        self.inner.step(weighted_input)
1317    }
1318    fn reset(&mut self) {
1319        self.inner.reset();
1320    }
1321    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1322        let d = PyDict::new(py);
1323        d.set_item("v", self.inner.v)?;
1324        d.set_item("u", self.inner.u)?;
1325        Ok(d.into_any().unbind())
1326    }
1327}
1328
1329#[pyclass(
1330    name = "Loihi2Neuron",
1331    module = "sc_neurocore_engine.sc_neurocore_engine"
1332)]
1333#[derive(Clone)]
1334pub struct PyLoihi2Neuron {
1335    inner: neurons::Loihi2Neuron,
1336}
1337
1338#[pymethods]
1339impl PyLoihi2Neuron {
1340    #[new]
1341    fn new() -> Self {
1342        Self {
1343            inner: neurons::Loihi2Neuron::new(),
1344        }
1345    }
1346    fn step(&mut self, weighted_input: i32) -> i32 {
1347        self.inner.step(weighted_input)
1348    }
1349    fn reset(&mut self) {
1350        self.inner.reset();
1351    }
1352    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1353        let d = PyDict::new(py);
1354        d.set_item("s1", self.inner.s1)?;
1355        d.set_item("s2", self.inner.s2)?;
1356        d.set_item("s3", self.inner.s3)?;
1357        Ok(d.into_any().unbind())
1358    }
1359}
1360
1361#[pyclass(
1362    name = "TrueNorthNeuron",
1363    module = "sc_neurocore_engine.sc_neurocore_engine"
1364)]
1365#[derive(Clone)]
1366pub struct PyTrueNorthNeuron {
1367    inner: neurons::TrueNorthNeuron,
1368}
1369
1370#[pymethods]
1371impl PyTrueNorthNeuron {
1372    #[new]
1373    #[pyo3(signature = (threshold=100))]
1374    fn new(threshold: i32) -> Self {
1375        Self {
1376            inner: neurons::TrueNorthNeuron::new(threshold),
1377        }
1378    }
1379    fn step(&mut self, weighted_input: i32) -> i32 {
1380        self.inner.step(weighted_input)
1381    }
1382    fn reset(&mut self) {
1383        self.inner.reset();
1384    }
1385    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1386        let d = PyDict::new(py);
1387        d.set_item("v", self.inner.v)?;
1388        Ok(d.into_any().unbind())
1389    }
1390}
1391
1392py_neuron_default!("BrainScaleSAdExNeuron", PyBrainScaleSAdExNeuron, neurons::BrainScaleSAdExNeuron, state v, state w);
1393py_neuron_default!("SpiNNakerLIFNeuron", PySpiNNakerLIFNeuron, neurons::SpiNNakerLIFNeuron, state v, state refrac_count);
1394
1395#[pyclass(
1396    name = "SpiNNaker2Neuron",
1397    module = "sc_neurocore_engine.sc_neurocore_engine"
1398)]
1399#[derive(Clone)]
1400pub struct PySpiNNaker2Neuron {
1401    inner: neurons::SpiNNaker2Neuron,
1402}
1403
1404#[pymethods]
1405impl PySpiNNaker2Neuron {
1406    #[new]
1407    fn new() -> Self {
1408        Self {
1409            inner: neurons::SpiNNaker2Neuron::new(),
1410        }
1411    }
1412    fn step(&mut self, current: i32) -> i32 {
1413        self.inner.step(current)
1414    }
1415    fn reset(&mut self) {
1416        self.inner.reset();
1417    }
1418    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1419        let d = PyDict::new(py);
1420        d.set_item("v", self.inner.v)?;
1421        Ok(d.into_any().unbind())
1422    }
1423}
1424
1425py_neuron_default!("DPINeuron", PyDPINeuron, neurons::DPINeuron, state i_mem);
1426
1427#[pyclass(
1428    name = "AkidaNeuron",
1429    module = "sc_neurocore_engine.sc_neurocore_engine"
1430)]
1431#[derive(Clone)]
1432pub struct PyAkidaNeuron {
1433    inner: neurons::AkidaNeuron,
1434}
1435
1436#[pymethods]
1437impl PyAkidaNeuron {
1438    #[new]
1439    #[pyo3(signature = (threshold=100))]
1440    fn new(threshold: i32) -> Self {
1441        Self {
1442            inner: neurons::AkidaNeuron::new(threshold),
1443        }
1444    }
1445    fn step(&mut self, weight: i32) -> i32 {
1446        self.inner.step(weight as f64)
1447    }
1448    fn reset(&mut self) {
1449        self.inner.reset();
1450    }
1451    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1452        let d = PyDict::new(py);
1453        d.set_item("v", self.inner.v)?;
1454        d.set_item("rank", self.inner.rank)?;
1455        Ok(d.into_any().unbind())
1456    }
1457}
1458
1459py_neuron_default!("NeuroGridNeuron", PyNeuroGridNeuron, neurons::NeuroGridNeuron, state v_s, state v_d);
1460
1461// ═══════════════════════════════════════════════════════════════════
1462// rate.rs models
1463// ═══════════════════════════════════════════════════════════════════
1464
1465#[pyclass(
1466    name = "McCullochPittsNeuron",
1467    module = "sc_neurocore_engine.sc_neurocore_engine"
1468)]
1469#[derive(Clone)]
1470pub struct PyMcCullochPittsNeuron {
1471    inner: neurons::McCullochPittsNeuron,
1472}
1473
1474#[pymethods]
1475impl PyMcCullochPittsNeuron {
1476    #[new]
1477    #[pyo3(signature = (theta=1.0))]
1478    fn new(theta: f64) -> Self {
1479        Self {
1480            inner: neurons::McCullochPittsNeuron::new(theta),
1481        }
1482    }
1483    fn step(&self, weighted_input: f64) -> i32 {
1484        self.inner.step(weighted_input)
1485    }
1486}
1487
1488// SigmoidRateNeuron: step returns f64
1489#[pyclass(
1490    name = "SigmoidRateNeuron",
1491    module = "sc_neurocore_engine.sc_neurocore_engine"
1492)]
1493#[derive(Clone)]
1494pub struct PySigmoidRateNeuron {
1495    inner: neurons::SigmoidRateNeuron,
1496}
1497
1498#[pymethods]
1499impl PySigmoidRateNeuron {
1500    #[new]
1501    fn new() -> Self {
1502        Self {
1503            inner: neurons::SigmoidRateNeuron::new(),
1504        }
1505    }
1506    fn step(&mut self, current: f64) -> f64 {
1507        self.inner.step(current)
1508    }
1509    fn reset(&mut self) {
1510        self.inner.reset();
1511    }
1512    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1513        let d = PyDict::new(py);
1514        d.set_item("r", self.inner.r)?;
1515        Ok(d.into_any().unbind())
1516    }
1517}
1518
1519// ThresholdLinearRateNeuron: step returns f64
1520#[pyclass(
1521    name = "ThresholdLinearRateNeuron",
1522    module = "sc_neurocore_engine.sc_neurocore_engine"
1523)]
1524#[derive(Clone)]
1525pub struct PyThresholdLinearRateNeuron {
1526    inner: neurons::ThresholdLinearRateNeuron,
1527}
1528
1529#[pymethods]
1530impl PyThresholdLinearRateNeuron {
1531    #[new]
1532    fn new() -> Self {
1533        Self {
1534            inner: neurons::ThresholdLinearRateNeuron::new(),
1535        }
1536    }
1537    fn step(&mut self, current: f64) -> f64 {
1538        self.inner.step(current)
1539    }
1540    fn reset(&mut self) {
1541        self.inner.reset();
1542    }
1543}
1544
1545// AstrocyteModel: step returns f64
1546#[pyclass(
1547    name = "AstrocyteModel",
1548    module = "sc_neurocore_engine.sc_neurocore_engine"
1549)]
1550#[derive(Clone)]
1551pub struct PyAstrocyteModel {
1552    inner: neurons::AstrocyteModel,
1553}
1554
1555#[pymethods]
1556impl PyAstrocyteModel {
1557    #[new]
1558    fn new() -> Self {
1559        Self {
1560            inner: neurons::AstrocyteModel::new(),
1561        }
1562    }
1563    fn step(&mut self, current: f64) -> f64 {
1564        self.inner.step(current)
1565    }
1566    fn reset(&mut self) {
1567        self.inner.reset();
1568    }
1569    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1570        let d = PyDict::new(py);
1571        d.set_item("ca", self.inner.ca)?;
1572        d.set_item("h", self.inner.h)?;
1573        d.set_item("ip3", self.inner.ip3)?;
1574        Ok(d.into_any().unbind())
1575    }
1576}
1577
1578// TsodyksMarkramNeuron: step(current, presynaptic_spike)
1579#[pyclass(
1580    name = "TsodyksMarkramNeuron",
1581    module = "sc_neurocore_engine.sc_neurocore_engine"
1582)]
1583#[derive(Clone)]
1584pub struct PyTsodyksMarkramNeuron {
1585    inner: neurons::TsodyksMarkramNeuron,
1586}
1587
1588#[pymethods]
1589impl PyTsodyksMarkramNeuron {
1590    #[new]
1591    fn new() -> Self {
1592        Self {
1593            inner: neurons::TsodyksMarkramNeuron::new(),
1594        }
1595    }
1596    #[pyo3(signature = (current, presynaptic_spike=false))]
1597    fn step(&mut self, current: f64, presynaptic_spike: bool) -> i32 {
1598        self.inner.step(current, presynaptic_spike)
1599    }
1600    fn reset(&mut self) {
1601        self.inner.reset();
1602    }
1603    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1604        let d = PyDict::new(py);
1605        d.set_item("v", self.inner.v)?;
1606        d.set_item("x", self.inner.x)?;
1607        d.set_item("u", self.inner.u)?;
1608        Ok(d.into_any().unbind())
1609    }
1610}
1611
1612py_neuron_default!("LiquidTimeConstantNeuron", PyLiquidTimeConstantNeuron, neurons::LiquidTimeConstantNeuron, state x);
1613
1614// CompteWMNeuron: step(current, spike_in)
1615#[pyclass(
1616    name = "CompteWMNeuron",
1617    module = "sc_neurocore_engine.sc_neurocore_engine"
1618)]
1619#[derive(Clone)]
1620pub struct PyCompteWMNeuron {
1621    inner: neurons::CompteWMNeuron,
1622}
1623
1624#[pymethods]
1625impl PyCompteWMNeuron {
1626    #[new]
1627    fn new() -> Self {
1628        Self {
1629            inner: neurons::CompteWMNeuron::new(),
1630        }
1631    }
1632    #[pyo3(signature = (current, spike_in=false))]
1633    fn step(&mut self, current: f64, spike_in: bool) -> i32 {
1634        self.inner.step(current, spike_in)
1635    }
1636    fn reset(&mut self) {
1637        self.inner.reset();
1638    }
1639    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1640        let d = PyDict::new(py);
1641        d.set_item("v", self.inner.v)?;
1642        d.set_item("s_nmda", self.inner.s_nmda)?;
1643        Ok(d.into_any().unbind())
1644    }
1645}
1646
1647// SiegertTransferFunction: step returns f64, no &mut self
1648#[pyclass(
1649    name = "SiegertTransferFunction",
1650    module = "sc_neurocore_engine.sc_neurocore_engine"
1651)]
1652#[derive(Clone)]
1653pub struct PySiegertTransferFunction {
1654    inner: neurons::SiegertTransferFunction,
1655}
1656
1657#[pymethods]
1658impl PySiegertTransferFunction {
1659    #[new]
1660    fn new() -> Self {
1661        Self {
1662            inner: neurons::SiegertTransferFunction::new(),
1663        }
1664    }
1665    fn step(&self, current: f64) -> f64 {
1666        self.inner.step(current)
1667    }
1668}
1669
1670// ═══════════════════════════════════════════════════════════════════
1671// rate.rs models requiring non-default constructors or Vec state
1672// ═══════════════════════════════════════════════════════════════════
1673
1674#[pyclass(
1675    name = "FractionalLIFNeuron",
1676    module = "sc_neurocore_engine.sc_neurocore_engine"
1677)]
1678#[derive(Clone)]
1679pub struct PyFractionalLIFNeuron {
1680    inner: neurons::FractionalLIFNeuron,
1681}
1682
1683#[pymethods]
1684impl PyFractionalLIFNeuron {
1685    #[new]
1686    #[pyo3(signature = (alpha=0.8, max_hist=50))]
1687    fn new(alpha: f64, max_hist: usize) -> Self {
1688        Self {
1689            inner: neurons::FractionalLIFNeuron::new(alpha, max_hist),
1690        }
1691    }
1692    fn step(&mut self, current: f64) -> i32 {
1693        self.inner.step(current)
1694    }
1695    fn reset(&mut self) {
1696        self.inner.reset();
1697    }
1698    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1699        let d = PyDict::new(py);
1700        d.set_item("v", self.inner.v)?;
1701        Ok(d.into_any().unbind())
1702    }
1703}
1704
1705#[pyclass(
1706    name = "ParallelSpikingNeuron",
1707    module = "sc_neurocore_engine.sc_neurocore_engine"
1708)]
1709#[derive(Clone)]
1710pub struct PyParallelSpikingNeuron {
1711    inner: neurons::ParallelSpikingNeuron,
1712}
1713
1714#[pymethods]
1715impl PyParallelSpikingNeuron {
1716    #[new]
1717    #[pyo3(signature = (kernel_size=8, v_threshold=1.0))]
1718    fn new(kernel_size: usize, v_threshold: f64) -> Self {
1719        Self {
1720            inner: neurons::ParallelSpikingNeuron::new(kernel_size, v_threshold),
1721        }
1722    }
1723    fn step(&mut self, current: f64) -> i32 {
1724        self.inner.step(current)
1725    }
1726    fn reset(&mut self) {
1727        self.inner.reset();
1728    }
1729}
1730
1731#[pyclass(
1732    name = "AmariNeuralField",
1733    module = "sc_neurocore_engine.sc_neurocore_engine"
1734)]
1735#[derive(Clone)]
1736pub struct PyAmariNeuralField {
1737    inner: neurons::AmariNeuralField,
1738}
1739
1740#[pymethods]
1741impl PyAmariNeuralField {
1742    #[new]
1743    #[pyo3(signature = (n=64))]
1744    fn new(n: usize) -> Self {
1745        Self {
1746            inner: neurons::AmariNeuralField::new(n),
1747        }
1748    }
1749    fn step(&mut self, input: Vec<f64>) -> f64 {
1750        self.inner.step(&input)
1751    }
1752    fn reset(&mut self) {
1753        self.inner.reset();
1754    }
1755    fn get_state<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
1756        self.inner.u.clone().into_pyarray(py)
1757    }
1758}
1759
1760// ═══════════════════════════════════════════════════════════════════
1761// interneurons.rs models (6 specific interneuron types)
1762// ═══════════════════════════════════════════════════════════════════
1763
1764py_neuron_default!("PVFastSpikingNeuron", PyPVFastSpikingNeuron, neurons::PVFastSpikingNeuron, state v, state h, state n, state p);
1765py_neuron_default!("SSTNeuron", PySSTNeuron, neurons::SSTNeuron, state v, state m, state h, state n, state p, state s, state r);
1766py_neuron_default!("VIPNeuron", PyVIPNeuron, neurons::VIPNeuron, state v, state h, state n, state a, state b);
1767py_neuron_default!("ChandelierNeuron", PyChandelierNeuron, neurons::ChandelierNeuron, state v, state h, state n, state d, state p);
1768py_neuron_default!("CerebellarBasketNeuron", PyCerebellarBasketNeuron, neurons::CerebellarBasketNeuron, state v, state h, state n, state a, state b, state ca);
1769py_neuron_default!("MartinottiNeuron", PyMartinottiNeuron, neurons::MartinottiNeuron, state v, state m, state h, state n, state p, state s);
1770
1771// ═══════════════════════════════════════════════════════════════════
1772// motor.rs models
1773// ═══════════════════════════════════════════════════════════════════
1774
1775py_neuron_default!("AlphaMotorNeuron", PyAlphaMotorNeuron, neurons::AlphaMotorNeuron, state v, state h, state n, state m_pic, state ca);
1776py_neuron_default!("GammaMotorNeuron", PyGammaMotorNeuron, neurons::GammaMotorNeuron, state v, state adapt);
1777py_neuron_default!("UpperMotorNeuron", PyUpperMotorNeuron, neurons::UpperMotorNeuron, state v, state m, state h, state n, state p, state s);
1778py_neuron_default!("RenshawCell", PyRenshawCell, neurons::RenshawCell, state v, state h, state n, state adapt);
1779py_neuron_default!("MotorUnit", PyMotorUnit, neurons::MotorUnit, state v, state adapt, state force);
1780
1781// cerebellar.rs models
1782py_neuron_default!("GranuleCell", PyGranuleCell, neurons::GranuleCell, state v, state m, state h, state n, state ca);
1783py_neuron_default!("GolgiCell", PyGolgiCell, neurons::GolgiCell, state v, state m, state h, state p_na, state n, state a, state b, state w, state m_t, state s, state c_n, state r, state ca);
1784py_neuron_default!("StellateCell", PyStellateCell, neurons::StellateCell, state v, state h, state n, state p);
1785py_neuron_default!("LugaroCell", PyLugaroCell, neurons::LugaroCell, state v, state adapt);
1786py_neuron_default!("UnipolarBrushCell", PyUnipolarBrushCell, neurons::UnipolarBrushCell, state v, state persistent);
1787py_neuron_default!("DCNNeuron", PyDCNNeuron, neurons::DCNNeuron, state v, state h, state n, state p, state s, state r, state ca);
1788
1789// channels.rs models
1790py_neuron_default!("PersistentNaNeuron", PyPersistentNaNeuron, neurons::PersistentNaNeuron, state v, state h, state n, state p);
1791py_neuron_default!("IhNeuron", PyIhNeuron, neurons::IhNeuron, state v, state h, state n, state r);
1792py_neuron_default!("TTypeCaNeuron", PyTTypeCaNeuron, neurons::TTypeCaNeuron, state v, state h, state n, state s);
1793py_neuron_default!("ATypeKNeuron", PyATypeKNeuron, neurons::ATypeKNeuron, state v, state h, state n, state a, state b);
1794py_neuron_default!("BKNeuron", PyBKNeuron, neurons::BKNeuron, state v, state h, state n, state ca);
1795py_neuron_default!("SKNeuron", PySKNeuron, neurons::SKNeuron, state v, state h, state n, state ca);
1796py_neuron_default!("NMDANeuron", PyNMDANeuron, neurons::NMDANeuron, state v, state h, state n, state s_nmda);
1797
1798// population.rs models
1799py_neuron_default!("MontbrioMeanField", PyMontbrioMeanField, neurons::MontbrioMeanField, state r, state v);
1800py_neuron_default!("BrunelNetwork", PyBrunelNetwork, neurons::BrunelNetwork, state r_e, state r_i);
1801py_neuron_default!("TUMNetwork", PyTUMNetwork, neurons::TUMNetwork, state r, state x, state u);
1802py_neuron_default!("ElBoustaniNetwork", PyElBoustaniNetwork, neurons::ElBoustaniNetwork, state r_e, state r_i, state s);
1803
1804// misc.rs models
1805py_neuron_default!("GradedSynapseNeuron", PyGradedSynapseNeuron, neurons::GradedSynapseNeuron, state v);
1806py_neuron_default!("GapJunctionNeuron", PyGapJunctionNeuron, neurons::GapJunctionNeuron, state v);
1807py_neuron_default!("FrankenhaeUserHuxleyAxon", PyFHAxon, neurons::FrankenhaeUserHuxleyAxon, state v, state m, state h, state n, state p);
1808py_neuron_default!("NodeOfRanvier", PyNodeOfRanvier, neurons::NodeOfRanvier, state v, state m, state h, state p, state s);
1809py_neuron_default!("MyelinatedAxon", PyMyelinatedAxon, neurons::MyelinatedAxon, state v_inter);
1810py_neuron_default!("CardiacPurkinjeFibre", PyCardiacPurkinjeFibre, neurons::CardiacPurkinjeFibre, state v, state d, state f, state y);
1811py_neuron_default!("SmoothMuscleCell", PySmoothMuscleCell, neurons::SmoothMuscleCell, state v, state ca, state ca_store);
1812py_neuron_default!("EndocrineBetaCell", PyEndocrineBetaCell, neurons::EndocrineBetaCell, state v, state n, state ca);
1813
1814// ═══════════════════════════════════════════════════════════════════
1815// sensory.rs models (10 sensory neuron types)
1816// ═══════════════════════════════════════════════════════════════════
1817
1818// Graded sensory neurons (step returns f64)
1819macro_rules! py_sensory_graded {
1820    ($pylit:literal, $pyname:ident, $rust:ty $(, state $sname:ident)*) => {
1821        #[pyclass(name = $pylit, module = "sc_neurocore_engine.sc_neurocore_engine")]
1822        #[derive(Clone)]
1823        pub struct $pyname { inner: $rust }
1824
1825        #[pymethods]
1826        impl $pyname {
1827            #[new]
1828            fn new() -> Self { Self { inner: <$rust>::default() } }
1829
1830            fn step(&mut self, input: f64) -> f64 { self.inner.step(input) }
1831
1832            fn reset(&mut self) { self.inner.reset(); }
1833
1834            fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1835                let d = PyDict::new(py);
1836                $(d.set_item(stringify!($sname), self.inner.$sname)?;)*
1837                Ok(d.into_any().unbind())
1838            }
1839        }
1840    };
1841}
1842
1843py_sensory_graded!("InnerHairCell", PyInnerHairCell, neurons::InnerHairCell, state v, state ca, state q, state c, state w);
1844py_sensory_graded!("OuterHairCell", PyOuterHairCell, neurons::OuterHairCell, state v, state motility);
1845py_sensory_graded!("RodPhotoreceptor", PyRodPhotoreceptor, neurons::RodPhotoreceptor, state v, state cgmp, state ca);
1846py_sensory_graded!("ConePhotoreceptor", PyConePhotoreceptor, neurons::ConePhotoreceptor, state v, state cgmp);
1847py_sensory_graded!("TasteReceptorCell", PyTasteReceptorCell, neurons::TasteReceptorCell, state v, state ca, state ip3, state atp_release);
1848
1849// Spiking sensory neurons (step returns i32)
1850py_neuron_default!("MerkelCell", PyMerkelCell, neurons::MerkelCell, state v, state adapt);
1851py_neuron_default!("Nociceptor", PyNociceptor, neurons::Nociceptor, state v, state sensitisation);
1852py_neuron_default!("OlfactoryReceptorNeuron", PyOlfactoryReceptorNeuron, neurons::OlfactoryReceptorNeuron, state v, state camp, state adapt, state pde4);
1853
1854// RetinalGanglionCell and PacinianCorpuscle: spiking but non-default constructors
1855py_neuron_default!("RetinalGanglionCell", PyRetinalGanglionCell, neurons::RetinalGanglionCell, state baseline, state on_centre);
1856py_neuron_default!("PacinianCorpuscle", PyPacinianCorpuscle, neurons::PacinianCorpuscle, state v, state prev_pressure, state adapt);
1857
1858// ═══════════════════════════════════════════════════════════════════
1859// neuron.rs models (legacy module — AdEx, ExpIF, Lapicque)
1860// ═══════════════════════════════════════════════════════════════════
1861
1862#[pyclass(
1863    name = "AdExNeuron",
1864    module = "sc_neurocore_engine.sc_neurocore_engine"
1865)]
1866#[derive(Clone)]
1867pub struct PyAdExNeuron {
1868    inner: neuron::AdExNeuron,
1869}
1870
1871#[pymethods]
1872impl PyAdExNeuron {
1873    #[new]
1874    fn new() -> Self {
1875        Self {
1876            inner: neuron::AdExNeuron::new(),
1877        }
1878    }
1879    fn step(&mut self, current: f64) -> i32 {
1880        self.inner.step(current)
1881    }
1882    fn reset(&mut self) {
1883        self.inner.reset();
1884    }
1885    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1886        let d = PyDict::new(py);
1887        d.set_item("v", self.inner.v)?;
1888        d.set_item("w", self.inner.w)?;
1889        Ok(d.into_any().unbind())
1890    }
1891}
1892
1893#[pyclass(
1894    name = "ExpIfNeuron",
1895    module = "sc_neurocore_engine.sc_neurocore_engine"
1896)]
1897#[derive(Clone)]
1898pub struct PyExpIfNeuron {
1899    inner: neuron::ExpIfNeuron,
1900}
1901
1902#[pymethods]
1903impl PyExpIfNeuron {
1904    #[new]
1905    fn new() -> Self {
1906        Self {
1907            inner: neuron::ExpIfNeuron::new(),
1908        }
1909    }
1910    fn step(&mut self, current: f64) -> i32 {
1911        self.inner.step(current)
1912    }
1913    fn reset(&mut self) {
1914        self.inner.reset();
1915    }
1916    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1917        let d = PyDict::new(py);
1918        d.set_item("v", self.inner.v)?;
1919        Ok(d.into_any().unbind())
1920    }
1921}
1922
1923#[pyclass(
1924    name = "LapicqueNeuron",
1925    module = "sc_neurocore_engine.sc_neurocore_engine"
1926)]
1927#[derive(Clone)]
1928pub struct PyLapicqueNeuron {
1929    inner: neuron::LapicqueNeuron,
1930}
1931
1932#[pymethods]
1933impl PyLapicqueNeuron {
1934    #[new]
1935    #[pyo3(signature = (tau=20.0, resistance=1.0, threshold=1.0, dt=1.0))]
1936    fn new(tau: f64, resistance: f64, threshold: f64, dt: f64) -> Self {
1937        Self {
1938            inner: neuron::LapicqueNeuron::new(tau, resistance, threshold, dt),
1939        }
1940    }
1941    fn step(&mut self, current: f64) -> i32 {
1942        self.inner.step(current)
1943    }
1944    fn reset(&mut self) {
1945        self.inner.reset();
1946    }
1947    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1948        let d = PyDict::new(py);
1949        d.set_item("v", self.inner.v)?;
1950        Ok(d.into_any().unbind())
1951    }
1952}
1953
1954// ═══════════════════════════════════════════════════════════════════
1955// Registration function — call from lib.rs pymodule init
1956// ═══════════════════════════════════════════════════════════════════
1957
1958pub fn register_neuron_classes(m: &Bound<'_, PyModule>) -> PyResult<()> {
1959    // ai_optimized
1960    m.add_class::<PyMultiTimescaleNeuron>()?;
1961    m.add_class::<PyAttentionGatedNeuron>()?;
1962    m.add_class::<PyPredictiveCodingNeuron>()?;
1963    m.add_class::<PySelfReferentialNeuron>()?;
1964    m.add_class::<PyCompositionalBindingNeuron>()?;
1965    m.add_class::<PyDifferentiableSurrogateNeuron>()?;
1966    m.add_class::<PyContinuousAttractorNeuron>()?;
1967    m.add_class::<PyMetaPlasticNeuron>()?;
1968    // gap models (ai_optimized)
1969    m.add_class::<PyAdaptiveThresholdMoENeuron>()?;
1970    m.add_class::<PyHybridLinearAttentionNeuron>()?;
1971    m.add_class::<PyQuantumInspiredLIFNeuron>()?;
1972    // trivial
1973    m.add_class::<PyQuadraticIFNeuron>()?;
1974    m.add_class::<PyThetaNeuron>()?;
1975    m.add_class::<PyPerfectIntegratorNeuron>()?;
1976    m.add_class::<PyGatedLIFNeuron>()?;
1977    m.add_class::<PyNonlinearLIFNeuron>()?;
1978    m.add_class::<PySFANeuron>()?;
1979    m.add_class::<PyMATNeuron>()?;
1980    m.add_class::<PyEscapeRateNeuron>()?;
1981    m.add_class::<PyKLIFNeuron>()?;
1982    m.add_class::<PyInhibitoryLIFNeuron>()?;
1983    m.add_class::<PyComplementaryLIFNeuron>()?;
1984    m.add_class::<PyParametricLIFNeuron>()?;
1985    m.add_class::<PyNonResettingLIFNeuron>()?;
1986    m.add_class::<PyAdaptiveThresholdIFNeuron>()?;
1987    m.add_class::<PySigmaDeltaNeuron>()?;
1988    m.add_class::<PyEnergyLIFNeuron>()?;
1989    m.add_class::<PyIntegerQIFNeuron>()?;
1990    m.add_class::<PyClosedFormContinuousNeuron>()?;
1991    // simple_spiking
1992    m.add_class::<PyFitzHughNagumoNeuron>()?;
1993    m.add_class::<PyMorrisLecarNeuron>()?;
1994    m.add_class::<PyHindmarshRoseNeuron>()?;
1995    m.add_class::<PyResonateAndFireNeuron>()?;
1996    m.add_class::<PyBalancedResonateAndFireNeuron>()?;
1997    m.add_class::<PyFitzHughRinzelNeuron>()?;
1998    m.add_class::<PyMcKeanNeuron>()?;
1999    m.add_class::<PyTermanWangOscillator>()?;
2000    m.add_class::<PyBendaHerzNeuron>()?;
2001    m.add_class::<PyBrunelWangNeuron>()?;
2002    m.add_class::<PyAlphaNeuron>()?;
2003    m.add_class::<PyCOBALIFNeuron>()?;
2004    m.add_class::<PyGutkinErmentroutNeuron>()?;
2005    m.add_class::<PyWilsonHRNeuron>()?;
2006    m.add_class::<PyChayNeuron>()?;
2007    m.add_class::<PyChayKeizerNeuron>()?;
2008    m.add_class::<PyShermanRinzelKeizerNeuron>()?;
2009    m.add_class::<PyButeraRespiratoryNeuron>()?;
2010    m.add_class::<PyEPropALIFNeuron>()?;
2011    m.add_class::<PySuperSpikeNeuron>()?;
2012    m.add_class::<PyLearnableNeuronModel>()?;
2013    m.add_class::<PyPernarowskiNeuron>()?;
2014    // maps
2015    m.add_class::<PyChialvoMapNeuron>()?;
2016    m.add_class::<PyRulkovMapNeuron>()?;
2017    m.add_class::<PyIbarzTanakaMapNeuron>()?;
2018    m.add_class::<PyMedvedevMapNeuron>()?;
2019    m.add_class::<PyCazellesMapNeuron>()?;
2020    m.add_class::<PyCourageNekorkinMapNeuron>()?;
2021    m.add_class::<PyAiharaMapNeuron>()?;
2022    m.add_class::<PyKilincBhattMapNeuron>()?;
2023    m.add_class::<PyErmentroutKopellMapNeuron>()?;
2024    // biophysical
2025    m.add_class::<PyHodgkinHuxleyNeuron>()?;
2026    m.add_class::<PyTraubMilesNeuron>()?;
2027    m.add_class::<PyWangBuzsakiNeuron>()?;
2028    m.add_class::<PyConnorStevensNeuron>()?;
2029    m.add_class::<PyDestexheThalamicNeuron>()?;
2030    m.add_class::<PyHuberBraunNeuron>()?;
2031    m.add_class::<PyGolombFSNeuron>()?;
2032    m.add_class::<PyPospischilNeuron>()?;
2033    m.add_class::<PyMainenSejnowskiNeuron>()?;
2034    m.add_class::<PyDeSchutterPurkinjeNeuron>()?;
2035    m.add_class::<PyPlantR15Neuron>()?;
2036    m.add_class::<PyPrescottNeuron>()?;
2037    m.add_class::<PyMihalasNieburNeuron>()?;
2038    m.add_class::<PyGLIFNeuron>()?;
2039    m.add_class::<PyGIFPopulationNeuron>()?;
2040    m.add_class::<PyAvRonCardiacNeuron>()?;
2041    m.add_class::<PyDurstewitzDopamineNeuron>()?;
2042    m.add_class::<PyHillTononiNeuron>()?;
2043    m.add_class::<PyBertramPhantomBurster>()?;
2044    m.add_class::<PyYamadaNeuron>()?;
2045    // multi_compartment
2046    m.add_class::<PyPinskyRinzelNeuron>()?;
2047    m.add_class::<PyHayL5PyramidalNeuron>()?;
2048    m.add_class::<PyMarderSTGNeuron>()?;
2049    m.add_class::<PyRallCableNeuron>()?;
2050    m.add_class::<PyBoothRinzelNeuron>()?;
2051    m.add_class::<PyDendrifyNeuron>()?;
2052    m.add_class::<PyTwoCompartmentLIFNeuron>()?;
2053    // gap models (multi_compartment)
2054    m.add_class::<PyDendriticNMDANeuron>()?;
2055    m.add_class::<PyMulticompartmentMCNNeuron>()?;
2056    m.add_class::<PyAstrocyteLIFNeuron>()?;
2057    // special
2058    m.add_class::<PyPoissonNeuron>()?;
2059    m.add_class::<PyInhomogeneousPoissonNeuron>()?;
2060    m.add_class::<PyGammaRenewalNeuron>()?;
2061    m.add_class::<PyStochasticIFNeuron>()?;
2062    m.add_class::<PyGalvesLocherbachNeuron>()?;
2063    m.add_class::<PySpikeResponseNeuron>()?;
2064    m.add_class::<PyGLMNeuron>()?;
2065    m.add_class::<PyWilsonCowanUnit>()?;
2066    m.add_class::<PyJansenRitUnit>()?;
2067    m.add_class::<PyWongWangUnit>()?;
2068    m.add_class::<PyErmentroutKopellPopulation>()?;
2069    m.add_class::<PyWendlingNeuron>()?;
2070    m.add_class::<PyLarterBreakspearNeuron>()?;
2071    // hardware
2072    m.add_class::<PyLoihiCUBANeuron>()?;
2073    m.add_class::<PyLoihi2Neuron>()?;
2074    m.add_class::<PyTrueNorthNeuron>()?;
2075    m.add_class::<PyBrainScaleSAdExNeuron>()?;
2076    m.add_class::<PySpiNNakerLIFNeuron>()?;
2077    m.add_class::<PySpiNNaker2Neuron>()?;
2078    m.add_class::<PyDPINeuron>()?;
2079    m.add_class::<PyAkidaNeuron>()?;
2080    m.add_class::<PyNeuroGridNeuron>()?;
2081    // rate
2082    m.add_class::<PyMcCullochPittsNeuron>()?;
2083    m.add_class::<PySigmoidRateNeuron>()?;
2084    m.add_class::<PyThresholdLinearRateNeuron>()?;
2085    m.add_class::<PyAstrocyteModel>()?;
2086    m.add_class::<PyTsodyksMarkramNeuron>()?;
2087    m.add_class::<PyLiquidTimeConstantNeuron>()?;
2088    m.add_class::<PyCompteWMNeuron>()?;
2089    m.add_class::<PySiegertTransferFunction>()?;
2090    m.add_class::<PyFractionalLIFNeuron>()?;
2091    m.add_class::<PyParallelSpikingNeuron>()?;
2092    m.add_class::<PyAmariNeuralField>()?;
2093    m.add_class::<PyLeakyCompeteFireNeuron>()?;
2094    m.add_class::<PyArcaneNeuron>()?;
2095    // neuron.rs (legacy)
2096    m.add_class::<PyAdExNeuron>()?;
2097    m.add_class::<PyExpIfNeuron>()?;
2098    m.add_class::<PyLapicqueNeuron>()?;
2099    // interneurons
2100    m.add_class::<PyPVFastSpikingNeuron>()?;
2101    m.add_class::<PySSTNeuron>()?;
2102    m.add_class::<PyVIPNeuron>()?;
2103    m.add_class::<PyChandelierNeuron>()?;
2104    m.add_class::<PyCerebellarBasketNeuron>()?;
2105    m.add_class::<PyMartinottiNeuron>()?;
2106    // motor
2107    m.add_class::<PyAlphaMotorNeuron>()?;
2108    m.add_class::<PyGammaMotorNeuron>()?;
2109    m.add_class::<PyUpperMotorNeuron>()?;
2110    m.add_class::<PyRenshawCell>()?;
2111    m.add_class::<PyMotorUnit>()?;
2112    // sensory
2113    m.add_class::<PyInnerHairCell>()?;
2114    m.add_class::<PyOuterHairCell>()?;
2115    m.add_class::<PyRodPhotoreceptor>()?;
2116    m.add_class::<PyConePhotoreceptor>()?;
2117    m.add_class::<PyRetinalGanglionCell>()?;
2118    m.add_class::<PyMerkelCell>()?;
2119    m.add_class::<PyPacinianCorpuscle>()?;
2120    m.add_class::<PyNociceptor>()?;
2121    m.add_class::<PyOlfactoryReceptorNeuron>()?;
2122    m.add_class::<PyTasteReceptorCell>()?;
2123    // gap models (sensory)
2124    m.add_class::<PyDirectionSelectiveRGC>()?;
2125    m.add_class::<PyCochlearHairCell>()?;
2126    // cerebellar
2127    m.add_class::<PyGranuleCell>()?;
2128    m.add_class::<PyGolgiCell>()?;
2129    m.add_class::<PyStellateCell>()?;
2130    m.add_class::<PyLugaroCell>()?;
2131    m.add_class::<PyUnipolarBrushCell>()?;
2132    m.add_class::<PyDCNNeuron>()?;
2133    // channels
2134    m.add_class::<PyPersistentNaNeuron>()?;
2135    m.add_class::<PyIhNeuron>()?;
2136    m.add_class::<PyTTypeCaNeuron>()?;
2137    m.add_class::<PyATypeKNeuron>()?;
2138    m.add_class::<PyBKNeuron>()?;
2139    m.add_class::<PySKNeuron>()?;
2140    m.add_class::<PyNMDANeuron>()?;
2141    // population
2142    m.add_class::<PyMontbrioMeanField>()?;
2143    m.add_class::<PyBrunelNetwork>()?;
2144    m.add_class::<PyTUMNetwork>()?;
2145    m.add_class::<PyElBoustaniNetwork>()?;
2146    // misc
2147    m.add_class::<PyGradedSynapseNeuron>()?;
2148    m.add_class::<PyGapJunctionNeuron>()?;
2149    m.add_class::<PyFHAxon>()?;
2150    m.add_class::<PyNodeOfRanvier>()?;
2151    m.add_class::<PyMyelinatedAxon>()?;
2152    m.add_class::<PyCardiacPurkinjeFibre>()?;
2153    m.add_class::<PySmoothMuscleCell>()?;
2154    m.add_class::<PyEndocrineBetaCell>()?;
2155    // gap synapse models
2156    m.add_class::<PyTripletStdpSynapse>()?;
2157    m.add_class::<PyShortTermPlasticitySynapse>()?;
2158    m.add_class::<PyDopamineStdpSynapse>()?;
2159    Ok(())
2160}
2161
2162// LeakyCompeteFireNeuron: step(Vec) -> Vec
2163#[pyclass(
2164    name = "LeakyCompeteFireNeuron",
2165    module = "sc_neurocore_engine.sc_neurocore_engine"
2166)]
2167#[derive(Clone)]
2168pub struct PyLeakyCompeteFireNeuron {
2169    inner: neurons::LeakyCompeteFireNeuron,
2170}
2171
2172#[pymethods]
2173impl PyLeakyCompeteFireNeuron {
2174    #[new]
2175    #[pyo3(signature = (n_units=4))]
2176    fn new(n_units: usize) -> Self {
2177        Self {
2178            inner: neurons::LeakyCompeteFireNeuron::new(n_units),
2179        }
2180    }
2181    fn step(&mut self, currents: Vec<f64>) -> Vec<i32> {
2182        self.inner.step(&currents)
2183    }
2184    fn reset(&mut self) {
2185        self.inner.reset();
2186    }
2187    fn get_state(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
2188        let d = PyDict::new(py);
2189        d.set_item("v", self.inner.v.clone())?;
2190        Ok(d.into_any().unbind())
2191    }
2192}