Skip to main content

simular/demos/
kepler_orbit.rs

1//! Demo 5: Kepler Orbit Conservation Laws
2//!
3//! Two-body orbital mechanics verifying all three Kepler laws plus conservation.
4//!
5//! # Governing Equations
6//!
7//! ```text
8//! Newton's Gravitation: F = -GMm/r² r̂
9//! Specific Energy:      E = v²/2 - μ/r = -μ/(2a)    (constant)
10//! Angular Momentum:     L = r × v                    (constant vector)
11//! Kepler's Third Law:   T² = (4π²/μ) a³
12//! ```
13//!
14//! # EDD Cycle
15//!
16//! 1. **Equations**: E constant, L constant, T² ∝ a³
17//! 2. **Failing Tests**: E drifts >1e-10, L drifts >1e-12, T error >1e-6
18//! 3. **Implementation**: Störmer-Verlet symplectic integrator
19//! 4. **Verification**: All three conservation laws hold
20//! 5. **Falsification**: Add third body perturbation, laws break
21
22use super::{CriterionStatus, EddDemo, FalsificationStatus, IntegratorType};
23use crate::engine::rng::SimRng;
24use serde::{Deserialize, Serialize};
25
26/// 2D vector for orbital mechanics.
27#[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    /// 2D cross product (returns scalar z-component).
60    #[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/// Kepler Orbit demo state.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct KeplerOrbitDemo {
90    /// Position vector (m).
91    pub position: Vec2,
92    /// Velocity vector (m/s).
93    pub velocity: Vec2,
94    /// Gravitational parameter μ = GM (m³/s²).
95    pub mu: f64,
96    /// Current simulation time (s).
97    pub time: f64,
98    /// Initial specific orbital energy.
99    pub initial_energy: f64,
100    /// Initial angular momentum magnitude.
101    pub initial_angular_momentum: f64,
102    /// Initial semi-major axis.
103    pub initial_semi_major_axis: f64,
104    /// Maximum energy drift observed.
105    pub max_energy_drift: f64,
106    /// Maximum angular momentum drift observed.
107    pub max_angular_momentum_drift: f64,
108    /// Number of steps taken.
109    pub step_count: u64,
110    /// Integrator type.
111    pub integrator: IntegratorType,
112    /// Energy conservation tolerance.
113    pub energy_tolerance: f64,
114    /// Angular momentum tolerance.
115    pub angular_momentum_tolerance: f64,
116    /// Period tolerance.
117    pub period_tolerance: f64,
118    /// Position history for orbit plotting.
119    #[serde(skip)]
120    pub orbit_history: Vec<Vec2>,
121    /// Maximum history length.
122    pub max_history: usize,
123    /// Enable third body perturbation.
124    pub perturbation_enabled: bool,
125    /// Third body position (if enabled).
126    pub perturber_position: Vec2,
127    /// Third body mass parameter.
128    pub perturber_mu: f64,
129    /// Seed for reproducibility.
130    pub seed: u64,
131    /// RNG for any stochastic elements.
132    #[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    /// Create a new Kepler orbit demo (Earth-like orbit).
145    #[must_use]
146    pub fn new(seed: u64) -> Self {
147        // Earth-like orbit around Sun-like star
148        let mu: f64 = 1.327_124_400_18e20; // GM_sun (m³/s²)
149        let r: f64 = 1.496e11; // 1 AU (m)
150        let v_circular: f64 = (mu / r).sqrt(); // Circular orbit velocity
151
152        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), // Jupiter-like distance
174            perturber_mu: mu * 0.001,                    // Jupiter mass ratio
175            seed,
176            rng: Some(SimRng::new(seed)),
177        };
178
179        // Calculate initial conserved quantities
180        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    /// Create orbit with specific eccentricity.
188    #[must_use]
189    pub fn with_eccentricity(seed: u64, eccentricity: f64) -> Self {
190        let mut demo = Self::new(seed);
191
192        // Adjust velocity for desired eccentricity
193        // At perihelion: v = sqrt(μ/a * (1+e)/(1-e)) for e < 1
194        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            // Position at perihelion
202            let r_perihelion = a * (1.0 - e);
203            demo.position = Vec2::new(r_perihelion, 0.0);
204        }
205
206        // Recalculate initial values
207        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    /// Calculate specific orbital energy: E = v²/2 - μ/r.
215    #[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    /// Calculate angular momentum magnitude: L = |r × v|.
228    #[must_use]
229    pub fn angular_momentum(&self) -> f64 {
230        self.position.cross(&self.velocity).abs()
231    }
232
233    /// Calculate semi-major axis: a = -μ/(2E).
234    #[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 // Parabolic or hyperbolic
241        }
242    }
243
244    /// Calculate orbital period: T = 2π√(a³/μ).
245    #[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    /// Calculate eccentricity.
256    #[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 // Parabolic or hyperbolic
265        }
266    }
267
268    /// Get relative energy drift.
269    #[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    /// Get relative angular momentum drift.
280    #[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    /// Calculate gravitational acceleration at position.
291    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        // Primary body acceleration
298        let a_primary = *pos * (-self.mu / (r * r * r));
299
300        // Add perturber if enabled
301        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    /// Step using Störmer-Verlet (symplectic).
315    fn step_stormer_verlet(&mut self, dt: f64) {
316        // Half step velocity
317        let a_n = self.acceleration(&self.position);
318        let v_half = self.velocity + a_n * (0.5 * dt);
319
320        // Full step position
321        self.position = self.position + v_half * dt;
322
323        // Half step velocity with new acceleration
324        let a_n1 = self.acceleration(&self.position);
325        self.velocity = v_half + a_n1 * (0.5 * dt);
326    }
327
328    /// Step using RK4 (non-symplectic).
329    fn step_rk4(&mut self, dt: f64) {
330        let pos0 = self.position;
331        let vel0 = self.velocity;
332
333        // k1
334        let a1 = self.acceleration(&pos0);
335        let k1_r = vel0;
336        let k1_v = a1;
337
338        // k2
339        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        // k3
346        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        // k4
353        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        // Update
360        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    /// Enable or disable perturbation.
365    pub fn set_perturbation(&mut self, enabled: bool) {
366        self.perturbation_enabled = enabled;
367    }
368
369    /// Set integrator type.
370    pub fn set_integrator(&mut self, integrator: IntegratorType) {
371        self.integrator = integrator;
372    }
373
374    /// Run simulation for given number of orbits.
375    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    /// Get current true anomaly (angle in orbit).
391    #[must_use]
392    pub fn true_anomaly(&self) -> f64 {
393        self.position.y.atan2(self.position.x)
394    }
395
396    /// Record position for orbit history.
397    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                // Simple Euler (unstable for orbits)
420                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        // Track maximum drifts
430        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        // Record for visualization
441        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// =============================================================================
495// WASM Bindings
496// =============================================================================
497
498#[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                // "verlet" and any other value defaults to StormerVerlet
554                _ => 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// =============================================================================
573// Tests - Following EDD Methodology
574// =============================================================================
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    // =========================================================================
581    // Phase 1: Equation - Define what we're testing
582    // =========================================================================
583
584    #[test]
585    fn test_equation_specific_energy() {
586        let demo = KeplerOrbitDemo::new(42);
587
588        // E = v²/2 - μ/r
589        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        // L = |r × v| (z-component in 2D)
601        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        // T² = (4π²/μ) a³
612        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    // =========================================================================
625    // Phase 2: Failing Test - Non-symplectic integrator fails
626    // =========================================================================
627
628    #[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        // Run for 1 orbit
635        demo.run_orbits(1.0, 1000);
636
637        // Euler should have significant energy drift
638        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        // Run for several orbits
652        demo.run_orbits(5.0, 1000);
653
654        // With perturbation, conservation laws break
655        // (Energy is NOT conserved with external forces)
656        // Note: This documents behavior, may or may not "fail" depending on perturbation strength
657        let drift = demo.energy_drift();
658        println!("With perturbation: energy drift = {drift:.2e}");
659    }
660
661    // =========================================================================
662    // Phase 3: Implementation - Störmer-Verlet conserves
663    // =========================================================================
664
665    #[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        // Run for 10 orbits
672        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        // Run for 10 orbits
687        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    // =========================================================================
697    // Phase 4: Verification - Long-term stability
698    // =========================================================================
699
700    #[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        // Run for 100 orbits
707        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        // Run for 10 orbits
724        demo.run_orbits(10.0, 2000); // More steps for elliptical
725
726        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    // =========================================================================
735    // Phase 5: Falsification - Document how to break conservation
736    // =========================================================================
737
738    #[test]
739    fn test_falsification_large_timestep() {
740        let mut demo = KeplerOrbitDemo::new(42);
741        demo.set_integrator(IntegratorType::StormerVerlet);
742
743        // Very large timestep (only 10 steps per orbit)
744        demo.run_orbits(1.0, 10);
745
746        let drift = demo.energy_drift();
747        // Document - may or may not fail depending on step size
748        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    // =========================================================================
762    // Integration tests
763    // =========================================================================
764
765    #[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        // Circular orbit should have low eccentricity
798        let e = demo.eccentricity();
799        assert!(
800            e < 0.01,
801            "Circular orbit eccentricity should be ~0, got {e}"
802        );
803
804        // Period should be about 1 year
805        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    // =========================================================================
816    // Additional coverage tests
817    // =========================================================================
818
819    #[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        // Magnitude
832        assert!((v1.magnitude() - 5.0).abs() < 1e-10);
833
834        // Normalize
835        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        // Dot product
841        assert!((v1.dot(&v2) - 11.0).abs() < 1e-10);
842
843        // Cross product
844        assert!((v1.cross(&v2) - 2.0).abs() < 1e-10);
845
846        // Add
847        let sum = v1 + v2;
848        assert!((sum.x - 4.0).abs() < 1e-10);
849        assert!((sum.y - 6.0).abs() < 1e-10);
850
851        // Sub
852        let diff = v1 - v2;
853        assert!((diff.x - 2.0).abs() < 1e-10);
854        assert!((diff.y - 2.0).abs() < 1e-10);
855
856        // Mul scalar
857        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        // Eccentricity > 0.99 should be clamped
905        let demo = KeplerOrbitDemo::with_eccentricity(42, 1.5);
906        // Should not crash and produce a valid orbit
907        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        // Set position very close to zero to test the edge case
929        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        // Set up escape velocity to get E >= 0
938        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        // Set up escape velocity
949        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        // Set up escape velocity
960        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        // Manipulate to get initial_energy = 0
971        demo.initial_energy = 0.0;
972        let drift = demo.energy_drift();
973        // Should return absolute difference
974        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        // Position should have changed
992        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        // History should be capped at max_history
1018        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        // Euler should accumulate drift
1035        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        // Euler should fail strict tolerance
1047        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        // Run a small timestep directly
1072        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        // Fresh demo should verify
1080        assert!(demo.verify_equation());
1081    }
1082}