pub fn recency(hours_since: f32) -> f32 {
0.995_f32.powf(hours_since.max(0.0))
}
pub fn familiarity(peak: f32, elapsed_days: f32, tau_days: f32, decay_exp: f32) -> f32 {
peak * (1.0 + elapsed_days.max(0.0) / tau_days).powf(-decay_exp)
}
pub fn exposed_peak(familiarity_now: f32, rate_per_day: f32, dt_days: f32) -> f32 {
(familiarity_now + rate_per_day * dt_days * (1.0 - familiarity_now)).clamp(0.0, 1.0)
}
pub fn grudge(memories: impl IntoIterator<Item = (f32, f32)>) -> f32 {
memories
.into_iter()
.map(|(valence, r)| (-valence).max(0.0) * r)
.fold(0.0_f32, f32::max)
.clamp(0.0, 1.0)
}
pub fn forgiving_pace(trust: f32, forgiveness: f32) -> f32 {
1.0 + forgiveness.max(0.0) * trust.clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recency_halves_in_about_138_hours() {
assert!((recency(0.0) - 1.0).abs() < 1e-6);
assert!((recency(138.0) - 0.5).abs() < 0.01);
assert_eq!(recency(-5.0), 1.0);
}
#[test]
fn familiarity_follows_the_power_law() {
assert!((familiarity(0.8, 3.0, 1.0, 0.5) - 0.4).abs() < 1e-6);
assert_eq!(familiarity(0.8, 0.0, 1.0, 0.5), 0.8);
}
#[test]
fn exposure_approaches_one_and_never_exceeds_it() {
let f = exposed_peak(0.0, 2.0, 1.0);
assert!((f - 1.0).abs() < 1e-6);
assert!(exposed_peak(0.9, 2.0, 1.0) <= 1.0);
}
#[test]
fn grudge_counts_only_negative_memories_and_clamps() {
assert_eq!(grudge([(0.5, 1.0), (0.3, 1.0)]), 0.0);
assert!((grudge([(-0.3, 1.0), (-0.3, 1.0)]) - 0.3).abs() < 1e-6);
assert!((grudge([(-0.5, 1.0), (-0.2, 1.0)]) - 0.5).abs() < 1e-6);
assert!((grudge([(-0.3, 1.0)]) - 0.3).abs() < 1e-6);
assert_eq!(grudge([(-0.9, 1.0), (-0.9, 1.0)]), 0.9);
assert_eq!(grudge([(-1.4, 1.0)]), 1.0);
}
#[test]
fn a_trusted_offender_is_forgiven_faster_and_none_is_the_old_curve() {
assert_eq!(forgiving_pace(1.0, 0.0), 1.0);
assert_eq!(forgiving_pace(0.3, -2.0), 1.0);
assert_eq!(forgiving_pace(0.0, 2.0), 1.0);
assert!((forgiving_pace(0.5, 2.0) - 2.0).abs() < 1e-6);
assert!((forgiving_pace(1.0, 2.0) - 3.0).abs() < 1e-6);
assert!(
(forgiving_pace(7.0, 2.0) - 3.0).abs() < 1e-6,
"trust is clamped"
);
assert!((recency(69.0 * forgiving_pace(0.5, 2.0)) - 0.5).abs() < 0.01);
}
}