GNSS RTK Positioning (gnc-autonomy/navigation/gnss-rtk-positioning)
Use when you must compute the ECEF position of a GNSS rover relative to a
fixed base station, the baseline vector b = rover minus base, from
common-view L1 carrier-phase observables recorded at both receivers over
the epochs of an observation arc. For every epoch and every tracked
satellite with a supplied ECEF position, the leaf forms the single
difference across the receivers, then the double differences across
satellite pairs (non-reference minus reference), scales them to metres by
the L1 wavelength, and solves the stacked least-squares normal equations
for the float baseline and the float ambiguities. The integer
double-difference ambiguities are resolved by enumerating rounding
candidate sets around the rounded float vector, scoring each candidate by
the quadratic form on the float covariance, and applying a ratio test;
the winning integer set is then imposed and the measurements re-solved
for the FIXED baseline, reported as the ENU offset at the base. The
module is pure stdlib math, deterministic, no RNG, and performs no orbit
propagation: the satellite ECEF positions at each epoch are supplied
inputs, as a receiver would receive them from an ephemeris service. It
pairs with gnss-pseudorange-positioning (code absolute), gnss-carrier-
smoothing (code smoothing, not differencing) and gnss-doppler-velocity-
positioning (single-receiver velocity), which own the single-receiver
steps below this differential step; the fences are described under
Related leaves.
Domain quick reference
- Module constants: C_LIGHT = 299792458.0 m/s, F_L1 = 1575.42e6 Hz,
LAMBDA_L1 = C_LIGHT / F_L1 = 0.190293672798 m/cycle (the L1
wavelength, exact by construction), R_EARTH = 6378137.0 m
(spherical-Earth demo context for the ENU conversion only),
MIN_SATELLITES = 5 (4 non-reference pairs), MIN_EPOCHS = 2,
PIVOT_MIN = 1e-300 (singularity floor of the Gaussian elimination),
DEFAULT_SEARCH_RADIUS = 2 cycles, DEFAULT_RATIO_MIN = 3.0.
- Phase model: the raw L1 carrier phase in cycles at receiver i (base r
or rover u) for satellite j at epoch t is phi_i^j(t) = rho_i^j(t)/lambda
- fdt_i(t) - fdts^j - N_i^j, with rho the geometric range, dt_i the
receiver clock offset, dts^j the broadcast satellite clock offset and
N_i^j a per-receiver per-satellite integer tracking constant. Nothing
in the module needs the absolute phase level: the double differences
use only differences of the printed phase streams.
- Single difference (cycles): SD^j(t) = phi_u^j(t) - phi_r^j(t); the
satellite clock term fdts^j cancels exactly, the receiver clock
difference f(dt_u - dt_r) remains, common to every satellite at the
epoch.
- Double difference (cycles) of pair j against reference satellite 0:
DD^j(t) = SD^j(t) - SD^0(t); the receiver clock difference cancels
exactly, and in metres y_j(t) = LAMBDA_L1DD^j(t) =
(rho_u^j - rho_r^j - rho_u^0 + rho_r^0)(t) - LAMBDA_L1(N^j - N^0),
where the true integer double-difference ambiguity of the pair is
N_j = N^j - N^0, constant across the arc when no cycle slip occurs.
- Linearized geometry: for a baseline b short against the slant range,
(rho_u^j - rho_r^j)(t) = -u_j(t) dot b to first order, so
y_j(t) = -(u_j(t) - u_0(t)) dot b - LAMBDA_L1*N_j + eps_j(t), with u
the unit line of sight from the BASE position to the satellite. The
single-pass linear model leaves an O(|b|^2/rho) curvature residual of
about 1e-5 m on the 23 m demo baseline, absorbed by the least squares
as the model floor.
- Float system: unknowns x = (b_x, b_y, b_z, A_1 ... A_m) with
A_j = LAMBDA_L1N_j in metres; the measurement row for pair j at
epoch t is [-du_x, -du_y, -du_z, 0 ... -1 ... 0] with du = u_j - u_0
and the -1 on the j-th ambiguity column; y = H x + eps. Normal
equations (H^T H) x = H^T y are solved once (the model is linear at
the base), no iteration. m = S - 1 pairs, mT measurements, 3 + m
unknowns, redundancy m*T - (3 + m) = 7 on the worked example.
- Precision: residual r_i = y_i - (H x)_i; sigma0 =
sqrt(sum(r_i^2) / (mT - (3 + m))); per-axis 1-sigma
sigma0sqrt(diag_ii((H^T H)^-1)) for the three baseline axes, and
sigma0*sqrt(diag_jj(...))/LAMBDA_L1 for the ambiguities (cycles).
- Integer resolution: with n_float the float ambiguities in cycles,
round to n_round and enumerate every integer vector n within
Chebyshev distance SEARCH_RADIUS of n_round (5^5 = 3125 candidates on
the worked example). Score each candidate by the float-covariance
quadratic form q(n) = (n - n_float)^T Q^-1 (n - n_float), with Q the
m x m float ambiguity covariance block in cycles^2 taken from the
inverse normal matrix scaled by 1/LAMBDA_L1^2. ratio =
q(second-best)/q(best); the set is resolved when ratio >= RATIO_MIN.
The fixed residual RSS of a candidate, min_b ||y + lambdan +
dub||^2 over the baseline only, ranks the candidates the same way.
- Fixed solution: impose the winning integer set n and re-solve the
baseline-only least squares over the shifted measurements
y_j(t) + LAMBDA_L1n_j with rows -du_j(t); per-axis 1-sigma from
sigma0sqrt(diag((G^T G)^-1)) with sigma0 over m*T - 3 degrees of
freedom.
- ENU conversion: spherical base latitude and longitude from the base
ECEF position, local basis e = (-sin lon, cos lon, 0), n = (-sin lat
cos lon, -sin lat sin lon, cos lat), u = (cos lat cos lon, cos lat
sin lon, sin lat); the ENU offset is (b dot e, b dot n, b dot u). At
the demo base (lat 0, lon 0) this is exactly (b_y, b_z, b_x).
- Time-differenced arm: the per-pair epoch difference y_j(t2) - y_j(t1)
= -(du_j(t2) - du_j(t1)) dot b + (eps_j(t2) - eps_j(t1)) carries NO
ambiguity term (the integers are constant across a slip-free arc),
giving the ambiguity-free baseline-only geometry solve used as the
coarse precursor read.
- RTCA DO-229 frames the GNSS airborne navigation context; the relations
above are standard engineering methodology, name and paraphrase only.
Workflow
- Fix the observation arc: per-epoch records with the satellite ECEF
positions (supplied inputs, no orbit propagation), the raw rover and
base L1 phase streams in cycles (same satellite order every epoch),
the base ECEF position and the reference satellite index. The module
constants anchor every later step; line_of_sight checks each
satellite for coincidence with the base.
- Form the per-satellite single differences across the receivers at
each epoch: single_difference returns rover minus base phase in
cycles; the broadcast satellite clock offsets cancel in every single
difference.
- Form the double differences across the satellite pairs and epochs:
form_double_differences subtracts the reference-satellite single
difference from every non-reference one, and the solvers scale the
cycles by LAMBDA_L1 to the metre observables y. The receiver clock
difference cancels exactly in the double differences.
- Solve the float baseline: solve_float_baseline stacks the rows
[-du, -e_j] over (b_x, b_y, b_z, A_1 ... A_m) and solves the normal
equations (H^T H) x = H^T y, returning the float baseline, the float
ambiguities in metres and cycles, sigma0, the residual RMS, the
covariance diagonal and the per-axis and per-ambiguity 1-sigma
precision.
- Resolve the integer ambiguities: resolve_integer_ambiguities
enumerates the rounding candidate sets within a Chebyshev radius of
the rounded float vector, scores each by the float-covariance
quadratic form, applies the ratio test (threshold 3.0) and returns
the winning integer set with its ratio and the fixed residual RSS of
the two best candidates.
- Impose the winning integer set: fixed_baseline_solution re-solves
the baseline-only least squares over the shifted measurements
y_j(t) + LAMBDA_L1*n_j and returns the FIXED baseline with the
per-axis 1-sigma precision from the baseline-only normal matrix.
- Report the fixed baseline as the ENU offset at the base:
ecef_to_enu converts the fixed baseline ECEF vector to the local
east, north, up offset at the base position.
- Run the ambiguity-free time-differenced precursor read and the
identity checks: solve_td_baseline fits the epoch-differenced double
differences (no ambiguity term in a slip-free arc) as the coarse
geometry read; the clock-cancellation, ambiguity-constancy and
no-cycle-residue identities confirm the double-difference model.
- Confirm the deterministic checks with the contract test of the
Behavior contract (gate 3) section.
Worked example
Fixed base station at the equator on the prime meridian, ECEF position
(6378137.000, 0.000, 0.000) m (lat 0, lon 0, sea level on the spherical
earth). TRUE rover baseline ECEF (-2.000, 20.000, 12.000) m, length
23.409 m, so the TRUE ENU offset is (20.000 E, 12.000 N, -2.000 U) m.
Six MEO satellites at 55.5 deg inclination, semi-major axis 2.656e7 m,
tracked at three epochs t = 0, 600 and 1200 s of a 20 minute static-
baseline observation arc; the reference satellite is A. TRUE integer
double-difference ambiguities versus A (cycles): DD1 (B) 487, DD2 (C)
-196, DD3 (D) 372, DD4 (E) -514, DD5 (F) 259. About 1.1 mm of
deterministic double-difference noise is documented per pair per epoch.
All values below are REAL outputs of the leaf module (stdlib math,
deterministic, no RNG, exit 0), matching the wave-44 spec anchors.
Single differences (cycles, rover minus base) per epoch, re-formed from
the phase streams:
- t = 0 s: 61.390112, -534.393732, 139.265625, -269.678384, 601.354211,
-349.059214.
- t = 600 s: 62.112875, -532.390137, 143.677535, -266.935707,
606.103938, -341.154513.
- t = 1200 s: 61.047395, -531.504721, 146.701148, -266.319046,
609.004990, -334.516046.
Float solution (single-pass linear least squares at the base, direct
normal equations, 15 measurements, 8 unknowns, 7 degrees of freedom):
- Baseline float ECEF (-2.026417, 19.980484, 12.047023) m; per-axis
error versus truth (-0.0264, -0.0195, 0.0470) m and 3-D error
0.0574 m (0.25 percent of the 23.4 m baseline): the float baseline is
metre-to-decimetre level, as expected before the integer fix.
- Per-axis 1-sigma (0.013254, 0.015327, 0.028501) m: the radial (x,
local vertical) axis is the weakest.
- sigma0 0.001269 m, residual RMS 0.000867 m (the ~1.1 mm injected
noise plus the 1e-5 m curvature terms).
- Float ambiguities (cycles) with error versus the true integers and
the 1-sigma from the covariance: DD1 487.333307 (+0.3333, sigma
0.21262), DD2 -195.980692 (+0.0193, sigma 0.05863), DD3 372.069111
(+0.0691, sigma 0.04662), DD4 -513.642088 (+0.3579, sigma 0.20499),
DD5 259.050702 (+0.0507, sigma 0.08256). Max float ambiguity error
0.358 cycles, every error below 0.5 cycles, so nearest-integer
rounding of the float solution IS the true integer set.
Integer resolution (rounding candidate sets at radius 2 cycles around
the rounded float, 5 dimensions, ratio test on the float covariance at
threshold 3.0):
- Candidates searched 3125 (= 5^5).
- Best candidate (487, -196, 372, -514, 259) = the true set,
float-covariance q 8.93e-6, fixed residual RSS 2.020e-05 m^2 (noise
level).
- Second-best (486, -195, 371, -516, 260), q 4.481e-3, fixed residual
RSS 4.492e-03 m^2 (two orders above the best RSS).
- Ratio q_second/q_best = 501.5 >= 3.0, resolved True; the residual-RSS
ranking agrees (ratio about 222).
Fixed solution (integer set (487, -196, 372, -514, 259) imposed):
- Baseline ECEF (-2.000112, 20.000074, 12.000159) m; per-axis error
versus truth (-0.112, +0.074, +0.159) mm and 3-D error 0.209 mm:
imposing the integers moves the rover fix from the 5.7 cm float level
to the sub-millimetre level.
- Per-axis 1-sigma (0.003460, 0.000614, 0.000782) m (3.46 mm radial,
0.61 and 0.78 mm along-track/cross-track): the vertical (radial) axis
carries the weakest precision, as for the single-receiver geometry.
- sigma0 0.001297 m, residual RMS 0.001160 m.
- ENU offset (E 20.000074, N 12.000159, U -2.000112) m; error versus
truth (+0.074, +0.159, -0.112) mm.
Time-differenced arm (ambiguity-free double differences across adjacent
epochs, geometry-only precursor solve over 10 equations):
- Baseline (-2.022031, 19.986400, 12.034498) m, per-axis error
(-0.0220, -0.0136, 0.0345) m and 3-D error 0.0431 m: the coarse
decimetre-level geometry read that the float/fix pipeline refines.
- Residual RMS 0.001816 m; no cycle-level integer residue survives the
epoch difference (a one-cycle residue would be about 0.19 m).
Noiseless cross-check (noise offsets zeroed, curvature terms still
present):
- Float baseline 3-D error 1.74e-05 m, max ambiguity error 1.36e-04
cycles, residual RMS 6.31e-07 m: the O(|b|^2/rho) curvature of the
linearized model is the float floor.
- Nearest-integer rounding equals the true set; noiseless fixed
baseline 3-D error 8.22e-06 m.
Read-off: L1 double-difference carrier-phase observables from a 6-
satellite geometry over a 20 minute arc with about 1.1 mm noise give a
float baseline accurate to about 6 cm (0.25 percent of the 23.4 m
baseline) with float ambiguities within 0.36 cycles of the integers, so
the rounding-candidate search resolves the true integer set with a ratio
of 501.5 and the fixed rover baseline lands within 0.21 mm (3-D) of the
truth with a 3.5 mm vertical 1-sigma.
Verification
- Run scripts/test_gnss_rtk_positioning.py; the contract test must pass
under both the system python3 and the pyenv 3.13.12 hook interpreter
(39 tests, offline, deterministic).
- Confirm the worked-example contract: float per-axis error below
0.10 m on every axis and 3-D error below 0.15 m (module 0.0574 m),
sigma0 within 1e-3 m of 0.001269 m, residual RMS below 0.005 m, every
float ambiguity within 0.5 cycles of the true integer (module max
0.358) and the per-ambiguity 1-sigma within 1e-2 cycles of the anchor
values (0.21261, 0.05863, 0.04662, 0.20498, 0.08256).
- Confirm the precision identity: every per-axis 1-sigma equals sigma0
times the square root of the covariance diagonal, the anchor values
(0.01325, 0.01533, 0.02850) m hold within 1e-3 m, and the ambiguity
sigmas are the metre covariance diagonal divided by LAMBDA_L1.
- Confirm the integer-resolution contract: candidates_searched 3125,
best_candidate (487, -196, 372, -514, 259), resolved True, ratio
501.5 within 1 percent, second_best (486, -195, 371, -516, 260),
best_rss below 1e-4 m^2 and second_rss above best_rss by a factor of
100 or more.
- Confirm the fixed-solution contract: baseline within 0.001 m of the
truth per axis (module errors -0.112, +0.074, +0.159 mm), 3-D error
below 0.001 m, per-axis 1-sigma within 1e-4 m of (0.00346, 0.00061,
0.00078) m and the ENU offset within 0.0005 m of (20.0, 12.0, -2.0).
- Confirm the time-differenced arm: 3-D error below 0.2 m (module
0.0431 m), residual RMS below 0.005 m, and the epoch-differenced
observables reproduce the documented noise differences with no
cycle-level residue.
- Confirm the closed-form identities: single_difference(150.25, 152.75)
= -2.5 cycles, form_double_differences([12.25, 9.75, -3.5], 0) =
[-2.5, -15.75] cycles, time_differenced_dd([1.2, 3.4, 5.6],
[1.0, 3.0, 5.0]) = [0.2, 0.4, 0.6] cycles, ecef_to_enu((-2, 20, 12),
(6378137, 0, 0)) = (20, 12, -2) and LAMBDA_L1*F_L1 = C_LIGHT.
- Confirm the clock-cancellation identity: shifting the rover clock by
a common offset at every epoch and adding the satellite clock offsets
to both receivers leaves every double difference at machine precision
(below 1e-6 cycles of leakage) and leaves the float solve unchanged.
- Confirm the noiseless identity: with the noise offsets zeroed the
float 3-D error stays below 1e-4 m and the fixed 3-D error below
1e-4 m.
- Confirm ValueError rejection of every non-physical input: fewer than
5 satellites or fewer than 2 epochs, position/phase length mismatch,
satellite count changing between epochs, reference index out of
range, search_radius below 1 cycle, ratio_min at or below 1, an
integer_ambiguities tuple of the wrong length, a coincident satellite,
non-finite positions, phases, epochs or base position, a zero base
position in ecef_to_enu, and a singular normal matrix.
- Confirm determinism: repeated solves return identical outputs; the
module imports nothing beyond math and contains no RNG and no orbit
propagation.
Related leaves
- skills/gnc-autonomy/navigation/gnss-pseudorange-positioning: owns the
single-receiver absolute code fix (position and clock bias from
pseudoranges); this leaf is differential and carrier-phase based,
solves no pseudorange and estimates no receiver clock bias (the
receiver clock terms cancel in the double differences).
- skills/gnc-autonomy/navigation/gnss-carrier-smoothing: owns the Hatch
smoothing side of the carrier observable (smoothing CODE with carrier
increments in one receiver); this leaf differences raw carrier phases
between two receivers, never smooths code and never runs a Hatch
recursion, and integer ambiguity resolution is in scope here only.
- skills/gnc-autonomy/navigation/gnss-doppler-velocity-positioning:
owns the single-receiver snapshot velocity step from carrier-phase
delta-range-rate observables with Kepler propagation; this leaf
consumes static carrier-phase accumulations, estimates no velocity
and performs no orbit propagation (the satellite ECEF positions at
each epoch are supplied inputs).
- skills/gnc-autonomy/navigation/gnss-raim-fde: owns integrity on the
single-receiver measurement set; this leaf produces no detection
verdict and no protection level (the arc is fault-free by
assumption).
- skills/gnc-autonomy/navigation/dilution-of-precision and the other
GNSS navigation siblings of the pack: the remaining single-receiver
geometry and quality context around this differential step.
- skills/gnc-autonomy/space/orbit-determination and
skills/gnc-autonomy/space/orbit-dynamics: the space-domain siblings;
their propagation machinery is never needed here because satellite
positions arrive as per-epoch inputs.
Pitfalls
- Confusing this leaf with the code position fix: the rover position
this leaf reports is the differential baseline relative to a FIXED
base from double-difference carrier phase, not an absolute
single-point code fix. Code pseudorange positioning and receiver
clock-bias estimation belong to gnss-pseudorange-positioning.
- Treating the carrier observable as something to smooth: this leaf
differences raw carrier-phase streams between two receivers and never
smooths code pseudoranges. Hatch smoothing, smoothed ranges and
code-carrier divergence monitoring belong to gnss-carrier-smoothing;
the smoothing leaf explicitly excludes the integer ambiguity
resolution that is the core step here.
- Expecting a velocity or a propagated constellation: this leaf
estimates no velocity, no range rate and no clock drift, and it
performs no Kepler or broadcast-ephemeris propagation; the satellite
ECEF positions at each epoch are supplied inputs (the domain of
gnss-doppler-velocity-positioning).
- Forgetting that the clock terms cancel in the double differences:
the receiver clock difference is common to every satellite at an
epoch and cancels when the reference satellite is subtracted, so no
clock-bias unknown appears in the state; the state is the 3-axis
baseline plus one ambiguity per non-reference pair.
- Solving only the baseline: the float state is (b_x, b_y, b_z,
A_1 ... A_m) with one float ambiguity per pair, 8 unknowns for 6
satellites; dropping the ambiguity columns leaves the decimetre-level
time-differenced read (step 8) rather than the fixed solution.
- Reading the float baseline as the final answer: the float fix is at
the 6 cm level on the worked example; the integer-constrained fix
lands at 0.21 mm, so reporting the float baseline without running the
integer resolution undersells the observable by two orders of
magnitude.
- Quoting the vertical axis precision as if it matched the horizontal:
the radial (local vertical) axis is the weakest for the geometry
(anchor 1-sigma 3.46 mm against 0.61 and 0.78 mm on the other axes).
- Trusting an arc with a cycle slip: the double-difference ambiguities
are constant only across a slip-free arc; the model assumes no cycle
slip (cycle-slip detection and repair are out of scope for this
leaf), so a slipped arc corrupts the integer search before the ratio
test can catch it.
- Treating the q-form weight as a full covariance with the sigma0
scale: Q is the inverse-normal ambiguity block scaled by
1/LAMBDA_L1^2 per the defining relation; the sigma0 scale cancels in
the ratio test and is omitted from the quadratic-form weight.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 skills/gnc-autonomy/navigation/gnss-rtk-positioning/scripts/test_gnss_rtk_positioning.py
The test covers the worked-example contract with the module's real
outputs as targets: the step-1 module constants and line-of-sight
geometry, the step-2 single-difference traverse with the spec phase-
stream table, the step-3 double-difference traverse with the clock-
cancellation identity below 1e-6 cycles, the step-4 float solve on the
worked set (baseline, ambiguities, sigma0, residual RMS, covariance
precision identity, system dimensions), the step-5 integer resolution
(candidate count 3125, best and second-best candidate sets, ratio 501.5
within 1 percent, residual-RSS ordering), the step-6 fixed solution with
the imposed integer set (sub-mm baseline, per-axis sigmas) and the
ambiguity-constancy identity across epochs, the step-7 ENU offset at the
demo base, the step-8 time-differenced precursor read with the
no-cycle-residue identity, the noiseless curvature-floor cross-check,
ValueError rejection of every non-physical input in the spec validation
list (fewer than 5 satellites, fewer than 2 epochs, length mismatch,
count change, reference index, search radius, ratio threshold, wrong
ambiguity tuple length, coincident satellite, non-finite values, zero
base, singular normal matrix), determinism across reruns and the
pure-stdlib no-RNG import check. It passes under both the system python3
and the pyenv 3.13.12 hook interpreter.
Compliance
- Standards referenced, not reproduced: RTCA DO-229 frames the GNSS
airborne navigation context; this leaf implements standard engineering
methodology with name and paraphrase only, no MOPS algorithm or table
text, summary-only per standards-map.yaml.
- compliance: STANDARDS-REF, gated: false.
1---2name: gnss-rtk-positioning3description: Use when you must compute the position of a GNSS rover relative to a fixed base station from double-difference carrier-phase observables: form the per-satellite single differences across the receivers at each epoch of a common-view observation arc, then the double differences across satellite pairs and epochs, and solve the stacked least-squares normal equations for the float baseline and the per-pair float ambiguities. Resolve the integer ambiguities by rounding candidate sets around the float solution with a ratio test on the float covariance, impose the winning integer set, and re-solve for the fixed baseline with per-axis 1-sigma precision. Produces the float baseline and the fixed rover baseline in ECEF, the resolved integer ambiguity set with its ratio, and the fixed ENU offset at the base. Trigger: carrier phase differential, RTK positioning, integer ambiguity resolution, double difference baseline, fixed baseline ENU offset.4license: Apache-2.05---67# GNSS RTK Positioning (gnc-autonomy/navigation/gnss-rtk-positioning)89Use when you must compute the ECEF position of a GNSS rover relative to a10fixed base station, the baseline vector b = rover minus base, from11common-view L1 carrier-phase observables recorded at both receivers over12the epochs of an observation arc. For every epoch and every tracked13satellite with a supplied ECEF position, the leaf forms the single14difference across the receivers, then the double differences across15satellite pairs (non-reference minus reference), scales them to metres by16the L1 wavelength, and solves the stacked least-squares normal equations17for the float baseline and the float ambiguities. The integer18double-difference ambiguities are resolved by enumerating rounding19candidate sets around the rounded float vector, scoring each candidate by20the quadratic form on the float covariance, and applying a ratio test;21the winning integer set is then imposed and the measurements re-solved22for the FIXED baseline, reported as the ENU offset at the base. The23module is pure stdlib math, deterministic, no RNG, and performs no orbit24propagation: the satellite ECEF positions at each epoch are supplied25inputs, as a receiver would receive them from an ephemeris service. It26pairs with gnss-pseudorange-positioning (code absolute), gnss-carrier-27smoothing (code smoothing, not differencing) and gnss-doppler-velocity-28positioning (single-receiver velocity), which own the single-receiver29steps below this differential step; the fences are described under30Related leaves.3132## Domain quick reference3334- Module constants: C_LIGHT = 299792458.0 m/s, F_L1 = 1575.42e6 Hz,35 LAMBDA_L1 = C_LIGHT / F_L1 = 0.190293672798 m/cycle (the L136 wavelength, exact by construction), R_EARTH = 6378137.0 m37 (spherical-Earth demo context for the ENU conversion only),38 MIN_SATELLITES = 5 (4 non-reference pairs), MIN_EPOCHS = 2,39 PIVOT_MIN = 1e-300 (singularity floor of the Gaussian elimination),40 DEFAULT_SEARCH_RADIUS = 2 cycles, DEFAULT_RATIO_MIN = 3.0.41- Phase model: the raw L1 carrier phase in cycles at receiver i (base r42 or rover u) for satellite j at epoch t is phi_i^j(t) = rho_i^j(t)/lambda43 + f*dt_i(t) - f*dts^j - N_i^j, with rho the geometric range, dt_i the44 receiver clock offset, dts^j the broadcast satellite clock offset and45 N_i^j a per-receiver per-satellite integer tracking constant. Nothing46 in the module needs the absolute phase level: the double differences47 use only differences of the printed phase streams.48- Single difference (cycles): SD^j(t) = phi_u^j(t) - phi_r^j(t); the49 satellite clock term f*dts^j cancels exactly, the receiver clock50 difference f*(dt_u - dt_r) remains, common to every satellite at the51 epoch.52- Double difference (cycles) of pair j against reference satellite 0:53 DD^j(t) = SD^j(t) - SD^0(t); the receiver clock difference cancels54 exactly, and in metres y_j(t) = LAMBDA_L1*DD^j(t) =55 (rho_u^j - rho_r^j - rho_u^0 + rho_r^0)(t) - LAMBDA_L1*(N^j - N^0),56 where the true integer double-difference ambiguity of the pair is57 N_j = N^j - N^0, constant across the arc when no cycle slip occurs.58- Linearized geometry: for a baseline b short against the slant range,59 (rho_u^j - rho_r^j)(t) = -u_j(t) dot b to first order, so60 y_j(t) = -(u_j(t) - u_0(t)) dot b - LAMBDA_L1*N_j + eps_j(t), with u61 the unit line of sight from the BASE position to the satellite. The62 single-pass linear model leaves an O(|b|^2/rho) curvature residual of63 about 1e-5 m on the 23 m demo baseline, absorbed by the least squares64 as the model floor.65- Float system: unknowns x = (b_x, b_y, b_z, A_1 ... A_m) with66 A_j = LAMBDA_L1*N_j in metres; the measurement row for pair j at67 epoch t is [-du_x, -du_y, -du_z, 0 ... -1 ... 0] with du = u_j - u_068 and the -1 on the j-th ambiguity column; y = H x + eps. Normal69 equations (H^T H) x = H^T y are solved once (the model is linear at70 the base), no iteration. m = S - 1 pairs, m*T measurements, 3 + m71 unknowns, redundancy m*T - (3 + m) = 7 on the worked example.72- Precision: residual r_i = y_i - (H x)_i; sigma0 =73 sqrt(sum(r_i^2) / (m*T - (3 + m))); per-axis 1-sigma74 sigma0*sqrt(diag_ii((H^T H)^-1)) for the three baseline axes, and75 sigma0*sqrt(diag_jj(...))/LAMBDA_L1 for the ambiguities (cycles).76- Integer resolution: with n_float the float ambiguities in cycles,77 round to n_round and enumerate every integer vector n within78 Chebyshev distance SEARCH_RADIUS of n_round (5^5 = 3125 candidates on79 the worked example). Score each candidate by the float-covariance80 quadratic form q(n) = (n - n_float)^T Q^-1 (n - n_float), with Q the81 m x m float ambiguity covariance block in cycles^2 taken from the82 inverse normal matrix scaled by 1/LAMBDA_L1^2. ratio =83 q(second-best)/q(best); the set is resolved when ratio >= RATIO_MIN.84 The fixed residual RSS of a candidate, min_b ||y + lambda*n +85 du*b||^2 over the baseline only, ranks the candidates the same way.86- Fixed solution: impose the winning integer set n and re-solve the87 baseline-only least squares over the shifted measurements88 y_j(t) + LAMBDA_L1*n_j with rows -du_j(t); per-axis 1-sigma from89 sigma0*sqrt(diag((G^T G)^-1)) with sigma0 over m*T - 3 degrees of90 freedom.91- ENU conversion: spherical base latitude and longitude from the base92 ECEF position, local basis e = (-sin lon, cos lon, 0), n = (-sin lat93 cos lon, -sin lat sin lon, cos lat), u = (cos lat cos lon, cos lat94 sin lon, sin lat); the ENU offset is (b dot e, b dot n, b dot u). At95 the demo base (lat 0, lon 0) this is exactly (b_y, b_z, b_x).96- Time-differenced arm: the per-pair epoch difference y_j(t2) - y_j(t1)97 = -(du_j(t2) - du_j(t1)) dot b + (eps_j(t2) - eps_j(t1)) carries NO98 ambiguity term (the integers are constant across a slip-free arc),99 giving the ambiguity-free baseline-only geometry solve used as the100 coarse precursor read.101- RTCA DO-229 frames the GNSS airborne navigation context; the relations102 above are standard engineering methodology, name and paraphrase only.103104## Workflow1051061. Fix the observation arc: per-epoch records with the satellite ECEF107 positions (supplied inputs, no orbit propagation), the raw rover and108 base L1 phase streams in cycles (same satellite order every epoch),109 the base ECEF position and the reference satellite index. The module110 constants anchor every later step; line_of_sight checks each111 satellite for coincidence with the base.1122. Form the per-satellite single differences across the receivers at113 each epoch: single_difference returns rover minus base phase in114 cycles; the broadcast satellite clock offsets cancel in every single115 difference.1163. Form the double differences across the satellite pairs and epochs:117 form_double_differences subtracts the reference-satellite single118 difference from every non-reference one, and the solvers scale the119 cycles by LAMBDA_L1 to the metre observables y. The receiver clock120 difference cancels exactly in the double differences.1214. Solve the float baseline: solve_float_baseline stacks the rows122 [-du, -e_j] over (b_x, b_y, b_z, A_1 ... A_m) and solves the normal123 equations (H^T H) x = H^T y, returning the float baseline, the float124 ambiguities in metres and cycles, sigma0, the residual RMS, the125 covariance diagonal and the per-axis and per-ambiguity 1-sigma126 precision.1275. Resolve the integer ambiguities: resolve_integer_ambiguities128 enumerates the rounding candidate sets within a Chebyshev radius of129 the rounded float vector, scores each by the float-covariance130 quadratic form, applies the ratio test (threshold 3.0) and returns131 the winning integer set with its ratio and the fixed residual RSS of132 the two best candidates.1336. Impose the winning integer set: fixed_baseline_solution re-solves134 the baseline-only least squares over the shifted measurements135 y_j(t) + LAMBDA_L1*n_j and returns the FIXED baseline with the136 per-axis 1-sigma precision from the baseline-only normal matrix.1377. Report the fixed baseline as the ENU offset at the base:138 ecef_to_enu converts the fixed baseline ECEF vector to the local139 east, north, up offset at the base position.1408. Run the ambiguity-free time-differenced precursor read and the141 identity checks: solve_td_baseline fits the epoch-differenced double142 differences (no ambiguity term in a slip-free arc) as the coarse143 geometry read; the clock-cancellation, ambiguity-constancy and144 no-cycle-residue identities confirm the double-difference model.1459. Confirm the deterministic checks with the contract test of the146 Behavior contract (gate 3) section.147148## Worked example149150Fixed base station at the equator on the prime meridian, ECEF position151(6378137.000, 0.000, 0.000) m (lat 0, lon 0, sea level on the spherical152earth). TRUE rover baseline ECEF (-2.000, 20.000, 12.000) m, length15323.409 m, so the TRUE ENU offset is (20.000 E, 12.000 N, -2.000 U) m.154Six MEO satellites at 55.5 deg inclination, semi-major axis 2.656e7 m,155tracked at three epochs t = 0, 600 and 1200 s of a 20 minute static-156baseline observation arc; the reference satellite is A. TRUE integer157double-difference ambiguities versus A (cycles): DD1 (B) 487, DD2 (C)158-196, DD3 (D) 372, DD4 (E) -514, DD5 (F) 259. About 1.1 mm of159deterministic double-difference noise is documented per pair per epoch.160All values below are REAL outputs of the leaf module (stdlib math,161deterministic, no RNG, exit 0), matching the wave-44 spec anchors.162163Single differences (cycles, rover minus base) per epoch, re-formed from164the phase streams:165166- t = 0 s: 61.390112, -534.393732, 139.265625, -269.678384, 601.354211,167 -349.059214.168- t = 600 s: 62.112875, -532.390137, 143.677535, -266.935707,169 606.103938, -341.154513.170- t = 1200 s: 61.047395, -531.504721, 146.701148, -266.319046,171 609.004990, -334.516046.172173Float solution (single-pass linear least squares at the base, direct174normal equations, 15 measurements, 8 unknowns, 7 degrees of freedom):175176- Baseline float ECEF (-2.026417, 19.980484, 12.047023) m; per-axis177 error versus truth (-0.0264, -0.0195, 0.0470) m and 3-D error178 0.0574 m (0.25 percent of the 23.4 m baseline): the float baseline is179 metre-to-decimetre level, as expected before the integer fix.180- Per-axis 1-sigma (0.013254, 0.015327, 0.028501) m: the radial (x,181 local vertical) axis is the weakest.182- sigma0 0.001269 m, residual RMS 0.000867 m (the ~1.1 mm injected183 noise plus the 1e-5 m curvature terms).184- Float ambiguities (cycles) with error versus the true integers and185 the 1-sigma from the covariance: DD1 487.333307 (+0.3333, sigma186 0.21262), DD2 -195.980692 (+0.0193, sigma 0.05863), DD3 372.069111187 (+0.0691, sigma 0.04662), DD4 -513.642088 (+0.3579, sigma 0.20499),188 DD5 259.050702 (+0.0507, sigma 0.08256). Max float ambiguity error189 0.358 cycles, every error below 0.5 cycles, so nearest-integer190 rounding of the float solution IS the true integer set.191192Integer resolution (rounding candidate sets at radius 2 cycles around193the rounded float, 5 dimensions, ratio test on the float covariance at194threshold 3.0):195196- Candidates searched 3125 (= 5^5).197- Best candidate (487, -196, 372, -514, 259) = the true set,198 float-covariance q 8.93e-6, fixed residual RSS 2.020e-05 m^2 (noise199 level).200- Second-best (486, -195, 371, -516, 260), q 4.481e-3, fixed residual201 RSS 4.492e-03 m^2 (two orders above the best RSS).202- Ratio q_second/q_best = 501.5 >= 3.0, resolved True; the residual-RSS203 ranking agrees (ratio about 222).204205Fixed solution (integer set (487, -196, 372, -514, 259) imposed):206207- Baseline ECEF (-2.000112, 20.000074, 12.000159) m; per-axis error208 versus truth (-0.112, +0.074, +0.159) mm and 3-D error 0.209 mm:209 imposing the integers moves the rover fix from the 5.7 cm float level210 to the sub-millimetre level.211- Per-axis 1-sigma (0.003460, 0.000614, 0.000782) m (3.46 mm radial,212 0.61 and 0.78 mm along-track/cross-track): the vertical (radial) axis213 carries the weakest precision, as for the single-receiver geometry.214- sigma0 0.001297 m, residual RMS 0.001160 m.215- ENU offset (E 20.000074, N 12.000159, U -2.000112) m; error versus216 truth (+0.074, +0.159, -0.112) mm.217218Time-differenced arm (ambiguity-free double differences across adjacent219epochs, geometry-only precursor solve over 10 equations):220221- Baseline (-2.022031, 19.986400, 12.034498) m, per-axis error222 (-0.0220, -0.0136, 0.0345) m and 3-D error 0.0431 m: the coarse223 decimetre-level geometry read that the float/fix pipeline refines.224- Residual RMS 0.001816 m; no cycle-level integer residue survives the225 epoch difference (a one-cycle residue would be about 0.19 m).226227Noiseless cross-check (noise offsets zeroed, curvature terms still228present):229230- Float baseline 3-D error 1.74e-05 m, max ambiguity error 1.36e-04231 cycles, residual RMS 6.31e-07 m: the O(|b|^2/rho) curvature of the232 linearized model is the float floor.233- Nearest-integer rounding equals the true set; noiseless fixed234 baseline 3-D error 8.22e-06 m.235236Read-off: L1 double-difference carrier-phase observables from a 6-237satellite geometry over a 20 minute arc with about 1.1 mm noise give a238float baseline accurate to about 6 cm (0.25 percent of the 23.4 m239baseline) with float ambiguities within 0.36 cycles of the integers, so240the rounding-candidate search resolves the true integer set with a ratio241of 501.5 and the fixed rover baseline lands within 0.21 mm (3-D) of the242truth with a 3.5 mm vertical 1-sigma.243244## Verification245246- Run scripts/test_gnss_rtk_positioning.py; the contract test must pass247 under both the system python3 and the pyenv 3.13.12 hook interpreter248 (39 tests, offline, deterministic).249- Confirm the worked-example contract: float per-axis error below250 0.10 m on every axis and 3-D error below 0.15 m (module 0.0574 m),251 sigma0 within 1e-3 m of 0.001269 m, residual RMS below 0.005 m, every252 float ambiguity within 0.5 cycles of the true integer (module max253 0.358) and the per-ambiguity 1-sigma within 1e-2 cycles of the anchor254 values (0.21261, 0.05863, 0.04662, 0.20498, 0.08256).255- Confirm the precision identity: every per-axis 1-sigma equals sigma0256 times the square root of the covariance diagonal, the anchor values257 (0.01325, 0.01533, 0.02850) m hold within 1e-3 m, and the ambiguity258 sigmas are the metre covariance diagonal divided by LAMBDA_L1.259- Confirm the integer-resolution contract: candidates_searched 3125,260 best_candidate (487, -196, 372, -514, 259), resolved True, ratio261 501.5 within 1 percent, second_best (486, -195, 371, -516, 260),262 best_rss below 1e-4 m^2 and second_rss above best_rss by a factor of263 100 or more.264- Confirm the fixed-solution contract: baseline within 0.001 m of the265 truth per axis (module errors -0.112, +0.074, +0.159 mm), 3-D error266 below 0.001 m, per-axis 1-sigma within 1e-4 m of (0.00346, 0.00061,267 0.00078) m and the ENU offset within 0.0005 m of (20.0, 12.0, -2.0).268- Confirm the time-differenced arm: 3-D error below 0.2 m (module269 0.0431 m), residual RMS below 0.005 m, and the epoch-differenced270 observables reproduce the documented noise differences with no271 cycle-level residue.272- Confirm the closed-form identities: single_difference(150.25, 152.75)273 = -2.5 cycles, form_double_differences([12.25, 9.75, -3.5], 0) =274 [-2.5, -15.75] cycles, time_differenced_dd([1.2, 3.4, 5.6],275 [1.0, 3.0, 5.0]) = [0.2, 0.4, 0.6] cycles, ecef_to_enu((-2, 20, 12),276 (6378137, 0, 0)) = (20, 12, -2) and LAMBDA_L1*F_L1 = C_LIGHT.277- Confirm the clock-cancellation identity: shifting the rover clock by278 a common offset at every epoch and adding the satellite clock offsets279 to both receivers leaves every double difference at machine precision280 (below 1e-6 cycles of leakage) and leaves the float solve unchanged.281- Confirm the noiseless identity: with the noise offsets zeroed the282 float 3-D error stays below 1e-4 m and the fixed 3-D error below283 1e-4 m.284- Confirm ValueError rejection of every non-physical input: fewer than285 5 satellites or fewer than 2 epochs, position/phase length mismatch,286 satellite count changing between epochs, reference index out of287 range, search_radius below 1 cycle, ratio_min at or below 1, an288 integer_ambiguities tuple of the wrong length, a coincident satellite,289 non-finite positions, phases, epochs or base position, a zero base290 position in ecef_to_enu, and a singular normal matrix.291- Confirm determinism: repeated solves return identical outputs; the292 module imports nothing beyond math and contains no RNG and no orbit293 propagation.294295## Related leaves296297- skills/gnc-autonomy/navigation/gnss-pseudorange-positioning: owns the298 single-receiver absolute code fix (position and clock bias from299 pseudoranges); this leaf is differential and carrier-phase based,300 solves no pseudorange and estimates no receiver clock bias (the301 receiver clock terms cancel in the double differences).302- skills/gnc-autonomy/navigation/gnss-carrier-smoothing: owns the Hatch303 smoothing side of the carrier observable (smoothing CODE with carrier304 increments in one receiver); this leaf differences raw carrier phases305 between two receivers, never smooths code and never runs a Hatch306 recursion, and integer ambiguity resolution is in scope here only.307- skills/gnc-autonomy/navigation/gnss-doppler-velocity-positioning:308 owns the single-receiver snapshot velocity step from carrier-phase309 delta-range-rate observables with Kepler propagation; this leaf310 consumes static carrier-phase accumulations, estimates no velocity311 and performs no orbit propagation (the satellite ECEF positions at312 each epoch are supplied inputs).313- skills/gnc-autonomy/navigation/gnss-raim-fde: owns integrity on the314 single-receiver measurement set; this leaf produces no detection315 verdict and no protection level (the arc is fault-free by316 assumption).317- skills/gnc-autonomy/navigation/dilution-of-precision and the other318 GNSS navigation siblings of the pack: the remaining single-receiver319 geometry and quality context around this differential step.320- skills/gnc-autonomy/space/orbit-determination and321 skills/gnc-autonomy/space/orbit-dynamics: the space-domain siblings;322 their propagation machinery is never needed here because satellite323 positions arrive as per-epoch inputs.324325## Pitfalls326327- Confusing this leaf with the code position fix: the rover position328 this leaf reports is the differential baseline relative to a FIXED329 base from double-difference carrier phase, not an absolute330 single-point code fix. Code pseudorange positioning and receiver331 clock-bias estimation belong to gnss-pseudorange-positioning.332- Treating the carrier observable as something to smooth: this leaf333 differences raw carrier-phase streams between two receivers and never334 smooths code pseudoranges. Hatch smoothing, smoothed ranges and335 code-carrier divergence monitoring belong to gnss-carrier-smoothing;336 the smoothing leaf explicitly excludes the integer ambiguity337 resolution that is the core step here.338- Expecting a velocity or a propagated constellation: this leaf339 estimates no velocity, no range rate and no clock drift, and it340 performs no Kepler or broadcast-ephemeris propagation; the satellite341 ECEF positions at each epoch are supplied inputs (the domain of342 gnss-doppler-velocity-positioning).343- Forgetting that the clock terms cancel in the double differences:344 the receiver clock difference is common to every satellite at an345 epoch and cancels when the reference satellite is subtracted, so no346 clock-bias unknown appears in the state; the state is the 3-axis347 baseline plus one ambiguity per non-reference pair.348- Solving only the baseline: the float state is (b_x, b_y, b_z,349 A_1 ... A_m) with one float ambiguity per pair, 8 unknowns for 6350 satellites; dropping the ambiguity columns leaves the decimetre-level351 time-differenced read (step 8) rather than the fixed solution.352- Reading the float baseline as the final answer: the float fix is at353 the 6 cm level on the worked example; the integer-constrained fix354 lands at 0.21 mm, so reporting the float baseline without running the355 integer resolution undersells the observable by two orders of356 magnitude.357- Quoting the vertical axis precision as if it matched the horizontal:358 the radial (local vertical) axis is the weakest for the geometry359 (anchor 1-sigma 3.46 mm against 0.61 and 0.78 mm on the other axes).360- Trusting an arc with a cycle slip: the double-difference ambiguities361 are constant only across a slip-free arc; the model assumes no cycle362 slip (cycle-slip detection and repair are out of scope for this363 leaf), so a slipped arc corrupts the integer search before the ratio364 test can catch it.365- Treating the q-form weight as a full covariance with the sigma0366 scale: Q is the inverse-normal ambiguity block scaled by367 1/LAMBDA_L1^2 per the defining relation; the sigma0 scale cancels in368 the ratio test and is omitted from the quadratic-form weight.369370## Behavior contract (gate 3)371372Run the deterministic contract test (stdlib unittest, offline):373374 python3 skills/gnc-autonomy/navigation/gnss-rtk-positioning/scripts/test_gnss_rtk_positioning.py375376The test covers the worked-example contract with the module's real377outputs as targets: the step-1 module constants and line-of-sight378geometry, the step-2 single-difference traverse with the spec phase-379stream table, the step-3 double-difference traverse with the clock-380cancellation identity below 1e-6 cycles, the step-4 float solve on the381worked set (baseline, ambiguities, sigma0, residual RMS, covariance382precision identity, system dimensions), the step-5 integer resolution383(candidate count 3125, best and second-best candidate sets, ratio 501.5384within 1 percent, residual-RSS ordering), the step-6 fixed solution with385the imposed integer set (sub-mm baseline, per-axis sigmas) and the386ambiguity-constancy identity across epochs, the step-7 ENU offset at the387demo base, the step-8 time-differenced precursor read with the388no-cycle-residue identity, the noiseless curvature-floor cross-check,389ValueError rejection of every non-physical input in the spec validation390list (fewer than 5 satellites, fewer than 2 epochs, length mismatch,391count change, reference index, search radius, ratio threshold, wrong392ambiguity tuple length, coincident satellite, non-finite values, zero393base, singular normal matrix), determinism across reruns and the394pure-stdlib no-RNG import check. It passes under both the system python3395and the pyenv 3.13.12 hook interpreter.396397## Compliance398399- Standards referenced, not reproduced: RTCA DO-229 frames the GNSS400 airborne navigation context; this leaf implements standard engineering401 methodology with name and paraphrase only, no MOPS algorithm or table402 text, summary-only per standards-map.yaml.403- compliance: STANDARDS-REF, gated: false.