A long position track approximated by a sequence of contiguous, independently
fitted Orbis.GNSS.ReducedOrbit segments.
A single Orbis.GNSS.ReducedOrbit distills a whole track into one set of mean
elements; extrapolated over a day it drifts (GPS ~thousands of km with the
circular model, ~8 km eccentric). A Piecewise model instead splits the span
[t0, t1] into contiguous segments of :segment_s seconds and fits each one
with the existing Orbis.GNSS.ReducedOrbit.fit/2. Every query then lands
inside a fit window, so the error collapses to the in-window residual
(sub-km to a few km) rather than the extrapolation error — at the cost of
storing N small models. It is pure orchestration over the single-segment
primitives; the orbit math, frames, and time scales are unchanged.
This is the best accuracy-per-byte option for caching and transport. It is
not orbit determination and not a substitute for SP3 or SGP4 — it is a
compact approximation, honest about its residual through a source-backed
drift/3.
Models
Each segment is one of the two Orbis.GNSS.ReducedOrbit models, selected with the
same :model option:
:circular_secular(the default) — a circular orbit;:eccentric_secular— recovers the radiala·esignal.
See Orbis.GNSS.ReducedOrbit for the per-model details.
Size / accuracy tradeoff
Storage grows ~linearly with the number of segments: a span of T seconds
split at :segment_s holds ceil(T / segment_s) models, each serialized via
Orbis.GNSS.ReducedOrbit.to_map/1. Shorter segments cost more bytes but keep every
query closer to the centre of a fit window, shrinking the residual. The table
below is the max position error over a full day (model-vs-source, measured
through drift/3 against the vendored MGEX fixtures, GPST), where both the
single and piecewise models are fitted over that same full day and evaluated
across it. (Fitting only part of the span and extrapolating is far worse for
the single model — see Orbis.GNSS.ReducedOrbit; here the single model gets its
best case, the whole-span least-squares fit, and piecewise still wins.)
circular_secular
| Orbit class | single | 2 h pw | 4 h pw | 6 h pw |
|---|---|---|---|---|
| GPS, e ~ 0.024 (G21) | ~1 437 km | ~331 km | ~653 km | ~830 km |
| Galileo, e ~ 1e-4 (E01) | ~7 km | ~1 km | ~3 km | ~4 km |
| BeiDou MEO, e ~ 9e-4 (C21) | ~54 km | ~12 km | ~24 km | ~38 km |
| BeiDou IGSO, e ~ 5e-3 (C08) | ~533 km | ~50 km | ~102 km | ~147 km |
eccentric_secular
| Orbit class | single | 2 h pw | 4 h pw | 6 h pw |
|---|---|---|---|---|
| GPS, e ~ 0.024 (G21) | ~440 m | ~90 m | ~280 m | ~310 m |
| Galileo, e ~ 1e-4 (E01) | ~780 m | ~90 m | ~260 m | ~380 m |
| BeiDou MEO, e ~ 9e-4 (C21) | ~430 m | ~120 m | ~330 m | ~420 m |
| BeiDou IGSO, e ~ 5e-3 (C08) | ~870 m | ~20 m | ~70 m | ~130 m |
For the eccentric model the single whole-day fit is already sub-km; piecewise
still cuts the max error by roughly 3× to tens of × (most cells ~3-9×, the
near-circular IGSO as much as ~40×). For the circular model the unmodelled
a·e radial signal dominates and piecewise's benefit is largest in absolute
terms (hundreds of km off GPS/IGSO). Note the shorter the segment the smaller
the residual: the 2 h split beats the 4 h and 6 h splits in every cell, the
monotonic accuracy-for-bytes tradeoff.
These are characterisations, not guarantees; the exact numbers shift with the
fit window, segment length, and drift cadence. Always measure a given fit with
drift/3 against the source.
Segment selection
Segments tile [t0, t1] with no gaps. A query epoch is resolved by finding the
segment whose half-open interval [seg_t0, seg_t1) contains it; the final
segment is treated as inclusive at the very end so the exact end-of-span epoch
resolves to the last segment. An epoch exactly on an interior boundary resolves
to the later segment (where it is the in-window start), which is
deterministic. Selection is O(segments); the segment count is modest and the
ordered list is binary-searchable if it ever grows large. An epoch before t0
or after t1 returns {:error, :out_of_range}.
Persistence
to_map/1 emits a stable, versioned container (string keys, JSON-safe) holding
the per-segment maps via Orbis.GNSS.ReducedOrbit.to_map/1; from_map/1 validates
the version and model and reconstructs, with the same tagged-error discipline
as the single model.
Summary
Functions
Evaluate the piecewise model error against the source ephemeris over the whole span.
Fit a piecewise model over a span, one contiguous Orbis.GNSS.ReducedOrbit segment
per :segment_s seconds.
Reconstruct a piecewise model from a to_map/1 map. Validates the version and
model id.
Position of the piecewise model at epoch, ECEF (ITRF) meters by default.
Position and velocity of the piecewise model at epoch.
Select the segment whose coverage interval contains epoch.
Serialize a piecewise model to a stable, versioned, JSON-safe map (string
keys). Each segment's model is serialized via Orbis.GNSS.ReducedOrbit.to_map/1.
See from_map/1 for the inverse.
Types
@type epoch() :: Orbis.GNSS.ReducedOrbit.epoch()
@type segment() :: %{ t0: NaiveDateTime.t(), t1: NaiveDateTime.t(), model: Orbis.GNSS.ReducedOrbit.t() }
@type t() :: %Orbis.GNSS.ReducedOrbit.Piecewise{ frame: String.t(), model: String.t(), segment_s: number(), segments: [segment()], time_scale: String.t(), version: pos_integer(), window: {NaiveDateTime.t(), NaiveDateTime.t()} }
Functions
@spec drift( t(), Orbis.GNSS.SP3.t() | [{epoch(), {number(), number(), number()}}], keyword() ) :: {:ok, map()} | {:error, term()}
Evaluate the piecewise model error against the source ephemeris over the whole span.
Samples the source across the span and compares each truth sample to the
covering segment's ECEF position (the single-segment drift NIF is per-model, so
the piecewise report is composed in Elixir from position/3). For an
Orbis.GNSS.SP3 source it samples over :window (defaulting to the model's full
span) at :cadence_s for :satellite_id; for a sample list it uses those
directly. Returns
{:ok, %{per_epoch: [%{epoch:, error_m:}], max_m:, rms_m:, threshold_horizon:,
requested:, used:}}matching the single-segment Orbis.GNSS.ReducedOrbit.drift/3 report. Epochs outside
the model's span are skipped (counted in requested, not used).
threshold_horizon is the first epoch the ECEF error exceeds :threshold_m
(or nil).
@spec fit( Orbis.GNSS.SP3.t() | [{epoch(), {number(), number(), number()}}], keyword() ) :: {:ok, t()} | {:error, term()}
Fit a piecewise model over a span, one contiguous Orbis.GNSS.ReducedOrbit segment
per :segment_s seconds.
Sources
- an
Orbis.GNSS.SP3handle — requires:satellite_idand:window; each segment is fitted withOrbis.GNSS.ReducedOrbit.fit/2over its sub-window at:cadence_s; - a list of
{epoch, {x_m, y_m, z_m}}ECEF samples — partitioned by segment interval, each sublist fitted directly.
Options
:window—{t0, t1}epochs bounding the full span (t1strictly aftert0, else:invalid_window):segment_s— positive segment length in seconds, e.g.7200(non-positive →:invalid_segment):cadence_s— positive SP3 sampling step in seconds:satellite_id— e.g."G05"(SP3 source):model—:circular_secular(default) or:eccentric_secular:time_scale— for the sample-list source, the scale its epochs are in
Segments are contiguous (seg_t1 of one is seg_t0 of the next); the final
segment may be shorter. A :segment_s at least the full span yields a single
segment equal to the whole window (piecewise with one segment ≡ single).
Returns {:ok, %Orbis.GNSS.ReducedOrbit.Piecewise{}} or a tagged error. The error
set is exactly the single model's fit errors ({:too_few_samples, got, req},
:invalid_window, :invalid_cadence, :satellite_id_required,
{:unsupported_model, m}, {:unsupported_source_frame, f},
{:unsupported_time_scale, s}, :transform_unavailable, …) plus
:invalid_segment. A too-few-samples failure on a non-terminal segment is
surfaced (a genuinely under-covered interior span is an error, not a silent
hole); only the terminal short segment may be dropped. If nothing fits at all,
{:error, {:too_few_samples, 0, 4}}.
Reconstruct a piecewise model from a to_map/1 map. Validates the version and
model id.
Returns {:ok, %Orbis.GNSS.ReducedOrbit.Piecewise{}} or
{:error, {:unsupported_version, v}} / {:error, {:unsupported_model, m}} /
{:error, :malformed_map}. A segment whose inner model fails from_map, or
whose model id differs from the container, makes the whole map malformed —
never a raise, never a nil-filled struct.
@spec position(t(), epoch(), keyword()) :: {:ok, Orbis.GNSS.ReducedOrbit.vec3()} | {:error, term()}
Position of the piecewise model at epoch, ECEF (ITRF) meters by default.
Selects the segment covering epoch and delegates to
Orbis.GNSS.ReducedOrbit.position/3. Pass frame: :gcrs for the inertial position.
An epoch outside the full span returns {:error, :out_of_range}.
Position and velocity of the piecewise model at epoch.
Selects the covering segment and delegates to
Orbis.GNSS.ReducedOrbit.position_velocity/3. An epoch outside the full span
returns {:error, :out_of_range}.
Select the segment whose coverage interval contains epoch.
Returns {:ok, segment} or {:error, :out_of_range}. Interior boundaries
resolve to the later segment; the exact end-of-span epoch resolves to the last
segment.
Serialize a piecewise model to a stable, versioned, JSON-safe map (string
keys). Each segment's model is serialized via Orbis.GNSS.ReducedOrbit.to_map/1.
See from_map/1 for the inverse.