Skip to main content

proof_engine/relativistic/
grav_time.rs

1//! Gravitational time dilation.
2
3use glam::{Vec2, Vec4};
4
5/// Schwarzschild time dilation factor: sqrt(1 - rs/r).
6/// Returns 0 at the event horizon (r = rs), imaginary inside.
7pub fn schwarzschild_time_dilation(r: f64, rs: f64) -> f64 {
8    if r <= rs {
9        return 0.0;
10    }
11    (1.0 - rs / r).sqrt()
12}
13
14/// Gravitational redshift between two radii in Schwarzschild geometry.
15/// f_obs / f_emit = sqrt((1 - rs/r_emit) / (1 - rs/r_obs))
16/// Returns the wavelength ratio: lambda_obs / lambda_emit.
17pub fn gravitational_redshift(r_emit: f64, r_obs: f64, rs: f64) -> f64 {
18    let factor_emit = schwarzschild_time_dilation(r_emit, rs);
19    let factor_obs = schwarzschild_time_dilation(r_obs, rs);
20    if factor_obs.abs() < 1e-15 {
21        return f64::INFINITY;
22    }
23    factor_emit / factor_obs
24}
25
26/// Proper time rate at radius r around a mass M.
27/// d(tau)/dt = sqrt(1 - 2GM/(rc^2))
28#[allow(non_snake_case)]
29pub fn proper_time_rate(r: f64, mass: f64, G: f64, c: f64) -> f64 {
30    let rs = 2.0 * G * mass / (c * c);
31    schwarzschild_time_dilation(r, rs)
32}
33
34/// GPS correction: combined special + general relativistic time correction.
35/// Returns the correction in seconds per day.
36///
37/// GR effect: clocks higher in gravity run faster.
38/// SR effect: moving clocks run slower.
39pub fn gps_correction(orbit_radius: f64, earth_mass: f64, earth_radius: f64) -> f64 {
40    let c = 299_792_458.0;
41    let G = 6.674e-11;
42    let rs = 2.0 * G * earth_mass / (c * c);
43
44    // GR: rate at orbit vs surface
45    let gr_surface = schwarzschild_time_dilation(earth_radius, rs);
46    let gr_orbit = schwarzschild_time_dilation(orbit_radius, rs);
47    // Fractional GR difference (orbit clock runs faster)
48    let gr_frac = gr_orbit / gr_surface - 1.0;
49
50    // SR: orbital velocity
51    let v_orbit = (G * earth_mass / orbit_radius).sqrt();
52    let beta = v_orbit / c;
53    // SR time dilation (moving clock runs slow)
54    let sr_frac = -0.5 * beta * beta; // to first order
55
56    let total_frac = gr_frac + sr_frac;
57    total_frac * 86400.0 // seconds per day
58}
59
60/// 2D grid of time dilation factors around a mass.
61#[derive(Debug, Clone)]
62pub struct GravTimeDilationField {
63    pub width: usize,
64    pub height: usize,
65    pub center: Vec2,
66    pub rs: f64,
67    pub factors: Vec<f64>,
68    pub cell_size: f32,
69}
70
71impl GravTimeDilationField {
72    pub fn new(width: usize, height: usize, center: Vec2, mass: f64, c: f64, g_const: f64, cell_size: f32) -> Self {
73        let rs = 2.0 * g_const * mass / (c * c);
74        let mut factors = Vec::with_capacity(width * height);
75        for iy in 0..height {
76            for ix in 0..width {
77                let x = (ix as f32 - width as f32 / 2.0) * cell_size + center.x;
78                let y = (iy as f32 - height as f32 / 2.0) * cell_size + center.y;
79                let r = ((x - center.x).powi(2) + (y - center.y).powi(2)).sqrt() as f64;
80                factors.push(schwarzschild_time_dilation(r, rs));
81            }
82        }
83        Self { width, height, center, rs, factors, cell_size }
84    }
85
86    /// Get the time dilation factor at a grid position.
87    pub fn get(&self, ix: usize, iy: usize) -> f64 {
88        if ix < self.width && iy < self.height {
89            self.factors[iy * self.width + ix]
90        } else {
91            1.0
92        }
93    }
94
95    /// Sample the time dilation factor at an arbitrary position.
96    pub fn sample(&self, pos: Vec2) -> f64 {
97        let r = (pos - self.center).length() as f64;
98        schwarzschild_time_dilation(r, self.rs)
99    }
100
101    /// Get the Schwarzschild radius.
102    pub fn schwarzschild_radius(&self) -> f64 {
103        self.rs
104    }
105
106    /// Find the radius where time dilation equals a given factor.
107    pub fn radius_for_factor(&self, factor: f64) -> f64 {
108        // factor = sqrt(1 - rs/r) => factor^2 = 1 - rs/r => r = rs / (1 - factor^2)
109        if factor >= 1.0 {
110            return f64::INFINITY;
111        }
112        if factor <= 0.0 {
113            return self.rs;
114        }
115        self.rs / (1.0 - factor * factor)
116    }
117}
118
119/// Render clocks with tick rates adjusted by local gravitational time dilation.
120#[derive(Debug, Clone)]
121pub struct GravTimeRenderer {
122    pub field: GravTimeDilationField,
123    pub clock_size: f32,
124}
125
126impl GravTimeRenderer {
127    pub fn new(field: GravTimeDilationField) -> Self {
128        Self {
129            field,
130            clock_size: 1.0,
131        }
132    }
133
134    /// Get the tick rate at a given position (0 to 1, where 0 = frozen at horizon).
135    pub fn tick_rate_at(&self, pos: Vec2) -> f32 {
136        self.field.sample(pos) as f32
137    }
138
139    /// Compute clock hand angle at a position given coordinate time.
140    pub fn clock_angle_at(&self, pos: Vec2, coordinate_time: f64) -> f32 {
141        let rate = self.field.sample(pos);
142        let proper_time = coordinate_time * rate;
143        let seconds = proper_time % 60.0;
144        (seconds / 60.0 * std::f64::consts::TAU) as f32
145    }
146
147    /// Generate clock visualization data for multiple positions.
148    pub fn render_clocks(
149        &self,
150        positions: &[Vec2],
151        coordinate_time: f64,
152    ) -> Vec<(Vec2, f32, f32)> {
153        // Returns (position, hand_angle, tick_rate)
154        positions.iter().map(|pos| {
155            let rate = self.field.sample(*pos);
156            let angle = self.clock_angle_at(*pos, coordinate_time);
157            (*pos, angle, rate as f32)
158        }).collect()
159    }
160
161    /// Compute the color for a clock based on its time dilation.
162    /// Blue = fast (far from mass), red = slow (near mass).
163    pub fn clock_color(&self, pos: Vec2) -> Vec4 {
164        let rate = self.field.sample(pos) as f32;
165        Vec4::new(1.0 - rate, 0.2, rate, 1.0)
166    }
167}
168
169/// Compute the Shapiro time delay for a signal passing near a mass.
170/// Extra delay = (4GM/c^3) * ln(4 * r1 * r2 / b^2)
171/// where r1, r2 are distances of emitter/receiver from the mass, b is closest approach.
172#[allow(non_snake_case)]
173pub fn shapiro_delay(mass: f64, r1: f64, r2: f64, b: f64, G: f64, c: f64) -> f64 {
174    if b <= 0.0 {
175        return f64::INFINITY;
176    }
177    let factor = 4.0 * G * mass / (c * c * c);
178    factor * (4.0 * r1 * r2 / (b * b)).ln()
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    const C: f64 = 299_792_458.0;
186    const G: f64 = 6.674e-11;
187
188    #[test]
189    fn test_schwarzschild_at_infinity() {
190        let factor = schwarzschild_time_dilation(1e20, 1.0);
191        assert!((factor - 1.0).abs() < 1e-10);
192    }
193
194    #[test]
195    fn test_schwarzschild_at_horizon() {
196        let factor = schwarzschild_time_dilation(1.0, 1.0);
197        assert!(factor.abs() < 1e-10, "Should be zero at horizon: {}", factor);
198    }
199
200    #[test]
201    fn test_schwarzschild_inside_horizon() {
202        let factor = schwarzschild_time_dilation(0.5, 1.0);
203        assert_eq!(factor, 0.0, "Inside horizon returns 0");
204    }
205
206    #[test]
207    fn test_gravitational_redshift() {
208        // Light emitted near horizon, observed far away, should be heavily redshifted
209        let rs = 1.0;
210        let ratio = gravitational_redshift(1.1 * rs, 1000.0 * rs, rs);
211        // ratio = sqrt(1-1/1.1) / sqrt(1-1/1000) ~ sqrt(0.0909) / 1 ~ 0.3015
212        assert!(ratio < 1.0, "Should be redshifted: {}", ratio);
213    }
214
215    #[test]
216    fn test_gps_correction_approximately_38us() {
217        let orbit_r = 26_571_000.0; // GPS orbit
218        let earth_mass = 5.972e24;
219        let earth_r = 6_371_000.0;
220        let correction = gps_correction(orbit_r, earth_mass, earth_r);
221        let correction_us = correction * 1e6;
222        assert!(
223            (correction_us - 38.0).abs() < 10.0,
224            "GPS correction: {} us/day, expected ~38",
225            correction_us
226        );
227    }
228
229    #[test]
230    fn test_proper_time_rate() {
231        let earth_mass = 5.972e24;
232        let rate = proper_time_rate(6_371_000.0, earth_mass, G, C);
233        // Should be very close to 1.0
234        assert!((rate - 1.0).abs() < 1e-8);
235        assert!(rate < 1.0, "Surface clock runs slow vs infinity");
236    }
237
238    #[test]
239    fn test_grav_time_field() {
240        let field = GravTimeDilationField::new(
241            20, 20, Vec2::ZERO, 1e30, C, G, 100.0,
242        );
243        // Center should have some dilation
244        let center_factor = field.get(10, 10);
245        // Far corner should be closer to 1
246        let corner_factor = field.get(0, 0);
247        // Both should be positive
248        assert!(center_factor >= 0.0);
249        assert!(corner_factor >= 0.0);
250    }
251
252    #[test]
253    fn test_radius_for_factor() {
254        let field = GravTimeDilationField::new(10, 10, Vec2::ZERO, 1e30, C, G, 1.0);
255        let r = field.radius_for_factor(0.5);
256        // Verify: sqrt(1 - rs/r) = 0.5 => 1 - rs/r = 0.25 => r = rs/0.75
257        let expected = field.rs / 0.75;
258        assert!((r - expected).abs() / expected < 1e-10);
259    }
260
261    #[test]
262    fn test_shapiro_delay_positive() {
263        let m_sun = 1.989e30;
264        let delay = shapiro_delay(m_sun, 1.5e11, 1.5e11, 6.96e8, G, C);
265        // Should be positive and on the order of ~200 microseconds for the sun
266        assert!(delay > 0.0);
267        let delay_us = delay * 1e6;
268        assert!(delay_us > 100.0 && delay_us < 500.0,
269            "Shapiro delay: {} us", delay_us);
270    }
271
272    #[test]
273    fn test_grav_time_renderer() {
274        let field = GravTimeDilationField::new(10, 10, Vec2::ZERO, 1e30, C, G, 1000.0);
275        let renderer = GravTimeRenderer::new(field);
276        let rate = renderer.tick_rate_at(Vec2::new(1000.0, 0.0));
277        assert!(rate > 0.0 && rate <= 1.0);
278    }
279}