use proptest::prelude::*;
use satkit::kepler::Anomaly;
use satkit::{Duration, ITRFCoord, Instant, Kepler, Quaternion, TimeScale, TLE};
use std::f64::consts::{PI, TAU};
proptest! {
#[test]
fn kepler_pv_roundtrip(
a in 6.6e6..5.0e7f64,
e in 0.0..0.95f64,
i in 0.0..PI,
raan in 0.0..TAU,
argp in 0.0..TAU,
nu in 0.0..TAU,
) {
let k = Kepler::new(a, e, i, raan, argp, Anomaly::True(nu));
let (r, v) = k.to_pv();
prop_assert!(r.norm().is_finite() && v.norm().is_finite());
let k2 = Kepler::from_pv(r, v);
prop_assert!(k2.is_ok(), "from_pv failed: {:?}", k2.err());
let (r2, v2) = k2.unwrap().to_pv();
prop_assert!((r - r2).norm() / r.norm() < 1e-9, "position round-trip error {:e}", (r - r2).norm() / r.norm());
prop_assert!((v - v2).norm() / v.norm() < 1e-9,
"velocity round-trip error {:e}", (v - v2).norm() / v.norm());
}
#[test]
fn kepler_anomaly_roundtrip(
e in 0.0..0.99f64,
ma in 0.0..TAU,
) {
let k = Kepler::with_mean_anomaly(7.0e6, e, 0.5, 0.0, 0.0, ma);
let ma2 = k.mean_anomaly().rem_euclid(TAU);
let diff = (ma2 - ma).abs();
let diff = diff.min(TAU - diff); prop_assert!(diff < 1e-9, "mean anomaly round-trip error {:e} at e={}", diff, e);
}
#[test]
fn kepler_period_closure(
a in 6.6e6..5.0e7f64,
e in 0.0..0.95f64,
nu in 0.0..TAU,
) {
let k = Kepler::new(a, e, 0.6, 1.0, 2.0, Anomaly::True(nu));
let k2 = k.propagate(&Duration::from_seconds(k.period()));
let (r, _) = k.to_pv();
let (r2, _) = k2.to_pv();
prop_assert!((r - r2).norm() / r.norm() < 1e-6,
"state after one period differs by {:e}", (r - r2).norm() / r.norm());
}
}
proptest! {
#[test]
fn geodesic_distance_finite_nonnegative(
lat1 in -90.0..90.0f64,
lon1 in -180.0..180.0f64,
lat2 in -90.0..90.0f64,
lon2 in -180.0..180.0f64,
) {
let p1 = ITRFCoord::from_geodetic_deg(lat1, lon1, 0.0);
let p2 = ITRFCoord::from_geodetic_deg(lat2, lon2, 0.0);
let (d, h1, h2) = p1.geodesic_distance(&p2);
prop_assert!(d.is_finite(), "distance not finite for ({lat1},{lon1})→({lat2},{lon2})");
prop_assert!(d >= 0.0, "negative distance {d}");
prop_assert!(h1.is_finite() && h2.is_finite(), "headings not finite");
prop_assert!(d < 2.1e7, "distance {d} exceeds half circumference");
prop_assert!(p1.distance_to(&p1) < 1e-6);
}
#[test]
fn geodetic_roundtrip(
lat in -89.999..89.999f64,
lon in -179.999..179.999f64,
alt in -5.0e3..1.0e7f64,
) {
let c = ITRFCoord::from_geodetic_deg(lat, lon, alt);
prop_assert!((c.latitude_deg() - lat).abs() < 1e-8, "lat error");
prop_assert!((c.longitude_deg() - lon).abs() < 1e-8, "lon error");
prop_assert!((c.hae() - alt).abs() < 1e-4, "alt error {:e}", (c.hae() - alt).abs());
}
}
proptest! {
#[test]
fn mjd_roundtrip(
mjd in 41317.0..69807.0f64, ) {
for scale in [TimeScale::UTC, TimeScale::TAI, TimeScale::TT, TimeScale::GPS, TimeScale::TDB] {
let t = Instant::from_mjd_with_scale(mjd, scale);
let mjd2 = t.as_mjd_with_scale(scale);
let err_us = (mjd2 - mjd).abs() * 86400.0e6;
prop_assert!(err_us < 5.0, "{scale} round-trip error {err_us} µs at mjd {mjd}");
}
}
#[test]
fn unixtime_roundtrip(ut in 0.0..2.4e9f64) {
let t = Instant::from_unixtime(ut);
prop_assert!((t.as_unixtime() - ut).abs() < 5e-6,
"unixtime round-trip error {:e}", (t.as_unixtime() - ut).abs());
}
#[test]
fn instant_duration_arithmetic(
mjd in 50000.0..60000.0f64,
dt_us in -1_000_000_000_000i64..1_000_000_000_000i64, ) {
let t = Instant::from_mjd_with_scale(mjd, TimeScale::TAI);
let d = Duration::from_seconds(dt_us as f64 * 1e-6);
let t2 = t + d - d;
prop_assert!(((t2 - t).as_seconds()).abs() < 1e-6);
}
}
proptest! {
#[test]
fn tle_format_roundtrip(
sat_num in 1..99999i32,
inclination in 0.0..180.0f64,
raan in 0.0..360.0f64,
eccen in 0.0..0.9999f64,
argp in 0.0..360.0f64,
ma in 0.0..360.0f64,
mm in 0.5..17.0f64,
bstar in -1.0e-2..1.0e-2f64,
epoch_days in 0.0..10000.0f64, ) {
let mut tle = TLE::default();
tle.sat_num = sat_num;
tle.inclination = inclination;
tle.raan = raan;
tle.eccen = eccen;
tle.arg_of_perigee = argp;
tle.mean_anomaly = ma;
tle.mean_motion = mm;
tle.bstar = bstar;
tle.epoch = Instant::from_date(2000, 1, 1).unwrap() + Duration::from_days(epoch_days);
let lines = tle.to_2line();
prop_assert!(lines.is_ok(), "to_2line failed: {:?}", lines.err());
let lines = lines.unwrap();
let tle2 = TLE::load_2line(&lines[0], &lines[1]);
prop_assert!(tle2.is_ok(), "load_2line failed: {:?}\n{}\n{}", tle2.err(), lines[0], lines[1]);
let tle2 = tle2.unwrap();
prop_assert_eq!(tle2.sat_num, tle.sat_num);
prop_assert!((tle2.inclination - tle.inclination).abs() < 1e-4);
prop_assert!((tle2.raan - tle.raan).abs() < 1e-4);
prop_assert!((tle2.eccen - tle.eccen).abs() < 1e-7);
prop_assert!((tle2.arg_of_perigee - tle.arg_of_perigee).abs() < 1e-4);
prop_assert!((tle2.mean_anomaly - tle.mean_anomaly).abs() < 1e-4);
prop_assert!((tle2.mean_motion - tle.mean_motion).abs() < 1e-7);
let bstar_err = (tle2.bstar - tle.bstar).abs();
prop_assert!(bstar_err <= 1e-4 * tle.bstar.abs().max(1e-9),
"bstar round-trip error {:e} for {:e}", bstar_err, tle.bstar);
prop_assert!((tle2.epoch - tle.epoch).as_seconds().abs() < 1e-2);
}
#[test]
fn tle_parser_never_panics(
l1 in ".{0,90}",
l2 in ".{0,90}",
) {
let _ = TLE::load_2line(&l1, &l2);
let _ = TLE::from_lines(&[l1, l2]);
}
}
proptest! {
#[test]
fn quaternion_rigid_rotation(
ax in -1.0..1.0f64,
ay in -1.0..1.0f64,
az in -1.0..1.0f64,
angle in -TAU..TAU,
vx in -1.0e6..1.0e6f64,
vy in -1.0e6..1.0e6f64,
vz in -1.0e6..1.0e6f64,
) {
let axis = numeris::vector![ax, ay, az];
prop_assume!(axis.norm() > 1e-3); let axis = axis * (1.0 / axis.norm());
let q = Quaternion::from_axis_angle(axis, angle);
let v = numeris::vector![vx, vy, vz];
let rotated = q * v;
prop_assert!((rotated.norm() - v.norm()).abs() <= 1e-9 * v.norm().max(1.0),
"norm not preserved: {:e}", (rotated.norm() - v.norm()).abs());
let back = q.conjugate() * rotated;
prop_assert!((back - v).norm() <= 1e-9 * v.norm().max(1.0),
"conjugate did not undo rotation: {:e}", (back - v).norm());
}
#[test]
fn quaternion_euler_roundtrip(
roll in -3.1..3.1f64,
pitch in -1.5..1.5f64,
yaw in -3.1..3.1f64,
) {
let q = Quaternion::from_euler(roll, pitch, yaw);
let (r2, p2, y2) = q.to_euler();
prop_assert!((r2 - roll).abs() < 1e-9, "roll error {:e}", (r2 - roll).abs());
prop_assert!((p2 - pitch).abs() < 1e-9, "pitch error {:e}", (p2 - pitch).abs());
prop_assert!((y2 - yaw).abs() < 1e-9, "yaw error {:e}", (y2 - yaw).abs());
}
}
#[test]
fn regression_high_eccen_period_closure() {
let k = Kepler::new(
11568493.745532092,
0.8828701159267661,
0.6,
1.0,
2.0,
Anomaly::True(2.879249859070718),
);
let k2 = k.propagate(&Duration::from_seconds(k.period()));
let (r, _) = k.to_pv();
let (r2, _) = k2.to_pv();
let rel = (r - r2).norm() / r.norm();
assert!(rel < 1e-6, "one-period closure error {rel:e}");
}