1use super::{CriterionStatus, EddDemo, FalsificationStatus, IntegratorType};
22use crate::engine::rng::SimRng;
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct HarmonicOscillatorDemo {
28 pub x: f64,
30 pub v: f64,
32 pub omega: f64,
34 pub mass: f64,
36 pub time: f64,
38 pub initial_energy: f64,
40 pub max_energy_drift: f64,
42 pub step_count: u64,
44 pub integrator: IntegratorType,
46 pub energy_tolerance: f64,
48 #[serde(skip)]
50 #[allow(dead_code)]
51 rng: Option<SimRng>,
52 pub seed: u64,
54}
55
56impl Default for HarmonicOscillatorDemo {
57 fn default() -> Self {
58 Self::new(42)
59 }
60}
61
62impl HarmonicOscillatorDemo {
63 #[must_use]
65 pub fn new(seed: u64) -> Self {
66 let x = 1.0; let v = 0.0; let omega = 2.0 * std::f64::consts::PI; let mass = 1.0;
70
71 let initial_energy = Self::compute_energy_static(x, v, omega, mass);
72
73 Self {
74 x,
75 v,
76 omega,
77 mass,
78 time: 0.0,
79 initial_energy,
80 max_energy_drift: 0.0,
81 step_count: 0,
82 integrator: IntegratorType::StormerVerlet,
83 energy_tolerance: 1e-10,
84 rng: Some(SimRng::new(seed)),
85 seed,
86 }
87 }
88
89 pub fn set_integrator(&mut self, integrator: IntegratorType) {
91 self.integrator = integrator;
92 }
93
94 pub fn set_initial_conditions(&mut self, x: f64, v: f64) {
96 self.x = x;
97 self.v = v;
98 self.initial_energy = self.compute_energy();
99 self.max_energy_drift = 0.0;
100 self.time = 0.0;
101 self.step_count = 0;
102 }
103
104 #[must_use]
106 pub fn compute_energy(&self) -> f64 {
107 Self::compute_energy_static(self.x, self.v, self.omega, self.mass)
108 }
109
110 fn compute_energy_static(x: f64, v: f64, omega: f64, mass: f64) -> f64 {
111 0.5 * mass * (v * v + omega * omega * x * x)
112 }
113
114 #[must_use]
116 pub fn energy_drift(&self) -> f64 {
117 let current_energy = self.compute_energy();
118 (current_energy - self.initial_energy).abs() / self.initial_energy
119 }
120
121 #[must_use]
123 pub fn analytical_position(&self) -> f64 {
124 let amplitude = (self.x * self.x + (self.v / self.omega).powi(2)).sqrt();
127 let phase = (-self.v / self.omega).atan2(self.x);
128 amplitude * (self.omega * self.time + phase).cos()
129 }
130
131 #[must_use]
133 pub fn analytical_velocity(&self) -> f64 {
134 let amplitude = (self.x * self.x + (self.v / self.omega).powi(2)).sqrt();
135 let phase = (-self.v / self.omega).atan2(self.x);
136 -amplitude * self.omega * (self.omega * self.time + phase).sin()
137 }
138
139 fn step_stormer_verlet(&mut self, dt: f64) {
141 let omega_sq = self.omega * self.omega;
147
148 let a_n = -omega_sq * self.x;
150 let v_half = self.v + 0.5 * dt * a_n;
151
152 self.x += dt * v_half;
154
155 let a_n1 = -omega_sq * self.x;
157 self.v = v_half + 0.5 * dt * a_n1;
158 }
159
160 fn step_rk4(&mut self, dt: f64) {
162 let omega_sq = self.omega * self.omega;
164
165 let x0 = self.x;
166 let v0 = self.v;
167
168 let k1_x = v0;
170 let k1_v = -omega_sq * x0;
171
172 let k2_x = v0 + 0.5 * dt * k1_v;
174 let k2_v = -omega_sq * (x0 + 0.5 * dt * k1_x);
175
176 let k3_x = v0 + 0.5 * dt * k2_v;
178 let k3_v = -omega_sq * (x0 + 0.5 * dt * k2_x);
179
180 let k4_x = v0 + dt * k3_v;
182 let k4_v = -omega_sq * (x0 + dt * k3_x);
183
184 self.x = x0 + (dt / 6.0) * (k1_x + 2.0 * k2_x + 2.0 * k3_x + k4_x);
186 self.v = v0 + (dt / 6.0) * (k1_v + 2.0 * k2_v + 2.0 * k3_v + k4_v);
187 }
188
189 fn step_euler(&mut self, dt: f64) {
191 let omega_sq = self.omega * self.omega;
193
194 let new_x = self.x + dt * self.v;
195 let new_v = self.v - dt * omega_sq * self.x;
196
197 self.x = new_x;
198 self.v = new_v;
199 }
200
201 pub fn run_periods(&mut self, num_periods: usize, steps_per_period: usize) {
203 contract_pre_iterator!();
204 let period = 2.0 * std::f64::consts::PI / self.omega;
205 let dt = period / steps_per_period as f64;
206
207 for _ in 0..(num_periods * steps_per_period) {
208 self.step(dt);
209 }
210 }
211
212 #[must_use]
214 pub fn phase_space(&self) -> (f64, f64) {
215 (self.x, self.v)
216 }
217
218 #[must_use]
220 pub fn period_count(&self) -> f64 {
221 self.time * self.omega / (2.0 * std::f64::consts::PI)
222 }
223}
224
225impl EddDemo for HarmonicOscillatorDemo {
226 fn name(&self) -> &'static str {
227 "Harmonic Oscillator Energy Conservation"
228 }
229
230 fn emc_ref(&self) -> &'static str {
231 "physics/harmonic_oscillator"
232 }
233
234 fn step(&mut self, dt: f64) {
235 match self.integrator {
236 IntegratorType::StormerVerlet => self.step_stormer_verlet(dt),
237 IntegratorType::RK4 => self.step_rk4(dt),
238 IntegratorType::Euler => self.step_euler(dt),
239 }
240
241 self.time += dt;
242 self.step_count += 1;
243
244 let drift = self.energy_drift();
246 if drift > self.max_energy_drift {
247 self.max_energy_drift = drift;
248 }
249 }
250
251 fn verify_equation(&self) -> bool {
252 self.energy_drift() < self.energy_tolerance
253 }
254
255 fn get_falsification_status(&self) -> FalsificationStatus {
256 let drift = self.energy_drift();
257 let passed = drift < self.energy_tolerance;
258
259 FalsificationStatus {
260 verified: passed,
261 criteria: vec![CriterionStatus {
262 id: "HO-ENERGY".to_string(),
263 name: "Energy conservation".to_string(),
264 passed,
265 value: drift,
266 threshold: self.energy_tolerance,
267 }],
268 message: if passed {
269 format!(
270 "Energy conserved: drift = {:.2e} < {:.2e}",
271 drift, self.energy_tolerance
272 )
273 } else {
274 format!(
275 "FALSIFIED: Energy drift = {:.2e} > {:.2e}",
276 drift, self.energy_tolerance
277 )
278 },
279 }
280 }
281
282 fn reset(&mut self) {
283 *self = Self::new(self.seed);
284 }
285}
286
287#[cfg(feature = "wasm")]
292mod wasm {
293 use super::{EddDemo, HarmonicOscillatorDemo, IntegratorType};
294 use wasm_bindgen::prelude::*;
295
296 #[wasm_bindgen]
297 pub struct WasmHarmonicOscillator {
298 inner: HarmonicOscillatorDemo,
299 }
300
301 #[wasm_bindgen]
302 impl WasmHarmonicOscillator {
303 #[wasm_bindgen(constructor)]
304 pub fn new(seed: u64) -> Self {
305 Self {
306 inner: HarmonicOscillatorDemo::new(seed),
307 }
308 }
309
310 pub fn step(&mut self, dt: f64) {
311 self.inner.step(dt);
312 }
313
314 pub fn get_x(&self) -> f64 {
315 self.inner.x
316 }
317
318 pub fn get_v(&self) -> f64 {
319 self.inner.v
320 }
321
322 pub fn get_energy(&self) -> f64 {
323 self.inner.compute_energy()
324 }
325
326 pub fn get_energy_drift(&self) -> f64 {
327 self.inner.energy_drift()
328 }
329
330 pub fn get_time(&self) -> f64 {
331 self.inner.time
332 }
333
334 pub fn verify_equation(&self) -> bool {
335 self.inner.verify_equation()
336 }
337
338 pub fn set_integrator(&mut self, integrator: &str) {
339 self.inner.integrator = match integrator {
340 "rk4" => IntegratorType::RK4,
341 "euler" => IntegratorType::Euler,
342 _ => IntegratorType::StormerVerlet,
344 };
345 }
346
347 pub fn reset(&mut self) {
348 self.inner.reset();
349 }
350
351 pub fn get_status_json(&self) -> String {
352 serde_json::to_string(&self.inner.get_falsification_status()).unwrap_or_default()
353 }
354 }
355}
356
357#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
370 fn test_equation_energy_formula() {
371 let demo = HarmonicOscillatorDemo::new(42);
373
374 let x = demo.x;
375 let v = demo.v;
376 let omega = demo.omega;
377 let mass = demo.mass;
378
379 let energy = 0.5 * mass * (v * v + omega * omega * x * x);
380 assert!((energy - demo.compute_energy()).abs() < 1e-15);
381 }
382
383 #[test]
384 fn test_equation_analytical_solution() {
385 let demo = HarmonicOscillatorDemo::new(42);
387 assert!((demo.analytical_position() - demo.x).abs() < 1e-10);
388 }
389
390 #[test]
395 fn test_failing_euler_energy_conservation() {
396 let mut demo = HarmonicOscillatorDemo::new(42);
398 demo.set_integrator(IntegratorType::Euler);
399 demo.energy_tolerance = 1e-6; demo.run_periods(10, 100);
403
404 assert!(
406 !demo.verify_equation(),
407 "Euler method should NOT conserve energy"
408 );
409 }
410
411 #[test]
412 fn test_failing_rk4_long_term() {
413 let mut demo = HarmonicOscillatorDemo::new(42);
415 demo.set_integrator(IntegratorType::RK4);
416 demo.energy_tolerance = 1e-10; demo.run_periods(1000, 100);
420
421 let drift = demo.energy_drift();
424 assert!(drift > 0.0, "RK4 should have some drift: {drift}");
426 }
427
428 #[test]
433 fn test_verlet_energy_conservation_short() {
434 let mut demo = HarmonicOscillatorDemo::new(42);
435 demo.set_integrator(IntegratorType::StormerVerlet);
436 demo.energy_tolerance = 1e-10;
437
438 demo.run_periods(100, 1000);
440
441 assert!(
442 demo.verify_equation(),
443 "Störmer-Verlet should conserve energy, drift = {}",
444 demo.energy_drift()
445 );
446 }
447
448 #[test]
449 fn test_verlet_energy_conservation_long() {
450 let mut demo = HarmonicOscillatorDemo::new(42);
451 demo.set_integrator(IntegratorType::StormerVerlet);
452 demo.energy_tolerance = 1e-8; demo.run_periods(1000, 1000);
456
457 assert!(
458 demo.verify_equation(),
459 "Störmer-Verlet should conserve energy over 1000 periods, drift = {}",
460 demo.energy_drift()
461 );
462 }
463
464 #[test]
469 fn test_verification_phase_space_bounded() {
470 let mut demo = HarmonicOscillatorDemo::new(42);
471 demo.run_periods(100, 1000);
472
473 let (x, v) = demo.phase_space();
474 let amplitude = (x * x + (v / demo.omega).powi(2)).sqrt();
475
476 assert!(
478 (amplitude - 1.0).abs() < 0.01,
479 "Amplitude should be conserved: {amplitude}"
480 );
481 }
482
483 #[test]
484 fn test_verification_period_accurate() {
485 let mut demo = HarmonicOscillatorDemo::new(42);
486
487 demo.run_periods(10, 10000);
489
490 assert!(
492 (demo.x - 1.0).abs() < 0.001,
493 "Position after 10 periods: {} (expected ~1.0)",
494 demo.x
495 );
496 }
497
498 #[test]
503 fn test_falsification_large_timestep() {
504 let mut demo = HarmonicOscillatorDemo::new(42);
505 demo.set_integrator(IntegratorType::StormerVerlet);
506 demo.energy_tolerance = 1e-6;
507
508 let period = 2.0 * std::f64::consts::PI / demo.omega;
510 let dt = period / 5.0; for _ in 0..50 {
513 demo.step(dt);
514 }
515
516 let drift = demo.energy_drift();
518 println!(
520 "Falsification test: dt={dt:.4}, drift={drift:.2e}, periods={}",
521 demo.period_count()
522 );
523 }
524
525 #[test]
526 fn test_falsification_status_structure() {
527 let demo = HarmonicOscillatorDemo::new(42);
528 let status = demo.get_falsification_status();
529
530 assert!(status.verified);
531 assert_eq!(status.criteria.len(), 1);
532 assert_eq!(status.criteria[0].id, "HO-ENERGY");
533 }
534
535 #[test]
540 fn test_demo_trait_implementation() {
541 let mut demo = HarmonicOscillatorDemo::new(42);
542
543 assert_eq!(demo.name(), "Harmonic Oscillator Energy Conservation");
544 assert_eq!(demo.emc_ref(), "physics/harmonic_oscillator");
545
546 demo.step(0.001);
547 assert!(demo.time > 0.0);
548
549 demo.reset();
550 assert_eq!(demo.time, 0.0);
551 }
552
553 #[test]
554 fn test_serialization() {
555 let demo = HarmonicOscillatorDemo::new(42);
556 let json = serde_json::to_string(&demo).expect("serialize");
557 assert!(json.contains("omega"));
558
559 let restored: HarmonicOscillatorDemo = serde_json::from_str(&json).expect("deserialize");
560 assert_eq!(restored.omega, demo.omega);
561 }
562
563 #[test]
564 fn test_reproducibility() {
565 let mut demo1 = HarmonicOscillatorDemo::new(42);
566 let mut demo2 = HarmonicOscillatorDemo::new(42);
567
568 demo1.run_periods(10, 100);
569 demo2.run_periods(10, 100);
570
571 assert_eq!(demo1.x, demo2.x);
572 assert_eq!(demo1.v, demo2.v);
573 }
574
575 #[test]
580 fn test_default() {
581 let demo = HarmonicOscillatorDemo::default();
582 assert_eq!(demo.seed, 42);
583 assert_eq!(demo.step_count, 0);
584 }
585
586 #[test]
587 fn test_set_initial_conditions() {
588 let mut demo = HarmonicOscillatorDemo::new(42);
589 demo.set_initial_conditions(2.0, 1.0);
590
591 assert!((demo.x - 2.0).abs() < 1e-10);
592 assert!((demo.v - 1.0).abs() < 1e-10);
593 assert_eq!(demo.time, 0.0);
594 assert_eq!(demo.step_count, 0);
595 assert!((demo.max_energy_drift - 0.0).abs() < 1e-10);
596 }
597
598 #[test]
599 fn test_analytical_velocity() {
600 let demo = HarmonicOscillatorDemo::new(42);
601 let analytical_v = demo.analytical_velocity();
603 assert!(
604 analytical_v.abs() < 1e-10,
605 "Analytical velocity at t=0 should be ~0: {analytical_v}"
606 );
607 }
608
609 #[test]
610 fn test_phase_space() {
611 let demo = HarmonicOscillatorDemo::new(42);
612 let (x, v) = demo.phase_space();
613 assert!((x - 1.0).abs() < 1e-10);
614 assert!(v.abs() < 1e-10);
615 }
616
617 #[test]
618 fn test_period_count() {
619 let mut demo = HarmonicOscillatorDemo::new(42);
620 assert!((demo.period_count() - 0.0).abs() < 1e-10);
621
622 demo.run_periods(5, 100);
623 assert!(
624 (demo.period_count() - 5.0).abs() < 0.01,
625 "Period count after 5 periods: {}",
626 demo.period_count()
627 );
628 }
629
630 #[test]
631 fn test_energy_drift_calculation() {
632 let mut demo = HarmonicOscillatorDemo::new(42);
633 let initial_drift = demo.energy_drift();
634 assert!(initial_drift < 1e-15, "Initial drift should be ~0");
635
636 demo.set_integrator(IntegratorType::Euler);
638 demo.run_periods(1, 100);
639 let after_drift = demo.energy_drift();
640 assert!(
641 after_drift > 0.0,
642 "After Euler integration, drift should be > 0"
643 );
644 }
645
646 #[test]
647 fn test_max_energy_drift_tracking() {
648 let mut demo = HarmonicOscillatorDemo::new(42);
649 demo.set_integrator(IntegratorType::Euler);
650 demo.run_periods(10, 100);
651
652 assert!(
653 demo.max_energy_drift > 0.0,
654 "Max energy drift should be tracked"
655 );
656 }
657
658 #[test]
659 fn test_step_count_tracking() {
660 let mut demo = HarmonicOscillatorDemo::new(42);
661 assert_eq!(demo.step_count, 0);
662
663 demo.step(0.01);
664 assert_eq!(demo.step_count, 1);
665
666 demo.run_periods(1, 100);
667 assert_eq!(demo.step_count, 101);
668 }
669
670 #[test]
671 fn test_falsification_status_failed() {
672 let mut demo = HarmonicOscillatorDemo::new(42);
673 demo.set_integrator(IntegratorType::Euler);
674 demo.energy_tolerance = 1e-15; demo.run_periods(10, 100);
676
677 let status = demo.get_falsification_status();
678 assert!(!status.verified);
679 assert!(status.message.contains("FALSIFIED"));
680 }
681
682 #[test]
683 fn test_compute_energy_static() {
684 let demo = HarmonicOscillatorDemo::new(42);
686 let energy = demo.compute_energy();
687 let expected = 0.5 * demo.mass * demo.omega * demo.omega;
689 assert!(
690 (energy - expected).abs() < 1e-10,
691 "Energy should match expected: {} vs {}",
692 energy,
693 expected
694 );
695 }
696
697 #[test]
698 fn test_clone() {
699 let demo = HarmonicOscillatorDemo::new(42);
700 let cloned = demo.clone();
701 assert_eq!(demo.x, cloned.x);
702 assert_eq!(demo.v, cloned.v);
703 assert_eq!(demo.omega, cloned.omega);
704 }
705
706 #[test]
707 fn test_debug() {
708 let demo = HarmonicOscillatorDemo::new(42);
709 let debug_str = format!("{demo:?}");
710 assert!(debug_str.contains("HarmonicOscillatorDemo"));
711 }
712
713 #[test]
714 fn test_all_integrator_types() {
715 for integrator in [
717 IntegratorType::StormerVerlet,
718 IntegratorType::RK4,
719 IntegratorType::Euler,
720 ] {
721 let mut demo = HarmonicOscillatorDemo::new(42);
722 demo.set_integrator(integrator);
723 demo.step(0.001);
724 assert!(demo.time > 0.0);
725 }
726 }
727
728 #[test]
729 fn test_run_periods_boundary() {
730 let mut demo = HarmonicOscillatorDemo::new(42);
731 demo.run_periods(0, 100);
733 assert_eq!(demo.step_count, 0);
734
735 demo.run_periods(1, 1);
738 assert_eq!(demo.step_count, 1);
739 }
740
741 #[test]
742 fn test_analytical_solution_after_quarter_period() {
743 let mut demo = HarmonicOscillatorDemo::new(42);
744 let quarter_period = std::f64::consts::PI / (2.0 * demo.omega);
746 let dt = quarter_period / 1000.0;
747
748 for _ in 0..1000 {
749 demo.step(dt);
750 }
751
752 assert!(
756 demo.x.abs() < 0.01,
757 "x after quarter period: {} (expected ~0)",
758 demo.x
759 );
760 }
761}