use super::PhysState;
use crate::conservation::checkers_impl::{EnergyConservationChecker, MomentumConservationChecker};
use crate::conservation::checkers_types::{
ConservationReport, ConservationViolationDetail, NoetherCheckResult, PhysicalSymmetry,
};
pub struct EntropyConservationChecker {
pub abs_tolerance: f64,
pub check_clausius: bool,
}
impl EntropyConservationChecker {
pub fn new(abs_tolerance: f64) -> Self {
Self {
abs_tolerance,
check_clausius: true,
}
}
pub fn without_clausius(mut self) -> Self {
self.check_clausius = false;
self
}
pub fn default_tolerances() -> Self {
Self::new(1e-6)
}
pub fn check_pair(
&self,
initial: &PhysState,
final_state: &PhysState,
step_index: Option<usize>,
) -> ConservationReport {
let mut report = ConservationReport::new("EntropyConservationChecker");
report.states_checked = 1;
let s_i = initial.get("entropy").unwrap_or(0.0);
let s_f = final_state.get("entropy").unwrap_or(0.0);
let delta = s_f - s_i;
report.track_change(delta.abs());
if delta < -self.abs_tolerance {
report.add_violation(ConservationViolationDetail::new(
"Second Law of Thermodynamics",
"entropy",
s_i,
s_f,
self.abs_tolerance,
step_index,
));
}
if self.check_clausius {
let q = final_state
.get("heat_flow")
.or_else(|| initial.get("heat_flow"))
.unwrap_or(0.0);
let t = final_state
.get("temperature")
.or_else(|| initial.get("temperature"))
.unwrap_or(0.0);
if t > 1e-10 {
let q_over_t = q / t;
if delta < q_over_t - self.abs_tolerance {
report.add_violation(ConservationViolationDetail::new(
"Clausius Inequality (ΔS ≥ Q/T)",
"entropy_vs_heat",
q_over_t,
delta,
self.abs_tolerance,
step_index,
));
}
}
}
report
}
pub fn check_trajectory(&self, states: &[PhysState]) -> ConservationReport {
let mut report = ConservationReport::new("EntropyConservationChecker");
if states.len() < 2 {
report.states_checked = states.len();
return report;
}
report.states_checked = states.len() - 1;
for (idx, window) in states.windows(2).enumerate() {
let pair_report = self.check_pair(&window[0], &window[1], Some(idx));
report.track_change(pair_report.max_absolute_change);
for v in pair_report.violations {
report.add_violation(v);
}
}
report
}
}
pub struct AngularMomentumChecker {
pub abs_tolerance: f64,
pub rel_tolerance: f64,
pub check_torque_consistency: bool,
}
impl AngularMomentumChecker {
pub fn new(abs_tolerance: f64, rel_tolerance: f64) -> Self {
Self {
abs_tolerance,
rel_tolerance,
check_torque_consistency: true,
}
}
pub fn default_tolerances() -> Self {
Self::new(1e-6, 1e-3)
}
pub fn without_torque_check(mut self) -> Self {
self.check_torque_consistency = false;
self
}
fn component_magnitude(lx: f64, ly: f64, lz: f64) -> f64 {
(lx * lx + ly * ly + lz * lz).sqrt()
}
pub fn check_pair(
&self,
initial: &PhysState,
final_state: &PhysState,
step_index: Option<usize>,
) -> ConservationReport {
let mut report = ConservationReport::new("AngularMomentumChecker");
report.states_checked = 1;
let (lx_i, ly_i, lz_i) = (
initial.get("angular_momentum_x").unwrap_or(0.0),
initial.get("angular_momentum_y").unwrap_or(0.0),
initial.get("angular_momentum_z").unwrap_or(0.0),
);
let (lx_f, ly_f, lz_f) = (
final_state.get("angular_momentum_x").unwrap_or(0.0),
final_state.get("angular_momentum_y").unwrap_or(0.0),
final_state.get("angular_momentum_z").unwrap_or(0.0),
);
let mag_initial = Self::component_magnitude(lx_i, ly_i, lz_i);
for (name, l_i, l_f) in [("Lx", lx_i, lx_f), ("Ly", ly_i, ly_f), ("Lz", lz_i, lz_f)] {
let delta = (l_f - l_i).abs();
report.track_change(delta);
let abs_violation = delta > self.abs_tolerance;
let rel_violation = mag_initial > 1e-300 && delta / mag_initial > self.rel_tolerance;
if abs_violation || rel_violation {
report.add_violation(ConservationViolationDetail::new(
"Angular Momentum Conservation",
name,
l_i,
l_f,
self.abs_tolerance,
step_index,
));
}
}
if self.check_torque_consistency {
let dt = final_state
.get("dt")
.or_else(|| initial.get("dt"))
.unwrap_or(0.0);
if dt > 1e-300 {
for (name, torque_key, l_i, l_f) in [
("Lx", "torque_x", lx_i, lx_f),
("Ly", "torque_y", ly_i, ly_f),
("Lz", "torque_z", lz_i, lz_f),
] {
let tau = final_state
.get(torque_key)
.or_else(|| initial.get(torque_key))
.unwrap_or(0.0);
let expected_delta = tau * dt;
let actual_delta = l_f - l_i;
let discrepancy = (actual_delta - expected_delta).abs();
if discrepancy > self.abs_tolerance {
report.add_violation(ConservationViolationDetail::new(
format!("Torque Consistency (τ = ΔL/Δt) — {name}"),
torque_key,
expected_delta,
actual_delta,
self.abs_tolerance,
step_index,
));
}
}
}
}
report
}
pub fn check_trajectory(&self, states: &[PhysState]) -> ConservationReport {
let mut report = ConservationReport::new("AngularMomentumChecker");
if states.len() < 2 {
report.states_checked = states.len();
return report;
}
report.states_checked = states.len() - 1;
for (idx, window) in states.windows(2).enumerate() {
let pair_report = self.check_pair(&window[0], &window[1], Some(idx));
report.track_change(pair_report.max_absolute_change);
for v in pair_report.violations {
report.add_violation(v);
}
}
report
}
}
#[derive(Debug, Clone)]
pub struct NoetherSymmetryValidator {
pub symmetries: Vec<PhysicalSymmetry>,
pub abs_tolerance: f64,
}
impl NoetherSymmetryValidator {
pub fn new(symmetries: Vec<PhysicalSymmetry>, abs_tolerance: f64) -> Self {
Self {
symmetries,
abs_tolerance,
}
}
pub fn validate(&self, states: &[PhysState]) -> Vec<NoetherCheckResult> {
self.symmetries
.iter()
.map(|&sym| {
let report = match sym {
PhysicalSymmetry::TimeTranslation => {
EnergyConservationChecker::new(self.abs_tolerance, f64::MAX)
.check_trajectory(states)
}
PhysicalSymmetry::SpatialTranslation => {
MomentumConservationChecker::new(self.abs_tolerance, f64::MAX)
.check_trajectory(states)
}
PhysicalSymmetry::Rotation => {
AngularMomentumChecker::new(self.abs_tolerance, f64::MAX)
.check_trajectory(states)
}
};
let conserved = report.passed;
NoetherCheckResult {
symmetry: sym,
conserved,
report,
}
})
.collect()
}
pub fn all_conserved(&self, states: &[PhysState]) -> bool {
self.validate(states).iter().all(|r| r.conserved)
}
}