apsis-rs/validation
ApsisRS contributors 0e3d735935 docs: bilingual comments for the orbit validation scripts 2026-10-02 01:42:39 +08:00
..
python docs: bilingual comments for the orbit validation scripts 2026-10-02 01:42:39 +08:00
tools physics: add IAU 2000A nutation and IAU 2006 precession 2026-09-26 16:48:57 +08:00
README.md docs: give every note an English and a Chinese version 2026-09-30 19:23:26 +08:00
README.zh.md docs: give every note an English and a Chinese version 2026-09-30 19:23:26 +08:00
pixi.lock rf: replace gaseous and rain models with ITU-R P.676-12 and P.838-3 2026-09-26 15:13:48 +08:00
pixi.toml docs: bilingual comments for the CI workflow, manifests and scripts 2026-10-02 01:40:39 +08:00

README.md

Cross-check validation

English | 简体中文

This directory holds the numerical cross-checks of the Rust implementation against public reference implementations. The Python side is for validation only: it is not a Rust dependency and takes no part in the Rust build.

The Python environment

It is managed with pixi:

cd validation
pixi install

The libraries that are allowed:

  • sgp4: the SGP4 propagation cross-check
  • astropy: the time and coordinate cross-checks
  • numpy: numerical computation
  • skyfield: the orbit and visibility cross-checks, optional
  • itur: the ITU-R reference implementations of P.676, P.838 and P.618, for the atmosphere and rain cross-checks

Adding a new Python library needs to be assessed and agreed first.

Layout

validation/
├── pixi.toml
├── python/
│   ├── orbit/           # SGP4 and two-body cross-checks
│   ├── time/            # time scale and Julian date cross-checks
│   ├── coord/           # coordinate transform cross-checks
│   ├── link/            # link budget cross-checks
│   └── constellation/   # coverage geometry cross-checks
└── data/
    └── expected/        # data dumped by the Rust side for comparison

The flow

The Rust side writes the data through an example of apsis-rs, as CSV on standard output, formatted by hand without pulling in serde:

# Geodetic -> ECEF
cargo run -p apsis-rs --features physics --example dump_geodesy \
    > validation/data/expected/rust_geodetic.csv

# Time scale conversions
cargo run -p apsis-rs --features physics --example dump_time \
    > validation/data/expected/rust_time.csv

# SGP4 and SDP4 propagation
cargo run -p apsis-rs --features orbit --example dump_orbit \
    > validation/data/expected/rust_sgp4.csv

# Link budget
cargo run -p apsis-rs --features rf --example dump_link \
    > validation/data/expected/rust_link.csv

# ITU-R P.676-12 gaseous absorption
cargo run -p apsis-rs --features rf --example dump_gas \
    > validation/data/expected/rust_gas.csv

# ITU-R P.838-3 rain attenuation coefficients
cargo run -p apsis-rs --features rf --example dump_rain \
    > validation/data/expected/rust_rain.csv

# ITU-R P.618-13 rain on a slant path
cargo run -p apsis-rs --features rf --example dump_rain_path \
    > validation/data/expected/rust_rain_path.csv

# ITU-R S.465 reference antenna pattern
cargo run -p apsis-rs --features rf --example dump_s465 \
    > validation/data/expected/rust_s465.csv

# ITU-R P.839-4 zero degree isotherm height
cargo run -p apsis-rs --features rf --example dump_p839 \
    > validation/data/expected/rust_p839.csv

# ITU-R P.676-12 exact slant path, layered integration with a refractive
# index ray trace
cargo run -p apsis-rs --features rf --example dump_gas_exact \
    > validation/data/expected/rust_gas_exact.csv

# IAU 2000/2006 Earth orientation: precession, nutation, frame offsets,
# GCRS to ITRF
cargo run -p apsis-rs --features physics --example dump_iau2000 \
    > validation/data/expected/rust_iau2000.csv

# TEME to the Earth-fixed frame, the SGP4 output chain
cargo run -p apsis-rs --features physics --example dump_teme \
    > validation/data/expected/rust_teme.csv

# Coverage geometry: elevation, nadir angle, central angle, slant range and
# footprint area
cargo run -p apsis-rs --features network --example dump_coverage \
    > validation/data/expected/rust_coverage.csv

# The two-body model: elements to a state vector and back
cargo run -p apsis-rs --features orbit --example dump_kepler \
    > validation/data/expected/rust_kepler.csv

# Two-line element parsing: fields, epoch expansion, modulo ten checksum
cargo run -p apsis-rs --features orbit --example dump_tle \
    > validation/data/expected/rust_tle.csv

The Python side reads the same inputs, recomputes them with a reference implementation, and reports the differences:

pixi run python validation/python/coord/check_geodetic.py
pixi run python validation/python/time/check_time.py
pixi run python validation/python/orbit/check_sgp4.py
pixi run python validation/python/link/check_link.py
pixi run python validation/python/atmosphere/check_gas.py
pixi run python validation/python/atmosphere/check_rain.py
pixi run python validation/python/atmosphere/check_rain_path.py
pixi run python validation/python/link/check_s465.py
pixi run python validation/python/atmosphere/check_p839.py
pixi run python validation/python/atmosphere/check_gas_exact.py
pixi run python validation/python/time/check_iau2000.py
pixi run python validation/python/coord/check_teme.py
pixi run python validation/python/constellation/check_coverage.py
pixi run python validation/python/orbit/check_kepler.py
pixi run python validation/python/orbit/check_tle.py

Every script exits with zero to pass, one to exceed the tolerance, and two when the input file is missing.

Tolerances

Quantity Tolerance
Position and distance < 1e-3 km for SGP4 propagation, 1e-9 km for a geometric quantity
Velocity < 1e-6 km/s
Time < 1e-6 s
Coverage angle 1e-12 rad
Area and coverage fraction 1e-12 relative
Link budget and attenuation 1e-6 to 1e-9 dB, depending on the model
Earth orientation matrix 1e-9

Each script names its tolerance as a constant at the top of its source, set by the physical quantity it compares; see the "Tolerances of the cross-checks" section of docs/design/errors.md.

When a check exceeds its tolerance the fix is in the Rust algorithm. The tolerance is not widened unless that has been agreed.

Every script must run offline on its own and reach no network. check_time.py disables the astropy Earth orientation download, which none of the conversions in this crate needs.

The checks that exist

Script What it compares Reference Worst difference
coord/check_geodetic.py Geodetic to ECEF, 280 points across latitude, longitude and height astropy.coordinates.EarthLocation 1.1e-8 m
time/check_time.py UTC to TAI, 117 instants covering every leap second boundary astropy.time.Time 5.3e-7 s
orbit/check_sgp4.py SGP4 and SDP4 propagation, 4 element sets (near Earth, highly eccentric, deep space, GTO) at 9 instants the sgp4 reference library position 1.2e-8 km, velocity 5.3e-12 km/s
link/check_link.py link budget, 4 scenarios (Ku downlink, S band, Ka wideband, L band) the closed form link equation, rewritten in Python 5.0e-13 dB
atmosphere/check_gas.py gaseous absorption, 18 frequencies covering the 60 GHz oxygen complex and the 22 and 183 GHz water lines the ITU-R P.676-12 implementation in itur dry air 6.1e-11, water 6.4e-9, relative
atmosphere/check_rain.py rain coefficients, 17 frequencies at 4 elevation and polarisation pairs the ITU-R P.838-3 implementation in itur k 7.5e-9, alpha 6.5e-13, relative
atmosphere/check_rain_path.py rain on a slant path, 8 scenarios the ITU-R P.618-13 equations, transcribed to Python with coefficients from itur 3.4e-13 km and 4.3e-13 dB
link/check_s465.py reference antenna pattern, 3 apertures at 8 off-axis angles the ITU-R S.465 equations, transcribed to Python 4.0e-10 dB
atmosphere/check_p839.py zero degree isotherm height, 15 points worldwide the ITU-R P.839-4 implementation in itur 3.3e-13 km
atmosphere/check_gas_exact.py exact slant path, 11 scenarios from 1 to 100 GHz at 1 to 90 degrees elevation the exact method of ITU-R P.676-12 in itur 9.6e-9, relative
time/check_iau2000.py precession, nutation, frame offsets, GAST and GCRS to ITRF, 8 epochs by 28 quantities ERFA 2.0.1, derived from IAU SOFA nutation 6.6e-15 rad, BPN matrix 6.0e-15, GAST 1.1e-11 rad
coord/check_teme.py TEME to the Earth-fixed frame, 4 epochs gmst82 and pom00 from ERFA, both with zero polar motion 4.9e-10 km
constellation/check_coverage.py coverage geometry, 6 scenarios of LEO, MEO and GEO at low, usual and strict masks, plus a round trip between the central angle and the elevation the spherical triangle relations, rewritten in Python from the geometry angle 0, area 2.2e-16 relative
orbit/check_kepler.py two-body conversion over 7 orbits (circular, low, moderate and high eccentricity, retrograde, near polar and geostationary), plus a state back to elements at 36 true anomalies the perifocal formulae, the rotation matrix and the vis-viva equation, rewritten in Python position, velocity and period 0; energy 3.0e-14 km^2/s^2; recovered elements about 1e-11 km
orbit/check_tle.py element parsing, 8 element sets by 17 fields, covering the two digit year expansion, the implied leading zero of the eccentricity, the exponential drag term and the modulo ten checksum an independent column indexed reader of the NORAD format exact, field by field

A note on the radio frequency cross-checks

The radio frequency layer divides into two kinds of model: those with an authoritative reference implementation, and those that are purely a closed form expression.

  • Where a reference implementation exists it is compared directly: Earth orientation uses erfa, derived from IAU SOFA, and the atmosphere and rain use itur. Every step of the link budget is elementary arithmetic, and check_link.py rewrites the same equation independently in Python.
  • The rest are checked by asserting analytic properties: the unit tests of each crate verify what the model must satisfy, such as the 3 dB point, the square law falloff, the behaviour of a Friis cascade, and the value of the P.838-3 coefficients at the tabulated frequencies.

Every recommendation is at its current revision: P.676-12, P.838-3, P.618-13, P.839-4 and S.465. The coefficients of P.838-3 are a closed form curve fit, not the superseded numerical table of P.838-1.

A note on the SGP4 cross-check convention

The epoch of a TLE is a UTC calendar instant, but SGP4 treats it as a uniform time origin, which is the convention the theory was fitted with. Converting the epoch to TAI before handing it to the propagator would introduce a leap second offset of about 33 seconds, and that offset would propagate into the luni-solar series of the deep space branch. Both sides of the cross-check therefore use the UTC Modified Julian Date as the element set states it.

A note on the difference in the time cross-check conventions

There are two conventions for a floating point day number of UTC:

  • This repository uses a linear Modified Julian Date, where TAI - UTC is a step function and the offset is constant for the rest of a leap second day.
  • astropy and ERFA use a quasi-Julian date, where a leap second day has 86401 seconds and their tai - utc difference varies through the day.

Comparing the two Julian dates directly would therefore measure a difference of convention rather than a numerical error. check_time.py compares the TAI instant instead: both sides take the same UTC calendar instant and the resulting TAI Modified Julian Date is compared. The comparison uses an integer day plus a fraction of a day in two parts, which avoids the roughly 40 us of resolution a single f64 loses near an MJD of 60000.

dump_time writes the TAI instant in the same two parts for the same reason.