Skip to main content

sc_neurocore_engine/neurons/rate/
mcculloch_pitts.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 — McCulloch-Pitts neuron model
8
9/// McCulloch-Pitts 1943 — excitatory-count threshold with absolute inhibition.
10#[derive(Clone, Debug)]
11pub struct McCullochPittsNeuron {
12    pub theta: i32,
13}
14
15impl McCullochPittsNeuron {
16    /// Construct a source-faithful neuron with a positive afferent-count threshold.
17    pub fn new(theta: i32) -> Result<Self, String> {
18        if theta <= 0 {
19            return Err("theta must be a positive signed 32-bit integer".into());
20        }
21        Ok(Self { theta })
22    }
23
24    /// Revalidate the public fixed threshold before any execution boundary.
25    pub fn validate(&self) -> Result<(), String> {
26        if self.theta <= 0 {
27            return Err("theta must be a positive signed 32-bit integer".into());
28        }
29        Ok(())
30    }
31
32    /// Evaluate one preceding-instant afferent pattern without cell state.
33    pub fn try_step(&self, excitatory_count: i32, inhibitory_active: bool) -> Result<i32, String> {
34        self.validate()?;
35        if excitatory_count < 0 {
36            return Err("excitatory_count must be a non-negative signed 32-bit integer".into());
37        }
38        Ok(i32::from(
39            !inhibitory_active && excitatory_count >= self.theta,
40        ))
41    }
42}
43impl Default for McCullochPittsNeuron {
44    fn default() -> Self {
45        Self::new(1).expect("the default McCulloch-Pitts threshold is valid")
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn mcp_threshold() {
55        let n = McCullochPittsNeuron::default();
56        assert_eq!(n.try_step(2, false), Ok(1));
57        assert_eq!(n.try_step(0, false), Ok(0));
58    }
59
60    #[test]
61    fn mcp_below_threshold() {
62        let n = McCullochPittsNeuron::default();
63        assert_eq!(n.try_step(0, false), Ok(0));
64    }
65
66    #[test]
67    fn mcp_absolute_inhibition_vetoes_maximum_excitation() {
68        let n = McCullochPittsNeuron::default();
69        assert_eq!(n.try_step(i32::MAX, true), Ok(0));
70    }
71
72    #[test]
73    fn mcp_theta_two_is_and() {
74        let n = McCullochPittsNeuron::new(2).unwrap();
75        assert_eq!(n.try_step(0, false), Ok(0));
76        assert_eq!(n.try_step(1, false), Ok(0));
77        assert_eq!(n.try_step(2, false), Ok(1));
78    }
79
80    #[test]
81    fn mcp_rejects_non_positive_thresholds() {
82        assert!(McCullochPittsNeuron::new(0).is_err());
83        assert!(McCullochPittsNeuron::new(-1).is_err());
84    }
85
86    #[test]
87    fn mcp_rejects_negative_excitation() {
88        let n = McCullochPittsNeuron::default();
89        assert!(n.try_step(-1, false).is_err());
90    }
91
92    #[test]
93    fn mcp_revalidates_public_threshold_mutation() {
94        let n = McCullochPittsNeuron { theta: 0 };
95        assert!(n.try_step(1, false).is_err());
96    }
97
98    #[test]
99    fn mcp_is_stateless_across_history() {
100        let n = McCullochPittsNeuron::new(2).unwrap();
101        let outputs: Vec<i32> = [2, 0, 2, 0]
102            .into_iter()
103            .map(|count| n.try_step(count, false).unwrap())
104            .collect();
105        assert_eq!(outputs, vec![1, 0, 1, 0]);
106    }
107}