GNSS single-point positioning (SPP): recover a receiver position, clock bias, and geometry diagnostics from one epoch of pseudorange observations against a precise SP3 ephemeris or a broadcast navigation product.
This is the Elixir surface over the sidereon-core SPP solver. Given an
ephemeris source, an Sidereon.GNSS.SP3 product or an Sidereon.GNSS.Broadcast
handle (GPS / Galileo / BeiDou / GLONASS), a set of single-frequency
pseudoranges, the receive epoch, and the broadcast/atmosphere parameters, it
runs the transmit-time iteration and trust-region least-squares solve in the
crate and returns an Sidereon.GNSS.Positioning.Solution. A mixed-constellation
set is solved together with one receiver clock per system. No positioning math
lives on the Elixir side; this module marshals units and epoch arguments and
decodes the result.
Units at the boundary
- pseudoranges and the initial guess position/clock are meters;
- the recovered
positionis ITRF/IGS ECEF meters, matching the SP3 frame; geodeticlatitude/longitude are radians and height is meters;rx_clock_sis seconds;- pressure is hPa, temperature is kelvin, relative humidity is a
fraction in
[0, 1]; - the Klobuchar
alpha/betacoefficients are passed in their broadcast units.
The epoch is interpreted in the SP3 product's own time scale (typically GPST);
no leap-second shifting is applied. The seconds-since-J2000, second-of-day, and
fractional day-of-year arguments the crate needs are derived from the supplied
epoch via Sidereon.GNSS.Time.
Example
{:ok, sp3} = Sidereon.GNSS.SP3.load("igs.sp3")
observations = [{"G01", 2.41e7}, {"G02", 2.49e7}, {"G05", 2.05e7}, {"G07", 2.30e7}]
{:ok, solution} =
Sidereon.GNSS.Positioning.solve(sp3, observations, ~N[2020-06-24 12:00:00],
ionosphere: true,
troposphere: true,
klobuchar_alpha: {1.0e-8, 2.2e-8, -6.0e-8, -1.2e-7},
klobuchar_beta: {96_256.0, 131_072.0, -65_536.0, -589_824.0}
)
solution.position.x_m
solution.rx_clock_s
Summary
Types
One epoch's request in a solve_batch/3 call: a {observations, epoch} pair,
or a {observations, epoch, epoch_opts} triple whose epoch_opts are merged
over the batch-wide options for that epoch only.
An epoch as a NaiveDateTime or {{y, m, d}, {h, min, s}} tuple.
A {satellite_id, pseudorange_m} pseudorange observation.
Functions
Solve single-point positioning for one receive epoch.
Solve many independent SPP epochs against one shared SP3 product in a single call.
Solve single-point positioning from broadcast ephemeris ALONE.
Solve preferring precise SP3 products, falling back to broadcast ephemeris, reporting which source produced the fix and how stale it is.
Types
@type batch_request() :: {[observation()], epoch()} | {[observation()], epoch(), keyword()}
One epoch's request in a solve_batch/3 call: a {observations, epoch} pair,
or a {observations, epoch, epoch_opts} triple whose epoch_opts are merged
over the batch-wide options for that epoch only.
@type epoch() :: NaiveDateTime.t() | tuple()
An epoch as a NaiveDateTime or {{y, m, d}, {h, min, s}} tuple.
A {satellite_id, pseudorange_m} pseudorange observation.
Functions
@spec solve( Sidereon.GNSS.SP3.t() | Sidereon.GNSS.Broadcast.t(), [observation()], epoch(), keyword() ) :: {:ok, Sidereon.GNSS.Positioning.Solution.t()} | {:error, term()}
Solve single-point positioning for one receive epoch.
source is a loaded ephemeris product, an Sidereon.GNSS.SP3 precise product or an
Sidereon.GNSS.Broadcast broadcast-navigation product. observations is a
list of {satellite_id, pseudorange_m} pairs (ids like "G01", pseudoranges
in meters), and epoch is a NaiveDateTime or {{y, m, d}, {h, min, s}}
tuple in the product's time scale.
Options
:ionosphere- apply the broadcast Klobuchar ionosphere correction (defaultfalse); the L1 delay is scaled to each satellite's carrier by(f_L1/f)^2, covering GPS L1, Galileo E1, and BeiDou B1I. A GLONASS satellite's FDMA carrier is resolved per satellite from:glonass_channels; a GLONASS observation with the ionosphere requested but no (or out-of-range) channel is rejected with{:ionosphere_unsupported, sat}.:glonass_channels- the GLONASS FDMA channel map%{slot => channel}(default%{}), whereslotis the GLONASS satellite slot/PRN andchannelis its FDMA frequency channelk(valid[-7, +6]), as carried in the broadcast navfreq_channelfield or a RINEXGLONASS SLOT / FRQ #header. Used only to resolve the GLONASS carrier for the(f_L1/f_k)^2ionosphere scaling, so it matters only when:ionosphereistrueand the set contains GLONASS observations; an empty map leaves every other constellation bit-identical. A value that is not a%{integer => integer}map returns{:error, {:invalid_option, :glonass_channels}}.:troposphere- apply the Saastamoinen/Niell troposphere correction (defaultfalse):klobuchar_alpha- broadcast alpha coefficients, 4-tuple (default zeros):klobuchar_beta- broadcast beta coefficients, 4-tuple (default zeros):pressure_hpa- surface pressure, hPa (default1013.25):temperature_k- surface temperature, kelvin (default288.15):relative_humidity- relative humidity fraction[0, 1](default0.5):initial_guess-{x_m, y_m, z_m, b_m}start point (default all zeros):with_geodetic- also return the geodetic position (defaulttrue):max_pdop- optional positive PDOP ceiling. When set, a fix whose geometry is rank-deficient or whose PDOP exceeds the ceiling is refused with{:error, {:degenerate_geometry, pdop}}(a non-positive value is{:error, {:invalid_option, :max_pdop}}); default unset.:robust- run the core robust SPP FDE driver. Requires an explicit:weightsmap unless:unsafe_unit_weightsis set.
Optional convergence aids
:coarse_search- cold-start convergence-basin widening for a degraded or absent position prior (defaultnil= off, exact single solve from:initial_guess). The crate freezes its elevation mask and weights at the seed geometry, so a seed far from the true surface point (the{0,0,0,0}earth-center default, an antipodal last-known fix) either starves on the horizon or is refused by the integrity gates. When set, the solve runs once from each of a deterministic golden-spiral lattice of near-surface seeds (plus the caller's:initial_guess), routes every per-seed candidate through the same integrity gates, and selects the best redundant (redundancy >= 1) converged fix, so no hardcoded prior is needed. Acceptstrue(the default seed count), a positive integer seed count, or a keyword list[seeds: n]. Each seed is one extra crate solve, so the cost scales with the seed count; leave it off on the hot path where a good prior is known. Composes with the integrity gates (including:max_pdop) per candidate. A non-positive or non-integer value returns{:error, {:invalid_option, :coarse_search}}.:huber- whentrue(defaultfalse), apply opt-in crate-layer Huber/IRLS robust reweighting: the per-satellite weight is recomputed each outer iteration from the post-fit residual. Tuning, used only when:huberis set::huber_k(Huber constant, default1.345),:huber_sigma(MAD scale floor in metres, default1.0, the canonical core robust scale floor),:huber_max_iter(outer-loop cap, default5). A malformed:huber/:huber_k/:huber_sigma/:huber_max_itervalue returns{:error, {:invalid_option, key}}before any solve.
Regardless of options, a fix that did not converge to a physical receiver
position is refused rather than returned: one whose geocentric radius is
outside the plausible band (for example from the earth-center default seed, or
a wrong-root least-squares fix) gives {:error, {:implausible_position, radius_m}},
and a converged-flagged fix whose post-fit residual RMS is physically
implausible gives {:error, {:no_convergence, rms_m}}.
A mixed GPS+Galileo+BeiDou+GLONASS observation set is solved together with one receiver clock per GNSS (an inter-system bias is the difference between a system's clock and the reference system's), and dilution of precision is reported for the combined geometry as well.
Returns {:ok, %Sidereon.GNSS.Positioning.Solution{}} or {:error, reason},
where reason is one of {:too_few_satellites, used, required} (required is
3 + n_systems), :singular_geometry, {:duplicate_observation, sat},
{:ephemeris_lost, sat}, {:ionosphere_unsupported, sat} (the ionosphere
correction was requested for a system with no modeled single-frequency
carrier), {:degenerate_geometry, reason} (the geometry is rank-deficient, so
reason is :rank_deficient, or exceeds the optional :max_pdop ceiling, so
reason is the PDOP), {:implausible_position, radius_m} (the fix is outside
the plausible geocentric-radius band), {:no_convergence, rms_m} (a
converged-flagged fix with physically implausible post-fit residual RMS),
{:invalid_option, :max_pdop}, {:invalid_option, :coarse_search},
{:robust_requires_noise_model, :no_weights}, or {:invalid_option, key} for a
malformed :huber, :huber_k, :huber_sigma, or :huber_max_iter.
@spec solve_batch(Sidereon.GNSS.SP3.t(), [batch_request()], keyword()) :: {:ok, ok: Sidereon.GNSS.Positioning.Solution.t(), error: term()} | {:error, term()}
Solve many independent SPP epochs against one shared SP3 product in a single call.
Each epoch is solved exactly as solve/4 would solve it (the crate's reference
trust-region path), independently: the receiver clock and position are
re-estimated per epoch, so the epochs share only the immutable ephemeris and
policy. requests is a list of batch_request/0 (a {observations, epoch}
pair, or a {observations, epoch, epoch_opts} triple whose epoch_opts
override the batch-wide options for that epoch). The result list is in request
order, one {:ok, %Solution{}} | {:error, reason} per epoch with the same
reasons solve/4 returns; a per-epoch solve failure does not fail the batch.
Options
The correction and geometry options match solve/4 and apply to every epoch
unless a request's epoch_opts overrides them: :ionosphere, :troposphere,
:klobuchar_alpha, :klobuchar_beta, :pressure_hpa, :temperature_k,
:relative_humidity, :initial_guess, :glonass_channels, :with_geodetic,
:max_pdop, :coarse_search, :robust, and the :huber reweighting levers
(:huber, :huber_k, :huber_sigma, :huber_max_iter).
:parallel- fan the independent per-epoch solves across the crate's thread pool (defaulttrue). Elementiis byte-for-byte identical to the serial result; this only changes throughput. Setfalseto force the serial path.
A batch-wide configuration error (a malformed :huber, :coarse_search,
:max_pdop, :parallel, :glonass_channels, or an unparseable epoch) fails
the whole call with {:error, reason} rather than producing a partial list.
Returns {:ok, [per_epoch_result]} or {:error, reason}.
@spec solve_broadcast( Sidereon.GNSS.Broadcast.t(), [observation()], epoch(), keyword() ) :: {:ok, Sidereon.GNSS.Positioning.Solution.t()} | {:error, term()}
Solve single-point positioning from broadcast ephemeris ALONE.
The explicit, named broadcast-only entry point: the supported real-time /
offline mode, where the satellite states come from a parsed broadcast
navigation product (Sidereon.GNSS.Broadcast) rather than a precise SP3
product. Broadcast ephemeris is decoded from the navigation message a receiver
already tracks, so it needs no network.
This is a thin wrapper over solve/4 with a Sidereon.GNSS.Broadcast source,
bit-for-bit identical to calling solve/4 directly; it makes the
broadcast-only contract explicit in the call site. observations, epoch, and
opts are exactly as in solve/4. Returns
{:ok, %Sidereon.GNSS.Positioning.Solution{}} or {:error, reason}.
@spec solve_with_fallback( [Sidereon.GNSS.SP3.t()], Sidereon.GNSS.Broadcast.t(), [observation()], epoch(), keyword() ) :: {:ok, Sidereon.GNSS.Positioning.SourcedSolution.t()} | {:error, term()}
Solve preferring precise SP3 products, falling back to broadcast ephemeris, reporting which source produced the fix and how stale it is.
The precise path is tried first through the product-staleness selection layer
(Sidereon.GNSS.Staleness) at the receive epoch:
- if a precise product covers the epoch it is used, and the result's source is
{:precise, metadata}with:exact(zero staleness) metadata. The solve is bit-for-bit identical tosolve/4on that SP3, and a solve failure here is a genuine error returned as{:error, {:precise, reason}}, never masked by a silent broadcast re-solve; - if a stale-but-within-cap precise product is selected and produces a fix,
the source is
{:precise, metadata}with:nearest_prior(nonzero staleness) metadata; - if the selected stale product cannot serve the epoch, or the precise
selection is declined outright, broadcast produces the fix and the source is
{:broadcast, reason}recording why precise was not used.
precise is a list of Sidereon.GNSS.SP3 products (it may be empty, forcing
broadcast); broadcast is a Sidereon.GNSS.Broadcast product.
Options
:policy- aSidereon.GNSS.Staleness.Policybounding how stale a precise product may be before broadcast is preferred (default: a three-day cap). A zero-second cap forces broadcast whenever no product covers the exact epoch.
The remaining options match solve/4: :ionosphere, :troposphere,
:klobuchar_alpha, :klobuchar_beta, :pressure_hpa, :temperature_k,
:relative_humidity, :initial_guess, :with_geodetic, and
:glonass_channels. The :huber and :coarse_search convergence aids are
not part of the fallback entry, which uses the reference solve on both paths.
The broadcast fallback solve applies the
broadcast NAV header's BeiDou and Galileo ionosphere coefficients, exactly as
solve_broadcast/4.
Returns {:ok, %Sidereon.GNSS.Positioning.SourcedSolution{}} or
{:error, {:precise, reason}} / {:error, {:broadcast, reason}} where reason
is the SPP solve error from the path that failed.