#![deny(unsafe_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Stable,
PreTransition,
Transitioning,
Resolving,
}
impl std::fmt::Display for Phase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Phase::Stable => write!(f, "Stable"),
Phase::PreTransition => write!(f, "PreTransition"),
Phase::Transitioning => write!(f, "Transitioning"),
Phase::Resolving => write!(f, "Resolving"),
}
}
}
#[derive(Debug, Clone)]
struct QuantityState {
initial: f64,
current: f64,
tolerance: f64,
history: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct ConservationChecker {
quantities: HashMap<String, QuantityState>,
}
impl Default for ConservationChecker {
fn default() -> Self {
Self::new()
}
}
impl ConservationChecker {
pub fn new() -> Self {
Self {
quantities: HashMap::new(),
}
}
pub fn register(&mut self, name: impl Into<String>, initial_value: f64, tolerance: f64) {
let name = name.into();
self.quantities.insert(
name,
QuantityState {
initial: initial_value,
current: initial_value,
tolerance,
history: vec![initial_value],
},
);
}
pub fn update(&mut self, name: &str, value: f64) {
let state = self
.quantities
.get_mut(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
state.current = value;
}
pub fn is_conserved(&self, name: &str) -> bool {
let state = self
.quantities
.get(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
state.current >= state.initial - state.tolerance
}
pub fn violations(&self) -> Vec<String> {
self.quantities
.iter()
.filter(|(_, state)| state.current < state.initial - state.tolerance)
.map(|(name, _)| name.clone())
.collect()
}
pub fn snapshot(&mut self) {
for state in self.quantities.values_mut() {
state.history.push(state.current);
}
}
pub fn phase(&self, name: &str) -> Phase {
let state = self
.quantities
.get(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
let _rate = self.drift_rate(name);
if state.history.len() < 3 {
return Phase::Stable;
}
let is_violated = !self.is_conserved(name);
let len = state.history.len();
let recent_rate = if len >= 2 {
state.history[len - 1] - state.history[len - 2]
} else {
0.0
};
let older_rate = if len >= 4 {
state.history[len - 3] - state.history[len - 4]
} else {
0.0
};
let abs_recent = recent_rate.abs();
let noise_floor = state.tolerance.max(1.0) * 0.01;
if is_violated && recent_rate < -noise_floor {
Phase::Transitioning
} else if is_violated && recent_rate > noise_floor {
Phase::Resolving
} else if !is_violated && abs_recent > noise_floor && abs_recent > older_rate.abs() {
Phase::PreTransition
} else {
Phase::Stable
}
}
pub fn drift_rate(&self, name: &str) -> f64 {
let state = self
.quantities
.get(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
if state.history.len() < 2 {
return 0.0;
}
let n = state.history.len() as f64;
(state.history.last().unwrap() - state.history.first().unwrap()) / (n - 1.0)
}
pub fn current_value(&self, name: &str) -> f64 {
let state = self
.quantities
.get(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
state.current
}
pub fn initial_value(&self, name: &str) -> f64 {
let state = self
.quantities
.get(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
state.initial
}
pub fn snapshot_count(&self, name: &str) -> usize {
let state = self
.quantities
.get(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
state.history.len()
}
pub fn registered(&self) -> Vec<String> {
self.quantities.keys().cloned().collect()
}
pub fn deregister(&mut self, name: &str) -> bool {
self.quantities.remove(name).is_some()
}
pub fn reset_baseline(&mut self, name: &str) {
let state = self
.quantities
.get_mut(name)
.unwrap_or_else(|| panic!("quantity '{}' not registered", name));
state.initial = state.current;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_tracker_is_empty() {
let c = ConservationChecker::new();
assert!(c.registered().is_empty());
assert!(c.violations().is_empty());
}
#[test]
fn default_equals_new() {
let a = ConservationChecker::new();
let b = ConservationChecker::default();
assert_eq!(a.registered(), b.registered());
assert_eq!(a.violations(), b.violations());
}
#[test]
fn register_single_quantity() {
let mut c = ConservationChecker::new();
c.register("energy", 100.0, 0.0);
assert_eq!(c.registered(), vec!["energy"]);
}
#[test]
fn register_multiple_quantities() {
let mut c = ConservationChecker::new();
c.register("a", 10.0, 0.0);
c.register("b", 20.0, 0.0);
c.register("c", 30.0, 0.0);
let mut names = c.registered();
names.sort();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn register_overwrites_existing() {
let mut c = ConservationChecker::new();
c.register("x", 50.0, 1.0);
c.register("x", 99.0, 2.0);
assert_eq!(c.current_value("x"), 99.0);
assert!((c.initial_value("x") - 99.0).abs() < f64::EPSILON);
}
#[test]
fn initial_and_current_match_after_register() {
let mut c = ConservationChecker::new();
c.register("tokens", 42.0, 0.0);
assert!((c.initial_value("tokens") - 42.0).abs() < f64::EPSILON);
assert!((c.current_value("tokens") - 42.0).abs() < f64::EPSILON);
}
#[test]
fn update_changes_current_value() {
let mut c = ConservationChecker::new();
c.register("budget", 1000.0, 0.0);
c.update("budget", 950.0);
assert!((c.current_value("budget") - 950.0).abs() < f64::EPSILON);
}
#[test]
#[should_panic(expected = "not registered")]
fn update_panics_on_unknown_name() {
let mut c = ConservationChecker::new();
c.update("ghost", 10.0);
}
#[test]
fn update_to_same_value_is_fine() {
let mut c = ConservationChecker::new();
c.register("q", 10.0, 0.0);
c.update("q", 10.0);
assert!(c.is_conserved("q"));
}
#[test]
fn update_to_higher_value_is_fine() {
let mut c = ConservationChecker::new();
c.register("q", 10.0, 0.0);
c.update("q", 999.0);
assert!(c.is_conserved("q"));
}
#[test]
fn is_conserved_no_change() {
let mut c = ConservationChecker::new();
c.register("energy", 100.0, 0.0);
assert!(c.is_conserved("energy"));
}
#[test]
fn is_conserved_increase_ok() {
let mut c = ConservationChecker::new();
c.register("energy", 100.0, 0.0);
c.update("energy", 150.0);
assert!(c.is_conserved("energy"));
}
#[test]
fn is_conserved_decrease_violates_strict() {
let mut c = ConservationChecker::new();
c.register("energy", 100.0, 0.0);
c.update("energy", 99.9);
assert!(!c.is_conserved("energy"));
}
#[test]
fn is_conserved_within_tolerance() {
let mut c = ConservationChecker::new();
c.register("energy", 100.0, 5.0);
c.update("energy", 96.0);
assert!(c.is_conserved("energy"));
}
#[test]
fn is_conserved_exactly_at_tolerance_boundary() {
let mut c = ConservationChecker::new();
c.register("energy", 100.0, 5.0);
c.update("energy", 95.0); assert!(c.is_conserved("energy"));
}
#[test]
fn is_conserved_just_past_tolerance() {
let mut c = ConservationChecker::new();
c.register("energy", 100.0, 5.0);
c.update("energy", 94.999);
assert!(!c.is_conserved("energy"));
}
#[test]
#[should_panic(expected = "not registered")]
fn is_conserved_panics_on_unknown() {
ConservationChecker::new().is_conserved("nope");
}
#[test]
fn violations_none_when_all_ok() {
let mut c = ConservationChecker::new();
c.register("a", 10.0, 0.0);
c.register("b", 20.0, 0.0);
assert!(c.violations().is_empty());
}
#[test]
fn violations_reports_decreased() {
let mut c = ConservationChecker::new();
c.register("a", 10.0, 0.0);
c.register("b", 20.0, 0.0);
c.update("a", 5.0);
let v = c.violations();
assert_eq!(v, vec!["a"]);
}
#[test]
fn violations_multiple() {
let mut c = ConservationChecker::new();
c.register("a", 10.0, 0.0);
c.register("b", 20.0, 0.0);
c.register("c", 30.0, 0.0);
c.update("a", 5.0);
c.update("c", 25.0);
let mut v = c.violations();
v.sort();
assert_eq!(v, vec!["a", "c"]);
}
#[test]
fn snapshot_increments_count() {
let mut c = ConservationChecker::new();
c.register("q", 10.0, 0.0);
assert_eq!(c.snapshot_count("q"), 1); c.snapshot();
assert_eq!(c.snapshot_count("q"), 2);
c.snapshot();
assert_eq!(c.snapshot_count("q"), 3);
}
#[test]
fn snapshot_records_current_values() {
let mut c = ConservationChecker::new();
c.register("x", 100.0, 0.0);
c.update("x", 90.0);
c.snapshot();
assert!((c.drift_rate("x") - (-10.0)).abs() < f64::EPSILON);
}
#[test]
fn snapshot_captures_all_quantities() {
let mut c = ConservationChecker::new();
c.register("a", 10.0, 0.0);
c.register("b", 20.0, 0.0);
c.snapshot();
assert_eq!(c.snapshot_count("a"), 2);
assert_eq!(c.snapshot_count("b"), 2);
}
#[test]
fn drift_rate_zero_with_one_snapshot() {
let mut c = ConservationChecker::new();
c.register("q", 10.0, 0.0);
assert!((c.drift_rate("q") - 0.0).abs() < f64::EPSILON);
}
#[test]
fn drift_rate_positive_on_increase() {
let mut c = ConservationChecker::new();
c.register("q", 10.0, 0.0);
c.update("q", 20.0);
c.snapshot();
assert!((c.drift_rate("q") - 10.0).abs() < f64::EPSILON);
}
#[test]
fn drift_rate_negative_on_decrease() {
let mut c = ConservationChecker::new();
c.register("q", 100.0, 0.0);
c.update("q", 90.0);
c.snapshot();
assert!((c.drift_rate("q") - (-10.0)).abs() < f64::EPSILON);
}
#[test]
fn drift_rate_averages_over_many_snapshots() {
let mut c = ConservationChecker::new();
c.register("q", 0.0, 0.0);
for v in [10.0, 20.0, 30.0] {
c.update("q", v);
c.snapshot();
}
assert!((c.drift_rate("q") - 10.0).abs() < f64::EPSILON);
}
#[test]
#[should_panic(expected = "not registered")]
fn drift_rate_panics_on_unknown() {
ConservationChecker::new().drift_rate("nope");
}
#[test]
fn phase_stable_when_no_change() {
let mut c = ConservationChecker::new();
c.register("q", 100.0, 0.0);
c.snapshot();
c.snapshot();
assert_eq!(c.phase("q"), Phase::Stable);
}
#[test]
fn phase_transitioning_when_decreasing_and_violated() {
let mut c = ConservationChecker::new();
c.register("q", 100.0, 0.0);
c.update("q", 90.0);
c.snapshot();
c.update("q", 80.0);
c.snapshot();
assert_eq!(c.phase("q"), Phase::Transitioning);
}
#[test]
fn phase_resolving_when_violated_but_recovering() {
let mut c = ConservationChecker::new();
c.register("q", 100.0, 5.0);
c.update("q", 90.0);
c.snapshot();
c.update("q", 80.0);
c.snapshot();
c.update("q", 92.0);
c.snapshot();
assert_eq!(c.phase("q"), Phase::Resolving);
}
#[test]
fn phase_pre_transition_when_accelerating() {
let mut c = ConservationChecker::new();
c.register("q", 100.0, 50.0); c.update("q", 99.0);
c.snapshot();
c.update("q", 96.0); c.snapshot();
assert_eq!(c.phase("q"), Phase::PreTransition);
}
#[test]
fn phase_stable_with_few_snapshots() {
let mut c = ConservationChecker::new();
c.register("q", 10.0, 0.0);
assert_eq!(c.phase("q"), Phase::Stable);
}
#[test]
#[should_panic(expected = "not registered")]
fn phase_panics_on_unknown() {
ConservationChecker::new().phase("nope");
}
#[test]
fn phase_display() {
assert_eq!(format!("{}", Phase::Stable), "Stable");
assert_eq!(format!("{}", Phase::PreTransition), "PreTransition");
assert_eq!(format!("{}", Phase::Transitioning), "Transitioning");
assert_eq!(format!("{}", Phase::Resolving), "Resolving");
}
#[test]
fn deregister_removes_quantity() {
let mut c = ConservationChecker::new();
c.register("x", 10.0, 0.0);
assert!(c.deregister("x"));
assert!(c.registered().is_empty());
}
#[test]
fn deregister_unknown_returns_false() {
let mut c = ConservationChecker::new();
assert!(!c.deregister("ghost"));
}
#[test]
fn reset_baseline_clears_violation() {
let mut c = ConservationChecker::new();
c.register("budget", 100.0, 0.0);
c.update("budget", 50.0);
assert!(!c.is_conserved("budget"));
c.reset_baseline("budget");
assert!(c.is_conserved("budget"));
assert!((c.initial_value("budget") - 50.0).abs() < f64::EPSILON);
}
#[test]
#[should_panic(expected = "not registered")]
fn reset_baseline_panics_on_unknown() {
ConservationChecker::new().reset_baseline("nope");
}
#[test]
#[should_panic(expected = "not registered")]
fn snapshot_count_panics_on_unknown() {
ConservationChecker::new().snapshot_count("nope");
}
#[test]
#[should_panic(expected = "not registered")]
fn current_value_panics_on_unknown() {
ConservationChecker::new().current_value("nope");
}
#[test]
#[should_panic(expected = "not registered")]
fn initial_value_panics_on_unknown() {
ConservationChecker::new().initial_value("nope");
}
#[test]
fn budget_tracking_scenario() {
let mut c = ConservationChecker::new();
c.register("remaining", 5000.0, 1000.0);
c.update("remaining", 4500.0);
c.snapshot();
assert!(c.is_conserved("remaining"));
c.update("remaining", 4200.0);
c.snapshot();
assert!(c.is_conserved("remaining"));
c.update("remaining", 3900.0);
c.snapshot();
assert!(!c.is_conserved("remaining"));
}
#[test]
fn token_budget_depletion() {
let mut c = ConservationChecker::new();
c.register("tokens", 1000.0, 0.0);
for _ in 0..5 {
c.update("tokens", c.current_value("tokens") - 100.0);
c.snapshot();
}
assert!(!c.is_conserved("tokens"));
let v = c.violations();
assert!(v.contains(&"tokens".to_string()));
}
#[test]
fn multiple_snapshots_drift_calculation() {
let mut c = ConservationChecker::new();
c.register("q", 0.0, 0.0);
for i in 1..=10 {
c.update("q", i as f64 * 5.0);
c.snapshot();
}
assert!((c.drift_rate("q") - (50.0 / 10.0)).abs() < 1e-9);
}
#[test]
fn tolerance_zero_strict() {
let mut c = ConservationChecker::new();
c.register("strict", 100.0, 0.0);
c.update("strict", 99.999);
assert!(!c.is_conserved("strict"));
}
#[test]
fn large_tolerance_never_violates() {
let mut c = ConservationChecker::new();
c.register("lenient", 100.0, 10000.0);
c.update("lenient", -9000.0);
assert!(c.is_conserved("lenient"));
}
#[test]
fn negative_values_work() {
let mut c = ConservationChecker::new();
c.register("temp", -40.0, 0.0);
c.update("temp", -50.0);
assert!(!c.is_conserved("temp"));
c.update("temp", -30.0);
assert!(c.is_conserved("temp"));
}
#[test]
fn clone_independence() {
let mut c = ConservationChecker::new();
c.register("q", 10.0, 0.0);
let mut c2 = c.clone();
c2.update("q", 5.0);
assert!(c.is_conserved("q"));
assert!(!c2.is_conserved("q"));
}
#[test]
fn snapshot_count_panics_on_unknown2() {
let c = ConservationChecker::new();
let result = std::panic::catch_unwind(|| c.snapshot_count("nope"));
assert!(result.is_err());
}
#[test]
fn register_with_string_ref() {
let mut c = ConservationChecker::new();
let name = "quantity";
c.register(name, 42.0, 1.0);
assert!(c.is_conserved("quantity"));
}
#[test]
fn empty_violations_after_register() {
let mut c = ConservationChecker::new();
c.register("a", 10.0, 0.0);
assert!(c.violations().is_empty());
}
#[test]
fn phase_stable_on_increase() {
let mut c = ConservationChecker::new();
c.register("q", 100.0, 10.0);
c.update("q", 105.0);
c.snapshot();
c.update("q", 110.0);
c.snapshot();
c.update("q", 115.0); c.snapshot();
assert_eq!(c.phase("q"), Phase::Stable);
}
}