use crate::simulation::result_injection::StateVector;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConservationLaw {
Energy,
Mass,
Momentum,
AngularMomentum,
Charge,
}
impl ConservationLaw {
pub fn name(&self) -> &'static str {
match self {
ConservationLaw::Energy => "Energy Conservation",
ConservationLaw::Mass => "Mass Conservation",
ConservationLaw::Momentum => "Momentum Conservation",
ConservationLaw::AngularMomentum => "Angular Momentum Conservation",
ConservationLaw::Charge => "Charge Conservation",
}
}
}
pub struct ConservationChecker {
laws: Vec<ConservationLaw>,
tolerance: f64,
}
impl ConservationChecker {
pub fn new(tolerance: f64) -> Self {
Self {
laws: vec![ConservationLaw::Energy, ConservationLaw::Mass],
tolerance,
}
}
pub fn add_law(&mut self, law: ConservationLaw) {
if !self.laws.contains(&law) {
self.laws.push(law);
}
}
pub fn check(&self, trajectory: &[StateVector]) -> Vec<ViolationReport> {
self.laws
.iter()
.filter_map(|law| self.check_law(*law, trajectory))
.collect()
}
fn check_law(
&self,
law: ConservationLaw,
trajectory: &[StateVector],
) -> Option<ViolationReport> {
if trajectory.len() < 2 {
return None;
}
let quantity_name = match law {
ConservationLaw::Energy => "energy",
ConservationLaw::Mass => "mass",
ConservationLaw::Momentum => "momentum",
ConservationLaw::AngularMomentum => "angular_momentum",
ConservationLaw::Charge => "charge",
};
let initial_value = trajectory
.first()?
.state
.get(quantity_name)
.copied()
.unwrap_or(0.0);
let mut worst_value = initial_value;
let mut worst_change = 0.0_f64;
let mut worst_relative_change = 0.0_f64;
for state in trajectory.iter().skip(1) {
let value = state.state.get(quantity_name).copied().unwrap_or(0.0);
let change = (value - initial_value).abs();
let relative_change = if initial_value.abs() > 1e-10 {
change / initial_value.abs()
} else {
change
};
if relative_change > worst_relative_change {
worst_value = value;
worst_change = change;
worst_relative_change = relative_change;
}
}
if worst_relative_change > self.tolerance {
Some(ViolationReport {
law: law.name().to_string(),
initial_value,
final_value: worst_value,
change: worst_change,
relative_change: worst_relative_change,
tolerance: self.tolerance,
})
} else {
None
}
}
}
impl Default for ConservationChecker {
fn default() -> Self {
Self::new(0.01) }
}
#[derive(Debug, Clone)]
pub struct ViolationReport {
pub law: String,
pub initial_value: f64,
pub final_value: f64,
pub change: f64,
pub relative_change: f64,
pub tolerance: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conservation_checker() {
let checker = ConservationChecker::new(0.01);
let mut trajectory = Vec::new();
for i in 0..10 {
let mut state = std::collections::HashMap::new();
state.insert("energy".to_string(), 100.0); state.insert("mass".to_string(), 50.0);
trajectory.push(StateVector {
time: i as f64,
state,
});
}
let violations = checker.check(&trajectory);
assert!(violations.is_empty());
}
#[test]
fn test_conservation_violation() {
let checker = ConservationChecker::new(0.01);
let mut trajectory = Vec::new();
for i in 0..10 {
let mut state = std::collections::HashMap::new();
state.insert("energy".to_string(), 100.0 + i as f64 * 10.0);
trajectory.push(StateVector {
time: i as f64,
state,
});
}
let violations = checker.check(&trajectory);
assert!(!violations.is_empty());
assert_eq!(violations[0].law, "Energy Conservation");
}
#[test]
fn regression_mid_trajectory_spike_is_detected() {
let checker = ConservationChecker::new(0.01);
let mut trajectory = Vec::new();
let energies = [100.0, 100.5, 1000.0, 100.5, 100.2, 100.0];
for (i, &e) in energies.iter().enumerate() {
let mut state = std::collections::HashMap::new();
state.insert("energy".to_string(), e);
trajectory.push(StateVector {
time: i as f64,
state,
});
}
let first = trajectory.first().expect("non-empty").state["energy"];
let last = trajectory.last().expect("non-empty").state["energy"];
assert!((last - first).abs() / first.abs() < 0.01);
let violations = checker.check(&trajectory);
assert!(
!violations.is_empty(),
"mid-trajectory spike to 1000.0 must be detected even though the endpoints match"
);
let energy_violation = violations
.iter()
.find(|v| v.law == "Energy Conservation")
.expect("expected an energy conservation violation");
assert!((energy_violation.final_value - 1000.0).abs() < 1e-9);
}
#[test]
fn regression_no_false_positive_within_tolerance_throughout() {
let checker = ConservationChecker::new(0.05);
let mut trajectory = Vec::new();
let energies = [100.0, 102.0, 98.0, 103.0, 99.0, 101.0];
for (i, &e) in energies.iter().enumerate() {
let mut state = std::collections::HashMap::new();
state.insert("energy".to_string(), e);
trajectory.push(StateVector {
time: i as f64,
state,
});
}
let violations = checker.check(&trajectory);
assert!(
violations.is_empty(),
"small in-tolerance oscillation must not be flagged: {violations:?}"
);
}
}