Interfaces¶
See received-event RTK/IMU for the sequential native API and stdin executable, including input availability and timestamp contracts.
See runtime FIX recovery for optional residual containment, clean-candidate recovery and the off/on evaluation contract.
Python satellite observation analysis provides code residual, SNR and carrier-continuity diagnostics from the public bindings.
CLI¶
The primary interface is the gnss dispatcher plus native command binaries.
Examples:
gnss doctorgnss ros2-doctorgnss ros2-bag-doctorgnss field-reportgnss robotics-smokegnss sppgnss solve(--pos-uncertaintyappends per-epoch sigma columns, see below)gnss uncertainty-eval(truth-based honesty check of those sigma columns)gnss ppp(--nav,--sp3,--clk,--ionex,--dcb,--ssr,--ssr-rtcm)gnss clas-pppgnss qzss-l6-info(--compact-flush-policy <lag-tolerant-union|orbit-or-clock-only|orbit-and-clock-only>,--compact-atmos-merge-policy <stec-coeff-carry|no-carry|network-locked-stec-coeff-carry>,--compact-atmos-subtype-merge-policy <union|gridded-priority|combined-priority>,--compact-phase-bias-merge-policy <latest-union|message-reset|selected-mask-prune>,--compact-phase-bias-source-policy <arrival-order|subtype5-priority|subtype6-priority>,--compact-code-bias-composition-policy <direct-values|base-plus-network|base-only-if-present>,--compact-code-bias-bank-policy <pending-epoch|same-30s-bank|close-30s-bank|latest-preceding-bank|delayed-15s-bank>,--compact-phase-bias-composition-policy <direct-values|base-plus-network|base-only-if-present>,--compact-phase-bias-bank-policy <pending-epoch|same-30s-bank|close-30s-bank|latest-preceding-bank>,--compact-bias-row-materialization <overlap-only|selected-satellite-base-extend|all-base-satellite-extend>,--compact-row-construction-policy <independent|coupled-code-phase|row-first-value-second|network-row-driven>)gnss fetch-productsgnss ppp-products-signoffgnss ionex-infognss dcb-infognss visibilitygnss visibility-plotgnss moving-base-plotgnss moving-base-signoffgnss moving-base-preparegnss scorpion-moving-base-signoffgnss streamgnss convertgnss livegnss realtime-stress(recorded-data real-time stress harness, see Real-time stress)gnss rtcm-encodegnss station(check|start|status|stop|restart, with a station TOML)gnss rcvgnss web
For longer workflows, gnss web, gnss live-signoff,
gnss moving-base-signoff, gnss scorpion-moving-base-signoff,
gnss ppc-rtk-signoff, gnss ppc-coverage-matrix, and
gnss ppp-products-signoff
also accept --config-toml <file>. Templates are indexed in
configs/README.md and grouped by purpose.
For CLAS-oriented PPP experiments, gnss ppp also exposes the boundary and
selector knobs used by the experiment lane:
--clas-epoch-policy <strict-osr|hybrid-standard-ppp>--clas-osr-application <full-osr|orbit-clock-bias|orbit-clock-only>--clas-phase-continuity <full-repair|sis-continuity-only|repair-only|raw-phase-bias|no-phase-bias>--clas-phase-bias-values <full|phase-bias-only|compensation-only>--clas-phase-bias-reference-time <phase-bias-reference|clock-reference|observation-epoch>--clas-ssr-timing <lag-tolerant|clock-bound-phase-bias|clock-bound-atmos-and-phase-bias>--clas-expanded-values <full-composed|residual-only|polynomial-only>--clas-subtype12-values <full|planar|offset-only>--clas-residual-sampling <indexed-or-mean|indexed-only|mean-only>--clas-atmos-selection <grid-first|grid-guarded|balanced|freshness-first>--clas-atmos-stale-after-seconds <seconds>
Multi-constellation RTCM epoch assembly (--no-rtcm-epoch-assembly)¶
A receiver sends one MSM message per constellation with the same time tag and
sets the RTCM "multiple message" bit (legacy 1003/1004: synchronous GNSS flag)
on every message of the epoch except the last. gnss live (rover and base RTCM),
gnss replay (base RTCM) and gnss convert (RTCM to RINEX) merge those parts
into one epoch with one shared implementation
(io::ObservationEpochAssembler, fed by RTCMStreamDecoder::Event::
observation_more_follows or RTCMProcessor::lastObservationMoreFollows()):
- a part whose time differs from the pending epoch by more than 1 ms emits the pending epoch first (a lost last message cannot merge two epochs);
- otherwise it is merged; a (satellite, signal) already present is kept once;
- bit 0 emits the epoch immediately, so no latency is added;
- the pending epoch is flushed at end of stream.
A stream whose messages all carry bit 0 (every GPS-only stream) is unchanged.
It is on by default; --no-rtcm-epoch-assembly (in all three tools) restores
one epoch per observation message. gnss rtcm-encode sets the bit on every
observation message of an epoch except the last one emitted
(RTCMProcessor::encodeObservations(..., more_follows) defaults to false).
gnss stream only prints one diagnostic line per message and does not build
epochs, so it is not affected.
Parts can only be merged if their full GPST agrees, so RTCMStreamDecoder
keeps one GPST reference (setReferenceTime, referenceTime):
- non-GLONASS MSM messages carry only a time of week; the decoder gives such a part (week 0, or BeiDou's week-1 wrap) the week nearest to the reference, and the reference follows every resolved observation time (so a week rollover is followed);
- while a reference exists it is passed to the processor before every message, so GLONASS MSM epochs and 1020 ephemerides are dated from the stream and not from the host clock;
- the reference is seeded explicitly, otherwise from the toe of the first decoded GPS/Galileo/BeiDou/QZSS ephemeris. A time derived from the host clock (GLONASS MSM or 1020 decoded without a reference) never seeds or advances it;
- without a reference TOW-only parts stay week 0 and GLONASS uses the host clock, as before.
gnss live seeds both RTCM decoders from --nav-rinex (latest GPS toe); when
one decoder obtains a reference from stream ephemerides and the other has none
(a rover stream normally carries none) the other is seeded with it, and a
week-0 epoch from a UBX rover is given the week nearest to the newest decoder
reference. Without --nav-rinex and without any ephemeris in either stream the
head of the stream is not resolved (week 0, host-clock GLONASS).
gnss convert keeps its own --gps-week resolution. gnss replay --base-rtcm
decodes the base with the same RTCMStreamDecoder (and the same assembler): it
is seeded from --nav-rinex (latest GPS toe) when given, otherwise from the
first in-stream GPS/Galileo/BeiDou/QZSS ephemeris, so the base epochs get the
same GPST week as a rover RINEX and the result does not depend on the host
clock. Base epochs decoded before an in-stream seed existed (no --nav-rinex,
no early ephemeris) are resolved against that first reference after the pass.
Only a stream with neither a --nav-rinex nor any such ephemeris stays in
week 0 and cannot be aligned to a rover RINEX.
RTCM ephemeris messages (gnss rtcm-encode --eph-systems)¶
RTCMProcessor decodes and encodes broadcast ephemeris messages for five
constellations:
| message | constellation | notes |
|---|---|---|
| 1019 | GPS | |
| 1020 | GLONASS | time tag resolved against the stream reference, not the host clock |
| 1042 | BeiDou | layout and scaling as RTKLIB 2.4.3 b34 decode_type1042 |
| 1044 | QZSS | as decode_type1044 |
| 1045 / 1046 | Galileo F/NAV / I/NAV | as decode_type1045/1046 |
The decoded Ephemeris is the record the RINEX navigation reader produces for
the same broadcast:
- BeiDou:
toeandtocare GPST (BDT week + 1356, + 14 s);weekis the BDT week andtoesthe raw BDT toe seconds, which the satellite-position code uses.tgdis TGD1 (B1/B3),tgd_secondaryis TGD2 (B2/B3),iodeis AODE andiodcis AODC. GEO satellites (PRN 1 to 5 and 59 to 63) need nothing extra: the position code selects the GEO orbit model from the PRN. - QZSS: the library stores PRNs as printed in RINEX (J01 is 1), and the 4-bit satellite field of 1044 is that number. The 10-bit week is resolved like 1019: against the stream reference when one exists.
- Galileo: 1045 sets
data_source_codeto the F/NAV word (258) and 1046 to the I/NAV word (513), as before. - In all three,
tocis placed in the week nearesttoe.
isEphemerisMessage() is true for all six types, so RTCMStreamDecoder
(gnss live, gnss replay --base-rtcm), gnss convert --nav-out and
gnss stream --decode-navigation decode them. Before this, 1045/1046 were decoded
only by mergeRTCMEphemerides (used by gnss ppp --ssr-rtcm), which is
unchanged and still merges GPS 1019 and Galileo 1045/1046 only. A decoded
GPS, Galileo, BeiDou or QZSS ephemeris can seed the stream reference (see
above).
RTCMProcessor::encodeEphemeris writes the same six messages. The Galileo
message follows the record's navigation_message_type, else its RINEX
data-source word; a record whose type cannot be determined is not encoded. A
value that does not fit its field (for example a BeiDou af0 beyond 24 bits)
gives an invalid message, as for 1019. Not carried: the L2 code and fit
interval bits of 1044, and the Galileo reserved bits.
gnss rtcm-encode --eph-systems <G,R,E,J,C> selects the constellations that get
ephemeris messages: G 1019, R 1020, E 1045/1046, J 1044, C 1042. The default
is G,R, which gives the same bytes as before the option existed. Only
systems that are also in --systems are broadcast, because a consumer infers
the week from the first ephemeris it sees. Round-robin (--eph-interval) and
the initial burst (--no-eph-burst) cover the added systems. gnss
realtime-stress prepare --eph-systems passes the list to the base encode
only; without it the command line is unchanged.
Verification on the navigation file of the UrbanNav Tokyo runs (DLR BCEmerge base.nav): every Galileo (4645),
BeiDou (696) and QZSS (95, J01, J02, J03, J07) record was encoded, decoded and compared with the library's own
satellite position and clock function at nine times from toe - 2 h to toe + 2 h. All 5436 were encoded, and the worst
difference was 0.053 mm in position and 5e-16 s in clock. The same records, decoded by RTKLIB 2.4.3 b34 rtcm3.c and
re-read from its RINEX output (0.63 mm and 5e-15 s at worst), and encoded by RTKLIB gen_rtcm3 and decoded by the
library (the same 0.053 mm), agreed within the tolerance of 0.01 m and 1e-11 s. 693 of the 696 BeiDou 1042 frames are
bit-identical to RTKLIB's. RTKLIB differs from the encoder in the Galileo SISA index (it truncates where the encoder
takes the nearest index), and in the 1044 L2 code and fit interval bits. No 1044 stream from a real receiver or caster
was available: the real QZSS records are covered by those round trips and by the bit-level comparison with RTKLIB, not
by a live broadcast.
Ephemerides in the stream are not a drop-in for --nav-rinex when the file has an IONOSPHERIC CORR header (the PPC
navigation files do): gnss live --nav-rinex reads those parameters and applies a Klobuchar model in its SPP seed, and a
stream carries none, so without a navigation RINEX nav.ionosphere_model.valid stays false and no model is applied.
With a file that has no such header (the UrbanNav BCEmerge file) the two give the same status on 99.4 to 100 % of the
epochs and the same position to the print precision at p50
(measured).
Broadcast ephemerides from a u-blox rover (--ubx-nav, --nav-rinex-out)¶
A u-blox receiver logs the navigation subframes of the satellites it tracks
(UBX-RXM-SFRBX). UBXNavDecoder (ubx_nav_decode.hpp) turns them into
Ephemeris records and the GPS Klobuchar set, so a u-blox-only stream no
longer needs a navigation RINEX or an RTCM base that sends ephemerides.
| System | Signal (sigId) |
Message | Record complete on | Checks |
|---|---|---|---|---|
| GPS, QZSS | L1 C/A (0) | LNAV subframes 1 to 3 | subframe 3 | IODE of subframes 2 and 3 equals the low byte of IODC |
| GPS | L1 C/A (0) | subframe 4, page 18 (SV id 56) | Klobuchar alpha/beta, no ephemeris | none |
| Galileo | E1-B (1; 0 before protocol 27) | I/NAV word types 1 to 5 | word type 5 | CRC-24Q of every page pair, one IODnav in word types 1 to 4, word type 4 names the satellite |
| BeiDou MEO/IGSO | B1I, B2I, B3I D1 (0, 2, 4) | D1 subframes 1 to 3 | subframe 3 | consecutive subframe times, toc == toe |
| BeiDou GEO | B1I, B2I, B3I D2 (1, 3, 10; or sigId 0 on PRN 1 to 5, 59 to 63) |
D2 subframe 1, pages 1 to 10 | page 10 | consecutive page times, toc == toe |
| GLONASS | L1OF, L2OF (0, 2) | strings 1 to 4 | string 4 | Hamming code of every string, string 4 names the slot |
Not decoded: GPS and QZSS CNAV (L2C, L5), QZSS L1S, Galileo F/NAV (E5a) and E5b-I I/NAV, BeiDou B-CNAV (B1C, B2a), NavIC, SBAS. They are counted as skipped.
The layouts, scale factors, completion triggers and consistency checks follow
RTKLIB 2.4.3 b34 (rcv/ublox.c, rcvraw.c). GPS and BeiDou words are not
parity-checked, as in RTKLIB: the receiver forwards only subframes that passed
its own parity check. Signals of one message layout share a buffer, because a
receiver forwards only some of the strings or subframes of each signal (BeiDou
D1 on B1I and B2I, GLONASS L1OF and L2OF). Records are emitted in the form the
RINEX navigation reader produces for the same broadcast; Galileo records carry
the data-source word of RTCM 1046 (513: I/NAV E1-B, clock referenced to E5b/E1).
Time reference. The 10-bit GPS and QZSS week and the GLONASS day need the
receiver time, which is the newest UBX-RXM-RAWX epoch of the same stream.
Without one those records are dropped (no_time_reference); Galileo and BeiDou
carry full weeks. The GPS week is the one nearest to the reference, not the
latest one before the host clock as in RTKLIB, so old logs decode correctly.
gnss live --ubx-nav (default off; without it the .pos, the options and
the output are byte-identical to earlier releases). Decoded ephemerides are
merged with addEphemerisIfNew, like RTCM ephemerides: the first copy of a
record at once (unless it is a new issue of a satellite that already holds a valid record),
taken back out if a later copy of the same issue differs
(accept, then confirm; below). The first decoded GPS
Klobuchar set is used when no ionosphere model is valid yet; a --nav-rinex
that has an IONOSPHERIC CORR header wins. With the flag the rover's first
RAWX epoch is read before the base and becomes the GPST reference of the base
decoder when it has none: a base stream without ephemerides would otherwise be
dated from the host clock and never align with the rover. --ubx-nav skips a
record whose health marks the satellite unusable and counts it: GPS and QZSS
LNAV svh other than 0, Galileo E1-B HS or DVS other than 0, BeiDou SatH1
other than 0, GLONASS Bn MSB set or string-3 ln set (Ephemeris::glonass_ln,
filled by this decoder only). The summary: line gains ubx_nav_ephemerides=
(records added to the database, a record that was retracted and merged again
counts twice), ubx_nav_klobuchar= (0/1), ubx_nav_unhealthy_skipped=,
ubx_nav_unconfirmed=, ubx_nav_conflicts=, ubx_nav_retracted= and
ubx_nav_held_new_issue= (defined below). --ubx-nav needs --rover-ubx.
Accept, then confirm (gnss live --ubx-nav). Waiting for a second
transmission before using an ephemeris (confirm-twice, below) delays every record
by one repeat of its frame set: at a cold start the filter had ephemerides for
only some of the satellites until the repeats arrived, and on one development log
it fixed one L1 cycle off and held that fix. --ubx-nav therefore merges a record at once
when the satellite has nothing valid yet, and uses the second copy to catch a corrupted
one (UBXNavAcceptConfirm, ubx_nav_decode.hpp). Per satellite, on every complete
frame set the decoder produces (also the repeats it does not emit):
- A copy of an issue not seen for that satellite (the first record, or a new IODE/toe) is merged at once, after the health rule, unless rule A applies. The previous record stays. "Issue" is what the decoder itself uses to tell repeats: GPS/QZSS IODE, IODC, toe and toc; Galileo IODnav, toe and toc; BeiDou toe; GLONASS IODE.
- Rule A (a new issue while the satellite is covered). If the satellite
already holds a record that
--ubx-navmerged, that is still in the navigation data and is valid at the copy's reception time (Ephemeris::isValidat the receiver GPST of the message that completed the copy), the copy is held: it is merged only once it is confirmed (rule 2's test; the health rule is applied when it is merged). A copy that is never repeated is never merged. A copy corrupted in an issue field (a wrong toe, IODE or GLONASStb) looks like a new issue, and no second copy ever differs from it, so rule 1 alone would merge it and keep it; rule A keeps it out whenever the satellite has a valid record. A record that the navigation data holds from--nav-rinexor RTCM does not count as covering, and neither does a copy with no reception time yet (before the first RAWX epoch). Without a covering record rule 1 applies unchanged, which is what keeps the start-up latency of confirm-twice away. - A later copy of that issue that is equal in every broadcast field (as in confirm-twice) and assembled entirely from SFRBX messages received after the first copy's last message confirms it (a held record is merged now). An equal copy that reuses a message of the first one is the same transmission and changes nothing.
- Rule B (a confirmation cleans up). When a record of a satellite is confirmed,
every record of that satellite that
--ubx-navmerged, that is still unconfirmed and that has a different issue is removed from the navigation data and counted as retracted. A confirmed record is never removed by this rule, and a copy that was only held is not touched. This is what takes out a corrupted first copy merged at first acquisition, when the satellite had nothing valid yet, as soon as another issue of that satellite is confirmed. The rule runs on every confirmation, also one the health rule then refuses to merge and one made in fallback (rule 3). - A later copy that differs is a conflict. The record that was merged, if any, is
removed from the navigation data (
NavigationData::removeEphemeris, which erases only a record equal in every field, so a record that came from--nav-rinexor RTCM is never touched), and that satellite falls back to confirm-twice: nothing is merged for it until two equal copies from different transmissions exist. The first copy stays a pending candidate next to the differing one, because either may be the corrupted one, so the next copy equal to the first confirms it (at most four candidates are kept). After a confirmation the satellite is back at rule 1 for its next record. A held copy that meets a differing copy of its issue is a conflict too, with nothing to remove. - A copy that differs from a record that was already confirmed and is in the navigation data is ignored: two agreeing transmissions outweigh one differing copy, so a copy cannot retract a confirmed record. If the confirmed record never got into the navigation data (the health rule refused it), a differing copy is a record the navigation data does not hold and follows rule 1 (rule A does not apply: the issue is not new); BeiDou satellites clear their health flag within one toe this way.
Counters on the summary: line: ubx_nav_conflicts= is the number of copies that
differed from an unconfirmed copy (a conflict at rule 3, and every later copy that
matched no pending candidate of a satellite in fallback); ubx_nav_retracted=
the records actually removed from the navigation data, by rule 3 and by rule B;
ubx_nav_held_new_issue= the copies of a new issue that rule A held (counted once,
when first held, whether or not a confirmation follows);
ubx_nav_unconfirmed= the records merged and never confirmed (still unconfirmed at
the end, or forgotten unconfirmed), plus the candidates that were never confirmed: copies
still held by rule A at the end, or forgotten, and, for satellites in fallback, the ones replaced
or left pending (a retracted record is counted by ubx_nav_retracted= and not again here).
A single transmission of a
record, such as a satellite that sets after one frame set, therefore shows up as
ubx_nav_unconfirmed=, not as a conflict.
The price of merging at once is the exposure that remains: a copy corrupted in a way
the decoder's checks do not catch is in the navigation data from its first copy until it
is retracted, when the satellite had nothing valid yet: at first acquisition, or after
its last record expired. It is retracted by a differing second copy of its issue
(one repeat of the frame set, 30 s at the median) or, if it is corrupted in an issue
field and so never repeated, by the next confirmation of any other issue of the
satellite (rule B). A corrupted record that its satellite never repeats and that is
followed by no confirmed issue stays selectable for its validity window; it is the one thing
confirm-twice removes that this does not, and gnss convert --nav-rinex-out
therefore keeps confirm-twice. While the satellite holds a valid record, a corrupted
new issue is not merged at all (rule A). The cost of rule A is that a genuine new issue waits for
its second copy while the satellite holds a valid record (one repeat of the frame set, 30 s at the
median on both held-out logs below, 210 s at most); the older record serves meanwhile.
Confirm-twice (gnss convert --nav-rinex-out). The converter writes a record only
when the decoder has produced the same record twice from different transmissions
(UBXNavConfirmTwice); a file has no start-up latency to protect. (--ubx-nav
uses it only for a satellite in fallback, above.) "The same" is every broadcast field of the Ephemeris,
bit for bit; only tof, which is resolved against the receiver clock, is left
out. "Different transmissions" means the second copy is assembled entirely from
SFRBX messages received after the last message of the first, so a copy that
reuses a stored subframe of the first one does not count. A copy that differs
from the pending one replaces it (counted in nav_rinex: as conflicts=), a copy
equal to an already accepted record is a duplicate, and unconfirmed= counts the
pending ones that were never confirmed, replaced or left at the end. A record that was corrupted in one
transmission and passed the checks above (the GLONASS record of the round-2
measurement below) is therefore never accepted, at the price of one more transmission before
every new ephemeris is written: on the 2.67 h round-2 log below 30 s at the
median, 60 to 111 s at the 95th percentile and 240 s at most per system; on the two
held-out logs 30 s, 30 to 150 s and 630 s. The
decoder (UBXNavDecoder, also behind gnss ubx-info --decode-nav) still reports
every record, so the counts of decoded records and the comparison with RTKLIB
under "Measured" are decoder output, before this step; the rows for confirmed
records are after it.
gnss convert --format ubx --nav-rinex-out <file> writes the decoded
ephemerides as RINEX 3.04 navigation, with the last decoded Klobuchar set as
GPSA/GPSB header lines, when the input ends, and prints a nav_rinex: line
(count per system, klobuchar=0|1, unconfirmed=, conflicts=). It writes every record the receiver sent,
unhealthy ones included, with their health word: a converter keeps what it was
given, and only once confirmed twice (unchanged by accept-then-confirm). It is separate from --nav-out, which keeps its own converter-side
decoders and writes no Klobuchar lines. gnss ubx-info --decode-nav prints a
nav_decode: line with the same counts (also unfiltered).
Limits. A receiver forwards the navigation data only of satellites it decodes,
and often only part of the subframes, so a satellite that is tracked but not
decoded gets no ephemeris and a new one takes between a few seconds and a few
minutes (see below). A satellite that broadcasts a default or unhealthy data set
(GPS and QZSS health 63 with URA index 15, BeiDou health 1) gives a record like any
other, which IGS BRDC files leave out; the decoder emits it, --ubx-nav skips it
and --nav-rinex-out writes it. getEphemeris itself does not look at health, so
a record loaded from a RINEX file or an RTCM stream is still selected on toe alone.
Each record is as old as its subframes: decoding does not check the ephemeris
against the time. GLONASS strings are protected by a Hamming code only, which is
accepted with the single-error allowance of RTKLIB and cannot reject every
multi-bit error: one record in the round-2 measurement below (R16, tb 12 where the other
strings of the frame say 15) is such a corruption, decoded identically by RTKLIB;
confirm-twice (--nav-rinex-out, and a satellite in fallback) does not accept it,
because the satellite never repeats it. --ubx-nav merges a first copy at once when
the satellite holds nothing valid, so a record like that one is merged and, with no
second copy to differ from it, stays (counted in ubx_nav_unconfirmed=) until
another issue of that satellite is confirmed, which removes it (rule B,
ubx_nav_retracted=); with a valid record held it would not have been merged at all (rule A).
Measured. History: five rounds, each against a written plan with its thresholds
fixed in advance. Round 1 failed several gates for causes outside the decoder
arithmetic (forwarded default data sets selected among real records, a completeness
definition, and an RTKLIB that cannot decode u-blox Galileo E1-B). Round 2 added the
health rule above, redefined those gates and was scored on logs not used before
(develop 0f4153dc). Round 3 made --ubx-nav wait for a second transmission before
it uses a record (confirm-twice) and was stopped before scoring: on the development
log (3DOM rover A, below) the delay of one repeat at the cold start made the filter
fix one L1 cycle off and hold that fix, 283 FIXED epochs more than 0.5 m from the
reference against 0 without the delay. Round 4 (accept, then confirm) was scored on
Laura Bassi 2025-10-16 and 3DOM rover B 2025-06-05 and was a No-Go on one
availability gate, GLONASS on the Laura Bassi log, 10 of 12 satellites. The
evaluation took a satellite's health from the BRDC, which holds Bn only, while the
receiver broadcast the string-3 flag ln = 1 for two satellites (R10, R20), which
--ubx-nav skips as intended: a definition error in the evaluation, not a decoder
fault. Every other gate of round 4 passed, its end-to-end run included. Round 5
added rules A and B (round 4 did not cover a copy corrupted in an issue field, found
on a development log) and fixed the availability definition (a satellite counts when
RTKLIB, run on the same log up to that time, holds a healthy valid record for it;
below). It was scored on the two held-out logs that follow, with every rule, script
and threshold frozen before those logs were decoded, solved or compared with RTKLIB;
they were acquired by framing and hashing only. The logs of rounds 1 to 4 are
development data from round 5 on.
Scored round: two held-out logs. C7 is Laura Bassi, 2025-10-17, first 3 h (L1/L5 receiver, 2 Hz, 21 598 RAWX and 193 747 SFRBX messages), BRDC 2025/288 to 291. C8 is 3DOM rover B, 2025-06-06, first 4 h (u-blox F9P, 1 Hz, 14 400 RAWX and 399 652 SFRBX messages), with the same-day base file of station MOCA, BRDC 2025/156 to 158. The BRDC reference is the union of the daily IGS BKG files from the day before to the day after the span. The C8 base cut starts 28 s before the first rover epoch, not the planned 60 s, because the same-day base file begins there. Galileo E5a, BeiDou B2a and GPS L5 CNAV messages are counted and not decoded. A record is compared with the BRDC record of the same satellite, issue (IODE, IODnav, AODE or tb) and toe by the library's own satellite position and clock at nine times from toe - 2 h to toe + 2 h (GLONASS seven times, tb +- 15 min). Fixed in advance: at least 99 % of the confirmed healthy records matched a BRDC record and every matched record passed the comparison, at least 95 % of the counted satellites available per system (a system with fewer than 5 counted satellites is only reported), at most 1 % of the union different from RTKLIB. QZSS: no QZSS ephemeris was decoded in either log, so it was not evaluated.
C7, Laura Bassi 2025-10-17, 3 h:
| GPS | GLONASS | Galileo | BeiDou | |
|---|---|---|---|---|
Healthy records among those confirmed by a second transmission (the records --nav-rinex-out writes) that match a BRDC record of the same issue and toe and pass the comparison |
32 of 32 | 63 of 63 | 107 of 107 | 50 of 50 |
Worst difference of the records --ubx-nav merged (all matched a BRDC record) |
0.26 mm, 4.8e-16 s | 0.074 mm, 5.0e-16 s | 0.033 mm, 4.9e-16 s | 0.23 mm, 5.0e-16 s |
Counted satellites with a healthy valid record in the --ubx-nav database 15 min after their first epoch |
10 of 10 | 12 of 12 | 10 of 10 | 16 of 16 |
| Time from a satellite's first RAWX epoch to its first healthy merged record (satellites that got one), median / 95th percentile | 40.0 s / 1988.7 s | 41.8 s / 544.8 s | 49.0 s / 1350.2 s | 24.0 s / 1950.2 s |
Records of the decoder output (all health) that RTKLIB 2.4.3 b34 convbin also gives, with the two changes below |
35 of 35 | 65 of 65 | 107 of 107 | 60 of 60 |
C8, 3DOM rover B 2025-06-06, 4 h:
| GPS | GLONASS | Galileo | BeiDou | |
|---|---|---|---|---|
| Healthy records among those confirmed by a second transmission that match a BRDC record of the same issue and toe and pass the comparison | 29 of 29 | 62 of 62 | 139 of 139 | 48 of 48 |
Worst difference of the records --ubx-nav merged (all matched a BRDC record) |
0.25 mm, 4.7e-16 s | 0.069 mm, 4.8e-16 s | 0.034 mm, 4.5e-16 s | 0.39 mm, 4.8e-16 s |
Counted satellites with a healthy valid record in the --ubx-nav database 15 min after their first epoch |
14 of 14 | 14 of 14 | 12 of 12 | 14 of 14 |
| Time from a satellite's first RAWX epoch to its first healthy merged record (satellites that got one), median / 95th percentile | 20.0 s / 1113.2 s | 28.0 s / 1416.9 s | 29.0 s / 45.8 s | 34.0 s / 523.2 s |
| Records of the decoder output (all health) that RTKLIB also gives | 38 of 38 | 67 of 67 | 147 of 147 | 48 of 48 |
Every gate passed on both logs. The difference to RTKLIB is 0.00 % of the union in
every system, and every common record passes the comparison. The last GPS Klobuchar
set of each log (112 sets decoded on C7, 167 on C8) equals the RTKLIB value in every
printed digit; it differs from the header of the daily BRDC file, which is not
scored. Not scored: 9 BeiDou records on C7 and 1 GPS record on C8 are unhealthy and
were skipped by --ubx-nav. An independent extraction of Bn and ln from the raw
GLONASS strings agrees with the decoder on every GLONASS record of both logs, and
none had Bn MSB = 1 or ln = 1, so the GLONASS rows do not test the GLONASS health
rule.
A satellite is counted in the availability row when it had RAWX observations for at
least 30 min, RTKLIB (convbin run on the same log up to 15 min after the
satellite's first epoch) holds a record for it that is valid at that time and
healthy, and the receiver forwarded the frames of one record at least twice by then.
Not counted on C7: G03 and C42 (RTKLIB holds no record valid at that time), C49 and
C50 (SatH1 = 1 in the received broadcast) and E36 (frame count); none on C8. The
count with the record and its health taken from the BRDC (GLONASS ln from the raw
string 3), reported beside it, also gives 100 % in every system on both logs. A copy
of a new issue that rule A held was merged a median of 30 s after it was first held
on both logs (95th percentile 30 to 78 s per system, 210 s at most).
End to end on C8 (held out). N is gnss live --nav-rinex with the three BRDC days
merged (76 723 records), U is gnss live --ubx-nav with no navigation file. Both
read the rover log with --rover-ubx and use --preset low-cost. The base is the
same-day MOCA observation file (about 3.6 km from the rover, base position from the
RINEX header), encoded to RTCM MSM7 for GPS, GLONASS and Galileo with gnss
rtcm-encode and no ephemeris messages, so BeiDou ephemerides were decoded and not
used in the solution. The reference is the median ECEF position of the 14 396 FIXED
epochs of N, 4349567.9459 857394.7004 4570736.8047 m, taken from the N output alone
before the U output was read.
N (--nav-rinex) |
U (--ubx-nav) |
|
|---|---|---|
| Solutions | 14 400 | 14 379 (99.85 % of N) |
| FIXED epochs | 14 396 | 14 375 (99.85 % of N) |
| Same status as N | 99.83 % of N's epochs | |
| Both FIXED (14 375 epochs): horizontal difference, p50 / p95 / max | 0.0000 / 0.0006 / 0.0097 m | |
| FIXED epochs more than 0.5 m (3D) from the reference | 0 | 0 |
U lacks the first 21 epochs of N (its first solution is 21 s after the first rover
epoch, when enough ephemerides were complete) and no later one. With confirm-twice
instead (the rule of --nav-rinex-out, report-only arm) the first 50 epochs would
be missing: 14 350 solutions and 14 338 FIXED epochs, also with no FIXED epoch more
than 0.5 m from the reference; on this log confirm-twice caused no wrong FIX, on
rover A it did (below). Without the health rule the counts are the same here (14 379
solutions, 14 375 FIXED epochs, none more than 0.5 m): the log holds one unhealthy
GPS record, so it hardly tests that rule. The summary: line of U reads ubx_nav_ephemerides=283,
ubx_nav_klobuchar=1, ubx_nav_unhealthy_skipped=1, ubx_nav_unconfirmed=16,
ubx_nav_conflicts=0, ubx_nav_retracted=5 and ubx_nav_held_new_issue=236. All
283 merged records matched a BRDC record. No copy differed from an earlier copy of
its issue on C8, and none did in the offline replay of the merge on C7, so no
corrupted record was met on either held-out log and the retraction of one is covered
by unit tests only. The 5 retracted records are Galileo issue 107 of E04, E06, E13,
E15 and E27: correct records (within 2.2e-5 m of the BRDC record), merged at
04:00:57 GPST, 29 s after the first rover epoch, and taken out by rule B 60 s later,
when issue 108 of the same satellite was confirmed. The 16 unconfirmed are held
copies that matched a BRDC record and got no second copy before the log ended (13 of
them within its last 9 s).
Round-2 measurement (Laura Bassi, 2025-10-15, L1/L5 receiver, 2.67 h, 2 Hz, IGS BKG BRDC 2025/288; Galileo E5a, BeiDou B2a and GPS L5 CNAV messages are counted and not decoded). This log was held out in round 2 and has been development data since round 3. Records are compared with the BRDC as for the scored logs above.
| GPS | GLONASS | Galileo | BeiDou | QZSS | |
|---|---|---|---|---|---|
| Records decoded (of which unhealthy) | 37 (0) | 69 (0) | 92 (0) | 44 (5) | 0 |
| Healthy records with a BRDC record of the same issue and toe | 37 of 37 | 68 of 69 | 89 of 92 | 30 of 39 | - |
| The same, BRDC 2025/287 and 288 together | 37 of 37 | 68 of 69 | 92 of 92 | 39 of 39 | - |
| Worst difference of the matched records | 0.23 mm, 5e-16 s | 0.07 mm, 5e-16 s | 0.22 mm, 1e-15 s | 0.27 mm, 5e-16 s | - |
Same records as RTKLIB 2.4.3 b34 convbin (Galileo: with the patch below) |
37 of 37 | 68 of 68 | 92 of 92 | 44 of 44 | - |
The 13 healthy records without a BRDC match are 12 sets whose toe is on
2025-10-14 (the log starts at 23:58:27 UTC, and the daily BRDC file of the 15th
does not carry them; the file of the 14th does) and one GLONASS record (R16)
whose tb field was corrupted in transit in a way the Hamming code does not
reject: the other strings of that frame carry tb 15, and the record is up to 9
470 km off the neighbouring records within 15 min of its toe. RTKLIB decodes the
same record. The GPS Klobuchar sets (17 distinct ones) are identical to the
RTKLIB values in every printed digit, and the last one equals the BRDC header at
its print precision. The five unhealthy records are BeiDou satellites with
health 1; BRDC carries four of them.
Time from the first RAWX epoch of a satellite to its first ephemeris (median / 95th percentile): GPS 32.5 s / 1952 s, GLONASS 36.5 s / 2037 s, Galileo 41.5 s / 241 s, BeiDou 16.5 s / 1500 s. With the availability definition of that round (replaced in round 5), for a satellite tracked for at least 30 min with at least one LNAV/I-NAV/D1/strings message in its first 15 min, a healthy ephemeris valid at 15 min had been decoded for GPS 11 of 12 (92 %), GLONASS 14 of 14, Galileo 11 of 11 and BeiDou 11 of 13; the misses are a rising GPS satellite for which the receiver forwarded 3 messages in 15 min, a BeiDou satellite with 4, and BeiDou C49, whose health is 1 throughout. One tracked BeiDou satellite had no message in its first 15 min and is not counted.
RTKLIB b34 decodes none of the Galileo records: it takes an E1-B message of 8 words
(every protocol 27 and later receiver) for E5b-I and drops it, and wants a word type
6 in the buffer. With one line changed (E5b-I told by sigId) it returns exactly
the records of this decoder. convbin also scans the file once before converting
and keeps the subframe buffers, so its first records can be completed with subframes
from the end of the file; every comparison with RTKLIB in this section uses a
conversion pass with those buffers cleared. A differential run of the decoder
against RTKLIB's input_raw on 12 000 random but consistent records (QZSS and
BeiDou D2 included, 2000 per layout) gave identical records to 1e-12, and the
single-pass decoders agree on every record of the round-2 log to 1e-12 relative.
Development log, end to end (3DOM rover A, 2025-06-04, 5.25 h, 1 Hz, base 3.6 km
away, same setup, BRDC 2025/155, 18 895 rover epochs). The rules were developed on
this log, so it is not independent evidence. With the final rules --ubx-nav gave
18 877 against 18 895 solutions, the same status on 99.88 % of the epochs, 18 873
against 18 891 FIXED epochs, a horizontal difference between the two FIXED positions
of 0.0001 m at p50, 0.0010 m at p95 and 0.0126 m at most, and no FIXED epoch more
than 0.5 m from the median FIXED position of the --nav-rinex run. The 15 unhealthy
records of that log (GPS and QZSS health 63, BeiDou health 1) were skipped. Without
the health rule the same run had 18 700 FIXED epochs and 975 FIXED epochs more than
0.5 m from that position (up to 1.2 m from the --nav-rinex position of the same
epoch), because a default data set of G23 and G27 with the same toe as the real one
was selected. With confirm-twice instead of accept-then-confirm (round 3) it had 16
970 FIXED epochs and 283 more than 0.5 m. A rover with a clear sky and a base 3.6 km
away is the easy case, and both end-to-end logs are static rovers of the same data
set: nothing here says how --ubx-nav behaves in a canyon, with a poor antenna or
on a moving rover.
Causal output guard for gnss live (--kinematic-post-filter)¶
Opt-in (default off; without it gnss live output is byte-identical to
earlier releases) and adds no output delay. Once a FIX has been written, an
epoch whose height differs from the last written epoch by more than
max(1 m, 4 m/s * dt) (dt <= 0 counts as 1 s) is not written, and a dropped
epoch does not move the anchor. This is the height-step rule of gnss solve's
kinematic post-filter; both tools share one implementation
(rtk_validation::KinematicPostFilter). With the flag the summary: line
gains post_filter_dropped_epochs=; dropped epochs are missing rows in the
.pos. Measured effect and its limits: Real-time stress
harness.
Stall watchdog for gnss live (--stall-timeout-ms)¶
Opt-in (default 0 = off: reads block as before and the output, including the
summary: line, is unchanged). A tcp:// source that stays connected but sends
nothing blocks read() forever; --reconnect only reacts to read errors and
EOF, so a dead peer that never sends a FIN hangs gnss live.
With --stall-timeout-ms <ms> (> 0) gnss live polls a tcp:// or
serial:///device source for readability before each read(). If no byte
arrives for <ms> the source is stalled:
- with
--reconnect(tcp://only): the socket is closed and reopened, following--reconnect-delay-msand--reconnect-attempts; a stall counts as a failed read, sonconsecutive stalls exhaust--reconnect-attempts n; - without
--reconnect, and for serial/device sources (which have no reconnect): the source ends like EOF.
The summary: line gains rover_stalls= and base_stalls= (only when the flag
is set), and termination=rover_source_stalled / base_source_stalled when a
stall is what ended the run. The timer measures silence on the descriptor, not
epoch rate, so it is a transport-level dead-peer detector; pick a value above the
longest gap the peer legitimately leaves (a 1 Hz base needs well above 1000).
What it applies to:
tcp://rover/base: yes.serial:/// device base or rover RTCM: yes.--rover-ubxfrom a serial device: yes, the samepoll()before the deviceread(); a stall ends the rover source (no reconnect for serial). A UBX file: no.- RTCM files: no effect.
- NTRIP (
ntrip://,http(s)://): unchanged.NTRIPClientalready waits withselect()(1 s per read) and returns "no message" on a quiet but connected caster; it reconnects only when the socket is closed, not when the caster is silent, and--stall-timeout-msdoes not change that or reuse its timeout.
Only the source being read can stall: while gnss live waits on the base, the
rover socket is not read (and vice versa), so a stall is detected on the source
that is blocked (unless --base-outage-fallback spp
is given, which reads every live source in its own thread). See Real-time stress harness for the
base_blackhole / rover_blackhole faults that exercise it.
Base-outage SPP fallback for gnss live (--base-outage-fallback spp)¶
Opt-in (default off: the code path, the .pos and the summary: line are exactly
what they were). Without it a rover epoch that gets no base (no exact,
interpolated or held base) is skipped without any output, and while the base
source is silent gnss live blocks on the base read and does not read the rover
at all, so a base outage means no output for its whole length.
With --base-outage-fallback spp:
- A rover epoch that would be skipped for lack of a base is solved with the
library's single-point solver (
SPPProcessor, configured as the RTK processor configures its own SPP seed) and written with statusSPP(1in the.pos). It is neverFLOATorFIXED. The RTK processor, its filter state, the rover seed and the--kinematic-post-filterare not touched by these epochs; RTK resumes on the next epoch that has a base, as it does today. The rows carry no differential age (NaNwith--pos-uncertainty). - File sources: the existing alignment decides. Every epoch it would skip gets an SPP row immediately, so base epochs missing for a span give SPP rows in the span and the RTK rows elsewhere are byte-identical to a run without the flag.
- Live sources (
tcp://,ntrip://,serial:///device base or rover, including a serial--rover-ubx): each live source is read by its own thread into a bounded queue (4096 messages or UBX chunks), so a silent base no longer stops the rover from being read. The threads only do blocking I/O; decoding, epoch assembly, alignment and all solvers stay on the main thread. A rover epoch is declared baseless oncemax(1 s, --base-hold-seconds + base interval)of wall time has passed since the newest base epoch was taken from the source (the base interval is the spacing of the first two base epochs, then tracked within 2x, so an outage gap cannot stretch it) and no newer base epoch is queued. A rover epoch that the--base-hold-secondswindow still covers is held (RTK) as before. The default timeout is 1.5 s (1 Hz base, 0.5 s hold). - Without the flag no thread is created. With it, the threads stop on every exit
path (EOF,
--stall-timeout-mstermination,--max-epochs, an exception): blocking waits run in 50 mspoll()slices that check an abort flag (RTCMReader::setAbortable()/requestAbort()), and the threads are joined before the summary is printed. NTRIP keeps its own boundedselect()wait. - The
summary:line gainsfallback_spp_epochs=(only with the flag).skipped_rover_epochsstill counts every epoch that got no RTK base; the SPP rows are counted infallback_spp_epochsand inwritten_solutions.
Not covered: a base that never delivers a first epoch (the first aligned pair is
still awaited, because the week, the base position and the ephemerides come from
the base stream unless --nav-rinex and --base-ecef are given); a base source
that has ended (EOF, --stall-timeout-ms without --reconnect, exhausted
--reconnect-attempts) still ends the run as before; a silent rover is still
waited for. The fallback is a code-only position: its accuracy is that of
single-point positioning with the broadcast ephemerides. Measured: Real-time
stress harness.
Per-epoch uncertainty in .pos (--pos-uncertainty)¶
gnss solve, gnss replay and gnss live accept --pos-uncertainty (default
off; with it off the .pos is byte-identical to earlier releases, and it
requires --format pos). It appends seven columns after
RTKUpdateNISRejected and declare them in the % GPS_Week ... header:
| Column | Meaning |
|---|---|
sdn(m) sde(m) sdu(m) |
1-sigma north/east/up standard deviation in metres, from PositionSolution::positionCovarianceEnu() |
sdne(m) sdeu(m) sdun(m) |
covariance cross terms, RTKLIB signed square root sign(c)*sqrt(abs(c)) |
DiffAge(s) |
rover epoch time minus the base observation time used (0 for an exactly matched base epoch; for an interpolated or held base epoch, the age of the older sample) |
Unavailable values are written as nan. Existing column positions do not move,
so index-based readers keep working; Solution::loadFromFile also rebuilds the
ECEF covariance from these columns. The sigma is a model output, not a
certified bound: the default RTK reported covariance can be a constant (see
--rtk-reported-covariance). gnss solve has no NMEA output, so no GST
sentence is produced.
gnss uncertainty-eval --pos run.pos --reference-csv reference.csv
[--out-json s.json --out-csv epochs.csv --out-stanford-csv bins.csv] matches
the epochs to a PPC / UrbanNav reference.csv (--match-tolerance-s, default
0.25 s) and reports, for FIXED, FLOAT and all epochs: normalized errors
e_H/sigma_H (sigma_H = sqrt(sdn^2+sde^2)) and |e_U|/sdu (median, p95,
p99), exceedance fractions P(|e| > k*sigma) for k = 1, 2, 3, 5 beside the
Gaussian expectation, and Stanford-diagram bin counts of error against a
K*sigma bound (--k-h/--k-v, default 5.33) split into nominal, misleading
(error above the bound) and, with --alert-limit-h/-v, unavailable (bound above
the alert limit). K*sigma is a diagnostic proxy and is not a certified
protection level. A PNG is written with --out-png only when matplotlib is
installed.
Float-fix distance in .pos (--pos-fix-diagnostics)¶
gnss live --pos-fix-diagnostics, gnss solve --pos-fix-diagnostics and
gnss replay --pos-fix-diagnostics (default off; with it off the .pos is
byte-identical to earlier releases, and it requires --format pos) append two
columns after every other column (after DiffAge(s) when --pos-uncertainty
is also on) and declare them in the % GPS_Week ... header:
| Column | Meaning |
|---|---|
FloatFixH(m) |
horizontal distance, in the local ENU frame at the FIXED position, between the epoch's FLOAT position (computed before ambiguity resolution) and the FIXED position |
FloatFixU(m) |
absolute vertical difference between the same two positions |
Both are nan on every row that is not FIXED (FLOAT, SPP and fallback rows).
The float position is recorded read-only next to the solution
(PositionSolution::rtk_float_position_ecef); no filter state, position,
status or covariance changes, and every other column is byte-identical to a run
without the flag. Existing column positions do not move, so index-based
readers and Solution::loadFromFile keep working.
gnss solve: only for the RTK epoch's own fixed position¶
gnss solve post-processes the RTK epochs before writing them, so the value is
defined against the written row: the distance between the epoch's FLOAT
position and the position written in that row, reported only when the written
status is FIXED and the written position is still the RTK epoch's own
fixed position. Otherwise both columns are nan. gnss solve records the
position of every epoch that RTKProcessor returned as FIXED (read-only,
before any post-processing) and compares the written row with it bit for bit
at write time, so the rule holds for every step below without per-step
bookkeeping. Every non-FIXED row is nan. Post-processing that changes what
is written for an RTK FIXED epoch:
| Step | What it does to a FIXED epoch |
Written |
|---|---|---|
--demote-fixed-status-*, --fix-recovery, the realtime integrity gate |
demotes the status to FLOAT |
FLOAT row, nan |
| library fixed-quality gate (when enabled by a preset or option) | demotes to FLOAT, or can promote an epoch to FIXED at another candidate position |
FLOAT row, or a FIXED row that is not the RTK epoch's own fixed position: nan |
| kinematic post-filter (on by default), height-step jump | drops the epoch, or replaces it with a propagated FLOAT continuity row |
no row, or FLOAT row with nan |
| non-FIX drift, SPP height-step, FLOAT bridge-tail and FIXED bridge-burst guards | remove rows; they never edit a kept row | no row |
--safe-availability-fallback re-insertion of removed epochs |
re-inserts the epoch as SPP / PROPAGATED |
nan |
The SPP / continuity / propagated fallbacks that replace a rejected non-FIXED
solution only ever produce non-FIXED rows, so they are nan too. A written
FIXED row whose position differs from the RTK epoch's own, or whose epoch
carried no float reference, is nan as well (the fix diagnostics: line
counts them as nan_modified_fixed_rows and nan_no_float_reference_rows);
neither occurs in the runs below.
Measured (flag on, --mode kinematic --preset low-cost, RINEX, develop
36f05743 plus this option; gnss solve prints a fix diagnostics: summary
line with these counts):
| Run | RTK FIXED epochs |
written FIXED rows |
finite | FIXED rows with nan |
FIXED epochs without a row |
|---|---|---|---|---|---|
| PPC tokyo run1 | 8,432 | 8,271 | 8,271 | 0 | 161 |
| PPC tokyo run2 | 7,204 | 7,141 | 7,141 | 0 | 63 |
| PPC tokyo run3 | 11,115 | 10,960 | 10,960 | 0 | 155 |
| PPC nagoya run1 | 4,853 | 4,829 | 4,829 | 0 | 24 |
| PPC nagoya run2 | 4,580 | 4,544 | 4,544 | 0 | 36 |
| PPC nagoya run3 | 2,343 | 2,209 | 2,209 | 0 | 134 |
On tokyo run1 all 161 missing epochs are removed by the kinematic post-filter's
height-step drop (with --no-kinematic-post-filter all 8,432 are written, each
with finite columns). With the demotion options (--demote-fixed-status-nis-per-obs 2
--demote-fixed-status-max-ratio 4) demoted epochs are written as FLOAT with
nan; in a 5,000-epoch run every written FIXED row is still finite. So with these settings the
nan on a FIXED epoch always means "the row is not FIXED any more" and a
written FIXED row always has a value. The float position is the epoch's own
float solution, so the same caveats as for gnss live apply.
gnss replay writes the RTK epoch result directly (no post-processing), so it
uses the gnss live definition: finite exactly on FIXED rows.
Measured separation with gnss solve (descriptive, seen data; nothing was
tuned on it and no threshold is recommended): AUROC of FloatFixH alone for a
wrong FIX (FIXED with 3D error > 0.5 m against reference.csv, nearest epoch
within 0.25 s) against a correct FIX, gnss solve --mode kinematic --preset
low-cost, RINEX inputs, written FIXED rows (all finite):
| Run | scored FIXED rows |
wrong FIX | AUROC FloatFixH |
|---|---|---|---|
| PPC tokyo run1 | 8,271 | 494 | 0.76 |
| PPC tokyo run2 | 7,141 | 67 | 0.88 |
| PPC tokyo run3 | 10,960 | 191 | 0.84 |
| PPC nagoya run1 | 4,829 | 245 | 0.84 |
| PPC nagoya run2 | 4,544 | 64 | 0.98 |
| PPC nagoya run3 | 2,209 | 219 | 0.78 |
Pooled 0.82 (36,674 correct, 1,280 wrong). These are the same six PPC runs the
option was examined on, so the numbers are not a held-out result, and they do
not by themselves transfer to other receivers or environments. The gnss live
table above uses RTCM replays and a different fix set, so the two are not
directly comparable.
This is a diagnostic, not an integrity bound: it is a number to threshold or log, not a certified protection level, and it carries no unit of probability. A large value means the integer solution moved the position far from what the float filter believed, which is more common on wrong fixes, but a small value does not make a fix correct.
Measured separation (descriptive, seen data; nothing was tuned on it and no
threshold is recommended): AUROC of FloatFixH alone for a wrong FIX (FIXED
with 3D error > 0.5 m against reference.csv, nearest epoch within 0.25 s)
against a correct FIX, gnss live --preset low-cost, multi-GNSS RTCM replay,
one run each:
| Run | FIXED epochs | wrong FIX | AUROC FloatFixH |
|---|---|---|---|
| UrbanNav Odaiba (Trimble) | 7687 | 78 | 0.81 |
| UrbanNav Odaiba (u-blox) | 2580 | 574 | 0.74 |
| UrbanNav Shinjuku (Trimble) | 13963 | 1687 | 0.73 |
| UrbanNav Shinjuku (u-blox) | 5043 | 276 | 0.75 |
| PPC tokyo run1 | 7884 | 247 | 0.80 |
| PPC tokyo run2 | 7142 | 141 | 0.90 |
| PPC tokyo run3 | 9897 | 274 | 0.84 |
| PPC nagoya run1 | 5252 | 137 | 0.77 |
| PPC nagoya run2 | 4321 | 52 | 0.95 |
| PPC nagoya run3 | 1964 | 72 | 0.78 |
Pooled: UrbanNav Tokyo 0.78, PPC 0.83. The AUROC is a ranking statistic over this data, with no threshold and no
false-alarm rate. Per run the median FloatFixH is 2 to 8 mm for a correct FIX
and 1 to 59 cm for a wrong FIX, and the two distributions overlap. Provenance:
develop e35583e1 plus this option; the four UrbanNav Tokyo runs (Odaiba and
Shinjuku, Trimble and u-blox, Applanix reference.csv, base ECEF
-3961904.9530 3348993.7578 3698211.7553) and the six PPC runs (Tokyo and
Nagoya runs 1 to 3, PPC reference.csv, base.nav), all replayed as
multi-GNSS RTCM MSM streams re-encoded from the RINEX observations. These are
the same data the option was examined on, so they are not a held-out result and
they do not by themselves transfer to other receivers or environments.
Broadcast ephemeris selection among same-toe copies¶
Merged RINEX navigation files and multi-stream RTCM carry more than one copy of
a broadcast record with the same toe: for Galileo an I/NAV and an F/NAV copy
(their af0 differs by decimetres, because the clocks reference E5b/E1 and
E5a/E1). NavigationData::getEphemeris() takes the first record of the
smallest age, so the copy used depended on the order in which the records
were added (RINEX load, gnss live merge, RTCM decode, copy). Each satellite's
records are now kept in a fixed total order: toe, then, for Galileo only, I/NAV
(data-source bit 9) before records without it, then data_source_code, iode,
iodc, toc, af0, af1, af2 and the remaining fields. A same-toe Galileo
I/NAV record is therefore always selected over its F/NAV copy; a lone F/NAV
record is still used (nothing is filtered), and non-Galileo ties are resolved by
the same order instead of by insertion order. The .pos of gnss solve,
gnss live and gnss ppp no longer changes when the navigation records are
permuted. GNSS_PPP_GAL_INAV and setGalileoEphemerisSource are unchanged.
Satellite health rule¶
gnss spp, gnss ppp and the FGO tools exclude a satellite whose broadcast
ephemeris is flagged unhealthy. They share one rule,
libgnss::ephemerisHealthy() (core/navigation.hpp): the full health word must
be zero, except that QZSS masks bit 0 (the L6 health bit, which does not affect
L1/L2/L5), as RTKLIB satexclude() does. There is no GLONASS special case and no
URA/variance rule. Ephemeris::health is uint16_t and every reader (RINEX
2/3/4, RTCM 1019/1020/1042/1044/1045/1046, gnss convert) stores the whole word.
It used to be uint8_t, which wrapped a Galileo E5b health word with only bit 8
set (256) to 0 and made that satellite look healthy.
RTK (gnss solve, gnss live, gnss replay) does not apply this rule; it
uses a satellite whatever its health flag. Applying it was checked under a
contract frozen before the run (2026-10-13) and was No-Go. On the UrbanNav Tokyo
merged base.nav, the BDS-3 satellites C18, C23 and C24 are flagged health=1
all day (they were in test at the time), and one of them was in the double
differences on 44 to 76 % of the epochs of the four Shinjuku and Odaiba runs.
Excluding them, pooled over the four runs with --preset low-cost, moved
FIXED epochs with a 3D error above 0.5 m from 7.47 % to 7.58 % of FIXED in
gnss solve and from 6.35 % to 9.50 % in multi-GNSS gnss live (limit +1.0
point), so the rule is not applied. On the six PPC runs and HK Medium-Urban-1 no
unhealthy satellite is observed by both receivers and the rule would change
nothing. If your navigation data carries ephemerides that are unhealthy because
they are wrong (rather than flagged during a test campaign), RTK will still use
them.
RTK hold-fix fallback (known weakness)¶
After min_hold_count consecutive FIXED epochs (8 with --preset low-cost), an
epoch whose ambiguity resolution fails can still be written as FIXED by the
hold-fix fallback (RTKProcessor::tryHoldFix). It reuses the integers held from
the previous fix and rounds the float double difference of every pair that was not
held (a satellite that joined, or an ambiguity that was reset), without a ratio
test. It accepts the result if it lies within 1.0 m (0.1 m static) of the last fix,
and then holds the rounded integers as well. The ratio column of such a row shows
the configured threshold (3.0 with --preset low-cost), not a measured ratio.
This matters when satellites become usable one after another shortly after a fix,
which is what a cold start without a navigation file looks like. The ephemerides
then arrive from the rover's own subframes (--ubx-nav) or from the base stream.
A survey with a written plan fixed in advance measured this on three static 3DOM
logs (rover A 2025-06-04, rover B 2025-06-05 and 2025-06-06, MOCA base about 3.6 km
away, gnss live --preset low-cost). Each log was cold-started every 300 s for
30 min, 143 starts per arm. The arms differ only in how the ephemerides arrive:
| Ephemerides | FIXED epochs | FIXED > 0.5 m (3D) | Starts holding a 0.1–0.5 m FIX for ≥ 60 s |
|---|---|---|---|
--nav-rinex (all at the first epoch) |
256 512 | 0 | 0 |
--ubx-nav, no navigation file |
253 313 | 2 241 (0.88 %) | 3 |
| Base stream, ephemeris messages round-robin over 30 s, no navigation file | 253 924 | 1 329 (0.52 %) | 3 |
In every episode a subset fix on 7 to 9 satellites came first. Galileo and GLONASS
satellites joined within seconds, the full-set ratio fell to 1.0–1.4, and the
hold-fix fallback kept the FIX on 83 to 100 % of the epochs while rounding 4 to 12
new pairs. The held position was 0.2 to 2.4 m off, for up to 30 min. In the
instrumented runs, every FIXED row whose ratio was exactly 3.0 was a hold-fix row
(6 429 of 6 429). Over all runs, 97 % (--ubx-nav) and 88 % (round-robin) of the
FIXED rows more than 0.1 m off had that ratio.
Two changes were checked, each under a plan frozen before the run.
Constraining only the held integers and leaving the new pairs float (2026-10-13) was No-Go:
- It removed the held 0.1–0.5 m episodes with
--ubx-nav(3 starts to 0, FIXED0.5 m 2 241 to 1 787) and improved
gnss solveon PPC and UrbanNav (FIXED 0.5 m 5.77 % to 3.95 % of FIXED). - It also let the fallback run far more often (2.4 times in
gnss solveand 3.2 times ingnss liveon PPC and UrbanNav), with few held pairs and many float ones. The round-robin arm went from 1 329 to 2 499 FIXED epochs > 0.5 m, and multi-GNSSgnss liveon PPC and UrbanNav from 5.60 % to 7.69 % (before the Doppler slip fix), against a limit of +0.2 points.
The second change refuses the fallback whenever any current pair has no held
integer, so a pair that joined or was reset ends the held FIX instead of being
rounded (2026-10-14). It removed almost all wrong fixes of the cold-start survey,
but cost 6 to 8 % of the FIXED epochs on PPC and UrbanNav, most of them correct ones. The
plan kept at least 98 % of the FIXED epochs, so as the default it was No-Go. It is
offered as the opt-in --hold-fix-unchanged-set of gnss solve, gnss live and
gnss replay (RTKConfig::hold_fix_unchanged_set_only). Without the option every
tool is byte-identical to before. With it the output is byte-identical to the rule
that was measured:
Run set (--preset low-cost) |
Without the option | With --hold-fix-unchanged-set |
|---|---|---|
Cold-start survey above, --ubx-nav: FIXED > 0.5 m / starts holding 0.1–0.5 m ≥ 60 s |
2 241 / 3 | 16 / 0 |
| Same, round-robin ephemerides from the base | 1 329 / 3 | 53 / 0 |
Same, --nav-rinex |
0 / 0 | 0 / 0 |
gnss solve, PPC 6 runs + UrbanNav Tokyo 4 runs: FIXED > 0.5 m / FIXED |
4 074 / 70 574 (5.77 %) | 2 026 / 65 293 (3.10 %) |
Multi-GNSS gnss live, PPC 2 + UrbanNav Tokyo 4: FIXED > 0.5 m / FIXED |
1 630 / 39 771 (4.10 %) | 1 555 / 37 407 (4.16 %) |
Held-out check (3DOM rover B 2025-06-07 and 2025-06-08, not used before, 86 cold starts per arm): FIXED > 0.5 m, --nav-rinex / --ubx-nav / round-robin |
2 / 26 / 1 828 | 0 / 26 / 50 |
- FIXED epochs kept with the option:
- 99.7 to 99.9 % in the survey arms;
- 99.65 to 99.92 % on the held-out logs;
- 92.5 % in
gnss solveand 94.1 % ingnss liveon PPC and UrbanNav (correct FIXED epochs −4.9 % and −6.0 %). - In
gnss livethe option removed few wrong fixes: 1 630 to 1 555, so the share of wrong FIXED epochs rose slightly (4.10 to 4.16 %). Thegnss liverow was measured after the Doppler slip fix of 2026-10-15. - On the held-out logs the gain rests on few starts:
- One round-robin start accounts for 1 778 of the 1 828 wrong FIXED epochs. It held a fix 0.9 to 1.5 m off from a hold-fix row; with the option those 2 s were FLOAT, and the next fix was correct (0.03 m, ratio 3.4).
- One
--nav-rinexstart had the only held 0.1–0.5 m episode (451 s). - The 26 wrong FIXED epochs of
--ubx-navwere the same with and without the option.
A third rule, also opt-in, refuses the fallback when too few current pairs carry a
held integer: --hold-fix-min-matched-pairs <n> of gnss solve, gnss live and
gnss replay (RTKConfig::hold_fix_min_matched_pairs, reject reason
matched_pairs_below_min). The default 4 is the floor the fallback always had, so
without the option every tool is byte-identical to before.
It comes from a survey on urban data this project had not used before (2026-10-14):
- 11 SmartPNT-POS vehicle runs in Wuhan and Shanghai, with a GNSS/INS reference at the antenna.
- 2 TEX-CUP days in Austin, with an iXblue GNSS/INS reference.
- Before any data was read, a fixed rule split them into development runs (6 + 1) and check runs (5 + 1).
With gnss solve --preset low-cost, the development runs showed:
- 3.73 % of the scored FIXED epochs were more than 0.5 m (3D) off.
- 31 % of those were hold-fix rows. Hold-fix rows were wrong 26 % of the time, against 2.7 % for fixes from ambiguity resolution.
- The wrong hold-fix rows held few pairs: median 14, against 24 for correct ones.
- Almost all of the wrong hold-fix rows had every candidate pair held, so
--hold-fix-unchanged-setdoes not refuse them directly.
The minimum n = 12 was chosen on the development runs by a rule written down before the scan. The rule keeps at least 98 % of the correct fixes, then takes the fewest wrong ones. The value was then measured once on the check runs under a frozen plan. Each cell is FIXED > 0.5 m / scored FIXED:
Run set (--preset low-cost) |
Without the options | --hold-fix-min-matched-pairs 12 |
--hold-fix-unchanged-set |
|---|---|---|---|
| Urban development runs (SmartPNT 6, TEX-CUP 2019-05-09) | 1 015 / 27 213 (3.73 %) | 696 / 26 801 (2.60 %) | 651 / 25 273 (2.58 %) |
| Urban check runs (SmartPNT 5, TEX-CUP 2019-05-12), scored once | 533 / 31 510 (1.69 %) | 331 / 30 845 (1.07 %) | 330 / 29 876 (1.10 %) |
gnss solve, PPC 6 runs + UrbanNav Tokyo 4 runs |
4 074 / 70 574 (5.77 %) | 1 972 / 69 112 (2.85 %) | 2 026 / 65 293 (3.10 %) |
Multi-GNSS gnss live, PPC 2 + UrbanNav Tokyo 4 |
1 630 / 39 771 (4.10 %) | 1 550 / 38 926 (3.98 %) | 1 555 / 37 407 (4.16 %) |
- Correct FIXED epochs kept, in the order development runs, check runs,
gnss solveon PPC and UrbanNav,gnss liveon PPC and UrbanNav: - with
--hold-fix-min-matched-pairs 12: 99.6 %, 98.5 %, 101.0 % and 98.0 %; - with
--hold-fix-unchanged-set: 94.0 %, 95.4 %, 95.1 % and 94.0 %. - On the check runs the gain rests on one run.
- TEX-CUP 2019-05-12 went from 221 to 19 wrong FIXED epochs; a 200-epoch wrong episode was replaced by correct fixes.
- Four of the five SmartPNT check runs were unchanged.
- On the fifth (Data12) the wrong count stayed at 186, and 4.8 % of its correct FIXED epochs were lost.
- The PPC and UrbanNav rows were not used to choose n. They had been used for
the earlier hold-fix decisions above. The
gnss liverow was measured after the Doppler slip fix of 2026-10-15, which changed only UrbanNav Odaiba with the u-blox receiver. - Not measured: the cold-start survey, where
--hold-fix-unchanged-setis the measured option, and the two options together. - Scoring of the urban data:
- FIXED epochs are matched to the reference within 0.01 s.
- Epochs whose reference 3D sigma exceeds 0.10 m are not scored.
- The datasets are SmartPNT-POS (Zhu et al., IEEE Transactions on Instrumentation and Measurement, 2024, doi:10.1109/TIM.2024.3488156) and TEX-CUP (Narula et al., IEEE/ION PLANS 2020, arXiv:2005.00709).
Until the default changes:
- Give
gnss livea navigation file (--nav-rinex, for example the IGS BRDC of the day) when you can, also together with--ubx-nav. - When post-processing a drive with
gnss solve --preset low-cost, add--hold-fix-min-matched-pairs 12. On the runs above it removed a third to a half of the wrong FIXED epochs and kept 98 to 101 % of the correct ones. Ingnss livethe gain was smaller. - Use
--hold-fix-unchanged-setwhen you prefer fewer wrong fixes to more fixes, in particular on a cold start without a navigation file. - Without either option, treat a FIXED row whose ratio equals the ratio threshold as held, not validated.
RTK fixes from few satellites¶
An ambiguity-resolution FIX can rest on very few double differences.
--min-fix-satellites <k> of gnss solve, gnss live and gnss replay
(RTKConfig::min_fix_satellites, reject reason fixed_satellites_below_min)
refuses such a fix:
- It applies to every ambiguity-resolution FIX: full set, subset, and the WL/NL fallback. The fix is refused when its fixed set has fewer than k non-reference satellites. All frequencies of a satellite count once.
- A refused epoch stays FLOAT unless the hold-fix fallback applies. That path has
its own floor,
--hold-fix-min-matched-pairs. - The WL/NL fallback always needed 3 resolved satellites; with the option it needs k.
- The default 0 disables the check, so without the option every tool is byte-identical to before.
It comes from the urban development runs above, with
gnss solve --preset low-cost --hold-fix-min-matched-pairs 12:
- Full-set ambiguity resolution gave 76.6 % of the FIXED epochs more than 0.5 m (3D) off. Subsets gave 18.5 % and the hold-fix fallback 4.9 %.
- The smaller the fixed set of an ambiguity-resolution FIX, the more often it was wrong:
| Non-reference satellites in the fixed set | 2 | 3 | 4 | 15 or more |
|---|---|---|---|---|
| Wrong | 32.0 % | 16.1 % | 9.1 % | 0.2 % |
k = 4 was chosen on the development runs by a rule written down before the scan:
- Keep at least 95 % of the correct fixes.
- Take the fewest wrong ones. Within 5 % of that, prefer the smaller k.
- No run may get more than 5 wrong FIXED epochs worse.
The value was then measured once on held-out data under a frozen plan. All rows use
--preset low-cost --hold-fix-min-matched-pairs 12. Each cell is FIXED > 0.5 m /
scored FIXED:
| Run set | Without the option | --min-fix-satellites 4 |
Correct FIXED epochs kept |
|---|---|---|---|
| Urban development runs (SmartPNT 6, TEX-CUP 2019-05-09) | 696 / 26 801 (2.60 %) | 355 / 25 636 (1.38 %) | 96.8 % |
| Held-out GREAT-WHU run (Wuhan, 2020-12-16), scored once | 43 / 8 724 (0.49 %) | 14 / 8 663 (0.16 %) | 99.6 % |
| Held-out urban check runs (SmartPNT 5, TEX-CUP 2019-05-12), scored once with k | 331 / 30 845 (1.07 %) | 127 / 29 705 (0.43 %) | 96.9 % |
gnss solve, PPC 6 runs + UrbanNav Tokyo 4 runs |
1 972 / 69 112 (2.85 %) | 806 / 66 336 (1.22 %) | 97.6 % |
Multi-GNSS gnss live, PPC 2 + UrbanNav Tokyo 4 |
1 550 / 38 926 (3.98 %) | 518 / 36 447 (1.42 %) | 96.1 % |
- The held-out rows together:
- 374 / 39 569 (0.95 %) went to 141 / 38 368 (0.37 %), keeping 97.5 % of the correct FIXED epochs.
- Every run with FIXED epochs had fewer wrong ones.
- The lowest share of correct FIXED epochs kept was 93.7 % (TEX-CUP 2019-05-12).
gnss livekept 93.7 to 99.7 % per run. Before the Doppler slip fix of 2026-10-15, UrbanNav Odaiba with the u-blox receiver lost 39.5 %: false slips had left few satellites in its fixed sets. After the fix it went from 43 / 3 456 to 8 / 3 253 and kept 95.1 %. Thegnss liverow above was measured after the fix.- Only one of the three GREAT-WHU sessions counted.
- The 2021-10-13 session, which its authors describe as a GNSS challenge, never reached FIX, with or without the option. It was 1.6 to 19.7 km from the base.
- The 2021-10-12 session was excluded by the frozen rule. Its reference file does not say which point it describes, and the drive turned too little to locate that point from the data.
- Scored outside the plan with the IMU lever arm from that session's configuration, the 2021-10-12 session went from 2 / 835 to 1 / 825 (98.9 % of the correct FIXED epochs kept).
- The SmartPNT and TEX-CUP check runs had been opened once before, to measure
--hold-fix-min-matched-pairs 12. k was never run on them before this measurement and they were not used to choose it. - The PPC and UrbanNav rows were not used to choose k.
- Without
--hold-fix-min-matched-pairs 12(development runs only), k = 4 took--preset low-costfrom 1 015 / 27 213 (3.73 %) to 442 / 26 227 (1.69 %), keeping 98.4 % of the correct FIXED epochs. - Not measured: cold starts.
- Scoring of the GREAT-WHU run:
- Same definitions as the SmartPNT-POS and TEX-CUP runs above.
- The reference is at the GNSS antenna, as its file name (
...toGNSS) says. A lever-arm fit on the fixes of--preset low-costagreed within 5 mm. - The data are the sample data of the GREAT-PVT and GREAT-MSF repositories (GREAT group, Wuhan University).
When post-processing a drive with
gnss solve --preset low-cost --hold-fix-min-matched-pairs 12, add
--min-fix-satellites 4 if you prefer fewer wrong fixes to a few more fixes. On the
run sets above it removed 49 to 67 % of the remaining wrong FIXED epochs and kept
96.1 to 99.6 % of the correct ones. Check its cost on your own receiver.
Docker¶
The repo also ships a multi-stage Dockerfile. The runtime image installs:
- the
gnssdispatcher onPATH - native CLI binaries under
/opt/libgnsspp/bin - the
libgnssppPython package viaPYTHONPATH
The default entrypoint is gnss, so container invocations look like:
docker run --rm -it -v "$PWD:/workspace" libgnsspp:latest --help
docker run --rm -it -v "$PWD:/workspace" libgnsspp:latest ppp --help
docker run --rm -it -p 8085:8085 -v "$PWD:/workspace" libgnsspp:latest web --host 0.0.0.0 --port 8085 --root /workspace
docker compose up gnss-web
Python¶
Python bindings expose:
- RINEX header and epoch inspection
.posloading and solution statistics- coordinate conversion helpers
- file-based
SPP,PPP, andRTKsolve helpers - read-only artifact helpers under
libgnsspp.artifacts:load_summary(),summary_schema(),load_pos(), andpos_stats()
ROS2¶
The ROS2 playback node publishes .pos files as:
sensor_msgs/NavSatFixgeometry_msgs/PoseStampednav_msgs/Path- solution status and satellite-count telemetry
Local web UI¶
gnss web is a local visualization layer for existing artifacts, including field report summaries and Markdown previews, live, moving-base, PPC, and PPP-product sign-off summaries, artifact bundles, moving-base history charts, commercial receiver side-by-side metrics, next debug commands, and direct links to summary, plot, product, comparison CSV/PNG, match CSV, and provenance files.
It shows:
- Odaiba metrics
- PPC summaries
- live sign-off summaries
- moving-base plots
- commercial receiver comparisons for PPC and moving-base sign-offs
- 2D trajectories
- visibility summaries and a polar visibility view
gnss rcvreceiver status
Experiment Interface¶
PPP-AR experiments now use a minimal shared interface under
scripts/experiments/ppp_ar/.
The current experiment surface is:
scripts/experiments/ppp_ar/strategies.tomlscripts/experiments/ppp_ar/input.example.tomlscripts/experiments/ppp_ar/suite.example.tomlscripts/experiments/ppp_ar/run_experiments.py
The current CLAS PPP experiment family includes:
osr_float_pipelineosr_float_strict_pipelineosr_float_hybrid_pipelineosr_float_orbit_clock_bias_pipelineosr_float_orbit_clock_only_pipelineosr_float_phase_bias_only_value_pipelineosr_float_compensation_only_value_pipelineosr_float_clock_reference_time_pipelineosr_float_observation_reference_time_pipelineosr_float_sis_continuity_only_pipelineosr_float_repair_only_pipelineosr_float_raw_phase_bias_pipelineosr_float_no_phase_bias_pipelineosr_float_clock_bound_phase_bias_pipelineosr_float_clock_bound_atmos_phase_bias_pipelineosr_float_orbit_or_clock_source_pipelineosr_float_orbit_and_clock_source_pipelineosr_float_no_carry_atmos_merge_pipelineosr_float_network_locked_atmos_merge_pipelineosr_float_gridded_priority_subtype_merge_pipelineosr_float_combined_priority_subtype_merge_pipelineosr_float_message_reset_phase_bias_merge_pipelineosr_float_subtype5_priority_phase_bias_source_pipelineosr_float_subtype6_priority_phase_bias_source_pipelineosr_float_base_plus_network_code_bias_composition_pipelineosr_float_base_only_code_bias_composition_pipelineosr_float_same_30s_code_bias_bank_pipelineosr_float_close_30s_code_bias_bank_pipelineosr_float_latest_preceding_code_bias_bank_pipelineosr_float_selected_satellite_base_extend_pipelineosr_float_all_base_satellite_extend_pipelineosr_float_base_plus_network_phase_bias_composition_pipelineosr_float_base_only_phase_bias_composition_pipelineosr_float_same_30s_phase_bias_bank_pipelineosr_float_close_30s_phase_bias_bank_pipelineosr_float_latest_preceding_phase_bias_bank_pipelineosr_float_residual_only_value_pipelineosr_float_planar_subtype12_value_pipelineosr_float_offset_only_subtype12_value_pipelineosr_float_indexed_only_residual_sampling_pipelineosr_float_mean_only_residual_sampling_pipelineosr_float_selected_mask_prune_phase_bias_merge_pipelineosr_float_polynomial_only_value_pipelineosr_float_guarded_pipelineosr_float_balanced_pipelineosr_float_freshness_pipelineosr_ar_pipelineosr_ar_strict_pipelineosr_ar_hybrid_pipelineosr_ar_orbit_clock_bias_pipelineosr_ar_orbit_clock_only_pipelineosr_ar_phase_bias_only_value_pipelineosr_ar_compensation_only_value_pipelineosr_ar_clock_reference_time_pipelineosr_ar_observation_reference_time_pipelineosr_ar_sis_continuity_only_pipelineosr_ar_repair_only_pipelineosr_ar_raw_phase_bias_pipelineosr_ar_no_phase_bias_pipelineosr_ar_clock_bound_phase_bias_pipelineosr_ar_clock_bound_atmos_phase_bias_pipelineosr_ar_orbit_or_clock_source_pipelineosr_ar_orbit_and_clock_source_pipelineosr_ar_no_carry_atmos_merge_pipelineosr_ar_network_locked_atmos_merge_pipelineosr_ar_gridded_priority_subtype_merge_pipelineosr_ar_combined_priority_subtype_merge_pipelineosr_ar_message_reset_phase_bias_merge_pipelineosr_ar_subtype5_priority_phase_bias_source_pipelineosr_ar_subtype6_priority_phase_bias_source_pipelineosr_ar_base_plus_network_code_bias_composition_pipelineosr_ar_base_only_code_bias_composition_pipelineosr_ar_same_30s_code_bias_bank_pipelineosr_ar_close_30s_code_bias_bank_pipelineosr_ar_latest_preceding_code_bias_bank_pipelineosr_ar_selected_satellite_base_extend_pipelineosr_ar_all_base_satellite_extend_pipelineosr_ar_base_plus_network_phase_bias_composition_pipelineosr_ar_base_only_phase_bias_composition_pipelineosr_ar_same_30s_phase_bias_bank_pipelineosr_ar_close_30s_phase_bias_bank_pipelineosr_ar_latest_preceding_phase_bias_bank_pipelineosr_ar_residual_only_value_pipelineosr_ar_planar_subtype12_value_pipelineosr_ar_offset_only_subtype12_value_pipelineosr_ar_indexed_only_residual_sampling_pipelineosr_ar_mean_only_residual_sampling_pipelineosr_ar_selected_mask_prune_phase_bias_merge_pipelineosr_ar_polynomial_only_value_pipelineosr_ar_guarded_pipelineosr_ar_balanced_pipelineosr_ar_freshness_pipeline
The stable rule is that every strategy arm must share:
- one
obs - one
nav - one CLAS correction source (
qzss_l6or pre-expandedssr_csv) - one
reference_ecef - one
max_epochs - one output schema
When promotion is under evaluation, the runner can also take a suite config and compare the same strategy set across multiple CLAS cases before changing the default solver path.
If reference_ecef is omitted for a case, the runner falls back to the RINEX
header APPROX POSITION XYZ. That keeps new cases easy to add while still
using one shared metric schema.
The current output schema for comparisons is:
wall_time_sppp_solution_rate_pctppp_fixed_solutionsfallback_solutionsclas_hybrid_fallback_epochsmean_3d_error_mmedian_3d_error_mp95_3d_error_mmax_3d_error_mreadability_scoreextensibility_score
This keeps the stable core separate from the experiment lane while still making results directly comparable.
For compact-SSR ingestion experiments, the shared decode boundary also exposes:
gnss qzss-l6-info --compact-flush-policy <lag-tolerant-union|orbit-or-clock-only|orbit-and-clock-only>gnss qzss-l6-info --compact-atmos-merge-policy <stec-coeff-carry|no-carry|network-locked-stec-coeff-carry>gnss qzss-l6-info --compact-atmos-subtype-merge-policy <union|gridded-priority|combined-priority>gnss qzss-l6-info --compact-phase-bias-merge-policy <latest-union|message-reset|selected-mask-prune>gnss qzss-l6-info --compact-phase-bias-source-policy <arrival-order|subtype5-priority|subtype6-priority>gnss qzss-l6-info --compact-code-bias-composition-policy <direct-values|base-plus-network|base-only-if-present>gnss qzss-l6-info --compact-code-bias-bank-policy <pending-epoch|same-30s-bank|close-30s-bank|latest-preceding-bank|delayed-15s-bank>gnss qzss-l6-info --compact-bias-row-materialization <overlap-only|selected-satellite-base-extend|all-base-satellite-extend>gnss qzss-l6-info --compact-phase-bias-composition-policy <direct-values|base-plus-network|base-only-if-present>gnss qzss-l6-info --compact-phase-bias-bank-policy <pending-epoch|same-30s-bank|close-30s-bank|latest-preceding-bank>gnss ppp --receiver-antenna-type <type>gnss clas-ppp --compact-atmos-merge-policy <stec-coeff-carry|no-carry|network-locked-stec-coeff-carry>gnss clas-ppp --compact-atmos-subtype-merge-policy <union|gridded-priority|combined-priority>gnss clas-ppp --compact-phase-bias-merge-policy <latest-union|message-reset|selected-mask-prune>gnss clas-ppp --compact-phase-bias-source-policy <arrival-order|subtype5-priority|subtype6-priority>gnss clas-ppp --compact-code-bias-composition-policy <direct-values|base-plus-network|base-only-if-present>gnss clas-ppp --compact-code-bias-bank-policy <pending-epoch|same-30s-bank|close-30s-bank|latest-preceding-bank|delayed-15s-bank>gnss clas-ppp --antex <antennas.atx>gnss clas-ppp --receiver-antenna-type <type>gnss clas-ppp --clas-atmos-selection <grid-first|grid-guarded|balanced|freshness-first>gnss clas-ppp --clas-atmos-stale-after-seconds <seconds>gnss clas-ppp --compact-bias-row-materialization <overlap-only|selected-satellite-base-extend|all-base-satellite-extend>gnss clas-ppp --compact-phase-bias-composition-policy <direct-values|base-plus-network|base-only-if-present>gnss clas-ppp --compact-phase-bias-bank-policy <pending-epoch|same-30s-bank|close-30s-bank|latest-preceding-bank>
When a suite config is used, the output tree is partitioned by case:
output/.../<case_key>/compact_ssr.csvoutput/.../<case_key>/expanded_ssr.csvoutput/.../<case_key>/<strategy>/<strategy>.posoutput/.../<case_key>/<strategy>/<strategy>_summary.json
Moving-base¶
gnss solve, gnss replay, and gnss live accept --mode moving-base.
gnss moving-base-prepare extracts rover/base UBX streams plus a per-epoch reference CSV from a ROS2 moving-base bag or Zenodo zip. It can also export the rover receiver NAV-PVT solution as a normalized commercial receiver CSV. gnss moving-base-signoff is the validation entrypoint for replay/live runs against those references. gnss scorpion-moving-base-signoff wraps the public SCORPION bag flow by chaining prepare, BRDC nav fetch, replay validation, and receiver side-by-side matching. Together they cover:
- ROS2 bag or zip ingestion with u-blox
NAV-PVT/RXM-RAWX/NAV-RELPOSNED - replay inputs via
--rover-ubxand--base-ubx - public receiver side-by-side summaries via
--commercial-csv/--commercial-pos - fix rate
- baseline error percentiles
- heading error percentiles
- realtime factor
- decoder error counts