Skip to main content

axon_encoder/
lib.rs

1//! # axon-encoder
2//!
3//! Flexible sensory encoding for spiking neural networks: continuous signals
4//! in, spike events out. Optional [`EncodingGains`] scale rate / threshold /
5//! latency / sensitivity without requiring an external neuromodulator runtime.
6
7pub mod encoder;
8pub mod encoders;
9pub mod error;
10pub mod modulators;
11#[cfg(feature = "ndarray")]
12pub mod ndarray_ext;
13pub mod poisson;
14pub mod rng;
15pub mod types;
16
17pub use error::EncoderError;
18#[cfg(feature = "ndarray")]
19pub use ndarray_ext::NdarrayEncoderExt;
20
21pub mod prelude {
22    pub use crate::Encoder;
23    pub use crate::ModulatedEncoder;
24    pub use crate::encoder::*;
25    pub use crate::encoders::*;
26    pub use crate::error::*;
27    pub use crate::modulators::*;
28    #[cfg(feature = "ndarray")]
29    pub use crate::ndarray_ext::NdarrayEncoderExt;
30    pub use crate::poisson::*;
31    pub use crate::types::*;
32}
33
34use modulators::{EncodingGains, NeuroModulators, NeuromodulatorGainCurves};
35use types::EncodedOutput;
36
37/// Encoders that can apply neuromodulator-driven gain curves.
38///
39/// Object-safe so callers can use `&mut dyn ModulatedEncoder` when the concrete
40/// encoder type is not known at compile time. Implementations map the relevant
41/// component of [`EncodingGains`] to encoder-specific scaling; public modulator
42/// helpers are provided once here.
43///
44/// Concrete encoders also keep inherent `encode_with_modulators` /
45/// `encode_step_with_modulators` wrappers so existing call sites need not import
46/// this trait.
47///
48/// # Examples
49///
50/// Prefer the **streaming** path for doctests: batch `encode_with_modulators` is
51/// stochastic, while `encode_step_with_modulators` on rate encoders is deterministic.
52///
53/// ```rust
54/// use axon_encoder::prelude::*;
55/// # fn main() -> Result<(), EncoderError> {
56/// let mut enc = RateEncoder::try_new(0.0, 100.0, (0.0, 1.0), 0.01)?;
57/// let mods = NeuroModulators {
58///     dopamine: 1.0,
59///     ..Default::default()
60/// };
61/// let curves = NeuromodulatorGainCurves {
62///     dopamine: ModulatorGainCurves {
63///         firing_rate: Some(GainCurve::new((0.0, 1.0), (1.0, 2.0))),
64///         ..Default::default()
65///     },
66///     ..Default::default()
67/// };
68/// // Accumulates rate_hz * dt; at unit input with elevated gain, a spike fires soon.
69/// let mut saw_spike = false;
70/// for _ in 0..20 {
71///     if !enc
72///         .encode_step_with_modulators(&[1.0], &mods, &curves)
73///         .spikes
74///         .is_empty()
75///     {
76///         saw_spike = true;
77///         break;
78///     }
79/// }
80/// assert!(saw_spike);
81/// # Ok(())
82/// # }
83/// ```
84pub trait ModulatedEncoder: Encoder {
85    /// Encodes input using already evaluated encoding gains.
86    ///
87    /// Implementations must sanitize `gains` (or the component they use) before
88    /// applying them.
89    fn encode_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput;
90
91    /// Encodes one streaming step using already evaluated encoding gains.
92    ///
93    /// Stateful encoders should override this when streaming requires distinct
94    /// state handling from the batch path.
95    fn encode_step_with_gains(&mut self, input: &[f32], gains: EncodingGains) -> EncodedOutput {
96        self.encode_with_gains(input, gains)
97    }
98
99    /// Encodes input using neuromodulator-driven gain curves.
100    fn encode_with_modulators(
101        &mut self,
102        input: &[f32],
103        modulators: &NeuroModulators,
104        gain_curves: &NeuromodulatorGainCurves,
105    ) -> EncodedOutput {
106        self.encode_with_gains(input, gain_curves.evaluate(modulators))
107    }
108
109    /// Encodes one streaming step using neuromodulator-driven gain curves.
110    fn encode_step_with_modulators(
111        &mut self,
112        input: &[f32],
113        modulators: &NeuroModulators,
114        gain_curves: &NeuromodulatorGainCurves,
115    ) -> EncodedOutput {
116        self.encode_step_with_gains(input, gain_curves.evaluate(modulators))
117    }
118}
119
120/// The core trait for all encoders in this crate.
121///
122/// Encoders convert continuous analog values into discrete spike events for
123/// spiking neural networks (SNNs). Two modes are supported:
124///
125/// - **Batch mode** (`encode`): Process a complete input vector at once.
126/// - **Streaming mode** (`encode_step`): Process incrementally, one step at a time.
127///
128/// # Example
129///
130/// ```rust
131/// use axon_encoder::prelude::*;
132/// # fn main() -> Result<(), EncoderError> {
133///
134/// let mut encoder = RateEncoder::try_new(5.0, 50.0, (0.0, 1.0), 0.010)?;
135/// let input = [0.25, 0.75, 0.5];
136///
137/// // Batch encoding
138/// let output = encoder.encode(&input);
139///
140/// // Reset for streaming (if using stateful encoder)
141/// encoder.reset();
142/// # Ok(())
143/// # }
144/// ```
145pub trait Encoder {
146    /// Encodes a slice of analog values into spike events (batch mode).
147    fn encode(&mut self, input: &[f32]) -> EncodedOutput;
148
149    /// Encodes a single step incrementally (streaming mode).
150    ///
151    /// By default, this delegates to `encode()` for stateless encoders.
152    /// Stateful encoders should override this to maintain state between calls.
153    ///
154    /// # Arguments
155    ///
156    /// * `input` - A slice of analog values to encode
157    ///
158    /// # Returns
159    ///
160    /// An `EncodedOutput` containing any spike events generated in this step
161    fn encode_step(&mut self, input: &[f32]) -> EncodedOutput {
162        self.encode(input)
163    }
164
165    /// Resets the encoder to its initial state
166    fn reset(&mut self);
167}
168
169#[cfg(test)]
170mod tests {
171    #[test]
172    fn test_lib_prelude_imports() {
173        use crate::prelude::*;
174        let _ = EncoderConfig::default();
175    }
176
177    /// Guard: `axon-encoder` must not depend on the neuromod crate (#21).
178    ///
179    /// Uses `cargo metadata` so table syntax, workspace inheritance, rename/
180    /// package aliases, and normal/dev/build/target scopes are all covered
181    /// without matching description prose.
182    #[test]
183    fn cargo_toml_has_no_neuromod_crate_dependency() {
184        // `CARGO` is always set when this crate is built by cargo (no fallback branch).
185        let output = std::process::Command::new(env!("CARGO"))
186            .args(["metadata", "--no-deps", "--locked", "--format-version", "1"])
187            .current_dir(env!("CARGO_MANIFEST_DIR"))
188            .output()
189            .expect("spawn cargo metadata");
190        // Always materialize stderr so a --locked/offline failure is actionable
191        // and codecov does not see a cold format arm.
192        let metadata_detail = format!(
193            "cargo metadata failed (status={:?}): {}",
194            output.status.code(),
195            String::from_utf8_lossy(&output.stderr)
196        );
197        assert!(output.status.success(), "{metadata_detail}");
198
199        let meta: serde_json::Value =
200            serde_json::from_slice(&output.stdout).expect("parse cargo metadata json");
201        let packages = meta["packages"].as_array().expect("packages array");
202        let deps = packages
203            .iter()
204            .find(|p| p["name"] == "axon-encoder")
205            .expect("axon-encoder package in metadata")["dependencies"]
206            .as_array()
207            .expect("dependencies array");
208
209        // Collect offenders so a failure names them; build the message on the
210        // success path too so codecov patch does not see cold format arms.
211        let forbidden: Vec<&serde_json::Value> =
212            deps.iter().filter(|d| d["name"] == "neuromod").collect();
213        let detail = format!(
214            "forbidden neuromod deps (name/kind): {:?}",
215            forbidden
216                .iter()
217                .map(|d| (&d["name"], &d["kind"]))
218                .collect::<Vec<_>>()
219        );
220        assert!(forbidden.is_empty(), "{detail}");
221    }
222
223    #[test]
224    fn test_encoder_default_encode_step_delegates_to_encode() {
225        use crate::prelude::*;
226
227        struct PassThrough;
228        impl Encoder for PassThrough {
229            fn encode(&mut self, input: &[f32]) -> EncodedOutput {
230                let mut out = EncodedOutput::new();
231                for (i, &v) in input.iter().enumerate() {
232                    out.spikes.push(SpikeEvent {
233                        channel: i as u16,
234                        timestamp: v as u64,
235                        polarity: true,
236                    });
237                }
238                out
239            }
240            fn reset(&mut self) {}
241        }
242
243        let mut enc = PassThrough;
244        let out = enc.encode_step(&[1.0, 2.0]);
245        assert_eq!(out.spikes.len(), 2);
246    }
247}