Product-staleness graceful degradation for time-varying GNSS products.
Time-varying products (IONEX vertical-TEC maps, rapid/predicted SP3 orbit and
clock files) publish with latency and gaps, so the product for the exact
requested epoch is not always on hand. This is the Elixir surface over the
sidereon-core selection layer: given a SET of already-parsed products and a
requested epoch (or epoch range), it returns a usable product plus a
Sidereon.GNSS.Staleness.StalenessMetadata describing which source epoch was
used and how stale it is, falling back to the most-recent product within a
configurable staleness cap. A request that would rely on a product older than
the cap fails with a typed error instead of returning data past the cap, so a
degraded answer is never substituted silently.
This layer is pure and does no networking: it selects among products the
caller has already parsed (Sidereon.GNSS.SP3.load/1,
Sidereon.GNSS.Ionosphere.load_ionex/1). Fetching the products is a separate,
per-binding concern.
Degradation paths
:exact- a product covers the requested epoch; it is returned untouched, so the downstream evaluation is bit-for-bit identical to querying it directly. Staleness is zero.:nearest_prior(SP3) - no product covers the epoch, so the most-recent prior product is used as-is, with staleness measured from its last epoch.:diurnal_shift(IONEX) - no product covers the requested day, so a prior day's grid is advanced by whole days onto the requested epoch (TEC is approximately 24-hour periodic). Only the epoch axis moves; grid values are unchanged.
Epochs
Epochs are a NaiveDateTime or {{y, m, d}, {h, min, s}} tuple, interpreted
in the product's own time scale (no leap-second shifting), and converted to
seconds since J2000 via Sidereon.GNSS.Time. The IONEX map-epoch axis is
integer seconds, so an IONEX request must be a whole-second epoch.
Summary
Types
An epoch as a NaiveDateTime or {{y, m, d}, {h, min, s}} tuple.
A typed selection failure. info for :beyond_staleness_cap is a map with the
requested/source epochs, the staleness, and the cap, all in J2000 seconds.
Functions
Select an IONEX product usable at epoch, degrading to a diurnal-shifted prior
product within policy.
Select an IONEX product usable across [start_epoch, end_epoch].
Select the SP3 product to use for epoch, degrading to the most-recent prior
product within policy.
Select an SP3 product usable across [start_epoch, end_epoch].
Types
@type epoch() :: NaiveDateTime.t() | tuple()
An epoch as a NaiveDateTime or {{y, m, d}, {h, min, s}} tuple.
@type selection_error() :: :empty_product_set | {:invalid_range, float(), float()} | {:no_prior_product, float()} | {:beyond_staleness_cap, map()} | {:invalid_product, String.t()} | {:invalid_policy, float()} | {:overflow, String.t()}
A typed selection failure. info for :beyond_staleness_cap is a map with the
requested/source epochs, the staleness, and the cap, all in J2000 seconds.
Functions
@spec select_ionex([reference()], epoch(), Sidereon.GNSS.Staleness.Policy.t()) :: {:ok, Sidereon.GNSS.Staleness.IonexSelection.t()} | {:error, term()}
Select an IONEX product usable at epoch, degrading to a diurnal-shifted prior
product within policy.
handles is a list of parsed-IONEX references from
Sidereon.GNSS.Ionosphere.parse_ionex/1 or load_ionex/1. The IONEX axis is
integer seconds, so epoch must be a whole-second epoch. Returns
{:ok, %Sidereon.GNSS.Staleness.IonexSelection{}} or {:error, reason}.
@spec select_ionex_over_range( [reference()], epoch(), epoch(), Sidereon.GNSS.Staleness.Policy.t() ) :: {:ok, Sidereon.GNSS.Staleness.IonexSelection.t()} | {:error, term()}
Select an IONEX product usable across [start_epoch, end_epoch].
See select_ionex/3; this is the range case.
@spec select_sp3([Sidereon.GNSS.SP3.t()], epoch(), Sidereon.GNSS.Staleness.Policy.t()) :: {:ok, Sidereon.GNSS.Staleness.Sp3Selection.t()} | {:error, term()}
Select the SP3 product to use for epoch, degrading to the most-recent prior
product within policy.
An :exact result covers epoch; a :nearest_prior result is the best
in-cap candidate and may end before epoch (its coverage gap is the reported
staleness), so a downstream solve against it can still fail to serve epoch.
products is a list of parsed Sidereon.GNSS.SP3 products. Returns
{:ok, %Sidereon.GNSS.Staleness.Sp3Selection{}} or {:error, reason}.
@spec select_sp3_over_range( [Sidereon.GNSS.SP3.t()], epoch(), epoch(), Sidereon.GNSS.Staleness.Policy.t() ) :: {:ok, Sidereon.GNSS.Staleness.Sp3Selection.t()} | {:error, term()}
Select an SP3 product usable across [start_epoch, end_epoch].
See select_sp3/3; this is the range case, where staleness is measured to the
range end (the most-stale point).