SP3-c / SP3-d precise-ephemeris products (IGS precise orbits + clocks).
This is the Elixir surface over the sidereon-core SP3 parser and
scipy.interpolate-matched position/clock interpolation. It is not the
JPL-SPK reader (Sidereon.Ephemeris): SP3 carries GNSS satellite states in the
ITRF/IGS ECEF frame, in meters, tagged by a GNSS satellite id like "G01".
A file is parsed once into a resource handle held by the BEAM; evaluation operates on that handle and never re-reads the file.
Example
{:ok, sp3} = Sidereon.GNSS.SP3.load("/path/to/igs.sp3")
{:ok, state} =
Sidereon.GNSS.SP3.position(sp3, "G01", ~N[2020-06-24 00:00:00])
state.x_m # ITRF/IGS ECEF X, meters
state.clock_s # satellite clock offset, seconds (or nil if no estimate)Epochs
The query epoch is interpreted in the file's own time scale (read from the
SP3 header, typically GPST). Pass a NaiveDateTime or a
{{year, month, day}, {hour, minute, second}} tuple; it is converted to the
split Julian date with the same midnight-boundary convention the parser uses
(no leap-second shifting; the epoch stays in the file's scale).
Summary
Functions
Return a copy of other with its clocks shifted onto reference's clock datum
(the clock-datum primitive, applied).
Estimate the per-epoch reference-clock offset of other relative to
reference (the clock-datum primitive).
Return the product coverage interval.
Number of parsed epochs held by the SP3 product.
Return the parsed SP3 epoch grid as seconds since J2000.
Interpolate one satellite at J2000-second epochs in the product time scale.
Load and parse an SP3-c / SP3-d file into a product handle.
Like load/1 but raises on failure.
Merge several SP3 products from different analysis centers into one consistent precise-ephemeris dataset.
Build the versioned stable identity for exact SP3 artifacts and merge policy.
Parse an in-memory SP3 byte buffer (already decompressed) into a handle.
Interpolate the state of satellite sat_id at epoch.
Extract the product as the canonical precise-ephemeris samples, in SI units, one per real position record in ascending epoch order.
Build canonical precise-interpolant artifact bytes from this SP3 product.
Return observed/predicted status derived from the SP3 record flags.
Return the SP3/RINEX satellite identifiers declared by the product header.
Alias for satellite_ids/1, matching the Python/WASM satellites accessor.
Return the exact parsed state of sat_id at epoch_index.
Return all exact parsed states at epoch_index.
Serialize the product to standard SP3-c / SP3-d text as iodata. Pure, no I/O.
Serialize the product to an SP3 text binary.
Types
Functions
Return a copy of other with its clocks shifted onto reference's clock datum
(the clock-datum primitive, applied).
At every epoch the offset could be estimated, each clocked satellite's offset
has the datum subtracted, so the result's clocks are directly comparable to
reference's. Positions are untouched. Epochs without an estimate are left
unchanged. The returned product interpolates like any other SP3.
Returns {:ok, %Sidereon.GNSS.SP3{}} or {:error, reason}.
Options
:min_common: minimum common clocked satellites per epoch (default5)
Estimate the per-epoch reference-clock offset of other relative to
reference (the clock-datum primitive).
Precise clock products from different centers are referenced to different
station/ensemble clocks, so their raw clocks differ by a per-epoch common
offset that drifts over the day. This returns that datum: a list of maps
%{jd_whole: float, jd_fraction: float, offset_s: float, satellites: integer},
one per epoch where at least :min_common common clocked satellites let the
(robust median) offset be estimated. Subtract offset_s from other's clocks
to put both products on reference's datum. Orbit positions need no such
treatment; every center reports ITRF center-of-mass coordinates.
Options
:min_common: minimum common clocked satellites per epoch (default5)
Return the product coverage interval.
The start and end are the first and last SP3 node epochs, expressed as
seconds since J2000 in the product's own time scale. Public evaluators reject
epochs outside this interval by default; pass extrapolate: true to the
evaluator to opt into the lower-level interpolation behavior.
@spec epoch_count(t()) :: non_neg_integer()
Number of parsed epochs held by the SP3 product.
This is the count of actual * epoch nodes parsed from the file, not just the
header declaration. The value matches length(epochs_j2000_seconds(sp3)) for
ordinary SP3 products.
Return the parsed SP3 epoch grid as seconds since J2000.
Values are in the product's own time scale, ascending, and correspond exactly to the parsed SP3 node epochs. Use this accessor when a caller needs the original sample grid rather than an interpolated state.
@spec interpolate(t(), String.t(), [number()]) :: {:ok, Sidereon.GNSS.PreciseEphemeris.StateBatch.t()} | {:error, term()}
Interpolate one satellite at J2000-second epochs in the product time scale.
This returns the same Sidereon.GNSS.PreciseEphemeris.StateBatch used by the
precise-interpolant accessors.
Load and parse an SP3-c / SP3-d file into a product handle.
Returns {:ok, %Sidereon.GNSS.SP3{}} or {:error, reason}. The file is read and
parsed exactly once; the parsed product is held as a resource handle.
Like load/1 but raises on failure.
Merge several SP3 products from different analysis centers into one consistent precise-ephemeris dataset.
sources is a list of loaded products in precedence order (earlier wins
ties). This is orthogonal to time-stitching: it combines providers at the same
epochs on one shared time grid. Mixed-cadence products are unioned onto the
finest input cadence by default, using only records actually present in an
input and never interpolating. For every (epoch, satellite) cell:
- Union satellite coverage: a satellite present in any input may appear in the merged product wherever a source actually carries it.
- Consensus: the largest subset of sources agreeing within tolerance is
combined; sources outside it are recorded as outliers. A cell with no
agreeing subset of
:min_agreeis quarantined (omitted), never averaged across disagreeing centers. A lone source is carried through. - Cell precedence: with
combine: :precedence, the earliest source present in each cell wins, so a lower-precedence source fills a preferred source's missing cell. Setprecedence_scope: :satellite_arcto retain one owner for a whole satellite arc. - Optional precedence guard:
:outlier_rejectmakes a contested precedence cell require a mutually agreeing cluster of at leastmax(:min_agree, 2)sources. A corrupt preferred value is replaced by the earliest member of the deterministic largest cluster and recorded.
Returns {:ok, %Sidereon.GNSS.SP3{}, report} or {:error, reason}, where
report is a map with :quarantined, :single_source, and
:position_outliers, and :clock_outliers lists. Each entry is a map
%{satellite: "G03", jd_whole: float, jd_fraction: float, sources: [0, 2]}
(sources are zero-based indices into sources).
report.agreement quantifies how tightly the consensus sources clustered about
the combined product. It is a map with the whole-product aggregates
:position_rms_m, :position_max_m, :clock_rms_s, and :clock_max_s, plus
:cells (per-(epoch, satellite) statistics, one per accepted cell) and
:epochs (per-epoch aggregates). The RMS fields are nil when no
multi-source consensus exists for that channel. Position maximum covers every
accepted cell; clock maximum covers every clock-bearing accepted cell, so
either maximum may be 0.0 for a single-source cell. The clock fields of a
cell are nil only when the cell carries no clock.
Options
:position_tolerance_m: position agreement tolerance, meters (default0.5):clock_tolerance_s: clock agreement tolerance, seconds (default5.0e-9):min_agree: agreeing sources required to accept a contested cell (default2):clock_min_common: common clocked satellites for the clock-datum estimate (default5):combine::mean(default),:median, or:precedence:precedence_scope::cell(default) or:satellite_arc:outlier_reject:nil(default/current behavior), or a map/keyword list with:position_mand:clock_nstolerances:epoch_interval_s: require this target epoch interval, seconds:systems: restrict output to systems such as[:gps]or["G", "E"]:asserted_frame_label_sets: coordinate-label sets the caller asserts are equivalent without frame math:helmert: enable catalog Helmert reconciliation for known ITRF/IGS labels
Build the versioned stable identity for exact SP3 artifacts and merge policy.
Contributor order and map order do not affect mean or median identity. With
combine: :precedence, source order is an effective policy control and is
therefore bound in order. Every contributor must carry complete
requested/resolved identity, distributor, product and archive digests and
lengths, official filename, and compression. Cache and HTTP observations are
intentionally excluded. The returned map includes core's complete canonical
:contributors and, for precedence combination, the ordered
:precedence_contributors.
Parse an in-memory SP3 byte buffer (already decompressed) into a handle.
@spec position(t(), String.t(), NaiveDateTime.t() | tuple(), keyword()) :: {:ok, Sidereon.GNSS.SP3.State.t()} | {:error, term()}
Interpolate the state of satellite sat_id at epoch.
sat_id is the canonical SP3/RINEX token, e.g. "G01" (GPS PRN 1), "E12",
"C30". epoch is a NaiveDateTime or a
{{year, month, day}, {hour, minute, second}} tuple, interpreted in the
file's own time scale.
By default, epochs outside the parsed SP3 node coverage return
{:error, :outside_coverage}. Pass extrapolate: true to opt into the
lower-level interpolation behavior near the product edges.
Returns {:ok, %Sidereon.GNSS.SP3.State{}} or {:error, reason}.
@spec precise_ephemeris_samples(t()) :: [Sidereon.GNSS.PreciseEphemerisSample.t()]
Extract the product as the canonical precise-ephemeris samples, in SI units, one per real position record in ascending epoch order.
Each element is a Sidereon.GNSS.PreciseEphemerisSample carrying the satellite
token, the epoch (split Julian date tagged with the product's time scale), the
ECEF position in meters, the optional clock in seconds, and the SP3 E
clock-event flag. Round-tripping these back through
Sidereon.GNSS.PreciseEphemeris.from_samples/1 rebuilds the same interpolatable
source.
Examples
{:ok, sp3} = Sidereon.GNSS.SP3.load("igs.sp3")
samples = Sidereon.GNSS.SP3.precise_ephemeris_samples(sp3)
{:ok, source} = Sidereon.GNSS.PreciseEphemeris.from_samples(samples)
Build canonical precise-interpolant artifact bytes from this SP3 product.
Return observed/predicted status derived from the SP3 record flags.
:epochs contains one entry per parsed epoch with :observed,
:orbit_predicted_satellites, and :clock_predicted_satellites. The
:observed_through split-Julian-date map is the last epoch before the first
predicted record, or the final epoch for a fully observed product. It is
nil when the first epoch is already predicted or the product is empty.
This metadata uses the file's actual per-record P flags and never assumes a
fixed ultra-rapid observed duration. Per-cell flags are also available from
state/3 and states_at/2.
Return the SP3/RINEX satellite identifiers declared by the product header.
These are canonical three-character tokens such as "G01", "E12", or
"C30". The list is read from the already-loaded SP3 handle; no file I/O or
interpolation is performed.
Examples
{:ok, sp3} = Sidereon.GNSS.SP3.parse(sp3_bytes)
ids = Sidereon.GNSS.SP3.satellite_ids(sp3)
"G01" in ids
Alias for satellite_ids/1, matching the Python/WASM satellites accessor.
@spec state(t(), String.t(), non_neg_integer()) :: {:ok, Sidereon.GNSS.SP3.State.t()} | {:error, term()}
Return the exact parsed state of sat_id at epoch_index.
epoch_index is zero-based into epochs_j2000_seconds/1. This accessor does
no interpolation: the returned state is the record stored in the SP3 file,
including optional velocity, optional clock-rate, and the SP3 status flags.
Missing all-zero orbit records are not fabricated; querying such a cell returns
{:error, {:unknown_satellite, sat_id}}.
Returns {:ok, %Sidereon.GNSS.SP3.State{}} or {:error, reason}.
@spec states_at(t(), non_neg_integer()) :: {:ok, [{String.t(), Sidereon.GNSS.SP3.State.t()}]} | {:error, term()}
Return all exact parsed states at epoch_index.
The result is an ascending satellite-id list of {satellite_id, state} pairs
for records actually present at that SP3 epoch. Satellites whose position
record is the SP3 missing-orbit sentinel are absent from the list.
Returns {:ok, [{satellite_id, %Sidereon.GNSS.SP3.State{}}]} or
{:error, reason}.
Serialize the product to standard SP3-c / SP3-d text as iodata. Pure, no I/O.
This is the inverse of load/1 / parse/1: a read → (merge/2) → write
pipeline round-trips to a single standard SP3 file any reader consumes. The
output is deterministic (same product → identical bytes). Header fields
(version, epoch count, satellite list, time system, week / seconds-of-week /
MJD / interval) are derived from the product. A satellite absent at an epoch is
written as the SP3 missing-orbit sentinel, so a quarantined merge/2 cell
re-reads as missing, never a fabricated position.
Examples
{:ok, sp3} = Sidereon.GNSS.SP3.load("igs.sp3")
iodata = Sidereon.GNSS.SP3.to_iodata(sp3)
{:ok, reparsed} = Sidereon.GNSS.SP3.parse(IO.iodata_to_binary(iodata))
Sidereon.GNSS.SP3.satellite_ids(reparsed) == Sidereon.GNSS.SP3.satellite_ids(sp3)
#=> true
Serialize the product to an SP3 text binary.