Orbis.GNSS.PrecisePositioning (Orbis v0.17.0)

Copy Markdown View Source

Carrier-phase precise-positioning primitives.

This is the first precise-positioning layer above the code and carrier-phase combinations in Orbis.GNSS.IonosphereFree / Orbis.GNSS.CarrierPhase. It solves one SP3-backed epoch from dual-frequency ionosphere-free code and phase observations:

P_IF_i = rho_i(x) + b - c * dt_sat_i + T_i
L_IF_i = rho_i(x) + b - c * dt_sat_i + T_i + N_i

where x is the receiver ECEF position, b is the receiver clock in metres, T_i is the optional a-priori slant tropospheric delay plus any estimated residual zenith delay mapped to the line of sight, and N_i is one float carrier-phase ambiguity per satellite, also in metres. The single-epoch state is linearized and iterated over [x, y, z, b, N_1, N_2, ...].

solve_float/4 solves one epoch. solve_float_epochs/3 solves a static multi-epoch arc with one receiver position, one receiver clock per epoch, and one ambiguity per satellite held constant across the arc. That multi-epoch model is the first step where carrier phase can tighten position instead of being absorbed entirely by one ambiguity per epoch. Multi-epoch and fixed solves can also estimate one residual zenith troposphere delay over the arc (estimate_ztd: true) after the a-priori Saastamoinen/Niell correction.

solve_fixed_epochs/3 starts from the same multi-epoch float model, searches integer ambiguity candidates on an explicit caller-supplied wavelength grid, then re-solves position and per-epoch clocks with those ambiguities held fixed. solve_widelane_fixed_epochs/3 is the dual-frequency convenience path: it fixes the Melbourne-Wubbena wide-lane integer first, subtracts that known contribution from the ionosphere-free phase ambiguity, then runs the bounded integer least-squares search on the remaining narrow-lane integer.

Observation shape

Observations may be maps or tuples:

%{satellite_id: "G05", code_m: 24_000_000.0, phase_m: 24_012_345.0}
{"G05", 24_000_000.0, 24_012_345.0}

code_m and phase_m should normally be ionosphere-free combinations. Use Orbis.GNSS.IonosphereFree.iono_free/4 and Orbis.GNSS.IonosphereFree.iono_free_phase_cycles/4 to form them from raw dual-frequency RINEX observations.

Summary

Types

A set of raw dual-frequency observations for one epoch.

Raw dual-frequency code/phase observation for wide-lane/narrow-lane fixing.

A set of code/phase observations for one epoch.

A dual-frequency ionosphere-free code/phase observation.

A receiver ECEF position in metres.

Functions

Solve a static multi-epoch position with integer-fixed ambiguities.

Solve a float-ambiguity carrier-phase position for one SP3-backed epoch.

Solve a static multi-epoch float-ambiguity carrier-phase position.

Solve a static multi-epoch position from raw dual-frequency observations by fixing wide-lane then narrow-lane ambiguities.

Types

dual_frequency_epoch_observations()

@type dual_frequency_epoch_observations() ::
  %{epoch: NaiveDateTime.t(), observations: [dual_frequency_observation()]}
  | {NaiveDateTime.t(), [dual_frequency_observation()]}

A set of raw dual-frequency observations for one epoch.

dual_frequency_observation()

@type dual_frequency_observation() :: %{
  :satellite_id => String.t(),
  :p1_m => number(),
  :p2_m => number(),
  :phi1_cyc => number(),
  :phi2_cyc => number(),
  :f1_hz => number(),
  :f2_hz => number(),
  optional(:lli1) => integer() | nil,
  optional(:lli2) => integer() | nil
}

Raw dual-frequency code/phase observation for wide-lane/narrow-lane fixing.

epoch_observations()

@type epoch_observations() ::
  %{epoch: NaiveDateTime.t(), observations: [observation()]}
  | {NaiveDateTime.t(), [observation()]}

A set of code/phase observations for one epoch.

observation()

@type observation() ::
  %{satellite_id: String.t(), code_m: number(), phase_m: number()}
  | {String.t(), number(), number()}

A dual-frequency ionosphere-free code/phase observation.

receiver()

@type receiver() ::
  {number(), number(), number()}
  | %{x_m: number(), y_m: number(), z_m: number()}

A receiver ECEF position in metres.

Functions

solve_fixed_epochs(source, epoch_observations, opts \\ [])

@spec solve_fixed_epochs(Orbis.GNSS.SP3.t(), [epoch_observations()], keyword()) ::
  {:ok, Orbis.GNSS.PrecisePositioning.FixedSolution.t()} | {:error, term()}

Solve a static multi-epoch position with integer-fixed ambiguities.

The function first solves the float multi-epoch model (solve_float_epochs/3), converts each float ambiguity from metres to cycles using the explicit :ambiguity_wavelength_m option, searches nearby integer candidates, and re-solves the receiver position and per-epoch clocks with the best integer ambiguities held fixed.

Required option

  • :ambiguity_wavelength_m - either a positive scalar wavelength in metres for every satellite, or a map %{"G05" => wavelength_m, ...}.

Additional options

  • :integer_ratio_threshold - minimum second-best / best weighted-score ratio for metadata.integer_status == :fixed (default 3.0).
  • :integer_search_radius_cycles / :integer_candidate_limit - retained and still validated for backward compatibility, but no longer bound the search: integer resolution uses the LAMBDA method (decorrelation + reduction + mlambda search), which finds the true integer-least-squares optimum for any geometry with no search box, so it cannot return {:error, {:too_many_integer_candidates, ...}}.
  • :ambiguity_offset_m - optional scalar or %{"G05" => offset_m, ...} map subtracted from each float ambiguity before converting to cycles and added back after fixing (default 0.0). This is mainly for affine carrier-phase combinations such as wide-lane/narrow-lane fixing.

The fixed solution is returned even when the ratio test is not met; in that case metadata.integer_status is :not_fixed so callers can reject it.

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

@spec solve_float(Orbis.GNSS.SP3.t(), [observation()], NaiveDateTime.t(), keyword()) ::
  {:ok, Orbis.GNSS.PrecisePositioning.Solution.t()} | {:error, term()}

Solve a float-ambiguity carrier-phase position for one SP3-backed epoch.

source is a loaded Orbis.GNSS.SP3 product. observations is a list of ionosphere-free code/phase pairs for one epoch. epoch is interpreted in the SP3 product's time scale.

Options

  • :initial_guess - {x_m, y_m, z_m, clock_m}. If omitted, the code observations are first passed through Orbis.GNSS.Positioning.solve/4 with ionosphere/troposphere disabled, and that code-only solution seeds the float solve.
  • :spp_initial_guess - code-only SPP seed used only when :initial_guess is omitted (default {0, 0, 0, 0}).
  • :code_sigma_m - code row standard deviation (default 1.0 m).
  • :phase_sigma_m - phase row standard deviation (default 0.01 m).
  • :elevation_weighting - when true, scale both code and phase row standard deviations by 1 / sin(elevation) so low-elevation observations contribute less to the float, fixed, and ambiguity-covariance solves (default false).
  • :max_iterations - maximum nonlinear iterations (default 8).
  • :position_tolerance_m - position-update convergence threshold (default 1.0e-4 m).
  • :clock_tolerance_m - receiver-clock update threshold (default 1.0e-4 m).
  • :troposphere - apply an a-priori Saastamoinen/Niell slant tropospheric delay to both code and phase (default false).
  • :pressure_hpa - surface pressure in hPa when :troposphere is true (default 1013.25).
  • :temperature_k - surface temperature in kelvin when :troposphere is true (default 288.15).
  • :relative_humidity - relative humidity fraction when :troposphere is true (default 0.5).
  • :estimate_ztd - on multi-epoch/fixed solves only, estimate one residual zenith troposphere delay in metres over the whole static arc, mapped with the Niell wet mapping factor. Requires troposphere: true (default false).
  • :ztd_tolerance_m - residual-ZTD update convergence threshold when :estimate_ztd is true (default 1.0e-4 m).

Returns {:ok, %Solution{}} or {:error, reason}. Reasons include :no_observations, {:too_few_satellites, used, 4}, {:duplicate_observation, sat}, {:invalid_observation, entry}, :invalid_initial_guess, {:invalid_sigma, key}, {:invalid_option, key}, {:code_seed_failed, reason}, {:no_ephemeris, sat, reason}, {:troposphere_failed, sat, reason}, and :singular_geometry. If the iteration limit is reached after a valid solve step, the function returns a solution with metadata.converged == false and metadata.status == :max_iterations so callers can inspect the residuals and decide whether to reject it.

solve_float_epochs(source, epoch_observations, opts \\ [])

@spec solve_float_epochs(Orbis.GNSS.SP3.t(), [epoch_observations()], keyword()) ::
  {:ok, Orbis.GNSS.PrecisePositioning.MultiEpochSolution.t()} | {:error, term()}

Solve a static multi-epoch float-ambiguity carrier-phase position.

epoch_observations is a list of %{epoch: epoch, observations: obs} maps or {epoch, obs} tuples. The receiver position is static across the whole arc, each epoch gets its own receiver clock, and each satellite gets one ambiguity held constant across every epoch where that satellite appears.

This model is still float ambiguity only. It does not fix integer ambiguities or estimate a stochastic PPP process, but it lets changing geometry across the arc separate position from carrier ambiguities.

Options are the same as solve_float/4, plus:

  • :ambiguity_tolerance_m - maximum ambiguity-update convergence threshold (default 1.0e-4 m).

Returns {:ok, %MultiEpochSolution{}} or {:error, reason}. Reasons include :no_epochs, {:too_few_epochs, used, 2}, {:duplicate_epoch, epoch}, {:too_few_epoch_observations, epoch, used, 4}, {:too_few_equations, equations, unknowns}, and the same observation, option, ephemeris, seeding, and geometry errors as solve_float/4.

solve_widelane_fixed_epochs(source, dual_epoch_observations, opts \\ [])

@spec solve_widelane_fixed_epochs(
  Orbis.GNSS.SP3.t(),
  [dual_frequency_epoch_observations()],
  keyword()
) ::
  {:ok, Orbis.GNSS.PrecisePositioning.FixedSolution.t()} | {:error, term()}

Solve a static multi-epoch position from raw dual-frequency observations by fixing wide-lane then narrow-lane ambiguities.

This is the real-data convenience layer above solve_fixed_epochs/3. Each observation must carry both code and carrier phase on two bands:

%{
  satellite_id: "G05",
  p1_m: 24_000_000.0,
  p2_m: 24_000_004.0,
  phi1_cyc: 123_456_789.0,
  phi2_cyc: 96_123_456.0,
  f1_hz: 1_575_420_000.0,
  f2_hz: 1_227_600_000.0,
  lli1: 0,
  lli2: 0
}

For each satellite the function first estimates the Melbourne-Wubbena wide-lane integer Nw = N1 - N2 over the arc. It then forms ionosphere-free code/phase observations and fixes the remaining band-1 narrow-lane integer with bounded integer least-squares using lambda_NL = c / (f1 + f2). The returned fixed_ambiguities_cycles are those band-1 narrow-lane integers; the wide-lane integers are exposed as wide_lane_ambiguities_cycles.

Options

Accepts the same solve and integer-search options as solve_fixed_epochs/3, plus:

  • :wide_lane_min_epochs - minimum usable Melbourne-Wubbena epochs per satellite (default 2).
  • :wide_lane_tolerance_cycles - maximum absolute distance between the averaged wide-lane float value and the nearest integer (default 0.5 cycles).
  • :on_cycle_slip - what to do when a satellite arc has a detected cycle slip: :error returns {:error, {:cycle_slip_detected, sat, epoch, reasons}} (default); :drop_satellite removes that satellite from the wide-lane and narrow-lane solve; :split_arc resets that satellite's ambiguity at each slip and keeps any resulting arc with at least :wide_lane_min_epochs usable epochs. Dropped satellites are reported in metadata.dropped_cycle_slip_sats; split fragments are reported in metadata.split_cycle_slip_arcs. Split fragments use ambiguity ids such as "G21#2" in used_sats and the ambiguity maps, while ephemeris lookup and residual rows continue to use the physical satellite id ("G21").

Cycle slips are detected with Orbis.GNSS.CarrierPhase.detect_cycle_slips/2; pass :gf_threshold_m / :mw_threshold_cycles to tune that detector.