solar_line_core/
relativistic.rs

1/// Relativistic corrections for high-velocity orbital transfers.
2///
3/// At velocities reaching ~1-2.5% of the speed of light (as seen in SOLAR LINE
4/// brachistochrone transfers), special relativistic effects become potentially
5/// measurable. This module quantifies:
6///
7/// - Lorentz factor γ
8/// - Time dilation (proper time vs coordinate time)
9/// - Relativistic vs classical Tsiolkovsky rocket equation
10/// - Relativistic kinetic energy vs classical approximation
11use crate::units::{KmPerSec, Seconds};
12
13/// Speed of light in km/s — re-exported from constants module for convenience.
14pub use crate::constants::C_KM_S;
15
16/// Compute the Lorentz factor γ = 1 / √(1 - β²), where β = v/c.
17///
18/// Returns γ ≥ 1. For v = 0, returns exactly 1.0.
19/// Panics if v ≥ c (speed of light).
20pub fn lorentz_factor(v: KmPerSec) -> f64 {
21    let beta = v.value() / C_KM_S;
22    assert!(
23        beta.abs() < 1.0,
24        "velocity must be less than c ({} km/s), got {} km/s",
25        C_KM_S,
26        v.value()
27    );
28    1.0 / (1.0 - beta * beta).sqrt()
29}
30
31/// Compute β = v/c (velocity as a fraction of the speed of light).
32pub fn beta(v: KmPerSec) -> f64 {
33    v.value() / C_KM_S
34}
35
36/// Time dilation: compute proper time (ship time) given coordinate time.
37///
38/// For a constant-velocity segment at speed v:
39///   Δτ = Δt / γ = Δt × √(1 - β²)
40///
41/// This is an approximation — real brachistochrone transfers involve
42/// continuously varying velocity. For a more accurate calculation,
43/// use `brachistochrone_proper_time`.
44pub fn time_dilation_constant_v(coordinate_time: Seconds, v: KmPerSec) -> Seconds {
45    let gamma = lorentz_factor(v);
46    Seconds(coordinate_time.value() / gamma)
47}
48
49/// Proper time lost due to time dilation at constant velocity.
50///
51/// Returns the difference: coordinate_time - proper_time.
52/// This is how much less time passes on the ship compared to an
53/// external observer.
54pub fn time_dilation_loss(coordinate_time: Seconds, v: KmPerSec) -> Seconds {
55    let proper = time_dilation_constant_v(coordinate_time, v);
56    Seconds(coordinate_time.value() - proper.value())
57}
58
59/// Relativistic kinetic energy factor relative to classical.
60///
61/// Classical: KE = ½mv²
62/// Relativistic: KE = (γ - 1)mc²
63///
64/// Returns the ratio: (γ - 1)c² / (½v²)
65/// For v << c this approaches 1.0.
66/// For v ~ 0.025c this is ~1.0003 (0.03% correction).
67pub fn kinetic_energy_correction_factor(v: KmPerSec) -> f64 {
68    let beta_val = beta(v);
69    let gamma = lorentz_factor(v);
70
71    // Avoid division by zero for v=0
72    if beta_val.abs() < 1e-15 {
73        return 1.0;
74    }
75
76    let relativistic_ke = (gamma - 1.0) * C_KM_S * C_KM_S;
77    let classical_ke = 0.5 * v.value() * v.value();
78    relativistic_ke / classical_ke
79}
80
81/// Classical (Newtonian) Tsiolkovsky ΔV.
82///
83/// Δv = ve × ln(m0/mf)
84pub fn classical_delta_v(exhaust_velocity: KmPerSec, mass_ratio: f64) -> KmPerSec {
85    assert!(mass_ratio > 0.0, "mass ratio must be positive");
86    KmPerSec(exhaust_velocity.value() * mass_ratio.ln())
87}
88
89/// Relativistic rocket equation (constant-thrust, constant-exhaust-velocity).
90///
91/// For a relativistic rocket with exhaust velocity ve and mass ratio m0/mf,
92/// the final coordinate velocity is:
93///
94///   v_final = c × tanh(ve/c × ln(m0/mf))
95///
96/// This is the Ackeret relativistic rocket equation. When ve << c,
97/// tanh(x) ≈ x and this reduces to the classical Tsiolkovsky equation.
98///
99/// Note: This gives the velocity achievable from rest. For ve << c,
100/// v_final ≈ ve × ln(m0/mf) (classical limit).
101pub fn relativistic_delta_v(exhaust_velocity: KmPerSec, mass_ratio: f64) -> KmPerSec {
102    assert!(mass_ratio > 0.0, "mass ratio must be positive");
103    let x = exhaust_velocity.value() / C_KM_S * mass_ratio.ln();
104    KmPerSec(C_KM_S * x.tanh())
105}
106
107/// Fractional correction between relativistic and classical ΔV.
108///
109/// Returns (classical - relativistic) / classical.
110///
111/// A positive value means the relativistic ΔV is less than the classical
112/// (as expected — tanh(x) < x for x > 0). This represents how much
113/// the classical equation overestimates the achievable velocity.
114pub fn delta_v_correction_fraction(exhaust_velocity: KmPerSec, mass_ratio: f64) -> f64 {
115    let classical = classical_delta_v(exhaust_velocity, mass_ratio);
116    let relativistic = relativistic_delta_v(exhaust_velocity, mass_ratio);
117
118    if classical.value().abs() < 1e-15 {
119        return 0.0;
120    }
121
122    (classical.value() - relativistic.value()) / classical.value()
123}
124
125/// Compute proper time for a brachistochrone (constant-acceleration) transfer.
126///
127/// For a flip-at-midpoint brachistochrone at constant proper acceleration `a`,
128/// covering distance `d` (coordinate distance), the proper time experienced
129/// by the crew is:
130///
131///   τ = (2c/a) × arcsinh(a × t_half / c)
132///
133/// where t_half is the coordinate time for the acceleration phase (half the
134/// total coordinate time), computed from the relativistic kinematics.
135///
136/// For a << c²/d (non-relativistic), this approaches the classical result.
137///
138/// Parameters:
139/// - `distance`: total transfer distance in km
140/// - `accel_km_s2`: constant proper acceleration in km/s²
141///
142/// Returns (coordinate_time, proper_time) both in seconds.
143pub fn brachistochrone_times(distance: crate::units::Km, accel_km_s2: f64) -> (Seconds, Seconds) {
144    let d = distance.value();
145    let a = accel_km_s2;
146    let c = C_KM_S;
147
148    // Half-distance for the acceleration phase
149    let d_half = d / 2.0;
150
151    // Relativistic coordinate time for half the journey (accelerating):
152    // d_half = (c²/a)(√(1 + (at/c)²) - 1)
153    // Solving for t: t = (c/a)√((d_half·a/c² + 1)² - 1)
154    let x = d_half * a / (c * c) + 1.0;
155    let t_half = (c / a) * (x * x - 1.0).sqrt();
156    let t_total = 2.0 * t_half;
157
158    // Proper time for half the journey:
159    // τ_half = (c/a) × arcsinh(a × t_half / c)
160    let tau_half = (c / a) * (a * t_half / c).asinh();
161    let tau_total = 2.0 * tau_half;
162
163    (Seconds(t_total), Seconds(tau_total))
164}
165
166/// Peak velocity at the midpoint of a brachistochrone transfer.
167///
168/// v_peak = a × t_half / √(1 + (a × t_half / c)²)
169///
170/// For non-relativistic cases, this simplifies to a × t_half.
171pub fn brachistochrone_peak_velocity(distance: crate::units::Km, accel_km_s2: f64) -> KmPerSec {
172    let d = distance.value();
173    let a = accel_km_s2;
174    let c = C_KM_S;
175
176    let d_half = d / 2.0;
177    let x = d_half * a / (c * c) + 1.0;
178    let t_half = (c / a) * (x * x - 1.0).sqrt();
179
180    let at_over_c = a * t_half / c;
181    let v_peak = c * at_over_c / (1.0 + at_over_c * at_over_c).sqrt();
182
183    KmPerSec(v_peak)
184}
185
186/// Summary of relativistic effects for a given velocity.
187///
188/// Returns (gamma, beta, time_dilation_ppm, ke_correction_ppm):
189/// - gamma: Lorentz factor
190/// - beta: v/c fraction
191/// - time_dilation_ppm: parts per million of time dilation (τ shorter than t)
192/// - ke_correction_ppm: parts per million correction to kinetic energy
193pub fn effects_summary(v: KmPerSec) -> (f64, f64, f64, f64) {
194    let b = beta(v);
195    let g = lorentz_factor(v);
196
197    // Time dilation: fraction of time "lost" = 1 - 1/γ
198    let td_fraction = 1.0 - 1.0 / g;
199    let td_ppm = td_fraction * 1e6;
200
201    // KE correction: fraction above classical
202    let ke_factor = kinetic_energy_correction_factor(v);
203    let ke_ppm = (ke_factor - 1.0) * 1e6;
204
205    (g, b, td_ppm, ke_ppm)
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::units::Km;
212
213    #[test]
214    fn test_speed_of_light() {
215        // c = 299,792.458 km/s (exact)
216        assert!((C_KM_S - 299_792.458).abs() < 1e-3);
217    }
218
219    #[test]
220    fn test_lorentz_factor_zero_velocity() {
221        let gamma = lorentz_factor(KmPerSec(0.0));
222        assert!((gamma - 1.0).abs() < 1e-15, "γ at v=0 should be 1.0");
223    }
224
225    #[test]
226    fn test_lorentz_factor_known_values() {
227        // At v = 0.1c: γ = 1/√(1-0.01) = 1/√0.99 ≈ 1.00504
228        let v_01c = KmPerSec(0.1 * C_KM_S);
229        let gamma = lorentz_factor(v_01c);
230        let expected = 1.0 / (1.0 - 0.01_f64).sqrt();
231        assert!(
232            (gamma - expected).abs() < 1e-10,
233            "γ at 0.1c: {} vs expected {}",
234            gamma,
235            expected
236        );
237    }
238
239    #[test]
240    fn test_lorentz_factor_half_c() {
241        // At v = 0.5c: γ = 1/√(1-0.25) = 1/√0.75 ≈ 1.1547
242        let v = KmPerSec(0.5 * C_KM_S);
243        let gamma = lorentz_factor(v);
244        let expected = 1.0 / (0.75_f64).sqrt();
245        assert!(
246            (gamma - expected).abs() < 1e-10,
247            "γ at 0.5c: {} vs expected {}",
248            gamma,
249            expected
250        );
251    }
252
253    #[test]
254    fn test_beta_computation() {
255        let v = KmPerSec(2997.92458); // 0.01c = 1% of light speed
256        let b = beta(v);
257        assert!((b - 0.01).abs() < 1e-10, "β should be 0.01, got {}", b);
258    }
259
260    #[test]
261    fn test_time_dilation_at_rest() {
262        let t = Seconds(3600.0); // 1 hour
263        let proper = time_dilation_constant_v(t, KmPerSec(0.0));
264        assert!(
265            (proper.value() - 3600.0).abs() < 1e-10,
266            "no time dilation at rest"
267        );
268    }
269
270    #[test]
271    fn test_time_dilation_at_1_percent_c() {
272        // v = 0.01c → γ ≈ 1.00005
273        // Time dilation loss over 1 year ≈ 1577 seconds (26 min)
274        let v = KmPerSec(0.01 * C_KM_S);
275        let one_year = Seconds(365.25 * 86400.0);
276        let loss = time_dilation_loss(one_year, v);
277
278        // Expected: Δτ = t × (1 - 1/γ)
279        let gamma = lorentz_factor(v);
280        let expected_loss = one_year.value() * (1.0 - 1.0 / gamma);
281        assert!(
282            (loss.value() - expected_loss).abs() < 1e-6,
283            "time loss: {} s vs expected {} s",
284            loss.value(),
285            expected_loss
286        );
287
288        // At 0.01c, loss should be ~0.005% of total time (tiny but measurable)
289        let loss_fraction = loss.value() / one_year.value();
290        assert!(
291            loss_fraction > 1e-5 && loss_fraction < 1e-3,
292            "loss fraction at 1%c: {}",
293            loss_fraction
294        );
295    }
296
297    #[test]
298    fn test_kinetic_energy_correction_at_zero() {
299        let factor = kinetic_energy_correction_factor(KmPerSec(0.0));
300        assert!(
301            (factor - 1.0).abs() < 1e-10,
302            "KE correction at v=0 should be 1.0"
303        );
304    }
305
306    #[test]
307    fn test_kinetic_energy_correction_at_low_v() {
308        // At v = 0.01c: γ ≈ 1.00005
309        // KE_rel / KE_class = (γ-1)c² / (½v²)
310        // = (0.00005 × c²) / (0.5 × (0.01c)²)
311        // = 0.00005 / (0.5 × 0.0001)
312        // ≈ 1.00005
313        let v = KmPerSec(0.01 * C_KM_S);
314        let factor = kinetic_energy_correction_factor(v);
315        // Should be very close to 1 — well within 0.1%
316        assert!(
317            (factor - 1.0).abs() < 0.001,
318            "KE correction at 0.01c: {} (expected ~1.0)",
319            factor
320        );
321    }
322
323    #[test]
324    fn test_kinetic_energy_correction_increases_with_v() {
325        let factor_1 = kinetic_energy_correction_factor(KmPerSec(0.01 * C_KM_S));
326        let factor_5 = kinetic_energy_correction_factor(KmPerSec(0.05 * C_KM_S));
327        let factor_10 = kinetic_energy_correction_factor(KmPerSec(0.10 * C_KM_S));
328
329        assert!(factor_5 > factor_1, "KE correction should increase with v");
330        assert!(factor_10 > factor_5, "KE correction should increase with v");
331    }
332
333    #[test]
334    fn test_classical_delta_v() {
335        // Classical: Δv = ve × ln(m0/mf)
336        let ve = KmPerSec(9806.65); // Isp 10^6 s
337        let mr = std::f64::consts::E; // mass ratio = e → Δv = ve
338        let dv = classical_delta_v(ve, mr);
339        assert!(
340            (dv.value() - ve.value()).abs() < 1e-6,
341            "Δv should equal ve for mass ratio e"
342        );
343    }
344
345    #[test]
346    fn test_relativistic_delta_v_low_speed() {
347        // At low velocities, relativistic ≈ classical
348        let ve = KmPerSec(10.0); // 10 km/s (chemical rockets)
349        let mr = 3.0;
350        let classical = classical_delta_v(ve, mr);
351        let relativistic = relativistic_delta_v(ve, mr);
352
353        let diff = (classical.value() - relativistic.value()).abs();
354        assert!(
355            diff / classical.value() < 1e-6,
356            "at low v, relativistic should match classical: rel diff = {}",
357            diff / classical.value()
358        );
359    }
360
361    #[test]
362    fn test_relativistic_delta_v_less_than_classical() {
363        // Relativistic Δv is always ≤ classical (tanh(x) ≤ x)
364        let ve = KmPerSec(9806.65); // Isp 10^6 s → ve ≈ 0.033c
365        let mr = 10.0; // mass ratio 10 → classical Δv ≈ 22,578 km/s ≈ 7.5%c
366        let classical = classical_delta_v(ve, mr);
367        let relativistic = relativistic_delta_v(ve, mr);
368
369        assert!(
370            relativistic.value() <= classical.value(),
371            "relativistic ({}) should be ≤ classical ({})",
372            relativistic.value(),
373            classical.value()
374        );
375
376        // At 7.5%c the correction should be noticeable but small
377        let correction = delta_v_correction_fraction(ve, mr);
378        assert!(
379            correction > 0.0 && correction < 0.1,
380            "correction at 7.5%c: {}",
381            correction
382        );
383    }
384
385    #[test]
386    fn test_relativistic_delta_v_high_mass_ratio() {
387        // Very high mass ratio → velocity approaches c asymptotically
388        // ve = 0.1c, mr = 1e10 → x = 0.1 × ln(1e10) ≈ 2.3 → tanh(2.3) ≈ 0.98
389        let ve = KmPerSec(0.1 * C_KM_S); // ve = 0.1c
390        let mr = 1e10;
391        let relativistic = relativistic_delta_v(ve, mr);
392
393        // tanh approaches 1, so v → c, but not instantly
394        assert!(
395            relativistic.value() < C_KM_S,
396            "relativistic velocity should be < c"
397        );
398        // At ve=0.1c, mr=1e10: v ≈ 0.98c
399        assert!(
400            relativistic.value() > 0.95 * C_KM_S,
401            "at extreme mass ratio, velocity should be close to c: {} km/s ({}c)",
402            relativistic.value(),
403            relativistic.value() / C_KM_S
404        );
405
406        // With even higher mass ratio, should get closer to c
407        let mr_extreme = 1e100;
408        let rel_extreme = relativistic_delta_v(ve, mr_extreme);
409        assert!(
410            rel_extreme.value() > relativistic.value(),
411            "higher mass ratio should give higher velocity"
412        );
413        assert!(
414            rel_extreme.value() > 0.999 * C_KM_S,
415            "at extreme mass ratio (1e100), should be very close to c: {} km/s",
416            rel_extreme.value()
417        );
418    }
419
420    #[test]
421    fn test_delta_v_correction_kestrel_ep01() {
422        // Kestrel EP01: Δv ≈ 8497 km/s, Isp = 10^6 s (ve ≈ 9806.65 km/s)
423        // β_peak ≈ 8497 / 299792 ≈ 0.0283 (2.83% c)
424        // mass_ratio = exp(8497 / 9806.65) ≈ 2.376
425        let ve = KmPerSec(9806.65);
426        let mr = (8497.0_f64 / 9806.65).exp();
427        let correction = delta_v_correction_fraction(ve, mr);
428
429        // At ~2.8%c, the correction should be on the order of 0.01-0.1%
430        assert!(correction > 0.0, "correction should be positive");
431        assert!(
432            correction < 0.01,
433            "correction at 2.8%c should be < 1%, got {}%",
434            correction * 100.0
435        );
436    }
437
438    #[test]
439    fn test_brachistochrone_times_classical_limit() {
440        // For low acceleration and short distance (non-relativistic),
441        // coordinate time and proper time should be nearly identical
442        let d = Km(550_630_800.0); // Mars-Jupiter closest
443        let a = 0.032_783; // km/s² (EP01 72h scenario)
444
445        let (t_coord, t_proper) = brachistochrone_times(d, a);
446
447        // Classical: t = sqrt(4d/a) = 2 × sqrt(d/a) for half-distance
448        // Actually: d = ½at² for half-journey → t_half = sqrt(2d_half/a) = sqrt(d/a)
449        // Total: t = 2 × sqrt(d/a)
450        // Wait — classical brachistochrone: d = ½ × a × (t/2)² × 2 = a × t²/4
451        // So t = 2 × sqrt(d/a)
452        let t_classical = 2.0 * (d.value() / a).sqrt();
453
454        // Should be close for non-relativistic case (within ~0.01%)
455        let rel_diff = (t_coord.value() - t_classical).abs() / t_classical;
456        assert!(
457            rel_diff < 1e-4,
458            "coordinate time should match classical: {} vs {}, rel diff = {}",
459            t_coord.value(),
460            t_classical,
461            rel_diff
462        );
463
464        // Proper time should also be close to coordinate time
465        let td_diff = (t_coord.value() - t_proper.value()).abs() / t_coord.value();
466        assert!(
467            td_diff < 1e-3,
468            "proper time should be close to coordinate time: diff = {}",
469            td_diff
470        );
471    }
472
473    #[test]
474    fn test_brachistochrone_times_high_accel() {
475        // High acceleration: a = 1 km/s² over 1 AU
476        // Peak velocity will be significant fraction of c
477        let d = Km(149_597_870.7); // 1 AU
478        let a = 1.0; // 1 km/s² ≈ 102g
479
480        let (t_coord, t_proper) = brachistochrone_times(d, a);
481
482        // Proper time should be less than coordinate time
483        assert!(
484            t_proper.value() < t_coord.value(),
485            "proper time should be less than coordinate time"
486        );
487        assert!(t_proper.value() > 0.0, "proper time should be positive");
488    }
489
490    #[test]
491    fn test_brachistochrone_peak_velocity() {
492        // EP01 scenario: Mars-Jupiter closest, 72h
493        let d = Km(550_630_800.0);
494        let a = 0.032_783; // km/s²
495
496        let v_peak = brachistochrone_peak_velocity(d, a);
497
498        // Classical: v_peak = a × t_half = a × sqrt(d/a) = sqrt(a × d)
499        // Actually: v_peak = a × (t/2) where t = 2√(d/a)
500        // v_peak = a × √(d/a) = √(a × d)
501        let v_classical = (a * d.value()).sqrt();
502
503        // Should be close in non-relativistic regime
504        let rel_diff = (v_peak.value() - v_classical).abs() / v_classical;
505        assert!(
506            rel_diff < 1e-4,
507            "peak velocity: {} vs classical {}, diff = {}",
508            v_peak.value(),
509            v_classical,
510            rel_diff
511        );
512
513        // Check the velocity is in the right ballpark (~4249 km/s ≈ 1.4% c)
514        assert!(
515            v_peak.value() > 4000.0 && v_peak.value() < 5000.0,
516            "EP01 peak velocity should be ~4249 km/s, got {} km/s",
517            v_peak.value()
518        );
519    }
520
521    #[test]
522    fn test_brachistochrone_peak_velocity_capped_at_c() {
523        // Extremely high acceleration should still give v < c
524        let d = Km(1e12); // very large distance
525        let a = 100.0; // very high acceleration
526
527        let v_peak = brachistochrone_peak_velocity(d, a);
528        assert!(
529            v_peak.value() < C_KM_S,
530            "peak velocity should be < c, got {} km/s",
531            v_peak.value()
532        );
533    }
534
535    #[test]
536    fn test_delta_v_divergence_at_10_percent_c() {
537        // At v=0.1c, the relativistic ΔV should diverge from classical by >0.5%
538        let ve = KmPerSec(9806.65); // Kestrel Isp 10^6 s
539                                    // Mass ratio to achieve ~0.1c classically: exp(0.1c/ve) = exp(29979/9807) ≈ exp(3.057) ≈ 21.3
540        let target_v = 0.1 * C_KM_S; // ~29979 km/s
541        let mr = (target_v / ve.value()).exp();
542
543        let classical = classical_delta_v(ve, mr);
544        let relativistic = relativistic_delta_v(ve, mr);
545        let correction = delta_v_correction_fraction(ve, mr);
546
547        // Classical should give exactly target_v
548        assert!(
549            (classical.value() - target_v).abs() < 1e-6,
550            "classical should give ~0.1c: {} km/s",
551            classical.value()
552        );
553        // Relativistic should give less (tanh < identity)
554        assert!(
555            relativistic.value() < classical.value(),
556            "relativistic ({}) should be less than classical ({})",
557            relativistic.value(),
558            classical.value()
559        );
560        // At 0.1c, the correction should be measurable (~0.33%)
561        // tanh(x)/x ≈ 1 - x²/3 for small x, so correction ≈ x²/3
562        // where x = ve/c × ln(mr) ≈ 0.1 → correction ≈ 0.01/3 ≈ 0.33%
563        assert!(
564            correction > 0.003,
565            "at 0.1c, relativistic correction should exceed 0.3%, got {:.4}%",
566            correction * 100.0
567        );
568        // But should still be modest (< 1%)
569        assert!(
570            correction < 0.01,
571            "at 0.1c, correction should be < 1%, got {:.4}%",
572            correction * 100.0
573        );
574    }
575
576    #[test]
577    fn test_brachistochrone_time_dilation_increases_with_accel() {
578        // Higher acceleration → higher peak velocity → more time dilation
579        let d = Km(149_597_870.7); // 1 AU
580
581        let (t_coord_low, t_proper_low) = brachistochrone_times(d, 0.01); // 0.01 km/s² ≈ 1g
582        let (t_coord_high, t_proper_high) = brachistochrone_times(d, 1.0); // 1.0 km/s² ≈ 102g
583
584        // Both should have proper time < coordinate time
585        assert!(t_proper_low.value() < t_coord_low.value());
586        assert!(t_proper_high.value() < t_coord_high.value());
587
588        // Time dilation fraction should be larger for higher acceleration
589        let dilation_low = 1.0 - t_proper_low.value() / t_coord_low.value();
590        let dilation_high = 1.0 - t_proper_high.value() / t_coord_high.value();
591        assert!(
592            dilation_high > dilation_low,
593            "higher accel should give more time dilation: low={:.2e} high={:.2e}",
594            dilation_low,
595            dilation_high
596        );
597
598        // At 1 km/s² over 1 AU, peak velocity is significant — dilation should be measurable
599        let v_peak = brachistochrone_peak_velocity(d, 1.0);
600        assert!(
601            v_peak.value() > 0.01 * C_KM_S,
602            "peak velocity at 1 km/s² should exceed 1%c: {} km/s",
603            v_peak.value()
604        );
605    }
606
607    #[test]
608    fn test_effects_summary_solar_line_velocities() {
609        // Test at key velocities from the series:
610        // 1500 km/s (cruise), 4249 km/s (EP01 peak), 7600 km/s (max peak)
611
612        let (gamma_1500, beta_1500, td_1500, _ke_1500) = effects_summary(KmPerSec(1500.0));
613        assert!(
614            (beta_1500 - 1500.0 / C_KM_S).abs() < 1e-10,
615            "β at 1500 km/s"
616        );
617        assert!(gamma_1500 > 1.0, "γ should be > 1");
618        assert!(td_1500 > 0.0, "time dilation should be positive");
619
620        // At 1500 km/s (0.5% c): very small effects
621        assert!(
622            td_1500 < 100.0,
623            "time dilation at 0.5%c should be < 100 ppm, got {} ppm",
624            td_1500
625        );
626
627        // At 7600 km/s (2.5% c): still small but measurable
628        let (_gamma_7600, _beta_7600, td_7600, _ke_7600) = effects_summary(KmPerSec(7600.0));
629        assert!(
630            td_7600 > td_1500,
631            "time dilation should increase with velocity"
632        );
633        assert!(
634            td_7600 < 1000.0,
635            "time dilation at 2.5%c should be < 1000 ppm, got {} ppm",
636            td_7600
637        );
638    }
639
640    #[test]
641    fn test_effects_summary_ep01_peak() {
642        // EP01: Mars→Ganymede 72h, peak velocity ~4249 km/s ≈ 1.42%c
643        let v = KmPerSec(4249.0);
644        let (gamma, beta_val, td_ppm, ke_ppm) = effects_summary(v);
645
646        // β ≈ 0.0142
647        assert!(
648            (beta_val - 0.01417).abs() < 0.001,
649            "β at 4249 km/s: {}",
650            beta_val
651        );
652
653        // γ ≈ 1 + β²/2 ≈ 1.0001
654        assert!(gamma > 1.0 && gamma < 1.001, "γ at 1.4%c: {}", gamma);
655
656        // Time dilation: ~100 ppm (β²/2 × 10^6)
657        assert!(
658            td_ppm > 50.0 && td_ppm < 200.0,
659            "time dilation at 1.4%c: {} ppm",
660            td_ppm
661        );
662
663        // KE correction: similar magnitude
664        assert!(
665            ke_ppm > 0.0 && ke_ppm < 200.0,
666            "KE correction at 1.4%c: {} ppm",
667            ke_ppm
668        );
669    }
670
671    #[test]
672    fn test_effects_summary_max_peak() {
673        // Maximum peak velocity ~7600 km/s ≈ 2.53%c (from highest-accel brachistochrone)
674        let v = KmPerSec(7600.0);
675        let (gamma, beta_val, td_ppm, ke_ppm) = effects_summary(v);
676
677        // β ≈ 0.0253
678        assert!(
679            (beta_val - 0.02535).abs() < 0.001,
680            "β at 7600 km/s: {}",
681            beta_val
682        );
683
684        // Time dilation: ~321 ppm (β²/2 × 10^6)
685        // Over 72h = 259,200s → loss ≈ 83s
686        // At 2.5%c, β²/2 ≈ 3.2e-4, so loss ≈ 259200 × 3.2e-4 ≈ 83s
687        // This is measurable but negligible for navigation (~0.032%)
688        let loss_72h = td_ppm * 1e-6 * 259200.0;
689        assert!(
690            loss_72h > 50.0 && loss_72h < 150.0,
691            "time dilation loss over 72h at 2.5%c should be ~83s, got {} s",
692            loss_72h
693        );
694
695        // All corrections should be under 1000 ppm (0.1%)
696        assert!(
697            td_ppm < 1000.0 && ke_ppm < 1000.0,
698            "corrections at 2.5%c: td={} ppm, ke={} ppm",
699            td_ppm,
700            ke_ppm
701        );
702
703        // Verify gamma and beta are consistent
704        let expected_gamma = 1.0 / (1.0 - beta_val * beta_val).sqrt();
705        assert!(
706            (gamma - expected_gamma).abs() < 1e-10,
707            "γ consistency check"
708        );
709    }
710}