Ionospheric Delay Correction (gnc-autonomy/navigation/ionospheric-delay-correction)
Use when a GNSS single-frequency navigation chain must correct an L1
pseudorange for the broadcast ionospheric delay before it is passed to a
position fix. This leaf implements the Klobuchar broadcast ionospheric
delay algorithm in pure Python, stdlib only: the earth-centred angle to
the ionospheric pierce point, the subionospheric point and its
geomagnetic latitude, the local time of day, the amplitude and period
quartic polynomials in the broadcast alpha and beta coefficients, the
day-curve shape about the 14:00 local-time peak, the vertical delay with
its 5 ns night floor, and the elevation obliquity factor mapping the
vertical delay to the slant delay. It pairs with
gnc-autonomy/navigation/gnss-pseudorange-positioning, which consumes the
corrected pseudorange in its iterated least squares fix, and
gnc-autonomy/navigation/gnss-carrier-smoothing, whose ionospheric
divergence monitor tracks the code-carrier growth rate rather than the
per-source delay magnitude computed here.
Domain quick reference
- Earth-centred angle: with E_SC = el/180 (semicircles), psi_SC =
0.0137/(E_SC + 0.11) - 0.022; psi_deg = 180 * psi_SC.
- Subionospheric point: lat_i_SC = clamp(lat_SC + psi_SCcos(az_rad),
-0.416, 0.416), lon_i_SC = lon_SC + psi_SCsin(az_rad)/cos(pi*lat_i_SC),
with the returned longitude normalized into [-180, 180) because a
low-elevation pierce point near the antimeridian can wrap past it.
- Pierce-point geomagnetic latitude: phi_m_SC = lat_i_SC + 0.064 *
cos(pi*(lon_i_SC - 1.617)), the pole offset and pole longitude of the
broadcast model in semicircles.
- Local time of day: t_local = (43200 * lon_i_SC + tow) mod 86400
seconds, periodic in the pierce-point longitude and the GPS time of
week.
- Amplitude and period: A = sum alpha_n * phi_m_SC^n clamped to >= 0.0,
P = sum beta_n * phi_m_SC^n clamped to >= 72000 s, both quartic
polynomials evaluated at phi_m in semicircles.
- Day-curve shape: x = 2pi(t_local - 50400)/P; for |x| < pi/2,
c = 1 - x^2/2 + x^4/24 (day branch); otherwise c = 0.0 (night branch).
- Vertical delay: T_vert = 5e-9 + A*c seconds, never below the 5 ns base.
- Obliquity factor: F = 1 + 2*((96 - el)/90)^3, el in degrees
(Klobuchar 1987 practical form), strictly decreasing on (0, 90].
- Slant delay: T_slant = F * T_vert; metres = seconds * c (vacuum speed
of light, 299792458.0 m/s).
- Units: latitudes in degrees north, longitudes in degrees east
(negative west) in [-180, 180], elevation and azimuth in degrees
(azimuth from north, clockwise, [0, 360)), times in seconds, one
semicircle = 180 degrees. IS-GPS-200 section 20.3.3.5.1 and RTCA
DO-229 frame the broadcast context; the relations above are the
standard engineering method, summary-only.
Workflow
- Fix the geometry: user latitude and longitude, satellite elevation
and azimuth. Compute the earth-centred angle with
earth_center_angle_deg, then the subionospheric point with
subionospheric_point_deg.
- Compute the pierce-point geomagnetic latitude with
geomagnetic_latitude_deg, and the local time of day at the pierce
point with local_time_seconds using the GPS time of week.
- Evaluate the broadcast-alpha-beta-coefficient polynomials at that
geomagnetic latitude: amplitude_seconds for A and period_seconds for
P, each clamped per the model.
- Form the day-curve shape with day_shape_factor at the local time and
period, then the vertical delay with vertical_delay_seconds.
- Map the vertical delay to the slant delay with slant_factor and
slant_delay_seconds, the full klobuchar-broadcast-model pipeline from
raw geometry and coefficients to the slant ionospheric delay.
- Convert to metres with delay_meters or the slant_delay_meters
convenience wrapper before subtracting the correction from the L1
pseudorange.
- Confirm the deterministic checks with the contract test
scripts/test_ionospheric_delay_correction.py.
Worked example
Mid-latitude user at 40.0 deg N, -105.0 deg (105 deg W), GPS time of
week 0.0 s, satellite at 40.0 deg elevation and 160.0 deg azimuth, with
the documented Klobuchar example broadcast set alpha =
[0.8382e-08, -0.7451e-08, -0.5960e-07, 0.1192e-06] and beta =
[0.1306e+06, -0.3277e+05, -0.6554e+05, 0.1311e+06].
- Earth-centred angle: earth_center_angle_deg(40.0) = 3.46274247491639
deg.
- Subionospheric point: subionospheric_point_deg(40.0, -105.0, 40.0,
160.0) = (36.7460864486391 deg N, -103.521982351049 deg), no latitude
clamp engaged.
- Pierce-point geomagnetic latitude: geomagnetic_latitude_deg(36.74609,
-103.52198) = 46.2306740519375 deg (0.256837 SC).
- Local time of day: local_time_seconds(-103.521982351049, 0.0) =
61554.7242357482 s (17:06 local, afternoon).
- Amplitude: amplitude_seconds(alpha, 46.2306740519375) =
4.55630181644608e-09 s. Period: period_seconds(beta, 46.2306740519375)
= 120081.223784471 s, above the 72000 s clamp.
- Day curve: day_shape_factor(61554.7242357482, 120081.223784471) =
0.834503142819751 (day branch).
- Vertical delay: vertical_delay_seconds(alpha, beta, 46.2306740519375,
61554.7242357482) = 8.80224818545959e-09 s (8.802248 ns), the 5 ns
base plus 4.556302 ns times 0.834503.
- Obliquity factor: slant_factor(40.0) = 1.48179972565158.
- Slant delay: slant_delay_seconds(alpha, beta, 40.0, -105.0, 40.0,
160.0, 0.0) = 1.30431689463311e-08 s (13.043169 ns), and
slant_delay_meters gives 3.910244 m.
Elevation sweep at the same user and azimuth (T_slant grows steeply
toward the horizon through the obliquity factor while the vertical
delay stays nearly flat): 10 deg gives 25.268371 ns / 7.575267 m (F
2.745010); 30 deg gives 15.879641 ns / 4.760597 m (F 1.788741); 40 deg
gives 13.043169 ns / 3.910244 m (F 1.481800); 60 deg gives 9.827876 ns /
2.946323 m (F 1.128000); 90 deg gives 8.648579 ns / 2.592779 m (F
1.000593, the zenith slant-to-vertical ratio).
Verification
- Confirm the worked-example chain (earth-centred angle, subionospheric
point, geomagnetic latitude, local time, amplitude, period, day
shape, vertical delay, slant delay and slant delay in metres) matches
the anchors above within 1e-6 relative.
- Confirm slant_factor(90.0) equals the closed form
1 + 2*(6/90)^3 = 1.00059259259259 within 1e-12, that slant_factor is
strictly decreasing on (0, 90], and that it approaches
1 + 2*(96/90)^3 = 3.4272592592592593 as elevation approaches zero.
- Confirm day_shape_factor(50400, P) = 1.0 exactly (the 14:00 peak) and
that vertical_delay_seconds at t_local 3600 returns exactly the 5e-9 s
night floor.
- Confirm amplitude_seconds and period_seconds collapse to alpha0 and
beta0 at the geomagnetic equator (phi_m = 0), and that a negative raw
amplitude clamps to 0.0 while a raw period below 72000 s clamps to
72000.0.
- Confirm subionospheric_point_deg normalizes a longitude that wraps
past the antimeridian into [-180, 180) without raising.
- Confirm every non-physical input (elevation outside (0, 90], user
latitude/longitude/azimuth out of range, GPS time of week out of
[0, 604800), a malformed or negative-constant coefficient set, an
out-of-range geomagnetic latitude or local time, a negative delay)
raises ValueError, including through the full slant_delay_seconds
pipeline.
- Run the contract test offline: python3
scripts/test_ionospheric_delay_correction.py (35 tests,
deterministic).
Related leaves
- gnc-autonomy/navigation/gnss-carrier-smoothing: owns the carrier
smoothing recursion and the ionospheric divergence monitor on the
code-carrier growth rate, not the per-source delay magnitude.
- gnc-autonomy/navigation/gnss-pseudorange-positioning: consumes the
corrected pseudorange in its iterated least squares fix and residual
statistics.
- gnc-autonomy/navigation/gnss-rtk-positioning: the differential
technique that cancels most of the ionospheric delay through double
differences instead of modeling it per source.
Pitfalls
- Feeding the vertical delay straight into the pseudorange correction:
the model corrects a slant line of sight, so vertical_delay_seconds
must always be mapped through slant_factor before it is subtracted
from a real pseudorange (3.910244 m slant against 2.638848 m vertical
in the worked example).
- Treating psi_SC or phi_m_SC in degrees: the amplitude and period
polynomials and the pole-offset relations are pinned in semicircles
(1 SC = 180 deg); feeding degree values directly into the polynomial
sum inflates every coefficient term by a factor of 180.
- Skipping the longitude normalization: a low-elevation pierce point
near the antimeridian (subionospheric_point_deg(0.0, 179.0, 5.0,
90.0) unwraps to -167.061613 deg) must be wrapped into [-180, 180)
before it feeds local_time_seconds or geomagnetic_latitude_deg, or
the periodic pole and local-time relations see the wrong branch.
- Reporting the raw polynomial sum instead of the clamped model value:
a negative raw amplitude clamps to 0.0 and a raw period below 72000 s
clamps to 72000.0 by model definition, not as an error condition.
- Confusing this leaf with gnss-carrier-smoothing's ionospheric
divergence monitor: that sibling tracks the code-minus-carrier growth
rate of an already-smoothed range, never the per-source delay
magnitude computed here, and this leaf never touches the Hatch
recursion or a smoothed range.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_ionospheric_delay_correction.py
The test covers the worked-example chain through every step of the
Workflow above (earth-centred angle, subionospheric point, geomagnetic
latitude, local time, amplitude and period polynomials, day-curve
shape, vertical and slant delay, metre conversion), the elevation sweep
with its per-row obliquity factors and monotone slant metres, the
obliquity closed-form and low-elevation limit, the zenith
slant-to-vertical ratio, the peak and night-floor identities, the
geomagnetic-equator polynomial collapse, the amplitude and period
clamps, longitude normalization, determinism, physical sanity bounds,
and ValueError rejection of every non-physical input including through
the full pipeline.
Compliance
- Standards referenced, not reproduced: RTCA DO-229 and IS-GPS-200
section 20.3.3.5.1 define the broadcast ionospheric correction
algorithm; the relations above are a summary paraphrase per
standards-map.yaml, never verbatim text.
- compliance: STANDARDS-REF, gated: false.
1---2name: ionospheric-delay-correction3description: Use when you must correct the L1 pseudorange for the broadcast ionospheric delay: apply the klobuchar-broadcast-model to the user position and the satellite line of sight, compute the geomagnetic latitude of the ionospheric pierce point from the earth-centred angle and the azimuth, evaluate the amplitude and period quartic polynomials in the broadcast alpha and beta coefficients at that geomagnetic latitude, form the vertical delay from the 5 ns base and the day-curve shape about the 14:00 local-time peak, and map the vertical delay to the slant delay with the elevation obliquity factor. Produces the pierce-point geomagnetic latitude, the amplitude and period coefficients, the vertical delay in seconds and the slant ionospheric delay in seconds and metres that gate the L1 pseudorange correction before positioning. Trigger: klobuchar broadcast model, ionospheric delay correction, slant delay correction, pierce point geometry, broadcast alpha and beta coefficients.4license: Apache-2.05---67# Ionospheric Delay Correction (gnc-autonomy/navigation/ionospheric-delay-correction)89Use when a GNSS single-frequency navigation chain must correct an L110pseudorange for the broadcast ionospheric delay before it is passed to a11position fix. This leaf implements the Klobuchar broadcast ionospheric12delay algorithm in pure Python, stdlib only: the earth-centred angle to13the ionospheric pierce point, the subionospheric point and its14geomagnetic latitude, the local time of day, the amplitude and period15quartic polynomials in the broadcast alpha and beta coefficients, the16day-curve shape about the 14:00 local-time peak, the vertical delay with17its 5 ns night floor, and the elevation obliquity factor mapping the18vertical delay to the slant delay. It pairs with19gnc-autonomy/navigation/gnss-pseudorange-positioning, which consumes the20corrected pseudorange in its iterated least squares fix, and21gnc-autonomy/navigation/gnss-carrier-smoothing, whose ionospheric22divergence monitor tracks the code-carrier growth rate rather than the23per-source delay magnitude computed here.2425## Domain quick reference2627- Earth-centred angle: with E_SC = el/180 (semicircles), psi_SC =28 0.0137/(E_SC + 0.11) - 0.022; psi_deg = 180 * psi_SC.29- Subionospheric point: lat_i_SC = clamp(lat_SC + psi_SC*cos(az_rad),30 -0.416, 0.416), lon_i_SC = lon_SC + psi_SC*sin(az_rad)/cos(pi*lat_i_SC),31 with the returned longitude normalized into [-180, 180) because a32 low-elevation pierce point near the antimeridian can wrap past it.33- Pierce-point geomagnetic latitude: phi_m_SC = lat_i_SC + 0.064 *34 cos(pi*(lon_i_SC - 1.617)), the pole offset and pole longitude of the35 broadcast model in semicircles.36- Local time of day: t_local = (43200 * lon_i_SC + tow) mod 8640037 seconds, periodic in the pierce-point longitude and the GPS time of38 week.39- Amplitude and period: A = sum alpha_n * phi_m_SC^n clamped to >= 0.0,40 P = sum beta_n * phi_m_SC^n clamped to >= 72000 s, both quartic41 polynomials evaluated at phi_m in semicircles.42- Day-curve shape: x = 2*pi*(t_local - 50400)/P; for |x| < pi/2,43 c = 1 - x^2/2 + x^4/24 (day branch); otherwise c = 0.0 (night branch).44- Vertical delay: T_vert = 5e-9 + A*c seconds, never below the 5 ns base.45- Obliquity factor: F = 1 + 2*((96 - el)/90)^3, el in degrees46 (Klobuchar 1987 practical form), strictly decreasing on (0, 90].47- Slant delay: T_slant = F * T_vert; metres = seconds * c (vacuum speed48 of light, 299792458.0 m/s).49- Units: latitudes in degrees north, longitudes in degrees east50 (negative west) in [-180, 180], elevation and azimuth in degrees51 (azimuth from north, clockwise, [0, 360)), times in seconds, one52 semicircle = 180 degrees. IS-GPS-200 section 20.3.3.5.1 and RTCA53 DO-229 frame the broadcast context; the relations above are the54 standard engineering method, summary-only.5556## Workflow57581. Fix the geometry: user latitude and longitude, satellite elevation59 and azimuth. Compute the earth-centred angle with60 earth_center_angle_deg, then the subionospheric point with61 subionospheric_point_deg.622. Compute the pierce-point geomagnetic latitude with63 geomagnetic_latitude_deg, and the local time of day at the pierce64 point with local_time_seconds using the GPS time of week.653. Evaluate the broadcast-alpha-beta-coefficient polynomials at that66 geomagnetic latitude: amplitude_seconds for A and period_seconds for67 P, each clamped per the model.684. Form the day-curve shape with day_shape_factor at the local time and69 period, then the vertical delay with vertical_delay_seconds.705. Map the vertical delay to the slant delay with slant_factor and71 slant_delay_seconds, the full klobuchar-broadcast-model pipeline from72 raw geometry and coefficients to the slant ionospheric delay.736. Convert to metres with delay_meters or the slant_delay_meters74 convenience wrapper before subtracting the correction from the L175 pseudorange.767. Confirm the deterministic checks with the contract test77 scripts/test_ionospheric_delay_correction.py.7879## Worked example8081Mid-latitude user at 40.0 deg N, -105.0 deg (105 deg W), GPS time of82week 0.0 s, satellite at 40.0 deg elevation and 160.0 deg azimuth, with83the documented Klobuchar example broadcast set alpha =84[0.8382e-08, -0.7451e-08, -0.5960e-07, 0.1192e-06] and beta =85[0.1306e+06, -0.3277e+05, -0.6554e+05, 0.1311e+06].8687- Earth-centred angle: earth_center_angle_deg(40.0) = 3.4627424749163988 deg.89- Subionospheric point: subionospheric_point_deg(40.0, -105.0, 40.0,90 160.0) = (36.7460864486391 deg N, -103.521982351049 deg), no latitude91 clamp engaged.92- Pierce-point geomagnetic latitude: geomagnetic_latitude_deg(36.74609,93 -103.52198) = 46.2306740519375 deg (0.256837 SC).94- Local time of day: local_time_seconds(-103.521982351049, 0.0) =95 61554.7242357482 s (17:06 local, afternoon).96- Amplitude: amplitude_seconds(alpha, 46.2306740519375) =97 4.55630181644608e-09 s. Period: period_seconds(beta, 46.2306740519375)98 = 120081.223784471 s, above the 72000 s clamp.99- Day curve: day_shape_factor(61554.7242357482, 120081.223784471) =100 0.834503142819751 (day branch).101- Vertical delay: vertical_delay_seconds(alpha, beta, 46.2306740519375,102 61554.7242357482) = 8.80224818545959e-09 s (8.802248 ns), the 5 ns103 base plus 4.556302 ns times 0.834503.104- Obliquity factor: slant_factor(40.0) = 1.48179972565158.105- Slant delay: slant_delay_seconds(alpha, beta, 40.0, -105.0, 40.0,106 160.0, 0.0) = 1.30431689463311e-08 s (13.043169 ns), and107 slant_delay_meters gives 3.910244 m.108109Elevation sweep at the same user and azimuth (T_slant grows steeply110toward the horizon through the obliquity factor while the vertical111delay stays nearly flat): 10 deg gives 25.268371 ns / 7.575267 m (F1122.745010); 30 deg gives 15.879641 ns / 4.760597 m (F 1.788741); 40 deg113gives 13.043169 ns / 3.910244 m (F 1.481800); 60 deg gives 9.827876 ns /1142.946323 m (F 1.128000); 90 deg gives 8.648579 ns / 2.592779 m (F1151.000593, the zenith slant-to-vertical ratio).116117## Verification118119- Confirm the worked-example chain (earth-centred angle, subionospheric120 point, geomagnetic latitude, local time, amplitude, period, day121 shape, vertical delay, slant delay and slant delay in metres) matches122 the anchors above within 1e-6 relative.123- Confirm slant_factor(90.0) equals the closed form124 1 + 2*(6/90)^3 = 1.00059259259259 within 1e-12, that slant_factor is125 strictly decreasing on (0, 90], and that it approaches126 1 + 2*(96/90)^3 = 3.4272592592592593 as elevation approaches zero.127- Confirm day_shape_factor(50400, P) = 1.0 exactly (the 14:00 peak) and128 that vertical_delay_seconds at t_local 3600 returns exactly the 5e-9 s129 night floor.130- Confirm amplitude_seconds and period_seconds collapse to alpha0 and131 beta0 at the geomagnetic equator (phi_m = 0), and that a negative raw132 amplitude clamps to 0.0 while a raw period below 72000 s clamps to133 72000.0.134- Confirm subionospheric_point_deg normalizes a longitude that wraps135 past the antimeridian into [-180, 180) without raising.136- Confirm every non-physical input (elevation outside (0, 90], user137 latitude/longitude/azimuth out of range, GPS time of week out of138 [0, 604800), a malformed or negative-constant coefficient set, an139 out-of-range geomagnetic latitude or local time, a negative delay)140 raises ValueError, including through the full slant_delay_seconds141 pipeline.142- Run the contract test offline: python3143 scripts/test_ionospheric_delay_correction.py (35 tests,144 deterministic).145146## Related leaves147148- gnc-autonomy/navigation/gnss-carrier-smoothing: owns the carrier149 smoothing recursion and the ionospheric divergence monitor on the150 code-carrier growth rate, not the per-source delay magnitude.151- gnc-autonomy/navigation/gnss-pseudorange-positioning: consumes the152 corrected pseudorange in its iterated least squares fix and residual153 statistics.154- gnc-autonomy/navigation/gnss-rtk-positioning: the differential155 technique that cancels most of the ionospheric delay through double156 differences instead of modeling it per source.157158## Pitfalls159160- Feeding the vertical delay straight into the pseudorange correction:161 the model corrects a slant line of sight, so vertical_delay_seconds162 must always be mapped through slant_factor before it is subtracted163 from a real pseudorange (3.910244 m slant against 2.638848 m vertical164 in the worked example).165- Treating psi_SC or phi_m_SC in degrees: the amplitude and period166 polynomials and the pole-offset relations are pinned in semicircles167 (1 SC = 180 deg); feeding degree values directly into the polynomial168 sum inflates every coefficient term by a factor of 180.169- Skipping the longitude normalization: a low-elevation pierce point170 near the antimeridian (subionospheric_point_deg(0.0, 179.0, 5.0,171 90.0) unwraps to -167.061613 deg) must be wrapped into [-180, 180)172 before it feeds local_time_seconds or geomagnetic_latitude_deg, or173 the periodic pole and local-time relations see the wrong branch.174- Reporting the raw polynomial sum instead of the clamped model value:175 a negative raw amplitude clamps to 0.0 and a raw period below 72000 s176 clamps to 72000.0 by model definition, not as an error condition.177- Confusing this leaf with gnss-carrier-smoothing's ionospheric178 divergence monitor: that sibling tracks the code-minus-carrier growth179 rate of an already-smoothed range, never the per-source delay180 magnitude computed here, and this leaf never touches the Hatch181 recursion or a smoothed range.182183## Behavior contract (gate 3)184185Run the deterministic contract test (stdlib unittest, offline):186187 python3 scripts/test_ionospheric_delay_correction.py188189The test covers the worked-example chain through every step of the190Workflow above (earth-centred angle, subionospheric point, geomagnetic191latitude, local time, amplitude and period polynomials, day-curve192shape, vertical and slant delay, metre conversion), the elevation sweep193with its per-row obliquity factors and monotone slant metres, the194obliquity closed-form and low-elevation limit, the zenith195slant-to-vertical ratio, the peak and night-floor identities, the196geomagnetic-equator polynomial collapse, the amplitude and period197clamps, longitude normalization, determinism, physical sanity bounds,198and ValueError rejection of every non-physical input including through199the full pipeline.200201## Compliance202203- Standards referenced, not reproduced: RTCA DO-229 and IS-GPS-200204 section 20.3.3.5.1 define the broadcast ionospheric correction205 algorithm; the relations above are a summary paraphrase per206 standards-map.yaml, never verbatim text.207- compliance: STANDARDS-REF, gated: false.