1use crate::astro::constants::time::{DAYS_PER_JULIAN_CENTURY, J2000_JD};
13use crate::astro::constants::units::ARCSEC_TO_RAD;
14use crate::astro::data::iau2000a::*;
15use crate::astro::math::mat3::Mat3;
16
17use std::f64::consts::PI;
18
19const TAU: f64 = 2.0 * PI;
20const ASEC360: f64 = 1_296_000.0;
21const TENTH_USEC_2_RAD: f64 = ARCSEC_TO_RAD / 1.0e7;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
25pub enum NutationError {
26 #[error("invalid nutation {field}: {reason}")]
28 InvalidInput {
29 field: &'static str,
30 reason: &'static str,
31 },
32}
33
34fn invalid_input(field: &'static str, reason: &'static str) -> NutationError {
35 NutationError::InvalidInput { field, reason }
36}
37
38fn validate_finite(field: &'static str, value: f64) -> Result<(), NutationError> {
39 if value.is_finite() {
40 Ok(())
41 } else {
42 Err(invalid_input(field, "must be finite"))
43 }
44}
45
46fn validate_finite_values<const N: usize>(
47 field: &'static str,
48 values: [f64; N],
49) -> Result<[f64; N], NutationError> {
50 if values.iter().all(|value| value.is_finite()) {
51 Ok(values)
52 } else {
53 Err(invalid_input(field, "components must be finite"))
54 }
55}
56
57fn positive_fmod(value: f64, modulus: f64) -> f64 {
62 let mut result = value % modulus;
63 if result < 0.0 {
64 result += modulus;
65 }
66 result
67}
68
69pub fn skyfield_fundamental_arguments(t: f64) -> Result<[f64; 5], NutationError> {
76 validate_finite("t", t)?;
77 validate_finite_values(
78 "fundamental_arguments",
79 skyfield_fundamental_arguments_unchecked(t),
80 )
81}
82
83fn skyfield_fundamental_arguments_unchecked(t: f64) -> [f64; 5] {
84 const FA0: [f64; 5] = [
85 485868.249036,
86 1287104.79305,
87 335779.526232,
88 1072260.70369,
89 450160.398036,
90 ];
91 const FA1: [f64; 5] = [
92 1717915923.2178,
93 129596581.0481,
94 1739527262.8478,
95 1602961601.2090,
96 -6962890.5431,
97 ];
98 const FA2: [f64; 5] = [31.8792, -0.5532, -12.7512, -6.3706, 7.4722];
99 const FA3: [f64; 5] = [0.051635, 0.000136, -0.001037, 0.006593, 0.007702];
100 const FA4: [f64; 5] = [
101 -0.00024470,
102 -0.00001149,
103 0.00000417,
104 -0.00003169,
105 -0.00005939,
106 ];
107
108 let mut args = [0.0_f64; 5];
109 for i in 0..5 {
110 let mut value = FA4[i] * t;
111 value += FA3[i];
112 value *= t;
113 value += FA2[i];
114 value *= t;
115 value += FA1[i];
116 value *= t;
117 value += FA0[i];
118 value %= ASEC360;
119 args[i] = value * ARCSEC_TO_RAD;
120 }
121 args
122}
123
124pub fn skyfield_iau2000a_radians(jd_tt: f64) -> Result<(f64, f64), NutationError> {
133 validate_finite("jd_tt", jd_tt)?;
134 let values = skyfield_iau2000a_radians_unchecked(jd_tt);
135 validate_finite("dpsi", values.0)?;
136 validate_finite("deps", values.1)?;
137 Ok(values)
138}
139
140pub(crate) fn skyfield_iau2000a_radians_unchecked(jd_tt: f64) -> (f64, f64) {
141 const ANOMALY_CONSTANT: [f64; 14] = [
142 2.35555598,
143 6.24006013,
144 1.627905234,
145 5.198466741,
146 2.18243920,
147 4.402608842,
148 3.176146697,
149 1.753470314,
150 6.203480913,
151 0.599546497,
152 0.874016757,
153 5.481293871,
154 5.321159000,
155 0.02438175,
156 ];
157 const ANOMALY_COEFFICIENT: [f64; 14] = [
158 8328.6914269554,
159 628.301955,
160 8433.466158131,
161 7771.3771468121,
162 -33.757045,
163 2608.7903141574,
164 1021.3285546211,
165 628.3075849991,
166 334.0612426700,
167 52.9690962641,
168 21.3299104960,
169 7.4781598567,
170 3.8127774000,
171 0.00000538691,
172 ];
173
174 let t = (jd_tt - J2000_JD) / DAYS_PER_JULIAN_CENTURY;
175
176 let fundamental_args = skyfield_fundamental_arguments_unchecked(t);
177
178 let mut dpsi = 0.0_f64;
179 let mut deps = 0.0_f64;
180
181 for row in 0..678 {
183 let mut arg = 0.0_f64;
184 for i in 0..5 {
185 arg += (NALS_T[row][i] as f64) * fundamental_args[i];
186 }
187
188 let sarg = arg.sin();
189 let carg = arg.cos();
190
191 dpsi += sarg * LUNISOLAR_LONGITUDE_COEFFICIENTS[row][0];
192 dpsi += sarg * LUNISOLAR_LONGITUDE_COEFFICIENTS[row][1] * t;
193 dpsi += carg * LUNISOLAR_LONGITUDE_COEFFICIENTS[row][2];
194
195 deps += carg * LUNISOLAR_OBLIQUITY_COEFFICIENTS[row][0];
196 deps += carg * LUNISOLAR_OBLIQUITY_COEFFICIENTS[row][1] * t;
197 deps += sarg * LUNISOLAR_OBLIQUITY_COEFFICIENTS[row][2];
198 }
199
200 let mut planetary_args = [0.0_f64; 14];
202 for i in 0..14 {
203 planetary_args[i] = ANOMALY_CONSTANT[i] + ANOMALY_COEFFICIENT[i] * t;
204 }
205 planetary_args[13] *= t;
206
207 for row in 0..687 {
209 let mut arg = 0.0_f64;
210 for i in 0..14 {
211 arg += (NAPL_T[row][i] as f64) * planetary_args[i];
212 }
213
214 let sarg = arg.sin();
215 let carg = arg.cos();
216
217 dpsi += sarg * NUTATION_COEFFICIENTS_LONGITUDE[row][0];
218 dpsi += carg * NUTATION_COEFFICIENTS_LONGITUDE[row][1];
219
220 deps += sarg * NUTATION_COEFFICIENTS_OBLIQUITY[row][0];
221 deps += carg * NUTATION_COEFFICIENTS_OBLIQUITY[row][1];
222 }
223
224 (dpsi * TENTH_USEC_2_RAD, deps * TENTH_USEC_2_RAD)
225}
226
227pub fn skyfield_mean_obliquity_radians(jd_tdb: f64) -> Result<f64, NutationError> {
233 validate_finite("jd_tdb", jd_tdb)?;
234 let value = skyfield_mean_obliquity_radians_unchecked(jd_tdb);
235 validate_finite("mean_obliquity_radians", value)?;
236 Ok(value)
237}
238
239pub(crate) fn skyfield_mean_obliquity_radians_unchecked(jd_tdb: f64) -> f64 {
240 let t = (jd_tdb - J2000_JD) / DAYS_PER_JULIAN_CENTURY;
241 let epsilon = ((((-0.0000000434 * t - 0.000000576) * t + 0.00200340) * t - 0.0001831) * t
242 - 46.836769)
243 * t
244 + 84381.406;
245 epsilon * ARCSEC_TO_RAD
246}
247
248pub fn build_skyfield_nutation_matrix(
255 mean_obliquity_radians: f64,
256 true_obliquity_radians: f64,
257 psi_radians: f64,
258) -> Result<Mat3, NutationError> {
259 validate_finite("mean_obliquity_radians", mean_obliquity_radians)?;
260 validate_finite("true_obliquity_radians", true_obliquity_radians)?;
261 validate_finite("psi_radians", psi_radians)?;
262 validate_mat3(
263 "nutation_matrix",
264 build_skyfield_nutation_matrix_unchecked(
265 mean_obliquity_radians,
266 true_obliquity_radians,
267 psi_radians,
268 ),
269 )
270}
271
272pub(crate) fn build_skyfield_nutation_matrix_unchecked(
273 mean_obliquity_radians: f64,
274 true_obliquity_radians: f64,
275 psi_radians: f64,
276) -> Mat3 {
277 let cobm = mean_obliquity_radians.cos();
278 let sobm = mean_obliquity_radians.sin();
279 let cobt = true_obliquity_radians.cos();
280 let sobt = true_obliquity_radians.sin();
281 let cpsi = psi_radians.cos();
282 let spsi = psi_radians.sin();
283
284 [
285 [cpsi, -spsi * cobm, -spsi * sobm],
286 [
287 spsi * cobt,
288 cpsi * cobm * cobt + sobm * sobt,
289 cpsi * sobm * cobt - cobm * sobt,
290 ],
291 [
292 spsi * sobt,
293 cpsi * cobm * sobt - sobm * cobt,
294 cpsi * sobm * sobt + cobm * cobt,
295 ],
296 ]
297}
298
299fn validate_mat3(field: &'static str, mat: Mat3) -> Result<Mat3, NutationError> {
300 if mat.iter().flatten().all(|value| value.is_finite()) {
301 Ok(mat)
302 } else {
303 Err(invalid_input(field, "components must be finite"))
304 }
305}
306
307pub fn skyfield_equation_of_the_equinoxes_complimentary_terms(
315 jd_tt: f64,
316) -> Result<f64, NutationError> {
317 validate_finite("jd_tt", jd_tt)?;
318 let value = skyfield_equation_of_the_equinoxes_complimentary_terms_unchecked(jd_tt);
319 validate_finite("equation_of_the_equinoxes_terms", value)?;
320 Ok(value)
321}
322
323pub(crate) fn skyfield_equation_of_the_equinoxes_complimentary_terms_unchecked(jd_tt: f64) -> f64 {
324 const KE1: [i32; 14] = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
325
326 #[rustfmt::skip]
327 const KE0_T: [[i32; 14]; 33] = [
328 [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
329 [0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
330 [0, 0, 2, -2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
331 [0, 0, 2, -2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
332 [0, 0, 2, -2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
333 [0, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
334 [0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
335 [0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
336 [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
337 [0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
338 [1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
339 [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
340 [0, 1, 2, -2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
341 [0, 1, 2, -2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
342 [0, 0, 4, -4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0],
343 [0, 0, 1, -1, 1, 0, -8, 12, 0, 0, 0, 0, 0, 0],
344 [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
345 [0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
346 [1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
347 [1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
348 [0, 0, 2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
349 [0, 1, -2, 2, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
350 [0, 1, -2, 2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
351 [0, 0, 0, 0, 0, 0, 8, -13, 0, 0, 0, 0, 0, -1],
352 [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
353 [2, 0, -2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
354 [1, 0, 0, -2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
355 [0, 1, 2, -2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
356 [1, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
357 [0, 0, 4, -2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0],
358 [0, 0, 2, -2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0],
359 [1, 0, -2, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0],
360 [1, 0, -2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
361 ];
362
363 #[rustfmt::skip]
364 const SE0_T_0: [f64; 33] = [
365 0.00264096, 0.00006352, 0.00001175, 0.00001121, -0.00000455, 0.00000202,
366 0.00000198, -0.00000172, -0.00000141, -0.00000126, -0.00000063, -0.00000063,
367 0.00000046, 0.00000045, 0.00000036, -0.00000024, 0.00000032, 0.00000028,
368 0.00000027, 0.00000026, -0.00000021, 0.00000019, 0.00000018, -0.00000010,
369 0.00000015, -0.00000014, 0.00000014, -0.00000014, 0.00000014, 0.00000013,
370 -0.00000011, 0.00000011, 0.00000011,
371 ];
372
373 #[rustfmt::skip]
374 const SE0_T_1: [f64; 33] = [
375 -0.00000039, -0.00000002, 0.00000001, 0.00000001, 0.0, 0.0,
376 0.0, 0.0, -0.00000001, -0.00000001, 0.0, 0.0,
377 0.0, 0.0, 0.0, -0.00000012, 0.0, 0.0,
378 0.0, 0.0, 0.0, 0.0, 0.0, 0.00000005,
379 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
380 0.0, 0.0, 0.0,
381 ];
382
383 const SE1_0: f64 = -0.00000087;
384
385 let t = (jd_tt - J2000_JD) / DAYS_PER_JULIAN_CENTURY;
386
387 let mut fa = [0.0_f64; 14];
390
391 fa[0] = (485868.249036
392 + (715923.2178 + (31.8792 + (0.051635 + (-0.00024470) * t) * t) * t) * t)
393 * ARCSEC_TO_RAD
394 + positive_fmod(1325.0 * t, 1.0) * TAU;
395
396 fa[1] = (1287104.793048
397 + (1292581.0481 + (-0.5532 + (0.000136 + (-0.00001149) * t) * t) * t) * t)
398 * ARCSEC_TO_RAD
399 + positive_fmod(99.0 * t, 1.0) * TAU;
400
401 fa[2] = (335779.526232
402 + (295262.8478 + (-12.7512 + (-0.001037 + (0.00000417) * t) * t) * t) * t)
403 * ARCSEC_TO_RAD
404 + positive_fmod(1342.0 * t, 1.0) * TAU;
405
406 fa[3] = (1072260.703692
407 + (1105601.2090 + (-6.3706 + (0.006593 + (-0.00003169) * t) * t) * t) * t)
408 * ARCSEC_TO_RAD
409 + positive_fmod(1236.0 * t, 1.0) * TAU;
410
411 fa[4] = (450160.398036
412 + (-482890.5431 + (7.4722 + (0.007702 + (-0.00005939) * t) * t) * t) * t)
413 * ARCSEC_TO_RAD
414 + positive_fmod(-5.0 * t, 1.0) * TAU;
415
416 fa[5] = 4.402608842 + 2608.7903141574 * t;
417 fa[6] = 3.176146697 + 1021.3285546211 * t;
418 fa[7] = 1.753470314 + 628.3075849991 * t;
419 fa[8] = 6.203480913 + 334.0612426700 * t;
420 fa[9] = 0.599546497 + 52.9690962641 * t;
421 fa[10] = 0.874016757 + 21.3299104960 * t;
422 fa[11] = 5.481293872 + 7.4781598567 * t;
423 fa[12] = 5.311886287 + 3.8133035638 * t;
424 fa[13] = (0.024381750 + 0.00000538691 * t) * t;
425
426 for angle in &mut fa {
427 *angle = positive_fmod(*angle, TAU);
428 }
429
430 let mut a = 0.0_f64;
432 for i in 0..14 {
433 a += (KE1[i] as f64) * fa[i];
434 }
435 let mut c_terms = SE1_0 * a.sin();
436 c_terms *= t;
437
438 for row in 0..33 {
440 let mut arg = 0.0_f64;
441 for i in 0..14 {
442 arg += (KE0_T[row][i] as f64) * fa[i];
443 }
444 c_terms += SE0_T_0[row] * arg.sin();
445 c_terms += SE0_T_1[row] * arg.cos();
446 }
447
448 c_terms * ARCSEC_TO_RAD
449}
450
451#[cfg(test)]
452mod tests {
453 use super::{
454 skyfield_fundamental_arguments, skyfield_iau2000a_radians, skyfield_mean_obliquity_radians,
455 NutationError,
456 };
457
458 #[test]
459 fn public_nutation_helpers_reject_non_finite_times() {
460 assert_eq!(
461 skyfield_fundamental_arguments(f64::NAN),
462 Err(NutationError::InvalidInput {
463 field: "t",
464 reason: "must be finite"
465 })
466 );
467 assert_eq!(
468 skyfield_iau2000a_radians(f64::INFINITY),
469 Err(NutationError::InvalidInput {
470 field: "jd_tt",
471 reason: "must be finite"
472 })
473 );
474 assert_eq!(
475 skyfield_mean_obliquity_radians(f64::NEG_INFINITY),
476 Err(NutationError::InvalidInput {
477 field: "jd_tdb",
478 reason: "must be finite"
479 })
480 );
481 }
482}