Warning
Under heavy development. The command line, config format and Python API may change without notice.
A Python port of R2R's NavManager, built in the same style as sbe-qa-processing. From a research vessel's raw GNSS files and a small cruise config, it produces:
- QA statistics and checks: completeness, gaps, records out of sequence, bad quality indicators, unreasonable speeds and accelerations, and the satellite, HDOP, altitude, speed and acceleration ranges.
- a PDF report, every figure as an SVG, and a Jupyter notebook that re-runs the assessment.
- by default, the standard R2R GeoCSV 2.0 navigation products (
--no-productsleaves them out):- bestres (
<CRUISE>_bestres.geoCSV): every fix, with bad fixes flagged (#), and speed and course over ground; - 1min (
<CRUISE>_1min.geoCSV): the good fix nearest each whole minute; - control (
<CRUISE>_control.geoCSV): Douglas–Peucker control points of 1min.
- bestres (
- optionally with the R2R commands, an R2R QA 2.0 XML report with R2R's six tests, ratings and bounds.
It runs from the command line and can be integrated with OpenVDM to run as a post-transfer hook and end-of-cruise task.
docs/examples contain example products from a real cruise (SP2613): the PDF report, with and without R2R's certificate, products, a figure and the R2R QA 2.0 XML.
Requires Python 3.11+, uv and git. INSTALL.md has the commands for macOS, Debian and Ubuntu, RHEL, AlmaLinux and Rocky Linux, and Windows, the install for the OpenVDM hook, the map data, and installing for a ship without internet. In short:
git clone https://github.com/OceanDataTools/nav-qa-processing.git
cd nav-qa-processing
uv sync
uv run pytestuv run nav-qa-processing formats # the raw formats it knows
uv run nav-qa-processing run path/to/fileset -f nav29 -o output/MYCRUISE
uv run nav-qa-processing run path/to/fileset -c my_config.toml -o output/MYCRUISEWithout -c, the products are named after the fileset directory, every fix in it is read, and
the expected-area check is n/a. A config TOML sets the cruise's ID, dates and
area, the GNSS files and their format, and the thresholds; every field is optional.
COOKBOOK.md walks through whole workflows with real output: a directory of a ship's raw nav files (finding the format, a config TOML, several sensors, the notebook, Python), and an R2R fileset (downloading it, its cruise TOML, R2R's assessment, comparing with R2R's products).
nav-qa-processing --help lists the commands, and nav-qa-processing COMMAND --help (or
nav-qa-processing r2r COMMAND --help) documents each one. The core commands are formats,
run and fetch-map-data; the R2R commands are a group under r2r; the
OpenVDM commands are openvdm, config-from-openvdm and site-config.
Every command follows these conventions:
-o/--outputis a directory; onlysite-configwrites a single file. Commands that make a cruise TOML write<OUTPUT_DIR>/<CRUISE_ID>_<FILESET_ID>.toml(defaultconfigs/), or to stdout with-o -.- Existing outputs aren't overwritten without
--force. A command checks before it does any work or asks R2R anything. - Fileset IDs are integers.
- A command exits 0 on success and 1 when it refuses (e.g. existing outputs) or fails with an
expected error (a server that can't be reached, R2R not having what was asked for, a config
that can't run the QA, a bad theme), with a one-line message on stderr, e.g.
nav-qa-processing: couldn't reach R2R (service.rvdata.us): ... Check the network connection and try again. -v/--verbose, before or after the command, logs more:-vis INFO and-vvis DEBUG (default WARNING).-vvalso prints the traceback of an expected error.
nav-qa-processing formats [ID]
Without ID, lists R2R's raw navigation format IDs (nav01 … nav33, uhdas, siomet,
metoc_nav), whether this version can read each one, and the NMEA sentences it knows. With
ID, describes that format (layout, sentences, vessels, R2R format IDs, examples) or sentence.
IDs have two digits, so they sort in order: nav02, not NavManager's nav2. NavManager's
spelling is still accepted wherever a format ID is given (-f, [nav] format, formats ID).
| Argument | Default | Description |
|---|---|---|
ID |
none | A format ID or NMEA sentence, e.g. nav29, GGA |
nav-qa-processing run [-c CONFIG_TOML] [-f FORMAT_ID] [--nav NAME] [-o OUTPUT_DIR] [--force]
[--theme THEME_TOML] [--execute-notebook] [--no-products] FILESET_DIR
Reads the fileset's primary GNSS feed, flags bad fixes, computes the QA statistics and checks, and writes the three products, the figures and the PDF report.
| Argument | Default | Description |
|---|---|---|
FILESET_DIR |
required | A directory of GNSS files, or a BagIt bag (bagit.txt, manifest-md5.txt and the files under data/). .gz and .bz2 files are read as they are |
-c, --config CONFIG_TOML |
none | A config TOML. Without one, the products are named after FILESET_DIR |
-f, --format FORMAT_ID |
the config's [nav] format |
The GNSS files' format (see formats). Needed when the config doesn't set it. With several feeds, only with --nav |
--nav NAME |
every feed | With a config listing several position sensors ([[nav]]), run only the one of this name |
-o, --output OUTPUT_DIR |
output |
Directory for the products, created if needed |
--force |
off | Replace existing products and reports; without it, run stops if any of them exists |
--theme THEME_TOML |
none | A theme file for the PDF, figures and notebook; the config's [theme] applies on top |
--execute-notebook |
off | Run the notebook (needs nbclient and ipykernel, in the dev group) and save it with its outputs. Its last cell, which writes the reports again, isn't run |
--no-products |
off | Don't write the GeoCSV products (bestres, 1min, control): only the figures, PDF and notebook. The PDF still gives each product's record count, its file marked "not written", and the notebook rewrites the reports without them |
It writes <CRUISE>_bestres.geoCSV, <CRUISE>_1min.geoCSV and <CRUISE>_control.geoCSV,
and the figures as SVGs in <CRUISE>_plots/, where <CRUISE> is the config's [cruise] id, or
the fileset directory's name. With several position sensors, every output of a feed other than
the primary also carries the feed's name, e.g. <CRUISE>_seapath_bestres.geoCSV:
| Figure | Shows |
|---|---|
track_map.svg |
The good track over coastlines, broken at gaps of max_gap or longer between good fixes, which are drawn faint and dashed (a stretch too small to show as a line at the map's scale is a dot), flagged fixes by QC flag (each under its first flag; the legend gives the QC table's count and how many are drawn), the 1min control points, the start and end, QC's position steps (smaller when there are many), the fixes the "Over water" check counts over land (ringed) and the expected area, with a world inset. The legend and inset never cover the track: with no clear place on the map, the legend goes below it and the inset is left out. Polar stereographic beyond 70°, and whole across the antimeridian |
speed_acceleration.svg |
Speed over ground and the acceleration QC tests, at the good fixes, against time, with max_speed and QC's acceleration limit |
timeline.svg |
When there are fixes, the gaps of max_gap or longer, and the flagged fixes by flag. Each record is marked once, under its first flag; a row gives the QC table's count and how many of its records are in the rows above |
histograms.svg |
The time between fixes, satellites, HDOP and GGA quality (invalid qualities in the secondary color); a field the receiver doesn't report (GLL: satellites, HDOP) is marked so |
Long cruises are reduced for drawing (a simplified track line, min/max bands per time bin, at most 3,000 markers per flag); counts in the legends are the full ones.
It also writes <CRUISE>_qa_report.pdf, which embeds the SVGs as vector drawings. Its sections
follow one another without page breaks; a section starts a new page only when it doesn't fit
on the current one:
| Sections | Contents |
|---|---|
| Summary | The title block (cruise, vessel, dates, expected area, contact, the fileset, its nav files and format, versions) and the checks in a line on the worst status's color (e.g. "2 of 8 checks warn: Completeness 47.34 %; Longest gap 13:30:25.500 (48625.500 s)."; with no usable fixes, a warning that no check could be made), then any warnings about the run, e.g. nav files outside the cruise window with their time span (the console summary and the hook's log say the same), every check with its status, the checks by file (what each check counted in each nav file read and the sentences the reader couldn't use, the files it couldn't read, those skipped as outside the cruise window, and the total), the statistics over the fixes that pass QC (their number, first and last fix, satellite, HDOP, altitude, speed and acceleration ranges) with the most common and longest times between fixes (the longest as hh:mm:ss) over every usable record, and the products |
| Quality control | Records flagged by flag, QC's notes (raised limits, position steps), reader and manifest errors (the first 12, then a count; with none, a line saying so and whether a manifest was checked), the gaps of max_gap or longer (the 10 longest, with their start, end, length as hh:mm:ss and the files either side, then a count), and the coverage timeline |
| Figures | The trackline map; speed and acceleration with sampling and fix quality, sharing a page |
Integrations put their pages after the title block: r2r run adds R2R's certificate,
with R2R's ranges over every record before QC.
And it writes <CRUISE>_qa.ipynb, a notebook that re-runs the assessment step by step: the
config (the config file, or the run's config written into it as TOML), run_qa, the checks,
statistics, QC flags and errors, every figure, the products, and writing the reports again
(all but the notebook itself). Edit its paths or the config's [thresholds] and re-run it to
change the assessment; see Opening the notebook. It prints a
summary: the fixes read, QC's flags, the products' sizes, and every check with its status.
nav-qa-processing fetch-map-data
Downloads the Natural Earth layers the trackline map uses (1:10m land, lakes and coastlines,
and the 1:110m world for the inset, about 27 MB) into cartopy's data directory, so the map has
coastlines offline, e.g. at sea. Without them, the map is drawn without coastlines and says so,
and the "Over water" check, which uses the same land and lakes, is n/a. Setting the environment
variable NAV_QA_NO_MAP_DATA has the same effect without touching the network; the tests set
it, so they never download map data (tests marked map_data use it when it's already here).
It takes no arguments.
The notebook imports this package, so it has to run on the project's Python (.venv), not on
the Python of whichever Jupyter opens it. A Jupyter installed with Homebrew, Anaconda or pip
starts its own Python by default, which doesn't have this project's packages, so the first cell
fails with ModuleNotFoundError: No module named 'nav_qa_processing'. Either:
Run JupyterLab from the project. Nothing to set up, and nothing is added to .venv or
uv.lock:
uv run --with jupyterlab jupyter lab output/SP2613/SP2613_qa.ipynbOr use the Jupyter you already have. Register the project's Python as a kernel, once per checkout:
uv run python -m ipykernel install --user --name nav-qa-processing --display-name "Python (nav-qa-processing)"Then, in each notebook, choose Kernel → Change Kernel… → Python (nav-qa-processing). The
generated notebooks ask for the default python3 kernel, so do this for each new one. It needs
ipykernel from the dev group; if you move the checkout or delete .venv, register it again;
jupyter kernelspec uninstall nav-qa-processing removes it.
The notebook's paths to the fileset, the config, the theme and the output directory are
absolute, so it doesn't matter where Jupyter starts. --execute-notebook runs the notebook
with the project's Python and needs neither setup. It runs every cell but the last, which
would rewrite the reports just written; run that one yourself to write them again.
A config TOML describes the cruise, the GNSS feed and the thresholds. Every table and key is
optional, but a key that isn't one of them is an error, which says where a misplaced or
misspelled key belongs (cruise_id (top level): did you mean [cruise] id?), as is a value
where a table belongs or a table where a value does ([cruise] extent must be a table). The keys of an
R2R cruise TOML are accepted too. configs/config.example.toml shows every
key, commented where it's optional or at its default. An example:
[cruise]
id = "SP2613" # the products' file name prefix and GeoCSV cruise_id
name = "MPL Internship Program Day at Sea"
pi = "Pereira, Filipe"
vessel_name = "Robert Gordon Sproul"
depart_date = 2026-07-09 # TOML dates, not strings
arrive_date = 2026-07-09
doi = "doi:10.7284/911454" # GeoCSV source_event
[cruise.extent] # expected area [decimal degrees, east and north positive]
westernmost = -117.38
easternmost = -117.22
southernmost = 32.59
northernmost = 32.71
[fileset]
doi = "doi:10.7284/169851" # GeoCSV source_dataset
manifest = "MD5_Summary.txt" # checksums for a plain directory; relative to this file
[nav] # the primary GNSS feed
format = "nav29"
files = "data/*bx992*.txt" # glob within the fileset
[nav.device] # the receiver or INS, for the reports; every key optional
make = "Trimble"
model = "BX992"
location = "main mast"
[provenance]
contact_institution = "Scripps Institution of Oceanography"
contact_email = "ship-tech@example.org"
[geocsv]
attribution = "Scripps Institution of Oceanography"
[thresholds] # defaults below
max_accel = 1.0| Table | Key | Used for |
|---|---|---|
[cruise] |
id |
The products' file names and GeoCSV cruise_id. Default: the fileset directory's name. In file names, /, \, <>:"|?* and control characters become _, so every output stays in -o and Windows can write it; the reports show the ID as given |
name, pi, location, vessel_name |
The reports, when given | |
depart_date, arrive_date |
The cruise's days, from 00:00 UTC on depart_date to the end of arrive_date: only files with fixes in them are read, and QC flags fixes more than date_margin_days outside them |
|
doi |
GeoCSV source_event |
|
[cruise.extent] |
westernmost, easternmost, southernmost, northernmost |
The "Within the expected area" check (n/a without it). All four are needed. A box across the antimeridian has westernmost > easternmost, e.g. 178 to -178 |
[fileset] |
doi |
GeoCSV source_dataset: the raw fileset |
manifest |
A checksum file for a plain directory: <md5> <path> lines, paths relative to the manifest's directory, as in OpenVDM's MD5 summary. A BagIt bag uses its own manifest-md5.txt |
|
[nav] |
format |
The GNSS files' format ID (see formats). Required, here or as run -f |
files |
A glob of the GNSS files within the fileset. Default: every file in the data directory | |
[nav.device] |
make, model, serial_number, location |
The receiver or INS that logged the feed, shown in the reports' title block with the GGA talker IDs found in the data (e.g. GN, a multi-constellation receiver; IN, an INS). All optional. In an R2R cruise TOML it defaults to R2R's [fileset.device] (make ID, model name, location), and the r2r config-from-* commands write it out, so run -c with the cruise TOML reports the receiver too |
[[nav]] |
name, primary, and the keys above |
Several position sensors, e.g. a POS MV and a Seapath: one [[nav]] table each, with its own format, files and [nav.device]. Each needs a name (letters, digits, _, -); one has primary = true, else the first is. run assesses each; the others' outputs are named <CRUISE>_<NAME>_…. R2R's cruise TOMLs have one feed |
[provenance] |
contact_institution, contact_email |
The reports' contact |
[geocsv] |
attribution |
GeoCSV attribution |
[thresholds] |
see below | QC and the QA checks |
[theme] |
see Themes | The PDF, figures and notebook |
[thresholds], with their defaults:
| Key | Default | Used by |
|---|---|---|
max_speed |
8.7 [m s-1] |
QA: a speed between successive fixes above this is unreasonable. Not a QC rule |
max_gap |
300.0 [s] |
QA: a step between fixes this long or longer is a gap |
min_completeness |
95.0 [%] |
Check: completeness passes at or above this |
max_longest_gap |
900.0 [s] |
Check: the longest gap passes at or below this |
max_out_of_sequence |
5.0 [%] |
Check: records out of sequence |
max_bad_quality |
5.0 [%] |
Check: records with a bad GGA quality indicator, of those reporting one |
max_unreasonable_speeds |
5.0 [%] |
Check: unreasonable speeds |
max_unreasonable_accelerations |
5.0 [%] |
Check: unreasonable accelerations |
land_tolerance |
3000.0 [m] |
Check "Over water": a fix on land farther than this from Natural Earth's 1:10m coasts and river centerlines is reported in the check's detail. Harbors the coastline draws as land (RR2212's Oakland inner harbor: up to 1,438 m) and rivers whose generalized centerline runs off a ship under way (RR1802 on the Willamette: up to 2,630 m) lie within it, counted as ports |
max_inland |
10000.0 [m] |
Check "Over water": a fix on land farther than this from water warns. Between land_tolerance and this, fixes are reported, not warned of: waterways the map lacks, e.g. Seattle's ship canal (RC0014: up to 3,929 m). A fix plainly ashore is kilometres to hundreds of kilometres inland |
max_accel |
1.0 [m s-2] |
QC and QA: the horizontal acceleration limit |
accel_time_base |
2.0 [s] |
QC and QA: acceleration at a fix uses velocities over at least this long before and after it, so GNSS noise at high sample rates isn't flagged |
max_reach_speed |
25.0 [m s-1] |
QC: a fix the ship couldn't have reached from the track at this speed (plus reach_tolerance) is flagged unreachable, e.g. a jump to near 0, 0 or minutes at a wrong position. Far above any ship's speed: not a speed limit |
reach_tolerance |
250.0 [m] |
QC: slack for the reachability rule; closer jumps are left to the acceleration rule |
gps_start, gps_end |
1994-03-01, 2034-12-31 |
QC: valid GPS dates |
date_margin_days |
30.0 [days] |
QC: allowed outside the cruise dates |
max_time_step_days |
30.0 [days] |
QC: the largest step forward in time |
min_quality, max_quality |
1, 5 |
QC and QA: valid GGA fix qualities (0 invalid, 6 dead reckoning, 7 manual, 8 simulator) |
min_satellites, max_satellites |
4, 50 |
QC: satellites used in the fix |
max_hdop |
50.0 |
QC: HDOP (0 is invalid too) |
max_altitude |
500.0 [m] |
QC: largest antenna altitude magnitude |
stale_speed |
0.25 [m s-1] |
QC: a repeated position is stale when the median of the preceding speeds is above this |
stale_window |
10 [speeds] |
QC: the speeds for that median |
The QC rules are described in src/nav_qa_processing/qc.py.
Parsing rules:
- An unknown key, in any table, is an error that says where a misplaced key or table belongs, or
the closest key to a misspelled one (
[thresholds] max_gpa: did you mean max_gap?). So is a value of the wrong shape: a value where a table belongs, or a table where a value does. - The keys of an R2R cruise TOML are accepted, so it works as a config too,
as is the site TOML's
[openvdm] output_extra_directory. [fileset] manifestand theme fonts and logo are relative to the TOML file;[nav] filesto the fileset.
The PDF, its figures and the notebook can take a house style: page size and margins, fonts,
text sizes, colors, a logo and the footer. Pass a theme file with --theme (run and r2r run), or set a [theme] table in the config, which applies over the file's settings.
configs/theme.example.toml shows every setting:
uv run nav-qa-processing run path/to/fileset -c my_config.toml --theme configs/theme.example.toml[theme] # in a config TOML
page_size = "A4"
logo = "logo.png" # relative to the TOML file
organization = "Example Oceanographic Institution"
palette = "colorblind" # Okabe-Ito colors, distinct with common color vision deficiencies
[theme.colors]
water = "#e6f0f7"| Key | Default | Description |
|---|---|---|
page_size |
letter |
letter or A4 |
margin |
0.6 [in] |
Page margins |
font, bold_font |
DejaVu Sans | TrueType (.ttf) fonts for the PDF and figures |
font_scale, figure_font_scale |
1.0 |
Text sizes in the PDF and in the figures |
logo |
none | PNG or JPEG beside the report title |
organization |
none | At the start of the footer |
footer |
the report title | Footer text |
palette |
default |
default or colorblind (Okabe-Ito); [colors] then changes single colors |
[colors] |
the palette's | #rrggbb colors: primary (the good track), secondary (flagged fixes, limits, the expected area), status_pass, status_warn, status_fail, status_info, status_na, table_header, table_grid, muted_text, footer_text, land, water, coast, rating_green, rating_yellow, rating_red |
A bad setting (an unknown key, a color that isn't #rrggbb, a missing font or logo) stops the
run before it reads anything, with a one-line message. QC flags are drawn in the palette's
distinct colors (matplotlib's tab10, or Okabe–Ito with colorblind). Fonts must be TrueType
(.ttf). The notebook re-creates the same theme, so figures re-run there match the PDF.
The r2r commands add R2R's six navigation tests, its rating and the R2R QA 2.0 XML, and write
the products with R2R's GeoCSV source values. They take a cruise TOML with R2R's IDs, which
comes from R2R's catalog:
uv run nav-qa-processing r2r fetch-fileset SP2613 169851 # into data/SP2613_169851_gnss/
uv run nav-qa-processing r2r config-from-catalog SP2613 169851 # configs/SP2613_169851.toml
uv run nav-qa-processing r2r run configs/SP2613_169851.toml data/SP2613_169851_gnss -o output/SP2613r2r run writes, by R2R's names:
| Output | Contents |
|---|---|
<CRUISE>_bestres.geoCSV, _1min.geoCSV, _control.geoCSV |
The products, as run writes them, with R2R's attribution, repository and DOIs |
<CRUISE>_<FILESET>_qa.2.0.<TIME>.xml |
R2R QA 2.0 report: R2R's structure, tests, bounds, infos and ranges, with its stylesheet reference. <TIME> is the run's, as in the products' creation_date, without its colons (2026-07-31T200556Z; R2R's names have them, which Windows doesn't allow) |
<CRUISE>_plots/, <CRUISE>_qa_report.pdf |
The figures and PDF, as run writes them, with R2R's certificate after the title block: R2R's fileset, vessel, ports, receiver and format, the rating as a G/Y/R banner in the theme's rating colors, each test's rating, result, G and Y bounds and detail, the infos, and R2R's ranges (NSV, HDOP, altitude, speed and acceleration over every record before QC, as the XML gives them). The PDF's subject is the rating, e.g. R2R rating G |
<CRUISE>_qa.ipynb |
The notebook, as run writes it, loading the cruise TOML (or, for a config built in Python, the cruise TOML written into it), then running R2R's tests, showing the certificate's tables, and writing the reports with the certificate and a new QA 2.0 XML, named by the notebook's run time |
run -c takes the same cruise TOML and ignores the R2R fields.
nav-qa-processing r2r run [-o OUTPUT_DIR] [--force] [--theme THEME_TOML] [--execute-notebook]
[--no-products] [--r2rnav] CRUISE_TOML FILESET_DIR
Runs the QA as run does, then R2R's six tests, and writes the products, figures, PDF with
R2R's certificate, notebook and QA 2.0 XML.
| Argument | Default | Description |
|---|---|---|
CRUISE_TOML |
required | An R2R cruise TOML, with the cruise ID and dates and [nav] format |
FILESET_DIR |
required | A BagIt bag or directory of GNSS files, as for run |
-o, --output OUTPUT_DIR |
output |
Directory for the products and report |
--force |
off | Replace existing products |
--theme THEME_TOML |
none | A theme file; the cruise TOML's [theme] applies on top |
--execute-notebook |
off | Run the notebook and save its outputs |
--no-products |
off | Don't write the GeoCSV products, as for run; the QA 2.0 XML, and with --r2rnav the r2rnav products, are written still |
--r2rnav |
off | Also write the products as legacy r2rnav, <CRUISE>_bestres|1min|control.r2rnav, the format NavManager wrote them in (format-r2rnav.txt: tab-separated, three // header lines, a flagged bestres record as # and time through antenna height). R2R itself now publishes GeoCSV |
It prints the R2R rating and each test's rating and result, then the core summary.
nav-qa-processing r2r config-from-catalog [-o OUTPUT_DIR] [--force] [--no-qa-report]
CRUISE_ID FILESET_ID
Writes an R2R cruise TOML from R2R's catalog: the cruise's name, chief scientist, vessel,
operator and scheduler, dates, ports, DOIs and navigation bounding box, and the GNSS receiver,
R2R format and nav format. It checks that FILESET_ID is one of the cruise's navigation filesets (GNSS, or an INS such as
Falkor (too)'s POS/MV), and
lists them if it isn't. R2R's API has no port coordinates, country or state, format alias or
catalog dates; those come from R2R's QA report for the fileset, when it has one with fileset
info (R2R's reports from 2024 and earlier don't).
| Argument | Default | Description |
|---|---|---|
CRUISE_ID |
required | R2R cruise ID, e.g. SP2613 |
FILESET_ID |
required | R2R's raw GNSS fileset ID, an integer, e.g. 169851 |
-o, --output OUTPUT_DIR |
configs |
Directory for <CRUISE_ID>_<FILESET_ID>.toml, or - for stdout |
--force |
off | Replace an existing TOML |
--no-qa-report |
off | Use the catalog API only |
It warns when this version has no reader for the fileset's R2R format; set [nav] format by
hand then.
nav-qa-processing r2r config-from-qa [-o OUTPUT_DIR] [--force] R2R_QA_XML
Writes an R2R cruise TOML from the provenance and fileset info of one of R2R's QA 2.0 reports. Use it to check this tool's results against R2R's.
| Argument | Default | Description |
|---|---|---|
R2R_QA_XML |
required | R2R's QA 2.0 XML for the fileset, e.g. the r2r_qa.2.0.xml that r2r fetch-fileset downloads |
-o, --output OUTPUT_DIR |
configs |
Directory for <CRUISE_ID>_<FILESET_ID>.toml, or - for stdout |
--force |
off | Replace an existing TOML |
nav-qa-processing r2r fetch-fileset [-o OUTPUT_DIR] [--force] CRUISE_ID FILESET_ID
Downloads a released R2R GNSS fileset (a BagIt bag) into
<OUTPUT_DIR>/<CRUISE_ID>_<FILESET_ID>_gnss/, with R2R's QA report beside it as
r2r_qa.2.0.xml and R2R's r2rnav product bag (the three .geoCSV files) in products/.
When R2R answers with an HTTP error (e.g. 503 while its service is down), the command says
which download failed in one line and exits 1. If the raw files arrived but the QA report or
products didn't, it says so: run it again with --force to fetch them.
| Argument | Default | Description |
|---|---|---|
CRUISE_ID |
required | R2R cruise ID |
FILESET_ID |
required | R2R's raw GNSS fileset ID, an integer |
-o, --output OUTPUT_DIR |
data |
Directory for the fileset's directory |
--force |
off | Download into an existing fileset directory |
An R2R cruise TOML is a config TOML with R2R's catalog details added. It must
have [cruise] id, depart_date and arrive_date. The r2r config-from-* commands write it;
this is tests/configs/SP2613_169851.toml:
[cruise]
id = "SP2613"
name = "MPL Internship Program Day at Sea"
pi = "Pereira, Filipe"
vessel_id = "32QU" # R2R's vessel ID: the ICES ship code
vessel_name = "Robert Gordon Sproul"
operator_id = "edu.ucsd.sio" # R2R organization IDs
scheduler_id = "org.unols"
date_issued = "2026-07-22" # R2R's catalog dates, as R2R writes them
date_modified = "2026-07-31T04:28:14Z"
depart_date = 2026-07-09 # required
arrive_date = 2026-07-09 # required
doi = "doi:10.7284/911454"
[cruise.extent]
westernmost = -117.3773103608
easternmost = -117.2262308092
southernmost = 32.5997440998
northernmost = 32.7053695673
[cruise.depart_port] # and [cruise.arrive_port]
name = "San Diego"
port_id = "100055" # R2R port ID
country_id = "us"
state_id = "ca"
longitude = -117.175
latitude = 32.71667
[cruise.arrive_port]
name = "San Diego"
port_id = "100055"
country_id = "us"
state_id = "ca"
longitude = -117.175
latitude = 32.71667
[fileset]
id = "169851" # R2R's raw GNSS fileset ID
doi = "doi:10.7284/169851"
date_created = "2026-07-22"
format_id = "100150" # R2R's format ID, and its alias
format_alias = "starc_raw"
[fileset.device] # the GNSS receiver, as R2R catalogs it
id = "101253"
make_id = "com.trimble"
model_id = "100542"
model_name = "BX992"
[nav]
format = "nav29"
[nav.device] # the receiver for the reports: R2R's device, written out
make = "com.trimble" # so `run -c` reports it too
model = "BX992"
[provenance]
distro_type = "post-cruise"
contact_institution = "Rolling Deck to Repository (R2R)"
contact_institution_id = "us.rvdata"
contact_email = "info@rvdata.us"
[geocsv]
attribution = "Rolling Deck to Repository (R2R) Program; http://www.rvdata.us/"The keys it adds to a config TOML, all written to the XML:
| Table | Key | Default | Description |
|---|---|---|---|
[cruise] |
id, depart_date, arrive_date |
required | id is made a valid file name as in a config TOML |
vessel_id |
blank | R2R's vessel ID, the ICES ship code (e.g. 32QU) |
|
operator_id, scheduler_id |
blank | R2R organization IDs (e.g. edu.ucsd.sio, org.unols) |
|
date_issued, date_modified |
blank | R2R's catalog dates for the cruise | |
[cruise.depart_port], [cruise.arrive_port] |
name, port_id, country_id, state_id, longitude, latitude |
blank | The ports, as in R2R's catalog |
[fileset] |
id |
blank | R2R's fileset ID. Without it, the report is named <CRUISE>_gnss |
date_created |
blank | R2R's catalog date for the fileset | |
format_id, format_alias |
blank | R2R's format ID and alias for the raw files | |
[fileset.device] |
id, make_id, model_id, model_name, location_name |
blank | The GNSS receiver, as R2R catalogs it |
[provenance] |
distro_type |
post-cruise |
R2R's distribution type |
contact_institution_id |
blank | The contact institution's R2R organization ID |
R2R's six tests, written to the XML with R2R's names and bounds:
| Test | G | Y | otherwise R |
|---|---|---|---|
PercentCompleteness |
≥ 95 % | ≥ 75 % | |
LongestGapEpoch |
≤ 900 s | ≤ 86,400 s | |
PercentRecordsOutOfSequence |
≤ 5 % | ≤ 10 % | |
PercentRecordsWithBadQualityIndicator |
≤ 5 % | ≤ 10 % | |
PercentUnreasonableSpeeds |
≤ 5 % | ≤ 10 % | |
PercentUnreasonableAccelerations |
≤ 5 % | ≤ 10 % |
The overall rating is G if every test is G, R if any is R, else Y. Each result is rated as
written in the report. How the statistics are computed, and where that differs from NavManager,
is in src/nav_qa_processing/stats.py, src/nav_qa_processing/integrations/r2r/rules.py and
docs/navmanager-differences.md.
The tests compare this tool with released R2R filesets: their pre-QC records, products, QA
statistics and QA 2.0 reports. Where the ratings differ, the cause is in
docs/navmanager-differences.md. SP2613, SP2503, BH19-30 and
RC0014 are committed in tests/data/ and unpacked into data/ by the tests, so CI downloads
nothing; fetch the others with r2r fetch-fileset (into data/). The tests that need one skip
without it and name the command.
| Fileset | R2R | This tool |
|---|---|---|
| SP2613 / 169851 (Sproul, one day) | G | G; every result equal; products equal by value |
| SP2503 / 167484 (Sproul) | R: completeness 47.34 % (R), gap 48,625.5 s (Y) | R, the same ratings |
| SP2424 / 161900 (Sproul, 150,925 fixes) | G | G; accelerations differ by definition |
| SP1925 / 134952 (Sproul, flagged fixes) | no QA 2.0 report | QC's flags compared with R2R's |
| RR2406 / 161075 (Revelle) | R on five tests, from sentences its logger split across lines | Y: the sentences rejoined, one 47-minute gap |
| RR2212 / 151339 (Revelle) | R: out of sequence 50.08 %, files processed out of order | G |
| SP2220 / 150139 (Sproul) | R: no fix for most of the cruise | R: bad quality 82.61 %, completeness, gap |
On the same filesets and machine, this tool takes 1.7 to 2.5 times less time than NavManager and 2.8 to 11 times more memory: NavManager streams through fixed buffers, and this tool holds the whole cruise, about 190 bytes a fix. A 20-day cruise at 2 Hz (3.5 M fixes) takes NavManager 8.9 minutes in 279 MB, and this tool 3.6 minutes in 786 MB. The measurements, why, and how to repeat them are in docs/performance.md.
On a ship running OpenVDM, the QA runs from OpenVDM's hooks through the cruise:
- During the cruise, after each transfer of the collection system that logs the GNSS
(
postCollectionSystemTransfer), at most every--min-interval: a failing receiver or a missing feed shows while there's time to fix it. - When the cruise is finalized (
preFinalizeCurrentCruise): the final reports, on the whole cruise. OpenVDM waits for them, so they're in the finalized cruise. - Afterwards, by hand: any earlier cruise, from the
cruise_config.jsonin its directory (A past cruise).
Each run writes the core products and reports (GeoCSV, SVG plots, PDF, notebook), not R2R's,
into an OpenVDM extra directory. Setting it up (the install, the extra directory, the ship's
site TOML and the hooks in openvdm.yaml) is in
INSTALL.md.
- The cruise, from OpenVDM's API (
api/warehouse/getCruiseConfig): its ID, name, PI, location, dates and extent, the warehouse directory, the transfer's destination, the extra directories and the MD5 summary. For a past cruise, from itscruise_config.jsoninstead. The current cruise comes from the API rather than itscruise_config.jsonfor two reasons: the hook sets the cruise extent through the API, after asking it which cruise is current, and OpenVDM rewrites the file only at setup, when the cruise is edited, on an export and at finalize, so an extra directory or transfer added mid-cruise isn't in it yet. - The vessel's name and contact, from the site TOML, or where it leaves them blank, from
openvdm.yaml'svesselblock (OpenVDM 2.17). For a past cruise, from itscruise_config.json, the vessel it was on. - The GNSS feed: OpenVDM doesn't know which of a collection system's files are the GNSS
feed, or their format, so the ship's site TOML says (
[nav] formatandfiles). A ship logging several position sensors lists each as a[[nav]]table.
- Assesses each feed of the transfer: the site TOML's
[nav], or each[[nav]]whosetransferis this one or has none, the primary first. A non-primary feed's outputs carry its name (<CRUISE>_seapath_…). - Skips a transfer without new or updated files matching a feed's
files(--changed-files), and with--min-intervala feed whose reports are newer than that. - Reads the cruise's nav files from the transfer's directory, within the cruise's dates (an open-ended cruise ends today, UTC), and checks their checksums against OpenVDM's MD5 summary. Files changed after the summary was written count as pending, not failures.
- Writes into the extra directory, replacing earlier ones: the GeoCSV products, the plots,
the PDF, the notebook, and
<CRUISE>_config.toml, a snapshot of what OpenVDM and the site TOML supplied, which the notebook re-runs from. Run as root, it gives them to the warehouse user. - Adds those files to the cruise's MD5 summary, with OpenVDM's
utils/update_md5_summary.py(OpenVDM 2.16.1 or later). - Sets OpenVDM's cruise extent (
cruiseExtent, OpenVDM 2.17) to the box around the primary feed's fixes that pass QC, so a jump to 0, 0 never stretches it. Its "Within the expected area" check is then n/a, and other feeds are checked against the primary's track. With--no-set-extent, without aworkerApiKey, for a past cruise or before OpenVDM 2.17, it checks the fixes against OpenVDM's extent instead, else the site TOML's[extent]. INSTALL.md covers the setup it needs. - Fails in one line, which OpenVDM shows as the hook's message. One feed's failure doesn't stop the others. Updating the MD5 summary or the extent can fail without failing the hook: it logs a warning.
OpenVDM logs the hook's output in /var/log/openvdm/post_hooks.log. To see it directly, run the
hook's command by hand, as root, without --changed-files or --min-interval
(INSTALL.md step 5). Each run prints the cruise and feed it found, what it
assumed (note: lines), the QA summary and the files it wrote.
| Message | Meaning |
|---|---|
no new or updated nav files; nothing to do |
None of the transfer's files match a feed's files glob. By hand, leave --changed-files out |
its reports, from ..., are newer than --min-interval 6h; nothing to do |
The last run was within the interval, so its reports stand. By hand, leave --min-interval out |
... doesn't exist yet; nothing to do |
The transfer hasn't brought any files yet |
0 fixes ... from 0 files |
The files glob matches nothing in the transfer's directory: check it with ls there |
every nav file is outside the cruise window |
The files' dates aren't within the cruise's start and end in OpenVDM. OpenVDM's sample data is from 2012-04-28 and 29 |
no nav format ...: set [nav] format in the site config |
The site TOML (--site-config) has no feed format |
no site TOML at ... |
Neither --site-config nor the install's configs/site.toml: write it with site-config (INSTALL.md step 3). config-from-openvdm runs without one, to show what's missing |
no collection system transfer named ... |
TRANSFER isn't a transfer's name in OpenVDM (Configuration > Collection System Transfers) |
no extra directory named 'NAV_QA' |
Add it in OpenVDM (Configuration > Extra Directories), or set the site TOML's [openvdm] output_extra_directory |
no openvdm.yaml at ... |
OpenVDM is installed elsewhere: give --openvdm-config |
the reports aren't in OpenVDM's MD5 summary: ... |
OpenVDM's update_md5_summary.py is missing (OpenVDM before 2.16.1) or failed; for a past cruise, OpenVDM's warehouse doesn't have it. The reports are written all the same |
OpenVDM's cruise extent wasn't set: ... |
OpenVDM refused it (its WORKER_API_KEY doesn't match workerApiKey), is older than 2.17, or moved on to another cruise during the run |
note: openvdm.yaml has no workerApiKey ... |
The hook can't set the extent, so it checks the fixes against OpenVDM's instead |
nav-qa-processing openvdm [--openvdm-config OPENVDM_YAML] [--cruise-config CRUISE_CONFIG_JSON]
[--site-config SITE_TOML] [--no-set-extent]
[--changed-files FILES] [--theme THEME_TOML]
[--execute-notebook] [--no-products] [--min-interval DURATION]
TRANSFER
The hook: assesses the collection system transfer's nav files for OpenVDM's current cruise, or
with --cruise-config any cruise, as above. Installed openvdm.yaml
files call it, so its arguments keep their names; they're named as in sbe-qa-processing's hook.
| Argument | Default | Description |
|---|---|---|
TRANSFER |
required | The collection system transfer's name; in a hook, {collectionSystemTransferName} |
--openvdm-config OPENVDM_YAML |
/opt/openvdm/server/etc/openvdm.yaml |
OpenVDM's settings: its web root (siteRoot), the workerApiKey and, from OpenVDM 2.17, the vessel block. Needs the openvdm extra. With --cruise-config, only where OpenVDM is installed, for the MD5 summary |
--cruise-config CRUISE_CONFIG_JSON |
none | A cruise's cruise_config.json, which OpenVDM writes into each cruise's directory: assess that cruise, past or current, without OpenVDM's API. A past cruise says how |
--site-config SITE_TOML |
the install's configs/site.toml (the hook needs one) |
The ship's site TOML; the hook needs its [nav] format |
--no-set-extent |
off | Don't set OpenVDM's cruiseExtent from the fixes that pass QC; check the fixes against it (else the site TOML's [extent]) instead |
--changed-files FILES |
none | OpenVDM's {newFiles} and {updatedFiles}; repeatable. When given, the QA runs only if they include nav files ([nav] files, or any file without it) |
--theme THEME_TOML |
none | A theme file; the site TOML's [theme] applies on top |
--execute-notebook |
off | Run the notebook and save its outputs |
--no-products |
off | Don't write the GeoCSV products into the extra directory, as for run. bestres has a line per fix and grows through the cruise (about 300 MB for 20 days at 2 Hz), and the hook rewrites it after every transfer |
--min-interval DURATION |
none | Skip a feed whose reports are newer than this: a number and s, m, h or d, e.g. 6h. Each run assesses the whole cruise so far, so a long cruise's take longer than OpenVDM's transfer interval. Leave it out of a preFinalizeCurrentCruise hook, which writes the final reports. Choosing it |
nav-qa-processing config-from-openvdm [--openvdm-config OPENVDM_YAML]
[--cruise-config CRUISE_CONFIG_JSON]
[--site-config SITE_TOML] [--no-set-extent]
[-o OUTPUT_DIR] [--force] TRANSFER
Builds the config TOML the hook would use, from OpenVDM's current cruise (or with
--cruise-config any cruise) and the site TOML, without running the QA, and reports what's
missing or assumed. TRANSFER, --openvdm-config, --cruise-config, --site-config and
--no-set-extent are as for openvdm: when the hook would
set the cruise extent, the config has no expected area. To review it, or run the QA by hand:
uv run nav-qa-processing config-from-openvdm OpenRVDAS --site-config configs/site.toml # configs/<CRUISE>.toml
uv run nav-qa-processing run /path/to/cruise/OpenRVDAS -c configs/<CRUISE>.toml -o output/<CRUISE>| Argument | Default | Description |
|---|---|---|
-o, --output OUTPUT_DIR |
configs |
Directory for <CRUISE_ID>.toml (and <CRUISE_ID>_<NAME>.toml for each other feed of the transfer), or - for stdout with one feed |
--force |
off | Replace an existing TOML |
OpenVDM's API knows only the current cruise, but OpenVDM writes each cruise's config into its
directory as cruise_config.json (its default name, set by CRUISE_CONFIG_FN in OpenVDM's
www/app/Core/Config.php): its ID, name, PI, location, dates and extent, the transfers' and
extra directories' destinations, the MD5 summary's name, and the vessel it was on. Give it with
--cruise-config, and the hook or config-from-openvdm take the cruise from it instead of the
API, with the site TOML as for the current cruise:
# Rebuild a past cruise's reports in its NAV_QA extra directory, as the hook would have
sudo /opt/nav-qa-processing/.venv/bin/nav-qa-processing openvdm OpenRVDAS \
--cruise-config /data/CruiseData/SP2613/cruise_config.json \
--site-config /opt/nav-qa-processing/configs/site.toml
# Or keep it as a config TOML, and run the QA anywhere
nav-qa-processing config-from-openvdm OpenRVDAS --site-config configs/site.toml \
--cruise-config /data/CruiseData/SP2613/cruise_config.json -o configs # configs/SP2613.toml
nav-qa-processing run /data/CruiseData/SP2613/OpenRVDAS -c configs/SP2613.toml -o output/SP2613- The cruise's directory is the JSON's, wherever the cruise now is: an archived or copied cruise works, as long as its directory keeps the cruise's ID as its name, as OpenVDM names it.
- OpenVDM's cruise extent isn't set: it's the current cruise's. The fixes are checked against the JSON's extent.
- The reports go into the cruise's MD5 summary where OpenVDM is installed and still has the cruise in its warehouse: they're given to OpenVDM by their full paths, so it refuses files outside its directory for the cruise. Otherwise, or without OpenVDM, the hook warns that the reports aren't in the summary and still succeeds.
- The reports belong to the owner of the cruise's directory, run as root.
- A cruise the hook ran on also has
<CRUISE>_config.tomlbeside its reports, the config as of that run, whichrun -ctakes as it is.
nav-qa-processing site-config [-o SITE_TOML] [--openvdm-config OPENVDM_YAML] [--force]
Writes the site TOML by prompting, on stderr, for each field OpenVDM doesn't
store: the vessel name and report contact (their defaults the site TOML's, else those in
openvdm.yaml's vessel block), the GNSS feed's format (one with a reader; the
prompt lists them), files glob, device (make, model, serial number, location) and transfer, any
the vessel's top speed in m s-1 ([thresholds] max_speed, default 8.7), other position
sensors (a name each, then the same; - drops one), the GeoCSV attribution, the extra directory and an expected
cruise area. Enter keeps the [default] and - clears it. It works offline. Over an existing
file (with --force) its values are the defaults, and at the end of input every remaining
field keeps its default, so with no input the file is written back unchanged. The other
[thresholds] and [theme] are kept as they are.
| Argument | Default | Description |
|---|---|---|
-o, --output SITE_TOML |
the install's configs/site.toml, where the hook reads it |
The site TOML to write, or - for stdout |
--openvdm-config OPENVDM_YAML |
/opt/openvdm/server/etc/openvdm.yaml, when it exists |
From OpenVDM 2.17, its vessel block's name and contact are the prompts' defaults where the site TOML has none, and are written into it. Without the file, the defaults are the site TOML's, or blank |
--force |
off | Replace an existing site TOML, starting from its values |
The per-ship settings OpenVDM doesn't store. Only [nav] format is required;
configs/site.example.toml shows every key. Any other key is an error, as in a config TOML.
A site TOML describes the ship, so it has a [vessel] table and no [cruise]: the hook gets
each cruise's ID, name, chief scientist, location and dates from OpenVDM, and writes them with
the site's settings into a config TOML for the cruise, <CRUISE>_config.toml, where the
vessel's name is [cruise] vessel_name. A [cruise] table here, or a [vessel] one in a config
TOML, is an error that says so.
| Table | Key | Default | Description |
|---|---|---|---|
[vessel] |
name |
blank | The vessel's name, for the report header. Wins over openvdm.yaml's vessel.name (OpenVDM 2.17), which fills it when blank |
[nav] |
format |
required | The GNSS feed's format ID (nav-qa-processing formats), e.g. nav33 for OpenRVDAS's ISO timestamp + GGA |
[nav] |
files |
every file | A glob of the GNSS feed's files within the collection system's directory, e.g. raw/POSMV_GGA-*.txt. Also what --changed-files is matched against |
[nav.device] |
make, model, serial_number, location |
blank | The receiver or INS, for the reports, as in a config TOML |
[[nav]] |
name, primary, transfer, and the keys above |
one feed | Several position sensors, as in a config TOML. transfer names the collection system transfer a feed is in; the hook runs the feeds of its TRANSFER and those without one |
[extent] |
westernmost, easternmost, southernmost, northernmost |
none | The expected cruise area, used when the hook doesn't set OpenVDM's cruiseExtent and OpenVDM has none. westernmost > easternmost crosses the antimeridian |
[openvdm] |
output_extra_directory |
NAV_QA |
The OpenVDM extra directory the reports go into |
[provenance] |
contact_institution, contact_email |
blank | The report contact. Wins over openvdm.yaml's vessel.contact (OpenVDM 2.17), which fills them when blank |
[geocsv] |
attribution |
blank | The GeoCSV products' attribution |
[thresholds] |
as in a config TOML | defaults | QC and QA limits. max_speed, the vessel's top speed [m s-1] (default 8.7), is always written: a step faster than it counts as an unreasonable speed |
[theme] |
as in a config TOML | none | The reports' look; paths relative to the site TOML |
uv run pytest -n auto --dist loadgroup # all tests, on every core
uv run pytest tests/test_cli.py::test_version # one test
uv run ruff check . && uv run ruff format --check .CI (.github/workflows/ci.yml) runs Ruff and the tests on Linux (Python 3.11 to 3.15, and
3.11.3, before tarfile's "data" filter, like Debian 12's 3.11.2), macOS and Windows on every pull
request and push to main. It downloads nothing: the tests unpack the reference filesets
committed in tests/data/ and draw on Natural Earth clipped to their areas.
The tests never download map data either; with fetch-map-data run, the tests marked map_data
use the full layers.
tests/test_openvdm_contract.py checks the OpenVDM hook against OpenVDM's own code rather than
stand-ins: how OpenVDM runs the hook commands INSTALL.md documents, its
update_md5_summary.py, and the cruise config file it writes. It needs a checkout of OpenVDM's
2.17 branch with a venv of its requirements, at OPENVDM_SOURCE (default ../openvdm), and
skips without one; CI's "OpenVDM contract" job clones it:
git clone --branch 2.17 https://github.com/OceanDataTools/openvdm.git ../openvdm
uv venv --python 3.12 ../openvdm/venv && VIRTUAL_ENV=../openvdm/venv uv pip install -r ../openvdm/requirements.txt
uv run pytest tests/test_openvdm_contract.py