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
* 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>
133 lines
5.5 KiB
C++
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);
|
|
}
|