v1.0.0-rc.173 (#83)
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

* 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>
This commit was merged in pull request #83.
This commit is contained in:
2026-09-29 15:57:32 +02:00
committed by leonarski_f
parent 6dfe065365
commit 84228bf8be
452 changed files with 23762 additions and 3779 deletions
+91
View File
@@ -4,6 +4,7 @@
#include <catch2/catch_all.hpp>
#include <random>
#include "../image_analysis/scale_merge/ErrorModel.h"
#include "../image_analysis/scale_merge/HKLKey.h"
#include "../image_analysis/scale_merge/Merge.h"
#include "../image_analysis/scale_merge/ResolutionCutoff.h"
@@ -387,6 +388,27 @@ TEST_CASE("CCHalfFrameFactors_DeadStretchDoesNotSetTheTypicalScale") {
CHECK(factor[100] == Catch::Approx(1.0).epsilon(1e-3));
}
// Equal weights give the ordinary R_meas terms, whatever their common value.
TEST_CASE("WeightedRmeas_EqualWeightsAreTheOrdinaryRmeas") {
// Three observations 9, 10, 11 of a reflection with <I> = 10: sum|dev| = 2, sum I = 30.
double num, den;
REQUIRE(WeightedRmeasTerms(0.5 * 2.0, 0.5 * 30.0, 0.5 * 3, 0.25 * 3, num, den));
CHECK(num == Catch::Approx(std::sqrt(3.0 / 2.0) * 2.0));
CHECK(den == Catch::Approx(30.0));
}
// An observation that carries almost no information counts for almost nothing: the terms tend to those
// of the reflection without it, and a reflection left with one effective observation has none.
TEST_CASE("WeightedRmeas_NegligibleWeightDropsOut") {
// 9 and 11 at weight 1, <I> = 10; a third at 40 with weight 1e-6.
const double v = 1e-6;
double num, den;
REQUIRE(WeightedRmeasTerms(2.0 + v * 30.0, 20.0 + v * 40.0, 2.0 + v, 2.0 + v * v, num, den));
CHECK(num == Catch::Approx(std::sqrt(2.0) * 2.0).epsilon(1e-4));
CHECK(den == Catch::Approx(20.0).epsilon(1e-4));
CHECK_FALSE(WeightedRmeasTerms(0.0, 10.0, 1.0, 1.0, num, den));
}
// A fall-off region a logistic cannot follow: CC1/2 drops through the target and comes straight back
// up. The fitted crossing is then an extrapolation far past the bins it was made over, and reading
// the cut off it writes the data deep into the noise; the crossing the bins themselves show is where
@@ -406,3 +428,72 @@ TEST_CASE("ResolutionCutoff_RaggedFallOffIsReadOffTheBins") {
REQUIRE(rc.d_cut);
CHECK(*rc.d_cut > 2.30); // coarser than the band that correlates again
}
namespace {
// Samples of var = a*s2 + b^2*I2 over four decades of counting I/sigma, with counting variances
// spread over a factor of 100 at every intensity, and a fraction of gross outliers.
std::vector<ErrorModelSample> SyntheticErrorModelSamples(double a, double b, double max_snr,
double outlier_fraction) {
std::mt19937 rng(7);
std::uniform_real_distribution<double> u(0.0, 1.0);
std::normal_distribution<double> z(0.0, 1.0);
std::vector<ErrorModelSample> out;
for (int i = 0; i < 200000; ++i) {
const double s2 = std::pow(10.0, 2.0 * u(rng));
const double snr = max_snr * std::pow(10.0, -4.0 * u(rng));
const double I2 = snr * snr * s2;
const double e = z(rng);
double dev2 = (a * s2 + b * b * I2) * e * e;
if (u(rng) < outlier_fraction) dev2 *= 400.0;
out.push_back({s2, I2, dev2, 2.0f});
}
return out;
}
}
// The fit recovers a and b from observations whose counting variances differ widely inside every
// intensity bin (where a ratio of bin medians does not), with 0.3% gross outliers in the pool.
TEST_CASE("ErrorModel_RecoversAAndBThroughOutliers") {
const auto pool = SyntheticErrorModelSamples(1.3, 0.03, 300.0, 0.003);
std::vector<ErrorModelBinned> scratch;
const auto fit = FitErrorModel(pool, scratch, 4);
REQUIRE(fit.active);
CHECK(fit.b_measured);
CHECK(fit.b_resolved);
CHECK(fit.a == Catch::Approx(1.3).epsilon(0.03));
CHECK(1.0 / std::sqrt(fit.b2) == Catch::Approx(1.0 / 0.03).epsilon(0.05));
}
// A few observations whose counting variance is hugely overstated - their scatter is ordinary - do not
// set the fit: each sample counts by its deviation relative to its own variance.
TEST_CASE("ErrorModel_OverstatedCountingVarianceDoesNotSetTheFit") {
auto pool = SyntheticErrorModelSamples(1.3, 0.03, 300.0, 0.0);
for (size_t i = 0; i < pool.size(); i += 200)
pool[i].s2 *= 1e4;
std::vector<ErrorModelBinned> scratch;
const auto fit = FitErrorModel(pool, scratch, 4);
REQUIRE(fit.active);
CHECK(fit.a == Catch::Approx(1.3).epsilon(0.03));
CHECK(1.0 / std::sqrt(fit.b2) == Catch::Approx(1.0 / 0.03).epsilon(0.05));
}
// The same pool on one thread and on many gives the same numbers.
TEST_CASE("ErrorModel_SameOnAnyNumberOfThreads") {
const auto pool = SyntheticErrorModelSamples(1.1, 0.05, 100.0, 0.001);
std::vector<ErrorModelBinned> scratch;
const auto one = FitErrorModel(pool, scratch, 1);
const auto many = FitErrorModel(pool, scratch, 16);
CHECK(one.a == many.a);
CHECK(one.b2 == many.b2);
}
// Data that never reach where b could be seen: a is fitted alone and b held at 0.
TEST_CASE("ErrorModel_BNotMeasuredOnWeakData") {
const auto pool = SyntheticErrorModelSamples(0.9, 0.05, 1.5, 0.0);
std::vector<ErrorModelBinned> scratch;
const auto fit = FitErrorModel(pool, scratch, 4);
REQUIRE(fit.active);
CHECK(!fit.b_measured);
CHECK(fit.b2 == 0.0);
CHECK(fit.a == Catch::Approx(0.9).epsilon(0.03));
}