RTK-facing carrier/code double-difference primitives.
A base receiver and a rover receiver observing the same satellites have receiver-clock terms that differ by station but are common to every satellite. A single difference subtracts base from rover for the same satellite; a double difference subtracts a reference satellite's single difference:
DD_s = (rover_s - base_s) - (rover_ref - base_ref)The receiver clocks cancel in the second subtraction. Satellite-clock, ephemeris, and short-baseline atmosphere errors that are common between base and rover also cancel in the receiver single difference. The remaining carrier-phase double differences are the measurement surface used by RTK baseline estimation and integer ambiguity fixing.
double_differences/3 returns normalized measurements. solve_rtk_float/1
and solve_rtk_fixed/1 operate on already prepared RTK epochs.
Example
iex> base = [
...> {"G01", 20_100.0, 20_103.0},
...> {"G02", 21_105.0, 21_110.0}
...> ]
iex> rover = [
...> {"G01", 20_040.0, 20_044.0},
...> {"G02", 21_060.0, 21_066.0}
...> ]
iex> {:ok, result} = Sidereon.GNSS.RTK.double_differences(base, rover, reference_satellite_id: "G01")
iex> result.double_differences
[%{satellite_id: "G02", reference_satellite_id: "G01", ambiguity_id: "G02", code_m: 15.0, phase_m: 17.0}]
Summary
Types
One RTK epoch carrying paired base/rover observations and satellite positions.
One non-reference satellite's double-difference observation.
One RTK epoch carrying raw dual-frequency base/rover observations.
Raw dual-frequency code/carrier observation for wide-lane/narrow-lane RTK.
ECEF position in metres.
Code and carrier-phase observation in metres.
Double-difference result with deterministic satellite ordering.
Satellite ECEF position keyed by satellite id.
Functions
Build code and carrier-phase double differences from base and rover observations.
Fix wide-lane RTK arc ambiguities by delegating to the core arc helper.
Build ionosphere-free RTK arc epochs from dual-frequency epochs and fixed wide-lane integers.
Solve a sequential RTK baseline arc from raw rover+base epochs, delegating the
whole driver (epoch normalization, reference selection, sequential filter, and
per-epoch ambiguity search) to the sidereon-core solve_rtk_arc kernel.
Solve a static integer-fixed RTK baseline from normalized RTK epochs.
Solve a static float RTK baseline from normalized RTK epochs.
Solve a static RTK arc with a typed core-style configuration map.
Solve a static RTK baseline directly from parsed RINEX OBS handles and SP3.
Solve a static dual-frequency wide-lane fixed RTK baseline from RINEX OBS and SP3.
Types
@type baseline_epoch() :: %{ :base_observations => [observation()], :rover_observations => [observation()], :satellite_positions_m => satellite_positions(), optional(:base_satellite_positions_m) => satellite_positions(), optional(:rover_satellite_positions_m) => satellite_positions(), optional(:velocity_mps) => ecef_input(), optional(:epoch) => term() }
One RTK epoch carrying paired base/rover observations and satellite positions.
:epoch is preserved in residual diagnostics; it is not interpreted by this
first solver layer because satellite positions are supplied by the caller.
:satellite_positions_m is used for satellite selection and elevation
weighting. When the caller has receiver-specific transmit-time positions, it
may also provide :base_satellite_positions_m and
:rover_satellite_positions_m; otherwise both default to
:satellite_positions_m.
@type double_difference() :: %{ satellite_id: String.t(), reference_satellite_id: String.t(), ambiguity_id: String.t(), code_m: float(), phase_m: float() }
One non-reference satellite's double-difference observation.
@type dual_frequency_baseline_epoch() :: %{ :base_observations => [dual_frequency_observation()], :rover_observations => [dual_frequency_observation()], :satellite_positions_m => satellite_positions(), optional(:base_satellite_positions_m) => satellite_positions(), optional(:rover_satellite_positions_m) => satellite_positions(), optional(:epoch) => term() }
One RTK epoch carrying raw dual-frequency base/rover observations.
@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(:ambiguity_id) => String.t(), optional(:lli1) => integer() | nil, optional(:lli2) => integer() | nil }
Raw dual-frequency code/carrier observation for wide-lane/narrow-lane RTK.
p1_m / p2_m are code pseudoranges in metres, phi1_cyc / phi2_cyc are
carrier phases in cycles, and f1_hz / f2_hz are the corresponding carrier
frequencies. :ambiguity_id is normally omitted; the wide-lane solver sets it
internally when :on_cycle_slip is :split_arc.
@type ecef_input() :: {number(), number(), number()} | %{x_m: number(), y_m: number(), z_m: number()}
ECEF position in metres.
@type observation() :: %{ :satellite_id => String.t(), :code_m => number(), :phase_m => number(), optional(:ambiguity_id) => String.t(), optional(:lli) => integer() | nil, optional(:loss_of_lock_indicator) => integer() | nil } | {String.t(), number(), number()}
Code and carrier-phase observation in metres.
Map observations may optionally carry :ambiguity_id to identify a carrier
arc and :lli (or :loss_of_lock_indicator) for single-frequency
loss-of-lock handling. Tuple observations use the satellite id as the
ambiguity id and have no LLI.
@type result() :: %{ reference_satellite_id: String.t(), double_differences: [double_difference()], dropped_sats: [String.t()] }
Double-difference result with deterministic satellite ordering.
@type satellite_positions() :: %{required(String.t()) => ecef_input()}
Satellite ECEF position keyed by satellite id.
Functions
@spec double_differences([observation()], [observation()], keyword()) :: {:ok, result()} | {:error, term()}
Build code and carrier-phase double differences from base and rover observations.
Observations can be maps with :satellite_id, :code_m, and :phase_m, or
{satellite_id, code_m, phase_m} tuples. Satellites are paired by id; any
satellite not present at both receivers is reported in :dropped_sats.
Options:
:reference_satellite_id- reference satellite for the second difference: a satellite id binary (single-system data only) or a per-system map covering every observed system. When omitted, each system's lexicographically first common satellite is selected deterministically. Non-reference satellites difference against their own system's reference.
Returns {:ok, result} or a tagged error. At least two common satellites are
required so one non-reference double difference can be produced.
@spec fix_wide_lane_rtk_arc([dual_frequency_baseline_epoch()], map()) :: {:ok, map()} | {:error, term()}
Fix wide-lane RTK arc ambiguities by delegating to the core arc helper.
The returned map includes :geometry_quality for the wide-lane ambiguity
design.
@spec prepare_ionosphere_free_rtk_arc( [dual_frequency_baseline_epoch()], %{required(String.t()) => integer()}, map() ) :: {:ok, map()} | {:error, term()}
Build ionosphere-free RTK arc epochs from dual-frequency epochs and fixed wide-lane integers.
Solve a sequential RTK baseline arc from raw rover+base epochs, delegating the
whole driver (epoch normalization, reference selection, sequential filter, and
per-epoch ambiguity search) to the sidereon-core solve_rtk_arc kernel.
This is a thin delegation to the core arc driver. epochs is a list of raw
epoch maps:
:base,:rover- lists of observation maps%{satellite_id:, ambiguity_id:, code_m:, phase_m:}:satellite_positions_m-%{satellite_id => {x, y, z}}shared-position map:base_satellite_positions_m,:rover_satellite_positions_m- optional per-receiver transmit-time position maps (default to the shared map):velocity_mps- optional rover ECEF velocity{vx, vy, vz}:prediction_time_s- optional epoch time coordinate
config is a map:
:base_m- base station ECEF{x, y, z}:reference-:auto(default),{:satellite, id}, or{:per_system, %{letter => id}}:model-%{code_sigma_m:, phase_sigma_m:, stochastic_model:, elevation_weighting?:, sagnac?:}:baseline_prior_sigma_m,:ambiguity_prior_sigma_m:initial_baseline_m-{x, y, z}(default{0, 0, 0}):wavelengths_m,:offsets_m-%{ambiguity_id => value}:update_opts- the per-epoch update controls (seearc_update_opts)
Returns {:ok, solution} with :references, per-epoch :epochs, and the
carried :final_state, or {:error, reason}. Each epoch map includes
:geometry_quality.
@spec solve_rtk_fixed(map()) :: {:ok, Sidereon.GNSS.RTK.FixedBaselineSolution.t()} | {:error, term()}
Solve a static integer-fixed RTK baseline from normalized RTK epochs.
@spec solve_rtk_float(map()) :: {:ok, Sidereon.GNSS.RTK.FloatBaselineSolution.t()} | {:error, term()}
Solve a static float RTK baseline from normalized RTK epochs.
Solve a static RTK arc with a typed core-style configuration map.
The returned map includes :geometry_quality for the static batch design.
@spec solve_static_rinex_rtk_baseline( Sidereon.GNSS.SP3.t(), Sidereon.GNSS.RINEX.Observations.t(), Sidereon.GNSS.RINEX.Observations.t(), ecef_input(), keyword() | map() ) :: {:ok, map()} | {:error, term()}
Solve a static RTK baseline directly from parsed RINEX OBS handles and SP3.
base_m is the base antenna reference point in ECEF metres. Options include
:model, :reference, :max_epochs, :arc_options, :preprocessing,
:float_options, :fixed_options, :residual_options, :float_only_systems,
:initial_baseline_m, and :receiver_antenna_corrections.
@spec solve_wide_lane_fixed_rinex_rtk_baseline( Sidereon.GNSS.SP3.t(), Sidereon.GNSS.RINEX.Observations.t(), Sidereon.GNSS.RINEX.Observations.t(), ecef_input(), keyword() | map() ) :: {:ok, map()} | {:error, term()}
Solve a static dual-frequency wide-lane fixed RTK baseline from RINEX OBS and SP3.
The result contains the decoded static float and fixed solutions plus
:wide_lane metadata for the wide-lane integers used before the final
narrow-lane solve.