Files
Jungfraujoch/tests/RotationScaleWalkTest.cpp
T
leonarski_f 84228bf8be
Build Packages / Create release (push) Successful in 24s
Build Packages / build:viewer:macos-arm64:nocuda (push) Successful in 3m29s
Build Packages / build:rugnux:macos-arm64:nocuda (push) Successful in 2m43s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m27s
Build Packages / build:rugnux:linux-x86_64:cuda (push) Successful in 9m53s
Build Packages / build:viewer:linux-x86_64:nocuda (push) Successful in 9m58s
Build Packages / build:viewer:linux-x86_64:cuda (push) Successful in 11m22s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 13m39s
Build Packages / build:viewer:windows-x86_64:nocuda (push) Successful in 18m37s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 16m32s
Build Packages / build:viewer:windows-x86_64:cuda (push) Successful in 24m11s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m3s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m23s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m41s
Build Packages / Generate python client (push) Successful in 50s
Build Packages / Build documentation (push) Successful in 1m16s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m0s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 18m38s
Build Packages / build:rugnux:windows-x86_64:cuda (push) Successful in 14m33s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 17m55s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m50s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 18m38s
Build Packages / Unit tests (push) Successful in 1h46m14s
v1.0.0-rc.173 (#83)
* jfjoch_broker: Optional per-dataset authentication - statistics, images and plots can require a bearer token, which jfjoch_viewer supports.
* jfjoch_viewer: Dark mode and a theme-matched colour scheme, a magnifier panel, and simpler contrast and background controls.
* Rugnux: Multiple performance improvements on GPU and CPU (CPU-only processing up to 40% faster, faster image decoding on ARM), with unchanged results.
* Rugnux: `--model` rigid-body refinement runs on the GPU, and the model-validation check is faster and more reliable.
* Rugnux: Improved scaling and merging - error model, outlier rejection, absorption correction and French-Wilson amplitudes now agree more closely with XDS and ctruncate.
* Rugnux: Improved integration - radial background on powder and ice rings, crowded rotation data keep their reflections, and CPU-only builds integrate large unit cells as GPU builds do.
* Rugnux: More robust detector geometry - measured beam centre, X-ray bandwidth and goniometer rate, and geometry refinement accepted only on significant evidence.
* Rugnux: Merged files are written in the standard setting, or in the setting of a reference MTZ, structure-factor mmCIF or model, with its free-R flags.
* Rugnux: Richer report - ice and powder rings, further lattices, superstructure candidates and mosaicity, with warnings worded as prompts to check.
* Rugnux: Clear error messages when a data set needs more GPU or host memory than is available.

Reviewed-on: #83
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-29 15:57:32 +02:00

95 lines
4.1 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <cmath>
#include <catch2/catch_all.hpp>
#include "../rugnux/Rugnux.h"
namespace {
// A synthetic sweep whose stage turned `true_scale` times the stored angles. Scored at a scale k,
// the validation spots stay on the lattice as long as the angles track the rotation, and the share
// that does falls off with the relative rate error; a wrong spindle angle keeps half a percent.
struct SyntheticSweep {
double true_scale;
int64_t spots = 35000;
int index_calls = 0;
int refit_calls = 0;
ValidationSpotEvidence IndexAt(float k) {
++index_calls;
const double error = std::fabs(true_scale / k - 1.0);
const double on = 0.9 * std::max(0.0, 1.0 - 30.0 * error);
return ValidationSpotEvidence{spots, std::llround(on * spots), std::llround(0.005 * spots)};
}
// The post-refinement at k, relative to k. It reads only 70 % of the error that is left: it
// sees only the frames the angles at k still track.
std::optional<double> RefitAt(float k) {
++refit_calls;
return 1.0 + 0.7 * (true_scale / k - 1.0);
}
RotationScaleWalk Walk(double first_fit) {
return WalkRotationScale(first_fit, [this](float k) { return IndexAt(k); },
[this](float k) { return RefitAt(k); }, 8);
}
};
}
TEST_CASE("ValidationEvidencePrefers", "[RotationScale]") {
const ValidationSpotEvidence base{10000, 3000, 50};
// 1 % more of the spots beyond chance is under the noise of two 30 % shares over 10000 spots
// (sqrt(2 * 0.3 * 0.7 / 10000) = 0.65 %, times 3.29); 5 % is well over it.
CHECK_FALSE(ValidationEvidencePrefers(base, {10000, 3100, 50}));
CHECK(ValidationEvidencePrefers(base, {10000, 3500, 50}));
// A candidate is judged against its own null: more spots on the lattice bought by a null that
// rose just as much is no gain.
CHECK_FALSE(ValidationEvidencePrefers(base, {10000, 3500, 550}));
// Never against itself, and nothing that scored nothing wins.
CHECK_FALSE(ValidationEvidencePrefers(base, base));
CHECK_FALSE(ValidationEvidencePrefers(base, {}));
CHECK(ValidationEvidencePrefers({}, base));
}
TEST_CASE("WalkRotationScale_ReachesTheFixedPoint", "[RotationScale]") {
// A stage 3 % slow. The first fit reads 70 % of that; each refit at the adopted scale reads 70 %
// of what is left, and the walk goes on as long as the validation frames prefer the new scale.
SyntheticSweep sweep{0.97};
const auto walk = sweep.Walk(sweep.RefitAt(1.0f).value());
CHECK(walk.scale == Catch::Approx(0.97).margin(0.001));
CHECK(walk.scale != 1.0f);
CHECK(walk.evidence.on_lattice > sweep.IndexAt(1.0f).on_lattice);
CHECK(sweep.refit_calls > 2);
CHECK_FALSE(walk.trail.empty());
}
TEST_CASE("WalkRotationScale_StoredAnglesStand", "[RotationScale]") {
SECTION("A healthy stage: a fit off by noise scores no better than the stored angles") {
SyntheticSweep sweep{1.0};
const auto walk = sweep.Walk(1.0002);
CHECK(walk.scale == 1.0f);
CHECK(sweep.index_calls == 2); // the stored angles and the fit, nothing more
CHECK(sweep.refit_calls == 0);
}
SECTION("A real but small error the spots cannot resolve beyond their noise") {
SyntheticSweep sweep{0.999};
sweep.spots = 400;
const auto walk = sweep.Walk(0.9993);
CHECK(walk.scale == 1.0f);
}
SECTION("A fit that tracks something other than the rotation scores worse, and is refused") {
SyntheticSweep sweep{1.0};
const auto walk = sweep.Walk(0.98);
CHECK(walk.scale == 1.0f);
CHECK(sweep.refit_calls == 0);
}
SECTION("A fit of exactly one asks for no probe at all") {
SyntheticSweep sweep{1.0};
const auto walk = sweep.Walk(1.0);
CHECK(walk.scale == 1.0f);
CHECK(walk.trail.empty());
CHECK(sweep.index_calls == 0);
}
}