geo4d
4D (spatio-temporal) geodesy in pure f64, built on geo3d.
The fourth dimension is time. Where geo3d answers where is this?, geo4d answers where is
this, when is it there, and how does the Earth itself move underneath it in the meantime? It takes
the 3D geometry — ellipsoids, ECEF/ENU frames, geodesics, static volumes — and binds it to a
high-precision atomic clock, so you can dead-reckon a track to an exact instant, propagate a survey
mark across tectonic time, predict when two aircraft lose separation, or ask whether a drone enters a
temporary no-fly zone while it is active.
geo3d is a dependency and its whole API is re-exported, so cargo add geo4d gives you the 3D stack
(Geodetic, Ecef, LocalFrame, WGS84, Sphere, …) and the time axis, from one crate.
Angles are degrees, lengths metres, times seconds (or a Duration); everything except Trajectory
is Copy, allocation-free and #![forbid(unsafe_code)].
Install
Optional: cargo add geo4d --features serde derives Serialize/Deserialize on every type (and
turns on geo3d's serde too).
Quick start
use ;
let now = from_gregorian_utc;
// Two aircraft, each a position + course-over-ground (course°, ground speed, climb) at that time.
let a = from_lat_lon
.state_from_course;
let b = from_lat_lon
.state_from_course;
// When is their closest approach, and how near?
let cpa = a.closest_approach;
assert!;
// Is a 5 NM / 1000 ft protected zone breached in the next 10 minutes?
let zone = new;
let window = for_duration;
let alert = a.conflict; // Some(Conflict { enters, exits, tca }) or None
assert!;
Every Rust example in this README is compiled and run as a doctest by CI, so they are safe to copy.
Guide
Time: epochs, durations and scales
An Epoch is one instant, stored to the nanosecond on the TAI (atomic) scale, so it is Ord and
epoch arithmetic never trips over leap seconds. A Duration is a signed span of whole nanoseconds.
Epoch − Epoch is a Duration; Epoch ± Duration is an Epoch.
Convert to and from the scales that matter — Tai, Gps, Tt, Utc — plus Julian dates, GPS
week/time-of-week, calendars and ITRF decimal years.
use ;
// The J2000.0 epoch: 2000-01-01T12:00:00 TT = Julian Date 2451545.0.
let j2000 = from_gregorian;
assert_eq!;
assert!;
assert!; // TAI − UTC was 32 s in 2000
// Build from Unix time, read GPS week / time-of-week, do exact arithmetic.
let t = from_unix_seconds;
let = t.gps_week_seconds;
assert_eq!;
assert_eq!;
// Decimal years use the ITRF/GDA calendar convention: 2020.0 is 2020-01-01T00:00:00 UTC.
assert_eq!;
TimeWindow is a span between two optional epochs — the temporal half of a geofence and the search
bound for conflict prediction.
use ;
let open = from_gregorian_utc;
let match_time = for_duration;
assert!;
assert!;
assert_eq!;
Timestamped coordinates and state vectors
Geodetic4 and Ecef4 are a geo3d position plus an Epoch. StateVector adds a velocity — the
minimum needed to say where something will be — and dead-reckons to any epoch.
use ;
let t0 = from_unix_seconds;
// A satellite state, propagated 10 s along its velocity.
let s = new;
let later = s.at;
assert!;
// A geodetic fix and its ECEF / ECI counterparts at that instant.
let fix = from_lat_lon;
let ecef = fix.to_ecef4;
let eci = ecef.to_eci; // rotated by the epoch's Earth-rotation angle
assert_eq!;
assert!;
Trajectories
A Trajectory is a time-ordered path — a flight plan, an orbit arc, a track history — with a rule
for reading a position between the samples. The interpolation choices mirror the CCSDS Orbit
Ephemeris Message: piecewise Linear in ECEF, cubic Hermite when velocities are known, or
CatmullRom from positions alone. What happens off the ends is an explicit Extrapolation policy,
never a silent guess. This is the one part of the crate that allocates.
use ;
let t0 = from_unix_seconds;
// Sampled every minute, with velocities → cubic Hermite (C¹, passes through each state exactly).
let traj = from_states.unwrap.with_interpolation;
let state = traj.state_at.unwrap; // position and velocity
assert!;
// Resample onto a fixed grid, or read a geodetic fix at any instant.
let every_10s = traj.resample;
assert_eq!; // 0 s .. 120 s inclusive
assert!;
// Off the ends: None by default, or hold / extend if you opt in.
let held = traj.clone.with_extrapolation;
assert!;
assert!;
Datum shifts and plate motion
The ground moves. A mark in Sydney drifts about 5.7 cm/yr north-east, so its ITRF coordinate in 2026 is nearly two metres from its 1994 one. Two operations capture this, and they must not be confused:
- Same frame, different epoch —
PlateMotion: propagate a coordinate along its tectonic plate,X(t₂) = X(t₁) + V·(t₂ − t₁), using the ITRF2020 plate-motion model (13 named plates). - Same epoch, different frame —
Helmert14: a 14-parameter time-dependent Helmert, evaluated at the coordinate's epoch before it is applied. Named transforms for ITRF↔ITRF and Australia's GDA2020 are built in; build any other from the published parameters.
use ;
// Propagate a Sydney coordinate from 2020.0 to 2026.0 along the Australian plate.
let mark = from_lat_lon.to_ecef4;
let in_2026 = AUSTRALIA.propagate;
assert!; // ~34 cm in 6 yr
// Transform a GNSS position from ITRF2020 to ITRF2014 at its own epoch (a few mm apart).
let p = new;
let itrf2014 = itrf2020_to_itrf2014.apply;
assert!;
assert_eq!;
// GDA94 → GDA2020 shifts a coordinate by 1.4–1.8 m across Australia (26 years of plate motion
// plus a frame change) — about 1.5 m at Sydney.
let g20 = gda94_to_gda2020.apply;
assert!;
Closest approach, conflict and intercept
4D kinematics in absolute time. Because each StateVector carries its own epoch, two tracks fixed at
different times can still be compared — the answers come back as epochs and windows.
use ;
let t = from_unix_seconds;
// Time and distance of closest approach.
let a = new;
let b = new;
let cpa = closest_approach;
assert!; // 5 m apart, 5 s from now
// Cylindrical loss-of-separation window (horizontal + vertical minima).
let conf = conflict;
assert!;
// Fixed-speed intercept of a moving target ("lead collision").
let origin = new;
let target = new;
let ix = intercept.unwrap;
assert!; // flies at the requested speed
assert!; // and meets it
Probability of collision
A miss distance alone does not say how dangerous a conjunction is — that depends on how well each
object's position is known. Given a position covariance for each object and a combined hard-body
radius, collision_probability evaluates the short-term-encounter integral with Chan's series, the
method operational conjunction assessment uses.
use ;
let t = from_unix_seconds;
let a = new;
let b = new;
// Head-on, zero miss, 10 m 1σ each way, 5 m combined size → Pc = 1 − exp(−R²/2σ²) ≈ 6 %.
let pc = collision_probability;
assert!;
4D geofencing
A Timed<V> boxes any geo3d volume — Sphere, Cone, Cylinder, AltitudeBand — into a
TimeWindow, so a point is inside only when it is inside the shape and the window is open. A
Trajectory can be tested against one to find whether, and when, it first enters.
use ;
// A temporary flight restriction over a stadium: 2 km radius, surface to 3 km, for a 3-hour match.
let kickoff = from_gregorian_utc;
let tfr = for_duration;
// Inside the cylinder, but only while the match is on.
let overhead = new;
assert!;
assert!;
// Does a drone's flight path enter it while it is active?
let start = from_lat_lon.to_ecef4;
let vel = new.velocity_of_course;
let drone = from_states.unwrap;
let _entry = tfr.first_entry; // Some(epoch) or None
Serde
With the serde feature every type — Epoch, Duration, StateVector, Trajectory, the volumes,
and the re-exported geo3d coordinate types — serialises to plain fields.
// cargo add geo4d --features serde
let json = to_string?;
Precision and accuracy classes
Each function states its accuracy class, and the tests hold it:
- Exact / integer —
EpochandDurationare nanosecond-exact integers on the TAI timeline; the scale offsets (TT − TAI = 32.184 s, GPS = TAI − 19 s) and the IERS leap-second table (10 s in 1972 to 37 s since 2017) are exact. Calendar and GPS-week round trips hold to the nanosecond. - First order (constant velocity) —
StateVectordead reckoning,closest_approach,conflict,interceptandPlateMotionpropagation are linear-motion models, documented as such. - Model-bounded —
collision_probabilityis the short-term-encounter (rectilinear, Gaussian) model with Chan's series;Helmert14is the linearised 14-parameter Helmert;Trajectorylinear interpolation sags below a curved path by aboutd²/8R(≈ 2 cm over 1 km). - Reference data — plate poles are the ITRF2020 plate-motion model (Altamimi et al. 2023); the ITRF and GDA2020 transformation parameters are the published IERS / ICSM values.
The sidereal angle used for ECI is the IAU 2000 Earth Rotation Angle (with UT1 ≈ UTC); sub-arcsecond
precession, nutation and polar motion are out of scope, as in geo3d.
Design
- Builds on
geo3d; re-exports its whole API so downstream code needs one dependency. Cargo's dead-code elimination means you pay only for what you use. - Time lives on a single continuous TAI counter; leap seconds, scale offsets and calendars are applied only at the boundary, so arithmetic can never land "inside" a leap second.
- Everything except
TrajectoryisCopyand allocation-free;#![forbid(unsafe_code)]. - Degrees, metres and seconds at the API; radians and internal nanoseconds never leak out.
Not in scope, by design: an orbit propagator (use sgp4 and feed
its states to a Trajectory), full IERS Earth-orientation (UT1/polar motion) modelling, map
projections, and the 2D/planar and terrain geometry that lives in the wider
geo ecosystem.
Develop
CI runs all of the above plus a build on the declared minimum Rust version (1.85).
Versioning and license
Plain semver; tags vX.Y.Z at the published commit; see CHANGELOG.md.
Apache-2.0, see LICENSE.