ublox-gps-data-processing/README.md
= 6f85a6f1ed Add GCGC virtual-reference-station solution, VBS exploration suite, and report improvements
Pipeline (process_gps.py):
- New standard solution ppk_vrs: a virtual reference station synthesized at
  the capture position from the GCGC master station the CORS stage already
  fetched (zero baseline). Gated on GCGC availability; --no-vrs to disable.
  Engine validated in experiments/ (gates G1/G2a) and benchmarked equivalent
  to Trimble Pivot's commercial VRS on ground truth.
- Track output now selects among PPK-family solutions by measured dispersion,
  not nominal baseline (protects against poor network-edge VRS data).
- Warm-up exclusion: first 5 minutes (--warmup-min) removed from all quality
  metrics and figures; prominent note in both reports; data outputs unchanged.
- Professional report language: abbreviations expanded on first use, solution
  methods defined, no shorthand in tables or figure labels; fig5 reframed as
  "self-reported precision (uncalibrated) vs measured dispersion" with the
  optimism factor annotated.
- Local-base robustness: refuse to combine base files at different positions;
  RTCM logs and Data Shop RINEX accepted for the parallel GCGC method.

Streaming (stream_gps.py):
- Credentials via gitignored gcgc.env (template gcgc.env.example), env vars
  take precedence; RTCM correction stream recorded to stream_<ts>.rtcm3 for
  zero-baseline post-processing; live GGA uplink and sea dynamic model remain
  the buoy defaults; warm-up excluded from session statistics.

Experiments (new):
- vbs_synth.py: geometric virtual-base synthesis engine (RINEX 2.11 patcher,
  SP3 orbits, light-time + Earth-rotation per position, clock-robust).
- vbs_iono.py: carrier-leveled slant-ionosphere estimation + station-network
  interpolation with leave-one-out validation (median 7 cm; 1.2 cm / 10 km
  growth; DCBs as daily constants via median polish).
- vbs_compare.py: observation-domain comparison of commercial VRS files vs
  synthesized bases (ambiguity-detrended correction content).
- FINDINGS.md: gate results, commercial benchmark, offshore analysis, and
  promotion rationale.

Docs: README validation-results section explaining why the synthesized VRS
(kinematic) is the preferred solution for buoy deployments; VRS orders and
experiment data moved under gitignored experiments/data/.
2026-07-27 21:50:10 -04:00

16 KiB
Raw Blame History

u-blox GPS Module Data Procesing (u-blox ZED-X20P)

Tooling for evaluating u-blox ZED-X20P all-band GNSS modules, targeting buoy deployments. Converts raw u-center capture files (.ubx) into corrected positions and produces a full diagnostics report, and supports live streaming with real-time corrections.

Two operating modes:

Mode Script Accuracy Correction source
Post-processing process_gps.py dm-cm (PPK) NGS CORS base data, auto-fetched (free, open)
Real-time streaming stream_gps.py 1-3 cm (RTK fix) GCGC RTN NTRIP (free registration) or Galileo HAS (FW ≥ HPG 2.10)

Both consume/produce standard formats (RINEX 3, .pos, CSV, KML) via RTKLIB (demo5 fork) and widely used open-source Python libraries (pyubx2, pygnssutils, numpy, pandas, matplotlib).

Motion policy (important)

Processing never assumes the antenna was stationary. Buoys drift slowly and drift within a few metres is statistically indistinguishable from measurement noise. Everything defaults to kinematic; pass --static to process_gps.py only when the antenna is independently known to have been fixed (e.g. bench/roof test).

Quick start

bash setup.sh                                    # one-time environment bootstrap
venv/bin/python process_gps.py <capture.ubx>     # process a capture
# results in processed/<capture-stem>/  (report.html is the human summary)

Requirements

Linux with python3 (3.10+), gcc/make, git, curl, and network access for setup.sh downloads. No sudo required and everything installs into the project directory (venv/, tools/).


setup.sh

Environment bootstrap (safe to re-run; each step skips if already satisfied):

  1. Python venv (venv/): pyubx2, pygnssutils, pyserial, numpy, pandas, pyproj, matplotlib.
  2. RTKLIB demo5 v2.5.1 built from source into tools/bin/ (convbin, rnx2rtkp, pos2kml). The demo5 fork is required: stock RTKLIB 2.4.3 silently drops all BeiDou and GPS L5 / Galileo E5 observations from this receiver (~30% of the measurements). If the build fails, setup falls back to the stock apt package and creates a tools/STOCK_RTKLIB marker — process_gps.py then adds a prominent data-loss warning to every report until a demo5 build succeeds.
  3. Antenna calibration models into tools/antex/: igs20.atx (IGS, covers CORS base-station antennas) and ngs20.atx (NGS absolute calibrations, covers the rover antenna). The rover antenna is resolved automatically and written to tools/antenna.json (see Antenna configuration below).
  4. CRX2RNX (Hatanaka RINEX decompressor) — fallback for CORS stations that only publish compressed .d observation files.
  5. NGS CORS station catalog cache (used for nearest-base auto-selection).
  6. Self-check summary of every tool and the resolved antenna.

process_gps.py

venv/bin/python process_gps.py <capture.ubx> [options]

Stages (idempotent — outputs are cached, so re-running later only fills in what was previously unavailable, e.g. CORS/IGS data that had not been published yet):

  1. inventory — parses the .ubx (RXM-RAWX/SFRBX raw observations, NAV-PVT/SAT/SIG, MON-RF) and the .uc2x sidecar's session metadata. A capture without raw observations (e.g. u-center debug-mode logs) gets a diagnostics-only report and a clear notice.
  2. rinex — RINEX 3.04 obs+nav via demo5 convbin; verifies epoch count and BDS/L5 presence; extracts broadcast ionosphere parameters.
  3. single — standalone single-point solution (~1 m, works offline).
  4. ppk — differential post-processing against the nearest operational NGS CORS station (auto-selected by distance from the capture's own mean position; MSEV at 34 km for the Hattiesburg test site). Hourly base files are fetched automatically (~1 h publication lag; if not yet published the stage reports "re-run later" and everything else proceeds). Base antenna type is read from the base RINEX header; base coordinates are ITRF2020, velocity-propagated to the capture epoch.
  5. ppp (--ppp) — precise point positioning against IGS rapid/final orbit+clock products (anonymous download from BKG/ESA, ~1 day lag). Kinematic PPP by default per the motion policy; note RTKLIB's PPP-kinematic engine is weak and may yield no solution — PPK is the reference method.
  6. tracktrack.csv (UTC + decimal-degree lat/lon + per-epoch quality and 1-σ sd columns + onboard comparison columns) and track.kml.
  7. diagnosticsdiagnostics.json, report.md, report.html (self-contained, figures embedded) covering: constellation metrics (per-band C/N0, cycle slips, residuals), ionosphere (broadcast Klobuchar + measured dual-frequency slant delay/TECU), positional error metrics (RMS/R50/R95 dispersion, formal σ, inter-solution offsets), validation of every computed solution against the receiver's own onboard NAV-PVT positions (epoch-matched, PASS/FAIL), timing (epoch jitter, tAcc, leap seconds), and RF health (jamming indicator, AGC, antenna supervisor state).

Options

Flag Meaning
--interval N track.csv output interval in seconds (default 1 = native rate)
--warmup-min N receiver warm-up window excluded from ALL quality metrics and figures (default 5; track.csv keeps the full session; note shown atop both reports)
--static antenna verifiably stationary: enables static PPK/PPP solutions and static error framing. Never for buoys.
--no-ppk skip base-station download/processing
--no-vrs skip the synthesized GCGC VRS solution
--ppp additionally run PPP against IGS precise products
--base SSSS force a specific CORS station id (e.g. MSEV)
--base-obs FILE... explicit base file(s) for the parallel GCGC/local PPK method — normally unnecessary (<capture>.rtcm3 sidecars and gcgc_base/ are auto-discovered)
--base-xyz X Y Z ECEF position of the local base (else its RINEX header position is used — verify its datum)
--max-base-km KM base-station search radius (default 150)
--force-stage S delete and re-run one stage (rinex,single,ppk,ppp,track)

Antenna configuration

Receiver-antenna phase-center corrections (PCO/PCV) matter at the cm level and are applied during PPK/PPP via ANTEX calibration files.

  • Automatic: setup.sh downloads ngs20.atx/igs20.atx and searches for the project's antenna (ArduSimple calibrated quad-band, NGS calibration AS-ANT3BCAL01). The resolved 20-character ANTEX type string is written to tools/antenna.json:

    { "antex_name": "AS-ANT3BCAL     NONE", "source": "ngs20.atx" }
    
  • Different antenna: edit antex_name in tools/antenna.json to the exact ANTEX type string of your antenna (padding/spacing matters, copy it verbatim from the TYPE / SERIAL NO line of an antenna block in tools/antex/ngs20.atx or igs20.atx). Browse available calibrations at https://geodesy.noaa.gov/ANTCAL/ . If your antenna has an NGS calibration but a different model string, also update the ANT3B search pattern in setup.sh so future setups re-resolve it automatically.

  • No calibration available: set antex_name to null (or leave it unresolved) — processing continues without rover PCO/PCV and the report carries a warning. Expect a few cm of systematic error, mostly in height.

  • Base antenna needs no configuration: its type is read from the CORS RINEX header and looked up in the same ANTEX files.

  • Antenna height: assumed 0 (positions refer to the antenna reference point). For a surveyed monument, subtract your ARP offset downstream.

Other important settings and inputs

Capture requirements. Input .ubx files must contain raw observations: UBX-RXM-RAWX + UBX-RXM-SFRBX enabled (plus NAV-PVT; NAV-SAT/NAV-SIG/MON-RF enrich diagnostics). In u-center, do not record with the "debug messages" option — it floods the log with undocumented TRK/TUN/DBG/SEC messages and, in past sessions here, raw output was configured in flash but not active in RAM. The optional .uc2x sidecar (u-center 2 index) is only used for its session metadata header.

Real-time credentials (stream_gps.py). Free registration at http://rtn.usm.edu/RegisterAccount.aspx (Mississippi GCGC RTN). Provide the credentials on the machine that runs the streamer — preferred: copy the template and fill it in (the file is gitignored and loaded automatically):

cp gcgc.env.example gcgc.env    # then edit gcgc.env with the real values
venv/bin/python stream_gps.py --port /dev/ttyUSB0     # or COM7 on Windows

Alternatives: export GCGC_USER=... GCGC_PASS=... (or Windows setx GCGC_USER ...) — set variables take precedence over gcgc.env. Avoid the --ntrip-user/--ntrip-pass flags outside of quick tests: command-line arguments end up in shell history and process listings. Never commit real credentials; authentication is verified on the first NTRIP connect (a 401 appears in the log as "NTRIP connection failed").

Key streaming defaults (buoy-oriented): receiver dynamic-platform model sea (--dynmodel to override), live NMEA GGA uplink so the network-RTK virtual reference follows platform drift (--gga-fixed for bench tests), raw RXM-RAWX/SFRBX always logged so every session is post-processable, and --replay <file.ubx> offline test mode. Galileo HAS (~20 cm, no internet) activates automatically as fallback when firmware ≥ HPG 2.10 is detected.

GCGC data in post-processing (automatic parallel method). Beyond the real-time streams, a GCGC account also gives access to their Reference Data Shop (on-demand static/RINEX downloads from their 52 stations). No special invocation is needed — running process_gps.py <capture.ubx> as usual auto-discovers local GCGC data and, when found, runs a second PPK solution (ppk_gcgc) in parallel with the NGS CORS one, reported alongside all other results (it ranks first for the track output, since its baseline is shortest). Discovered sources:

  1. <capture>.rtcm3 sidecar — every stream_gps.py NTRIP session records its incoming VRS correction stream next to the raw log. A VRS is a virtual base at the receiver's own position (zero baseline) — the best possible PPK geometry.
  2. gcgc_base/ folder — drop Reference Data Shop RINEX downloads here; files whose observation window overlaps the capture are used automatically.

--base-obs FILE... explicitly feeds this method instead of auto-discovery. Note GCGC coordinates are NAD83(2011): supply --base-xyz (ECEF, ITRF) if frame consistency with the NGS-based solution matters — otherwise the base RINEX header position is used and the report warns about the ~1.5 m datum offset.

Synthesized GCGC VRS (ppk_vrs, standard). When the selected base is a GCGC network station, the pipeline additionally synthesizes a virtual base at the capture's own position (zero baseline) from that master's observations and runs PPK against it — the engine was gate-validated and benchmarked equivalent to Trimble Pivot's commercial VRS (see experiments/FINDINGS.md). Requires ESA orbit products (~1 day lag; the stage reports "pending" until then). Only provided when GCGC is available — for captures outside the GCGC network the solution is skipped automatically. Disable with --no-vrs. The track output picks among the PPK-family solutions by measured quality (empirical scatter), so a poor network-edge VRS never silently wins over a better direct solution.

Datums. PPK output is in the CORS base frame (ITRF2020, current epoch); GCGC real-time corrections are NAD83(2011) epoch 2010.0 — a constant ~1.5 m offset from ITRF/WGS84 in CONUS. Each CSV records its frame in the header; comparisons in the report account for the reference used. Onboard receiver positions are WGS84-aligned.

Data-source timing. CORS hourly files: ~1 h lag, expire after ~2 days (daily files remain, decimated to 30 s after 30 days). IGS rapid products: ~1 day lag. The pipeline caches everything it fetches under processed/<stem>/ and tells you when a re-run will find more data.

Repository layout

setup.sh                 environment bootstrap (run first)
process_gps.py           post-processing pipeline (Mode 2)
stream_gps.py            real-time streaming client (Mode 1)
run.sh                   convenience wrapper
notes/                   manual workflow notes + combine_rinex.sh helper
tools/                   built binaries, ANTEX models, caches   (generated)
venv/                    Python environment                     (generated)
processed/<capture>/     per-capture outputs: RINEX, .pos solutions,
                         track.csv, track.kml, figures/, diagnostics.json,
                         report.md, report.html                 (generated)

Validation and test results (July 2026)

Full detail, gate definitions, and reproduction commands: experiments/FINDINGS.md.

Test suite passed The virtual-reference-station tooling (experiments/vbs_synth.py, was integrated into the pipeline as ppk_vrs) passed four correctness tests: bit-identical output at zero displacement; round-trip displacement error of one quantization step (0.001); insensitivity to a physically representative 1 microsecond receiver-clock offset (millimetre agreement; an unphysical 1 ms test also documented why timestamp/pseudorange self-consistency is mandatory in synthesized files); and a ground-truth test in which a station with known coordinates, processed against a base synthesized at its own location from a station 44 km away, reproduced the direct-baseline solution statistics.

Ionosphere interpolation (leave-one-out, five stations). Carrier-leveled dual-frequency slant ionosphere interpolated across the Gulf Coast Geospatial Center (GCGC) / National Geodetic Survey (NGS) station network predicts a held-out station's ionosphere to 5-10 cm (median) inside the network, growing about 1.2 cm per 10 km — including 19 cm at a station 135 km outside the fitting cluster. Offshore implication: roughly 10-15 cm of ionospheric prediction error at 60 km beyond the coastal network.

Benchmark against the commercial product. Using ground-truth station MARY: our synthesized virtual base and Trimble Pivot's commercial virtual-reference- station product were statistically equivalent (fixed-solution horizontal RMS 7.5 cm vs 7.8 cm; both roughly halve the error and the wrong-fix count of the direct 44 km baseline). Observation-domain analysis showed the commercial product embeds only ~3-7 cm of correction content beyond pure geometry, roughly flat out to 92 km of displacement — including virtual stations the operator's portal generated 25 km and 60 km offshore. One commercial file (at the sparse network edge near Hattiesburg) carried a ~5 m bias and 3x noise; our own tooling at the same site with the same master station was clean, which is why the pipeline selects its track solution by measured dispersion rather than by nominal baseline length.

Why "PPK - synthesized virtual reference station (kinematic)" is likely the best solution for the buoy / boat use case.

  1. Zero-length baseline at the platform: the dominant baseline-dependent errors shrink to the atmospheric difference between master and platform, and the geometry term vanishes entirely.
  2. Measured performance: best dispersion of all methods on the reference capture (50% radius 0.39 m vs 0.41 m for the direct base), and the best fixed-solution accuracy in the ground-truth benchmark.
  3. Kinematic processing makes no motion assumption — correct for a drifting buoy by construction.
  4. Offshore reach: works wherever any GCGC/NGS master station exists within ~150 km; the commercial product's extrapolated corrections were shown to add only centimetres of content at those distances, so little is lost by synthesizing locally - and quality stays under our control (the network-edge failure above was a commercial-side defect our synthesis avoided).
  5. Fully automatic with open data: needs only the already-downloaded base observations plus European Space Agency orbit products (about one day of latency).

Caveats: validated on one (quiet) day so far; GPS+GLONASS until multi-GNSS orbit products are available for a given day; the measured ionospheric interpolation layer is not yet applied to the synthesized observations (planned); disturbed-ionosphere days untested.