use super::{mean_pole_arcsec, solid_earth_pole_tide};
use crate::constants::DAYS_PER_JULIAN_YEAR;
use crate::tides::{TideError, TideInputErrorKind};
const ZIM2_MEAN_POLE_X_ARCSEC: f64 = 0.099_210_452_943_189_61;
const ZIM2_MEAN_POLE_Y_ARCSEC: f64 = 0.411_715_365_046_771_64;
const E2003_MEAN_POLE_X_ARCSEC: f64 = 0.060_862_039_014_373_72;
const E2003_MEAN_POLE_Y_ARCSEC: f64 = 0.332_594_606_433_949_4;
const E2031_MEAN_POLE_X_ARCSEC: f64 = 0.108_472_357_888_432_58;
const E2031_MEAN_POLE_Y_ARCSEC: f64 = 0.430_824_602_441_250_26;
const ZIM2_ECEF_M: [f64; 3] = [
4_331_299.584_071_246,
567_537.707_032_023_1,
4_633_133.964_520_6,
];
const ZIM2_XP_ARCSEC: f64 = 0.169_051;
const ZIM2_YP_ARCSEC: f64 = 0.411_760;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
fn ecef_to_runeu(xsta: &[f64; 3], d: &[f64; 3]) -> (f64, f64, f64) {
let r = (xsta[0] * xsta[0] + xsta[1] * xsta[1] + xsta[2] * xsta[2]).sqrt();
let lon = xsta[1].atan2(xsta[0]);
let lat_gc = (xsta[2] / r).asin();
let (sinlat, coslat) = lat_gc.sin_cos();
let (sinlon, coslon) = lon.sin_cos();
let up_hat = [coslat * coslon, coslat * sinlon, sinlat];
let east_hat = [-sinlon, coslon, 0.0];
let north_hat = [-sinlat * coslon, -sinlat * sinlon, coslat];
let dot = |a: &[f64; 3]| a[0] * d[0] + a[1] * d[1] + a[2] * d[2];
(dot(&up_hat), dot(&north_hat), dot(&east_hat))
}
fn eq724_latitude_form(
xsta: &[f64; 3],
x_bar_arcsec: f64,
y_bar_arcsec: f64,
xp_arcsec: f64,
yp_arcsec: f64,
) -> (f64, f64, f64) {
let r = (xsta[0] * xsta[0] + xsta[1] * xsta[1] + xsta[2] * xsta[2]).sqrt();
let lon = xsta[1].atan2(xsta[0]);
let lat = (xsta[2] / r).asin();
let m1 = xp_arcsec - x_bar_arcsec;
let m2 = -(yp_arcsec - y_bar_arcsec);
let (sinlon, coslon) = lon.sin_cos();
let radial = m1 * coslon + m2 * sinlon;
let lambda = m1 * sinlon - m2 * coslon;
let up = -33.0e-3 * (2.0 * lat).sin() * radial;
let north = -9.0e-3 * (2.0 * lat).cos() * radial;
let east = 9.0e-3 * lat.sin() * lambda;
(up, north, east)
}
#[test]
fn mean_pole_secular_matches_iers_2018_coefficients() {
let (x_bar, y_bar) = mean_pole_arcsec(2000, 1, 1, 12.0);
assert!(approx_eq(x_bar, 0.055_0, 1.0e-12), "x_bar = {x_bar}");
assert!(approx_eq(y_bar, 0.320_5, 1.0e-12), "y_bar = {y_bar}");
let (x1, y1) = mean_pole_arcsec(2001, 1, 1, 12.0);
let step_yr = 366.0 / DAYS_PER_JULIAN_YEAR;
assert!(
approx_eq((x1 - x_bar) * 1000.0, 1.677 * step_yr, 1.0e-9),
"x rate {}",
(x1 - x_bar) * 1000.0
);
assert!(
approx_eq((y1 - y_bar) * 1000.0, 3.460 * step_yr, 1.0e-9),
"y rate {}",
(y1 - y_bar) * 1000.0
);
}
#[test]
fn pole_tide_matches_iers_eq724_latitude_form() {
let stations = [
ZIM2_ECEF_M,
[4_517_590.9, 837_910.5, 4_402_330.5], [1_130_773.0, -4_831_253.0, 3_994_200.0], [-2_409_600.0, 5_384_500.0, 2_407_800.0], [3_183_000.0, 1_421_000.0, -5_322_000.0], ];
let epochs = [
(
2003,
7,
1,
6.0,
0.10,
0.25,
E2003_MEAN_POLE_X_ARCSEC,
E2003_MEAN_POLE_Y_ARCSEC,
),
(
2026,
5,
13,
12.5,
ZIM2_XP_ARCSEC,
ZIM2_YP_ARCSEC,
ZIM2_MEAN_POLE_X_ARCSEC,
ZIM2_MEAN_POLE_Y_ARCSEC,
),
(
2031,
11,
20,
18.25,
-0.05,
0.55,
E2031_MEAN_POLE_X_ARCSEC,
E2031_MEAN_POLE_Y_ARCSEC,
),
];
let mut max_dev = 0.0_f64;
for xsta in &stations {
for &(y, mo, d, fhr, xp, yp, x_bar, y_bar) in &epochs {
let got =
solid_earth_pole_tide(xsta, y, mo, d, fhr, xp, yp).expect("valid pole tide input");
let (u_got, n_got, e_got) = ecef_to_runeu(xsta, &got);
let (u_ref, n_ref, e_ref) = eq724_latitude_form(xsta, x_bar, y_bar, xp, yp);
for (g, r) in [(u_got, u_ref), (n_got, n_ref), (e_got, e_ref)] {
let dev = (g - r).abs();
max_dev = max_dev.max(dev);
assert!(
dev < 1.0e-12,
"eq 7.24 mismatch at {y}-{mo}-{d}: got {g:.15e}, ref {r:.15e}, dev {dev:.3e}"
);
}
}
}
assert!(max_dev < 1.0e-12, "max deviation {max_dev:.3e}");
}
#[test]
fn pole_tide_zim2_real_eop_has_few_mm_magnitude() {
let d = solid_earth_pole_tide(
&ZIM2_ECEF_M,
2026,
5,
13,
12.5,
ZIM2_XP_ARCSEC,
ZIM2_YP_ARCSEC,
)
.expect("valid ZIM2 pole tide input");
let mag = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
assert!(
(0.5e-3..5.0e-3).contains(&mag),
"ZIM2 pole tide magnitude {mag:.4e} m outside the expected few-mm band"
);
let (u_got, n_got, e_got) = ecef_to_runeu(&ZIM2_ECEF_M, &d);
let (u_ref, n_ref, e_ref) = eq724_latitude_form(
&ZIM2_ECEF_M,
ZIM2_MEAN_POLE_X_ARCSEC,
ZIM2_MEAN_POLE_Y_ARCSEC,
ZIM2_XP_ARCSEC,
ZIM2_YP_ARCSEC,
);
assert!(approx_eq(u_got, u_ref, 1.0e-12), "up {u_got} vs {u_ref}");
assert!(approx_eq(n_got, n_ref, 1.0e-12), "north {n_got} vs {n_ref}");
assert!(approx_eq(e_got, e_ref, 1.0e-12), "east {e_got} vs {e_ref}");
assert!(u_got < 0.0 && u_got > -3.0e-3, "radial {u_got}");
}
#[test]
fn pole_tide_rejects_degenerate_geometry() {
assert_invalid(
solid_earth_pole_tide(&[0.0, 0.0, 0.0], 2026, 5, 13, 12.0, 0.1, 0.4),
"station radius",
TideInputErrorKind::NotPositive,
);
assert_invalid(
solid_earth_pole_tide(&[0.0, 0.0, 6_378_136.6], 2026, 5, 13, 12.0, 0.1, 0.4),
"station horizontal radius",
TideInputErrorKind::NotPositive,
);
}
#[test]
fn pole_tide_rejects_invalid_date_hour_and_nonfinite_pole() {
assert_invalid(
solid_earth_pole_tide(&ZIM2_ECEF_M, 2026, 13, 13, 12.0, 0.1, 0.4),
"civil datetime",
TideInputErrorKind::InvalidCivilDate,
);
assert_invalid(
solid_earth_pole_tide(&ZIM2_ECEF_M, 2026, 5, 13, 24.0, 0.1, 0.4),
"fractional hour",
TideInputErrorKind::OutOfRange,
);
assert_invalid(
solid_earth_pole_tide(&ZIM2_ECEF_M, 2026, 5, 13, 12.0, f64::NAN, 0.4),
"polar motion xp",
TideInputErrorKind::NonFinite,
);
assert_invalid(
solid_earth_pole_tide(&ZIM2_ECEF_M, 2026, 5, 13, 12.0, 0.1, f64::INFINITY),
"polar motion yp",
TideInputErrorKind::NonFinite,
);
}
fn assert_invalid(got: Result<[f64; 3], TideError>, field: &'static str, kind: TideInputErrorKind) {
assert_eq!(
got.expect_err("invalid pole tide input must error"),
TideError::InvalidInput { field, kind }
);
}