use serde::{Deserialize, Serialize};
pub fn base_level_activation(access_times: &[f64], now: f64, decay: f32) -> f32 {
if access_times.is_empty() {
return -20.0;
}
let d = f64::from(decay);
let sum: f64 = access_times
.iter()
.filter_map(|&t_k| {
let elapsed = now - t_k;
if elapsed > 0.0 {
Some(elapsed.powf(-d))
} else {
None
}
})
.sum();
if sum <= 0.0 {
return -20.0;
}
sum.ln() as f32
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessRingBuffer {
buf: Vec<f64>,
head: usize,
len: usize,
}
impl AccessRingBuffer {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "capacity must be > 0");
Self {
buf: vec![0.0; capacity],
head: 0,
len: 0,
}
}
pub fn capacity(&self) -> usize {
self.buf.len()
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn push(&mut self, timestamp: f64) {
self.buf[self.head] = timestamp;
self.head = (self.head + 1) % self.buf.len();
if self.len < self.buf.len() {
self.len += 1;
}
}
pub fn timestamps(&self) -> Vec<f64> {
if self.len == 0 {
return Vec::new();
}
let cap = self.buf.len();
let mut out = Vec::with_capacity(self.len);
if self.len < cap {
out.extend_from_slice(&self.buf[..self.len]);
} else {
out.extend_from_slice(&self.buf[self.head..]);
out.extend_from_slice(&self.buf[..self.head]);
}
out
}
pub fn activation(&self, now: f64, decay: f32) -> f32 {
let ts = self.timestamps();
base_level_activation(&ts, now, decay)
}
pub fn bootstrap(last_accessed: f64, access_count: u32, capacity: usize) -> Self {
let mut buf = Self::new(capacity);
if access_count == 0 {
return buf;
}
let count = f64::from(access_count);
for i in 0..access_count {
let t = last_accessed - (count - 1.0 - f64::from(i));
buf.push(t);
}
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_access_returns_zero() {
let now = 1000.0_f64;
let access_times = [now - 1.0];
let result = base_level_activation(&access_times, now, 0.5);
assert!((result - 0.0).abs() < 1e-5, "expected ≈0.0, got {result}");
}
#[test]
fn test_more_accesses_increase_activation() {
let now = 1000.0_f64;
let one_access = [now - 10.0];
let three_accesses = [now - 10.0, now - 20.0, now - 30.0];
let b1 = base_level_activation(&one_access, now, 0.5);
let b3 = base_level_activation(&three_accesses, now, 0.5);
assert!(
b3 > b1,
"3 accesses ({b3}) should yield higher activation than 1 access ({b1})"
);
}
#[test]
fn test_recent_access_higher_than_old() {
let now = 1000.0_f64;
let recent = [now - 5.0];
let old = [now - 90.0];
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 access ({b_recent}) should be higher than old ({b_old})"
);
}
#[test]
fn test_decay_parameter_affects_rate() {
let now = 1000.0_f64;
let access_times = [now - 100.0];
let b_slow = base_level_activation(&access_times, now, 0.3);
let b_fast = base_level_activation(&access_times, now, 0.7);
assert!(
b_slow > b_fast,
"d=0.3 should yield higher activation ({b_slow}) than d=0.7 ({b_fast}) for old memory"
);
}
#[test]
fn test_empty_access_history() {
let result = base_level_activation(&[], 1000.0, 0.5);
assert!(
result < -10.0,
"empty history should return < -10.0, got {result}"
);
}
#[test]
fn test_access_ring_buffer() {
let mut buf = AccessRingBuffer::new(5);
for i in 1u32..=8 {
buf.push(i as f64 * 10.0);
}
assert_eq!(buf.len(), 5, "buffer should hold exactly 5 items");
let ts = buf.timestamps();
assert_eq!(ts.len(), 5);
assert_eq!(
ts,
vec![40.0, 50.0, 60.0, 70.0, 80.0],
"timestamps() should return oldest-first: {ts:?}"
);
}
}