Skip to main content

noether_guard/
conservation.rs

1use crate::symmetry::Symmetry;
2
3/// A conservation law derived from a symmetry via Noether's theorem.
4#[derive(Debug, Clone)]
5pub struct ConservationLaw {
6    pub symmetry: Symmetry,
7    pub name: String,
8    pub initial_value: f64,
9    pub tolerance: f64,
10}
11
12impl ConservationLaw {
13    /// Create a new conservation law.
14    pub fn new(symmetry: Symmetry, initial_value: f64, tolerance: f64) -> Self {
15        Self {
16            name: symmetry.conserved_quantity().to_string(),
17            symmetry,
18            initial_value,
19            tolerance,
20        }
21    }
22
23    /// Check whether `value` is conserved within tolerance.
24    pub fn is_conserved(&self, value: f64) -> bool {
25        (value - self.initial_value).abs() <= self.tolerance
26    }
27
28    /// Compute the absolute drift from the initial value.
29    pub fn drift(&self, value: f64) -> f64 {
30        (value - self.initial_value).abs()
31    }
32
33    /// Compute the drift rate given a previous drift and elapsed time.
34    pub fn drift_rate(&self, previous_drift: f64, dt: f64) -> f64 {
35        if dt > 0.0 {
36            (self.initial_value - previous_drift).abs() / dt
37        } else {
38            0.0
39        }
40    }
41}
42
43/// Tracks a single conserved quantity over time.
44#[derive(Debug, Clone)]
45pub struct ConservedQuantity {
46    pub name: String,
47    pub values: Vec<(f64, f64)>,
48    pub drift_rate: f64,
49}
50
51impl ConservedQuantity {
52    pub fn new(name: &str) -> Self {
53        Self {
54            name: name.to_string(),
55            values: Vec::new(),
56            drift_rate: 0.0,
57        }
58    }
59
60    /// Record a (time, value) pair.
61    pub fn record(&mut self, time: f64, value: f64) {
62        self.values.push((time, value));
63        if self.values.len() >= 2 {
64            let (t0, v0) = self.values[self.values.len() - 2];
65            let (t1, v1) = self.values[self.values.len() - 1];
66            let dt = t1 - t0;
67            if dt > 0.0 {
68                self.drift_rate = (v1 - v0) / dt;
69            }
70        }
71    }
72
73    /// Current drift rate.
74    pub fn current_drift_rate(&self) -> f64 {
75        self.drift_rate
76    }
77
78    /// Total accumulated drift from first recorded value.
79    pub fn total_drift(&self) -> f64 {
80        if self.values.len() < 2 {
81            return 0.0;
82        }
83        let first = self.values.first().unwrap().1;
84        let last = self.values.last().unwrap().1;
85        (last - first).abs()
86    }
87
88    /// Number of recorded data points.
89    pub fn len(&self) -> usize {
90        self.values.len()
91    }
92
93    /// Whether any data has been recorded.
94    pub fn is_empty(&self) -> bool {
95        self.values.is_empty()
96    }
97}