use serde::{Deserialize, Serialize};
use crate::id::MemoryId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CausalEdge {
pub strength: f32,
pub confidence: f32,
pub evidence_count: u32,
#[serde(default)]
pub confounders: Vec<String>,
#[serde(default)]
pub provenance: Vec<MemoryId>,
#[serde(default)]
pub mechanism: String,
}
impl CausalEdge {
#[must_use]
pub fn from_weight(weight: f32) -> Self {
Self {
strength: weight.clamp(0.0, 1.0),
confidence: 0.5,
evidence_count: 1,
confounders: Vec::new(),
provenance: Vec::new(),
mechanism: String::new(),
}
}
#[must_use]
pub fn relevance_score(&self) -> f32 {
self.strength * self.confidence * (1.0 + self.evidence_count as f32).ln()
}
}
impl Default for CausalEdge {
fn default() -> Self {
Self {
strength: 0.5,
confidence: 0.5,
evidence_count: 1,
confounders: Vec::new(),
provenance: Vec::new(),
mechanism: String::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_weight_clamps_and_defaults() {
let ce = CausalEdge::from_weight(0.8);
assert!((ce.strength - 0.8).abs() < f32::EPSILON);
assert!((ce.confidence - 0.5).abs() < f32::EPSILON);
assert_eq!(ce.evidence_count, 1);
assert!(ce.confounders.is_empty());
assert!(ce.provenance.is_empty());
assert!(ce.mechanism.is_empty());
let ce2 = CausalEdge::from_weight(1.5);
assert!((ce2.strength - 1.0).abs() < f32::EPSILON);
}
#[test]
fn relevance_score_formula() {
let ce = CausalEdge {
strength: 0.8,
confidence: 0.9,
evidence_count: 5,
..Default::default()
};
let expected = 0.8 * 0.9 * (6.0_f32).ln();
let actual = ce.relevance_score();
assert!(
(actual - expected).abs() < 1e-5,
"expected {expected}, got {actual}"
);
}
#[test]
fn relevance_score_single_evidence() {
let ce = CausalEdge::default();
let expected = 0.5 * 0.5 * (2.0_f32).ln();
let actual = ce.relevance_score();
assert!(
(actual - expected).abs() < 1e-5,
"expected {expected}, got {actual}"
);
}
#[test]
fn serde_round_trip() {
let ce = CausalEdge {
strength: 0.7,
confidence: 0.85,
evidence_count: 3,
confounders: vec!["age".into(), "diet".into()],
provenance: vec![MemoryId::new(), MemoryId::new()],
mechanism: "dopamine pathway".into(),
};
let json = serde_json::to_string(&ce).unwrap();
let ce2: CausalEdge = serde_json::from_str(&json).unwrap();
assert_eq!(ce, ce2);
}
#[test]
fn default_values() {
let ce = CausalEdge::default();
assert!((ce.strength - 0.5).abs() < f32::EPSILON);
assert!((ce.confidence - 0.5).abs() < f32::EPSILON);
assert_eq!(ce.evidence_count, 1);
}
}