GNSS Doppler Velocity Positioning (gnc-autonomy/navigation/gnss-doppler-velocity-positioning)
Use when you must estimate the 3-D ECEF velocity of a GNSS receiver and its
receiver clock drift at a single epoch from per-satellite carrier-phase
delta-range-rate (doppler) observables. Each tracked satellite is
propagated from its broadcast-ephemeris Kepler elements to the
light-time-corrected transmit epoch; the range-rate model
rho_dot = (v_sat - v_rec) dot u + c*(dtr_dot - dts_dot) linearizes into
the geometry rows [u_x, u_y, u_z, -1.0] over the unknown state
(vx, vy, vz, c*dtr_dot), and the iterated least-squares normal equations
are solved by Gaussian elimination until the state correction falls below
tolerance. The receiver position enters from a pseudorange fix first or is
supplied; the internal pseudorange seed is never a reported output. The
module is pure stdlib math, deterministic, with no RNG anywhere. It pairs
with gnss-pseudorange-positioning for the position snapshot and with
gnss-carrier-smoothing for the smoothing side of the carrier observable;
both fences are described under Related leaves.
Domain quick reference
- Module constants: MU_EARTH = 3.986004418e14 m^3/s^2, OMEGA_EARTH =
7.2921150e-5 rad/s, C_LIGHT = 299792458.0 m/s, R_EARTH = 6378137.0 m
(spherical-earth demo receiver context), THETA_G0 = 1.1 rad (GMST at the
epoch, linear model theta(t) = THETA_G0 + OMEGA_EARTH*t),
EPHEMERIS_WINDOW = 7200.0 s, NEWTON_TOL = 1e-14, NEWTON_MAX = 60.
- Observable: the carrier-phase delta-range-rate (doppler) observable in
m/s is y_i = (v_sat_i - v_rec) dot u_i + c*(dtr_dot - dts_dot_i) +
eps_i, with u_i the unit line of sight from the receiver to the
satellite, v_rec the unknown receiver ECEF velocity and the receiver and
broadcast clock drifts (s/s) both scaled by c inside the model.
- Geometry epoch: the satellite state that produced the received signal is
evaluated at the transmit epoch t_tx = epoch - rho/c with rho the
geometric range, found by iteration; the light-time correction moves each
satellite about 300 m between reception and transmit evaluation. The LOS
runs from the receiver position at the reception epoch to the satellite
position at its transmit epoch.
- Linearized system: with the unknown state x = (vx, vy, vz, cdtr_dot),
the range-rate equation rearranges to u_i dot v_rec - cdtr_dot =
v_sat_i dot u_i - cdts_dot_i - y_i, so geometry row i is
[u_x, u_y, u_z, -1.0] and rhs_i = v_sat_i dot u_i - cdts_dot_i - y_i.
At least 4 satellites are needed; each pass solves the 4x4 normal
equations (H^T H) x = H^T z.
- Iteration: every pass re-evaluates each satellite state at its transmit
epoch, rebuilds the rows and rhs, and re-solves until the max state
correction falls below tol (deterministic: converges in 4 passes at
tol = 1e-9 on the worked example).
- Residual and precision: residual r_i = y_i - rho_dot_predicted with
rho_dot_predicted = (v_sat - v_rec) dot u + c*(dtr_dot - dts_dot);
residual_rms = sqrt(mean(r_i^2)); sigma0 = sqrt(sum(r_i^2)/(n - 4)) for
n > 4 (else the supplied doppler_sigma, default 0.05 m/s). The per-axis
1-sigma precision is sigma0sqrt(diag_ii((H^T H)^-1)) on the three
velocity axes and sigma0sqrt(diag_44) for the clock drift term, the
covariance diagonal taken from the converged normal matrix.
- Kepler propagation: two-body from the broadcast-style element record
(a, e, inc, raan, argp, M0 at toe), mean motion n = sqrt(MU/a^3), the
eccentric anomaly by Newton iteration on E - e sin E = M, perifocal
position and velocity rotated to ECI by Rz(raan)*Rx(inc)*Rz(argp), and
ECI-to-ECEF with v_ecef = R3(-theta) v_eci - omega_earth x r_ecef.
- Pseudorange seed: the internal geometry feeder solves the iterated
position LS with rows [-u, 1.0] over (x, y, z, clock bias in m) from
pseudorange records; it only supplies the receiver position when none is
provided and is never reported as an output of this leaf.
- RTCA DO-229 frames the GNSS receiver navigation context; the relations
above are standard engineering methodology, name and paraphrase only.
Workflow
- Fix the measurement set: the satellite element records, the carrier
delta-range-rate doppler observables y_i (m/s) at the reception epoch
and the receiver ECEF position (supplied position fix, or the internal
seed of step 2). mean_to_eccentric and the module constants anchor every
later step.
- Seed the receiver position when none is supplied:
pseudorange_position runs the iterated position least squares over the
pseudorange records and returns the geometry seed position only; the
position and clock bias are never reported outputs of this leaf.
- Propagate every satellite to the light-time-corrected transmit epoch:
kepler_state_eci propagates the two-body ECI state from the
broadcast-ephemeris elements (mean anomaly advanced, Kepler equation
solved, perifocal frame rotated to ECI) and ecef_state rotates it into
ECEF with the linear GMST model and the omega x r term.
- Form the line-of-sight geometry: line_of_sight returns the unit vector
u from the receiver to the satellite and the geometric range rho that
fixes the transmit-epoch offset rho/c.
- Predict the range rate of a candidate state:
predicted_range_rate applies rho_dot = (v_sat - v_rec) dot u +
c*(dtr_dot - dts_dot), the claim-form observable.
- Solve the 4x4 linear system: solve_normal4 runs Gaussian elimination
with partial pivoting on the normal equations of each pass; the clock
column carries the minus sign of the rearranged equation, so row dot
state = u dot v_rec - c*dtr_dot by construction.
- Run the iterated velocity fix: velocity_least_squares builds the rows
[u, -1.0] and rhs v_sat dot u - c*dts_dot - y_i over the doppler
residuals, re-evaluates the satellite states at the refreshed transmit
epochs each pass, and returns the report dict with the velocity, the
clock drift, the residuals and the precision terms.
- Read off the navigation outputs: the velocity fix (m/s), the recovered
clock drift in m/s and s/s, the post-fit residual RMS, sigma0 and the
per-axis 1-sigma precision from the doppler covariance, plus the
convergence state. These gate the navigation velocity output; confirm
with the contract test of the Behavior contract (gate 3) section.
Worked example
Demo receiver at the equator on the prime meridian, ECEF position
(6378137.000000, 0.000000, 0.000000) m (lat 0, lon 0, sea level on the
spherical earth), moving due north at 250 m/s, so the TRUE ECEF velocity
is (0.000000, 0.000000, 250.000000) m/s. TRUE receiver clock bias
1.200e-07 s and clock drift 1.200e-09 s/s (0.359751 m/s). Seven MEO
satellites at 55.5 deg inclination with semi-major axes near 2.656e7 m
placed so all seven sit above the local horizon (elevations 10.7 to
50.8 deg), toe = 0 s, reception epoch t = 0 s, THETA_G0 = 1.1 rad. All
values below are REAL outputs of the leaf module (stdlib math,
deterministic, exit 0), identical to the wave-43 spec anchors.
Constellation (broadcast-ephemeris style elements, deg; dts0 in s, dts_dot
in s/s, argp 0 for all):
- A: a 26560000.0 m, e 0.0080, raan -33.5573, M0 56.6777, dts0 -4.200e-09,
dts_dot -1.700e-10
- B: a 26558000.0 m, e 0.0120, raan 44.6470, M0 36.5232, dts0 2.800e-09,
dts_dot 9.000e-11
- C: a 26562000.0 m, e 0.0060, raan 104.6252, M0 14.4399, dts0 -1.900e-09,
dts_dot 2.100e-10
- D: a 26559000.0 m, e 0.0150, raan 98.5683, M0 -9.4352, dts0 3.600e-09,
dts_dot -6.000e-11
- E: a 26561000.0 m, e 0.0100, raan 141.4038, M0 -36.6604, dts0 -2.500e-09,
dts_dot 1.300e-10
- F: a 26560000.0 m, e 0.0180, raan 91.2561, M0 -52.6231, dts0 1.400e-09,
dts_dot -2.400e-10
- G: a 26557000.0 m, e 0.0090, raan 17.4134, M0 17.9818, dts0 -3.100e-09,
dts_dot 5.000e-11
Simulated doppler observables y_i (carrier delta range rate, m/s), with
documented deterministic noise offsets (m/s): A -270.264366 (noise
-0.031), B 342.286009 (0.024), C 143.629913 (0.045), D -29.518775
(-0.019), E 84.049775 (0.012), F -417.444778 (-0.037), G 56.378270
(0.028).
Velocity fix, receiver position SUPPLIED (the true position):
- Recovered velocity (m/s) (-0.023031, -0.027232, 249.970234), so the
per-axis error is (-0.023031, -0.027232, -0.029766) m/s and the 3-D
error is 0.046455 m/s: the 250 m/s north motion is recovered within
0.03 m/s on every axis.
- Recovered clock drift 0.345501 m/s = 1.152e-09 s/s versus the true
0.359751 m/s, error -0.014250 m/s (the broadcast dts_dot terms are
absorbed correctly, leaving the receiver drift as the 4th unknown).
- Residual RMS 0.023142 m/s, sigma0 0.035350 m/s (7 satellites, 3 degrees
of freedom), iterations 4, converged True.
- Per-axis 1-sigma precision (0.061974, 0.024222, 0.026807) m/s; the
radial (x, local vertical) axis is the weakest, as expected for a
horizon-confined geometry; clock drift 1-sigma 0.035651 m/s.
Pseudorange position feeder arm (geometry seed only, position never a
deliverable): the feeder converges to position (6378135.178265,
-1.180589, -1.303811) m, 3-D error 2.532 m, lumped clock bias 35.266 m,
residual RMS 1.630 m, iterations 5. Feeding that position into the doppler
LS recovers velocity (-0.023155, -0.027322, 249.970080) m/s, 3-D error
0.046667 m/s, and the maximum per-axis velocity difference versus the
supplied-position run is 1.537e-04 m/s, so the velocity fix is insensitive
to the position-seed error at the sub-mm/s level.
Noiseless cross-check: with the noise offsets set to zero, the LS recovers
(0.000000, 0.000000, 250.000000) m/s with 3-D error 1.649e-13 m/s and
residual RMS 1.075e-13 m/s (float noise only, still 4 iterations): the
exact-recovery identity of the linearized system.
Read-off: carrier-phase delta-range-rate observables from a 7-satellite
geometry with about 0.03 m/s measurement noise give a snapshot velocity
fix accurate to about 0.03-0.05 m/s per axis (about 0.1 percent of the
250 m/s platform speed) with the receiver clock drift recovered to
0.014 m/s, and the vertical (radial) axis carries the largest 1-sigma
precision of about 0.062 m/s.
Verification
- Run scripts/test_gnss_doppler_velocity_positioning.py; the contract test
must pass under both the system python3 and the pyenv 3.13.12 hook
interpreter (46 tests, offline, deterministic).
- Confirm the worked-example contract: per-axis velocity error below
0.10 m/s on every axis and 3-D error below 0.15 m/s (anchor 0.046455),
recovered clock drift within 0.05 m/s of 0.359751 m/s (anchor error
-0.014250), residual RMS below 0.05 m/s (anchor 0.023142), at most 8
iterations and converged True.
- Confirm the precision read-off: per_axis_sigma_mps equals sigma0 times
the square root of the covariance diagonal, the anchor per-axis values
(0.061974, 0.024222, 0.026807) m/s hold within 1e-3 and the clock drift
sigma 0.035651 m/s within 1e-3.
- Confirm clock_drift_mps = C_LIGHT * clock_drift_sps (the same float
divided by C_LIGHT, exact).
- Confirm the noiseless identity: zero noise offsets recover the true
velocity with 3-D error below 1e-6 m/s per axis (anchor 1.649e-13) and
residual RMS below 1e-6 m/s.
- Confirm the feeder arm: pseudorange_position on the documented
pseudoranges returns a position within 5 m of the truth (anchor 3-D
error 2.532 m) and the resulting velocity fix differs from the
supplied-position run by less than 1e-3 m/s per axis (anchor 1.537e-04).
- Confirm the Kepler sanity band: each satellite ECEF speed lies between
2500 and 3500 m/s (anchor 2926-3245), each elevation lies above 5 deg
and the light-time correction stays below 0.1 s.
- Confirm ValueError rejection of every non-physical input: e = 1.0 and
e < 0 in mean_to_eccentric and kepler_state_eci, a <= 0, epochs outside
the 7200 s ephemeris window, missing or non-finite element keys, a
satellite coincident with the receiver, fewer than 4 satellites, doppler
or pseudorange length mismatch, non-finite receiver position, doppler,
epoch or clock seed, iters = 0 and a singular 4x4 system.
- Confirm the module imports nothing beyond math and contains no RNG.
Related leaves
- skills/gnc-autonomy/navigation/gnss-pseudorange-positioning: the
position-domain sibling that owns the snapshot position and clock-bias
solution from pseudoranges; here the pseudorange solve is only the
internal geometry seed and the position is never a reported output.
- skills/gnc-autonomy/navigation/gnss-carrier-smoothing: owns the Hatch
smoothing side of the carrier observable; this leaf consumes only the
delta-range-rate observable and runs no smoothing recursion.
- skills/gnc-autonomy/navigation/gnss-raim-fde: owns integrity on the
pseudorange set; this leaf treats the doppler set as fault-free.
- skills/gnc-autonomy/navigation/gnss-carrier-smoothing and
skills/gnc-autonomy/navigation/dilution-of-precision: the remaining
GNSS navigation siblings of the pack.
- skills/gnc-autonomy/space/orbit-determination and
skills/gnc-autonomy/space/orbit-dynamics: the space-domain siblings;
the two-body Kepler propagation of broadcast-style elements to ECEF
satellite velocities is new to gnc-autonomy here.
- skills/space-systems/subsystems/doppler-shift: the spacecraft-comm
frequency-domain sibling; this leaf works in the range-rate domain in
m/s only and never computes a received frequency or offset.
Pitfalls
- Confusing this leaf with the position snapshot: the doppler LS solves
only the receiver velocity and clock drift. The position fix and clock
bias of the snapshot belong to gnss-pseudorange-positioning; here the
pseudorange solve is an internal geometry seed and the position is never
a reported output.
- Treating the observable as raw carrier phase: this leaf consumes the
carrier-phase delta-range-rate (doppler) observable in m/s only. Hatch
smoothing, smoothed range recursion, code-carrier divergence monitoring
and raw carrier-phase accumulation belong to gnss-carrier-smoothing.
- Working in the frequency domain: satellite velocities are never turned
into received frequencies, delta-f offsets or acquisition frequencies;
that is the spacecraft-comm domain of space-systems/subsystems/
doppler-shift. This leaf stays in m/s throughout.
- Ignoring the light-time correction: evaluating every satellite at the
reception epoch instead of the transmit epoch t_tx = epoch - rho/c moves
each state about 300 m, a real effect at the cm/s level of the velocity
fix; the iteration in velocity_least_squares handles it.
- Forgetting the clock drift is the 4th unknown: the state is
(vx, vy, vz, c*dtr_dot) with geometry rows [u, -1.0]; solving a 3-axis
velocity-only system over the doppler residuals absorbs the receiver
clock drift into every axis and corrupts the fix.
- Reading sigma0 with only 4 satellites: with n = 4 there are zero residual
degrees of freedom, so sigma0 falls back to the supplied doppler_sigma
(default 0.05 m/s); the covariance diagonal still reflects the geometry.
- Quoting the vertical axis precision as if it matched the horizontal: the
radial (local vertical) axis is the weakest for a horizon-confined
constellation (anchor 1-sigma 0.061974 m/s versus 0.024-0.027 m/s on the
other axes).
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 skills/gnc-autonomy/navigation/gnss-doppler-velocity-\
positioning/scripts/test_gnss_doppler_velocity_positioning.py
The test covers the worked-example contract with the module's real outputs
as targets: the step-1 module constants and measurement context, the
step-2 pseudorange_position geometry seed with its position and velocity
insensitivity bounds, the step-3 Kepler propagation and ECEF rotation
identities (circular-orbit sanity, period round trip, earth-rotation
subtraction), the step-4 line-of-sight geometry, the step-5
predicted_range_rate formula with the c-scaled net drift, the step-6
solve_normal4 solve and the row identity that pins the minus sign of the
clock column, the step-7 velocity_least_squares iterated fix on the worked
example (velocity, clock drift, residual RMS, sigma0, convergence) and the
step-8 precision read-off (per-axis sigma identity and anchor values), the
noiseless exact-recovery identity, the constellation sanity band (ECEF
speeds, elevations, light-time scale), ValueError rejection of every
non-physical input in the spec validation list, 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
receiver 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-doppler-velocity-positioning3description: Use when you must estimate the 3-D velocity of a GNSS receiver and its receiver clock drift at a single epoch from carrier-phase delta-range-rate (doppler) observables: propagate each satellite ECEF position and velocity from the broadcast-ephemeris Kepler elements, form the line-of-sight unit vector from the receiver position (pseudorange fix first, or supplied), predict the range rate rho_dot = (v_sat - v_rec) dot u + c*dt_dot, and solve the iterated least-squares system over the per-satellite doppler residuals, rows [u, -1.0], by 4x4 normal equations for the receiver velocity and clock drift. Produces the velocity fix in m/s with the per-axis 1-sigma precision from the doppler covariance, the recovered clock drift in m/s and s/s, the post-fit residual RMS and the convergence state that gate the navigation velocity output. Trigger: receiver velocity fix, doppler positioning, carrier delta range rate, clock drift estimate, velocity fix per-axis precision.4license: Apache-2.05---67# GNSS Doppler Velocity Positioning (gnc-autonomy/navigation/gnss-doppler-velocity-positioning)89Use when you must estimate the 3-D ECEF velocity of a GNSS receiver and its10receiver clock drift at a single epoch from per-satellite carrier-phase11delta-range-rate (doppler) observables. Each tracked satellite is12propagated from its broadcast-ephemeris Kepler elements to the13light-time-corrected transmit epoch; the range-rate model14rho_dot = (v_sat - v_rec) dot u + c*(dtr_dot - dts_dot) linearizes into15the geometry rows [u_x, u_y, u_z, -1.0] over the unknown state16(vx, vy, vz, c*dtr_dot), and the iterated least-squares normal equations17are solved by Gaussian elimination until the state correction falls below18tolerance. The receiver position enters from a pseudorange fix first or is19supplied; the internal pseudorange seed is never a reported output. The20module is pure stdlib math, deterministic, with no RNG anywhere. It pairs21with gnss-pseudorange-positioning for the position snapshot and with22gnss-carrier-smoothing for the smoothing side of the carrier observable;23both fences are described under Related leaves.2425## Domain quick reference2627- Module constants: MU_EARTH = 3.986004418e14 m^3/s^2, OMEGA_EARTH =28 7.2921150e-5 rad/s, C_LIGHT = 299792458.0 m/s, R_EARTH = 6378137.0 m29 (spherical-earth demo receiver context), THETA_G0 = 1.1 rad (GMST at the30 epoch, linear model theta(t) = THETA_G0 + OMEGA_EARTH*t),31 EPHEMERIS_WINDOW = 7200.0 s, NEWTON_TOL = 1e-14, NEWTON_MAX = 60.32- Observable: the carrier-phase delta-range-rate (doppler) observable in33 m/s is y_i = (v_sat_i - v_rec) dot u_i + c*(dtr_dot - dts_dot_i) +34 eps_i, with u_i the unit line of sight from the receiver to the35 satellite, v_rec the unknown receiver ECEF velocity and the receiver and36 broadcast clock drifts (s/s) both scaled by c inside the model.37- Geometry epoch: the satellite state that produced the received signal is38 evaluated at the transmit epoch t_tx = epoch - rho/c with rho the39 geometric range, found by iteration; the light-time correction moves each40 satellite about 300 m between reception and transmit evaluation. The LOS41 runs from the receiver position at the reception epoch to the satellite42 position at its transmit epoch.43- Linearized system: with the unknown state x = (vx, vy, vz, c*dtr_dot),44 the range-rate equation rearranges to u_i dot v_rec - c*dtr_dot =45 v_sat_i dot u_i - c*dts_dot_i - y_i, so geometry row i is46 [u_x, u_y, u_z, -1.0] and rhs_i = v_sat_i dot u_i - c*dts_dot_i - y_i.47 At least 4 satellites are needed; each pass solves the 4x4 normal48 equations (H^T H) x = H^T z.49- Iteration: every pass re-evaluates each satellite state at its transmit50 epoch, rebuilds the rows and rhs, and re-solves until the max state51 correction falls below tol (deterministic: converges in 4 passes at52 tol = 1e-9 on the worked example).53- Residual and precision: residual r_i = y_i - rho_dot_predicted with54 rho_dot_predicted = (v_sat - v_rec) dot u + c*(dtr_dot - dts_dot);55 residual_rms = sqrt(mean(r_i^2)); sigma0 = sqrt(sum(r_i^2)/(n - 4)) for56 n > 4 (else the supplied doppler_sigma, default 0.05 m/s). The per-axis57 1-sigma precision is sigma0*sqrt(diag_ii((H^T H)^-1)) on the three58 velocity axes and sigma0*sqrt(diag_44) for the clock drift term, the59 covariance diagonal taken from the converged normal matrix.60- Kepler propagation: two-body from the broadcast-style element record61 (a, e, inc, raan, argp, M0 at toe), mean motion n = sqrt(MU/a^3), the62 eccentric anomaly by Newton iteration on E - e sin E = M, perifocal63 position and velocity rotated to ECI by Rz(raan)*Rx(inc)*Rz(argp), and64 ECI-to-ECEF with v_ecef = R3(-theta) v_eci - omega_earth x r_ecef.65- Pseudorange seed: the internal geometry feeder solves the iterated66 position LS with rows [-u, 1.0] over (x, y, z, clock bias in m) from67 pseudorange records; it only supplies the receiver position when none is68 provided and is never reported as an output of this leaf.69- RTCA DO-229 frames the GNSS receiver navigation context; the relations70 above are standard engineering methodology, name and paraphrase only.7172## Workflow73741. Fix the measurement set: the satellite element records, the carrier75 delta-range-rate doppler observables y_i (m/s) at the reception epoch76 and the receiver ECEF position (supplied position fix, or the internal77 seed of step 2). mean_to_eccentric and the module constants anchor every78 later step.792. Seed the receiver position when none is supplied:80 pseudorange_position runs the iterated position least squares over the81 pseudorange records and returns the geometry seed position only; the82 position and clock bias are never reported outputs of this leaf.833. Propagate every satellite to the light-time-corrected transmit epoch:84 kepler_state_eci propagates the two-body ECI state from the85 broadcast-ephemeris elements (mean anomaly advanced, Kepler equation86 solved, perifocal frame rotated to ECI) and ecef_state rotates it into87 ECEF with the linear GMST model and the omega x r term.884. Form the line-of-sight geometry: line_of_sight returns the unit vector89 u from the receiver to the satellite and the geometric range rho that90 fixes the transmit-epoch offset rho/c.915. Predict the range rate of a candidate state:92 predicted_range_rate applies rho_dot = (v_sat - v_rec) dot u +93 c*(dtr_dot - dts_dot), the claim-form observable.946. Solve the 4x4 linear system: solve_normal4 runs Gaussian elimination95 with partial pivoting on the normal equations of each pass; the clock96 column carries the minus sign of the rearranged equation, so row dot97 state = u dot v_rec - c*dtr_dot by construction.987. Run the iterated velocity fix: velocity_least_squares builds the rows99 [u, -1.0] and rhs v_sat dot u - c*dts_dot - y_i over the doppler100 residuals, re-evaluates the satellite states at the refreshed transmit101 epochs each pass, and returns the report dict with the velocity, the102 clock drift, the residuals and the precision terms.1038. Read off the navigation outputs: the velocity fix (m/s), the recovered104 clock drift in m/s and s/s, the post-fit residual RMS, sigma0 and the105 per-axis 1-sigma precision from the doppler covariance, plus the106 convergence state. These gate the navigation velocity output; confirm107 with the contract test of the Behavior contract (gate 3) section.108109## Worked example110111Demo receiver at the equator on the prime meridian, ECEF position112(6378137.000000, 0.000000, 0.000000) m (lat 0, lon 0, sea level on the113spherical earth), moving due north at 250 m/s, so the TRUE ECEF velocity114is (0.000000, 0.000000, 250.000000) m/s. TRUE receiver clock bias1151.200e-07 s and clock drift 1.200e-09 s/s (0.359751 m/s). Seven MEO116satellites at 55.5 deg inclination with semi-major axes near 2.656e7 m117placed so all seven sit above the local horizon (elevations 10.7 to11850.8 deg), toe = 0 s, reception epoch t = 0 s, THETA_G0 = 1.1 rad. All119values below are REAL outputs of the leaf module (stdlib math,120deterministic, exit 0), identical to the wave-43 spec anchors.121122Constellation (broadcast-ephemeris style elements, deg; dts0 in s, dts_dot123in s/s, argp 0 for all):124125- A: a 26560000.0 m, e 0.0080, raan -33.5573, M0 56.6777, dts0 -4.200e-09,126 dts_dot -1.700e-10127- B: a 26558000.0 m, e 0.0120, raan 44.6470, M0 36.5232, dts0 2.800e-09,128 dts_dot 9.000e-11129- C: a 26562000.0 m, e 0.0060, raan 104.6252, M0 14.4399, dts0 -1.900e-09,130 dts_dot 2.100e-10131- D: a 26559000.0 m, e 0.0150, raan 98.5683, M0 -9.4352, dts0 3.600e-09,132 dts_dot -6.000e-11133- E: a 26561000.0 m, e 0.0100, raan 141.4038, M0 -36.6604, dts0 -2.500e-09,134 dts_dot 1.300e-10135- F: a 26560000.0 m, e 0.0180, raan 91.2561, M0 -52.6231, dts0 1.400e-09,136 dts_dot -2.400e-10137- G: a 26557000.0 m, e 0.0090, raan 17.4134, M0 17.9818, dts0 -3.100e-09,138 dts_dot 5.000e-11139140Simulated doppler observables y_i (carrier delta range rate, m/s), with141documented deterministic noise offsets (m/s): A -270.264366 (noise142-0.031), B 342.286009 (0.024), C 143.629913 (0.045), D -29.518775143(-0.019), E 84.049775 (0.012), F -417.444778 (-0.037), G 56.378270144(0.028).145146Velocity fix, receiver position SUPPLIED (the true position):147148- Recovered velocity (m/s) (-0.023031, -0.027232, 249.970234), so the149 per-axis error is (-0.023031, -0.027232, -0.029766) m/s and the 3-D150 error is 0.046455 m/s: the 250 m/s north motion is recovered within151 0.03 m/s on every axis.152- Recovered clock drift 0.345501 m/s = 1.152e-09 s/s versus the true153 0.359751 m/s, error -0.014250 m/s (the broadcast dts_dot terms are154 absorbed correctly, leaving the receiver drift as the 4th unknown).155- Residual RMS 0.023142 m/s, sigma0 0.035350 m/s (7 satellites, 3 degrees156 of freedom), iterations 4, converged True.157- Per-axis 1-sigma precision (0.061974, 0.024222, 0.026807) m/s; the158 radial (x, local vertical) axis is the weakest, as expected for a159 horizon-confined geometry; clock drift 1-sigma 0.035651 m/s.160161Pseudorange position feeder arm (geometry seed only, position never a162deliverable): the feeder converges to position (6378135.178265,163-1.180589, -1.303811) m, 3-D error 2.532 m, lumped clock bias 35.266 m,164residual RMS 1.630 m, iterations 5. Feeding that position into the doppler165LS recovers velocity (-0.023155, -0.027322, 249.970080) m/s, 3-D error1660.046667 m/s, and the maximum per-axis velocity difference versus the167supplied-position run is 1.537e-04 m/s, so the velocity fix is insensitive168to the position-seed error at the sub-mm/s level.169170Noiseless cross-check: with the noise offsets set to zero, the LS recovers171(0.000000, 0.000000, 250.000000) m/s with 3-D error 1.649e-13 m/s and172residual RMS 1.075e-13 m/s (float noise only, still 4 iterations): the173exact-recovery identity of the linearized system.174175Read-off: carrier-phase delta-range-rate observables from a 7-satellite176geometry with about 0.03 m/s measurement noise give a snapshot velocity177fix accurate to about 0.03-0.05 m/s per axis (about 0.1 percent of the178250 m/s platform speed) with the receiver clock drift recovered to1790.014 m/s, and the vertical (radial) axis carries the largest 1-sigma180precision of about 0.062 m/s.181182## Verification183184- Run scripts/test_gnss_doppler_velocity_positioning.py; the contract test185 must pass under both the system python3 and the pyenv 3.13.12 hook186 interpreter (46 tests, offline, deterministic).187- Confirm the worked-example contract: per-axis velocity error below188 0.10 m/s on every axis and 3-D error below 0.15 m/s (anchor 0.046455),189 recovered clock drift within 0.05 m/s of 0.359751 m/s (anchor error190 -0.014250), residual RMS below 0.05 m/s (anchor 0.023142), at most 8191 iterations and converged True.192- Confirm the precision read-off: per_axis_sigma_mps equals sigma0 times193 the square root of the covariance diagonal, the anchor per-axis values194 (0.061974, 0.024222, 0.026807) m/s hold within 1e-3 and the clock drift195 sigma 0.035651 m/s within 1e-3.196- Confirm clock_drift_mps = C_LIGHT * clock_drift_sps (the same float197 divided by C_LIGHT, exact).198- Confirm the noiseless identity: zero noise offsets recover the true199 velocity with 3-D error below 1e-6 m/s per axis (anchor 1.649e-13) and200 residual RMS below 1e-6 m/s.201- Confirm the feeder arm: pseudorange_position on the documented202 pseudoranges returns a position within 5 m of the truth (anchor 3-D203 error 2.532 m) and the resulting velocity fix differs from the204 supplied-position run by less than 1e-3 m/s per axis (anchor 1.537e-04).205- Confirm the Kepler sanity band: each satellite ECEF speed lies between206 2500 and 3500 m/s (anchor 2926-3245), each elevation lies above 5 deg207 and the light-time correction stays below 0.1 s.208- Confirm ValueError rejection of every non-physical input: e = 1.0 and209 e < 0 in mean_to_eccentric and kepler_state_eci, a <= 0, epochs outside210 the 7200 s ephemeris window, missing or non-finite element keys, a211 satellite coincident with the receiver, fewer than 4 satellites, doppler212 or pseudorange length mismatch, non-finite receiver position, doppler,213 epoch or clock seed, iters = 0 and a singular 4x4 system.214- Confirm the module imports nothing beyond math and contains no RNG.215216## Related leaves217218- skills/gnc-autonomy/navigation/gnss-pseudorange-positioning: the219 position-domain sibling that owns the snapshot position and clock-bias220 solution from pseudoranges; here the pseudorange solve is only the221 internal geometry seed and the position is never a reported output.222- skills/gnc-autonomy/navigation/gnss-carrier-smoothing: owns the Hatch223 smoothing side of the carrier observable; this leaf consumes only the224 delta-range-rate observable and runs no smoothing recursion.225- skills/gnc-autonomy/navigation/gnss-raim-fde: owns integrity on the226 pseudorange set; this leaf treats the doppler set as fault-free.227- skills/gnc-autonomy/navigation/gnss-carrier-smoothing and228 skills/gnc-autonomy/navigation/dilution-of-precision: the remaining229 GNSS navigation siblings of the pack.230- skills/gnc-autonomy/space/orbit-determination and231 skills/gnc-autonomy/space/orbit-dynamics: the space-domain siblings;232 the two-body Kepler propagation of broadcast-style elements to ECEF233 satellite velocities is new to gnc-autonomy here.234- skills/space-systems/subsystems/doppler-shift: the spacecraft-comm235 frequency-domain sibling; this leaf works in the range-rate domain in236 m/s only and never computes a received frequency or offset.237238## Pitfalls239240- Confusing this leaf with the position snapshot: the doppler LS solves241 only the receiver velocity and clock drift. The position fix and clock242 bias of the snapshot belong to gnss-pseudorange-positioning; here the243 pseudorange solve is an internal geometry seed and the position is never244 a reported output.245- Treating the observable as raw carrier phase: this leaf consumes the246 carrier-phase delta-range-rate (doppler) observable in m/s only. Hatch247 smoothing, smoothed range recursion, code-carrier divergence monitoring248 and raw carrier-phase accumulation belong to gnss-carrier-smoothing.249- Working in the frequency domain: satellite velocities are never turned250 into received frequencies, delta-f offsets or acquisition frequencies;251 that is the spacecraft-comm domain of space-systems/subsystems/252 doppler-shift. This leaf stays in m/s throughout.253- Ignoring the light-time correction: evaluating every satellite at the254 reception epoch instead of the transmit epoch t_tx = epoch - rho/c moves255 each state about 300 m, a real effect at the cm/s level of the velocity256 fix; the iteration in velocity_least_squares handles it.257- Forgetting the clock drift is the 4th unknown: the state is258 (vx, vy, vz, c*dtr_dot) with geometry rows [u, -1.0]; solving a 3-axis259 velocity-only system over the doppler residuals absorbs the receiver260 clock drift into every axis and corrupts the fix.261- Reading sigma0 with only 4 satellites: with n = 4 there are zero residual262 degrees of freedom, so sigma0 falls back to the supplied doppler_sigma263 (default 0.05 m/s); the covariance diagonal still reflects the geometry.264- Quoting the vertical axis precision as if it matched the horizontal: the265 radial (local vertical) axis is the weakest for a horizon-confined266 constellation (anchor 1-sigma 0.061974 m/s versus 0.024-0.027 m/s on the267 other axes).268269## Behavior contract (gate 3)270271Run the deterministic contract test (stdlib unittest, offline):272273 python3 skills/gnc-autonomy/navigation/gnss-doppler-velocity-\274positioning/scripts/test_gnss_doppler_velocity_positioning.py275276The test covers the worked-example contract with the module's real outputs277as targets: the step-1 module constants and measurement context, the278step-2 pseudorange_position geometry seed with its position and velocity279insensitivity bounds, the step-3 Kepler propagation and ECEF rotation280identities (circular-orbit sanity, period round trip, earth-rotation281subtraction), the step-4 line-of-sight geometry, the step-5282predicted_range_rate formula with the c-scaled net drift, the step-6283solve_normal4 solve and the row identity that pins the minus sign of the284clock column, the step-7 velocity_least_squares iterated fix on the worked285example (velocity, clock drift, residual RMS, sigma0, convergence) and the286step-8 precision read-off (per-axis sigma identity and anchor values), the287noiseless exact-recovery identity, the constellation sanity band (ECEF288speeds, elevations, light-time scale), ValueError rejection of every289non-physical input in the spec validation list, determinism across reruns290and the pure-stdlib no-RNG import check. It passes under both the system291python3 and the pyenv 3.13.12 hook interpreter.292293## Compliance294295- Standards referenced, not reproduced: RTCA DO-229 frames the GNSS296 receiver navigation context; this leaf implements standard engineering297 methodology with name and paraphrase only, no MOPS algorithm or table298 text, summary-only per standards-map.yaml.299- compliance: STANDARDS-REF, gated: false.