Files
Jungfraujoch/tests/RugnuxSweepsTest.cpp
leonarski_fandClaude Opus 5.5 fa912c7ff5 rugnux: several rotation sweeps are told apart from the inputs; --sweeps separate processes each alone
The inputs are planned as sweeps (RugnuxSweeps): one per CBF/marCCD/SMV file-name template - the name
with its frame number taken out, so any frame of a sweep names it and `rugnux a_*.cbf` is the one sweep
a, where several frames of one sweep used to be refused - and one per rotation master. Several stills
masters stay one stills run as before (the first master decides, and the stills reader refuses a rotation
one among them); a rotation master among stills is refused, and so are sweeps at different wavelengths.
One input is not planned at all. Other sweeps beside the given ones are named in the log and not
processed.

--sweeps separate|joint. separate processes each sweep alone, as if it were the only input, into
<prefix>_s<k>_* with its own log and report; a sweep that fails is reported and the others go on. joint,
the default, refuses with several sweeps until the joint pass exists. -s/-e and --force-still are
refused with several sweeps.

rugnux_cli.cpp: the one-input code is taken out of RunRugnux. The options and what RunRugnux set up before
parsing are members of RugnuxCli (Parse), and one input is a RugnuxInputRun copied from it, in two halves:
PrepareRugnuxInput (reading the input, the experiment and the run's configuration) and RunPreparedInput
(running it and writing its files); the locals the second half reads are members. The code itself is as
it was but for exit(EXIT_FAILURE) in the per-input code, which is now an exception the caller turns into
the exit code, so that one sweep's failure does not end the others. With one input the outputs are
identical: report (but for the times and command line) and .hkl byte for byte against the rc181 binary on
a CBF small-molecule sweep and an NXmx rotation set, and on the first sweep of a --sweeps separate run.

Tests: RugnuxSweepsTest (templates, single input untouched, frames by template, masters rotation /
stills / mixed / wavelength, the stills readers and mixtures left alone, sibling sweeps).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQqN9qRD57vnShzTC37
2026-10-28 18:49:48 +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);
}