Skip to main content

proof_engine/relativistic/
time_dilation.rs

1//! Time dilation effects for special and general relativity.
2
3use glam::{Vec2, Vec3, Vec4};
4use super::lorentz::lorentz_factor;
5
6/// Time dilation factor: returns gamma. Proper time interval = coordinate time / gamma.
7pub fn time_dilation_factor(v: f64, c: f64) -> f64 {
8    lorentz_factor(v, c)
9}
10
11/// A clock experiencing time dilation due to velocity.
12#[derive(Debug, Clone)]
13pub struct DilatedClock {
14    /// Accumulated proper time on this clock.
15    pub proper_time: f64,
16    /// Velocity of the clock relative to the observer's rest frame.
17    pub velocity: f64,
18    /// Speed of light.
19    pub c: f64,
20    /// Accumulated coordinate (observer) time.
21    pub accumulated: f64,
22}
23
24impl DilatedClock {
25    pub fn new(velocity: f64, c: f64) -> Self {
26        Self {
27            proper_time: 0.0,
28            velocity,
29            c,
30            accumulated: 0.0,
31        }
32    }
33
34    /// Advance the clock by `dt_observer` seconds of observer time.
35    /// The proper time advances more slowly by factor 1/gamma.
36    pub fn tick(&mut self, dt_observer: f64) {
37        self.accumulated += dt_observer;
38        let gamma = lorentz_factor(self.velocity, self.c);
39        self.proper_time += dt_observer / gamma;
40    }
41
42    /// Get the ratio of proper time to coordinate time.
43    pub fn rate(&self) -> f64 {
44        1.0 / lorentz_factor(self.velocity, self.c)
45    }
46
47    /// Reset the clock.
48    pub fn reset(&mut self) {
49        self.proper_time = 0.0;
50        self.accumulated = 0.0;
51    }
52
53    /// Set a new velocity (e.g., for acceleration phases).
54    pub fn set_velocity(&mut self, v: f64) {
55        self.velocity = v;
56    }
57
58    /// Get the current time difference between coordinate and proper time.
59    pub fn lag(&self) -> f64 {
60        self.accumulated - self.proper_time
61    }
62}
63
64/// Twin paradox calculation.
65/// Given a distance (one way) and travel speed v,
66/// returns (traveler_time, stay_at_home_time).
67/// Assumes instantaneous turnaround.
68pub fn twin_paradox(distance: f64, v: f64, c: f64) -> (f64, f64) {
69    let gamma = lorentz_factor(v, c);
70    let stay_time = 2.0 * distance / v;
71    let traveler_time = stay_time / gamma;
72    (traveler_time, stay_time)
73}
74
75/// Render clocks with tick rate proportional to proper time rate.
76#[derive(Debug, Clone)]
77pub struct TimeDilationRenderer {
78    pub c: f64,
79    pub show_clock_hands: bool,
80    pub clock_size: f32,
81}
82
83impl TimeDilationRenderer {
84    pub fn new(c: f64) -> Self {
85        Self {
86            c,
87            show_clock_hands: true,
88            clock_size: 1.0,
89        }
90    }
91
92    /// Compute the apparent tick rate of a clock at velocity v.
93    /// Returns a fraction of normal tick rate (0 to 1).
94    pub fn tick_rate(&self, v: f64) -> f32 {
95        let gamma = lorentz_factor(v, self.c);
96        (1.0 / gamma) as f32
97    }
98
99    /// Compute the clock hand angle for a given proper time.
100    /// One full rotation = 60 "seconds" of proper time.
101    pub fn clock_hand_angle(&self, proper_time: f64) -> f32 {
102        let seconds = proper_time % 60.0;
103        (seconds / 60.0 * std::f64::consts::TAU) as f32
104    }
105
106    /// Generate glyph data for a clock face at a position.
107    /// Returns positions for 12 hour markers and the hand endpoint.
108    pub fn clock_glyph_data(
109        &self,
110        center: Vec2,
111        proper_time: f64,
112    ) -> (Vec<Vec2>, Vec2) {
113        let mut markers = Vec::with_capacity(12);
114        let radius = self.clock_size;
115        for i in 0..12 {
116            let angle = (i as f32 / 12.0) * std::f32::consts::TAU;
117            markers.push(center + Vec2::new(angle.cos() * radius, angle.sin() * radius));
118        }
119        let hand_angle = self.clock_hand_angle(proper_time);
120        let hand_tip = center + Vec2::new(
121            hand_angle.cos() * radius * 0.8,
122            hand_angle.sin() * radius * 0.8,
123        );
124        (markers, hand_tip)
125    }
126
127    /// Render multiple clocks at different velocities.
128    pub fn render_clocks(
129        &self,
130        clocks: &[DilatedClock],
131        positions: &[Vec2],
132    ) -> Vec<(Vec<Vec2>, Vec2)> {
133        clocks.iter().zip(positions.iter()).map(|(clock, pos)| {
134            self.clock_glyph_data(*pos, clock.proper_time)
135        }).collect()
136    }
137}
138
139/// Dilated muon lifetime. Rest lifetime ~ 2.2 microseconds.
140/// Returns the dilated lifetime at speed v.
141pub fn muon_lifetime(v: f64) -> f64 {
142    let c = 299_792_458.0;
143    let rest_lifetime = 2.2e-6; // seconds
144    let gamma = lorentz_factor(v, c);
145    rest_lifetime * gamma
146}
147
148/// Gravitational time dilation (weak field approximation).
149/// dt_high / dt_low = 1 + g*h/c^2 (to first order).
150/// Returns the fractional time difference for height_diff.
151pub fn gravitational_time_dilation(height_diff: f64, g: f64, c: f64) -> f64 {
152    g * height_diff / (c * c)
153}
154
155/// Visualize two clocks at different velocities/heights side by side.
156#[derive(Debug, Clone)]
157pub struct ClockComparison {
158    pub clock_a: DilatedClock,
159    pub clock_b: DilatedClock,
160    pub position_a: Vec2,
161    pub position_b: Vec2,
162    pub label_a: String,
163    pub label_b: String,
164}
165
166impl ClockComparison {
167    pub fn new(
168        v_a: f64,
169        v_b: f64,
170        c: f64,
171        pos_a: Vec2,
172        pos_b: Vec2,
173    ) -> Self {
174        Self {
175            clock_a: DilatedClock::new(v_a, c),
176            clock_b: DilatedClock::new(v_b, c),
177            position_a: pos_a,
178            position_b: pos_b,
179            label_a: format!("v = {:.2}c", v_a / c),
180            label_b: format!("v = {:.2}c", v_b / c),
181        }
182    }
183
184    /// Advance both clocks by the same observer time.
185    pub fn tick(&mut self, dt_observer: f64) {
186        self.clock_a.tick(dt_observer);
187        self.clock_b.tick(dt_observer);
188    }
189
190    /// Get the time difference between the two clocks.
191    pub fn time_difference(&self) -> f64 {
192        self.clock_a.proper_time - self.clock_b.proper_time
193    }
194
195    /// Get the ratio of proper times.
196    pub fn time_ratio(&self) -> f64 {
197        if self.clock_b.proper_time.abs() < 1e-15 {
198            return 1.0;
199        }
200        self.clock_a.proper_time / self.clock_b.proper_time
201    }
202
203    /// Reset both clocks.
204    pub fn reset(&mut self) {
205        self.clock_a.reset();
206        self.clock_b.reset();
207    }
208
209    /// Generate rendering data for both clocks.
210    pub fn render_data(&self, renderer: &TimeDilationRenderer) -> ((Vec<Vec2>, Vec2), (Vec<Vec2>, Vec2)) {
211        let a = renderer.clock_glyph_data(self.position_a, self.clock_a.proper_time);
212        let b = renderer.clock_glyph_data(self.position_b, self.clock_b.proper_time);
213        (a, b)
214    }
215}
216
217/// Compute the GPS time correction needed per day.
218/// Combines special relativistic (velocity) and general relativistic (gravity) effects.
219/// Returns the correction in seconds per day.
220pub fn gps_time_correction(orbit_radius: f64, earth_mass: f64, earth_radius: f64) -> f64 {
221    let c = 299_792_458.0;
222    let G = 6.674e-11;
223
224    // Orbital velocity for circular orbit: v = sqrt(GM/r)
225    let v_sat = (G * earth_mass / orbit_radius).sqrt();
226
227    // SR effect: satellite clock runs slow by -v^2/(2c^2) per unit time (negative = slow)
228    let sr_correction = -v_sat * v_sat / (2.0 * c * c);
229
230    // GR effect: satellite clock runs fast by GM/(rc^2) - GM/(R_earth c^2)
231    let gr_correction = G * earth_mass / (earth_radius * c * c) - G * earth_mass / (orbit_radius * c * c);
232
233    // Total fractional correction per second, then scale to seconds per day
234    let total_fractional = sr_correction + gr_correction;
235    total_fractional * 86400.0
236}
237
238/// Distance a muon can travel at speed v given dilated lifetime.
239pub fn muon_travel_distance(v: f64) -> f64 {
240    let lifetime = muon_lifetime(v);
241    v * lifetime
242}
243
244/// Multi-step time dilation for a clock undergoing varying velocity.
245/// Takes a series of (duration, velocity) segments and returns total proper time.
246pub fn integrated_proper_time(segments: &[(f64, f64)], c: f64) -> f64 {
247    let mut total = 0.0;
248    for &(dt, v) in segments {
249        let gamma = lorentz_factor(v, c);
250        total += dt / gamma;
251    }
252    total
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    const C: f64 = 299_792_458.0;
260
261    #[test]
262    fn test_time_dilation_factor_at_rest() {
263        let factor = time_dilation_factor(0.0, C);
264        assert!((factor - 1.0).abs() < 1e-10);
265    }
266
267    #[test]
268    fn test_time_dilation_factor_half_c() {
269        let factor = time_dilation_factor(0.5 * C, C);
270        let expected = 1.0 / (1.0 - 0.25_f64).sqrt();
271        assert!((factor - expected).abs() < 1e-6);
272    }
273
274    #[test]
275    fn test_dilated_clock() {
276        let mut clock = DilatedClock::new(0.866 * C, C);
277        // gamma ~ 2
278        clock.tick(10.0);
279        // proper time ~ 5
280        assert!((clock.proper_time - 5.0).abs() < 0.1);
281        assert!((clock.accumulated - 10.0).abs() < 1e-10);
282    }
283
284    #[test]
285    fn test_twin_paradox() {
286        // Travel to a star 4 light-years away at 0.8c
287        let distance = 4.0 * C * 365.25 * 86400.0; // 4 light-years in meters
288        let v = 0.8 * C;
289        let (traveler, stay) = twin_paradox(distance, v, C);
290
291        // stay time = 2 * 4ly / 0.8c = 10 years
292        let stay_years = stay / (365.25 * 86400.0);
293        assert!((stay_years - 10.0).abs() < 0.01);
294
295        // gamma at 0.8c = 5/3, so traveler_time = 10 / (5/3) = 6 years
296        let traveler_years = traveler / (365.25 * 86400.0);
297        assert!((traveler_years - 6.0).abs() < 0.01);
298
299        // Traveler ages less
300        assert!(traveler < stay);
301    }
302
303    #[test]
304    fn test_muon_reaches_ground() {
305        let v = 0.998 * C;
306        let distance = muon_travel_distance(v);
307        // Atmosphere is ~15 km. Without dilation, muon travels ~660m.
308        // With dilation at 0.998c, gamma ~ 15.8, distance ~ 10.4 km.
309        // At even higher speeds they exceed 15 km.
310        let rest_distance = v * 2.2e-6;
311        assert!(distance > rest_distance);
312        // Check that dilated distance is much larger
313        assert!(distance > 5.0 * rest_distance);
314    }
315
316    #[test]
317    fn test_muon_lifetime_dilation() {
318        let v = 0.99 * C;
319        let dilated = muon_lifetime(v);
320        let rest = 2.2e-6;
321        let gamma = lorentz_factor(v, C);
322        assert!((dilated - rest * gamma).abs() < 1e-15);
323    }
324
325    #[test]
326    fn test_gravitational_time_dilation_weak_field() {
327        // At 1 meter height diff with g=9.8
328        let frac = gravitational_time_dilation(1.0, 9.8, C);
329        // Should be about 1.09e-16
330        assert!(frac > 0.0);
331        assert!(frac < 1e-14);
332    }
333
334    #[test]
335    fn test_gps_correction() {
336        // GPS satellite orbit radius ~ 26,571 km = 26_571_000 m
337        let orbit_r = 26_571_000.0;
338        let earth_mass = 5.972e24;
339        let earth_r = 6_371_000.0;
340
341        let correction = gps_time_correction(orbit_r, earth_mass, earth_r);
342        // GPS correction is approximately +38 microseconds/day
343        let correction_us = correction * 1e6;
344        assert!(
345            (correction_us - 38.0).abs() < 10.0,
346            "GPS correction: {} us/day, expected ~38",
347            correction_us
348        );
349    }
350
351    #[test]
352    fn test_clock_comparison() {
353        let mut comp = ClockComparison::new(
354            0.0, 0.866 * C, C,
355            Vec2::new(-5.0, 0.0),
356            Vec2::new(5.0, 0.0),
357        );
358        comp.tick(10.0);
359        assert!((comp.clock_a.proper_time - 10.0).abs() < 1e-10);
360        assert!((comp.clock_b.proper_time - 5.0).abs() < 0.1);
361        assert!(comp.time_difference() > 0.0);
362    }
363
364    #[test]
365    fn test_integrated_proper_time() {
366        let segments = vec![
367            (5.0, 0.0),       // at rest for 5s
368            (5.0, 0.866 * C), // at 0.866c for 5s (gamma~2)
369        ];
370        let tau = integrated_proper_time(&segments, C);
371        // 5 + 5/2 = 7.5
372        assert!((tau - 7.5).abs() < 0.1);
373    }
374
375    #[test]
376    fn test_dilated_clock_lag() {
377        let mut clock = DilatedClock::new(0.6 * C, C);
378        clock.tick(100.0);
379        assert!(clock.lag() > 0.0);
380        // gamma at 0.6c = 1.25, proper = 80, lag = 20
381        assert!((clock.lag() - 20.0).abs() < 0.1);
382    }
383}