diff --git a/src/truth/ecan.rs b/src/truth/ecan.rs new file mode 100644 index 0000000..144b437 --- /dev/null +++ b/src/truth/ecan.rs @@ -0,0 +1,274 @@ +//! Economic Attention Network (ECAN) for memory importance management. +//! +//! ECAN assigns Short-Term Importance (STI) and Long-Term Importance (LTI) +//! to memories, enabling natural prioritization of verified, frequently-confirmed +//! knowledge while deprioritizing stale or unreliable memories. +//! +//! Based on the OpenCog ECAN model adapted for the Bushidai Truth Engine. + +/// Clamp a value to the range [0.0, 1.0]. +#[inline] +fn clamp01(v: f32) -> f32 { + v.clamp(0.0, 1.0) +} + +/// ECAN parameters controlling attention dynamics. +#[derive(Debug, Clone)] +pub struct EcanParams { + /// STI decay rate per cycle (0.0–1.0). Higher = slower decay. + pub decay_rate: f32, + /// LTI update rate — how fast LTI accumulates from STI. + pub beta: f32, + /// Fraction of STI that spreads to LTI each cycle. + pub spread_factor: f32, + /// STI threshold below which LTI update is penalized. + pub threshold: f32, +} + +impl EcanParams { + /// Create EcanParams with the given decay rate and spread factor. + /// Other parameters use sensible defaults. + pub fn new(decay_rate: f32, spread_factor: f32) -> Self { + Self { + decay_rate: clamp01(decay_rate), + beta: 0.01, + spread_factor: clamp01(spread_factor), + threshold: 0.1, + } + } + + /// Apply STI decay. Each cycle, STI moves toward zero. + /// + /// ```text + /// new_sti = sti * decay_rate + /// ``` + pub fn decay_sti(&self, sti: f32) -> f32 { + clamp01(sti * self.decay_rate) + } + + /// Update LTI based on current STI. + /// + /// LTI accumulates slowly when STI is high (memory is actively relevant). + /// When STI falls below threshold, LTI update is penalized. + /// + /// ```text + /// new_lti = lti * (1 - beta) + spread_factor * sti + /// if sti < threshold: new_lti *= 0.5 + /// ``` + pub fn update_lti(&self, lti: f32, sti: f32) -> f32 { + let mut new_lti = lti * (1.0 - self.beta) + self.spread_factor * sti; + if sti < self.threshold { + new_lti *= 0.5; + } + clamp01(new_lti) + } + + /// Boost STI by a spread amount (e.g., when a memory is confirmed). + pub fn spread(&self, sti: f32, amount: f32) -> f32 { + clamp01(sti + amount) + } + + /// Run a full ECAN cycle: decay STI, then update LTI. + /// + /// The truth_value is used to weight the STI contribution: + /// higher truth values contribute more to LTI accumulation. + pub fn cycle(&self, sti: f32, lti: f32, truth_value: f32) -> (f32, f32) { + let new_sti = self.decay_sti(sti); + // Weight STI contribution by truth value for LTI update + let weighted_sti = new_sti * clamp01(truth_value); + let new_lti = self.update_lti(lti, weighted_sti); + (new_sti, new_lti) + } + + /// Compute initial STI and LTI for a newly scored memory. + /// + /// STI starts proportional to truth_value (high truth = high initial attention). + /// LTI starts proportional to confidence (high confidence = more reliable). + pub fn initialize(&self, truth_value: f32, confidence: f32) -> (f32, f32) { + let sti = clamp01(truth_value); + let lti = clamp01(confidence * 0.5); // Start LTI conservatively + (sti, lti) + } +} + +impl Default for EcanParams { + fn default() -> Self { + Self { + decay_rate: 0.95, + beta: 0.01, + spread_factor: 0.05, + threshold: 0.1, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn default_params() -> EcanParams { + EcanParams::default() + } + + #[test] + fn ecan_decay_reduces_sti() { + let params = default_params(); + let sti = 0.8; + let decayed = params.decay_sti(sti); + assert!(decayed < sti, "STI should decrease after decay"); + assert!(decayed > 0.0, "STI should remain positive"); + } + + #[test] + fn ecan_decay_converges_to_zero() { + let params = default_params(); + let mut sti = 1.0; + for _ in 0..200 { + sti = params.decay_sti(sti); + } + assert!( + sti < 0.001, + "STI should converge near zero after many cycles, got {}", + sti + ); + } + + #[test] + fn ecan_lti_grows_with_high_sti() { + let params = default_params(); + let lti = 0.1; + let high_sti = 0.9; + let new_lti = params.update_lti(lti, high_sti); + assert!( + new_lti > lti, + "LTI should grow when STI is high: {} > {}", + new_lti, + lti + ); + } + + #[test] + fn ecan_lti_penalized_below_threshold() { + let params = default_params(); + let lti = 0.5; + let low_sti = 0.01; // Below threshold of 0.1 + let new_lti = params.update_lti(lti, low_sti); + // With penalty, LTI should be roughly halved + assert!( + new_lti < lti * 0.6, + "LTI should be penalized below threshold: {} < {}", + new_lti, + lti * 0.6 + ); + } + + #[test] + fn ecan_lti_not_penalized_above_threshold() { + let params = default_params(); + let lti = 0.5; + let above_threshold_sti = 0.5; + let new_lti = params.update_lti(lti, above_threshold_sti); + // LTI should stay close to original or grow slightly + assert!( + new_lti >= lti * 0.9, + "LTI should not be heavily penalized above threshold: {} >= {}", + new_lti, + lti * 0.9 + ); + } + + #[test] + fn ecan_spread_increases_sti() { + let params = default_params(); + let sti = 0.5; + let boosted = params.spread(sti, 0.2); + assert_eq!(boosted, 0.7, "Spread should add to STI"); + } + + #[test] + fn ecan_spread_clamps_to_one() { + let params = default_params(); + let sti = 0.9; + let boosted = params.spread(sti, 0.5); + assert_eq!(boosted, 1.0, "Spread should clamp at 1.0"); + } + + #[test] + fn ecan_full_cycle() { + let params = default_params(); + let (new_sti, new_lti) = params.cycle(0.8, 0.3, 0.9); + // STI should decay + assert!(new_sti < 0.8, "STI should decay in cycle"); + assert!(new_sti > 0.0, "STI should remain positive"); + // LTI should adjust based on weighted STI + assert!(new_lti > 0.0, "LTI should remain positive"); + // Both bounded + assert!(new_sti <= 1.0 && new_lti <= 1.0); + } + + #[test] + fn ecan_cycle_low_truth_reduces_lti_contribution() { + let params = default_params(); + let (_, lti_high_truth) = params.cycle(0.8, 0.3, 0.9); + let (_, lti_low_truth) = params.cycle(0.8, 0.3, 0.1); + // Higher truth value should contribute more to LTI + assert!( + lti_high_truth >= lti_low_truth, + "High truth should contribute more to LTI: {} >= {}", + lti_high_truth, + lti_low_truth + ); + } + + #[test] + fn ecan_initialize_from_truth_value() { + let params = default_params(); + let (sti, lti) = params.initialize(0.9, 0.8); + assert_eq!(sti, 0.9, "Initial STI should equal truth_value"); + assert!( + (lti - 0.4).abs() < 0.001, + "Initial LTI should be confidence * 0.5 = 0.4, got {}", + lti + ); + } + + #[test] + fn ecan_initialize_zero() { + let params = default_params(); + let (sti, lti) = params.initialize(0.0, 0.0); + assert_eq!(sti, 0.0); + assert_eq!(lti, 0.0); + } + + #[test] + fn ecan_values_bounded() { + let params = default_params(); + // Test with extreme values + let decayed = params.decay_sti(1.5); + assert!(decayed <= 1.0, "Decay should clamp to 1.0"); + + let lti = params.update_lti(2.0, 2.0); + assert!(lti <= 1.0, "LTI update should clamp to 1.0"); + + let spread = params.spread(0.8, 0.5); + assert!(spread <= 1.0, "Spread should clamp to 1.0"); + + let (sti, lti) = params.cycle(1.5, 1.5, 1.5); + assert!(sti <= 1.0 && lti <= 1.0, "Cycle should clamp outputs"); + + let (sti, lti) = params.initialize(2.0, 2.0); + assert!( + sti <= 1.0 && lti <= 1.0, + "Initialize should clamp outputs" + ); + } + + #[test] + fn ecan_from_new() { + let params = EcanParams::new(0.90, 0.10); + assert_eq!(params.decay_rate, 0.90); + assert_eq!(params.spread_factor, 0.10); + assert_eq!(params.beta, 0.01); + assert_eq!(params.threshold, 0.1); + } +} diff --git a/src/truth/mod.rs b/src/truth/mod.rs index 76ce9fe..1204c63 100644 --- a/src/truth/mod.rs +++ b/src/truth/mod.rs @@ -1,3 +1,15 @@ //! Truth scoring engine — PLN deduction, ECAN attention, and memory scoring. +//! +//! This module implements neuro-symbolic reasoning for evaluating the +//! truthfulness of stored memories. Based on the Bushidai Truth Simulator +//! by Thijs Smits (TS87). +//! +//! ## Components +//! +//! - **PLN** (Probabilistic Logic Networks): Deduction rules for computing +//! truth values from evidence chains. +//! - **ECAN** (Economic Attention Network): Manages short-term and long-term +//! importance of memories, enabling natural prioritization of verified knowledge. +pub mod ecan; pub mod pln;