csm_memory/
singularity_decay.rs1use csm_core_lib::error::{MemoryError, Result};
4use csm_core_lib::hyperdim::Hypervector;
5
6use crate::singularity::{DecayCurve, Singularity, unix_now_secs};
7
8impl<H: Hypervector + 'static> Singularity<H> {
9 pub fn reinforce_association(&mut self, ns: &str, from: &str, to: &str) -> Result<()> {
12 let ns_state = self.ensure_namespace(ns)?;
13 let neighbors =
14 ns_state
15 .associations
16 .get_mut(from)
17 .ok_or_else(|| MemoryError::NotFound {
18 entity: "Association".to_string(),
19 id: format!("{from} -> {to}"),
20 })?;
21 let entry = neighbors.get_mut(to).ok_or_else(|| MemoryError::NotFound {
22 entity: "Association".to_string(),
23 id: format!("{from} -> {to}"),
24 })?;
25 entry.1 = unix_now_secs();
26 Ok(())
27 }
28
29 pub fn prune_decayed_associations(
35 &mut self,
36 ns: &str,
37 curve: DecayCurve,
38 threshold: f32,
39 ) -> usize {
40 let now = unix_now_secs();
41 let Some(ns_state) = self.namespaces.get_mut(ns) else {
42 return 0;
43 };
44 let mut removed = 0usize;
45 for neighbors in ns_state.associations.values_mut() {
46 let before = neighbors.len();
47 neighbors.retain(|_, (strength, created_at)| {
48 let elapsed = now.saturating_sub(*created_at);
49 curve.apply(*strength, elapsed) >= threshold
50 });
51 removed += before - neighbors.len();
52 }
53 ns_state.associations.retain(|_, v| !v.is_empty());
55 removed
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
62 use super::*;
63 use crate::ConceptBuilder;
64 use crate::singularity::SingularityConfig;
65 use csm_core_lib::HVec10240;
66
67 fn make_singularity() -> Singularity<HVec10240> {
68 Singularity::new(SingularityConfig::default())
69 }
70
71 fn inject(sing: &mut Singularity<HVec10240>, ns: &str, id: &str) {
72 let concept = ConceptBuilder::new(id)
73 .with_vector(HVec10240::random())
74 .build()
75 .unwrap();
76 sing.inject(ns, concept).unwrap();
77 }
78
79 #[test]
80 fn reinforce_resets_created_at() {
81 let mut sing = make_singularity();
82 let ns = "_default";
83 inject(&mut sing, ns, "a");
84 inject(&mut sing, ns, "b");
85 sing.associate(ns, "a", "b", 0.8).unwrap();
86
87 assert!(sing.reinforce_association(ns, "a", "b").is_ok());
89 }
90
91 #[test]
92 fn reinforce_nonexistent_returns_error() {
93 let mut sing = make_singularity();
94 let ns = "_default";
95 inject(&mut sing, ns, "a");
96
97 assert!(sing.reinforce_association(ns, "a", "b").is_err());
98 assert!(sing.reinforce_association(ns, "x", "y").is_err());
99 }
100
101 #[test]
102 fn prune_removes_decayed_associations() {
103 let mut sing = make_singularity();
104 let ns = "_default";
105 inject(&mut sing, ns, "a");
106 inject(&mut sing, ns, "b");
107 inject(&mut sing, ns, "c");
108 sing.associate(ns, "a", "b", 0.9).unwrap();
109 sing.associate(ns, "a", "c", 0.9).unwrap();
110
111 let removed = sing.prune_decayed_associations(ns, DecayCurve::None, 0.5);
113 assert_eq!(removed, 0);
114
115 let curve = DecayCurve::Step {
117 threshold_seconds: 0,
118 drop: 1.0,
119 };
120 let removed = sing.prune_decayed_associations(ns, curve, 0.5);
121 assert_eq!(removed, 2);
122 assert!(sing.get_associations(ns, "a").is_empty());
123 }
124}