pub fn triad_delta(rate_per_day: f32, dt_days: f32, affinity_ab: f32, affinity_bc: f32) -> f32 {
rate_per_day * dt_days * affinity_ab * affinity_bc
}
pub fn dyad_delta(rate_per_day: f32, dt_days: f32, affinity_ab: f32, affinity_ba: f32) -> f32 {
let mean = 0.5 * (affinity_ab + affinity_ba);
let step = (rate_per_day * dt_days).clamp(0.0, 1.0);
(mean - affinity_ab) * step
}
pub fn relax_toward_zero(value: f32, amount: f32) -> f32 {
if value > 0.0 {
(value - amount).max(0.0)
} else {
(value + amount).min(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn friend_of_a_friend_is_pulled_toward_friendship() {
assert!(triad_delta(0.5, 1.0, 0.8, 0.8) > 0.0);
assert!(triad_delta(0.5, 1.0, 0.8, -0.8) < 0.0);
}
#[test]
fn a_dyad_converges_on_its_mean_without_overshooting() {
let d = dyad_delta(1.0, 0.5, 0.30, 0.10);
assert!(
(d - (0.20 - 0.30) * 0.5).abs() < 1e-6,
"each moves half way to the mean"
);
assert!(
((0.30 + d) - (0.10 - d) - 0.10).abs() < 1e-6,
"the gap halves"
);
let d = dyad_delta(1.0, 1.0, 0.30, 0.10);
assert!(
((0.30 + d) - (0.10 - d)).abs() < 1e-6,
"a full step ends up equal"
);
assert_eq!(dyad_delta(2.0, 0.1, 0.25, 0.25), 0.0);
let d = dyad_delta(1000.0, 1.0, 0.30, 0.10);
assert!(
(0.30 + d - 0.20).abs() < 1e-6,
"clamped to the mean, not past it"
);
let (a, b) = (0.30_f32, -0.10_f32);
let d = dyad_delta(0.5, 0.25, a, b);
assert!(((a + d) + (b - d) - (a + b)).abs() < 1e-6);
}
#[test]
fn relaxation_never_crosses_zero() {
assert_eq!(relax_toward_zero(0.01, 0.05), 0.0);
assert_eq!(relax_toward_zero(-0.01, 0.05), 0.0);
assert!((relax_toward_zero(0.5, 0.1) - 0.4).abs() < 1e-6);
}
}