v0.9.0
This release adds three substantial capability areas on top of the 0.8.0 SGP4
substrate: a genuine three-axis strapdown INS, a loosely-coupled GNSS/INS
error-state EKF with closed-loop feedback, real snapshot and solution-separation
(ARAIM-style) RAIM with HPL/VPL and a runnable integrity scenario, and the first
step of GNSS-format interoperability (RINEX-3 GPS ephemeris ingestion).
Added
- RINEX 3 GPS navigation-message parser (
src/rinex.rs). First step toward
GNSS-format interoperability:parse_navreads a RINEX 3.x navigation file and
decodes each GPS (G) broadcast-ephemeris record — the eight-line SV/epoch +
BROADCAST ORBITblock — into aRinexEphemerisof Keplerian elements and
clock corrections, with field names and units per IS-GPS-200. Handles the
FortranD-exponent float format (parse_d) and fixed-width column layout;
records for non-GPS systems are skipped, not rejected, so a mixed file still
yields its GPS ephemerides. Four tests:D-exponent parsing (including blanks
and errors), a full record decoded against known field values with a GPS
semi-major-axis sanity check (√A² ≈ 26 560 km), non-GPS skipping, and the
empty-file case. - GPS broadcast-ephemeris → ECEF position (
src/rinex.rs).
RinexEphemeris::sv_position_ecef(t_tow)evaluates the satellite's Earth-fixed
position from the parsed ephemeris via the IS-GPS-200 §20.3.3.4.3.1 user
algorithm: Newton solution of Kepler's equation for the eccentric anomaly, the
second-harmonic argument-of-latitude / radius / inclination corrections, and the
rotation into ECEF accounting for Earth rotation since the reference epoch (with
the GPSμandΩ̇ₑmandated by the spec, and a week-rollovertkfold). Three
tests: the geocentric radius stays in the GPS band (≈ 26 560 km), the Earth-fixed
speed is ~3.9 km/s, and evaluating a full week away reproduces the same position. - GPS SV clock bias with the relativistic correction (
src/rinex.rs).
RinexEphemeris::sv_clock_bias_s(t_tow)evaluates the broadcast clock
polynomialaf0 + af1·Δt + af2·Δt²aboutTocplus the relativistic
eccentricity termF·e·√A·sin Ek(IS-GPS-200 §20.3.3.3.3.1). A new
EpochUtc::gps_time_of_weekconverts the record's calendar epoch to GPS
time-of-week via Julian-day arithmetic from the GPS epoch (1980-01-06), and the
Kepler solve is shared with the position evaluation. Tests: GPS time-of-week for
a Sunday/Tuesday/Saturday (week boundaries), and the clock bias being
af0-dominated with a present, bounded relativistic term. The L1 group-delay
TGDis exposed but deliberately not folded in. Honest scope: aPropagator
source, Galileo/BeiDou/GLONASS, and SP3 remain next steps. (docs/CAPABILITY.md
updated to match.) - User-runnable
integrityscenario kind (scenarios/integrity-raim.toml).
The RAIM availability capability is now reachable from the CLI/TOML like every
other pack:kind = "integrity"parses anIntegrityScenario(user orbit, one
or more GNSS constellations, elevation mask, and the(sigma, P_fa, P_md, AL_H, AL_V)integrity config), runsconstellation_raim_availability, and emits the
per-epoch HPL/VPL availability map as JSON plus a self-contained SVG —
protection levels over time against the alert limits, with a green/red
availability strip.kshana scenarios/integrity-raim.tomlwrites the JSON, the
chart, and an HTML report. The bundled scenario (24-satellite Walker, 1 m
dual-frequency ranging, APV-I limits) reports ~95 % availability; it documents
that single-frequency RAIM does not meet the vertical APV-I limit on one
constellation, which is why vertical guidance uses SBAS/dual-frequency/ARAIM.
Tests cover the dispatch, the availability-map JSON, and the SVG. - Runnable RAIM availability over a constellation (
src/raim.rs). The
integrity module had no caller —constellation_raim_availabilitymakes it a
genuine end-to-end entry point: at each epoch on a time grid it propagates the
visible satellites (the same SGP4/KeplerianPropagators the engine uses),
computes the no-fault protection levels, and judges availability against the
horizontal/vertical alert limits, returning aserde-serializable
RaimAvailabilityReport(per-epochn_visible/HPL/VPL/availableplus the
availability fraction). ARaimConfigbundles(sigma, P_fa, P_md, AL_H, AL_V)
and the per-epochraim_availability_epochis exposed for callers that resolve
their own geometry. Three tests: an epoch judged available under APV-I limits on
a ten-satellite geometry, made unavailable by an impossibly tight limit, and
Nonelevels below five satellites; and an end-to-end run over a 24-satellite
Walker constellation that yields a finite availability map and serializes. (Six
satellites — the residual-RAIM redundancy floor — honestly do not meet APV-I
even at 1 m ranging; APV-I availability needs the denser geometry the test uses.) - Stanford(-ESA) integrity diagram accumulator (
src/raim.rs). The standard
way to summarise an integrity monitor over many epochs: it plots actual
position error (x) against protection level (y) and classifies each epoch, by
the diagonaly = xand the alert limit, intoAvailable(PL bounds error,
within AL),SystemUnavailable(PL bounds error but exceeds AL — safe,
unusable),MisleadingInformation(PL < error ≤ AL), or
HazardouslyMisleadingInformation(PL < error and error > AL — the unsafe
failure).classify_stanfordis the pure classifier;StanfordDiagram
accumulates(error, PL)points against a fixed alert limit, exposing region
counts, availability, integrity-event totals, andserde-serializable points
for plotting/JSON export. Four tests: every region (including theerror == PL
bounded boundary), count/availability accumulation, and JSON round-trip. This
is the reporting surface for the RAIM protection levels; wiring it into the
constellation scenario and validating against a public dataset remain roadmap
items. - Solution-separation (ARAIM-style) RAIM (
src/raim.rs). A
multiple-hypothesis integrity monitor alongside the existing residual/parity
chi-squaredsnapshot_raim. For the all-in-view least-squares solution and
every single-satellite exclusion sub-solution, it forms the separation
Δ_k = x_k − x₀— zero-mean Gaussian under no fault with covariance
Cov(x_k) − Cov(x₀)(the nested-estimator identity, valid because the
all-in-view solution is BLUE) — and so it both detects a fault and
identifies the faulted satellite (the one whose exclusion gives the largest
normalized separation). Horizontal/vertical protection levels follow the
standard MHSS allocationPL = max(K_md·σ₀, max_k[K_fa·σ_ss,k + K_md·σ_k]),
withK_fa = Φ⁻¹(1−P_fa/2),K_md = Φ⁻¹(1−P_md). New dependency-free
normal_cdf/normal_quantilebuilt from the module's existing regularized
incomplete gamma (erf(x) = P(½,x²)). Four hand-derived tests: normal CDF /
quantile against textbook values (Φ(1.95996)=0.975, the 1e-7 tail = 5.1993,
symmetry); a fault-free geometry that does not alarm and yields finite, positive
HPL/VPL; a 60-σ single-satellite bias that is detected and correctly
identified (excluded_sv == 2); and the six-satellite redundancy floor. Closes
the audit's "tautological integrity — no real RAIM/HPL/VPL" P0 gap on the
algorithm side; gLAB-dataset validation and the Stanford-diagram accumulator
remain roadmap items. - Closed-loop GNSS/INS integration (
src/fusion/closed_loop.rs).
ClosedLoopInsGnsswires the error-state EKF kernel to the three-axis strapdown
mechanization: each IMU sample is corrected by the running bias estimates,
mechanized forward, and the EKF covariance time-propagated with the matching
navigation context; each GNSS position/velocity fix forms the INS−GNSS
innovation and feeds the estimated position, velocity, attitude error (ψ, as a
quaternion rotation) and accelerometer/gyro biases back into the solution,
resetting the error-state mean. Feeding the attitude back (not only the biases)
is required for stability — the tilt and accelerometer bias are a coupled pair,
so correcting one without the other diverges. INS and GNSS are compared in a
local tangent-plane NED frame using the mechanization's own radii of curvature
(newmechanization::radii_of_curvature;NavState::omega_ie_n/omega_en_n
exposed). This is the honest replacement for the hybrid pack's truth-snap
reset. Three tests: a closed loop nulling an injected 8 m / −5 m position error
to <0.1 m; an aided solution staying metre-bounded (<6 m) on a driving
trajectory while a free-running INS diverges past 100 m; and the milestone
benchmark — the fused solution's Monte-Carlo position RMS over a 60 s GNSS
outage beats an unaided open-loop dead-reckoner by >2× (≈4× across seeds).
Honest limitation documented in the module: in loosely-coupled mode the accel
bias and tilt are only weakly separable (both couple through gravity), so the
delivered value is the bounded, corrected state and a clean outage-entry — not a
precise inertial calibration; richer dynamics and the tightly-coupled extension
remain roadmap items. - Loosely-coupled GNSS/INS error-state EKF kernel (
src/fusion/gnss_ins_ekf.rs).
A 15-state error-state extended Kalman filter —δx = [δp, δv, ψ, b_a, b_g]—
with the strapdown error dynamics from Groves 2013 §14.2 (specific-force/tilt
coupling, Coriolis, body→nav bias projection, Gauss–Markov bias models), a
first-order discrete transitionΦ = I + F·dt, and a loosely-coupled
position+velocity measurement update (H = [I₃ 0 0 0 0; 0 I₃ 0 0 0]) in Joseph
form. Dependency-free dense linear algebra (Gauss–Jordan inverse, Joseph
covariance update). Atight_couplingcargo feature gates a documented,
not-yet-implemented pseudorange/Doppler update. 7 tests with hand-derived
expectations: the skew/cross-product identity, a verified 3×3 inverse,
covariance staying symmetric/PSD under prediction (and position uncertainty
growing un-aided), a position fix shrinking the position covariance, exact
recovery of a known position error at the analytic Kalman gainP/(P+R), and
smaller corrections under larger measurement noise. This is the kernel that
will replace the hybrid pack's open-loop truth-snap reset with closed-loop
feedback (pack wiring + NaveGo validation to follow). - Deterministic IMU error model for the 3-axis strapdown navigator (
src/inertial/imu_errors.rs).
ImuErrorModeldistorts a true body-frame(ω, f)pair into a measured one
through five systematic categories (IEEE Std 952-1997 §A.2; Groves 2013 §4.3,
Table 4.1): scale-factor (per-axis ppm gain error), misalignment /
cross-coupling (off-diagonal triad non-orthogonality), g-sensitivity (a
gyro rate bias proportional to specific force), quantization (rounding to
the output LSB), and rate-ramp (a linear-in-time drift — the third Allan
region), plus a constant turn-on bias. Every term defaults to zero, so
ImuErrorModel::ideal()is a transparent pass-through and existing scenarios
are unaffected. Each error source has an isolation test (scale linear to <0.01%,
misalignment cross-axis above the VRW floor, g-sensitivity bias, LSB grid,
linear ramp), and an end-to-end test drives a navigation error through the
mechanization from a distorted IMU. Not modelled: vibration rectification error,
temperature-gradient drift. (The shippedinertialscenario pack still runs the
legacy 1-DOF scalar budget; this model feeds the 3-axis library.) - Coning and sculling compensation for the strapdown integrator. The
attitude path adds the two-sampleconing_increment(½ Δθ_prev × Δθ_cur); a
coarse-rate (30 Hz, 5-samples/cycle) integration of a 5 Hz coning environment
is verified to track fine-rate truth ≥ 3× better with the correction than naive
increment-summing. The velocity path addssculling_increment(½ Δθ × Δv,
Groves eq. 5.82) and resolves the body velocity increment through a new
NavState::step_incrementsincrement-based update using the body-relative
rotationΔθ_rel = Δθ_b − C_n^b ζ, so an Earth-fixed platform incurs no
spurious sculling while a genuine vibration triggers the full term. - Full three-axis strapdown mechanization in the NED frame (
src/inertial/mechanization.rs).
NavState { q, v_ned, p_llh }is advanced bystep(gyro_b, accel_f_b, dt)using
the standard terrestrial-frame NED equations (Groves §5.4): body→NED attitude
corrected for the inertial-to-nav rateω_in = ω_ie + ω_en(Earth rotation +
transport rate); specific force resolved body→NED through the DCM; velocity
integratingv̇ = f_n − (2 ω_ie + ω_en) × v + g_n(Coriolis/transport + gravity);
and geodetic position via the meridian/transverse radii of curvature. Gravity is
the WGS-84 closed-form Somigliana normal (plumb-bob) gravity with a NIMA
free-air altitude correction — never a hard-coded constant. This is the genuine
three-axis navigator that supersedes the 1-DOF scalar error-budget path. Verified
by physical invariants: a platform bolted to the rotating Earth at 45°N (sensing
Earth rate + 1 g) stays within 1 mm over 60 s; a level north specific force gives
v_N ≈ a·tand½ a t²displacement; normal gravity matches the known
equator/pole/45° surface values and the free-air lapse rate. - Three-axis attitude representation for strapdown INS (
src/inertial/attitude.rs).
A unit-quaternionQuaterniontype (scalar-first, Hamilton convention) carrying
body→nav rotation, with a DCM view (to_dcm/from_dcmvia Shepperd's method),
Hamilton product, axis-angle and rotation-vector (exact exp-map) constructors,
and quaternion kinematics — both a first-order RK rate update (q̇ = ½ q ⊗ ω)
and a coning-corrected rotation-vector update. The two-sampleconing_increment
(Savage / Bryan–Lewantowski,½ Δθ_prev × Δθ_cur) supplies the rotation-rate
cross-coupling term that scalar dead-reckoning omits. This is the attitude
foundation for the full 3-axis mechanization that replaces the legacy 1-DOF
error-budget path. Verified against closed-form rotations: constant-rate
propagation matches the axis-angle quaternion to 1e-6, DCMs are orthonormal with
unit determinant, and coning vanishes for single-axis motion. (src/inertial.rs
is now thesrc/inertial/module directory; the public pathcrate::inertial
is unchanged.) - Geodetically-correct ground-station visibility (
src/frames.rs).
elevation,is_visible, andvisible_countcompute a satellite's elevation
above a ground station's horizon against the WGS-84 ellipsoid normal (the
true local vertical), not the geocentric radial — the two differ by up to the
~0.19° geodetic deflection, enough to flip near-horizon satellites in or out of
an elevation mask. Verified end-to-end (a Walker constellation propagated,
rotated TEME→ECEF, and counted from a geodetic site) and against the
geocentric approximation.
Changed
- CI reliability. The
test-python-bindingsjob now builds the wheel with
PyO3/maturin-action(manylinux container) instead of a raw hostmaturin build, eliminating an intermittentrustfmt-preview/cargo-fmtrustup
conflict on the runner image. Thedenyjob installscargo-denyas a
prebuilt binary viataiki-e/install-actioninstead of the Docker-based
cargo-deny-action, removing a Docker Hub registry-pull timeout. Neither
change affects the checks performed.
Full changelog: CHANGELOG.md · Docs: README