Skip to main content

celestial_coords/aberration/
mod.rs

1mod coefficients;
2
3use crate::{CoordError, CoordResult};
4use celestial_core::{
5    constants::{DAYS_PER_JULIAN_YEAR, J2000_JD, SPEED_OF_LIGHT_AU_PER_DAY},
6    Vector3,
7};
8use celestial_time::TT;
9use coefficients::Coefficients;
10
11pub struct EarthState {
12    pub barycentric_velocity: Vector3,
13    pub heliocentric_position: Vector3,
14}
15
16pub fn compute_earth_state(tt: &TT) -> CoordResult<EarthState> {
17    let jd = tt.to_julian_date();
18    let t = ((jd.jd1() - J2000_JD) + jd.jd2()) / DAYS_PER_JULIAN_YEAR;
19
20    if t.abs() > 100.0 {
21        return Err(CoordError::invalid_coordinate("Epoch outside 1900-2100"));
22    }
23
24    let t2 = t * t;
25
26    let e_coefs = [
27        [Coefficients::E0X, Coefficients::E1X, Coefficients::E2X],
28        [Coefficients::E0Y, Coefficients::E1Y, Coefficients::E2Y],
29        [Coefficients::E0Z, Coefficients::E1Z, Coefficients::E2Z],
30    ];
31    let s_coefs = [
32        [Coefficients::S0X, Coefficients::S1X, Coefficients::S2X],
33        [Coefficients::S0Y, Coefficients::S1Y, Coefficients::S2Y],
34        [Coefficients::S0Z, Coefficients::S1Z, Coefficients::S2Z],
35    ];
36
37    let mut ph = [0.0; 3];
38    let mut vh = [0.0; 3];
39    let mut vb = [0.0; 3];
40
41    for i in 0..3 {
42        let mut xyz = 0.0;
43        let mut xyzd = 0.0;
44
45        accumulate_terms(t, t2, e_coefs[i][0], &mut xyz, &mut xyzd, 0);
46        accumulate_terms(t, t2, e_coefs[i][1], &mut xyz, &mut xyzd, 1);
47        accumulate_terms(t, t2, e_coefs[i][2], &mut xyz, &mut xyzd, 2);
48
49        ph[i] = xyz;
50        vh[i] = xyzd / DAYS_PER_JULIAN_YEAR;
51
52        accumulate_terms(t, t2, s_coefs[i][0], &mut xyz, &mut xyzd, 0);
53        accumulate_terms(t, t2, s_coefs[i][1], &mut xyz, &mut xyzd, 1);
54        accumulate_terms(t, t2, s_coefs[i][2], &mut xyz, &mut xyzd, 2);
55
56        vb[i] = xyzd / DAYS_PER_JULIAN_YEAR;
57    }
58
59    let helio_pos_bcrs = rotate_to_bcrs(ph);
60    let bary_vel_bcrs = rotate_to_bcrs(vb);
61
62    Ok(EarthState {
63        barycentric_velocity: Vector3::new(bary_vel_bcrs[0], bary_vel_bcrs[1], bary_vel_bcrs[2]),
64        heliocentric_position: Vector3::new(
65            helio_pos_bcrs[0],
66            helio_pos_bcrs[1],
67            helio_pos_bcrs[2],
68        ),
69    })
70}
71
72fn accumulate_terms(t: f64, t2: f64, coefs: &[[f64; 3]], xyz: &mut f64, xyzd: &mut f64, power: u8) {
73    for &[a, b, c] in coefs {
74        let ct = c * t;
75        let p = b + ct;
76        let (sin_p, cos_p) = libm::sincos(p);
77
78        match power {
79            0 => {
80                *xyz += a * cos_p;
81                *xyzd -= a * c * sin_p;
82            }
83            1 => {
84                *xyz += a * t * cos_p;
85                *xyzd += a * (cos_p - ct * sin_p);
86            }
87            _ => {
88                *xyz += a * t2 * cos_p;
89                *xyzd += a * t * (2.0 * cos_p - ct * sin_p);
90            }
91        }
92    }
93}
94
95fn rotate_to_bcrs(v: [f64; 3]) -> [f64; 3] {
96    const AM12: f64 = 0.000000211284;
97    const AM13: f64 = -0.000000091603;
98    const AM21: f64 = -0.000000230286;
99    const AM22: f64 = 0.917482137087;
100    const AM23: f64 = -0.397776982902;
101    const AM32: f64 = 0.397776982902;
102    const AM33: f64 = 0.917482137087;
103
104    let (x, y, z) = (v[0], v[1], v[2]);
105    [
106        x + AM12 * y + AM13 * z,
107        AM21 * x + AM22 * y + AM23 * z,
108        AM32 * y + AM33 * z,
109    ]
110}
111
112const SCHWARZSCHILD_RADIUS_SUN_AU: f64 = 1.97412574336e-8;
113
114/// Apply gravitational light deflection by the Sun.
115///
116/// This implements the relativistic bending of starlight as it passes near the Sun,
117/// based on the algorithm in IERS Conventions.
118///
119/// # Arguments
120/// * `star_direction` - Unit vector from observer to star (BCRS)
121/// * `sun_to_observer` - Unit vector from Sun to observer (BCRS)
122/// * `sun_observer_distance_au` - Distance from Sun to observer in AU
123///
124/// # Returns
125/// Deflected star direction (unit vector)
126pub fn apply_light_deflection(
127    star_direction: Vector3,
128    sun_to_observer: Vector3,
129    sun_observer_distance_au: f64,
130) -> Vector3 {
131    // Deflection limiter: for nearby observers, use smaller limit
132    let em2 = sun_observer_distance_au * sun_observer_distance_au;
133    let em2_clamped = if em2 < 1.0 { 1.0 } else { em2 };
134    let dlim = 1e-6 / em2_clamped;
135
136    // For distant stars, the direction from Sun to star ≈ direction from observer to star
137    // So we use star_direction for both p and q in ERFA's eraLd
138    let q = star_direction;
139    let e = sun_to_observer;
140
141    // q + e
142    let qpe = Vector3::new(q.x + e.x, q.y + e.y, q.z + e.z);
143
144    // q . (q + e)
145    let qdqpe = q.dot(&qpe);
146
147    // Apply limiter to avoid division by zero when star is near Sun
148    let qdqpe_limited = if qdqpe > dlim { qdqpe } else { dlim };
149
150    // 2G*M / (c^2 * r * (q.(q+e))) = SRS / (em * (q.(q+e)))
151    // where SRS = Schwarzschild radius of Sun in AU
152    let w = SCHWARZSCHILD_RADIUS_SUN_AU / sun_observer_distance_au / qdqpe_limited;
153
154    // e × q (cross product)
155    let eq = Vector3::new(
156        e.y * q.z - e.z * q.y,
157        e.z * q.x - e.x * q.z,
158        e.x * q.y - e.y * q.x,
159    );
160
161    // p × (e × q) (cross product)
162    let p = star_direction;
163    let peq = Vector3::new(
164        p.y * eq.z - p.z * eq.y,
165        p.z * eq.x - p.x * eq.z,
166        p.x * eq.y - p.y * eq.x,
167    );
168
169    // Apply deflection: p1 = p + w * (p × (e × q))
170    Vector3::new(p.x + w * peq.x, p.y + w * peq.y, p.z + w * peq.z)
171}
172
173/// Remove gravitational light deflection by the Sun (inverse operation).
174pub fn remove_light_deflection(
175    deflected_direction: Vector3,
176    sun_to_observer: Vector3,
177    sun_observer_distance_au: f64,
178) -> Vector3 {
179    // We compute: d = forward(estimate) - estimate, then refine: estimate = observed - d
180    let mut d = Vector3::zeros();
181
182    for _ in 0..5 {
183        // Estimate the original direction
184        let before = Vector3::new(
185            deflected_direction.x - d.x,
186            deflected_direction.y - d.y,
187            deflected_direction.z - d.z,
188        )
189        .normalize();
190
191        // Apply forward deflection to the estimate
192        let after = apply_light_deflection(before, sun_to_observer, sun_observer_distance_au);
193
194        // Update correction
195        d = Vector3::new(after.x - before.x, after.y - before.y, after.z - before.z);
196    }
197
198    // Final result
199    Vector3::new(
200        deflected_direction.x - d.x,
201        deflected_direction.y - d.y,
202        deflected_direction.z - d.z,
203    )
204    .normalize()
205}
206
207/// Remove stellar aberration (inverse operation).
208pub fn remove_aberration(
209    apparent_direction: Vector3,
210    velocity_au_day: Vector3,
211    sun_earth_distance_au: f64,
212) -> Vector3 {
213    // Iterative approach matching ERFA's eraAticq
214    let mut d = Vector3::zeros();
215
216    for _ in 0..2 {
217        // Estimate the original direction
218        let before = Vector3::new(
219            apparent_direction.x - d.x,
220            apparent_direction.y - d.y,
221            apparent_direction.z - d.z,
222        )
223        .normalize();
224
225        // Apply forward aberration to the estimate
226        let after = apply_aberration(before, velocity_au_day, sun_earth_distance_au);
227
228        // Update correction
229        d = Vector3::new(after.x - before.x, after.y - before.y, after.z - before.z);
230    }
231
232    // Final result
233    Vector3::new(
234        apparent_direction.x - d.x,
235        apparent_direction.y - d.y,
236        apparent_direction.z - d.z,
237    )
238    .normalize()
239}
240
241pub fn apply_aberration(
242    direction: Vector3,
243    velocity_au_day: Vector3,
244    sun_earth_distance_au: f64,
245) -> Vector3 {
246    let v = Vector3::new(
247        velocity_au_day.x / SPEED_OF_LIGHT_AU_PER_DAY,
248        velocity_au_day.y / SPEED_OF_LIGHT_AU_PER_DAY,
249        velocity_au_day.z / SPEED_OF_LIGHT_AU_PER_DAY,
250    );
251
252    let v2 = v.x * v.x + v.y * v.y + v.z * v.z;
253    let bm1 = libm::sqrt(1.0 - v2);
254
255    let pdv = direction.dot(&v);
256    let w1 = 1.0 + pdv / (1.0 + bm1);
257    let w2 = SCHWARZSCHILD_RADIUS_SUN_AU / sun_earth_distance_au;
258
259    let p2 = Vector3::new(
260        direction.x * bm1 + w1 * v.x + w2 * (v.x - pdv * direction.x),
261        direction.y * bm1 + w1 * v.y + w2 * (v.y - pdv * direction.y),
262        direction.z * bm1 + w1 * v.z + w2 * (v.z - pdv * direction.z),
263    );
264
265    let r = p2.magnitude();
266    Vector3::new(p2.x / r, p2.y / r, p2.z / r)
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_earth_velocity_j2000() {
275        let tt = TT::j2000();
276        let state = compute_earth_state(&tt).unwrap();
277
278        let v_mag = state.barycentric_velocity.magnitude();
279        assert!(
280            (v_mag - 0.017).abs() < 0.001,
281            "Barycentric velocity should be ~0.017 AU/day, got {}",
282            v_mag
283        );
284    }
285
286    #[test]
287    fn test_aberration_magnitude() {
288        let tt = TT::j2000();
289        let state = compute_earth_state(&tt).unwrap();
290
291        let p = Vector3::new(1.0, 0.0, 0.0);
292        let s = state.heliocentric_position.magnitude();
293        let p_ab = apply_aberration(p, state.barycentric_velocity, s);
294
295        let disp_sq = (p_ab.x - p.x).powi(2) + (p_ab.y - p.y).powi(2) + (p_ab.z - p.z).powi(2);
296        let aberr_arcsec = libm::sqrt(disp_sq) * 206264.806247;
297
298        assert!(
299            aberr_arcsec < 25.0,
300            "Aberration should be < 25\", got {}",
301            aberr_arcsec
302        );
303    }
304
305    #[test]
306    fn test_aberration_roundtrip() {
307        let tt = TT::j2000();
308        let state = compute_earth_state(&tt).unwrap();
309        let sun_dist = state.heliocentric_position.magnitude();
310
311        let directions = [
312            Vector3::new(1.0, 0.0, 0.0),
313            Vector3::new(0.0, 1.0, 0.0),
314            Vector3::new(0.0, 0.0, 1.0),
315            Vector3::new(1.0, 1.0, 1.0).normalize(),
316        ];
317
318        for dir in directions {
319            let aberrated = apply_aberration(dir, state.barycentric_velocity, sun_dist);
320            let recovered = remove_aberration(aberrated, state.barycentric_velocity, sun_dist);
321
322            let diff = libm::sqrt(
323                (dir.x - recovered.x).powi(2)
324                    + (dir.y - recovered.y).powi(2)
325                    + (dir.z - recovered.z).powi(2),
326            );
327
328            // Iterative inverse gives ~1e-13 precision, not machine epsilon
329            assert!(diff < 1e-12, "Aberration roundtrip error: {:.2e}", diff);
330        }
331    }
332
333    #[test]
334    fn test_remove_aberration_is_inverse() {
335        let velocity = Vector3::new(0.01, 0.005, 0.002);
336        let sun_dist = 1.0;
337        let original = Vector3::new(0.6, 0.7, 0.3).normalize();
338
339        let aberrated = apply_aberration(original, velocity, sun_dist);
340        let recovered = remove_aberration(aberrated, velocity, sun_dist);
341
342        let diff = libm::sqrt(
343            (original.x - recovered.x).powi(2)
344                + (original.y - recovered.y).powi(2)
345                + (original.z - recovered.z).powi(2),
346        );
347
348        // Iterative inverse gives ~1e-13 precision, not machine epsilon
349        assert!(
350            diff < 1e-12,
351            "Aberration inverse should be within iterative precision: {:.2e}",
352            diff
353        );
354    }
355}