Skip to main content

celestial_coords/
lighttime.rs

1use crate::Distance;
2use celestial_core::Vector3;
3
4const C_AU_PER_DAY: f64 = 173.1446326846693;
5
6pub struct LightTimeCorrection {
7    light_time_days: f64,
8}
9
10impl LightTimeCorrection {
11    pub fn from_distance(distance: Distance) -> Self {
12        let distance_au = distance.au();
13        let light_time_days = distance_au / C_AU_PER_DAY;
14
15        Self { light_time_days }
16    }
17
18    pub fn from_position_vector(pos: Vector3) -> Self {
19        let distance_au = libm::sqrt(pos.x.powi(2) + pos.y.powi(2) + pos.z.powi(2));
20        let light_time_days = distance_au / C_AU_PER_DAY;
21
22        Self { light_time_days }
23    }
24
25    pub fn light_time_days(&self) -> f64 {
26        self.light_time_days
27    }
28
29    pub fn light_time_seconds(&self) -> f64 {
30        self.light_time_days * celestial_core::constants::SECONDS_PER_DAY_F64
31    }
32
33    pub fn apply_proper_motion(
34        &self,
35        pm_ra_mas_per_year: f64,
36        pm_dec_mas_per_year: f64,
37    ) -> (f64, f64) {
38        let years = self.light_time_days / 365.25;
39
40        let delta_ra_mas = pm_ra_mas_per_year * years;
41        let delta_dec_mas = pm_dec_mas_per_year * years;
42
43        (delta_ra_mas, delta_dec_mas)
44    }
45
46    pub fn apply_radial_velocity(&self, radial_velocity_km_s: f64) -> f64 {
47        radial_velocity_km_s * self.light_time_seconds()
48    }
49
50    /// Corrects a position vector for light-time delay.
51    ///
52    /// # Arguments
53    /// * `pos` - Position vector in AU
54    /// * `vel` - Velocity vector in AU/day
55    ///
56    /// # Returns
57    /// Corrected position vector in AU
58    pub fn correct_position_vector(pos: Vector3, vel: Vector3) -> Vector3 {
59        let lt = Self::from_position_vector(pos);
60        let t = lt.light_time_days();
61
62        Vector3::new(pos.x - vel.x * t, pos.y - vel.y * t, pos.z - vel.z * t)
63    }
64
65    /// Iteratively corrects a position vector for light-time delay with convergence checking.
66    ///
67    /// # Arguments
68    /// * `pos` - Position vector in AU
69    /// * `vel` - Velocity vector in AU/day
70    /// * `max_iterations` - Maximum number of iterations
71    ///
72    /// # Returns
73    /// Converged position vector in AU
74    pub fn iterate_correction(pos: Vector3, vel: Vector3, max_iterations: usize) -> Vector3 {
75        let mut corrected = pos;
76
77        for _ in 0..max_iterations {
78            let lt = Self::from_position_vector(corrected);
79            let t = lt.light_time_days();
80
81            let new_corrected =
82                Vector3::new(pos.x - vel.x * t, pos.y - vel.y * t, pos.z - vel.z * t);
83
84            let delta = libm::sqrt(
85                (new_corrected.x - corrected.x).powi(2)
86                    + (new_corrected.y - corrected.y).powi(2)
87                    + (new_corrected.z - corrected.z).powi(2),
88            );
89
90            corrected = new_corrected;
91
92            if delta < 1e-12 {
93                break;
94            }
95        }
96
97        corrected
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_light_time_from_distance() {
107        let distance = Distance::from_au(1.0).unwrap();
108        let lt = LightTimeCorrection::from_distance(distance);
109
110        assert!((lt.light_time_days() - 1.0 / C_AU_PER_DAY).abs() < 1e-12);
111        assert!((lt.light_time_seconds() - 499.00478).abs() < 1.0);
112    }
113
114    #[test]
115    fn test_proper_motion_correction() {
116        let distance = Distance::from_parsecs(10.0).unwrap();
117        let lt = LightTimeCorrection::from_distance(distance);
118
119        let (delta_ra, delta_dec) = lt.apply_proper_motion(100.0, 50.0);
120
121        assert!(delta_ra > 0.0);
122        assert!(delta_dec > 0.0);
123    }
124
125    #[test]
126    fn test_radial_velocity_correction() {
127        let distance = Distance::from_au(1.0).unwrap();
128        let lt = LightTimeCorrection::from_distance(distance);
129
130        let distance_change_km = lt.apply_radial_velocity(30.0);
131
132        assert!(distance_change_km > 0.0);
133    }
134
135    #[test]
136    fn test_position_vector_correction() {
137        let pos = Vector3::new(1.0, 0.0, 0.0);
138        let vel = Vector3::new(0.1, 0.0, 0.0);
139
140        let corrected = LightTimeCorrection::correct_position_vector(pos, vel);
141
142        assert!(corrected.x < pos.x);
143    }
144
145    #[test]
146    fn test_iterative_correction() {
147        let pos = Vector3::new(5.0, 0.0, 0.0);
148        let vel = Vector3::new(0.01, 0.0, 0.0);
149
150        let corrected = LightTimeCorrection::iterate_correction(pos, vel, 10);
151
152        assert!(corrected.x < pos.x);
153    }
154
155    #[test]
156    fn test_jupiter_light_time() {
157        let jupiter_distance = Distance::from_au(5.2).unwrap();
158        let lt = LightTimeCorrection::from_distance(jupiter_distance);
159
160        let expected_minutes = 5.2 * 499.0 / 60.0;
161        let actual_minutes = lt.light_time_seconds() / 60.0;
162
163        assert!((actual_minutes - expected_minutes).abs() < 1.0);
164    }
165
166    #[test]
167    fn test_romer_jupiter_moons_1676() {
168        let near_opposition = Distance::from_au(5.2 - 1.0).unwrap();
169        let far_opposition = Distance::from_au(5.2 + 1.0).unwrap();
170
171        let lt_near = LightTimeCorrection::from_distance(near_opposition);
172        let lt_far = LightTimeCorrection::from_distance(far_opposition);
173
174        let delay_seconds = lt_far.light_time_seconds() - lt_near.light_time_seconds();
175        let delay_minutes = delay_seconds / 60.0;
176
177        assert!((delay_minutes - 16.6).abs() < 1.0);
178    }
179
180    #[test]
181    fn test_barnards_star_proper_motion() {
182        let distance = Distance::from_parsecs(1.83).unwrap();
183        let lt = LightTimeCorrection::from_distance(distance);
184
185        let pm_ra = -798.58;
186        let pm_dec = 10328.12;
187
188        let (delta_ra, delta_dec) = lt.apply_proper_motion(pm_ra, pm_dec);
189
190        let light_years = lt.light_time_days() / 365.25;
191        assert!((delta_ra - (pm_ra * light_years)).abs() < 0.01);
192        assert!((delta_dec - (pm_dec * light_years)).abs() < 0.01);
193    }
194
195    #[test]
196    fn test_proxima_centauri_radial_velocity() {
197        let distance = Distance::from_parsecs(1.30).unwrap();
198        let lt = LightTimeCorrection::from_distance(distance);
199
200        let rv_km_s = -22.2;
201
202        let distance_change_km = lt.apply_radial_velocity(rv_km_s);
203
204        let expected_km = rv_km_s * lt.light_time_seconds();
205        assert!((distance_change_km - expected_km).abs() < 1e-6);
206    }
207
208    #[test]
209    fn test_sun_earth_light_time() {
210        let earth_sun = Distance::from_au(1.0).unwrap();
211        let lt = LightTimeCorrection::from_distance(earth_sun);
212
213        assert!((lt.light_time_seconds() - 499.0).abs() < 1.0);
214        assert!((lt.light_time_days() - 0.00577).abs() < 0.0001);
215    }
216
217    #[test]
218    fn test_mercury_transit_timing() {
219        let mercury_near = Distance::from_au(0.31).unwrap();
220        let mercury_far = Distance::from_au(0.47).unwrap();
221
222        let lt_near = LightTimeCorrection::from_distance(mercury_near);
223        let lt_far = LightTimeCorrection::from_distance(mercury_far);
224
225        let timing_difference = (lt_far.light_time_seconds() - lt_near.light_time_seconds()).abs();
226
227        assert!(timing_difference > 0.0);
228        assert!(timing_difference < 100.0);
229    }
230
231    #[test]
232    fn test_convergence_high_velocity_object() {
233        let pos = Vector3::new(100.0, 0.0, 0.0);
234        let vel = Vector3::new(1.0, 0.0, 0.0);
235
236        let single = LightTimeCorrection::correct_position_vector(pos, vel);
237        let iterated = LightTimeCorrection::iterate_correction(pos, vel, 10);
238
239        let improvement = (iterated.x - single.x).abs();
240        assert!(improvement > 0.0);
241    }
242
243    #[test]
244    fn test_aberration_effect_simulation() {
245        let distance = Distance::from_au(1.0).unwrap();
246        let lt = LightTimeCorrection::from_distance(distance);
247
248        let earth_orbital_velocity = 29.78;
249        let distance_change_km = lt.apply_radial_velocity(earth_orbital_velocity);
250
251        let distance_change_au = distance_change_km / 1.495978707e8;
252        assert!(distance_change_au.abs() < 0.1);
253    }
254}