Files
Jungfraujoch/tests/RugnuxSweepsTest.cpp
leonarski_f a9d5367013
Build Packages / Create release (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m43s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m7s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m48s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m22s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 12m24s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m29s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m41s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m44s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m41s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 26m9s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 26m3s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 28m28s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 31m16s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m44s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 26m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m52s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m44s
Build Packages / Unit tests (push) Successful in 1h46m55s
Build Packages / Validation Battery (push) Successful in 52m50s
v1.0.0-rc.181 (#92)
* Rugnux: Several rotation sweeps of one crystal are merged as one dataset (`rugnux a_master.h5 b_master.h5`), on a common indexing and cell, with a non-isomorphous sweep left out and reported; `--sweeps separate` processes each sweep on its own. Bruker `.sfrm` frames and imgCIF files with a full goniometer axis chain are read.
* Rugnux: More robust rotation processing - a retry with a more sensitive spot finder when no lattice is found, a self-consistent detector distance, long axes and doubled cells tested as hypotheses, a `-C` supercell multiple stepped down, and each ice ring merged, down-weighted or rejected on its own.
* Rugnux: Serial stills are merged with a partiality-aware error model, reported as `ERROR_MODEL_A/B/C` with no ISa or R_merge/R_meas/R_pim; runs without `-S` have an overall intensity scale about 0.86-0.88 times that of earlier versions, with unchanged statistics. A second crystal form is left out of the merge and reported, the indexing ambiguity is resolved more reliably, and `--model` no longer influences processing.
* Rugnux: The reported CC1/2 is the standard unweighted one, which can be lower than before on sweeps with weak frames; ISa comes with a 95% interval (`ISA_LOW`/`ISA_HIGH`).
* Rugnux: Model refinement adds anisotropic/TLS, local NCS and twin refinement where R-free supports them; building waters and relabelling ions is now off by default (`--refine-solvent` turns it on).
* NXmx: A detector of several modules is written as `module_0` ... `module_N` only, so DIALS reads each module as its own panel.
* Other: JPEG previews use a bundled encoder (libjpeg-turbo no longer needed), Rugnux prints how to cite it, and the release archives contain the HTML documentation.

Reviewed-on: #92
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-11-02 12:05:20 +01:00

133 lines
5.5 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include <filesystem>
#include <fstream>
#include <map>
#include <stdexcept>
#include "../rugnux/RugnuxSweeps.h"
namespace {
// Inputs described by name alone: "<template>_<n>.cbf" frames, "<name>_rot.h5" / "<name>_still.h5"
// masters, "<name>.cxi" for the stills readers; the wavelength from a table.
struct FakeInputs {
std::map<std::string, double> wavelength;
int sweeps_described = 0;
RugnuxInputDescription File(const std::string &f) const {
RugnuxInputDescription d;
if (f.ends_with(".cbf"))
d.frame_template = RugnuxFrameTemplate(f);
d.foreign = f.ends_with(".cxi");
return d;
}
RugnuxInputDescription Sweep(const std::string &f) {
sweeps_described++;
RugnuxInputDescription d = File(f);
d.rotation = f.ends_with(".cbf") || f.ends_with("_rot.h5");
d.wavelength_A = wavelength.contains(f) ? wavelength.at(f) : 1.0;
return d;
}
RugnuxSweepPlan Plan(const std::vector<std::string> &inputs) {
return PlanRugnuxSweeps(inputs, [this](const std::string &f) { return File(f); },
[this](const std::string &f) { return Sweep(f); });
}
};
} // namespace
TEST_CASE("RugnuxFrameTemplate", "[Rugnux][portable]") {
CHECK(RugnuxFrameTemplate("/d/x_1_0001.cbf") == "/d/x_1_####.cbf");
CHECK(RugnuxFrameTemplate("x_00017.cbf.gz") == "x_#####.cbf.gz");
CHECK(RugnuxFrameTemplate("formal.001") == "formal.###");
CHECK(RugnuxFrameTemplate("noframe.cbf").empty());
}
TEST_CASE("PlanRugnuxSweeps_single_input_is_not_planned", "[Rugnux][portable]") {
FakeInputs in;
CHECK(in.Plan({"a_0001.cbf"}).sweeps.empty());
CHECK(in.Plan({"run_rot.h5"}).sweeps.empty());
CHECK(in.sweeps_described == 0); // nothing is opened for one input
}
TEST_CASE("PlanRugnuxSweeps_frames_by_template", "[Rugnux][portable]") {
FakeInputs in;
SECTION("frames of one template are its one sweep, named by the first given") {
const auto plan = in.Plan({"d/a_0003.cbf", "d/a_0001.cbf", "d/a_0002.cbf"});
REQUIRE(plan.sweeps.size() == 1);
CHECK(plan.sweeps[0].input == "d/a_0003.cbf");
CHECK(plan.sweeps[0].frames);
}
SECTION("two templates are two sweeps, in the order given") {
const auto plan = in.Plan({"d/b_0001.cbf", "d/b_0002.cbf", "d/a_0001.cbf"});
REQUIRE(plan.sweeps.size() == 2);
CHECK(plan.sweeps[0].input == "d/b_0001.cbf");
CHECK(plan.sweeps[0].label == std::filesystem::path("d/b_####.cbf").string());
CHECK(plan.sweeps[1].input == "d/a_0001.cbf");
CHECK(plan.wavelength_A == Catch::Approx(1.0));
}
SECTION("the same name in two directories is two sweeps") {
CHECK(in.Plan({"d1/a_0001.cbf", "d2/a_0001.cbf"}).sweeps.size() == 2);
}
SECTION("sweeps at different wavelengths are refused") {
in.wavelength["d/b_0001.cbf"] = 1.2;
CHECK_THROWS_AS(in.Plan({"d/a_0001.cbf", "d/b_0001.cbf"}), std::runtime_error);
}
}
TEST_CASE("PlanRugnuxSweeps_masters", "[Rugnux][portable]") {
FakeInputs in;
SECTION("stills masters stay one stills run") {
CHECK(in.Plan({"a_still.h5", "b_still.h5", "c_still.h5"}).sweeps.empty());
CHECK(in.sweeps_described == 1); // the first decides; the stills reader refuses a rotation one
}
SECTION("rotation masters are a sweep each") {
const auto plan = in.Plan({"a_rot.h5", "b_rot.h5"});
REQUIRE(plan.sweeps.size() == 2);
CHECK(plan.sweeps[1].input == "b_rot.h5");
CHECK_FALSE(plan.sweeps[0].frames);
}
SECTION("rotation and stills are refused") {
CHECK_THROWS_AS(in.Plan({"a_rot.h5", "b_still.h5"}), std::runtime_error);
}
SECTION("rotation masters at different wavelengths are refused") {
in.wavelength["b_rot.h5"] = 0.98;
CHECK_THROWS_AS(in.Plan({"a_rot.h5", "b_rot.h5"}), std::runtime_error);
}
SECTION("a wavelength within the tolerance is the same") {
in.wavelength["b_rot.h5"] = 1.0 + 1e-4;
CHECK(in.Plan({"a_rot.h5", "b_rot.h5"}).sweeps.size() == 2);
}
}
TEST_CASE("PlanRugnuxSweeps_leaves_the_stills_readers_and_mixtures_alone", "[Rugnux][portable]") {
FakeInputs in;
CHECK(in.Plan({"a.cxi", "b.cxi"}).sweeps.empty());
CHECK(in.Plan({"a_0001.cbf", "b_rot.h5"}).sweeps.empty()); // the readers refuse the mix, as before
CHECK(in.sweeps_described == 0);
}
TEST_CASE("RugnuxSiblingSweeps", "[Rugnux][portable]") {
namespace fs = std::filesystem;
const fs::path dir = fs::path("rugnux_sweeps_siblings");
fs::remove_all(dir);
fs::create_directories(dir);
for (const char *name: {"a_0001.cbf", "a_0002.cbf", "b_0001.cbf", "b_0002.cbf", "b_0003.cbf", "c_0001.cbf.gz",
"notes.txt", "x_master.h5", "y_master.h5"})
std::ofstream(dir / name) << "x";
RugnuxSweepPlan plan;
plan.sweeps.push_back({(dir / "a_0001.cbf").string(), RugnuxFrameTemplate((dir / "a_0001.cbf").string()), true});
plan.sweeps.push_back({(dir / "x_master.h5").string(), (dir / "x_master.h5").string(), false});
const auto siblings = RugnuxSiblingSweeps(plan, {".cbf"});
REQUIRE(siblings.size() == 3);
CHECK(siblings[0] == (dir / "b_####.cbf").string() + " (3 frames)");
CHECK(siblings[1] == (dir / "c_####.cbf.gz").string() + " (1 frame)");
CHECK(siblings[2] == (dir / "y_master.h5").string());
fs::remove_all(dir);
}