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_iwhere 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 uses LAMBDA 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
@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.
@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.
@type epoch_observations() :: %{epoch: NaiveDateTime.t(), observations: [observation()]} | {NaiveDateTime.t(), [observation()]}
A set of code/phase observations for one epoch.
@type observation() :: %{satellite_id: String.t(), code_m: number(), phase_m: number()} | {String.t(), number(), number()}
A dual-frequency ionosphere-free code/phase observation.
A receiver ECEF position in metres.
Functions
@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_search_radius_cycles- initial search sphere is large enough to contain this half-window around each rounded float ambiguity (default1).:integer_ratio_threshold- minimum second-best / best weighted-score ratio formetadata.integer_status == :fixed(default3.0).:integer_candidate_limit- maximum candidates to evaluate before returning{:error, {:too_many_integer_candidates, count, limit}}(default50_000). If the decorrelated sphere search finds no integer point inside the search bound, the function returns{:error, {:no_integer_candidates, count}}.:ambiguity_offset_m- optional scalar or%{"G05" => offset_m, ...}map subtracted from each float ambiguity before converting to cycles and added back after fixing (default0.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.
@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 throughOrbis.GNSS.Positioning.solve/4with ionosphere/troposphere disabled, and that code-only solution seeds the float solve.:spp_initial_guess- code-only SPP seed used only when:initial_guessis omitted (default{0, 0, 0, 0}).:code_sigma_m- code row standard deviation (default1.0m).:phase_sigma_m- phase row standard deviation (default0.01m).:elevation_weighting- whentrue, scale both code and phase row standard deviations by1 / sin(elevation)so low-elevation observations contribute less to the float, fixed, and ambiguity-covariance solves (defaultfalse).:max_iterations- maximum nonlinear iterations (default8).:position_tolerance_m- position-update convergence threshold (default1.0e-4m).:clock_tolerance_m- receiver-clock update threshold (default1.0e-4m).:troposphere- apply an a-priori Saastamoinen/Niell slant tropospheric delay to both code and phase (defaultfalse).:pressure_hpa- surface pressure in hPa when:troposphereis true (default1013.25).:temperature_k- surface temperature in kelvin when:troposphereis true (default288.15).:relative_humidity- relative humidity fraction when:troposphereis true (default0.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. Requirestroposphere: true(defaultfalse).:ztd_tolerance_m- residual-ZTD update convergence threshold when:estimate_ztdis true (default1.0e-4m).
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.
@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 (default1.0e-4m).
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.
@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 LAMBDA 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 (default2).:wide_lane_tolerance_cycles- maximum absolute distance between the averaged wide-lane float value and the nearest integer (default0.5cycles).:on_cycle_slip- what to do when a satellite arc has a detected cycle slip::errorreturns{:error, {:cycle_slip_detected, sat, epoch, reasons}}(default);:drop_satelliteremoves that satellite from the wide-lane and narrow-lane solve;:split_arcresets that satellite's ambiguity at each slip and keeps any resulting arc with at least:wide_lane_min_epochsusable epochs. Dropped satellites are reported inmetadata.dropped_cycle_slip_sats; split fragments are reported inmetadata.split_cycle_slip_arcs. Split fragments use ambiguity ids such as"G21#2"inused_satsand 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.