Skip to main content

noether_guard/
monitor.rs

1use crate::conservation::ConservationLaw;
2use crate::drift::DriftDetector;
3use crate::error::Result;
4use crate::report::Report;
5use crate::symmetry::Symmetry;
6
7/// Snapshot of all conserved quantities at a point in time.
8#[derive(Debug, Clone)]
9pub struct MonitorSnapshot {
10    pub time: f64,
11    pub values: Vec<f64>,
12    pub violations: Vec<usize>,
13}
14
15/// Monitors multiple conservation laws simultaneously.
16#[derive(Debug, Clone)]
17pub struct ConservationMonitor {
18    pub laws: Vec<ConservationLaw>,
19    pub history: Vec<MonitorSnapshot>,
20    pub drift_detectors: Vec<DriftDetector>,
21}
22
23impl ConservationMonitor {
24    /// Create an empty monitor.
25    pub fn new() -> Self {
26        Self {
27            laws: Vec::new(),
28            history: Vec::new(),
29            drift_detectors: Vec::new(),
30        }
31    }
32
33    /// Pre-built Newtonian monitor: energy + momentum (3) + angular momentum (3) = 7 values.
34    pub fn newtonian(tolerance: f64) -> Self {
35        let mut m = Self::new();
36        m.laws.push(ConservationLaw::new(
37            Symmetry::TimeTranslation,
38            0.0,
39            tolerance,
40        ));
41        m.laws.push(ConservationLaw::new(
42            Symmetry::SpatialTranslation,
43            0.0,
44            tolerance,
45        ));
46        m.laws.push(ConservationLaw::new(Symmetry::Rotation, 0.0, tolerance));
47        m.drift_detectors = m
48            .laws
49            .iter()
50            .enumerate()
51            .map(|(i, law)| DriftDetector::new(i, law.tolerance * 10.0))
52            .collect();
53        m
54    }
55
56    /// Pre-built Hamiltonian monitor: energy only.
57    pub fn hamiltonian(tolerance: f64) -> Self {
58        let mut m = Self::new();
59        m.laws.push(ConservationLaw::new(
60            Symmetry::TimeTranslation,
61            0.0,
62            tolerance,
63        ));
64        m.drift_detectors = m
65            .laws
66            .iter()
67            .enumerate()
68            .map(|(i, law)| DriftDetector::new(i, law.tolerance * 10.0))
69            .collect();
70        m
71    }
72
73    /// Custom monitor with user-supplied laws.
74    pub fn custom(laws: Vec<ConservationLaw>) -> Self {
75        let drift_detectors = laws
76            .iter()
77            .enumerate()
78            .map(|(i, law)| DriftDetector::new(i, law.tolerance * 10.0))
79            .collect();
80        Self {
81            laws,
82            history: Vec::new(),
83            drift_detectors,
84        }
85    }
86
87    /// Number of scalar values expected per tick.
88    pub fn expected_value_count(&self) -> usize {
89        self.laws.iter().map(|l| l.symmetry.component_count()).sum()
90    }
91
92    /// Add a conservation law.
93    pub fn add_law(&mut self, law: ConservationLaw) {
94        let idx = self.laws.len();
95        self.laws.push(law);
96        self.drift_detectors
97            .push(DriftDetector::new(idx, self.laws.last().unwrap().tolerance * 10.0));
98    }
99
100    /// Record a snapshot: check all conservation laws at time `t`.
101    ///
102    /// `values` layout: for each law in order, emit `component_count()` values.
103    /// For Newtonian that is [energy, px, py, pz, lx, ly, lz].
104    pub fn tick(&mut self, time: f64, values: &[f64]) -> Result<()> {
105        let expected = self.expected_value_count();
106        if values.len() != expected {
107            return Err(crate::error::Error::ValueCountMismatch {
108                expected,
109                got: values.len(),
110            });
111        }
112
113        let mut violations = Vec::new();
114        let mut offset = 0;
115        for (i, law) in self.laws.iter().enumerate() {
116            let count = law.symmetry.component_count();
117            // Check the first component (scalar summary) against tolerance
118            if !law.is_conserved(values[offset]) {
119                violations.push(i);
120            }
121            // Update drift detector
122            if let Some(detector) = self.drift_detectors.get_mut(i) {
123                detector.record_drift(time, law.drift(values[offset]));
124            }
125            offset += count;
126        }
127
128        self.history.push(MonitorSnapshot {
129            time,
130            values: values.to_vec(),
131            violations: violations.clone(),
132        });
133
134        Ok(())
135    }
136
137    /// Compute overall health: fraction of snapshots with no violations.
138    pub fn health(&self) -> f64 {
139        if self.history.is_empty() {
140            return 1.0;
141        }
142        let clean = self.history.iter().filter(|s| s.violations.is_empty()).count();
143        clean as f64 / self.history.len() as f64
144    }
145
146    /// Total number of violations across all snapshots.
147    pub fn total_violations(&self) -> usize {
148        self.history.iter().map(|s| s.violations.len()).sum()
149    }
150
151    /// Generate a report of the monitoring session.
152    pub fn report(&self) -> Report {
153        let violated_law_indices: Vec<usize> = self
154            .history
155            .iter()
156            .flat_map(|s| s.violations.iter().copied())
157            .collect::<std::collections::HashSet<_>>()
158            .into_iter()
159            .collect();
160
161        let mut violations = Vec::new();
162        for &idx in &violated_law_indices {
163            if let Some(law) = self.laws.get(idx) {
164                let times: Vec<f64> = self
165                    .history
166                    .iter()
167                    .filter(|s| s.violations.contains(&idx))
168                    .map(|s| s.time)
169                    .collect();
170                let breaking_scale = self
171                    .drift_detectors
172                    .get(idx)
173                    .and_then(|d| d.breaking_scale);
174                violations.push(crate::report::Violation {
175                    law_name: law.name.clone(),
176                    law_index: idx,
177                    first_violation: times.first().copied(),
178                    last_violation: times.last().copied(),
179                    violation_count: times.len(),
180                    breaking_scale,
181                });
182            }
183        }
184
185        Report {
186            total_ticks: self.history.len(),
187            health: self.health(),
188            total_violations: self.total_violations(),
189            violations,
190        }
191    }
192}
193
194impl Default for ConservationMonitor {
195    fn default() -> Self {
196        Self::new()
197    }
198}