1pub mod conservation;
27pub mod drift;
28pub mod error;
29pub mod monitor;
30pub mod renormalize;
31pub mod report;
32pub mod symmetry;
33
34pub use conservation::{ConservationLaw, ConservedQuantity};
35pub use drift::DriftDetector;
36pub use error::{Error, Result};
37pub use monitor::{ConservationMonitor, MonitorSnapshot};
38pub use renormalize::RenormalizationGroup;
39pub use report::{Report, Violation};
40pub use symmetry::{Symmetry, SymmetryGroup};
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
49 fn symmetry_conserved_quantity_names() {
50 assert_eq!(Symmetry::TimeTranslation.conserved_quantity(), "Energy");
51 assert_eq!(Symmetry::SpatialTranslation.conserved_quantity(), "Momentum");
52 assert_eq!(Symmetry::Rotation.conserved_quantity(), "Angular Momentum");
53 }
54
55 #[test]
56 fn symmetry_component_counts() {
57 assert_eq!(Symmetry::TimeTranslation.component_count(), 1);
58 assert_eq!(Symmetry::SpatialTranslation.component_count(), 3);
59 assert_eq!(Symmetry::Rotation.component_count(), 3);
60 }
61
62 #[test]
63 fn gauge_symmetry() {
64 let g = SymmetryGroup {
65 name: "Charge".to_string(),
66 generators: 1,
67 };
68 let sym = Symmetry::Gauge(g);
69 assert_eq!(sym.conserved_quantity(), "Charge");
70 assert_eq!(sym.component_count(), 1);
71 }
72
73 #[test]
74 fn symmetry_display() {
75 assert!(format!("{}", Symmetry::TimeTranslation).contains("Energy"));
76 }
77
78 #[test]
81 fn conservation_law_is_conserved() {
82 let law = ConservationLaw::new(Symmetry::TimeTranslation, 100.0, 0.1);
83 assert!(law.is_conserved(100.05));
84 assert!(!law.is_conserved(100.2));
85 }
86
87 #[test]
88 fn conservation_law_drift() {
89 let law = ConservationLaw::new(Symmetry::TimeTranslation, 10.0, 1.0);
90 assert!((law.drift(12.0) - 2.0).abs() < 1e-10);
91 }
92
93 #[test]
94 fn conservation_law_drift_rate() {
95 let law = ConservationLaw::new(Symmetry::TimeTranslation, 10.0, 1.0);
96 let rate = law.drift_rate(10.0, 1.0);
97 assert!((rate - 0.0).abs() < 1e-10);
98 }
99
100 #[test]
103 fn conserved_quantity_record_and_drift() {
104 let mut q = ConservedQuantity::new("Energy");
105 q.record(0.0, 100.0);
106 q.record(1.0, 101.0);
107 q.record(2.0, 103.0);
108 assert!((q.total_drift() - 3.0).abs() < 1e-10);
109 assert!((q.current_drift_rate() - 2.0).abs() < 1e-10);
110 assert_eq!(q.len(), 3);
111 }
112
113 #[test]
114 fn conserved_quantity_empty() {
115 let q = ConservedQuantity::new("Energy");
116 assert!(q.is_empty());
117 assert_eq!(q.total_drift(), 0.0);
118 }
119
120 #[test]
123 fn monitor_newtonian_expected_values() {
124 let m = ConservationMonitor::newtonian(0.01);
125 assert_eq!(m.expected_value_count(), 7);
127 }
128
129 #[test]
130 fn monitor_hamiltonian_expected_values() {
131 let m = ConservationMonitor::hamiltonian(0.01);
132 assert_eq!(m.expected_value_count(), 1);
133 }
134
135 #[test]
136 fn monitor_tick_clean() {
137 let mut m = ConservationMonitor::hamiltonian(0.1);
138 m.laws[0].initial_value = 100.0;
139 m.tick(0.0, &[100.0]).unwrap();
140 m.tick(1.0, &[100.05]).unwrap();
141 assert_eq!(m.history.len(), 2);
142 assert!(m.history.iter().all(|s| s.violations.is_empty()));
143 assert!((m.health() - 1.0).abs() < 1e-10);
144 }
145
146 #[test]
147 fn monitor_tick_violation() {
148 let mut m = ConservationMonitor::hamiltonian(0.1);
149 m.laws[0].initial_value = 100.0;
150 m.tick(0.0, &[100.0]).unwrap();
151 m.tick(1.0, &[120.0]).unwrap();
152 assert_eq!(m.history[1].violations, vec![0]);
153 assert!((m.health() - 0.5).abs() < 1e-10);
154 }
155
156 #[test]
157 fn monitor_value_mismatch() {
158 let mut m = ConservationMonitor::hamiltonian(0.1);
159 let result = m.tick(0.0, &[1.0, 2.0]);
160 assert!(result.is_err());
161 }
162
163 #[test]
164 fn monitor_custom() {
165 let laws = vec![ConservationLaw::new(Symmetry::TimeTranslation, 50.0, 0.5)];
166 let m = ConservationMonitor::custom(laws);
167 assert_eq!(m.expected_value_count(), 1);
168 assert_eq!(m.laws.len(), 1);
169 }
170
171 #[test]
172 fn monitor_add_law() {
173 let mut m = ConservationMonitor::new();
174 m.add_law(ConservationLaw::new(Symmetry::TimeTranslation, 0.0, 0.01));
175 m.add_law(ConservationLaw::new(Symmetry::SpatialTranslation, 0.0, 0.01));
176 assert_eq!(m.expected_value_count(), 4);
177 }
178
179 #[test]
180 fn monitor_default() {
181 let m = ConservationMonitor::default();
182 assert_eq!(m.laws.len(), 0);
183 }
184
185 #[test]
188 fn drift_detector_no_drift() {
189 let mut d = DriftDetector::new(0, 1.0);
190 d.record_drift(0.0, 0.1);
191 d.record_drift(1.0, 0.2);
192 assert!(!d.is_drifting());
193 assert_eq!(d.sample_count(), 2);
194 }
195
196 #[test]
197 fn drift_detector_with_drift() {
198 let mut d = DriftDetector::new(0, 0.5);
199 d.record_drift(0.0, 0.1);
200 d.record_drift(1.0, 0.6);
201 assert!(d.is_drifting());
202 assert!((d.max_drift() - 0.6).abs() < 1e-10);
203 }
204
205 #[test]
206 fn drift_detector_first_violation() {
207 let mut d = DriftDetector::new(0, 0.5);
208 d.record_drift(0.0, 0.1);
209 d.record_drift(1.0, 0.6);
210 assert!((d.first_violation_time().unwrap() - 1.0).abs() < 1e-10);
211 }
212
213 #[test]
216 fn rg_coarse_grain() {
217 let data: Vec<(f64, f64)> = (0..10)
218 .map(|i| (i as f64, (i % 2) as f64))
219 .collect();
220 let rg = RenormalizationGroup::new(&data);
221 let coarse = rg.coarse_grain(2);
222 assert_eq!(coarse.len(), 5);
223 assert!((coarse[0].1 - 0.5).abs() < 1e-10);
225 }
226
227 #[test]
228 fn rg_coarse_grain_empty() {
229 let rg = RenormalizationGroup::new(&[]);
230 let coarse = rg.coarse_grain(2);
231 assert!(coarse.is_empty());
232 }
233
234 #[test]
235 fn rg_find_breaking_scale() {
236 let data: Vec<(f64, f64)> = (0..20).map(|i| (i as f64, i as f64 * 0.1)).collect();
238 let rg = RenormalizationGroup::new(&data);
239 let scale = rg.find_breaking_scale();
240 assert!(scale.is_some());
241 }
242
243 #[test]
244 fn rg_no_breaking_constant() {
245 let data: Vec<(f64, f64)> = (0..20).map(|i| (i as f64, 0.0)).collect();
246 let rg = RenormalizationGroup::new(&data);
247 let scale = rg.find_breaking_scale();
248 assert!(scale.is_none());
250 }
251
252 #[test]
253 fn rg_beta_function() {
254 let data: Vec<(f64, f64)> = (0..10).map(|i| (i as f64, i as f64)).collect();
255 let rg = RenormalizationGroup::new(&data);
256 let beta = rg.beta_function(&[1, 2, 4]);
257 assert_eq!(beta.len(), 3);
258 }
259
260 #[test]
263 fn report_text_clean() {
264 let report = Report {
265 total_ticks: 100,
266 health: 1.0,
267 total_violations: 0,
268 violations: vec![],
269 };
270 let text = report.to_text();
271 assert!(text.contains("satisfied"));
272 }
273
274 #[test]
275 fn report_text_with_violations() {
276 let report = Report {
277 total_ticks: 100,
278 health: 0.9,
279 total_violations: 10,
280 violations: vec![Violation {
281 law_name: "Energy".to_string(),
282 law_index: 0,
283 first_violation: Some(5.0),
284 last_violation: Some(50.0),
285 violation_count: 10,
286 breaking_scale: Some(0.25),
287 }],
288 };
289 let text = report.to_text();
290 assert!(text.contains("Energy"));
291 assert!(text.contains("90.0%"));
292 }
293
294 #[test]
295 fn report_json_roundtrip() {
296 let report = Report {
297 total_ticks: 50,
298 health: 1.0,
299 total_violations: 0,
300 violations: vec![],
301 };
302 let json = report.to_json();
303 let parsed: Report = serde_json::from_str(&json).unwrap();
304 assert_eq!(parsed.total_ticks, 50);
305 }
306
307 #[test]
310 fn double_pendulum_energy_drift() {
311 let mut monitor = ConservationMonitor::hamiltonian(0.001);
313 let dt = 0.01;
314 let mut x = 1.0_f64;
315 let mut v = 0.0_f64;
316 let omega = 2.0_f64;
317
318 let e0 = 0.5 * v * v + 0.5 * omega * omega * x * x;
320 monitor.laws[0].initial_value = e0;
321
322 for i in 0..500 {
323 let t = i as f64 * dt;
324 v += -omega * omega * x * dt;
326 x += v * dt;
327 let energy = 0.5 * v * v + 0.5 * omega * omega * x * x;
328 monitor.tick(t, &[energy]).unwrap();
329 }
330
331 let report = monitor.report();
332 assert!(report.total_violations > 0, "Euler method should violate energy conservation");
334 }
335
336 #[test]
337 fn newtonian_system_conserved() {
338 let mut monitor = ConservationMonitor::newtonian(0.1);
339 monitor.laws[0].initial_value = 10.0; monitor.laws[1].initial_value = 1.0; monitor.laws[2].initial_value = 0.5; for i in 0..100 {
345 let t = i as f64 * 0.01;
346 monitor.tick(t, &[10.0, 1.0, 0.0, 0.0, 0.5, 0.0, 0.0]).unwrap();
347 }
348 assert!((monitor.health() - 1.0).abs() < 1e-10);
349 assert_eq!(monitor.total_violations(), 0);
350 }
351}