# Measuring Rugnux: validation and timing This page is for anyone who has to say how well, or how fast, `rugnux` works on a machine: a colleague with a new GPU server, a laptop, a Mac, an ARM box, or an agent running there on someone's behalf. It explains the two measurements the project uses, how to run them, how to read what they print, and how to take timings that can be compared with someone else's. `tools/battery/README.md` in the source tree is the detailed guide to the runner itself: every option, the manifests, the scoring rules and the `results.json` schema. This page summarises it and does not replace it. ```{contents} On this page :local: :depth: 2 ``` ## Two measurements, one runner Everything goes through one script, `tools/battery/battery.py`. Do not write your own loop over datasets: numbers made by ad-hoc scripts have never been comparable with each other, which is why the old ones were retired. - **The validation battery** (`battery.py run`) processes a fixed list of rotation datasets and scores each answer against an independent reference. It answers *is the result right, and as good as the reference*. A full run takes hours. - **The timing benchmark** (`battery.py timing`) processes four sets, each started from a model that has to be placed by molecular replacement, so one short run exercises the whole pipeline and also checks that the answers are right. It answers *how fast, on this machine*. It takes minutes, and needs nothing on the machine but four open datasets. The two are kept apart on purpose. A battery run reads hundreds of gigabytes once, so its wall clock mostly measures the disk and the page cache (see [the page-cache trap](#the-page-cache)). Use the battery to judge correctness and the timing benchmark, run under controlled conditions, to judge speed. ### Setting up a machine The runner needs Python 3.9 or newer with `gemmi` and `numpy` (`pip install gemmi numpy`), a `rugnux` binary, the datasets, and a **site config**: a small JSON file naming this machine's data roots, where runs go, the GPU lock and the persisted baselines. `tools/battery/site.example.json` is the template. The runner looks for the config in `--site PATH`, then in `$JFJOCH_BATTERY_SITE`, then in `tools/battery/site.json`, which git ignores. `--site` is an option of the script itself, so it goes **before** the subcommand: ``` python3 tools/battery/battery.py --site /path/to/site.json run --rugnux build/rugnux/rugnux --tier smoke ``` A relative path in the site config (data roots, runs root, lock file, caches) is taken relative to the site config's own directory, so a config shipped beside its data works wherever it is unpacked. A `pdb_cache` copied from another machine can be used as it is: the coordinates are looked up where the cache now is, not where its `.json` files say they were first fetched. The data layout (`//...`) and where to get each arm's data are in the README's *Getting the data*. The open arm is public (each dataset's DOI is in [External test data](EXTERNAL_TEST_DATA.md)); the in-house data come from the maintainers. The timing benchmark needs only four open dataset directories and the smallest site config there is ([below](#what-a-machine-needs)); models, references and expected answers are all in the repository. ## The validation battery ### Arms and references The battery has three **arms**, each scored against a reference that does not come from `rugnux`: | arm | datasets | scored against | |---|---|---| | open | public PDB depositions of raw diffraction data, plus a few published small-molecule sets | the deposition: its space group, cell and resolution | | inhouse | standard test crystals (lysozyme, thaumatin, insulin, cytochrome C, myoglobin), small-molecule standards, and no-crystal controls | XDS, from the `CORRECT.LP` stored beside each dataset | | private | user data | XDS, as in-house | Each set runs **once**, with one command per arm. The open arm runs `rugnux -o p --no-export-unmerged --model `. The XDS arms run `rugnux -o p --no-export-unmerged [-A] --report-resolution , [--model ] `, with `-A` where XDS merged with Friedel's law off. Neither `--model` nor `--report-resolution` changes the processing: the merged reflections, the resolution cut and every decision are the same with or without them (this has been checked bit for bit). So one run gives both what a user gets and the extra report sections that compare it with the reference. ### Verdicts Every set gets one verdict. Scoring asks four questions in order, and the first that fails decides: did it run, is the lattice right, is the symmetry right, and, on the XDS arms, is the merge as good as XDS's? - **pass**: the lattice matches the reference (Niggli-reduced primitive cells: volume ratio 0.95 to 1.05, reduced edges within 2%) and so does the symmetry. On the open arm the space group is compared; on the XDS arms only the point group, because XDS never tests a screw axis. A group that the intensities cannot tell apart from the reference (same Laue class, same systematic absences) passes as `indistinguishable_alternative`. A row whose manifest lists a second defensible answer can pass as `accepted_alternative`. The report counts both kinds separately. - **fail**, with a `cause`: wrong lattice (`lattice_halved`, `lattice_doubled`, ...), wrong symmetry (`sym_under`, `sym_over`, `sym_screw`, ...), a reader or indexing failure, a timeout. - **crash**: rugnux was killed by a signal, its log shows `Processing failed`, a GPU fault, an uncaught exception, or a non-zero sanitizer summary. A crash fails the set whatever its report says. - **unscored**: a question the data cannot answer. Either there is no reference, or the hand of an enantiomorphic pair is in question, or a screw axis lies on a row the sweep never measured (and rugnux said so). This is not a pass, but it is not counted as a failure either. - **not_run**: the input is missing. A no-crystal control passes only when rugnux reports no lattice. **Data quality is reported, not scored**: resolution, CC1/2, R_meas, ISa, completeness and the model R-factors are shown next to the reference's values for a human to judge. ### The like-for-like table On the XDS arms, rugnux keeps its own resolution cut and the reference does not change it. `--report-resolution` adds a second statistics table (the `REFRES_*` keys of the report) made from the same merge but binned over XDS's own range. That gives two separate answers. The own numbers say what a user gets. The reference-range numbers say whether rugnux's merge is better than XDS's over the same reflections. Where rugnux's cut is coarser than XDS's, the shells past it were never merged, so the completeness there measures **coverage**, not a drop in quality. The report marks those rows. On the open arm the deposition gives only a space group, a cell and a resolution, so the data quality is judged from outside: R-free of the deposited model, from `rugnux --model` (a trend number, see below) and from the REFMAC check, and a per-shell correlation of the merge with the model beside the depositor's own data (always on, needs no CCP4). ### Running it ``` B="python3 tools/battery/battery.py --site /path/to/site.json" R=build/rugnux/rugnux $B run --rugnux $R --only 5reo,lyso_ref # named sets $B run --rugnux $R --tier smoke # readers, symmetry classes, known hard cases $B run --rugnux $R --tier ci # the routine regression check CI runs on every push $B run --rugnux $R # the full battery: open + inhouse $B run --rugnux $R --gpulock --model-check # queued behind the site's GPU lock, with REFMAC ``` `--threads N` passes `-N` to rugnux, `--extra ""` adds rugnux arguments to every set, `--label` names the run, and `--baseline RUN|none` picks the run the report is compared against. `--gpulock` puts every rugnux behind the command the site config names as `gpulock` (at PSI, a `flock` wrapper shared by every GPU job on the machine). A run made that way is marked "timing NOT a reference", and so is a run that found another process on the GPU before a set. `run` exits with status 1 when any set crashed or a ci-tier set failed. Other failures and every metric are reported, not judged. ### The run directory Each run gets a new directory `/__