use chrono::{DateTime, Utc};
use crate::types::MemoryRecord;
pub fn base_level_activation(record: &MemoryRecord, now: DateTime<Utc>, decay: f64) -> f64 {
if record.access_times.is_empty() {
return f64::NEG_INFINITY;
}
let mut total = 0.0;
for t_k in &record.access_times {
let age_secs = (now - *t_k).num_seconds() as f64;
let age_secs = age_secs.max(0.001); total += age_secs.powf(-decay);
}
if total <= 0.0 {
return f64::NEG_INFINITY;
}
total.ln()
}
pub fn spreading_activation(record: &MemoryRecord, context_keywords: &[String], weight: f64) -> f64 {
if context_keywords.is_empty() {
return 0.0;
}
let content_lower = record.content.to_lowercase();
let matches = context_keywords
.iter()
.filter(|kw| content_lower.contains(&kw.to_lowercase()))
.count();
weight * (matches as f64 / context_keywords.len() as f64)
}
pub fn retrieval_activation(
record: &MemoryRecord,
context_keywords: &[String],
now: DateTime<Utc>,
base_decay: f64,
context_weight: f64,
importance_weight: f64,
contradiction_penalty: f64,
) -> f64 {
let base = base_level_activation(record, now, base_decay);
if base == f64::NEG_INFINITY {
return f64::NEG_INFINITY;
}
let context = spreading_activation(record, context_keywords, context_weight);
let importance_boost = record.importance * importance_weight;
let penalty = if record.contradicted_by.is_some() {
contradiction_penalty
} else {
0.0
};
base + context + importance_boost - penalty
}
pub fn normalize_activation(activation: f64, center: f64, scale: f64) -> f64 {
if activation == f64::NEG_INFINITY {
return 0.0;
}
1.0 / (1.0 + (-(activation - center) / scale).exp())
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
fn make_record(age_secs: i64) -> (MemoryRecord, DateTime<Utc>) {
let now = Utc::now();
let access_time = now - Duration::seconds(age_secs);
let record = MemoryRecord {
id: "test".to_string(),
content: "test content".to_string(),
memory_type: crate::types::MemoryType::Factual,
importance: 0.5,
access_times: vec![access_time],
working_strength: 1.0,
core_strength: 0.0,
layer: crate::types::MemoryLayer::Working,
consolidation_count: 0,
last_consolidated: None,
created_at: access_time,
source: String::new(),
contradicts: None,
contradicted_by: None,
superseded_by: None,
pinned: false,
metadata: None,
};
(record, now)
}
#[test]
fn test_normalize_neg_infinity() {
assert_eq!(normalize_activation(f64::NEG_INFINITY, -5.5, 1.5), 0.0);
}
#[test]
fn test_normalize_center_gives_half() {
let result = normalize_activation(-5.5, -5.5, 1.5);
assert!((result - 0.5).abs() < 0.001, "center should map to ~0.5, got {result}");
}
#[test]
fn test_normalize_monotonic() {
let low = normalize_activation(-8.0, -5.5, 1.5);
let mid = normalize_activation(-5.5, -5.5, 1.5);
let high = normalize_activation(-2.0, -5.5, 1.5);
assert!(low < mid, "low {low} should be < mid {mid}");
assert!(mid < high, "mid {mid} should be < high {high}");
}
#[test]
fn test_normalize_bounded() {
for x in [-100.0, -10.0, -5.5, 0.0, 5.0, 100.0] {
let n = normalize_activation(x, -5.5, 1.5);
assert!(n >= 0.0 && n <= 1.0, "normalize({x}) = {n} out of [0,1]");
}
}
#[test]
fn test_recency_discrimination_improved() {
let d = 0.5;
let center = -5.5;
let scale = 1.5;
let b_1h = (3600.0_f64).powf(-d).ln(); let b_7d = (604800.0_f64).powf(-d).ln();
let old_delta = ((b_1h + 10.0) / 20.0).clamp(0.0, 1.0)
- ((b_7d + 10.0) / 20.0).clamp(0.0, 1.0);
let new_delta = normalize_activation(b_1h, center, scale)
- normalize_activation(b_7d, center, scale);
assert!(
new_delta > old_delta * 2.5,
"sigmoid delta ({new_delta:.4}) should be >2.5x old delta ({old_delta:.4})"
);
}
#[test]
fn test_base_level_recency() {
let (recent, now) = make_record(60); let (old, _) = make_record(604800);
let b_recent = base_level_activation(&recent, now, 0.5);
let b_old = base_level_activation(&old, now, 0.5);
assert!(b_recent > b_old, "recent ({b_recent}) should have higher activation than old ({b_old})");
}
#[test]
fn test_base_level_frequency() {
let now = Utc::now();
let mut frequent = make_record(3600).0;
for i in 1..5 {
frequent.access_times.push(now - Duration::seconds(3600 * i));
}
let (single, _) = make_record(3600);
let b_freq = base_level_activation(&frequent, now, 0.5);
let b_single = base_level_activation(&single, now, 0.5);
assert!(b_freq > b_single, "frequent ({b_freq}) should have higher activation than single ({b_single})");
}
#[test]
fn test_base_level_no_accesses() {
let (mut record, now) = make_record(60);
record.access_times.clear();
assert_eq!(base_level_activation(&record, now, 0.5), f64::NEG_INFINITY);
}
#[test]
fn test_spreading_activation_matches() {
let (record, _) = make_record(60);
let context = vec!["test".to_string(), "content".to_string()];
let spread = spreading_activation(&record, &context, 1.5);
assert!(spread > 0.0, "should have positive spreading activation");
assert!((spread - 1.5).abs() < 0.01, "full match should give ~weight, got {spread}");
}
#[test]
fn test_spreading_activation_no_match() {
let (record, _) = make_record(60);
let context = vec!["zzzznotpresent".to_string()];
let spread = spreading_activation(&record, &context, 1.5);
assert_eq!(spread, 0.0);
}
#[test]
fn test_normalize_scale_effect() {
let steep = normalize_activation(-3.0, -5.5, 0.5) - normalize_activation(-8.0, -5.5, 0.5);
let gentle = normalize_activation(-3.0, -5.5, 3.0) - normalize_activation(-8.0, -5.5, 3.0);
assert!(steep > 0.0);
assert!(gentle > 0.0);
assert!(steep > gentle, "steep ({steep}) should discriminate more than gentle ({gentle})");
}
}