use crate::constants::J2000;
#[cfg(feature = "python-tests")]
mod python_tests;
pub fn earth_rotation_angle(jd_ut1: f64, fraction_ut1: f64) -> f64 {
let th = 0.7790572732640 + 0.00273781191135448 * (jd_ut1 - J2000 + fraction_ut1);
(th.rem_euclid(1.0) + jd_ut1.rem_euclid(1.0) + fraction_ut1).rem_euclid(1.0)
}
pub fn sidereal_time(jd_ut1_whole: f64, ut1_fraction: f64, tdb_centuries: f64) -> f64 {
let theta = earth_rotation_angle(jd_ut1_whole, ut1_fraction);
let t = tdb_centuries;
let st = 0.014506
+ ((((-0.0000000368 * t - 0.000029956) * t - 0.00000044) * t + 1.3915817) * t
+ 4612.156534)
* t;
(st / 54000.0 + theta * 24.0).rem_euclid(24.0)
}
pub fn parse_finals2000a(data: &str) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let mut mjds = Vec::new();
let mut xs = Vec::new();
let mut ys = Vec::new();
for line in data.lines() {
if line.len() < 68 {
continue;
}
let mjd_str = line.get(7..15).unwrap_or("").trim();
let x_str = line.get(18..27).unwrap_or("").trim();
let y_str = line.get(37..46).unwrap_or("").trim();
if let (Ok(mjd), Ok(x), Ok(y)) = (
mjd_str.parse::<f64>(),
x_str.parse::<f64>(),
y_str.parse::<f64>(),
) {
mjds.push(mjd);
xs.push(x);
ys.push(y);
}
}
(mjds, xs, ys)
}
pub fn finals_to_polar_motion_table(
mjds: &[f64],
xs: &[f64],
ys: &[f64],
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let tt_jds: Vec<f64> = mjds
.iter()
.map(|&mjd| mjd + 2400000.5 + 69.184 / 86400.0)
.collect();
(tt_jds, xs.to_vec(), ys.to_vec())
}
const DEG2RAD: f64 = std::f64::consts::PI / 180.0;
pub fn refraction(alt_degrees: f64, temperature_c: f64, pressure_mbar: f64) -> f64 {
if !(-1.0..=89.9).contains(&alt_degrees) {
return 0.0;
}
let r = 0.016667 / ((alt_degrees + 7.31 / (alt_degrees + 4.4)) * DEG2RAD).tan();
r * (0.28 * pressure_mbar / (temperature_c + 273.0))
}
pub fn refract(alt_degrees: f64, temperature_c: f64, pressure_mbar: f64) -> f64 {
let mut refracted = alt_degrees;
for _ in 0..10 {
let delta = refraction(refracted, temperature_c, pressure_mbar);
let new = alt_degrees + delta;
if (new - refracted).abs() < 3.0e-5 {
return new;
}
refracted = new;
}
refracted
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_era_at_j2000() {
let era = earth_rotation_angle(J2000, 0.0);
assert_relative_eq!(era, 0.7790572732640, epsilon = 1e-10);
}
#[test]
fn test_era_range() {
for offset in &[-1000.0, -100.0, 0.0, 100.0, 1000.0] {
let era = earth_rotation_angle(J2000 + offset, 0.0);
assert!(
(0.0..1.0).contains(&era),
"ERA out of range for offset {offset}: {era}"
);
}
}
#[test]
fn test_gmst_at_j2000() {
let gmst = sidereal_time(J2000, 0.0, 0.0);
assert_relative_eq!(gmst, 18.697, epsilon = 0.01);
}
#[test]
fn test_gmst_range() {
for offset in &[-1000.0, -100.0, 0.0, 100.0, 1000.0] {
let t_centuries = offset / 36525.0;
let gmst = sidereal_time(J2000 + offset, 0.0, t_centuries);
assert!(
(0.0..24.0).contains(&gmst),
"GMST out of range for offset {offset}: {gmst}"
);
}
}
#[test]
fn test_gmst_increases_with_time() {
let gmst1 = sidereal_time(J2000, 0.0, 0.0);
let gmst2 = sidereal_time(J2000, 0.01, 0.01 / 36525.0);
let diff = (gmst2 - gmst1 + 24.0) % 24.0;
assert!(diff > 0.2 && diff < 0.3, "GMST increase = {diff}");
}
#[test]
fn test_polar_motion_matrix_identity_at_zero() {
let ts = crate::time::Timescale::default();
let t = ts.tdb_jd(2451545.0);
let w = t.polar_motion_matrix();
let det = w.determinant();
assert_relative_eq!(det, 1.0, epsilon = 1e-14);
}
#[test]
fn test_polar_motion_angles_without_table() {
let ts = crate::time::Timescale::default();
let t = ts.tdb_jd(2451545.0);
let (s_prime, x, y) = t.polar_motion_angles();
assert!(s_prime.abs() < 1e-3, "s_prime = {s_prime}");
assert_eq!(x, 0.0);
assert_eq!(y, 0.0);
}
#[test]
fn test_polar_motion_with_table() {
let mut ts = crate::time::Timescale::default();
let tt_jd = vec![2451445.0, 2451545.0, 2451645.0];
let x = vec![0.1, 0.1, 0.1];
let y = vec![0.2, 0.2, 0.2];
ts.set_polar_motion_table(tt_jd, x, y);
assert!(ts.has_polar_motion());
let t = ts.tdb_jd(2451545.0);
let (_, xp, yp) = t.polar_motion_angles();
assert_relative_eq!(xp, 0.1, epsilon = 1e-10);
assert_relative_eq!(yp, 0.2, epsilon = 1e-10);
}
#[test]
fn test_c_matrix_orthogonality() {
let ts = crate::time::Timescale::default();
let t = ts.tdb_jd(2451545.0);
let c = t.c_matrix();
let product = c.transpose() * c;
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert_relative_eq!(product[(i, j)], expected, epsilon = 1e-14);
}
}
}
#[test]
fn test_refraction_at_horizon() {
let r = refraction(0.0, 10.0, 1010.0);
assert!(
r > 0.4 && r < 0.7,
"Horizon refraction should be ~0.5°, got {r}"
);
}
#[test]
fn test_refraction_at_zenith() {
let r = refraction(90.0, 10.0, 1010.0);
assert_relative_eq!(r, 0.0, epsilon = 1e-10);
}
#[test]
fn test_refraction_below_horizon() {
assert_relative_eq!(refraction(-5.0, 10.0, 1010.0), 0.0);
}
#[test]
fn test_refraction_at_45() {
let r = refraction(45.0, 10.0, 1010.0);
assert!(
r > 0.01 && r < 0.03,
"45° refraction should be ~0.02°, got {r}"
);
}
#[test]
fn test_refract_raises_altitude() {
let apparent = refract(0.0, 10.0, 1010.0);
assert!(
apparent > 0.0,
"Refracted altitude should be above true altitude"
);
}
#[test]
fn test_refract_roundtrip() {
let true_alt = 10.0;
let apparent = refract(true_alt, 15.0, 1013.25);
assert!(apparent > true_alt);
assert!(apparent - true_alt < 0.2);
}
}