This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. * **rugnux: significantly better quality of results, and faster.** A large rework of integration, scaling, merging, geometry refinement and space-group determination, together with measurements the program previously made no attempt at - the direct beam before indexing, the beam stop, the goniometer rotation scale, and the stretches of a sweep the crystal did not deliver. A rotation dataset typically gains observations at better <I/sigma> and R_meas, and every `mx` and `scale` run writes a `<prefix>_report.txt` results report modelled on XDS's `CORRECT.LP`. Many defaults moved with it: spot detection is self-calibrating, beam-stop detection and rotation geometry post-refinement are on, resolution limits default to as far as the detector reaches, and ice-ring handling engages only where the crystal is measured to have ice. * **jfjoch_viewer:** the beam-stop shadow, the detector calibration and the beam-centre measurement are reachable from "Analyze dataset"; the settings panel reports how the sample moved and how polarized the beam was; image rendering and interaction are faster. * **Performance:** bitshuffle+LZ4 images are decoded on the GPU rather than on the host, with the bitshuffle inverse fused into preprocessing so the decompressed frame is never held in device memory. * **Broker, writer, packaging and build:** image-slot lifetime and locking fixes, per-image datasets sized by the images actually written, the Debian/Ubuntu broker package renamed to `jfjoch`, and `image_analysis` compiling under MSVC again. **Breaking change to the rugnux command line:** * `--azint-only` and `--scale` are **removed**, replaced by `--mode azint` and `--mode scale`; the full pipeline is `--mode mx` and remains the default. A script passing the old flags now fails with the list of valid modes rather than silently running the wrong one. * `-t`/`--stride` is **refused on rotation data**: skipping frames cuts every reflection's rocking curve, so the combined fulls and their partiality would be measured over frames the sweep never recorded. Select a contiguous range with `-s`/`-e` instead. `--mode azint` and `--force-still` still take a stride. **Breaking changes to OpenAPI** - regenerate the client (`jfjoch-client` 1.0.0-rc.161, `frontend/src/client`) or read the affected fields as optional: * `image_scale_b` is removed from the `plot_type` enum, so a client requesting that plot now gets an error rather than a curve. * `azim_int_settings.high_q_recipA`, `spot_finding_settings.high_resolution_limit` and `spot_finding_settings.low_resolution_limit` are no longer `required`. All three mean "no limit at that end" when unset and are omitted from the response instead of carrying a placeholder value, which raises in a client generated from an rc.160-or-earlier spec. A value of 0 is still accepted and means the same thing. **Breaking changes to the stored formats** - a consumer reading these fields must treat them as optional: * The per-image image-scale B factor is no longer computed, so `/entry/MX/imageScaleBFactor` is absent from newly written HDF5 files and the corresponding key is absent from the CBOR DataMessage and END blocks. Files written by rc.160 and earlier still contain it and still open; nothing in the pipeline reads it any more. * `_reflns.jfjoch_diffrn_ISa` now carries the whole-range `1/sqrt(a*b)` that XDS's ISa denotes, and the error-model `a` and `b` are reported in XDS's convention; the strong-reflection asymptote moves to `_reflns.jfjoch_diffrn_ISa_asymptotic`. **A file written by an earlier version carries the asymptote under the plain `ISa` name.** Reviewed-on: #71 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
7.1 KiB
Tests
The unit and integration tests are written with Catch2 and
collected into a single binary, tests/jfjoch_test. Build and run it with:
make -j$(nproc) jfjoch_test
cd tests
./jfjoch_test # everything
./jfjoch_test "<test name>" # one test case
./jfjoch_test "[tag]" # by tag
There are also benchmark and hardware routines, each printing its own usage:
jfjoch_hdf5_testto measure HDF5 dataset writing speed (single threaded). It doubles as the generator of the HDF5 files used by the external-software tests below.jfjoch_lite_perf_testto measure the CPU/GPU ("lite") analysis path - indexing, integration and optional file writing.jfjoch_fpga_testto test quality/performance of FPGA card(s) and software routines. With-Hit runs the high-level-synthesis C model on the CPU, so no FPGA device is needed.
Out-of-space handling is covered separately by jfjoch_hdf5_enospc_test, run under the enospc_shim
LD_PRELOAD module that makes writes fail with ENOSPC.
In addition, tests are executed to verify that datasets written by Jungfraujoch are readable by
other MX software (see Integration with MX data processing software) -
XDS through the Jungfraujoch, Durin and Neggia plugins, and DIALS xia2.ssx - for each of the
NXmx layouts. Input files for these programs are placed in the tests/xds, tests/xds_durin,
tests/xds_neggia and tests/crystfel folders. See .gitea/workflows/build_and_test.yml for the
exact commands; the CrystFEL fixtures are run by hand rather than in the pipeline.
Judging a change to the analysis itself
Two harnesses in the repository root run rugnux over a directory of stored datasets and score
the result. Neither is part of CI - run them when a change plausibly moves merged results, not as
a reflex. Both take their dataset list from outside the repository, because dataset and sample
identities are not committed.
rugnux_vs_xds.py- the rotation battery. Runs rugnux de novo over every crystal under a data root and tabulates reflections, observations, space group, R_meas, CC1/2, ISa and wall-clock time against the XDSCORRECT.LPbeside each dataset.rugnux_anomalous.py- the anomalous-peak-height arbiter, below.
The anomalous-peak-height arbiter
A change that touches partiality - a mosaicity estimator, a rocking-curve model, a background change, anything that alters how partial reflections are weighted - cannot be judged by the statistics we normally reach for:
| statistic | why it fails for this class of change |
|---|---|
ISa, R_meas, error-model b |
one measurement, not three; dominated by the low-resolution shells; not invariant to the uniform intensity rescale a partiality change produces |
| last-shell R_meas | moves with its denominator, i.e. the wrong way by construction |
rugnux --model R-free |
tracks its own zero-information floor, which moves ~22x more than R-free itself over the same sweep |
per-shell agreement with XDS_ASCII.HKL |
XDS never divides by partiality, so "divide less" moves us toward it mechanically; measured to put the optimum ~1.4x too low |
Anomalous difference density at known scatterer sites has none of these problems. It is read in units of the map's own sigma, so a uniform intensity rescale cancels exactly, and it is referenced to the structure rather than to another program's partiality model.
rugnux_anomalous.py measures it: shelxc + anode -a (CCP4) on each arm's merged reflections,
against a model that is placed once and then held fixed. It reports, per dataset, the mean site
height and the off-site noise floor, and, between arms, the paired per-site change.
# compare two arms (each a directory of <id>/<id>.hkl + .mtz)
./rugnux_anomalous.py --config <table>.json base=<dir-A> test=<dir-B>
# a parameter scan: numeric labels turn the arms into a curve with a per-dataset optimum
./rugnux_anomalous.py --config <table>.json \
0.85='<scan>/{name}/s0p85.hkl' 1.00='<scan>/{name}/s1.hkl' 1.20='<scan>/{name}/s1p2.hkl'
An arm is a rugnux output directory or a path template containing {name}. --place does the
one-off model placement, --write-config-template prints the config skeleton, and ANODE results
are cached under the config's workdir (a full 9-dataset x 11-arm scan takes under a minute).
The gate. A dataset counts only if its reference arm shows top peak > 1.5x the highest
off-site peak and at least 3 sites over 5 sigma. A dataset that fails is reported as
EXCLUDED, never as a zero - the difference between two noise measurements is not a measurement.
Standing dataset set (2026-08): 8 datasets from 7 crystals, 114 sulfur sites, all judged on native sulfur signal.
| crystals | space group | photon energy | sites each |
|---|---|---|---|
| 2 | P41212 | 12.4, 16.0 keV | 18 |
| 2 (lysozyme) | P43212 | 13.0, 5.0 keV | 27 |
| 3 (4 datasets - one crystal contributes two energies) | cubic, I-centred | 13.0, 6.0, 5.0, 5.0 keV | 6 |
Report n as crystals, not datasets: two energies of one crystal are not two independent votes,
and the tool prints both counts for that reason.
Traps this tool exists to encapsulate. Every one of them has already cost a working day:
- The phasing space group comes from the config, never from the merged file. I23 and
I213 have identical systematic absences (I-centring already forces the screw
condition), so no data can separate them, and phaser's automatic space-group test only tries
the enantiomorph - which for I23 is itself. Phasing an I-centred cubic case in the I23 that
both rugnux and XDS report gives TFZ 7-11 where the other member gives 30-50, and drops the mean
site height by a factor 3-10 - enough to make four good datasets look signal-free. Thirteen
classes of chiral space group are indistinguishable this way;
--placetries every member of the class and reports each one's LLG/TFZ. - Place the model once, from a reference arm, and reuse it unchanged. Re-phasing per arm lets the model move and contaminates the comparison. Refining the placed model against the dataset's own amplitudes is allowed (it lifts the peaks another 4-10%) as long as the same refined model is then used for every arm.
- The gate and the measurement must use the same model. Gating on one model and scoring the curve with another silently changes which datasets are in the set.
- The off-site floor skips special positions. A peak on the cell origin is a ripple of the
calculated phases, not a sample of the background; leaving it in inflates the floor by several
sigma and can turn a passing dataset into a failing one. Such peaks are reported in their own
speccolumn rather than dropped silently.
Reading the result. Judge the paired per-site change, with its standard error, pooled over
crystals. A per-dataset optimum whose arm does not beat the reference on the paired test is
flagged not significant vs ref and must not be quoted as a preference; so must one sitting on the
edge of the scanned grid (grid edge) - extend the grid instead.