Orbis.GNSS.ReducedOrbit (Orbis v0.9.1)

Copy Markdown View Source

A compact, fitted mean-element approximation of a satellite's orbit.

Orbis.GNSS.ReducedOrbit distills a position track — from a precise SP3 product or a list of ECEF samples — into a handful of mean elements that reproduce the motion cheaply, for caching, transport, and quick visibility math. It is not orbit determination and not a substitute for SGP4 or precise ephemeris: it deliberately discards short-period structure and is honest about the error it leaves behind (rms_m/max_m on the fit, and a source-backed drift/3).

Models

Two models are available, chosen with the :model option on fit/2:

  • :circular_secular (the default) — a circular orbit; best for near-circular orbits (Galileo).
  • :eccentric_secular — adds eccentricity through a nonsingular (h, k) parameterization, recovering the radial a·e signal (~hundreds of km for GPS/BeiDou) while degrading smoothly to the circular model as e -> 0.

circular_secular

A circular orbit (eccentricity fixed at zero) whose orbital plane precesses at a constant nodal rate. At an offset dt = t - t0 from the reference epoch the angles advance linearly,

u(t)    = arg_lat0 + n * dt          # argument of latitude
raan(t) = raan0    + raan_rate * dt
e       = 0

and the inertial (GCRS) position is the in-plane circle rotated by the node and inclination, r = Rz(raan) * Rx(i) * a * [cos u, sin u, 0].

The nodal rate raan_rate is fitted, but seeded from the J2 secular nodal regression (Vallado, Fundamentals of Astrodynamics and Applications):

raan_rate_j2 = -1.5 * n * J2 * (Re / a)^2 * cos(i)

Both the fitted value (raan_rate_rad_s) and the J2 seed (raan_rate_j2_rad_s) are kept; raan_rate_mode is "fitted_j2_seeded". The model does not claim to be a pure J2 propagation.

eccentric_secular

Eight free elements: the four circular plane elements plus h = e·sin ω, k = e·cos ω, L0 (mean argument of latitude at epoch), and n. Derived e = sqrt(h² + k²) and ω = atan2(h, k). At an offset dt the model advances λ = L0 + n·dt, forms the mean anomaly M = λ − ω, solves Kepler's equation E − e·sin E = M, and places the satellite at radius r = a(1 − e·cos E) and argument of latitude u = ω + ν. The (h, k) form is nonsingular: at e = 0 it reproduces circular_secular exactly with arg_lat0 = L0. The struct then carries h, k, e, and arg_perigee_rad (= ω).

Frames

Fitting and evaluation run internally in GCRS; positions are returned in ECEF (ITRF) meters by default, or GCRS via frame: :gcrs. ECEF velocity includes the Earth-rotation transport term. Sample/query epochs are interpreted consistently for the Earth-rotation conversion; the ECEF product (the primary output) is self-consistent across the fit, evaluation, and drift.

Expected accuracy

Representative drift (extrapolated model-vs-source position error): a fit to the first ~6 hours of an MGEX SP3 track, drifted over the rest of the day. Numbers are the max position error over the full day, measured against vendored MGEX products (GRG for GPS/Galileo, a trimmed GBM product for BeiDou):

Orbit classcircular_seculareccentric_secular
GPS, e ~ 0.024 (G21)~8 100 km~8 km
GPS, e ~ 0.020 (G02)~9 400 km~11 km
BeiDou IGSO, e ~ 5e-3 (C08)~2 200 km~12 km
BeiDou MEO, e ~ 9e-4 (C21)~140 km~5 km
Galileo, e ~ 1e-4 (E01)~8 km~8 km (≈ circular)

Reading of the table:

  • For eccentric orbits the circular model is unusable — the unmodelled radial a·e signal compounds badly under extrapolation. :eccentric_secular is the recommended model and brings the error down by one-to-three orders of magnitude. Note this holds even for the small eccentricities of BeiDou MEO/IGSO (e ~ 1e-3 to 5e-3): a few-hundred-metre to ~200 km radial signal still wrecks the circular extrapolation, so eccentric wins there too.
  • For near-circular orbits (Galileo, e ~ 1e-4) both models are comparable: the eccentric model is essentially identical to the circular one (~8 km either way), so it does not regress and is a safe default when the orbit class is unknown.

These figures are measured through drift/3 (the public NIF-backed pipeline, GPST) against the vendored fixtures; the exact numbers shift slightly with the fit window and drift cadence.

These are characterisations, not guarantees: always measure a given fit with drift/3 against the source. LEO accuracy is characterised once the SGP4/TLE source lands. This is a compact approximation for caching / visibility, never a substitute for SP3 or SGP4.

Persistence

to_map/1 emits a stable, versioned map (frame, model, units, epoch scale, elements, fit stats) that from_map/1 reads back, for caching/transport.

Summary

Functions

Evaluate the model error against the source ephemeris over a horizon.

Fit a mean-element model to a source orbit.

Reconstruct a model from a to_map/1 map. Validates the version and model id.

Position of the model at epoch, ECEF (ITRF) meters by default.

Position and velocity of the model at epoch.

Serialize a fitted model to a stable, versioned map (string keys) for caching or transport. See from_map/1 for the inverse.

Types

epoch()

@type epoch() ::
  NaiveDateTime.t()
  | {{integer(), integer(), integer()}, {integer(), integer(), number()}}

t()

@type t() :: %Orbis.GNSS.ReducedOrbit{
  a_m: float(),
  arg_lat_rad: float(),
  arg_perigee_rad: float() | nil,
  e: float(),
  epoch: NaiveDateTime.t(),
  fit: map(),
  frame: String.t(),
  h: float() | nil,
  i_rad: float(),
  k: float() | nil,
  mean_motion_rad_s: float(),
  model: String.t(),
  raan_rad: float(),
  raan_rate_j2_rad_s: float(),
  raan_rate_mode: String.t(),
  raan_rate_rad_s: float(),
  time_scale: String.t(),
  version: pos_integer()
}

vec3()

@type vec3() :: %{x_m: float(), y_m: float(), z_m: float()}

Functions

drift(model, sp3, opts)

@spec drift(
  t(),
  Orbis.GNSS.SP3.t() | [{epoch(), {number(), number(), number()}}],
  keyword()
) ::
  {:ok, map()} | {:error, term()}

Evaluate the model error against the source ephemeris over a horizon.

This compares the model to fresh truth samples (not to itself): for an Orbis.GNSS.SP3 source it samples the product over :window at :cadence_s for :satellite_id; for a list of {epoch, {x_m, y_m, z_m}} samples it uses those directly. Returns

{:ok, %{per_epoch: [%{epoch:, error_m:}], max_m:, rms_m:, threshold_horizon:,
        requested:, used:}}

where threshold_horizon is the first epoch the ECEF error exceeds :threshold_m (or nil if it never does / no threshold given).

fit(source, opts \\ [])

@spec fit(
  Orbis.GNSS.SP3.t() | [{epoch(), {number(), number(), number()}}],
  keyword()
) :: {:ok, t()} | {:error, term()}

Fit a mean-element model to a source orbit.

Sources

  • an Orbis.GNSS.SP3 handle — requires :satellite_id and :window; samples the product at :cadence_s over the window;
  • a list of {epoch, {x_m, y_m, z_m}} ECEF samples — the window is taken from the samples; :frame must be :ecef (the default).

Options

  • :model:circular_secular (default) or :eccentric_secular. The circular model fixes eccentricity at zero (best for near-circular orbits); the eccentric model recovers the a·e radial signal (recommended for GPS and other eccentric orbits). See the moduledoc accuracy table.
  • :satellite_id — e.g. "G05" (SP3 source)
  • :window{t0, t1} epochs bounding the fit (SP3 source)
  • :cadence_s — positive sampling step in seconds (SP3 source, default 900)
  • :frame — for the sample-list source, :ecef (default)
  • :time_scale — for the sample-list source, the scale its epochs are in ("UTC" default, e.g. "GPST"); SP3 sources use the product's own scale

Epochs are interpreted in the model's time scale (recorded on the result). The reference epoch t0 is the earliest sample, so the result is independent of the caller's sample order.

Returns {:ok, %Orbis.GNSS.ReducedOrbit{}} or a tagged error: {:too_few_samples, got, required}, :invalid_window, :invalid_cadence, :satellite_id_required, :singular_plane_fit, :raan_ambiguous, {:unsupported_source_frame, frame}, {:unsupported_model, model}, :transform_unavailable, :fit_did_not_converge.

from_map(map)

@spec from_map(map()) :: {:ok, t()} | {:error, term()}

Reconstruct a model from a to_map/1 map. Validates the version and model id.

Returns {:ok, %Orbis.GNSS.ReducedOrbit{}} or {:error, {:unsupported_version, v}} / {:error, {:unsupported_model, model}} / {:error, :malformed_map}.

position(model, epoch, opts \\ [])

@spec position(t(), epoch(), keyword()) :: {:ok, vec3()} | {:error, term()}

Position of the model at epoch, ECEF (ITRF) meters by default.

Pass frame: :gcrs for the inertial position. Returns {:ok, %{x_m:, y_m:, z_m:}} or {:error, reason}.

position_velocity(model, epoch, opts \\ [])

@spec position_velocity(t(), epoch(), keyword()) :: {:ok, map()} | {:error, term()}

Position and velocity of the model at epoch.

ECEF velocity includes the Earth-rotation transport term. Returns {:ok, %{position: %{x_m:, y_m:, z_m:}, velocity: %{vx_m_s:, vy_m_s:, vz_m_s:}}}.

to_map(m)

@spec to_map(t()) :: map()

Serialize a fitted model to a stable, versioned map (string keys) for caching or transport. See from_map/1 for the inverse.