1use std::collections::VecDeque;
4
5use ahash::{HashMap, HashSet};
6use mentedb_core::edge::EdgeType;
7use mentedb_core::types::MemoryId;
8
9use crate::csr::CsrGraph;
10
11#[derive(Debug, Clone)]
13pub struct PropagationConfig {
14 pub max_depth: usize,
16 pub edge_types: Vec<EdgeType>,
27 pub min_delta: f32,
30 pub max_affected: usize,
33 pub caused_dampening: f32,
35 pub supports_factor: f32,
37 pub contradicts_factor: f32,
39 pub supersedes_floor: f32,
41}
42
43impl Default for PropagationConfig {
44 fn default() -> Self {
45 Self {
46 max_depth: 5,
47 edge_types: vec![EdgeType::Caused, EdgeType::Supports],
48 min_delta: 0.01,
49 max_affected: 64,
50 caused_dampening: 0.9,
51 supports_factor: 0.5,
52 contradicts_factor: 0.7,
53 supersedes_floor: 0.1,
54 }
55 }
56}
57
58impl PropagationConfig {
59 pub fn all_edges() -> Self {
63 Self {
64 edge_types: vec![
65 EdgeType::Caused,
66 EdgeType::Supports,
67 EdgeType::Contradicts,
68 EdgeType::Supersedes,
69 ],
70 ..Self::default()
71 }
72 }
73}
74
75pub fn propagate_update(
84 graph: &CsrGraph,
85 changed_id: MemoryId,
86 new_confidence: f32,
87) -> Vec<(MemoryId, f32)> {
88 propagate_update_with_config(
89 graph,
90 changed_id,
91 new_confidence,
92 &PropagationConfig::all_edges(),
93 )
94}
95
96pub fn propagate_delta(
116 graph: &CsrGraph,
117 root: MemoryId,
118 delta: f32,
119 config: &PropagationConfig,
120) -> Vec<(MemoryId, f32)> {
121 let mut affected = Vec::new();
122 if graph.get_idx(root).is_none()
123 || !delta.is_finite()
124 || delta.abs() < config.min_delta
125 || config.max_affected == 0
126 {
127 return affected;
128 }
129
130 let mut visited = HashSet::default();
131 visited.insert(root);
132 let mut queue: VecDeque<(MemoryId, f32, usize)> = VecDeque::new();
133 queue.push_back((root, delta, 0));
134
135 while let Some((node, node_delta, depth)) = queue.pop_front() {
136 if depth >= config.max_depth {
137 continue;
138 }
139 for (neighbor, edge) in graph.outgoing(node) {
140 if visited.contains(&neighbor) || !config.edge_types.contains(&edge.edge_type) {
141 continue;
142 }
143 let factor = match edge.edge_type {
144 EdgeType::Caused => config.caused_dampening,
145 EdgeType::Supports => config.supports_factor,
146 EdgeType::Contradicts => -config.contradicts_factor,
147 _ => continue,
148 };
149 let child_delta = node_delta * edge.weight.clamp(0.0, 1.0) * factor;
150 if !child_delta.is_finite() || child_delta.abs() < config.min_delta {
151 continue;
152 }
153 visited.insert(neighbor);
154 affected.push((neighbor, child_delta));
155 if affected.len() >= config.max_affected {
156 return affected;
157 }
158 queue.push_back((neighbor, child_delta, depth + 1));
159 }
160 }
161 affected
162}
163
164pub fn propagate_update_with_config(
166 graph: &CsrGraph,
167 changed_id: MemoryId,
168 new_confidence: f32,
169 config: &PropagationConfig,
170) -> Vec<(MemoryId, f32)> {
171 let Some(_) = graph.get_idx(changed_id) else {
172 return Vec::new();
173 };
174
175 let mut confidences: HashMap<MemoryId, f32> = HashMap::default();
177 confidences.insert(changed_id, new_confidence);
178
179 let mut queue: VecDeque<(MemoryId, f32, usize)> = VecDeque::new();
181 queue.push_back((changed_id, new_confidence, 0));
182
183 let mut visited = HashSet::default();
184 visited.insert(changed_id);
185
186 while let Some((node, node_confidence, depth)) = queue.pop_front() {
187 if depth >= config.max_depth {
188 continue;
189 }
190
191 for (neighbor, edge) in graph.outgoing(node) {
192 if !config.edge_types.contains(&edge.edge_type) {
193 continue;
194 }
195 let new_conf = match edge.edge_type {
196 EdgeType::Caused => {
197 node_confidence * edge.weight * config.caused_dampening
199 }
200 EdgeType::Supports => {
201 let current = confidences.get(&neighbor).copied().unwrap_or(1.0);
203 let delta = node_confidence - 1.0; (current + delta * edge.weight * config.supports_factor).clamp(0.0, 1.0)
205 }
206 EdgeType::Contradicts => {
207 let current = confidences.get(&neighbor).copied().unwrap_or(1.0);
209 (current - node_confidence * edge.weight * config.contradicts_factor)
210 .clamp(0.0, 1.0)
211 }
212 EdgeType::Supersedes => {
213 let current = confidences.get(&neighbor).copied().unwrap_or(1.0);
215 current.min(node_confidence * config.supersedes_floor)
216 }
217 _ => continue,
219 };
220
221 confidences.insert(neighbor, new_conf);
222 if visited.insert(neighbor) {
223 queue.push_back((neighbor, new_conf, depth + 1));
224 }
225 }
226 }
227
228 confidences.into_iter().collect()
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use mentedb_core::edge::MemoryEdge;
235
236 fn make_edge(src: MemoryId, tgt: MemoryId, etype: EdgeType, weight: f32) -> MemoryEdge {
237 MemoryEdge {
238 source: src,
239 target: tgt,
240 edge_type: etype,
241 weight,
242 created_at: 1000,
243 valid_from: None,
244 valid_until: None,
245 label: None,
246 }
247 }
248
249 #[test]
250 fn test_caused_propagation() {
251 let mut g = CsrGraph::new();
252 let a = MemoryId::new();
253 let b = MemoryId::new();
254 g.add_edge(&make_edge(a, b, EdgeType::Caused, 1.0));
255
256 let result = propagate_update(&g, a, 0.5);
257 let map: HashMap<MemoryId, f32> = result.into_iter().collect();
258
259 assert!((map[&a] - 0.5).abs() < 0.001);
260 assert!((map[&b] - 0.45).abs() < 0.001);
262 }
263
264 #[test]
265 fn test_contradicts_propagation() {
266 let mut g = CsrGraph::new();
267 let a = MemoryId::new();
268 let b = MemoryId::new();
269 g.add_edge(&make_edge(a, b, EdgeType::Contradicts, 1.0));
270
271 let result = propagate_update(&g, a, 0.8);
272 let map: HashMap<MemoryId, f32> = result.into_iter().collect();
273
274 assert!((map[&b] - 0.44).abs() < 0.001);
276 }
277
278 #[test]
279 fn test_supersedes_propagation() {
280 let mut g = CsrGraph::new();
281 let a = MemoryId::new();
282 let b = MemoryId::new();
283 g.add_edge(&make_edge(a, b, EdgeType::Supersedes, 1.0));
284
285 let result = propagate_update(&g, a, 0.9);
286 let map: HashMap<MemoryId, f32> = result.into_iter().collect();
287
288 assert!((map[&b] - 0.09).abs() < 0.001);
290 }
291
292 #[test]
293 fn test_max_depth_limit() {
294 let mut g = CsrGraph::new();
296 let ids: Vec<MemoryId> = (0..10).map(|_| MemoryId::new()).collect();
297 for i in 0..9 {
298 g.add_edge(&make_edge(ids[i], ids[i + 1], EdgeType::Caused, 1.0));
299 }
300
301 let result = propagate_update(&g, ids[0], 1.0);
302 let map: HashMap<MemoryId, f32> = result.into_iter().collect();
303
304 assert!(map.len() <= 6);
306 }
307
308 fn delta_map(
309 g: &CsrGraph,
310 root: MemoryId,
311 delta: f32,
312 cfg: &PropagationConfig,
313 ) -> HashMap<MemoryId, f32> {
314 propagate_delta(g, root, delta, cfg).into_iter().collect()
315 }
316
317 #[test]
318 fn delta_damps_along_a_caused_chain_and_excludes_the_root() {
319 let mut g = CsrGraph::new();
320 let (a, b, c) = (MemoryId::new(), MemoryId::new(), MemoryId::new());
321 g.add_edge(&make_edge(a, b, EdgeType::Caused, 1.0));
322 g.add_edge(&make_edge(b, c, EdgeType::Caused, 1.0));
323 let map = delta_map(&g, a, -0.4, &PropagationConfig::default());
324 assert!(!map.contains_key(&a), "the root is the caller's to set");
325 assert!((map[&b] + 0.36).abs() < 1e-5);
326 assert!((map[&c] + 0.324).abs() < 1e-5);
327 }
328
329 #[test]
330 fn delta_default_edge_set_skips_conflict_edges() {
331 let mut g = CsrGraph::new();
332 let (a, x, y, s) = (
333 MemoryId::new(),
334 MemoryId::new(),
335 MemoryId::new(),
336 MemoryId::new(),
337 );
338 g.add_edge(&make_edge(a, x, EdgeType::Contradicts, 1.0));
339 g.add_edge(&make_edge(a, y, EdgeType::Supersedes, 1.0));
340 g.add_edge(&make_edge(a, s, EdgeType::Supports, 1.0));
341 let map = delta_map(&g, a, -0.4, &PropagationConfig::default());
342 assert_eq!(map.len(), 1);
343 assert!((map[&s] + 0.2).abs() < 1e-5);
344
345 let map = delta_map(&g, a, -0.4, &PropagationConfig::all_edges());
347 assert!((map[&x] - 0.28).abs() < 1e-5);
348 assert!(
349 !map.contains_key(&y),
350 "Supersedes is never walked by deltas"
351 );
352 }
353
354 #[test]
355 fn delta_respects_min_delta_and_max_affected() {
356 let mut g = CsrGraph::new();
357 let root = MemoryId::new();
358 for _ in 0..10 {
359 g.add_edge(&make_edge(root, MemoryId::new(), EdgeType::Caused, 1.0));
360 }
361 let cfg = PropagationConfig {
362 max_affected: 3,
363 ..PropagationConfig::default()
364 };
365 assert_eq!(propagate_delta(&g, root, -0.5, &cfg).len(), 3);
366 let cfg = PropagationConfig {
367 min_delta: 0.5,
368 ..PropagationConfig::default()
369 };
370 assert!(propagate_delta(&g, root, -0.5, &cfg).is_empty());
372 }
373
374 #[test]
375 fn test_unrelated_edges_dont_propagate() {
376 let mut g = CsrGraph::new();
377 let a = MemoryId::new();
378 let b = MemoryId::new();
379 g.add_edge(&make_edge(a, b, EdgeType::Related, 1.0));
380
381 let result = propagate_update(&g, a, 0.5);
382 assert_eq!(result.len(), 1);
384 }
385}