Sidereon.GNSS.Positioning (Sidereon v0.28.1)

Copy Markdown View Source

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 position is ITRF/IGS ECEF meters, matching the SP3 frame;
  • geodetic latitude/longitude are radians and height is meters;
  • rx_clock_s is seconds;
  • pressure is hPa, temperature is kelvin, relative humidity is a fraction in [0, 1];
  • the Klobuchar alpha/beta coefficients 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.

A Doppler observation as {satellite_id, doppler_hz}, or {satellite_id, doppler_hz, carrier_hz}, or {satellite_id, doppler_hz, carrier_hz, sat_clock_drift_s_s}.

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 receiver position from pseudoranges and attach a Doppler velocity solve.

Solve preferring precise SP3 products, falling back to broadcast ephemeris, reporting which source produced the fix and how stale it is.

Types

batch_request()

@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.

doppler_observation()

@type doppler_observation() ::
  {String.t(), number()}
  | {String.t(), number(), number()}
  | {String.t(), number(), number(), number()}

A Doppler observation as {satellite_id, doppler_hz}, or {satellite_id, doppler_hz, carrier_hz}, or {satellite_id, doppler_hz, carrier_hz, sat_clock_drift_s_s}.

epoch()

@type epoch() :: NaiveDateTime.t() | tuple()

An epoch as a NaiveDateTime or {{y, m, d}, {h, min, s}} tuple.

observation()

@type observation() :: {String.t(), number()}

A {satellite_id, pseudorange_m} pseudorange observation.

Functions

solve(source, observations, epoch, opts \\ [])

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 (default false); 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 %{}), where slot is the GLONASS satellite slot/PRN and channel is its FDMA frequency channel k (valid [-7, +6]), as carried in the broadcast nav freq_channel field or a RINEX GLONASS SLOT / FRQ # header. Used only to resolve the GLONASS carrier for the (f_L1/f_k)^2 ionosphere scaling, so it matters only when :ionosphere is true and 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 (default false)
  • :klobuchar_alpha - broadcast alpha coefficients, 4-tuple (default zeros)
  • :klobuchar_beta - broadcast beta coefficients, 4-tuple (default zeros)
  • :pressure_hpa - surface pressure, hPa (default 1013.25)
  • :temperature_k - surface temperature, kelvin (default 288.15)
  • :relative_humidity - relative humidity fraction [0, 1] (default 0.5)
  • :initial_guess - {x_m, y_m, z_m, b_m} start point (default all zeros)
  • :with_geodetic - also return the geodetic position (default true)
  • :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 :weights map unless :unsafe_unit_weights is set.

Optional convergence aids

  • :coarse_search - cold-start convergence-basin widening for a degraded or absent position prior (default nil = 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. Accepts true (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 - when true (default false), 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 :huber is set: :huber_k (Huber constant, default 1.345), :huber_sigma (MAD scale floor in metres, default 1.0, the canonical core robust scale floor), :huber_max_iter (outer-loop cap, default 5). A malformed :huber/:huber_k/:huber_sigma/:huber_max_iter value 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.

solve_batch(source, requests, opts \\ [])

@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 (default true). Element i is byte-for-byte identical to the serial result; this only changes throughput. Set false to 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}.

solve_broadcast(source, observations, epoch, opts \\ [])

@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}.

solve_with_doppler(source, observations, doppler_observations, epoch, opts \\ [])

Solve receiver position from pseudoranges and attach a Doppler velocity solve.

doppler_observations accepts {satellite_id, doppler_hz} rows. A row may also include carrier_hz and sat_clock_drift_s_s; omitted carrier defaults to GPS L1 and omitted satellite clock drift defaults to zero.

Options match the atmospheric, initial-guess, geodetic, GLONASS-channel, and Huber options from solve/4. This entry calls the core SPP+Doppler primitive directly and does not run the :max_pdop or :coarse_search policy layer.

solve_with_fallback(precise, broadcast, observations, epoch, opts \\ [])

@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 to solve/4 on 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 - a Sidereon.GNSS.Staleness.Policy bounding 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.