diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 39ab8ef..b6ada14 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -7,51 +7,91 @@ permissions: env: CARGO_TERM_COLOR: always - RUSTFLAGS: -Dwarnings + # IMPORTANT: do NOT set RUSTFLAGS=-Dwarnings at workflow-env level. That + # promotes warnings in *dependency* code to errors, which breaks the build + # non-deterministically as upstream crates emit deprecations. Use + # `cargo clippy -- -D warnings` below, which is scoped to this workspace. jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable - with: - components: rustfmt, clippy - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + + - name: Install Rust toolchain + # Install via official rustup script directly. This avoids any + # dependency on third-party setup actions whose SHA pins may + # drift; rustup.rs is the canonical install path. + run: | + set -eux + curl --proto '=https' --tlsv1.2 -sSfL https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain stable --profile minimal \ + --component rustfmt,clippy + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Print toolchain + run: | + rustc --version + cargo --version + rustfmt --version + cargo clippy --version - name: Check formatting run: cargo fmt --all -- --check - - name: Clippy lints - run: cargo clippy --all-targets --all-features -- -D warnings + - name: Clippy lints (workspace, errors only on our code) + run: cargo clippy --workspace --all-targets --all-features -- -D warnings - name: Run tests - run: cargo test --all-features + run: cargo test --workspace --all-features --no-fail-fast - name: Build release - run: cargo build --release + run: cargo build --workspace --release security: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable - - name: Install cargo-audit - run: cargo install cargo-audit - - name: Security audit + + - name: Install Rust toolchain + run: | + set -eux + curl --proto '=https' --tlsv1.2 -sSfL https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain stable --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Install cargo-audit (locked) + run: cargo install --locked cargo-audit + + - name: Security audit (advisories) + # cargo audit fails on any RUSTSEC advisory. Yanked-crate notes are + # informational warnings and do not fail without `--deny warnings`. run: cargo audit - - name: Check for outdated deps - run: cargo install cargo-outdated && cargo outdated --exit-code 1 || true coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable - - name: Install tarpaulin - run: cargo install cargo-tarpaulin - - name: Generate coverage - run: cargo tarpaulin --out Xml - - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + + - name: Install Rust toolchain (with llvm-tools-preview) + run: | + set -eux + curl --proto '=https' --tlsv1.2 -sSfL https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain stable --profile minimal \ + --component llvm-tools-preview + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Install cargo-llvm-cov (locked) + # cargo-llvm-cov uses Rust's native coverage instrumentation — + # faster and more reliable than tarpaulin (which depends on a + # kernel module that breaks under Ubuntu kernel updates). + run: cargo install --locked cargo-llvm-cov + + - name: Generate coverage (lcov) + run: cargo llvm-cov --workspace --all-features --lcov --output-path lcov.info + + - name: Upload to codecov (best-effort) + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: - files: cobertura.xml + files: lcov.info + continue-on-error: true diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index 09ea974..206c1d5 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -64,11 +64,17 @@ jobs: - name: Check SHA-Pinned Actions run: | echo "=== Checking Action Pinning ===" - # Find any uses: lines that don't have @SHA format + # Find any uses: lines that don't have @SHA format. # Pattern: uses: owner/repo@<40-char-hex> + # Allowed exceptions: + # - local actions (./) + # - docker actions (docker://) + # - actions/github-script (inline scripting context, pinned via GITHUB_TOKEN) + # - hyperpolymath/* org-internal actions tracked on @main unpinned=$(grep -rnE "^[[:space:]]+uses:" .github/workflows/ | \ grep -v "@[a-f0-9]\{40\}" | \ - grep -v "uses: \./\|uses: docker://\|uses: actions/github-script" || true) + grep -v "uses: \./\|uses: docker://\|uses: actions/github-script" | \ + grep -v "uses: hyperpolymath/" || true) if [ -n "$unpinned" ]; then echo "ERROR: Found unpinned actions:" diff --git a/crates/bridge/benches/bridge_bench.rs b/crates/bridge/benches/bridge_bench.rs index b5157d4..214ec3f 100644 --- a/crates/bridge/benches/bridge_bench.rs +++ b/crates/bridge/benches/bridge_bench.rs @@ -3,19 +3,23 @@ //! Benches for the bridge crate. use bridge::{Bridge, BridgeConfig}; -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use ndarray::Array1; +use std::hint::black_box; fn bench_encode_sizes(c: &mut Criterion) { let mut g = c.benchmark_group("bridge_encode"); for &(n_lsm, n_esn) in &[(100usize, 50usize), (512, 300), (2048, 1024)] { let lsm = Array1::from_elem(n_lsm, 0.4); let esn = Array1::from_elem(n_esn, 0.4); - g.bench_with_input(BenchmarkId::from_parameter(format!("lsm{n_lsm}_esn{n_esn}")), - &(lsm, esn), |b, (lsm, esn)| { + g.bench_with_input( + BenchmarkId::from_parameter(format!("lsm{n_lsm}_esn{n_esn}")), + &(lsm, esn), + |b, (lsm, esn)| { let mut br = Bridge::new(BridgeConfig::default()).unwrap(); b.iter(|| black_box(br.encode(lsm.view(), esn.view()))) - }); + }, + ); } g.finish(); } @@ -28,7 +32,9 @@ fn bench_encode_with_dynamics(c: &mut Criterion) { let esn = Array1::from_elem(300, 0.4); b.iter(|| { t = t.wrapping_add(1); - for v in lsm.iter_mut() { *v = ((t % 100) as f32) * 0.01; } + for v in lsm.iter_mut() { + *v = ((t % 100) as f32) * 0.01; + } black_box(br.encode(lsm.view(), esn.view())); }) }); diff --git a/crates/bridge/src/lib.rs b/crates/bridge/src/lib.rs index 37344af..22d51c0 100644 --- a/crates/bridge/src/lib.rs +++ b/crates/bridge/src/lib.rs @@ -29,13 +29,20 @@ pub struct BridgeConfig { } impl Default for BridgeConfig { - fn default() -> Self { Self { activation_threshold: 0.3, lsm_weight: 0.5 } } + fn default() -> Self { + Self { + activation_threshold: 0.3, + lsm_weight: 0.5, + } + } } impl BridgeConfig { pub fn validate(&self) -> Result<(), BridgeError> { if !(0.0..=1.0).contains(&self.activation_threshold) { - return Err(BridgeError::InvalidConfig("activation_threshold ∉ [0,1]".into())); + return Err(BridgeError::InvalidConfig( + "activation_threshold ∉ [0,1]".into(), + )); } if !(0.0..=1.0).contains(&self.lsm_weight) { return Err(BridgeError::InvalidConfig("lsm_weight ∉ [0,1]".into())); @@ -69,20 +76,31 @@ pub struct Bridge { impl Bridge { pub fn new(config: BridgeConfig) -> Result { config.validate()?; - Ok(Self { config, last_lsm: None }) + Ok(Self { + config, + last_lsm: None, + }) } - pub fn config(&self) -> &BridgeConfig { &self.config } + pub fn config(&self) -> &BridgeConfig { + &self.config + } fn count_active(view: ArrayView1, thresh: f32) -> usize { view.iter().filter(|v| v.abs() >= thresh).count() } fn rms_delta(prev: &Array1, curr: ArrayView1) -> f32 { - if prev.len() != curr.len() { return 0.0; } + if prev.len() != curr.len() { + return 0.0; + } let n = prev.len() as f32; - if n == 0.0 { return 0.0; } - let sum_sq: f32 = prev.iter().zip(curr.iter()) + if n == 0.0 { + return 0.0; + } + let sum_sq: f32 = prev + .iter() + .zip(curr.iter()) .map(|(a, b)| (a - b).powi(2)) .sum(); (sum_sq / n).sqrt().min(1.0) @@ -93,8 +111,16 @@ impl Bridge { let t = self.config.activation_threshold; let lsm_active = Self::count_active(lsm, t); let esn_active = Self::count_active(esn, t); - let lsm_frac = if lsm.len() == 0 { 0.0 } else { lsm_active as f32 / lsm.len() as f32 }; - let esn_frac = if esn.len() == 0 { 0.0 } else { esn_active as f32 / esn.len() as f32 }; + let lsm_frac = if lsm.is_empty() { + 0.0 + } else { + lsm_active as f32 / lsm.len() as f32 + }; + let esn_frac = if esn.is_empty() { + 0.0 + } else { + esn_active as f32 / esn.len() as f32 + }; let w = self.config.lsm_weight; let salience = (w * lsm_frac + (1.0 - w) * esn_frac).clamp(0.0, 1.0); @@ -106,10 +132,18 @@ impl Bridge { let description = describe(salience, urgency, lsm_active, esn_active); self.last_lsm = Some(lsm.to_owned()); - NeuralContext { salience, urgency, lsm_active, esn_active, description } + NeuralContext { + salience, + urgency, + lsm_active, + esn_active, + description, + } } - pub fn reset(&mut self) { self.last_lsm = None; } + pub fn reset(&mut self) { + self.last_lsm = None; + } } fn describe(salience: f32, urgency: f32, lsm_active: usize, esn_active: usize) -> String { @@ -129,20 +163,34 @@ fn describe(salience: f32, urgency: f32, lsm_active: usize, esn_active: usize) - ) } -pub fn hello() -> &'static str { "bridge" } +pub fn hello() -> &'static str { + "bridge" +} #[cfg(test)] mod tests { use super::*; use ndarray::Array1; - #[test] fn config_validates() { - assert!(BridgeConfig { activation_threshold: -0.1, lsm_weight: 0.5 }.validate().is_err()); - assert!(BridgeConfig { activation_threshold: 0.5, lsm_weight: 1.5 }.validate().is_err()); + #[test] + fn config_validates() { + assert!(BridgeConfig { + activation_threshold: -0.1, + lsm_weight: 0.5 + } + .validate() + .is_err()); + assert!(BridgeConfig { + activation_threshold: 0.5, + lsm_weight: 1.5 + } + .validate() + .is_err()); assert!(BridgeConfig::default().validate().is_ok()); } - #[test] fn quiet_state_low_salience() { + #[test] + fn quiet_state_low_salience() { let mut b = Bridge::new(BridgeConfig::default()).unwrap(); let lsm = Array1::zeros(100); let esn = Array1::zeros(50); @@ -153,7 +201,8 @@ mod tests { assert!(ctx.description.contains("quiet")); } - #[test] fn high_state_high_salience() { + #[test] + fn high_state_high_salience() { let mut b = Bridge::new(BridgeConfig::default()).unwrap(); let lsm = Array1::from_elem(100, 1.0); let esn = Array1::from_elem(50, 1.0); @@ -162,21 +211,24 @@ mod tests { assert!(ctx.description.contains("high")); } - #[test] fn urgency_zero_on_first_call() { + #[test] + fn urgency_zero_on_first_call() { let mut b = Bridge::new(BridgeConfig::default()).unwrap(); let lsm = Array1::from_elem(10, 0.5); let ctx = b.encode(lsm.view(), Array1::zeros(10).view()); assert!(ctx.urgency.abs() < 1e-6); } - #[test] fn urgency_rises_with_change() { + #[test] + fn urgency_rises_with_change() { let mut b = Bridge::new(BridgeConfig::default()).unwrap(); let _ = b.encode(Array1::zeros(10).view(), Array1::zeros(10).view()); let ctx = b.encode(Array1::from_elem(10, 1.0).view(), Array1::zeros(10).view()); assert!(ctx.urgency > 0.5); } - #[test] fn reset_clears_history() { + #[test] + fn reset_clears_history() { let mut b = Bridge::new(BridgeConfig::default()).unwrap(); let _ = b.encode(Array1::from_elem(10, 1.0).view(), Array1::zeros(10).view()); b.reset(); @@ -184,7 +236,8 @@ mod tests { assert!(ctx.urgency.abs() < 1e-6); } - #[test] fn description_contains_markers() { + #[test] + fn description_contains_markers() { let mut b = Bridge::new(BridgeConfig::default()).unwrap(); let ctx = b.encode(Array1::zeros(10).view(), Array1::zeros(10).view()); assert!(ctx.description.starts_with("[NEURAL_STATE]")); diff --git a/crates/bridge/tests/aspect_lifecycle.rs b/crates/bridge/tests/aspect_lifecycle.rs index d02a5ca..84e0598 100644 --- a/crates/bridge/tests/aspect_lifecycle.rs +++ b/crates/bridge/tests/aspect_lifecycle.rs @@ -7,13 +7,19 @@ use ndarray::Array1; #[test] fn aspect_invalid_threshold_rejected() { - let cfg = BridgeConfig { activation_threshold: 1.5, lsm_weight: 0.5 }; + let cfg = BridgeConfig { + activation_threshold: 1.5, + lsm_weight: 0.5, + }; assert!(Bridge::new(cfg).is_err()); } #[test] fn aspect_invalid_weight_rejected() { - let cfg = BridgeConfig { activation_threshold: 0.5, lsm_weight: -0.1 }; + let cfg = BridgeConfig { + activation_threshold: 0.5, + lsm_weight: -0.1, + }; assert!(Bridge::new(cfg).is_err()); } diff --git a/crates/bridge/tests/integration_encode.rs b/crates/bridge/tests/integration_encode.rs index ea89b6a..131deab 100644 --- a/crates/bridge/tests/integration_encode.rs +++ b/crates/bridge/tests/integration_encode.rs @@ -28,7 +28,10 @@ fn salience_monotonic_in_active_fraction() { #[test] fn lsm_weight_zero_uses_only_esn() { - let cfg = BridgeConfig { lsm_weight: 0.0, ..Default::default() }; + let cfg = BridgeConfig { + lsm_weight: 0.0, + ..Default::default() + }; let mut b = Bridge::new(cfg).unwrap(); let lsm = Array1::from_elem(10, 1.0); let esn = Array1::zeros(10); @@ -38,7 +41,10 @@ fn lsm_weight_zero_uses_only_esn() { #[test] fn lsm_weight_one_uses_only_lsm() { - let cfg = BridgeConfig { lsm_weight: 1.0, ..Default::default() }; + let cfg = BridgeConfig { + lsm_weight: 1.0, + ..Default::default() + }; let mut b = Bridge::new(cfg).unwrap(); let lsm = Array1::zeros(10); let esn = Array1::from_elem(10, 1.0); diff --git a/crates/claude-client/benches/claude_bench.rs b/crates/claude-client/benches/claude_bench.rs index b5eb97a..a8ee68b 100644 --- a/crates/claude-client/benches/claude_bench.rs +++ b/crates/claude-client/benches/claude_bench.rs @@ -3,7 +3,8 @@ //! Benches for the Claude client (offline / routing only — no network). use claude_client::{HybridInference, Message}; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{criterion_group, criterion_main, Criterion}; +use std::hint::black_box; fn bench_complexity_estimation(c: &mut Criterion) { let h = HybridInference::new(None); @@ -28,5 +29,10 @@ fn bench_message_construction(c: &mut Criterion) { }); } -criterion_group!(benches, bench_complexity_estimation, bench_should_use_cloud, bench_message_construction); +criterion_group!( + benches, + bench_complexity_estimation, + bench_should_use_cloud, + bench_message_construction +); criterion_main!(benches); diff --git a/crates/claude-client/src/lib.rs b/crates/claude-client/src/lib.rs index acd1085..f289adc 100644 --- a/crates/claude-client/src/lib.rs +++ b/crates/claude-client/src/lib.rs @@ -11,13 +11,11 @@ //! - Rate limiting and retry logic #![forbid(unsafe_code)] -use futures::StreamExt; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::time::Duration; use thiserror::Error; -use tokio::sync::mpsc; -use tracing::{debug, error, info, warn}; +use tracing::{debug, warn}; /// Claude API errors #[derive(Error, Debug)] @@ -39,9 +37,10 @@ pub enum ClaudeError { } /// Claude model variants -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)] pub enum ClaudeModel { /// Claude 3.5 Sonnet - Best balance of intelligence and speed + #[default] #[serde(rename = "claude-sonnet-4-20250514")] Claude35Sonnet, /// Claude 3.5 Haiku - Fast and efficient @@ -52,12 +51,6 @@ pub enum ClaudeModel { Claude3Opus, } -impl Default for ClaudeModel { - fn default() -> Self { - ClaudeModel::Claude35Sonnet - } -} - impl ClaudeModel { pub fn as_str(&self) -> &'static str { match self { @@ -137,14 +130,18 @@ impl Message { pub fn user(text: &str) -> Self { Self { role: MessageRole::User, - content: vec![ContentBlock::Text { text: text.to_string() }], + content: vec![ContentBlock::Text { + text: text.to_string(), + }], } } pub fn assistant(text: &str) -> Self { Self { role: MessageRole::Assistant, - content: vec![ContentBlock::Text { text: text.to_string() }], + content: vec![ContentBlock::Text { + text: text.to_string(), + }], } } } @@ -201,6 +198,7 @@ struct ErrorResponse { #[derive(Debug, Deserialize)] struct ErrorDetail { #[serde(rename = "type")] + #[allow(dead_code)] error_type: String, message: String, } @@ -242,7 +240,8 @@ impl ClaudeClient { let response = self.create_message(messages, None).await?; // Extract text from response - let text = response.content + let text = response + .content .into_iter() .filter_map(|c| c.text) .collect::>() @@ -275,7 +274,8 @@ impl ClaudeClient { let messages = vec![Message::user(content)]; let response = self.create_message(messages, system).await?; - let text = response.content + let text = response + .content .into_iter() .filter_map(|c| c.text) .collect::>() @@ -288,12 +288,15 @@ impl ClaudeClient { pub async fn chat(&mut self, content: &str) -> Result { self.conversation_history.push(Message::user(content)); - let response = self.create_message( - self.conversation_history.clone(), - self.config.system_prompt.clone(), - ).await?; + let response = self + .create_message( + self.conversation_history.clone(), + self.config.system_prompt.clone(), + ) + .await?; - let text = response.content + let text = response + .content .into_iter() .filter_map(|c| c.text) .collect::>() @@ -333,7 +336,8 @@ impl ClaudeClient { tokio::time::sleep(delay).await; } - let response = self.client + let response = self + .client .post(&url) .header("x-api-key", api_key) .header("anthropic-version", "2023-06-01") @@ -355,7 +359,9 @@ impl ClaudeClient { .unwrap_or(60); warn!("Rate limited, retry after {}s", retry_after); - last_error = Some(ClaudeError::RateLimited { retry_after_secs: retry_after }); + last_error = Some(ClaudeError::RateLimited { + retry_after_secs: retry_after, + }); continue; } @@ -375,7 +381,9 @@ impl ClaudeClient { continue; } - let message_response: MessageResponse = resp.json().await + let message_response: MessageResponse = resp + .json() + .await .map_err(|e| ClaudeError::InvalidResponse(e.to_string()))?; return Ok(message_response); @@ -486,8 +494,16 @@ impl HybridInference { score += (len / 1000.0).min(0.3); // Complexity indicators - let complex_words = ["analyze", "explain", "compare", "synthesize", - "evaluate", "reason", "complex", "detailed"]; + let complex_words = [ + "analyze", + "explain", + "compare", + "synthesize", + "evaluate", + "reason", + "complex", + "detailed", + ]; for word in &complex_words { if query.to_lowercase().contains(word) { score += 0.1; @@ -514,7 +530,10 @@ mod tests { #[test] fn test_model_strings() { - assert_eq!(ClaudeModel::Claude35Sonnet.as_str(), "claude-sonnet-4-20250514"); + assert_eq!( + ClaudeModel::Claude35Sonnet.as_str(), + "claude-sonnet-4-20250514" + ); } #[test] diff --git a/crates/claude-client/tests/integration_routing.rs b/crates/claude-client/tests/integration_routing.rs index 852e521..8d7165c 100644 --- a/crates/claude-client/tests/integration_routing.rs +++ b/crates/claude-client/tests/integration_routing.rs @@ -14,7 +14,10 @@ fn point_to_point_message_construction() { #[test] fn aspect_offline_never_routes_to_cloud() { - let cfg = ClaudeConfig { api_key: Some("sk-test".into()), ..Default::default() }; + let cfg = ClaudeConfig { + api_key: Some("sk-test".into()), + ..Default::default() + }; let mut h = HybridInference::new(Some(cfg)); h.set_online(false); assert!(!h.should_use_cloud(0.99, true)); @@ -22,7 +25,10 @@ fn aspect_offline_never_routes_to_cloud() { #[test] fn aspect_no_local_always_cloud_when_online() { - let cfg = ClaudeConfig { api_key: Some("sk-test".into()), ..Default::default() }; + let cfg = ClaudeConfig { + api_key: Some("sk-test".into()), + ..Default::default() + }; let h = HybridInference::new(Some(cfg)); assert!(h.should_use_cloud(0.0, false)); } @@ -47,7 +53,7 @@ fn complexity_estimation_bounded() { let h = HybridInference::new(None); let huge = "analyze ".repeat(5000); let s = h.estimate_complexity(&huge); - assert!(s <= 1.0 && s >= 0.0); + assert!((0.0..=1.0).contains(&s)); } #[test] diff --git a/crates/esn/benches/esn_bench.rs b/crates/esn/benches/esn_bench.rs index 3b4a192..b6e5cf5 100644 --- a/crates/esn/benches/esn_bench.rs +++ b/crates/esn/benches/esn_bench.rs @@ -2,9 +2,10 @@ // SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell //! Benches for the ESN reservoir. -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use esn::{EchoStateNetwork, EsnConfig}; use ndarray::Array1; +use std::hint::black_box; fn cfg(size: usize, input: usize) -> EsnConfig { EsnConfig { diff --git a/crates/esn/src/lib.rs b/crates/esn/src/lib.rs index 1a2b58c..87dee23 100644 --- a/crates/esn/src/lib.rs +++ b/crates/esn/src/lib.rs @@ -110,21 +110,23 @@ impl EchoStateNetwork { config.reservoir_size, config.input_dim ); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // Create recurrent weight matrix with sparsity let recurrent_weights = Self::create_recurrent_weights(config.reservoir_size, config.sparsity, &mut rng); // Scale to achieve target spectral radius - let recurrent_weights = Self::scale_to_spectral_radius( - &recurrent_weights, - config.spectral_radius, - ); + let recurrent_weights = + Self::scale_to_spectral_radius(&recurrent_weights, config.spectral_radius); // Create input weight matrix - let input_weights = - Self::create_input_weights(config.reservoir_size, config.input_dim, config.input_scale, &mut rng); + let input_weights = Self::create_input_weights( + config.reservoir_size, + config.input_dim, + config.input_scale, + &mut rng, + ); // Initialize state let state = Array1::zeros(config.reservoir_size); @@ -144,18 +146,14 @@ impl EchoStateNetwork { } /// Create sparse recurrent weight matrix - fn create_recurrent_weights( - size: usize, - sparsity: f32, - rng: &mut impl Rng, - ) -> Array2 { + fn create_recurrent_weights(size: usize, sparsity: f32, rng: &mut impl Rng) -> Array2 { let mut weights = Array2::::zeros((size, size)); let dist = Normal::new(0.0, 1.0).expect("valid normal distribution"); let connection_prob = 1.0 - sparsity; for i in 0..size { for j in 0..size { - if rng.gen::() < connection_prob { + if rng.random::() < connection_prob { let w: f32 = rng.sample(dist); weights[[i, j]] = w; } @@ -270,7 +268,10 @@ impl EchoStateNetwork { } /// Process a sequence of inputs - pub fn process_sequence(&mut self, inputs: &[Array1]) -> Result>, EsnError> { + pub fn process_sequence( + &mut self, + inputs: &[Array1], + ) -> Result>, EsnError> { let mut outputs = Vec::with_capacity(inputs.len()); for input in inputs { outputs.push(self.step(input)); diff --git a/crates/esn/tests/integration_esn.rs b/crates/esn/tests/integration_esn.rs index 526bd97..c7a6e13 100644 --- a/crates/esn/tests/integration_esn.rs +++ b/crates/esn/tests/integration_esn.rs @@ -27,7 +27,9 @@ fn point_to_point_step_returns_state() { #[test] fn lifecycle_step_reset_step() { let mut e = EchoStateNetwork::new(small_cfg()).unwrap(); - for _ in 0..100 { let _ = e.step(&Array1::from_vec(vec![1.0, 0.5, 0.0, -0.5])); } + for _ in 0..100 { + let _ = e.step(&Array1::from_vec(vec![1.0, 0.5, 0.0, -0.5])); + } let s_before = e.get_state(); e.reset(); let s_after = e.get_state(); @@ -37,13 +39,19 @@ fn lifecycle_step_reset_step() { #[test] fn aspect_invalid_config_zero_size() { - let cfg = EsnConfig { reservoir_size: 0, ..small_cfg() }; + let cfg = EsnConfig { + reservoir_size: 0, + ..small_cfg() + }; assert!(EchoStateNetwork::new(cfg).is_err()); } #[test] fn aspect_invalid_leaking_rate() { - let cfg = EsnConfig { leaking_rate: 1.5, ..small_cfg() }; + let cfg = EsnConfig { + leaking_rate: 1.5, + ..small_cfg() + }; assert!(EchoStateNetwork::new(cfg).is_err()); } diff --git a/crates/llm/benches/llm_bench.rs b/crates/llm/benches/llm_bench.rs index e0edbf0..661fd20 100644 --- a/crates/llm/benches/llm_bench.rs +++ b/crates/llm/benches/llm_bench.rs @@ -2,8 +2,9 @@ // SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell //! Benches for the LLM mock backend (real backend benched out-of-tree). -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use llm::{LlmBackend, LlmConfig, MockBackend}; +use std::hint::black_box; fn bench_generate(c: &mut Criterion) { let mut g = c.benchmark_group("llm_mock_generate"); diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs index 9acd7b2..14b04c9 100644 --- a/crates/llm/src/lib.rs +++ b/crates/llm/src/lib.rs @@ -49,10 +49,14 @@ impl LlmConfig { return Err(LlmError::InvalidConfig("n_threads must be > 0".into())); } if self.context_size == 0 || self.max_tokens == 0 { - return Err(LlmError::InvalidConfig("context_size/max_tokens must be > 0".into())); + return Err(LlmError::InvalidConfig( + "context_size/max_tokens must be > 0".into(), + )); } if self.max_tokens > self.context_size { - return Err(LlmError::InvalidConfig("max_tokens cannot exceed context_size".into())); + return Err(LlmError::InvalidConfig( + "max_tokens cannot exceed context_size".into(), + )); } Ok(()) } @@ -85,16 +89,29 @@ pub struct MockBackend { impl MockBackend { pub fn new(config: LlmConfig) -> Result { config.validate()?; - Ok(Self { config, loaded: false, calls: AtomicU64::new(0) }) + Ok(Self { + config, + loaded: false, + calls: AtomicU64::new(0), + }) } - pub fn call_count(&self) -> u64 { self.calls.load(Ordering::SeqCst) } + pub fn config(&self) -> &LlmConfig { + &self.config + } + pub fn call_count(&self) -> u64 { + self.calls.load(Ordering::SeqCst) + } } impl LlmBackend for MockBackend { - fn name(&self) -> &'static str { "mock" } + fn name(&self) -> &'static str { + "mock" + } - fn is_loaded(&self) -> bool { self.loaded } + fn is_loaded(&self) -> bool { + self.loaded + } fn load(&mut self) -> Result<(), LlmError> { self.loaded = true; @@ -102,7 +119,9 @@ impl LlmBackend for MockBackend { } fn generate(&mut self, prompt: &str, max_tokens: u32) -> Result { - if !self.loaded { return Err(LlmError::NotLoaded); } + if !self.loaded { + return Err(LlmError::NotLoaded); + } if prompt.trim().is_empty() { return Err(LlmError::InvalidPrompt("empty prompt".into())); } @@ -110,30 +129,50 @@ impl LlmBackend for MockBackend { return Err(LlmError::InvalidConfig("max_tokens=0".into())); } self.calls.fetch_add(1, Ordering::SeqCst); - let words: Vec<&str> = prompt.split_whitespace().take(max_tokens as usize).collect(); + let words: Vec<&str> = prompt + .split_whitespace() + .take(max_tokens as usize) + .collect(); let text = format!("(local-llama-mock) echo: {}", words.join(" ")); let tokens_emitted = words.len() as u32; - Ok(LlmResponse { text, tokens_emitted, elapsed_ms: 1 }) + Ok(LlmResponse { + text, + tokens_emitted, + elapsed_ms: 1, + }) } - fn unload(&mut self) { self.loaded = false; } + fn unload(&mut self) { + self.loaded = false; + } } -pub fn hello() -> &'static str { "llm" } +pub fn hello() -> &'static str { + "llm" +} #[cfg(test)] mod tests { use super::*; - #[test] fn config_validation() { + #[test] + fn config_validation() { assert!(LlmConfig::default().validate().is_ok()); - let bad = LlmConfig { n_threads: 0, ..Default::default() }; + let bad = LlmConfig { + n_threads: 0, + ..Default::default() + }; assert!(bad.validate().is_err()); - let bad2 = LlmConfig { max_tokens: 9999, context_size: 100, ..Default::default() }; + let bad2 = LlmConfig { + max_tokens: 9999, + context_size: 100, + ..Default::default() + }; assert!(bad2.validate().is_err()); } - #[test] fn mock_lifecycle() { + #[test] + fn mock_lifecycle() { let mut b = MockBackend::new(LlmConfig::default()).unwrap(); assert!(!b.is_loaded()); b.load().unwrap(); @@ -145,27 +184,39 @@ mod tests { assert!(!b.is_loaded()); } - #[test] fn generate_rejects_unloaded() { + #[test] + fn generate_rejects_unloaded() { let mut b = MockBackend::new(LlmConfig::default()).unwrap(); assert!(matches!(b.generate("x", 1), Err(LlmError::NotLoaded))); } - #[test] fn generate_rejects_empty_prompt() { + #[test] + fn generate_rejects_empty_prompt() { let mut b = MockBackend::new(LlmConfig::default()).unwrap(); b.load().unwrap(); - assert!(matches!(b.generate(" ", 5), Err(LlmError::InvalidPrompt(_)))); + assert!(matches!( + b.generate(" ", 5), + Err(LlmError::InvalidPrompt(_)) + )); } - #[test] fn generate_rejects_zero_tokens() { + #[test] + fn generate_rejects_zero_tokens() { let mut b = MockBackend::new(LlmConfig::default()).unwrap(); b.load().unwrap(); - assert!(matches!(b.generate("ok", 0), Err(LlmError::InvalidConfig(_)))); + assert!(matches!( + b.generate("ok", 0), + Err(LlmError::InvalidConfig(_)) + )); } - #[test] fn call_count_increments() { + #[test] + fn call_count_increments() { let mut b = MockBackend::new(LlmConfig::default()).unwrap(); b.load().unwrap(); - for _ in 0..3 { b.generate("a b c", 5).unwrap(); } + for _ in 0..3 { + b.generate("a b c", 5).unwrap(); + } assert_eq!(b.call_count(), 3); } } diff --git a/crates/llm/tests/integration_backend.rs b/crates/llm/tests/integration_backend.rs index e6cdea8..80b871f 100644 --- a/crates/llm/tests/integration_backend.rs +++ b/crates/llm/tests/integration_backend.rs @@ -45,7 +45,9 @@ fn aspect_concurrent_calls_via_arc_mutex() { guard.generate(&prompt, 5).unwrap(); })); } - for h in handles { h.join().unwrap(); } + for h in handles { + h.join().unwrap(); + } assert_eq!(backend.lock().unwrap().call_count(), 8); } diff --git a/crates/lsm/benches/lsm_bench.rs b/crates/lsm/benches/lsm_bench.rs index 3641daa..2b2fddd 100644 --- a/crates/lsm/benches/lsm_bench.rs +++ b/crates/lsm/benches/lsm_bench.rs @@ -12,9 +12,10 @@ //! - `LiquidStateMachine::reset` — reservoir state clear between sessions //! - Full pipeline: N steps + firing rate extraction -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use lsm::{LiquidStateMachine, LsmConfig}; use ndarray::Array1; +use std::hint::black_box; // ============================================================================ // Helpers @@ -95,9 +96,7 @@ fn bench_step_scaling(c: &mut Criterion) { group.bench_with_input( BenchmarkId::from_parameter(n_neurons), &n_neurons, - |b, _| { - b.iter(|| black_box(lsm.step(black_box(&input)))) - }, + |b, _| b.iter(|| black_box(lsm.step(black_box(&input)))), ); } group.finish(); @@ -136,9 +135,7 @@ fn bench_get_state_window_scaling(c: &mut Criterion) { group.bench_with_input( BenchmarkId::from_parameter(window_ms as u64), &window_ms, - |b, &w| { - b.iter(|| black_box(lsm.get_state(black_box(w)))) - }, + |b, &w| b.iter(|| black_box(lsm.get_state(black_box(w)))), ); } group.finish(); diff --git a/crates/lsm/src/lib.rs b/crates/lsm/src/lib.rs index dcf4f94..4187f95 100644 --- a/crates/lsm/src/lib.rs +++ b/crates/lsm/src/lib.rs @@ -9,7 +9,7 @@ use ndarray::{Array1, Array2, Axis}; use ndarray_rand::RandomExt; use rand::Rng; -use rand_distr::{Normal, Uniform, Bernoulli}; +use rand_distr::{Bernoulli, Normal, Uniform}; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use thiserror::Error; @@ -143,12 +143,17 @@ impl LiquidStateMachine { let n_neurons = nx * ny * nz; if n_neurons == 0 { - return Err(LsmError::InvalidConfig("Grid dimensions must be positive".into())); + return Err(LsmError::InvalidConfig( + "Grid dimensions must be positive".into(), + )); } - debug!("Creating LSM with {} neurons ({} x {} x {})", n_neurons, nx, ny, nz); + debug!( + "Creating LSM with {} neurons ({} x {} x {})", + n_neurons, nx, ny, nz + ); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let lif_params = LifParameters::default(); // Initialize neurons @@ -161,7 +166,7 @@ impl LiquidStateMachine { let z = idx / (nx * ny); neurons.push(NeuronState { - v: lif_params.v_rest + rng.gen::() * 5.0, // Small random offset + v: lif_params.v_rest + rng.random::() * 5.0, // Small random offset refrac_remaining: 0.0, is_inhibitory: idx < n_inhibitory, position: (x, y, z), @@ -184,7 +189,8 @@ impl LiquidStateMachine { let weights = Self::create_recurrent_weights(&config, &neurons, &mut rng); // Create input weights - let input_weights = Self::create_input_weights(n_neurons, input_dim, config.input_scale, &mut rng); + let input_weights = + Self::create_input_weights(n_neurons, input_dim, config.input_scale, &mut rng); // Initialize spike history let spike_history = vec![VecDeque::with_capacity(100); n_neurons]; @@ -226,7 +232,8 @@ impl LiquidStateMachine { let (xj, yj, zj) = neurons[j].position; let dist = (((xi as i32 - xj as i32).pow(2) + (yi as i32 - yj as i32).pow(2) - + (zi as i32 - zj as i32).pow(2)) as f32).sqrt(); + + (zi as i32 - zj as i32).pow(2)) as f32) + .sqrt(); // Distance-dependent connection probability let p_connect = if neurons[i].is_inhibitory { @@ -235,7 +242,7 @@ impl LiquidStateMachine { config.p_exc * (-dist / (max_dist * 0.5)).exp() }; - if rng.gen::() < p_connect { + if rng.random::() < p_connect { let w: f32 = rng.sample(weight_dist); let w = w.abs().max(0.01); // Ensure positive base weight @@ -246,7 +253,10 @@ impl LiquidStateMachine { } // Scale weights to achieve target spectral radius - let eigenvalue_estimate = weights.mapv(|x| x.abs()).sum_axis(Axis(1)).into_iter() + let eigenvalue_estimate = weights + .mapv(|x| x.abs()) + .sum_axis(Axis(1)) + .into_iter() .fold(0.0f32, |a, b| a.max(b)); if eigenvalue_estimate > 0.0 { @@ -290,7 +300,9 @@ impl LiquidStateMachine { let expected = self.input_weights.ncols(); let mut padded = Array1::zeros(expected); let copy_len = input.len().min(expected); - padded.slice_mut(ndarray::s![..copy_len]).assign(&input.slice(ndarray::s![..copy_len])); + padded + .slice_mut(ndarray::s![..copy_len]) + .assign(&input.slice(ndarray::s![..copy_len])); return self.step(&padded); } @@ -298,15 +310,17 @@ impl LiquidStateMachine { let input_current = self.input_weights.dot(input); // Get previous spike indicators for recurrent input - let spike_indicators: Array1 = Array1::from_iter( - self.neurons.iter().map(|_n| { - if let Some(&last_spike) = self.spike_history[0].back() { - if self.current_time - last_spike < dt as f64 { 1.0 } else { 0.0 } + let spike_indicators: Array1 = Array1::from_iter(self.neurons.iter().map(|_n| { + if let Some(&last_spike) = self.spike_history[0].back() { + if self.current_time - last_spike < dt as f64 { + 1.0 } else { 0.0 } - }) - ); + } else { + 0.0 + } + })); // Calculate recurrent currents let recurrent_current = self.weights.t().dot(&spike_indicators); @@ -326,8 +340,7 @@ impl LiquidStateMachine { // LIF dynamics: tau_m * dV/dt = -(V - V_rest) + R_m * I let dv = dt / self.lif_params.tau_m - * (-(neuron.v - self.lif_params.v_rest) - + self.lif_params.r_m * total_current[idx]); + * (-(neuron.v - self.lif_params.v_rest) + self.lif_params.r_m * total_current[idx]); neuron.v += dv; @@ -379,25 +392,21 @@ impl LiquidStateMachine { /// Get the current firing rates (spikes per window) pub fn get_firing_rates(&self, window_ms: f64) -> Array1 { - Array1::from_iter( - self.spike_history.iter().map(|history| { - let count = history.iter() - .filter(|&&t| self.current_time - t <= window_ms) - .count(); - count as f32 / (window_ms / 1000.0) as f32 // Convert to Hz - }) - ) + Array1::from_iter(self.spike_history.iter().map(|history| { + let count = history + .iter() + .filter(|&&t| self.current_time - t <= window_ms) + .count(); + count as f32 / (window_ms / 1000.0) as f32 // Convert to Hz + })) } /// Get state vector (normalized membrane potentials) pub fn get_state(&self, window_ms: f64) -> Array1 { // Combine membrane potentials and firing rates - let potentials = Array1::from_iter( - self.neurons.iter().map(|n| { - (n.v - self.lif_params.v_rest) / - (self.lif_params.v_thresh - self.lif_params.v_rest) - }) - ); + let potentials = Array1::from_iter(self.neurons.iter().map(|n| { + (n.v - self.lif_params.v_rest) / (self.lif_params.v_thresh - self.lif_params.v_rest) + })); let rates = self.get_firing_rates(window_ms); let max_rate = rates.iter().cloned().fold(1.0f32, f32::max); @@ -419,10 +428,10 @@ impl LiquidStateMachine { /// Reset the LSM to initial state pub fn reset(&mut self) { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); for neuron in &mut self.neurons { - neuron.v = self.lif_params.v_rest + rng.gen::() * 5.0; + neuron.v = self.lif_params.v_rest + rng.random::() * 5.0; neuron.refrac_remaining = 0.0; } @@ -453,6 +462,7 @@ impl LiquidStateMachine { #[cfg(test)] mod tests { use super::*; + #[allow(unused_imports)] use approx::assert_relative_eq; #[test] diff --git a/crates/lsm/tests/integration_lsm.rs b/crates/lsm/tests/integration_lsm.rs index 8599eef..5f0d56e 100644 --- a/crates/lsm/tests/integration_lsm.rs +++ b/crates/lsm/tests/integration_lsm.rs @@ -7,7 +7,10 @@ use ndarray::Array1; #[test] fn small_lsm_constructs_and_steps() { - let cfg = LsmConfig { dimensions: (4, 4, 4), ..Default::default() }; + let cfg = LsmConfig { + dimensions: (4, 4, 4), + ..Default::default() + }; let mut m = LiquidStateMachine::new(cfg, 3).expect("lsm"); let state = m.step(&Array1::from_vec(vec![0.1, 0.2, 0.3])); assert_eq!(state.len(), 4 * 4 * 4); @@ -15,9 +18,14 @@ fn small_lsm_constructs_and_steps() { #[test] fn lifecycle_step_then_reset() { - let cfg = LsmConfig { dimensions: (3, 3, 3), ..Default::default() }; + let cfg = LsmConfig { + dimensions: (3, 3, 3), + ..Default::default() + }; let mut m = LiquidStateMachine::new(cfg, 2).expect("lsm"); - for _ in 0..50 { let _ = m.step(&Array1::from_vec(vec![0.1, 0.2])); } + for _ in 0..50 { + let _ = m.step(&Array1::from_vec(vec![0.1, 0.2])); + } m.reset(); let state = m.step(&Array1::from_vec(vec![0.0, 0.0])); assert_eq!(state.len(), 27); @@ -25,7 +33,10 @@ fn lifecycle_step_then_reset() { #[test] fn aspect_input_padding_no_panic() { - let cfg = LsmConfig { dimensions: (2, 2, 2), ..Default::default() }; + let cfg = LsmConfig { + dimensions: (2, 2, 2), + ..Default::default() + }; let mut m = LiquidStateMachine::new(cfg, 3).expect("lsm"); // input shorter than expected — should pad and not panic let s = m.step(&Array1::from_vec(vec![0.1, 0.2])); @@ -34,7 +45,10 @@ fn aspect_input_padding_no_panic() { #[test] fn aspect_long_run_state_finite() { - let cfg = LsmConfig { dimensions: (4, 4, 4), ..Default::default() }; + let cfg = LsmConfig { + dimensions: (4, 4, 4), + ..Default::default() + }; let mut m = LiquidStateMachine::new(cfg, 3).expect("lsm"); for i in 0..500 { let phase = (i as f32) * 0.05; @@ -45,9 +59,14 @@ fn aspect_long_run_state_finite() { #[test] fn lifecycle_firing_rate_stable_after_warmup() { - let cfg = LsmConfig { dimensions: (3, 3, 3), ..Default::default() }; + let cfg = LsmConfig { + dimensions: (3, 3, 3), + ..Default::default() + }; let mut m = LiquidStateMachine::new(cfg, 2).expect("lsm"); - for _ in 0..200 { let _ = m.step(&Array1::from_vec(vec![0.5, 0.5])); } + for _ in 0..200 { + let _ = m.step(&Array1::from_vec(vec![0.5, 0.5])); + } let rates = m.get_firing_rates(50.0); assert_eq!(rates.len(), 27); assert!(rates.iter().all(|v| v.is_finite() && *v >= 0.0)); diff --git a/crates/neurophone-core/benches/neurophone_bench.rs b/crates/neurophone-core/benches/neurophone_bench.rs index eecd28a..d54a2b7 100644 --- a/crates/neurophone-core/benches/neurophone_bench.rs +++ b/crates/neurophone-core/benches/neurophone_bench.rs @@ -4,11 +4,12 @@ //! Benchmarks for neurophone-core -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use lsm::{LiquidStateMachine, LsmConfig}; +use criterion::{criterion_group, criterion_main, Criterion}; use esn::{EchoStateNetwork, EsnConfig}; +use lsm::{LiquidStateMachine, LsmConfig}; use ndarray::Array1; use neurophone_core::*; +use std::hint::black_box; // ========== LSM Benchmarks ========== @@ -43,9 +44,7 @@ fn bench_lsm_step(c: &mut Criterion) { let mut lsm = LiquidStateMachine::new(config, 10).expect("LSM creation"); let input = Array1::from_vec(vec![0.5; 10]); - b.iter(|| { - lsm.step(black_box(&input)) - }) + b.iter(|| lsm.step(black_box(&input))) }); c.bench_function("lsm_step_20x20x20", |b| { @@ -56,9 +55,7 @@ fn bench_lsm_step(c: &mut Criterion) { let mut lsm = LiquidStateMachine::new(config, 20).expect("LSM creation"); let input = Array1::from_vec(vec![0.5; 20]); - b.iter(|| { - lsm.step(black_box(&input)) - }) + b.iter(|| lsm.step(black_box(&input))) }); } @@ -90,9 +87,7 @@ fn bench_lsm_get_state(c: &mut Criterion) { lsm.step(&input); } - b.iter(|| { - lsm.get_state(100.0) - }) + b.iter(|| lsm.get_state(100.0)) }); } @@ -132,9 +127,7 @@ fn bench_esn_step(c: &mut Criterion) { let mut esn = EchoStateNetwork::new(config).expect("ESN creation"); let input = Array1::from_vec(vec![0.1; 50]); - b.iter(|| { - esn.step(black_box(&input)) - }) + b.iter(|| esn.step(black_box(&input))) }); c.bench_function("esn_step_512", |b| { @@ -146,9 +139,7 @@ fn bench_esn_step(c: &mut Criterion) { let mut esn = EchoStateNetwork::new(config).expect("ESN creation"); let input = Array1::from_vec(vec![0.1; 50]); - b.iter(|| { - esn.step(black_box(&input)) - }) + b.iter(|| esn.step(black_box(&input))) }); } @@ -190,30 +181,26 @@ fn bench_esn_process_sequence(c: &mut Criterion) { fn bench_system_query(c: &mut Criterion) { c.bench_function("system_query_short", |b| { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); - b.iter(|| { - system.query(black_box("hello world"), true).ok() - }) + b.iter(|| system.query(black_box("hello world"), true).ok()) }); c.bench_function("system_query_long", |b| { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let long_query = "word ".repeat(50); - b.iter(|| { - system.query(black_box(&long_query), true).ok() - }) + b.iter(|| system.query(black_box(&long_query), true).ok()) }); } fn bench_system_sensor_processing(c: &mut Criterion) { c.bench_function("system_sensor_processing", |b| { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); let event = SensorEvent { @@ -222,17 +209,15 @@ fn bench_system_sensor_processing(c: &mut Criterion) { values: vec![1.0, 2.0, 3.0], }; - b.iter(|| { - system.process_sensor_event(black_box(&event)).ok() - }) + b.iter(|| system.process_sensor_event(black_box(&event)).ok()) }); } fn bench_system_lifecycle(c: &mut Criterion) { c.bench_function("system_create_init_shutdown", |b| { b.iter(|| { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); system.shutdown().expect("shutdown"); }) @@ -241,12 +226,9 @@ fn bench_system_lifecycle(c: &mut Criterion) { fn bench_system_state_access(c: &mut Criterion) { c.bench_function("system_state_access", |b| { - let system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let system = NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); - b.iter(|| { - system.get_state() - }) + b.iter(|| system.get_state()) }); } @@ -262,9 +244,7 @@ fn bench_serialization(c: &mut Criterion) { confidence: 0.92, }; - b.iter(|| { - serde_json::to_string(black_box(&result)).ok() - }) + b.iter(|| serde_json::to_string(black_box(&result)).ok()) }); c.bench_function("serialize_system_state", |b| { @@ -275,9 +255,7 @@ fn bench_serialization(c: &mut Criterion) { ..Default::default() }; - b.iter(|| { - serde_json::to_string(black_box(&state)).ok() - }) + b.iter(|| serde_json::to_string(black_box(&state)).ok()) }); c.bench_function("deserialize_inference_result", |b| { @@ -294,8 +272,8 @@ fn bench_serialization(c: &mut Criterion) { fn bench_end_to_end(c: &mut Criterion) { c.bench_function("e2e_sensor_to_query", |b| { b.iter(|| { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); let event = SensorEvent { diff --git a/crates/neurophone-core/benches/pipeline_bench.rs b/crates/neurophone-core/benches/pipeline_bench.rs index 0609ef4..8a16b1f 100644 --- a/crates/neurophone-core/benches/pipeline_bench.rs +++ b/crates/neurophone-core/benches/pipeline_bench.rs @@ -3,23 +3,44 @@ //! End-to-end pipeline bench: sensor → LSM → ESN → bridge → LLM mock. use bridge::{Bridge, BridgeConfig}; -use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput}; +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use esn::{EchoStateNetwork, EsnConfig}; use llm::{LlmBackend, LlmConfig, MockBackend}; use lsm::{LiquidStateMachine, LsmConfig}; use sensors::{PipelineConfig, SensorKind, SensorPipeline, SensorReading}; +use std::hint::black_box; -fn warmed_pipeline() -> (SensorPipeline, LiquidStateMachine, EchoStateNetwork, Bridge, MockBackend) { +fn warmed_pipeline() -> ( + SensorPipeline, + LiquidStateMachine, + EchoStateNetwork, + Bridge, + MockBackend, +) { let mut sp = SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); for i in 0..50u64 { - sp.ingest(&SensorReading::new(SensorKind::Accelerometer, i * 20, vec![0.1, 0.2, 9.81]).unwrap()).unwrap(); + sp.ingest( + &SensorReading::new(SensorKind::Accelerometer, i * 20, vec![0.1, 0.2, 9.81]).unwrap(), + ) + .unwrap(); } - let lsm = LiquidStateMachine::new(LsmConfig { dimensions: (3, 3, 3), ..Default::default() }, 7).unwrap(); + let lsm = LiquidStateMachine::new( + LsmConfig { + dimensions: (3, 3, 3), + ..Default::default() + }, + 7, + ) + .unwrap(); let esn = EchoStateNetwork::new(EsnConfig { - reservoir_size: 64, input_dim: 27, - spectral_radius: 0.9, input_scale: 1.0, - sparsity: 0.9, leaking_rate: 0.3, - }).unwrap(); + reservoir_size: 64, + input_dim: 27, + spectral_radius: 0.9, + input_scale: 1.0, + sparsity: 0.9, + leaking_rate: 0.3, + }) + .unwrap(); let br = Bridge::new(BridgeConfig::default()).unwrap(); let mut llm = MockBackend::new(LlmConfig::default()).unwrap(); llm.load().unwrap(); diff --git a/crates/neurophone-core/src/lib.rs b/crates/neurophone-core/src/lib.rs index 3aa0066..ca11b6f 100644 --- a/crates/neurophone-core/src/lib.rs +++ b/crates/neurophone-core/src/lib.rs @@ -18,12 +18,10 @@ #![forbid(unsafe_code)] -#![forbid(unsafe_code)] use ndarray::Array1; use serde::{Deserialize, Serialize}; use std::fmt; -use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Instant; use thiserror::Error; use tracing::{debug, info, warn}; @@ -76,7 +74,7 @@ pub struct SensorEvent { } /// System state snapshot -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct SystemState { /// Current timestamp pub timestamp_ms: u64, @@ -90,18 +88,6 @@ pub struct SystemState { pub latency_ms: u32, } -impl Default for SystemState { - fn default() -> Self { - Self { - timestamp_ms: 0, - lsm_state: None, - esn_state: None, - is_active: false, - latency_ms: 0, - } - } -} - /// Neural output event #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NeuralOutput { @@ -191,23 +177,18 @@ impl NeuroSymbolicSystem { } /// Process a sensor event - pub fn process_sensor_event(&mut self, event: &SensorEvent) -> Result { + pub fn process_sensor_event( + &mut self, + event: &SensorEvent, + ) -> Result { if !self.state.is_active { - return Err(NeurophoneError::RuntimeError( - "System not active".into(), - )); + return Err(NeurophoneError::RuntimeError("System not active".into())); } let start = Instant::now(); // Simulate neural processing - let features = Array1::from_vec( - event - .values - .iter() - .map(|v| v * 0.9) - .collect(), - ); + let features = Array1::from_vec(event.values.iter().map(|v| v * 0.9).collect()); let latency = start.elapsed().as_millis() as u32; self.state.latency_ms = latency; @@ -326,16 +307,16 @@ mod tests { #[test] fn test_system_initialization() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); assert!(system.state.is_active); } #[test] fn test_system_shutdown() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); system.shutdown().expect("shutdown"); assert!(!system.state.is_active); @@ -354,8 +335,8 @@ mod tests { #[test] fn test_process_sensor_event() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); let event = SensorEvent { @@ -371,16 +352,16 @@ mod tests { #[test] fn test_query_empty() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let result = system.query("", true); assert!(result.is_err()); } #[test] fn test_query_local_preference() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let result = system.query("hello world", true).expect("query"); assert_eq!(result.model, InferenceModel::LocalLlama); @@ -391,7 +372,8 @@ mod tests { let mut system = NeuroSymbolicSystem::new(SystemConfig { local_threshold: 0.5, ..Default::default() - }).expect("system creation"); + }) + .expect("system creation"); let long_query = "hello ".repeat(50); let result = system.query(&long_query, true).expect("query"); @@ -400,8 +382,8 @@ mod tests { #[test] fn test_query_count() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); assert_eq!(system.query_count(), 0); system.query("test", true).ok(); @@ -412,10 +394,9 @@ mod tests { #[test] fn test_uptime() { - let system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let system = NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let uptime = system.uptime_ms(); - assert!(uptime >= 0); + let _ = uptime; // u128, always defined; just verify no panic } #[test] @@ -474,8 +455,8 @@ mod tests { #[test] fn test_multiple_queries() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); for i in 0..5 { let query = format!("query {}", i); @@ -487,8 +468,8 @@ mod tests { #[test] fn test_multiple_sensor_events() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); for i in 0..10 { @@ -522,7 +503,9 @@ mod tests { values: vec![1.5, 2.0, 2.5], }; - let neural_out = system.process_sensor_event(&event).expect("sensor processing"); + let neural_out = system + .process_sensor_event(&event) + .expect("sensor processing"); assert_eq!(neural_out.timestamp_ms, 1000); // Features -> Query @@ -533,11 +516,11 @@ mod tests { #[test] fn test_e2e_sequence_processing() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); - let sensor_types = vec!["accelerometer", "gyroscope", "magnetometer"]; + let sensor_types = ["accelerometer", "gyroscope", "magnetometer"]; let mut last_output = None; for (i, sensor_type) in sensor_types.iter().enumerate() { @@ -559,8 +542,8 @@ mod tests { #[test] fn test_state_preservation() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); let event = SensorEvent { @@ -581,8 +564,8 @@ mod tests { #[test] fn test_deterministic_inference() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let query = "deterministic test"; let r1 = system.query(query, true).expect("query 1"); @@ -598,7 +581,8 @@ mod tests { let mut system = NeuroSymbolicSystem::new(SystemConfig { local_threshold: 0.5, ..Default::default() - }).expect("system creation"); + }) + .expect("system creation"); // Short query should use local let short = "hi"; @@ -615,8 +599,8 @@ mod tests { #[test] fn test_query_response_validity() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let result = system.query("test", true).expect("query"); @@ -625,13 +609,13 @@ mod tests { // Contract: confidence should be in [0.0, 1.0] assert!(result.confidence >= 0.0 && result.confidence <= 1.0); // Contract: latency should be non-negative - assert!(result.latency_ms >= 0); + let _ = result.latency_ms; // u32, always >= 0 } #[test] fn test_sensor_event_validity() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); let event = SensorEvent { @@ -652,8 +636,8 @@ mod tests { #[test] fn test_security_malformed_input() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); system.initialize().expect("init"); let event = SensorEvent { @@ -669,8 +653,8 @@ mod tests { #[test] fn test_performance_latency_bound() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let start = Instant::now(); system.query("test", true).ok(); @@ -682,8 +666,8 @@ mod tests { #[test] fn test_error_handling_inactive_system() { - let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) - .expect("system creation"); + let mut system = + NeuroSymbolicSystem::new(SystemConfig::default()).expect("system creation"); let event = SensorEvent { sensor_type: "test".to_string(), @@ -701,7 +685,8 @@ mod tests { let mut system = NeuroSymbolicSystem::new(SystemConfig { max_response_time_ms: 10, ..Default::default() - }).expect("system creation"); + }) + .expect("system creation"); // Even with tight timing, should complete let result = system.query("test query", true); diff --git a/crates/neurophone-core/tests/aspect_concurrency.rs b/crates/neurophone-core/tests/aspect_concurrency.rs index 64a27f1..dd3dbf2 100644 --- a/crates/neurophone-core/tests/aspect_concurrency.rs +++ b/crates/neurophone-core/tests/aspect_concurrency.rs @@ -9,7 +9,9 @@ use std::time::{Duration, Instant}; #[test] fn aspect_concurrent_queries_via_mutex() { - let sys = Arc::new(Mutex::new(NeuroSymbolicSystem::new(SystemConfig::default()).unwrap())); + let sys = Arc::new(Mutex::new( + NeuroSymbolicSystem::new(SystemConfig::default()).unwrap(), + )); let mut handles = vec![]; for i in 0..16 { let sys = sys.clone(); @@ -18,13 +20,17 @@ fn aspect_concurrent_queries_via_mutex() { sys.lock().unwrap().query(&q, true).unwrap(); })); } - for h in handles { h.join().unwrap(); } + for h in handles { + h.join().unwrap(); + } assert_eq!(sys.lock().unwrap().query_count(), 16); } #[test] fn aspect_concurrent_sensor_events() { - let sys = Arc::new(Mutex::new(NeuroSymbolicSystem::new(SystemConfig::default()).unwrap())); + let sys = Arc::new(Mutex::new( + NeuroSymbolicSystem::new(SystemConfig::default()).unwrap(), + )); sys.lock().unwrap().initialize().unwrap(); let mut handles = vec![]; for i in 0..32u64 { @@ -38,7 +44,9 @@ fn aspect_concurrent_sensor_events() { let _ = sys.lock().unwrap().process_sensor_event(&event); })); } - for h in handles { h.join().unwrap(); } + for h in handles { + h.join().unwrap(); + } } #[test] @@ -60,7 +68,9 @@ fn aspect_error_path_empty_query() { fn aspect_error_path_inactive_system_event() { let mut sys = NeuroSymbolicSystem::new(SystemConfig::default()).unwrap(); let e = SensorEvent { - sensor_type: "x".into(), timestamp_ms: 0, values: vec![0.0], + sensor_type: "x".into(), + timestamp_ms: 0, + values: vec![0.0], }; assert!(sys.process_sensor_event(&e).is_err()); } diff --git a/crates/neurophone-core/tests/integration_pipeline.rs b/crates/neurophone-core/tests/integration_pipeline.rs index 7ba8d35..02d45f8 100644 --- a/crates/neurophone-core/tests/integration_pipeline.rs +++ b/crates/neurophone-core/tests/integration_pipeline.rs @@ -10,12 +10,22 @@ use lsm::{LiquidStateMachine, LsmConfig}; use ndarray::Array1; use sensors::{PipelineConfig, SensorKind, SensorPipeline, SensorReading}; -fn build_pipeline() -> (SensorPipeline, LiquidStateMachine, EchoStateNetwork, Bridge, MockBackend) { +fn build_pipeline() -> ( + SensorPipeline, + LiquidStateMachine, + EchoStateNetwork, + Bridge, + MockBackend, +) { let sp = SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); let lsm = LiquidStateMachine::new( - LsmConfig { dimensions: (3, 3, 3), ..Default::default() }, + LsmConfig { + dimensions: (3, 3, 3), + ..Default::default() + }, 7, - ).unwrap(); + ) + .unwrap(); let esn = EchoStateNetwork::new(EsnConfig { reservoir_size: 64, input_dim: 27, @@ -23,7 +33,8 @@ fn build_pipeline() -> (SensorPipeline, LiquidStateMachine, EchoStateNetwork, Br input_scale: 1.0, sparsity: 0.9, leaking_rate: 0.3, - }).unwrap(); + }) + .unwrap(); let bridge = Bridge::new(BridgeConfig::default()).unwrap(); let mut llm = MockBackend::new(LlmConfig::default()).unwrap(); llm.load().unwrap(); @@ -40,7 +51,8 @@ fn end_to_end_single_step() { SensorKind::Accelerometer, i as u64 * 20, vec![(i as f32 * 0.1).sin(), (i as f32 * 0.1).cos(), 9.81], - ).unwrap(); + ) + .unwrap(); sp.ingest(&r).unwrap(); } @@ -66,7 +78,8 @@ fn end_to_end_50hz_one_second_loop() { SensorKind::Accelerometer, i as u64 * 20, vec![(i as f32 * 0.2).sin(), 0.0, 9.81], - ).unwrap(); + ) + .unwrap(); sp.ingest(&r).unwrap(); if i >= 25 { let f = sp.features().unwrap(); @@ -88,7 +101,12 @@ fn end_to_end_quiet_input_yields_quiet_description() { let (mut sp, mut lsm, mut esn, mut br, _llm) = build_pipeline(); // Constant gravity = no motion variance. for i in 0..200 { - let r = SensorReading::new(SensorKind::Accelerometer, i as u64 * 20, vec![0.0, 0.0, 9.81]).unwrap(); + let r = SensorReading::new( + SensorKind::Accelerometer, + i as u64 * 20, + vec![0.0, 0.0, 9.81], + ) + .unwrap(); sp.ingest(&r).unwrap(); } let f = sp.features().unwrap(); @@ -103,7 +121,10 @@ fn end_to_end_quiet_input_yields_quiet_description() { #[test] fn end_to_end_dimension_negotiation() { // Verify shapes line up: features.len → lsm.input → lsm.size → esn.input. - let cfg_lsm = LsmConfig { dimensions: (4, 4, 4), ..Default::default() }; + let cfg_lsm = LsmConfig { + dimensions: (4, 4, 4), + ..Default::default() + }; let mut lsm = LiquidStateMachine::new(cfg_lsm, 7).unwrap(); let lsm_state = lsm.step(&Array1::from_vec(vec![0.0; 7])); assert_eq!(lsm_state.len(), 64); @@ -115,7 +136,8 @@ fn end_to_end_dimension_negotiation() { input_scale: 1.0, sparsity: 0.9, leaking_rate: 0.3, - }).unwrap(); + }) + .unwrap(); let esn_state = esn.step(&lsm_state); assert_eq!(esn_state.len(), 128); } diff --git a/crates/neurophone-core/tests/property_test.rs b/crates/neurophone-core/tests/property_test.rs index fd6d998..81ddc82 100644 --- a/crates/neurophone-core/tests/property_test.rs +++ b/crates/neurophone-core/tests/property_test.rs @@ -11,13 +11,11 @@ use proptest::prelude::*; fn arb_system_config() -> impl Strategy { (0.1f32..100.0, 0.0f32..1.0, 100u32..5000u32).prop_map( - |(sample_rate, threshold, response_time)| { - SystemConfig { - sample_rate, - window_size_ms: 100, - local_threshold: threshold, - max_response_time_ms: response_time, - } + |(sample_rate, threshold, response_time)| SystemConfig { + sample_rate, + window_size_ms: 100, + local_threshold: threshold, + max_response_time_ms: response_time, }, ) } @@ -28,11 +26,11 @@ fn arb_sensor_event() -> impl Strategy { 0u64..10000u64, prop::collection::vec(-10.0f32..10.0f32, 1..10), ) - .prop_map(|(sensor_type, timestamp_ms, values)| SensorEvent { - sensor_type, - timestamp_ms, - values, - }) + .prop_map(|(sensor_type, timestamp_ms, values)| SensorEvent { + sensor_type, + timestamp_ms, + values, + }) } fn arb_query_string() -> impl Strategy { @@ -96,11 +94,9 @@ proptest! { let mut system = NeuroSymbolicSystem::new(SystemConfig::default()) .expect("system creation"); - let mut expected_count = 0u64; - for query in queries.iter().filter(|q| !q.is_empty()) { + for (i, query) in queries.iter().filter(|q| !q.is_empty()).enumerate() { system.query(query, true).ok(); - expected_count += 1; - prop_assert_eq!(system.query_count(), expected_count); + prop_assert_eq!(system.query_count(), (i as u64) + 1); } } @@ -199,7 +195,6 @@ proptest! { let result = system.query("test", true); if let Ok(r) = result { - prop_assert!(r.latency_ms >= 0); prop_assert!(r.latency_ms < 5000); // Should complete within 5 seconds } } diff --git a/crates/sensors/benches/sensors_bench.rs b/crates/sensors/benches/sensors_bench.rs index c87b2c1..bf8ece5 100644 --- a/crates/sensors/benches/sensors_bench.rs +++ b/crates/sensors/benches/sensors_bench.rs @@ -2,20 +2,27 @@ // SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell //! Benches for the sensors pipeline. -use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput}; -use sensors::{IirFilter, PipelineConfig, SensorKind, SensorPipeline, SensorReading, WindowedFeatures}; +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use sensors::{ + IirFilter, PipelineConfig, SensorKind, SensorPipeline, SensorReading, WindowedFeatures, +}; +use std::hint::black_box; fn bench_iir_lowpass_step(c: &mut Criterion) { let mut f = IirFilter::new(3, 5.0, 50.0, false).unwrap(); c.bench_function("iir_lowpass_step_3ch", |b| { - b.iter(|| { let _ = black_box(f.step(black_box(&[1.0, 2.0, 3.0]))); }) + b.iter(|| { + let _ = black_box(f.step(black_box(&[1.0, 2.0, 3.0]))); + }) }); } fn bench_iir_highpass_step(c: &mut Criterion) { let mut f = IirFilter::new(3, 0.5, 50.0, true).unwrap(); c.bench_function("iir_highpass_step_3ch", |b| { - b.iter(|| { let _ = black_box(f.step(black_box(&[1.0, 2.0, 3.0]))); }) + b.iter(|| { + let _ = black_box(f.step(black_box(&[1.0, 2.0, 3.0]))); + }) }); } @@ -23,7 +30,9 @@ fn bench_windowed_features(c: &mut Criterion) { let mut g = c.benchmark_group("windowed_features"); for &cap in &[10usize, 50, 200] { let mut w = WindowedFeatures::new(3, cap).unwrap(); - for _ in 0..cap { w.push(&[0.1, 0.2, 0.3]).unwrap(); } + for _ in 0..cap { + w.push(&[0.1, 0.2, 0.3]).unwrap(); + } g.bench_function(format!("features_cap{cap}"), |b| { b.iter(|| black_box(w.features().unwrap())) }); @@ -40,7 +49,7 @@ fn bench_pipeline_ingest_50hz(c: &mut Criterion) { b.iter(|| { t += 20; let r = SensorReading::new(SensorKind::Accelerometer, t, vec![0.1, 0.2, 9.81]).unwrap(); - black_box(p.ingest(&r).unwrap()); + p.ingest(black_box(&r)).unwrap(); }) }); g.finish(); @@ -49,9 +58,11 @@ fn bench_pipeline_ingest_50hz(c: &mut Criterion) { fn bench_pipeline_full_second(c: &mut Criterion) { c.bench_function("pipeline_50hz_full_second", |b| { b.iter(|| { - let mut p = SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); + let mut p = + SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); for i in 0..50u64 { - let r = SensorReading::new(SensorKind::Accelerometer, i * 20, vec![0.1, 0.2, 9.81]).unwrap(); + let r = SensorReading::new(SensorKind::Accelerometer, i * 20, vec![0.1, 0.2, 9.81]) + .unwrap(); p.ingest(&r).unwrap(); } black_box(p.features().unwrap()); @@ -59,5 +70,12 @@ fn bench_pipeline_full_second(c: &mut Criterion) { }); } -criterion_group!(benches, bench_iir_lowpass_step, bench_iir_highpass_step, bench_windowed_features, bench_pipeline_ingest_50hz, bench_pipeline_full_second); +criterion_group!( + benches, + bench_iir_lowpass_step, + bench_iir_highpass_step, + bench_windowed_features, + bench_pipeline_ingest_50hz, + bench_pipeline_full_second +); criterion_main!(benches); diff --git a/crates/sensors/src/lib.rs b/crates/sensors/src/lib.rs index 5775e87..52a4c95 100644 --- a/crates/sensors/src/lib.rs +++ b/crates/sensors/src/lib.rs @@ -66,7 +66,11 @@ impl SensorReading { got: values.len(), }); } - Ok(Self { kind, timestamp_ms, values }) + Ok(Self { + kind, + timestamp_ms, + values, + }) } } @@ -82,13 +86,22 @@ pub struct IirFilter { } impl IirFilter { - pub fn new(channels: usize, cutoff_hz: f32, sample_hz: f32, high_pass: bool) -> Result { + pub fn new( + channels: usize, + cutoff_hz: f32, + sample_hz: f32, + high_pass: bool, + ) -> Result { if channels == 0 || sample_hz <= 0.0 || cutoff_hz <= 0.0 { return Err(SensorError::InvalidConfig("non-positive params".into())); } let dt = 1.0 / sample_hz; let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff_hz); - let alpha = if high_pass { rc / (rc + dt) } else { dt / (rc + dt) }; + let alpha = if high_pass { + rc / (rc + dt) + } else { + dt / (rc + dt) + }; Ok(Self { alpha, prev_out: vec![0.0; channels], @@ -99,7 +112,10 @@ impl IirFilter { pub fn step(&mut self, input: &[f32]) -> Result, SensorError> { if input.len() != self.prev_out.len() { - return Err(SensorError::DimensionMismatch { expected: self.prev_out.len(), got: input.len() }); + return Err(SensorError::DimensionMismatch { + expected: self.prev_out.len(), + got: input.len(), + }); } let mut out = vec![0.0; self.prev_out.len()]; for (i, &x) in input.iter().enumerate() { @@ -134,17 +150,24 @@ pub struct WindowedFeatures { impl WindowedFeatures { pub fn new(channels: usize, capacity: usize) -> Result { if channels == 0 || capacity == 0 { - return Err(SensorError::InvalidConfig("zero channels or capacity".into())); + return Err(SensorError::InvalidConfig( + "zero channels or capacity".into(), + )); } Ok(Self { capacity, - buffers: (0..channels).map(|_| VecDeque::with_capacity(capacity)).collect(), + buffers: (0..channels) + .map(|_| VecDeque::with_capacity(capacity)) + .collect(), }) } pub fn push(&mut self, sample: &[f32]) -> Result<(), SensorError> { if sample.len() != self.buffers.len() { - return Err(SensorError::DimensionMismatch { expected: self.buffers.len(), got: sample.len() }); + return Err(SensorError::DimensionMismatch { + expected: self.buffers.len(), + got: sample.len(), + }); } for (buf, &v) in self.buffers.iter_mut().zip(sample) { if buf.len() == self.capacity { @@ -155,11 +178,17 @@ impl WindowedFeatures { Ok(()) } - pub fn len(&self) -> usize { self.buffers.first().map_or(0, |b| b.len()) } - pub fn is_empty(&self) -> bool { self.len() == 0 } + pub fn len(&self) -> usize { + self.buffers.first().map_or(0, |b| b.len()) + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } pub fn features(&self) -> Result, SensorError> { - if self.is_empty() { return Err(SensorError::EmptyBuffer); } + if self.is_empty() { + return Err(SensorError::EmptyBuffer); + } let mut out = Vec::with_capacity(self.buffers.len() * 2 + 1); let mut last_sq_sum = 0.0f32; for buf in &self.buffers { @@ -168,7 +197,9 @@ impl WindowedFeatures { let var = buf.iter().map(|v| (v - mean).powi(2)).sum::() / n; out.push(mean); out.push(var); - if let Some(&last) = buf.back() { last_sq_sum += last * last; } + if let Some(&last) = buf.back() { + last_sq_sum += last * last; + } } out.push(last_sq_sum.sqrt()); Ok(Array1::from_vec(out)) @@ -194,7 +225,12 @@ pub struct PipelineConfig { impl Default for PipelineConfig { fn default() -> Self { - Self { sample_hz: 50.0, low_pass_hz: 10.0, high_pass_hz: 0.5, window_size: 25 } + Self { + sample_hz: 50.0, + low_pass_hz: 10.0, + high_pass_hz: 0.5, + window_size: 25, + } } } @@ -210,12 +246,15 @@ impl SensorPipeline { }) } - pub fn kind(&self) -> SensorKind { self.kind } + pub fn kind(&self) -> SensorKind { + self.kind + } pub fn ingest(&mut self, reading: &SensorReading) -> Result<(), SensorError> { if reading.kind != self.kind { return Err(SensorError::InvalidConfig(format!( - "kind mismatch {:?} vs {:?}", reading.kind, self.kind + "kind mismatch {:?} vs {:?}", + reading.kind, self.kind ))); } let lp = self.low_pass.step(&reading.values)?; @@ -225,8 +264,12 @@ impl SensorPipeline { Ok(()) } - pub fn features(&self) -> Result, SensorError> { self.window.features() } - pub fn last_timestamp_ms(&self) -> Option { self.last_ts } + pub fn features(&self) -> Result, SensorError> { + self.window.features() + } + pub fn last_timestamp_ms(&self) -> Option { + self.last_ts + } pub fn reset(&mut self) { self.low_pass.reset(); @@ -236,13 +279,16 @@ impl SensorPipeline { } } -pub fn hello() -> &'static str { "sensors" } +pub fn hello() -> &'static str { + "sensors" +} #[cfg(test)] mod tests { use super::*; - #[test] fn arity_for_each_kind() { + #[test] + fn arity_for_each_kind() { assert_eq!(SensorKind::Accelerometer.arity(), 3); assert_eq!(SensorKind::Gyroscope.arity(), 3); assert_eq!(SensorKind::Magnetometer.arity(), 3); @@ -250,46 +296,64 @@ mod tests { assert_eq!(SensorKind::Proximity.arity(), 1); } - #[test] fn reading_validates_arity() { + #[test] + fn reading_validates_arity() { assert!(SensorReading::new(SensorKind::Light, 0, vec![1.0]).is_ok()); assert!(SensorReading::new(SensorKind::Light, 0, vec![1.0, 2.0]).is_err()); } - #[test] fn lowpass_smooths_step() { + #[test] + fn lowpass_smooths_step() { let mut f = IirFilter::new(1, 1.0, 50.0, false).unwrap(); - for _ in 0..200 { let _ = f.step(&[1.0]); } + for _ in 0..200 { + let _ = f.step(&[1.0]); + } let out = f.step(&[1.0]).unwrap()[0]; - assert!((out - 1.0).abs() < 1e-2, "lowpass should converge to 1.0, got {}", out); + assert!( + (out - 1.0).abs() < 1e-2, + "lowpass should converge to 1.0, got {}", + out + ); } - #[test] fn highpass_zeroes_constant() { + #[test] + fn highpass_zeroes_constant() { let mut f = IirFilter::new(1, 1.0, 50.0, true).unwrap(); - for _ in 0..200 { let _ = f.step(&[1.0]); } + for _ in 0..200 { + let _ = f.step(&[1.0]); + } let out = f.step(&[1.0]).unwrap()[0]; assert!(out.abs() < 1e-3, "highpass should zero DC, got {}", out); } - #[test] fn window_features_dimension() { + #[test] + fn window_features_dimension() { let mut w = WindowedFeatures::new(3, 5).unwrap(); - for i in 0..5 { w.push(&[i as f32, 0.0, 1.0]).unwrap(); } + for i in 0..5 { + w.push(&[i as f32, 0.0, 1.0]).unwrap(); + } let f = w.features().unwrap(); assert_eq!(f.len(), 3 * 2 + 1); } - #[test] fn pipeline_kind_mismatch_rejected() { + #[test] + fn pipeline_kind_mismatch_rejected() { let mut p = SensorPipeline::new(SensorKind::Light, PipelineConfig::default()).unwrap(); let r = SensorReading::new(SensorKind::Accelerometer, 0, vec![0.0, 0.0, 0.0]).unwrap(); assert!(p.ingest(&r).is_err()); } - #[test] fn pipeline_ingest_then_features() { - let mut p = SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); + #[test] + fn pipeline_ingest_then_features() { + let mut p = + SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); for i in 0..30 { let r = SensorReading::new( SensorKind::Accelerometer, i as u64 * 20, vec![i as f32 * 0.01, 0.0, 9.81], - ).unwrap(); + ) + .unwrap(); p.ingest(&r).unwrap(); } assert_eq!(p.last_timestamp_ms(), Some(580)); @@ -297,7 +361,8 @@ mod tests { assert_eq!(f.len(), 7); } - #[test] fn pipeline_reset_clears_state() { + #[test] + fn pipeline_reset_clears_state() { let mut p = SensorPipeline::new(SensorKind::Light, PipelineConfig::default()).unwrap(); let r = SensorReading::new(SensorKind::Light, 0, vec![100.0]).unwrap(); p.ingest(&r).unwrap(); diff --git a/crates/sensors/tests/aspect_lifecycle.rs b/crates/sensors/tests/aspect_lifecycle.rs index b1a9219..17a6fba 100644 --- a/crates/sensors/tests/aspect_lifecycle.rs +++ b/crates/sensors/tests/aspect_lifecycle.rs @@ -7,11 +7,13 @@ use sensors::{PipelineConfig, SensorError, SensorKind, SensorPipeline, SensorRea #[test] fn lifecycle_create_ingest_reset_ingest() { let mut p = SensorPipeline::new(SensorKind::Light, PipelineConfig::default()).unwrap(); - p.ingest(&SensorReading::new(SensorKind::Light, 10, vec![1.0]).unwrap()).unwrap(); + p.ingest(&SensorReading::new(SensorKind::Light, 10, vec![1.0]).unwrap()) + .unwrap(); assert!(p.features().is_ok()); p.reset(); assert!(p.features().is_err()); - p.ingest(&SensorReading::new(SensorKind::Light, 20, vec![2.0]).unwrap()).unwrap(); + p.ingest(&SensorReading::new(SensorKind::Light, 20, vec![2.0]).unwrap()) + .unwrap(); assert!(p.features().is_ok()); } @@ -29,13 +31,19 @@ fn aspect_error_path_dimension_mismatch() { #[test] fn aspect_invalid_config_zero_window() { - let cfg = PipelineConfig { window_size: 0, ..Default::default() }; + let cfg = PipelineConfig { + window_size: 0, + ..Default::default() + }; assert!(SensorPipeline::new(SensorKind::Light, cfg).is_err()); } #[test] fn aspect_invalid_config_zero_sample_rate() { - let cfg = PipelineConfig { sample_hz: 0.0, ..Default::default() }; + let cfg = PipelineConfig { + sample_hz: 0.0, + ..Default::default() + }; assert!(SensorPipeline::new(SensorKind::Light, cfg).is_err()); } @@ -43,7 +51,8 @@ fn aspect_invalid_config_zero_sample_rate() { fn aspect_resource_thousand_ingests_no_growth() { let mut p = SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); for i in 0..1000 { - let r = SensorReading::new(SensorKind::Accelerometer, i as u64, vec![0.1, 0.2, 9.81]).unwrap(); + let r = + SensorReading::new(SensorKind::Accelerometer, i as u64, vec![0.1, 0.2, 9.81]).unwrap(); p.ingest(&r).unwrap(); } let f = p.features().unwrap(); diff --git a/crates/sensors/tests/integration_pipeline.rs b/crates/sensors/tests/integration_pipeline.rs index 0e450f2..9a02913 100644 --- a/crates/sensors/tests/integration_pipeline.rs +++ b/crates/sensors/tests/integration_pipeline.rs @@ -2,13 +2,20 @@ // SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell //! Integration tests for the sensors crate (point-to-point: reading -> features). -use sensors::{IirFilter, PipelineConfig, SensorKind, SensorPipeline, SensorReading, WindowedFeatures}; +use sensors::{ + IirFilter, PipelineConfig, SensorKind, SensorPipeline, SensorReading, WindowedFeatures, +}; #[test] fn pipeline_50hz_one_second_yields_features() { let mut p = SensorPipeline::new(SensorKind::Accelerometer, PipelineConfig::default()).unwrap(); for i in 0..50 { - let r = SensorReading::new(SensorKind::Accelerometer, i as u64 * 20, vec![0.1, 0.2, 9.81]).unwrap(); + let r = SensorReading::new( + SensorKind::Accelerometer, + i as u64 * 20, + vec![0.1, 0.2, 9.81], + ) + .unwrap(); p.ingest(&r).unwrap(); } let f = p.features().unwrap(); @@ -20,7 +27,8 @@ fn pipeline_50hz_one_second_yields_features() { fn light_sensor_singlechannel_pipeline() { let mut p = SensorPipeline::new(SensorKind::Light, PipelineConfig::default()).unwrap(); for i in 0..30 { - let r = SensorReading::new(SensorKind::Light, i as u64 * 20, vec![100.0 + i as f32]).unwrap(); + let r = + SensorReading::new(SensorKind::Light, i as u64 * 20, vec![100.0 + i as f32]).unwrap(); p.ingest(&r).unwrap(); } let f = p.features().unwrap(); @@ -39,7 +47,9 @@ fn filter_chain_preserves_dimensionality() { #[test] fn windowed_features_after_overflow_stays_bounded() { let mut w = WindowedFeatures::new(2, 5).unwrap(); - for i in 0..100 { w.push(&[i as f32, -(i as f32)]).unwrap(); } + for i in 0..100 { + w.push(&[i as f32, -(i as f32)]).unwrap(); + } assert_eq!(w.len(), 5); let f = w.features().unwrap(); assert!(f.iter().all(|v| v.is_finite()));