1use super::{CriterionStatus, EddDemo, FalsificationStatus, IntegratorType};
23use crate::engine::rng::SimRng;
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
28pub struct Vec2 {
29 pub x: f64,
30 pub y: f64,
31}
32
33impl Vec2 {
34 #[must_use]
35 pub const fn new(x: f64, y: f64) -> Self {
36 Self { x, y }
37 }
38
39 #[must_use]
40 pub fn magnitude(&self) -> f64 {
41 (self.x * self.x + self.y * self.y).sqrt()
42 }
43
44 #[must_use]
45 pub fn normalize(&self) -> Self {
46 let mag = self.magnitude();
47 if mag > f64::EPSILON {
48 Self::new(self.x / mag, self.y / mag)
49 } else {
50 Self::new(0.0, 0.0)
51 }
52 }
53
54 #[must_use]
55 pub fn dot(&self, other: &Self) -> f64 {
56 self.x * other.x + self.y * other.y
57 }
58
59 #[must_use]
61 pub fn cross(&self, other: &Self) -> f64 {
62 self.x * other.y - self.y * other.x
63 }
64}
65
66impl std::ops::Add for Vec2 {
67 type Output = Self;
68 fn add(self, other: Self) -> Self {
69 Self::new(self.x + other.x, self.y + other.y)
70 }
71}
72
73impl std::ops::Sub for Vec2 {
74 type Output = Self;
75 fn sub(self, other: Self) -> Self {
76 Self::new(self.x - other.x, self.y - other.y)
77 }
78}
79
80impl std::ops::Mul<f64> for Vec2 {
81 type Output = Self;
82 fn mul(self, scalar: f64) -> Self {
83 Self::new(self.x * scalar, self.y * scalar)
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct KeplerOrbitDemo {
90 pub position: Vec2,
92 pub velocity: Vec2,
94 pub mu: f64,
96 pub time: f64,
98 pub initial_energy: f64,
100 pub initial_angular_momentum: f64,
102 pub initial_semi_major_axis: f64,
104 pub max_energy_drift: f64,
106 pub max_angular_momentum_drift: f64,
108 pub step_count: u64,
110 pub integrator: IntegratorType,
112 pub energy_tolerance: f64,
114 pub angular_momentum_tolerance: f64,
116 pub period_tolerance: f64,
118 #[serde(skip)]
120 pub orbit_history: Vec<Vec2>,
121 pub max_history: usize,
123 pub perturbation_enabled: bool,
125 pub perturber_position: Vec2,
127 pub perturber_mu: f64,
129 pub seed: u64,
131 #[serde(skip)]
133 #[allow(dead_code)]
134 rng: Option<SimRng>,
135}
136
137impl Default for KeplerOrbitDemo {
138 fn default() -> Self {
139 Self::new(42)
140 }
141}
142
143impl KeplerOrbitDemo {
144 #[must_use]
146 pub fn new(seed: u64) -> Self {
147 let mu: f64 = 1.327_124_400_18e20; let r: f64 = 1.496e11; let v_circular: f64 = (mu / r).sqrt(); let position = Vec2::new(r, 0.0);
153 let velocity = Vec2::new(0.0, v_circular);
154
155 let mut demo = Self {
156 position,
157 velocity,
158 mu,
159 time: 0.0,
160 initial_energy: 0.0,
161 initial_angular_momentum: 0.0,
162 initial_semi_major_axis: 0.0,
163 max_energy_drift: 0.0,
164 max_angular_momentum_drift: 0.0,
165 step_count: 0,
166 integrator: IntegratorType::StormerVerlet,
167 energy_tolerance: 1e-10,
168 angular_momentum_tolerance: 1e-12,
169 period_tolerance: 1e-6,
170 orbit_history: Vec::new(),
171 max_history: 1000,
172 perturbation_enabled: false,
173 perturber_position: Vec2::new(5.0 * r, 0.0), perturber_mu: mu * 0.001, seed,
176 rng: Some(SimRng::new(seed)),
177 };
178
179 demo.initial_energy = demo.specific_energy();
181 demo.initial_angular_momentum = demo.angular_momentum();
182 demo.initial_semi_major_axis = demo.semi_major_axis();
183
184 demo
185 }
186
187 #[must_use]
189 pub fn with_eccentricity(seed: u64, eccentricity: f64) -> Self {
190 let mut demo = Self::new(seed);
191
192 let e = eccentricity.clamp(0.0, 0.99);
195 let a = demo.initial_semi_major_axis;
196
197 if e < 0.99 {
198 let v_perihelion = (demo.mu / a * (1.0 + e) / (1.0 - e)).sqrt();
199 demo.velocity = Vec2::new(0.0, v_perihelion);
200
201 let r_perihelion = a * (1.0 - e);
203 demo.position = Vec2::new(r_perihelion, 0.0);
204 }
205
206 demo.initial_energy = demo.specific_energy();
208 demo.initial_angular_momentum = demo.angular_momentum();
209 demo.initial_semi_major_axis = demo.semi_major_axis();
210
211 demo
212 }
213
214 #[must_use]
216 pub fn specific_energy(&self) -> f64 {
217 let v_sq = self.velocity.dot(&self.velocity);
218 let r = self.position.magnitude();
219
220 if r > f64::EPSILON {
221 0.5 * v_sq - self.mu / r
222 } else {
223 f64::NEG_INFINITY
224 }
225 }
226
227 #[must_use]
229 pub fn angular_momentum(&self) -> f64 {
230 self.position.cross(&self.velocity).abs()
231 }
232
233 #[must_use]
235 pub fn semi_major_axis(&self) -> f64 {
236 let e = self.specific_energy();
237 if e < 0.0 {
238 -self.mu / (2.0 * e)
239 } else {
240 f64::INFINITY }
242 }
243
244 #[must_use]
246 pub fn orbital_period(&self) -> f64 {
247 let a = self.semi_major_axis();
248 if a.is_finite() && a > 0.0 {
249 2.0 * std::f64::consts::PI * (a.powi(3) / self.mu).sqrt()
250 } else {
251 f64::INFINITY
252 }
253 }
254
255 #[must_use]
257 pub fn eccentricity(&self) -> f64 {
258 let e = self.specific_energy();
259 let l = self.angular_momentum();
260
261 if e < 0.0 {
262 (1.0 + 2.0 * e * l * l / (self.mu * self.mu)).sqrt()
263 } else {
264 1.0 }
266 }
267
268 #[must_use]
270 pub fn energy_drift(&self) -> f64 {
271 let current = self.specific_energy();
272 if self.initial_energy.abs() > f64::EPSILON {
273 (current - self.initial_energy).abs() / self.initial_energy.abs()
274 } else {
275 (current - self.initial_energy).abs()
276 }
277 }
278
279 #[must_use]
281 pub fn angular_momentum_drift(&self) -> f64 {
282 let current = self.angular_momentum();
283 if self.initial_angular_momentum > f64::EPSILON {
284 (current - self.initial_angular_momentum).abs() / self.initial_angular_momentum
285 } else {
286 (current - self.initial_angular_momentum).abs()
287 }
288 }
289
290 fn acceleration(&self, pos: &Vec2) -> Vec2 {
292 let r = pos.magnitude();
293 if r < f64::EPSILON {
294 return Vec2::new(0.0, 0.0);
295 }
296
297 let a_primary = *pos * (-self.mu / (r * r * r));
299
300 if self.perturbation_enabled {
302 let r_to_perturber = self.perturber_position - *pos;
303 let r_perturber = r_to_perturber.magnitude();
304 if r_perturber > f64::EPSILON {
305 let a_perturber = r_to_perturber
306 * (self.perturber_mu / (r_perturber * r_perturber * r_perturber));
307 return a_primary + a_perturber;
308 }
309 }
310
311 a_primary
312 }
313
314 fn step_stormer_verlet(&mut self, dt: f64) {
316 let a_n = self.acceleration(&self.position);
318 let v_half = self.velocity + a_n * (0.5 * dt);
319
320 self.position = self.position + v_half * dt;
322
323 let a_n1 = self.acceleration(&self.position);
325 self.velocity = v_half + a_n1 * (0.5 * dt);
326 }
327
328 fn step_rk4(&mut self, dt: f64) {
330 let pos0 = self.position;
331 let vel0 = self.velocity;
332
333 let a1 = self.acceleration(&pos0);
335 let k1_r = vel0;
336 let k1_v = a1;
337
338 let pos2 = pos0 + k1_r * (0.5 * dt);
340 let vel2 = vel0 + k1_v * (0.5 * dt);
341 let a2 = self.acceleration(&pos2);
342 let k2_r = vel2;
343 let k2_v = a2;
344
345 let pos3 = pos0 + k2_r * (0.5 * dt);
347 let vel3 = vel0 + k2_v * (0.5 * dt);
348 let a3 = self.acceleration(&pos3);
349 let k3_r = vel3;
350 let k3_v = a3;
351
352 let pos4 = pos0 + k3_r * dt;
354 let vel4 = vel0 + k3_v * dt;
355 let a4 = self.acceleration(&pos4);
356 let k4_r = vel4;
357 let k4_v = a4;
358
359 self.position = pos0 + (k1_r + k2_r * 2.0 + k3_r * 2.0 + k4_r) * (dt / 6.0);
361 self.velocity = vel0 + (k1_v + k2_v * 2.0 + k3_v * 2.0 + k4_v) * (dt / 6.0);
362 }
363
364 pub fn set_perturbation(&mut self, enabled: bool) {
366 self.perturbation_enabled = enabled;
367 }
368
369 pub fn set_integrator(&mut self, integrator: IntegratorType) {
371 self.integrator = integrator;
372 }
373
374 pub fn run_orbits(&mut self, num_orbits: f64, steps_per_orbit: usize) {
376 contract_pre_iterator!();
377 let period = self.orbital_period();
378 if !period.is_finite() {
379 return;
380 }
381
382 let dt = period / steps_per_orbit as f64;
383 let total_steps = (num_orbits * steps_per_orbit as f64) as usize;
384
385 for _ in 0..total_steps {
386 self.step(dt);
387 }
388 }
389
390 #[must_use]
392 pub fn true_anomaly(&self) -> f64 {
393 self.position.y.atan2(self.position.x)
394 }
395
396 fn record_position(&mut self) {
398 if self.orbit_history.len() >= self.max_history {
399 self.orbit_history.remove(0);
400 }
401 self.orbit_history.push(self.position);
402 }
403}
404
405impl EddDemo for KeplerOrbitDemo {
406 fn name(&self) -> &'static str {
407 "Kepler Orbit Conservation Laws"
408 }
409
410 fn emc_ref(&self) -> &'static str {
411 "physics/kepler_two_body"
412 }
413
414 fn step(&mut self, dt: f64) {
415 match self.integrator {
416 IntegratorType::StormerVerlet => self.step_stormer_verlet(dt),
417 IntegratorType::RK4 => self.step_rk4(dt),
418 IntegratorType::Euler => {
419 let a = self.acceleration(&self.position);
421 self.position = self.position + self.velocity * dt;
422 self.velocity = self.velocity + a * dt;
423 }
424 }
425
426 self.time += dt;
427 self.step_count += 1;
428
429 let e_drift = self.energy_drift();
431 let l_drift = self.angular_momentum_drift();
432
433 if e_drift > self.max_energy_drift {
434 self.max_energy_drift = e_drift;
435 }
436 if l_drift > self.max_angular_momentum_drift {
437 self.max_angular_momentum_drift = l_drift;
438 }
439
440 if self.step_count % 10 == 0 {
442 self.record_position();
443 }
444 }
445
446 fn verify_equation(&self) -> bool {
447 let energy_ok = self.energy_drift() < self.energy_tolerance;
448 let angular_ok = self.angular_momentum_drift() < self.angular_momentum_tolerance;
449
450 energy_ok && angular_ok
451 }
452
453 fn get_falsification_status(&self) -> FalsificationStatus {
454 let e_drift = self.energy_drift();
455 let l_drift = self.angular_momentum_drift();
456
457 let energy_passed = e_drift < self.energy_tolerance;
458 let angular_passed = l_drift < self.angular_momentum_tolerance;
459
460 FalsificationStatus {
461 verified: energy_passed && angular_passed,
462 criteria: vec![
463 CriterionStatus {
464 id: "KEP-ENERGY".to_string(),
465 name: "Energy conservation".to_string(),
466 passed: energy_passed,
467 value: e_drift,
468 threshold: self.energy_tolerance,
469 },
470 CriterionStatus {
471 id: "KEP-ANGULAR".to_string(),
472 name: "Angular momentum conservation".to_string(),
473 passed: angular_passed,
474 value: l_drift,
475 threshold: self.angular_momentum_tolerance,
476 },
477 ],
478 message: if energy_passed && angular_passed {
479 format!("Conservation verified: E_drift={e_drift:.2e}, L_drift={l_drift:.2e}")
480 } else {
481 format!(
482 "FALSIFIED: E_drift={e_drift:.2e} (tol={:.2e}), L_drift={l_drift:.2e} (tol={:.2e})",
483 self.energy_tolerance, self.angular_momentum_tolerance
484 )
485 },
486 }
487 }
488
489 fn reset(&mut self) {
490 *self = Self::new(self.seed);
491 }
492}
493
494#[cfg(feature = "wasm")]
499mod wasm {
500 use super::{EddDemo, IntegratorType, KeplerOrbitDemo};
501 use wasm_bindgen::prelude::*;
502
503 #[wasm_bindgen]
504 pub struct WasmKeplerOrbit {
505 inner: KeplerOrbitDemo,
506 }
507
508 #[wasm_bindgen]
509 impl WasmKeplerOrbit {
510 #[wasm_bindgen(constructor)]
511 pub fn new(seed: u64) -> Self {
512 Self {
513 inner: KeplerOrbitDemo::new(seed),
514 }
515 }
516
517 pub fn step(&mut self, dt: f64) {
518 self.inner.step(dt);
519 }
520
521 pub fn get_x(&self) -> f64 {
522 self.inner.position.x
523 }
524
525 pub fn get_y(&self) -> f64 {
526 self.inner.position.y
527 }
528
529 pub fn get_energy(&self) -> f64 {
530 self.inner.specific_energy()
531 }
532
533 pub fn get_angular_momentum(&self) -> f64 {
534 self.inner.angular_momentum()
535 }
536
537 pub fn get_energy_drift(&self) -> f64 {
538 self.inner.energy_drift()
539 }
540
541 pub fn get_time(&self) -> f64 {
542 self.inner.time
543 }
544
545 pub fn verify_equation(&self) -> bool {
546 self.inner.verify_equation()
547 }
548
549 pub fn set_integrator(&mut self, integrator: &str) {
550 self.inner.integrator = match integrator {
551 "rk4" => IntegratorType::RK4,
552 "euler" => IntegratorType::Euler,
553 _ => IntegratorType::StormerVerlet,
555 };
556 }
557
558 pub fn set_perturbation(&mut self, enabled: bool) {
559 self.inner.set_perturbation(enabled);
560 }
561
562 pub fn reset(&mut self) {
563 self.inner.reset();
564 }
565
566 pub fn get_status_json(&self) -> String {
567 serde_json::to_string(&self.inner.get_falsification_status()).unwrap_or_default()
568 }
569 }
570}
571
572#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
585 fn test_equation_specific_energy() {
586 let demo = KeplerOrbitDemo::new(42);
587
588 let v_sq = demo.velocity.dot(&demo.velocity);
590 let r = demo.position.magnitude();
591 let expected = 0.5 * v_sq - demo.mu / r;
592
593 assert!((demo.specific_energy() - expected).abs() < 1e-10);
594 }
595
596 #[test]
597 fn test_equation_angular_momentum() {
598 let demo = KeplerOrbitDemo::new(42);
599
600 let expected =
602 (demo.position.x * demo.velocity.y - demo.position.y * demo.velocity.x).abs();
603
604 assert!((demo.angular_momentum() - expected).abs() < 1e-10);
605 }
606
607 #[test]
608 fn test_equation_keplers_third_law() {
609 let demo = KeplerOrbitDemo::new(42);
610
611 let a = demo.semi_major_axis();
613 let t = demo.orbital_period();
614
615 let lhs = t * t;
616 let rhs = 4.0 * std::f64::consts::PI.powi(2) * a.powi(3) / demo.mu;
617
618 assert!(
619 (lhs - rhs).abs() / rhs < 1e-10,
620 "Kepler's third law: T²={lhs:.6e}, 4π²a³/μ={rhs:.6e}"
621 );
622 }
623
624 #[test]
629 fn test_failing_euler_energy() {
630 let mut demo = KeplerOrbitDemo::new(42);
631 demo.set_integrator(IntegratorType::Euler);
632 demo.energy_tolerance = 1e-6;
633
634 demo.run_orbits(1.0, 1000);
636
637 let drift = demo.energy_drift();
639 assert!(
640 drift > 1e-6,
641 "Euler should have energy drift >1e-6, got {drift:.2e}"
642 );
643 }
644
645 #[test]
646 fn test_failing_with_perturbation() {
647 let mut demo = KeplerOrbitDemo::new(42);
648 demo.set_perturbation(true);
649 demo.energy_tolerance = 1e-10;
650
651 demo.run_orbits(5.0, 1000);
653
654 let drift = demo.energy_drift();
658 println!("With perturbation: energy drift = {drift:.2e}");
659 }
660
661 #[test]
666 fn test_verification_energy_conservation() {
667 let mut demo = KeplerOrbitDemo::new(42);
668 demo.set_integrator(IntegratorType::StormerVerlet);
669 demo.energy_tolerance = 1e-10;
670
671 demo.run_orbits(10.0, 1000);
673
674 assert!(
675 demo.verify_equation(),
676 "Energy should be conserved, drift = {:.2e}",
677 demo.energy_drift()
678 );
679 }
680
681 #[test]
682 fn test_verification_angular_momentum_conservation() {
683 let mut demo = KeplerOrbitDemo::new(42);
684 demo.set_integrator(IntegratorType::StormerVerlet);
685
686 demo.run_orbits(10.0, 1000);
688
689 let l_drift = demo.angular_momentum_drift();
690 assert!(
691 l_drift < 1e-10,
692 "Angular momentum should be conserved, drift = {l_drift:.2e}"
693 );
694 }
695
696 #[test]
701 fn test_verification_long_term() {
702 let mut demo = KeplerOrbitDemo::new(42);
703 demo.set_integrator(IntegratorType::StormerVerlet);
704 demo.energy_tolerance = 1e-8;
705
706 demo.run_orbits(100.0, 1000);
708
709 assert!(
710 demo.verify_equation(),
711 "Long-term conservation failed: E_drift={:.2e}, L_drift={:.2e}",
712 demo.energy_drift(),
713 demo.angular_momentum_drift()
714 );
715 }
716
717 #[test]
718 fn test_verification_elliptical_orbit() {
719 let mut demo = KeplerOrbitDemo::with_eccentricity(42, 0.5);
720 demo.set_integrator(IntegratorType::StormerVerlet);
721 demo.energy_tolerance = 1e-8;
722
723 demo.run_orbits(10.0, 2000); assert!(
727 demo.verify_equation(),
728 "Elliptical orbit: E_drift={:.2e}, L_drift={:.2e}",
729 demo.energy_drift(),
730 demo.angular_momentum_drift()
731 );
732 }
733
734 #[test]
739 fn test_falsification_large_timestep() {
740 let mut demo = KeplerOrbitDemo::new(42);
741 demo.set_integrator(IntegratorType::StormerVerlet);
742
743 demo.run_orbits(1.0, 10);
745
746 let drift = demo.energy_drift();
747 println!("Large timestep (10 steps/orbit): energy drift = {drift:.2e}");
749 }
750
751 #[test]
752 fn test_falsification_status_structure() {
753 let demo = KeplerOrbitDemo::new(42);
754 let status = demo.get_falsification_status();
755
756 assert_eq!(status.criteria.len(), 2);
757 assert_eq!(status.criteria[0].id, "KEP-ENERGY");
758 assert_eq!(status.criteria[1].id, "KEP-ANGULAR");
759 }
760
761 #[test]
766 fn test_demo_trait_implementation() {
767 let mut demo = KeplerOrbitDemo::new(42);
768
769 assert_eq!(demo.name(), "Kepler Orbit Conservation Laws");
770 assert_eq!(demo.emc_ref(), "physics/kepler_two_body");
771
772 let period = demo.orbital_period();
773 let dt = period / 1000.0;
774 demo.step(dt);
775 assert!(demo.time > 0.0);
776
777 demo.reset();
778 assert_eq!(demo.time, 0.0);
779 }
780
781 #[test]
782 fn test_reproducibility() {
783 let mut demo1 = KeplerOrbitDemo::new(42);
784 let mut demo2 = KeplerOrbitDemo::new(42);
785
786 demo1.run_orbits(1.0, 100);
787 demo2.run_orbits(1.0, 100);
788
789 assert_eq!(demo1.position.x, demo2.position.x);
790 assert_eq!(demo1.position.y, demo2.position.y);
791 }
792
793 #[test]
794 fn test_orbital_elements() {
795 let demo = KeplerOrbitDemo::new(42);
796
797 let e = demo.eccentricity();
799 assert!(
800 e < 0.01,
801 "Circular orbit eccentricity should be ~0, got {e}"
802 );
803
804 let period = demo.orbital_period();
806 let year_seconds = 365.25 * 24.0 * 3600.0;
807 let period_error = (period - year_seconds).abs() / year_seconds;
808 assert!(
809 period_error < 0.01,
810 "Period should be ~1 year, error = {:.2}%",
811 period_error * 100.0
812 );
813 }
814
815 #[test]
820 fn test_default() {
821 let demo = KeplerOrbitDemo::default();
822 assert_eq!(demo.seed, 42);
823 assert_eq!(demo.step_count, 0);
824 }
825
826 #[test]
827 fn test_vec2_operations() {
828 let v1 = Vec2::new(3.0, 4.0);
829 let v2 = Vec2::new(1.0, 2.0);
830
831 assert!((v1.magnitude() - 5.0).abs() < 1e-10);
833
834 let normalized = v1.normalize();
836 assert!((normalized.magnitude() - 1.0).abs() < 1e-10);
837 assert!((normalized.x - 0.6).abs() < 1e-10);
838 assert!((normalized.y - 0.8).abs() < 1e-10);
839
840 assert!((v1.dot(&v2) - 11.0).abs() < 1e-10);
842
843 assert!((v1.cross(&v2) - 2.0).abs() < 1e-10);
845
846 let sum = v1 + v2;
848 assert!((sum.x - 4.0).abs() < 1e-10);
849 assert!((sum.y - 6.0).abs() < 1e-10);
850
851 let diff = v1 - v2;
853 assert!((diff.x - 2.0).abs() < 1e-10);
854 assert!((diff.y - 2.0).abs() < 1e-10);
855
856 let scaled = v1 * 2.0;
858 assert!((scaled.x - 6.0).abs() < 1e-10);
859 assert!((scaled.y - 8.0).abs() < 1e-10);
860 }
861
862 #[test]
863 fn test_vec2_normalize_zero() {
864 let zero = Vec2::new(0.0, 0.0);
865 let normalized = zero.normalize();
866 assert!((normalized.x).abs() < 1e-10);
867 assert!((normalized.y).abs() < 1e-10);
868 }
869
870 #[test]
871 fn test_vec2_default() {
872 let v = Vec2::default();
873 assert!((v.x).abs() < 1e-10);
874 assert!((v.y).abs() < 1e-10);
875 }
876
877 #[test]
878 fn test_set_integrator() {
879 let mut demo = KeplerOrbitDemo::new(42);
880 demo.set_integrator(IntegratorType::RK4);
881 assert!(matches!(demo.integrator, IntegratorType::RK4));
882 }
883
884 #[test]
885 fn test_set_perturbation() {
886 let mut demo = KeplerOrbitDemo::new(42);
887 assert!(!demo.perturbation_enabled);
888 demo.set_perturbation(true);
889 assert!(demo.perturbation_enabled);
890 }
891
892 #[test]
893 fn test_with_eccentricity_high() {
894 let demo = KeplerOrbitDemo::with_eccentricity(42, 0.9);
895 let e = demo.eccentricity();
896 assert!(
897 (e - 0.9).abs() < 0.1,
898 "High eccentricity orbit: expected ~0.9, got {e}"
899 );
900 }
901
902 #[test]
903 fn test_with_eccentricity_clamped() {
904 let demo = KeplerOrbitDemo::with_eccentricity(42, 1.5);
906 assert!(demo.specific_energy().is_finite());
908 }
909
910 #[test]
911 fn test_clone() {
912 let demo = KeplerOrbitDemo::new(42);
913 let cloned = demo.clone();
914 assert_eq!(demo.position.x, cloned.position.x);
915 assert_eq!(demo.velocity.y, cloned.velocity.y);
916 }
917
918 #[test]
919 fn test_debug() {
920 let demo = KeplerOrbitDemo::new(42);
921 let debug_str = format!("{demo:?}");
922 assert!(debug_str.contains("KeplerOrbitDemo"));
923 }
924
925 #[test]
926 fn test_specific_energy_near_zero() {
927 let mut demo = KeplerOrbitDemo::new(42);
928 demo.position = Vec2::new(0.0, 0.0);
930 let energy = demo.specific_energy();
931 assert!(energy.is_infinite() && energy < 0.0);
932 }
933
934 #[test]
935 fn test_semi_major_axis_parabolic() {
936 let mut demo = KeplerOrbitDemo::new(42);
937 let r = demo.position.magnitude();
939 let v_escape = (2.0 * demo.mu / r).sqrt();
940 demo.velocity = Vec2::new(0.0, v_escape * 1.1);
941 let a = demo.semi_major_axis();
942 assert!(a.is_infinite());
943 }
944
945 #[test]
946 fn test_orbital_period_infinite() {
947 let mut demo = KeplerOrbitDemo::new(42);
948 let r = demo.position.magnitude();
950 let v_escape = (2.0 * demo.mu / r).sqrt();
951 demo.velocity = Vec2::new(0.0, v_escape * 1.1);
952 let period = demo.orbital_period();
953 assert!(period.is_infinite());
954 }
955
956 #[test]
957 fn test_eccentricity_parabolic() {
958 let mut demo = KeplerOrbitDemo::new(42);
959 let r = demo.position.magnitude();
961 let v_escape = (2.0 * demo.mu / r).sqrt();
962 demo.velocity = Vec2::new(0.0, v_escape * 1.1);
963 let e = demo.eccentricity();
964 assert!((e - 1.0).abs() < 0.1 || e >= 1.0);
965 }
966
967 #[test]
968 fn test_energy_drift_zero_initial() {
969 let mut demo = KeplerOrbitDemo::new(42);
970 demo.initial_energy = 0.0;
972 let drift = demo.energy_drift();
973 assert!(drift > 0.0);
975 }
976
977 #[test]
978 fn test_angular_momentum_drift_zero_initial() {
979 let mut demo = KeplerOrbitDemo::new(42);
980 demo.initial_angular_momentum = 0.0;
981 let drift = demo.angular_momentum_drift();
982 assert!(drift > 0.0);
983 }
984
985 #[test]
986 fn test_step_rk4() {
987 let mut demo = KeplerOrbitDemo::new(42);
988 demo.set_integrator(IntegratorType::RK4);
989 let initial_pos = demo.position;
990 demo.step(1000.0);
991 assert!(
993 (demo.position.x - initial_pos.x).abs() > 1.0
994 || (demo.position.y - initial_pos.y).abs() > 1.0
995 );
996 }
997
998 #[test]
999 fn test_step_euler() {
1000 let mut demo = KeplerOrbitDemo::new(42);
1001 demo.set_integrator(IntegratorType::Euler);
1002 let initial_pos = demo.position;
1003 demo.step(1000.0);
1004 assert!(
1005 (demo.position.x - initial_pos.x).abs() > 1.0
1006 || (demo.position.y - initial_pos.y).abs() > 1.0
1007 );
1008 }
1009
1010 #[test]
1011 fn test_orbit_history() {
1012 let mut demo = KeplerOrbitDemo::new(42);
1013 demo.max_history = 10;
1014 for _ in 0..20 {
1015 demo.step(1000.0);
1016 }
1017 assert!(demo.orbit_history.len() <= demo.max_history);
1019 }
1020
1021 #[test]
1022 fn test_step_count_increment() {
1023 let mut demo = KeplerOrbitDemo::new(42);
1024 assert_eq!(demo.step_count, 0);
1025 demo.step(1000.0);
1026 assert_eq!(demo.step_count, 1);
1027 }
1028
1029 #[test]
1030 fn test_max_drift_tracking() {
1031 let mut demo = KeplerOrbitDemo::new(42);
1032 demo.set_integrator(IntegratorType::Euler);
1033 demo.run_orbits(0.1, 100);
1034 assert!(demo.max_energy_drift > 0.0 || demo.max_angular_momentum_drift >= 0.0);
1036 }
1037
1038 #[test]
1039 fn test_falsification_failed_state() {
1040 let mut demo = KeplerOrbitDemo::new(42);
1041 demo.set_integrator(IntegratorType::Euler);
1042 demo.energy_tolerance = 1e-15;
1043 demo.run_orbits(1.0, 100);
1044
1045 let status = demo.get_falsification_status();
1046 assert!(!status.verified || status.message.contains("FALSIFIED"));
1048 }
1049
1050 #[test]
1051 fn test_serialization() {
1052 let demo = KeplerOrbitDemo::new(42);
1053 let json = serde_json::to_string(&demo).expect("serialize");
1054 assert!(json.contains("mu"));
1055
1056 let restored: KeplerOrbitDemo = serde_json::from_str(&json).expect("deserialize");
1057 assert!((restored.mu - demo.mu).abs() < 1.0);
1058 }
1059
1060 #[test]
1061 fn test_run_orbits_zero() {
1062 let mut demo = KeplerOrbitDemo::new(42);
1063 demo.run_orbits(0.0, 100);
1064 assert_eq!(demo.step_count, 0);
1065 }
1066
1067 #[test]
1068 fn test_perturbation_acceleration() {
1069 let mut demo = KeplerOrbitDemo::new(42);
1070 demo.set_perturbation(true);
1071 demo.step(1000.0);
1073 assert!(demo.step_count > 0);
1074 }
1075
1076 #[test]
1077 fn test_verify_equation_all_pass() {
1078 let demo = KeplerOrbitDemo::new(42);
1079 assert!(demo.verify_equation());
1081 }
1082}