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
114pub fn apply_light_deflection(
127 star_direction: Vector3,
128 sun_to_observer: Vector3,
129 sun_observer_distance_au: f64,
130) -> Vector3 {
131 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 let q = star_direction;
139 let e = sun_to_observer;
140
141 let qpe = Vector3::new(q.x + e.x, q.y + e.y, q.z + e.z);
143
144 let qdqpe = q.dot(&qpe);
146
147 let qdqpe_limited = if qdqpe > dlim { qdqpe } else { dlim };
149
150 let w = SCHWARZSCHILD_RADIUS_SUN_AU / sun_observer_distance_au / qdqpe_limited;
153
154 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 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 Vector3::new(p.x + w * peq.x, p.y + w * peq.y, p.z + w * peq.z)
171}
172
173pub fn remove_light_deflection(
175 deflected_direction: Vector3,
176 sun_to_observer: Vector3,
177 sun_observer_distance_au: f64,
178) -> Vector3 {
179 let mut d = Vector3::zeros();
181
182 for _ in 0..5 {
183 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 let after = apply_light_deflection(before, sun_to_observer, sun_observer_distance_au);
193
194 d = Vector3::new(after.x - before.x, after.y - before.y, after.z - before.z);
196 }
197
198 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
207pub fn remove_aberration(
209 apparent_direction: Vector3,
210 velocity_au_day: Vector3,
211 sun_earth_distance_au: f64,
212) -> Vector3 {
213 let mut d = Vector3::zeros();
215
216 for _ in 0..2 {
217 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 let after = apply_aberration(before, velocity_au_day, sun_earth_distance_au);
227
228 d = Vector3::new(after.x - before.x, after.y - before.y, after.z - before.z);
230 }
231
232 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 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 assert!(
350 diff < 1e-12,
351 "Aberration inverse should be within iterative precision: {:.2e}",
352 diff
353 );
354 }
355}