use crate::merge::Merge;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayCounter {
accumulators: HashMap<String, (f64, u64)>,
lambda: f64,
total: f64,
}
impl DecayCounter {
pub fn new(half_life_secs: f64) -> Self {
let lambda = 2.0_f64.ln() / half_life_secs;
Self {
accumulators: HashMap::new(),
lambda,
total: 0.0,
}
}
pub fn record(&mut self, node: &str, value: f64, time_ns: u64) {
let entry = self.accumulators.entry(node.to_string()).or_insert((0.0, 0));
let elapsed_ns = time_ns.saturating_sub(entry.1);
let elapsed_secs = elapsed_ns as f64 / 1e9;
entry.0 *= (-self.lambda * elapsed_secs).exp();
entry.0 += value;
entry.1 = time_ns;
self.recompute_total();
}
pub fn node_value(&mut self, node: &str, now_ns: u64) -> f64 {
if let Some((val, last_time)) = self.accumulators.get(node) {
let elapsed_ns = now_ns.saturating_sub(*last_time);
let elapsed_secs = elapsed_ns as f64 / 1e9;
val * (-self.lambda * elapsed_secs).exp()
} else {
0.0
}
}
pub fn total(&mut self, now_ns: u64) -> f64 {
let mut sum = 0.0;
for (val, last_time) in self.accumulators.values() {
let elapsed_ns = now_ns.saturating_sub(*last_time);
let elapsed_secs = elapsed_ns as f64 / 1e9;
sum += val * (-self.lambda * elapsed_secs).exp();
}
self.total = sum;
sum
}
pub fn half_life(&self) -> f64 {
2.0_f64.ln() / self.lambda
}
pub fn lambda(&self) -> f64 {
self.lambda
}
fn recompute_total(&mut self) {
self.total = self.accumulators.values().map(|(v, _)| v).sum();
}
}
impl Merge for DecayCounter {
fn merge(&mut self, other: &Self) {
for (node, (val, time)) in &other.accumulators {
let entry = self.accumulators.entry(node.clone()).or_insert((0.0, 0));
if *time > entry.1 {
*entry = (*val, *time);
} else if *time == entry.1 {
entry.0 = entry.0.max(*val);
}
}
self.recompute_total();
}
}
impl PartialEq for DecayCounter {
fn eq(&self, other: &Self) -> bool {
(self.total - other.total).abs() < 0.001
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayConstraintState {
pub node_id: String,
pub satisfied: DecayCounter,
pub violations: DecayCounter,
pub half_life: f64,
}
impl DecayConstraintState {
pub fn new(node_id: &str, half_life_secs: f64) -> Self {
Self {
node_id: node_id.to_string(),
satisfied: DecayCounter::new(half_life_secs),
violations: DecayCounter::new(half_life_secs),
half_life: half_life_secs,
}
}
pub fn record_satisfied(&mut self, count: f64, time_ns: u64) {
self.satisfied.record(&self.node_id, count, time_ns);
}
pub fn record_violations(&mut self, count: f64, time_ns: u64) {
self.violations.record(&self.node_id, count, time_ns);
}
pub fn satisfaction_rate(&mut self, time_ns: u64) -> f64 {
let sat = self.satisfied.total(time_ns);
let vio = self.violations.total(time_ns);
let total = sat + vio;
if total == 0.0 { return 1.0; }
sat / total
}
pub fn violation_weight(&mut self, time_ns: u64) -> f64 {
self.violations.total(time_ns)
}
}
impl Merge for DecayConstraintState {
fn merge(&mut self, other: &Self) {
self.satisfied.merge(&other.satisfied);
self.violations.merge(&other.violations);
}
}
impl PartialEq for DecayConstraintState {
fn eq(&self, other: &Self) -> bool {
self.satisfied == other.satisfied && self.violations == other.violations
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merge::laws;
const NS_PER_SEC: u64 = 1_000_000_000;
#[test]
fn test_decay() {
let mut dc = DecayCounter::new(1.0); dc.record("a", 100.0, 0);
let val = dc.node_value("a", NS_PER_SEC);
assert!((val - 50.0).abs() < 1.0, "Expected ~50, got {:.1}", val);
let val = dc.node_value("a", 2 * NS_PER_SEC);
assert!((val - 25.0).abs() < 1.0, "Expected ~25, got {:.1}", val);
}
#[test]
fn test_recent_weighs_more() {
let mut dc = DecayCounter::new(1.0);
dc.record("a", 100.0, 0); dc.record("b", 100.0, 5 * NS_PER_SEC);
let total = dc.total(6 * NS_PER_SEC);
assert!(total < 60.0, "Recent violations should dominate, total={:.1}", total);
}
#[test]
fn test_merge_takes_latest() {
let mut a = DecayCounter::new(1.0);
a.record("x", 100.0, 10 * NS_PER_SEC);
let mut b = DecayCounter::new(1.0);
b.record("x", 200.0, 20 * NS_PER_SEC);
let merged = a.merged(&b);
assert!(merged.accumulators.get("x").unwrap().1 == 20 * NS_PER_SEC);
}
#[test]
fn test_decay_constraint_state() {
let mut state = DecayConstraintState::new("test", 60.0); state.record_satisfied(100.0, 0);
state.record_violations(5.0, 0);
let rate = state.satisfaction_rate(0);
assert!((rate - 0.952).abs() < 0.01);
}
#[test]
fn test_old_violations_decay() {
let mut state = DecayConstraintState::new("test", 1.0);
state.record_violations(100.0, 0);
let weight = state.violation_weight(10 * NS_PER_SEC);
assert!(weight < 1.0, "Old violations should decay: weight={:.2}", weight);
}
}