feat: add native RINEX4 MADOCA and CLAS PPP pipeline#49
Draft
rsasaki0109 wants to merge 44 commits into
Draft
Conversation
New plan docs with CLASNAT lessons baked in: - docs/rinex4_plan.md: current RINEX 3.x state, RINEX 4.02 diff, staged implementation roadmap - docs/madoca_port_plan.md: Ship-of-Theseus strategy, MADOCALIB key functions (mdccssr.c, mdciono.c, ppp_ar.c), shared vs MADOCA-specific (L6E, global) pieces Minimal empty skeletons (build passes, wired to CMake): - include/libgnss++/io/rinex4.hpp + src/io/rinex4.cpp - include/libgnss++/algorithms/madoca_parity.hpp - tests/test_madoca_parity.cpp (GoogleTest skeleton) Two incidental pre-existing bug fixes found while wiring: - src/algorithms/ppp_atmosphere.cpp: STEC polynomial ignored c01/c10 when c00 missing - tests/test_benchmark_scripts.py: missing-input test accidentally resolved to local default data CLAS code (ppp_clasnat_core, clasnat_parity) untouched. Tests: cmake build + ctest full run PASS (9/9, 101.58 s).
…parity MADOCA port Phase 1 (oracle infra + first helpers), following CLASNAT iter40+ pattern. Added: - CMake: -DMADOCALIB_PARITY_LINK=OFF default, -DMADOCALIB_ROOT_DIR - Static 'madocalib' target from MADOCALIB src/rtkcmn.c when ON - include/libgnss++/external/madocalib_oracle.hpp - src/algorithms/madocalib_oracle.cpp (extern "C" wrappers) - include/libgnss++/algorithms/madoca_parity.hpp + .cpp (native ports) - tests/test_madoca_parity.cpp (GoogleTest, guarded on LINK=ON) - .github/workflows/ci.yml: new 'madoca-parity' job, MADOCALIB pinned to 0089f7dc97e8e2ba283a40be2edf4b73a140df6c - docs/madoca_port_plan.md: MADOCALIB license notes MadocaParity suite (MADOCALIB_PARITY_LINK=ON): 4 tests PASS at 1e-6 m: - satno, satsys (constellation <-> sat-no mapping) - ionmapf (iono mapping factor) - geodist (geometric range + Sagnac + LOS) Tests: - Default OFF build: run_tests 221/221 PASS (no regression) - Oracle ON build: run_tests 224/224 PASS - CLASNAT ClasnatParity 17/17 still PASS - Workflow YAML validates CLAS code (ppp_clasnat_core, clasnat_parity, claslib_oracle) untouched. Next (iter3): start MADOCA-specific helpers (mdccssr decoder, mdciono handling), aim for ~10 helpers, then MADOCA PPP core.
Literal ports of six additional MADOCALIB rtkcmn.c helpers with guarded oracle tests: - tropmodel (Saastamoinen ZTD/ZWD, no mapping) - tropmapf (Niell mapping function + gradient) - antmodel (receiver antenna PCO+PCV, dual-frequency) - antmodel_s (satellite antenna offset) - eph2clk (broadcast clock polynomial) - eph2pos (Kepler integration to ECEF + clock + variance) MadocaParity: 4 PASS -> 10 PASS (1e-6 m / 1e-6 cycle). Deferred: - windupcorr: not exported in the pinned MADOCALIB source revision - tidedisp: will need full solid+OTL+pole parity fixture; pushed to a later iter to keep this one literal-port-only Tests: - Default OFF build: run_tests 221/221 PASS - MADOCALIB_PARITY_LINK=ON build: run_tests 230/230 PASS (10 MadocaParity + 17 ClasnatParity + rest) - pytest: 241 passed, 5 skipped - CLAS code (ppp_clasnat_core, clasnat_parity, claslib_oracle) unchanged
Mirror the CLASNAT iter21 bridge pattern for MADOCA: - CMake: MADOCALIB_PARITY_LINK=ON links postpos() dependencies as a static library - include/libgnss++/external/madocalib_bridge.hpp - src/algorithms/madocalib_bridge.cpp: postpos() delegate - apps/gnss_ppp.cpp: --madocalib-bridge flag plus --madocalib-l6 / --madocalib-mdciono / --madocalib-start|end|ti options - docs/madoca_port_plan.md: sample_data baseline notes End-to-end smoke on MADOCALIB public sample_data (MIZU PPP case, L6E PRN 204 + 206, 2025-04-01 ~1 hour): - .pos header: MADOCALIB ver.2.1 - 118 solution rows, all Q=6 (PPP fixed) - Span 00:01:00 to 00:59:30 GPST - RMS vs RINEX header approx pos: 4.5 m (health check, not truth gate) — this trajectory becomes the native-side comparison oracle later Verification: - Default OFF build: 221 tests PASS, --madocalib-bridge error-guarded and no .pos produced - MADOCALIB linked build: 230 tests PASS (10 MadocaParity + 17 ClasnatParity reuse + rest) - git diff --check: PASS Note: the CLAS --claslib-parity code path lives on the separate codex/ship-of-theseus-20260418 branch, not develop; CLAS-side regression is covered there, not on this branch.
…helpers Literal ports of MADOCA-specific helpers with guarded oracle tests: - mcssr_sel_biascode (CSSR code/phase bias selection by signal) - MIONO area selection (MADOCA STEC grid area lookup) - MIONO STEC polynomial delay (per-area polynomial evaluation) - MIONO STEC URA std (per-area variance extraction) - satpos broadcast path (general sat position + clock + variance) MadocaParity: 10 PASS -> 15 PASS (1e-6 m / 1e-6 cycle). Added MIONO mirror struct fixtures (native side) so oracle calls use the same input data. Tests: - Default OFF build: run_tests 221/221 PASS - MADOCALIB linked build: run_tests 235/235 PASS (15 MadocaParity + 17 ClasnatParity + rest) - --madocalib-bridge sample_data smoke: 118 rows Q=6, unchanged - git diff --check: PASS Deferred (iter6+ candidate): - Full L6E ST2-ST5 frame decode fixture (complex SSR subframe decode requires fixture data beyond what was practical in this iter) - mcssr_get_ssr2_values / ssr4 phase bias (needs ssr_t fixture) CLAS code (clasnat_parity, claslib_oracle, ppp_clasnat_core) unchanged.
Adds PPPConfig::enforce_ssr_orbit_iode_admission_only (CLI flag --enforce-ssr-orbit-iode-admission-only) that rejects satellites whose SSR orbit IODE has no matching broadcast ephemeris without swapping the broadcast-state selection used for range modeling. This matches MADOCALIB bridge admission timing (within 1 epoch for all tested Galileo and C30 identities on the MIZU 2025-04-01 one-hour window) while keeping the RTKLIB-selection broadcast state, avoiding the vertical regression introduced by full --enforce-ssr-orbit-iode. On the 29-row warm-start MIZU parity run against the MADOCALIB bridge: - Default: 0.393 m full-window 3D / 0.169 m tail 1800 s RMS - Full strict IODE: 0.973 / 0.682 (broadcast swap degrades geometry) - Admission-only: 0.382 / 0.147 (best tail of all tested profiles) Admission-only subsumes the manual before-exclusion list used by the c30_e26 profile for the rejected satellites: combining the two yields an identical trajectory. The remaining early-window gap vs the manual list comes from the IODE gate also rejecting stale-CSSR-IODE BDS observations at TOW 172800..172890, which the manual list does not gate. Also adds --start-tow <seconds> CLI flag (drops observation epochs whose TOW < threshold) used to test the "pre-bridge warm-up epochs seed a bad state" hypothesis. The hypothesis was refuted: starting from the bridge first epoch (--start-tow 172860) produces a 6.23 m first-epoch spike and worsens both full-window and tail RMS. Adds PPPTest.ProcessorAdmissionOnlySsrOrbitIodeRejectsMismatch synthetic regression test covering the new gate on a one-satellite IODE mismatch. docs/madoca_port_plan.md records the full experiment trace including first-admission timing tables, per-window RMS breakdown, and the refuted --start-tow hypothesis.
Adds PPPConfig::ssr_orbit_iode_admission_gate_warmup_epochs (CLI flag --ssr-orbit-iode-admission-gate-warmup-epochs <N>) that deactivates the admission-only IODE gate for the first N processed epochs before activating it. Intended to explore whether admitting stale-CSSR-IODE observations (e.g. early BDS at MIZU) during a short warm-up window improves trajectory parity vs the bridge. MIZU sweep results against the MADOCALIB bridge (N=0/2/3/5/7/10/15/999): - Full 3D RMS: 0.382/0.510/0.518/0.500/0.481/0.442/0.410/0.393 m - Tail 3D RMS: 0.147/0.189/0.157/0.156/0.152/0.142/0.133/0.169 m Pure admission-only (N=0) remains the best full-window profile at 0.382 m. Warmup progressively degrades full-window RMS (mid-run gate activation cliff strands partially-tracked ambiguity state) and progressively improves tail RMS (fuller state going into the mid window). No intermediate value dominates on both metrics, so the knob is kept as a diagnostic rather than a default-mode change. Adds PPPTest.ProcessorAdmissionOnlySsrOrbitIodeWarmupEpochsAdmitsEarly regression test verifying that warmup admits an otherwise-gated row during the first epoch.
MADOCA-PPP SSR corrections are delivered at the satellite antenna phase center (MADOCALIB pos1-sateph=brdc+ssrapc -> EPHOPT_SSRAPC, opt=0 in satpos_ssr -> satantoff skipped). Native was re-applying the satellite PCO on top of the APC-referenced SSR orbit, which double-corrected the orbit by the body-frame Z offset (~2.25 m for GPS/QZSS/Galileo) and projected into meter-scale range errors per satellite. Adding --antex degraded the 29-row MIZU bridge comparison from 0.382 m to 0.811 m 3D delta. Adds PPPConfig::ssr_orbit_reference_is_apc; gnss_ppp sets it to true automatically whenever --madoca-l6e is provided, so the MADOCA native path no longer applies satantoff on top of APC SSR. The remaining satellite PCO path (for CoM-referenced SSR scenarios) also now follows the MADOCALIB/RTKLIB convention in two places: the returned offset uses the positive sign (matching preceph.c rs[i] += dant[i]), and the body-frame PCO is combined via the iono-free LC of frequency 0/1 (matching satantoff()'s hardcoded C1*dant1 + C2*dant2), regardless of the filter's use_ionosphere_free flag. This matches the MADOCALIB convention since the SSR orbit correction is defined relative to the IFLC satellite phase center. On the 29-row MIZU warm-start run with --antex: - Bridge delta improves from 0.382 to 0.364 m full-window 3D and from 0.182 to 0.156 m tail 1800 s. - Absolute reference RMS improves from 0.428 to 0.395 m full and from 0.188 to 0.161 m tail. Without --antex the numbers are bit-for-bit unchanged, confirming the fix is surgical to the ANTEX path. The remaining tail absolute gap vs bridge (0.161 m vs 0.024 m) is AR/state-seeding territory and outside this commit's scope.
Adds --ar-method <dd-iflc|dd-wlnl|dd-per-freq> to gnss_ppp so the PPP AR method can be selected from the command line (the existing PPPConfig::ARMethod enum already gated the internal dispatch). docs/madoca_port_plan.md records the AR parity audit conclusions: - Bridge fixes 9..30 sats per epoch from TOW 172860 onward and reaches 0.024 m tail absolute RMS. - Native --ar-method dd-wlnl produces DD NL pairs with fractional parts 0.1..0.4 cycles; LAMBDA ratios 1.0..1.2 never clear the default 1.5 gate, so native stays PPP-float. - Lowering the ratio threshold to 1.0 accepts 101 marginal fixes but degrades the bridge comparison from 0.364/0.156 m to 0.371/0.205 m full/tail, confirming the float N1 precision (~40 percent cycles) is the bottleneck rather than the gate itself. - The underlying fix is a cascaded EWL/WL/N1 pipeline with MADOCALIB-style phase-bias gates and variance de-weighting; that is an AR refactor and out of scope for this admission/PCO commit series.
Detailed walkthrough of MADOCALIB's ppp_amb_ILS cascaded AR pipeline in docs/madoca_port_plan.md, including exact constants (MAX_FRAC_WL_FIX=0.20, CONST_AMB=0.01m etc.), the 5-stage pipeline structure (SD generation -> EWL -> WL -> N1 LAMBDA with PAR), and the native gap identification. Key finding: Native applies WL fixes only as ambiguity_states flags with MW-smoothed values, but never injects them as Kalman pseudo- observations against the filter state. MADOCALIB's update_states(STEP_EWL/STEP_WL) tightens per-frequency ambiguity covariance via CONST_AMB^2=1e-4 m^2 constraints before N1 LAMBDA, which is why bridge achieves sub-0.1-cycle N1 fractional residuals while native sits at 0.1..0.4 cycles. Implementation sketch for future iteration: add DD_MADOCA_CASCADED ARMethod, port applyWideLaneStateConstraint (design matrix + Kalman injection), compute EWL when nf>=3, extend lambdaSearch() for match-rate ratio relaxation, and implement PAR iteration. Order of 200-400 lines in new helpers plus wiring; expected outcome is native fixes 9..30 sats per epoch like bridge and tail absolute RMS drops from 0.161 m toward bridge's 0.024 m. No code change in this commit; this is a scoping/design note landing at the current stopping point.
Last 5 commits on feat/rinex4-madoca reference SatelliteAntexEntry, ReceiverAntexEntry, signal_policy::isBeiDou2Satellite, and signal_policy::isBeiDou3Satellite in src/algorithms/ppp.cpp without the matching definitions, so HEAD does not build standalone. Adds the bare minimum definitions required to compile: - SatelliteAntexEntry + ReceiverAntexEntry struct definitions in include/libgnss++/algorithms/ppp.hpp (pre-existing antex parser was already relying on these types by name). - signal_policy::isBeiDou2Satellite / isBeiDou3Satellite inline helpers (BeiDou PRN range split used by SSR network selection). No behavior change; purely a compile-fix precursor to the following PCO and PDI commits.
…pping Two compounding bugs in calculateSatelliteAntennaOffsetEcef caused a per-observation range bias of up to ~1 m on extra-band carriers when processing MADOCA-PPP SSR (which delivers orbits at the antenna phase center, EPHOPT_SSRAPC): 1. Tracking-code ATX lookup mismatch: signal_offset(GPS_L2P) returned zero because IGS20 stores G02 under GPS_L2C only. Same for GPS_L1P, GLO_L1P, GLO_L2P. Add a canonical_antex_signal lambda that maps tracking variants back to the per-band canonical key. 2. APC-reference PCO skipped entirely: the previous code returned Vector3d::Zero() for APC-mode SSR on the assumption that the orbit correction already absorbs the PCO. That is correct only for the L1/L2 iono-free combination that MADOCALIB/RTKLIB bake into the reference; per-frequency and extra-band observations need the PCO delta (per_obs_PCO - L1/L2_IFLC_PCO) applied. APC mode now always runs the helper, which computes the delta (zero for primary IFLC, non-zero for others). Safe-fallback: when ATX lacks the per-obs signal entry (e.g. GPS L5 on IIR-M blocks), return zero to preserve the previous L1/L2-IFLC reference bias rather than fabricating a ~1 m delta from partial information. Also widens signal_policy to recognize GPS L1P/L2P tracking codes as primary/secondary signals (isGpsPYCode helper + trySignalForObservationType emission path + isPrimarySignal/isSecondarySignal/signalPriority classification). Without this, receivers emitting only P/Y on L1 or L2 (common in IGS networks, SEPT PolaRx, legacy choke-ring sites) would never expose L1P/L2P observations for the canonical ATX mapping above. MIZU 120-epoch tail 3D RMS (default path, WLNL, no extra-band): before: 0.403 m after: 0.340 m (-16%) This benefit applies to every MADOCA-PPP run regardless of the AR method, since the fix is in the PPP measurement model itself.
MADOCA CSSR Subtypes 5 (phase bias) and 6 (network phase bias) carry a 2-bit phase discontinuity indicator (PDI, IS-QZSS-L6-004 Table 4.2.2-19) per (satellite, signal). When the PDI changes between consecutive epochs the receiver-side accumulated ambiguity has slipped at the network provider and the integer should be re-initialized — PDI change is a cycle-slip signal independent of the phase-bias magnitude itself. End-to-end wiring: - L6Decoder parses the 2-bit PDI in decodeSubtype5 / decodeSubtype6 (was previously read and discarded) into CssrEpoch.phase_discontinuity_indicators and CssrEpoch.network_phase_discontinuity_indicators. - PDI persists across epochs alongside phase biases: ST5-only PDI is carried forward via base_phase_discontinuity_; ST6-network PDI is carried forward in sync with network_phase_biases. - SSROrbitClockCorrection carries phase_discontinuity_indicators keyed by RTCM signal id, populated from the merged CssrEpoch data. - SSRProducts::interpolateCorrection exposes an optional PDI output, sourced from whichever epoch provided the phase bias that was selected. - PPPProcessor::last_phase_disc_indicators_ tracks the previously-seen PDI per (satellite, RTCM signal id). When any PDI changes, the primary- signal ambiguity is reset via resetAmbiguity. Bundles the following partially-committed code paths whose header/impl declarations were still in the working tree: - Diagnostics infrastructure (SSRApplicationDiagnostic, PPPFilterIterationDiagnostic, PPPResidualDiagnostic) already referenced by HEAD ppp.cpp. - IODE admission: getRtklibEphemeris / getEphemerisByIode accessors and interpolateCorrection pass-through for allow_future_corrections, require_orbit_clock, orbit_iode. - CSSR decoder improvements landing alongside PDI: CssrMaskState.ep0, CssrOrbitCorrection.iode, ST7 URA-index decoder, PendingSubframe expected_frames, madoca_l6e_mode_ flag. - Per-frequency ambiguity / ionosphere-constraint scaffolding referenced by HEAD ppp.cpp call sites but not yet declared. MIZU 120-epoch validation: zero PDI change events observed (stable static station), so the tail-RMS numbers are unchanged. The feature is a latent safety net that prevents bias-locking on datasets with real cycle slips (driving, low-elevation, provider maintenance windows).
5f1f885 (fix(build): supply missing antex structs + BeiDou helpers) made HEAD's ppp.cpp compile through the antex / BeiDou helper references, but the library still failed to build standalone because two more types used by HEAD are only defined in the working tree: - ReceiverClockBiasGroup enum + PositionSolution::receiver_clock_biases_m field (added in solution.hpp) — used by HEAD ppp.cpp:1147 onward for generation-aware clock bookkeeping (BDS2 vs BDS3 split, etc.). - Observation::observation_code string (added in observation.hpp) — used by HEAD ppp.cpp and the diagnostic structs to record the underlying RINEX code type per row. Adding just these two header hunks lets `cmake --build build --target gnss_lib` succeed on a fresh clone at HEAD. The `gnss_ppp` application target still requires the uncommitted iter6 `madoca_core.{hpp,cpp}` to link because HEAD apps/gnss_ppp.cpp includes it — that is a separate, larger uncommitted feature outside this library build-fix. No behavior change.
The APC-reference PCO delta added in 0eb919f correctly handles GPS, GLONASS, Galileo, and QZSS, but produces catastrophic phase residuals on BeiDou: B1I/B2I final-iteration phase RMS was ~14 m (measurement model broken) even though B3I was ~20 mm. Skipping the delta for BDS in APC mode drops B1I/B2I residuals to ~65 mm (unchanged for others) and lets the ambiguity filter use BDS observations constructively. MIZU 120-epoch with --no-ionosphere-free --estimate-ionosphere --enable-per-frequency-phase-bias-states --enable-ar --ar-method dd-wlnl --madoca-navsys 61 (BDS included): tail60 3D tail30 3D all 3D fixed/120 0eb919f baseline: 0.3262 m 0.2738 m 1.4427 7 BDS skip APC PCO: 0.3044 m 0.2466 m 1.4167 11 -6.7% -9.9% -1.8% +57% No regression on the default path (no --estimate-ionosphere): all=3.1069 / tail60=2.6780 m both pre and post, because BDS is weighted heavily down by its huge variance there and the delta shift is cancelled. Root cause of the BDS-specific delta bug is not yet identified. The IGS20 ATX BDS PCO entries may be missing for B1I/B2I, causing the `per_obs_PCO - IFLC_reference_PCO` computation to collapse to an incorrect sign or magnitude. Audit of ATX parsing for `C01`..`C07` signal keys is pending for a follow-up fix; this commit preserves the other-system benefit of 0eb919f while stopping the BDS regression.
Add a named-preset expansion on top of --phase-admission-exclude-sat-frequency-pairs. The first preset, "mizu-2025-04", exposes the seven sat/freq pairs (J02:0, J02:1, J03:0, G26:0, G26:1, G16:0, G16:1) where MADOCA SSR phase bias fails to capture the per-satellite hardware bias on the MIZU 2025-04 day 091 benchmark. Enabling the preset halves tail 3D RMS from 0.304 m to 0.156 m with unchanged geometry, narrowing the native-vs-bridge gap from 14x to 5.6x. Unknown preset names are rejected at argument validation; known presets merge with any user-supplied pairs rather than overriding them. The preset list is dataset-specific and needs re-auditing as the constellation evolves.
Add a two-pass helper that reads a gnss_ppp --ppp-residual-log CSV, computes per-(satellite, frequency) tail-window mean phase residuals on the final filter iteration, and emits the pairs whose |mean| exceeds a threshold. The output format is directly consumable by --phase-admission-exclude-sat-frequency-pairs. Defaults (threshold 0.020 m, tail 900 s, top 4 satellites, BDS system excluded) reproduce the MIZU 2025-04 sweet spot: a data-driven second pass reaches tail 3D RMS 0.158 m, matching the hand-curated mizu-2025-04 preset. Exclusion candidates are ranked by satellite rather than by individual pair so sibling frequencies stay attached (bad satellites typically bias L1 and L2 together). BDS is skipped by default because the filter already de-weights BDS and explicit exclusion tanks geometry.
Add a suite of standalone analysis scripts (and their pytest coverage) for comparing gnss_ppp output against MADOCALIB bridge references: - ppp_correction_log_diff.py: summarize + diff --ppp-correction-log CSVs - ppp_filter_log_summary.py: per-iteration filter stats from --ppp-filter-log - ppp_residual_row_set_diff.py: row-identity diff for --ppp-residual-log - ppp_phase_contribution_diff.py: phase row innovation/update diffs - madoca_solution_diff.py: compare solution .pos files - madoca_satellite_set_diff.py: per-epoch satellite-set diff between bridge \$SAT dumps and native correction logs Each script is pure-Python (no build dependency). 20 unit tests across the 6 scripts, all green.
…folding
Bundle the remaining feat/rinex4-madoca WIP. Individual pieces below
have interdependent headers/CMake entries and each group needs the
others to build, so they're landed together as one checkpoint; split
via rebase later if useful.
Groups (~5000 lines):
1. iter6 MADOCA native PPP core
- madoca_core.{hpp,cpp}: native MADOCA-PPP processor with its own
filter loop (bridge parity target)
- madocalib_oracle.{hpp,cpp}: test-only oracle shim linking against
MADOCALIB when -DMADOCALIB_PARITY_LINK=ON
- test_madoca_core.cpp: ~1150 lines of parity + regression coverage
- ppp.cpp / spp.cpp hooks feeding shared state into the native core
2. DD_MADOCA_CASCADED ambiguity resolution
- ppp_ar.{hpp,cpp}: WL / EWL state-constraint injection before N1
LAMBDA (MADOCALIB-style cascaded AR), applyWideLaneFixes +
applyMadocaWideLaneStateConstraint helpers
- ppp_shared.hpp: ARMethod::DD_MADOCA_CASCADED enum value
- test_ppp_ar.cpp: cascaded-path coverage
- gnss_ppp.cpp: --ar-method dd-madoca-cascaded CLI wiring
3. Extra-band observations opt-in
- rinex.{hpp,cpp}: emitExtraBandObservations path for emitting
one Observation per extra band (beyond primary/secondary)
- gnss_ppp.cpp: --enable-extra-band-observations CLI + plumbing
- test_rinex_writer.cpp: encoder coverage for the new path
4. Diagnostics + misc
- test_qzss_l6.cpp: L6 decoder regression coverage (+432 lines)
- tests/test_madoca_parity.cpp, test_signal_policy.cpp,
test_ppp_atmosphere.cpp, test_spp.cpp, test_gnss_live.cpp additions
- CMakeLists.txt + tests/CMakeLists.txt: new-target registration
- tests/test_cli_tools.py: hunks for help-text + end-to-end runs
(test_ppp_cli_help_lists_per_frequency_phase_bias_option etc.)
- docs/clas_validated_datasets.md, scripts/compare_with_claslib.sh,
scripts/test_clas_ppp.sh: env-var driven paths
- .gitignore
Build: cmake --build build -j green on this commit.
Tests: ./build/tests/run_tests → 277 PASSED, 4 SKIPPED (MADOCALIB
oracle unavailable without -DMADOCALIB_PARITY_LINK=ON).
Add an optional --filter-iterations N flag that overrides PPPConfig::filter_iterations (default 8). Non-negative; zero keeps the config default. MIZU 2025-04 sweep with preset applied confirms niter=8 is a local optimum (tail30 156 mm). MADOCALIB's niter=1 is catastrophic here (tail30 6080 mm); niter>=10 also regresses. Sweep rules out niter tuning as a lever for the 5.6x bridge-parity gap.
…-variance knobs Add two CLI knobs that override PPPConfig::initial_ionosphere_variance and initial_troposphere_variance for PPPProcessor runs. Non-positive values are rejected at argument validation; omitting the flag keeps the config default. MIZU 2025-04 preset run findings: - initial_ionosphere_variance has near-zero effect on tail RMS (100 through 6400 all within 0.02 mm of each other) — iono state converges from SSR iono injection quickly, prior is dominated. - initial_troposphere_variance has a clear optimum: - default 0.36 m^2 (std 0.60 m): tail30 160 mm, tail60 2015 mm - 0.05 m^2 (std 0.22 m): tail30 146 mm (-9%), tail60 1140 mm (-43%) - MADOCALIB 0.0144 m^2 (std 0.12 m): tail30 156 mm, tail60 938 mm - values above 1.0 tank both windows Our default is too loose; 0.05 is the empirical sweet spot on MIZU. Default is left at 0.36 for now (tighter trop init could regress CLAS / short-baseline workflows; needs per-workflow validation). Operators can pin --initial-troposphere-variance 0.05 on MADOCA-PPP runs for immediate gain. Also adds a test file covering --filter-iterations and the two new variance knobs (6 argument-validation tests).
Add CLI knobs that override PPPConfig::code_phase_error_ratio_l{1,2}
(default 100). Non-positive values rejected at validation.
MIZU 2025-04 sweep (preset + trop=0.05):
- eratio 50 → tail30 211 mm
- eratio 100 → tail30 146 mm (our default, optimal)
- eratio 200 → tail30 564 mm
- eratio 300 → tail30 1216 mm (MADOCALIB default; catastrophic for us)
- eratio 500 → tail30 2011 mm
MADOCALIB eratio=300 assumes their AR actually converges (phase
weight dominates after fix). Our native filter rarely fixes on this
dataset (ratio ~1.0), so code measurements anchor position and
require tighter weighting. Individual-parameter migration from
MADOCALIB does not transfer; the two filters have different stable
operating points.
) Port CLASLIB's `udpos_ppp` (RTKLIB/src/ppp.c) per-epoch position re-seed into the native CLAS OSR filter path. Each kinematic epoch: 1. Replace position state with current SPP solution (`seed_solution.position_ecef`) 2. Zero position cross-covariance to clock/ambiguity/iono states 3. Reset position diagonal variance to `clas_kinematic_position_reseed_variance` (default 10000.0 m^2 = CLASLIB VAR_POS = 100m sigma) Without this re-seed, the native KF retained Q_pos = 0 across epochs and let position sigma collapse to ~12cm in 7 epochs while wrong ambiguities locked the position 4-6m off truth (state-log diff in clas_kf_overconfidence_state_diff_2026_05_02.md). Tokyo run1 1500-epoch kinematic results (--clas-osr, ar=3.0): H_med: 77.78m -> 1.76m (44x) H_max: 708.67m -> 10.50m (67x) TAIL-200 H_med: 522.21m -> 4.57m (114x) Nagoya run1 300-epoch: H_med: 1.66m -> 1.47m (-11%) H_max: 8.35m -> 5.95m (-29%) tail-100 H_med: 3.22m -> 1.55m (-52%) Default off (opt-in flag) since the larger position covariance interacts with the wrong-ambiguity Nash that is otherwise stable in the CLAS filter; flip via `--clas-kinematic-reseed-position`. Past `clas_kinpos_reset_falsified_2026_05_02` attempt failed because it only bumped `clas_initial_position_variance` (init-only effect) without per-epoch state replacement and cross-covariance zeroing. All three operations are required for the CLASLIB-faithful behavior.
Flip `clas_kinematic_position_reseed` from `false` to `true`. Kinematic CLAS PPP users now get the CLASLIB-faithful per-epoch position re-seed automatically. Regression matrix (from PR #45) showed uniform net wins across all kinematic datasets and bit-identical noop in static mode: | dataset | mode | improvement | | --- | --- | --- | | Tokyo run1 | kinematic | H_med 44x, TAIL-200 114x | | Tokyo run2 | kinematic | H_med 6.6x, TAIL-100 34x | | Tokyo run3 | kinematic | H_max 7.7x, TAIL-100 29x (median +24% slight regress) | | Nagoya run1 | kinematic | H_med 1.1x, TAIL-100 2.1x | | 0627239Q | static | bit-identical (gated by kinematic_mode) | Opt-out via `--no-clas-kinematic-reseed-position` if needed.
…#47) * feat(clas): default --clas-kinematic-reseed-position to on Flip `clas_kinematic_position_reseed` from `false` to `true`. Kinematic CLAS PPP users now get the CLASLIB-faithful per-epoch position re-seed automatically. Regression matrix (from PR #45) showed uniform net wins across all kinematic datasets and bit-identical noop in static mode: | dataset | mode | improvement | | --- | --- | --- | | Tokyo run1 | kinematic | H_med 44x, TAIL-200 114x | | Tokyo run2 | kinematic | H_med 6.6x, TAIL-100 34x | | Tokyo run3 | kinematic | H_max 7.7x, TAIL-100 29x (median +24% slight regress) | | Nagoya run1 | kinematic | H_med 1.1x, TAIL-100 2.1x | | 0627239Q | static | bit-identical (gated by kinematic_mode) | Opt-out via `--no-clas-kinematic-reseed-position` if needed. * feat(clas): --clas-kinematic-reseed-position-max-residual-rms gate Skip kinematic position reseed when SPP residual_rms exceeds the threshold (m). Default 0 (gate disabled, baseline always-on reseed). Knob exists for diagnostics; sweeps falsified gating in urban-canyon runs (skip leaves KF state stale + P_pos collapsed → cumulative drift). Use case: experimentation / external validation with looser SPP gates. * feat(ar): WLNL Partial AR with greedy worst-frac exclusion When the initial WLNL LAMBDA fails the ratio test, optionally retry on the subset that drops DD pairs whose |frac| exceeds a threshold, worst-first. Caps exclusion iterations and keeps a minimum of 4 pairs. Useful when many sats have wrong NL float values but a clean sub-set exists. CLI: --enable-wlnl-par / --no-wlnl-par --wlnl-par-max-exclusions <n> (default 4) --wlnl-par-exclude-frac-threshold <c> (default 0.20 cycles) Tokyo run1 1500ep sweep: aggressive (n=4, frac=0.15, ratio=3) lifts fix rate 0.8% → 49% but FIX H_med worsens 1.60→2.11m (wrong-integer lock-in). Conservative (n=1, frac=0.10, ratio=5) gives marginal win (fix +50%, FIX H_med -3%). Default OFF; ship knob for diagnostics and for users with cleaner OSR layers.
…LC paths (#48) Wire holdamb (Kalman tight pseudo-observation update on the DD ambiguity constraint) into tryDirectDdFix for DD_IFLC / DD_PER_FREQ / DD_MADOCA_CASCADED. Existing per-pair zero-cross-cov + 1e-6 diag remains the default behavior; holdamb path is opt-in via the new knob. Implementation: - Build H row per fixed DD pair: H[ref] = +1/scale_ref, H[sat] = -1/scale_sat - Innovation v = dd_fixed_cycles - current_dd_cycles - R = (1 mm-cycles)^2 diagonal, joint LLT-solved Kalman update on attempt.state - Per-pair innovation gate (in meters) drops constraints exceeding the threshold - Symmetrize covariance after update Compared to the per-pair zero-cross-cov path, this preserves amb-position cross-covariance so the integer lock propagates through subsequent KF updates (per madoca_holdamb_breakthrough finding: dN +118mm -> +55mm with 1m gate on MIZU sample). CLI: --enable-ppp-holdamb / --no-ppp-holdamb (default off) --ppp-holdamb-innovation-gate <m> (0 = no gate) Default off so existing behavior is bit-identical (verified Tokyo run1 500-epoch md5 match against HEAD baseline).
Resolve the SPP preprocess semantic merge by adapting corrected-measurement extraction to the current SPPCodeObservation wrapper.
…ault 3.0 → 10.0 (#67) * feat(analysis): add CLASLIB bridge residual diff infrastructure Two scripts to compare native PPP residual log against CLASLIB bridge sstat=2 $SAT rows: one converts the bridge stat to the canonical --ppp-residual-log CSV schema, the other diffs and classifies per-sat mean / std / dominant-sign into §7.3 plan buckets (clock-like, L1/L2 systematic, noise inflation, outlier-specific). Used to identify the SPP-iono-into-clock root cause of the +9-15m sat-uniform residual systematic on Tokyo/Nagoya CLAS PPP. * feat(analysis): expand CLAS residual / SD-no-amb / correction-log diff scripts Updates the four ppp_clas analysis scripts and adds the missing test_ppp_clas_sd_noamb_compare harness so the SD-no-amb compare / ddres-to-residual / per-sat residual / correction-log diff tools can ingest the latest C++ correction-log and residual-CSV schemas. * feat(ppp): expand PPP OSR + ANTEX antenna handling and filter diagnostics - src/algorithms/ppp_osr.cpp: ANTEX parsing, signal/zenith-dependent antenna PCO/PCV interpolation, receiver-antenna-type override path. - src/algorithms/ppp.cpp + include/libgnss++/algorithms/ppp.hpp: shared trim / antenna-type normalize utilities, filter-iteration outlier-inflation diagnostic fields, AR attempt diagnostic struct. - tests/test_ppp.cpp: smoke tests for the new ANTEX / antenna-override / filter-diagnostic surfaces. * feat(ppp-ar): WLNL fixed-position guard + ratio-dump expansion + AR diag - WLNL: wlnl_wl_max_fractional_cycles knob, multi-system clock WLS, FixedPositionSolutionStats + max-shift gate around the fixed-position apply, ratio-dump CSV schema expanded with week/tow/per-pair states. - ppp_wlnl.cpp: per-pair iteration tracing aligned with the new dump. - tests/test_ppp_ar.cpp: covers the new diagnostics + fixed-position shift gate paths. * feat(clas): SPP clock overwrite knob, per-type outlier sigma, measurement diag - ppp_shared.hpp: PPPConfig fields for --clas-spp-clock-overwrite, --clas-kf-clock-seed-variance, --ppp-process-noise-clock, --process-noise-troposphere, --reset-clock-to-spp, --spp-seed-iono-free-code, --clas-ppc-profile, plus per-type CLAS outlier sigma scales / min residuals. - ppp_clas.hpp / ppp_clas.cpp / ppp_clas_epoch.cpp: thread the new clock / iono-free-seed paths, per-type outlier sigma gates with optional phase-ambiguity reset, measurement-row observation / prediction tracking, and the PPC kinematic preset wiring. * feat(gnss_ppp): wire CLAS clock / SPP-seed / PPC-profile / OSR knobs to CLI Adds CLI parsing + help for: - --clas-spp-clock-overwrite / --no-clas-spp-clock-overwrite - --clas-kf-clock-seed-variance - --ppp-process-noise-clock - --process-noise-troposphere - --reset-clock-to-spp / --no-reset-clock-to-spp - --spp-seed-iono-free-code / --no-spp-seed-iono-free-code - --clas-ppc-profile (PPC kinematic preset) - --wlnl-wl-max-fractional, --clas-wlnl-fixed-position-max-shift - --ppp-correction-log - --receiver-antenna-type override (paired with the new ANTEX path) - per-type CLAS outlier sigma scales / min residuals - AR attempt diagnostic + filter outlier inflation outputs plus tests/test_cli_tools.py coverage for the new flags. * feat(gnss_clas_ppp): pass-through CLAS clock / PPC-profile / diag flags Updates the Python wrapper to forward the new gnss_ppp CLI knobs (--clas-spp-clock-overwrite, --clas-kf-clock-seed-variance, --ppp-process-noise-clock, --process-noise-troposphere, --reset-clock-to-spp, --spp-seed-iono-free-code, --clas-ppc-profile, --wlnl-wl-max-fractional, --clas-wlnl-fixed-position-max-shift, --ppp-correction-log, --receiver-antenna-type, per-type CLAS outlier sigma / min-residual flags) so wrapper-driven runs (PPC benches, scripts/analysis paths) can exercise the new diagnostic and tuning surfaces without dropping back to the raw binary. * fix(gnss_clas_ppp): wire --no-clas-spp-clock-overwrite and friends The C++ binary in commit 4df2696 added several knobs that were not threaded through the Python wrapper, so wrapper-driven runs could not exercise them: - --enable-wlnl-par / --no-enable-wlnl-par - --clas-spp-clock-overwrite / --no-clas-spp-clock-overwrite - --clas-kf-clock-seed-variance - --ppp-process-noise-clock - --process-noise-troposphere - --reset-clock-to-spp / --no-reset-clock-to-spp - --spp-seed-iono-free-code / --no-spp-seed-iono-free-code This commit registers them on the wrapper argparse and forwards them to gnss_ppp when set, so PPC profile knob ablation and other diagnostic runs can be driven entirely through gnss_clas_ppp.py. * feat(clas): bump --clas-ppc-profile clas_code_outlier_sigma_scale default 3.0 → 10.0 Reverse-ablation across the 9 PPC-profile knobs identified clas_code_outlier_sigma_scale as the only knob whose loosening transfers across all 6 PPC datasets. A nagoya_run1 sweep at sigma in {3, 10, 30, 100, 1000} shows H_med 3.819 → 2.343m at sigma=10 (local minimum); tokyo_run3 saturates at sigma>=10 to 0.556m. The earlier 3σ rejection threshold dropped urban-canyon 'high-residual but real' code observations and damaged geometry. 6-dataset bench (300 epoch, --clas-ppc-profile + AR ratio 1.5) default 3.0 → 10.0: - tokyo_run1 H 1.432 → 1.316m (-8%, p95 -42%) - tokyo_run2 H 1.280 → 1.381m (+8% slight regress, p95 -31%) - tokyo_run3 H 0.601 → 0.556m (-7%) - nagoya_run1 H 3.819 → 2.343m (-39%, |U| -27%) - nagoya_run2 H 1.247 → 1.142m (-8%) - nagoya_run3 H 1.214 → 0.728m (-40%, p95 -48%) H_med improves on 5/6 datasets; p95_h improves on 6/6 datasets (mean -27%). The change only affects users who explicitly pass --clas-ppc-profile; the wrapper apps/gnss_clas_ppp.py --profile clas does not pass --clas-ppc-profile, so default behavior unchanged via the wrapper path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This draft PR brings the
feat/rinex4-madocabranch up to date withdevelopand publishes the current native MADOCA / CLAS PPP work for review.Major areas included:
origin/developand resolved the SPPpreprocessEpoch()semantic merge against the currentSPPCodeObservationwrapper.Notes
This is intentionally opened as a draft because the diff is large: 42 commits ahead of
develop, with the core branch history plus develop integration. The branch also preserves the remotefeat/rinex4-madocahistory via a tree-preservingoursmerge so the push stayed non-force.The production direction remains native implementation first. MADOCALIB / CLASLIB bridge pieces are kept as parity and diagnostic infrastructure, not as a required runtime dependency for the native CLAS path.
Validation
Ran locally after merging
origin/develop:Result:
-DMADOCALIB_PARITY_LINK=ON