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
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-28 18:49:48 +01:00
1 parent b13f1bfd2d
commit fa912c7ff5
7 files changed
+686 -125

No files matched your search

+19 -6
View File
@@ -30,9 +30,11 @@ rugnux -o myrun dataset_master.h5 # NXmx / EIGER / Jungfraujoch mas
rugnux -o myrun sweep_00001.cbf # CBF, marCCD or SMV sweep: name one frame
rugnux -o myrun dataset_master.h5 model.cif # with a model (or --model model.cif)
rugnux -o myrun x_master.h5 http://host:5232/ # follow a broker's collection while it is written
rugnux -o myrun --sweeps separate a_master.h5 b_master.h5 # several sweeps, each alone
```
Chosen when the file's goniometer axis turns. One sweep per run. A model is placed (rigid body, or
Chosen when the file's goniometer axis turns. Several sweeps can be given (below, Inputs); for now
each is processed alone with `--sweeps separate`. A model is placed (rigid body, or
molecular replacement), refined and validated after merging; it does not change the processing
decisions ([Models and reference data](RUGNUX_MODEL_AND_REFERENCE.md)).
@@ -91,14 +93,24 @@ spot finding or integration ([Re-scaling and re-merging](RUGNUX_ADVANCED.md#re-s
## Inputs
Inputs are files, never a directory, read in the order given (a shell glob gives them sorted by
name). Several inputs are read together only as one stills dataset in `--mode mx`: all the files of
name). Several inputs are one stills dataset in `--mode mx`, or several rotation sweeps (below): all the files of
one dataset - `rugnux run*.h5` (SACLA), `*.cxi`, `*.xtc` (LCLS) - or several Jungfraujoch/NXmx
masters of stills, read as one run: their images one after another, merged together, with the
indexing rate reported per master as well. Such masters must share the detector, wavelength,
distance and beam centre. Several inputs must be one format; a rotation sweep (name one frame of a
CBF/marCCD/SMV sweep), any `--mode` other than `mx`, and following a broker take one. What each
format needs is in [What Rugnux reads](RUGNUX_FORMATS.md); serial XFEL stills are read without a
geometry file where their own metadata places the detector
distance and beam centre. Several inputs must be one format; any `--mode` other than `mx`, and
following a broker, take one.
Several rotation sweeps are told apart from the files: one sweep per CBF/marCCD/SMV file-name
template (the name with its frame number taken out - any frame of a sweep names the whole sweep, so
`rugnux a_*.cbf` is the one sweep `a`), one per rotation master. Only the inputs given are processed;
other sweeps beside them are named in the log. A mix of rotation and stills masters is refused, and
so are sweeps at different wavelengths, `-s`/`-e` and `--force-still` with several sweeps.
`--sweeps separate` processes each sweep alone, as if it were the only input, into
`<prefix>_s<k>_*` (k = 1, 2, ... in the order given); processing several sweeps as one dataset
(`--sweeps joint`, the default) is not implemented yet and refuses.
What each format needs is in [What Rugnux reads](RUGNUX_FORMATS.md); serial XFEL stills are read
without a geometry file where their own metadata places the detector
([Serial XFEL inputs](RUGNUX_FORMATS.md#serial-xfel-inputs)).
An atomic model and a reference reflection file can be given among the inputs, in any order,
@@ -130,6 +142,7 @@ With the input already on disk, the URL is ignored.
| `-v, --verbose` | Show the whole log on the terminal in place of the steps; `<prefix>_log.txt` is written either way |
| `--simulate-live <ms>` | Read a stored input as the collection it was while being written: image *i* becomes readable (*i*+1)·`ms` ms after the start (0: all at once). The run takes the live path a broker URL takes, with the same result; for testing the live workflow without a broker. `--mode mx` only |
| `--fast-live` | With `--simulate-live` or a broker URL: run as the broker's rugnux live mode does — every decision stands where it was first taken, nothing is re-checked or run again once the sweep is in, the integration radius is fixed, and SIGINT cuts the run to what it has established. A difficult crystal can come out worse |
| `--sweeps joint\|separate` | Several rotation sweeps (see Inputs): `separate` processes each alone into `<prefix>_s<k>_*`; `joint` (the default), one dataset of all of them, is not implemented yet |
| `--no-gpu` | Run on the CPU only, as on a machine without a GPU, even where one is present: CUDA is not touched (nor, on macOS, the Apple GPU that Bragg integration, the model density and the molecular-replacement rotation function otherwise run on) and every GPU step takes its CPU path (`-X FFT` indexes with FFTW and `-X FFBIDX` with its CPU implementation; `-X cuFFT` is refused) |
### Mode
+2 -1
View File
@@ -40,7 +40,8 @@ TARGET_LINK_LIBRARIES(JFJochRugnux JFJochReader JFJochImageAnalysis JFJochPrevie
# The files of a run, written by the programs that run one (rugnux, the viewer) from what JFJochRugnux
# returns: the library itself writes none.
ADD_LIBRARY(JFJochRugnuxOutput STATIC RugnuxOutputFiles.cpp RugnuxOutputFiles.h RugnuxInputKind.cpp RugnuxInputKind.h)
ADD_LIBRARY(JFJochRugnuxOutput STATIC RugnuxOutputFiles.cpp RugnuxOutputFiles.h RugnuxInputKind.cpp RugnuxInputKind.h
RugnuxSweeps.cpp RugnuxSweeps.h)
TARGET_LINK_LIBRARIES(JFJochRugnuxOutput JFJochRugnux JFJochWriter JFJochModelWriter ZLIB::ZLIB)
# rugnux is the single offline analysis CLI; --mode picks what it does: mx (the full pipeline, the
+140
View File
@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "RugnuxSweeps.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <filesystem>
#include <map>
#include <set>
#include <stdexcept>
#include <spdlog/fmt/fmt.h>
namespace {
// How far two sweeps may state the wavelength apart and still be one dataset: JFJochMultiReader's
// tolerance for the inputs of one stills run.
constexpr double WAVELENGTH_TOLERANCE = 1e-3; // relative
std::string Lower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
return s;
}
}
std::string RugnuxFrameTemplate(const std::string &path) {
const std::filesystem::path p(path);
std::string name = p.filename().string();
size_t end = name.size();
while (end > 0 && !std::isdigit(static_cast<unsigned char>(name[end - 1])))
end--;
if (end == 0)
return {};
size_t start = end;
while (start > 0 && std::isdigit(static_cast<unsigned char>(name[start - 1])))
start--;
name.replace(start, end - start, std::string(end - start, '#'));
return (p.parent_path() / name).string();
}
RugnuxSweepPlan PlanRugnuxSweeps(const std::vector<std::string> &inputs,
const std::function<RugnuxInputDescription(const std::string &)> &describe_file,
const std::function<RugnuxInputDescription(const std::string &)> &describe_sweep) {
RugnuxSweepPlan plan;
if (inputs.size() < 2)
return plan;
std::vector<RugnuxInputDescription> files;
for (const auto &f: inputs)
files.push_back(describe_file(f));
const bool any_foreign = std::any_of(files.begin(), files.end(), [](const auto &d) { return d.foreign; });
const size_t n_frames = std::count_if(files.begin(), files.end(),
[](const auto &d) { return !d.frame_template.empty(); });
// Inputs of the stills readers, or a mix of frames and masters: read as before, which takes the
// former and refuses the latter.
if (any_foreign || (n_frames != 0 && n_frames != inputs.size()))
return plan;
if (n_frames == inputs.size()) {
std::set<std::string> seen;
for (size_t i = 0; i < inputs.size(); i++)
if (seen.insert(files[i].frame_template).second)
plan.sweeps.push_back({inputs[i], files[i].frame_template, true});
if (plan.sweeps.size() == 1)
return plan;
} else {
// A first input of stills is a stills run, read together as before - and refused there if any
// other input turns. A rotation first input asks every other one.
if (!describe_sweep(inputs[0]).rotation)
return plan;
std::vector<std::string> rotation{inputs[0]}, stills;
for (size_t i = 1; i < inputs.size(); i++)
(describe_sweep(inputs[i]).rotation ? rotation : stills).push_back(inputs[i]);
if (!stills.empty())
throw std::runtime_error(fmt::format(
"{} is a rotation sweep and {} a stills collection: rotation sweeps and stills are not "
"processed together; give them in separate runs", rotation.front(), stills.front()));
for (const auto &f: inputs)
plan.sweeps.push_back({f, f, false});
}
std::vector<double> wavelength;
for (const auto &s: plan.sweeps)
wavelength.push_back(describe_sweep(s.input).wavelength_A);
for (size_t k = 1; k < plan.sweeps.size(); k++)
if (std::fabs(wavelength[k] - wavelength[0]) > WAVELENGTH_TOLERANCE * wavelength[0])
throw std::runtime_error(fmt::format(
"{} is at {:.5f} A and {} at {:.5f} A: sweeps at different wavelengths are not processed "
"together; give them in separate runs", plan.sweeps[0].label, wavelength[0],
plan.sweeps[k].label, wavelength[k]));
plan.wavelength_A = wavelength[0];
return plan;
}
std::vector<std::string> RugnuxSiblingSweeps(const RugnuxSweepPlan &plan, const std::vector<std::string> &extensions) {
namespace fs = std::filesystem;
std::set<std::string> given, dirs_frames, dirs_masters;
for (const auto &s: plan.sweeps) {
const fs::path p(s.input);
const std::string dir = p.parent_path().empty() ? "." : p.parent_path().string();
if (s.frames) {
given.insert(fs::path(s.label).filename().string());
dirs_frames.insert(dir);
} else {
given.insert(p.filename().string());
dirs_masters.insert(dir);
}
}
auto has_extension = [&](std::string name) {
name = Lower(name);
if (name.size() > 3 && name.ends_with(".gz"))
name.resize(name.size() - 3);
return std::any_of(extensions.begin(), extensions.end(), [&](const auto &e) { return name.ends_with(e); });
};
// template (or master) name -> files, in name order
std::map<std::string, size_t> found;
std::error_code ec;
for (const auto &dir: dirs_frames)
for (const auto &e: fs::directory_iterator(dir, ec)) {
const std::string name = e.path().filename().string();
if (!e.is_regular_file() || !has_extension(name))
continue;
const std::string t = fs::path(RugnuxFrameTemplate(e.path().string())).filename().string();
if (!t.empty() && !given.contains(t))
found[(fs::path(dir) / t).string()]++;
}
for (const auto &dir: dirs_masters)
for (const auto &e: fs::directory_iterator(dir, ec)) {
const std::string name = e.path().filename().string();
if (e.is_regular_file() && name.ends_with("_master.h5") && !given.contains(name))
found[(fs::path(dir) / name).string()] = 0;
}
std::vector<std::string> out;
for (const auto &[name, n]: found)
out.push_back(n > 0 ? fmt::format("{} ({} frame{})", name, n, n == 1 ? "" : "s") : name);
return out;
}
+56
View File
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <functional>
#include <string>
#include <vector>
// The rotation sweeps a rugnux command line names, decided from the files in the order given:
// - frame files (CBF, marCCD, SMV): one sweep per file-name template - the name with its last run of
// digits, the frame number, taken out. Any frame of a sweep names the whole sweep, so several frames
// of one template are one sweep;
// - masters: one sweep per rotation master. Several stills masters stay one stills run, as before, and
// a mix of rotation and stills masters is refused.
// Sweeps at different wavelengths are refused. Nothing is planned for a single input, or for inputs only
// the stills readers take (XFEL HDF5, CXI, XTC): those are read as they always were.
// What the planner needs to know about one input. `frame_template` is empty for anything but a frame
// file; `rotation` and `wavelength_A` are asked only of the input that stands for a sweep.
struct RugnuxInputDescription {
std::string frame_template;
bool foreign = false; // read only by the stills readers (XFEL HDF5, CXI, XTC)
bool rotation = false;
double wavelength_A = 0.0;
};
struct RugnuxSweep {
std::string input; // the file the sweep is read from: its first frame given, or its master
std::string label; // the frame template or the master, for messages
bool frames = false; // a sweep of frame files (label is its template)
};
struct RugnuxSweepPlan {
// Empty: the inputs are read together as before (one input, a stills run, or a mix the readers refuse).
std::vector<RugnuxSweep> sweeps;
double wavelength_A = 0.0; // of every sweep, where there are several
};
// `describe_file` says what an input is from its name and contents without reading the sweep (its
// frame template, or whether it is only for the stills readers); `describe_sweep` reads the input that
// stands for a sweep for its rotation and wavelength. Throws std::runtime_error, with a message for the
// user, on a mix of rotation sweeps and stills, or on sweeps at different wavelengths.
RugnuxSweepPlan PlanRugnuxSweeps(const std::vector<std::string> &inputs,
const std::function<RugnuxInputDescription(const std::string &)> &describe_file,
const std::function<RugnuxInputDescription(const std::string &)> &describe_sweep);
// The template of a frame file name, with its directory: the last run of digits in the name replaced by
// '#' characters, one per digit ("/d/x_1_0001.cbf" -> "/d/x_1_####.cbf"). Empty for a name with no digits.
std::string RugnuxFrameTemplate(const std::string &path);
// The sweeps beside the given ones that were not given: frame templates of `extensions` (lower case, with
// the dot; a trailing ".gz" is looked through) in the directories of the frame sweeps, and other
// "*_master.h5" files beside the given masters - one line each, for the log. Only the inputs given are
// processed; these are named so that a sweep left out by accident shows.
std::vector<std::string> RugnuxSiblingSweeps(const RugnuxSweepPlan &plan, const std::vector<std::string> &extensions);
+336 -118
View File
@@ -85,6 +85,7 @@
#include "RugnuxOutputFiles.h"
#include "RugnuxConsole.h"
#include "RugnuxInputKind.h"
#include "RugnuxSweeps.h"
#ifdef __APPLE__
#include "../image_analysis/bragg_integration/BraggIntegrationEngineMetal.h"
#endif
@@ -100,6 +101,9 @@ constexpr double RELATIVE_B_DEFAULT_DEG = 10.0;
// mutually exclusive by construction.
enum class RugnuxMode { MX, Azint, Scale, Calibration, Refine };
// --sweeps: several rotation sweeps as one dataset (joint), or each alone into <prefix>_s<k> (separate).
enum class RugnuxSweepsMode { Joint, Separate };
// Azimuthal sectors used by --mode calibration --calibration rings when --azim-phi-bins was not given.
// The default of 1 is a plain radial profile, in which a ring has been averaged over every direction so
// nothing is left to say where its centre is (RingsFromAzimuthalProfile refuses below 4). Sweeping the
@@ -119,6 +123,7 @@ never a directory; a model or reflection file among them is recognised by its co
Rotation data (--mode mx, the default; chosen when the file's goniometer axis turns)
rugnux -o myrun dataset_master.h5 NXmx / EIGER / Jungfraujoch master file
rugnux -o myrun sweep_00001.cbf CBF, marCCD or SMV sweep: name one frame
rugnux -o myrun --sweeps separate a_master.h5 b_master.h5 several sweeps, each alone
rugnux -o myrun dataset_master.h5 model.cif with a model (or --model model.cif)
rugnux -o myrun x_master.h5 http://host:5232/ follow a broker's collection while it is written
A model is placed and refined after merging; it does not change the processing.
@@ -166,6 +171,8 @@ Basic options
--no-gpu CPU only, even where a GPU is present
--no-merge No scaling or merging; write the per-image _process.h5
--write-process-h5 Also write the (large) _process.h5 when merging
--sweeps separate Several rotation sweeps (masters or frame templates): each alone,
into <prefix>_s<k>_*; processing them as one dataset is to come
-v, --verbose Whole log on the terminal (it is always in <prefix>_log.txt)
All options (about 120, for unusual data and diagnosis), each with what it does:
@@ -279,7 +286,8 @@ enum {
OPT_POLARIZATION,
OPT_NO_REFINE_TILT,
OPT_REFINE_SCORE_ONLY,
OPT_REFINE_D_MIN
OPT_REFINE_D_MIN,
OPT_SWEEPS
};
static option long_options[] = {
@@ -303,6 +311,7 @@ static option long_options[] = {
{"no-refine-solvent", no_argument, nullptr, OPT_NO_REFINE_SOLVENT},
{"refine-score-only", no_argument, nullptr, OPT_REFINE_SCORE_ONLY},
{"refine-d-min", required_argument, nullptr, OPT_REFINE_D_MIN},
{"sweeps", required_argument, nullptr, OPT_SWEEPS},
{"export-unmerged", no_argument, nullptr, OPT_EXPORT_UNMERGED},
{"no-export-unmerged", no_argument, nullptr, OPT_NO_EXPORT_UNMERGED},
{"export-unmerged-partials", no_argument, nullptr, OPT_EXPORT_UNMERGED_PARTIALS},
@@ -804,70 +813,27 @@ extern "C" void fftwf_assertion_failed(const char *s, int line, const char *file
std::abort();
}
// The body of main. Settings setters and the pipeline itself throw JFJochException on input the parser
// cannot reject on its own (a resolution limit of zero, a distance of zero, a polarisation above one,
// an unreadable file), so main wraps this and reports rather than letting the exception terminate the
// process with no diagnostic and exit code 134.
static int RunRugnux(int argc, char **argv, bool no_gpu) {
// cuFFT plans are reused through the run and destroyed with it, ahead of the device reset in main.
FFTPlanReuse fft_plans;
// The whole invocation, reported at the end and written into the results report. Started before
// anything else so it covers reading the file and setting up as well as the processing.
const auto invocation_start = std::chrono::steady_clock::now();
RunProvenance provenance;
provenance.command_line = JoinCommandLine(argc, argv);
provenance.gpu_count = get_gpu_count();
#ifdef __APPLE__
// Without CUDA, the Bragg integration's integer pixel work runs on the Apple GPU (MXAnalysisWithoutFPGA).
const std::string metal_device = BraggIntegrationEngineMetal::DeviceName();
#else
const std::string metal_device;
#endif
provenance.gpu_description = get_gpu_description();
namespace {
// One input's run ended: what exit(EXIT_FAILURE) was where a command line ran one input only. A run of
// several sweeps goes on with the next sweep; RunRugnux returns the code.
struct RugnuxInputFailed { int code; };
// What Parse and PrepareRugnuxInput return when the run goes on.
constexpr int RUGNUX_CONTINUE = -1;
}
// The terminal shows the run as steps (RugnuxConsole) and every Logger writes to the log file
// instead; with -v the whole log is on the terminal as well. -v is looked for here, before the
// options are parsed, because the parser already logs.
// The command line as parsed, with what RunRugnux set up before it was parsed. The options are members so
// that the code reading and running one input (RugnuxInputRun) reads them by name, as it did when it was
// one function, and so that each sweep of a run of several is run from a copy of its own.
struct RugnuxCli {
bool no_gpu;
bool verbose = false;
for (int i = 1; i < argc; i++)
if (std::strcmp(argv[i], "-v") == 0 || std::strcmp(argv[i], "--verbose") == 0)
verbose = true;
console = std::make_shared<RugnuxConsole>(verbose);
std::atexit(CloseConsoleOnExit);
// Files with an older time than this are not this run's (the margin covers coarse file times).
const auto files_since = std::filesystem::file_time_type::clock::now() - std::chrono::seconds(2);
auto log_sinks = std::make_shared<spdlog::sinks::dist_sink_mt>();
// What is logged before the log file can be opened - its name comes from -o - waits here.
auto early_log = std::make_shared<spdlog::sinks::ringbuffer_sink_mt>(10000);
if (verbose)
log_sinks->add_sink(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
else
log_sinks->add_sink(MakeConsoleSink(console));
log_sinks->add_sink(early_log);
Logger::RouteAll(log_sinks);
// The HDF5 library prints its own error stack on stderr; every failure it reports reaches the
// reader as an error code anyway, which is turned into a message there.
H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr);
PrintBanner(*console);
Logger logger("rugnux");
#ifdef JFJOCH_USE_CUDA
const char *build = "CUDA build";
#else
const char *build = "CPU build";
#endif
logger.Info("rugnux Version {}{} - {}", jfjoch_version(),
jfjoch_git_sha1().empty() ? std::string()
: fmt::format(" (git {} {})", jfjoch_git_sha1().substr(0, 6), jfjoch_git_date()), build);
logger.Info("Build flags: {}", jfjoch_build_cxx_flags().empty() ? "none" : jfjoch_build_cxx_flags());
logger.Info("Build: {}", jfjoch_build_info(true));
logger.Info("Command line: {}", provenance.command_line);
logger.Info("GPU: {}", no_gpu ? std::string("not used (--no-gpu) - running on the CPU")
: provenance.gpu_count > 0 ? fmt::format("{} ({} visible)", provenance.gpu_description, provenance.gpu_count)
: !metal_device.empty() ? fmt::format("{} through Metal, for the Bragg integration only - the rest runs on the CPU", metal_device)
: std::string("none visible - running on the CPU"));
RegisterHDF5Filter();
std::chrono::steady_clock::time_point invocation_start;
RunProvenance provenance;
std::string metal_device;
std::filesystem::file_time_type files_since;
std::shared_ptr<spdlog::sinks::dist_sink_mt> log_sinks;
std::shared_ptr<spdlog::sinks::ringbuffer_sink_mt> early_log;
Logger logger{"rugnux"}; // made after RunRugnux has routed every Logger to the log
std::string input_file;
std::string output_prefix = "output";
@@ -990,6 +956,62 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
bool simple_stills_flag = false; // --simple-stills: disable the default stills partiality post-refinement
std::optional<double> outlier_reject_nsigma; // merge per-observation outlier rejection
std::vector<std::string> input_files;
std::string broker_url;
std::shared_ptr<const RugnuxModel> model;
RugnuxSweepsMode sweeps_mode = RugnuxSweepsMode::Joint; // --sweeps
RugnuxCli(bool no_gpu_, std::chrono::steady_clock::time_point invocation_start_, RunProvenance provenance_,
std::string metal_device_, std::filesystem::file_time_type files_since_,
std::shared_ptr<spdlog::sinks::dist_sink_mt> log_sinks_,
std::shared_ptr<spdlog::sinks::ringbuffer_sink_mt> early_log_)
: no_gpu(no_gpu_), invocation_start(invocation_start_), provenance(std::move(provenance_)),
metal_device(std::move(metal_device_)), files_since(files_since_), log_sinks(std::move(log_sinks_)),
early_log(std::move(early_log_)) {}
// The command line: RUGNUX_CONTINUE, or the exit code of a run that ends here.
int Parse(int argc, char **argv);
// Each sweep of the command line, or its one input.
int Run();
};
// One input - one sweep, or one stills run - read, set up and processed. Copied from the command line it
// belongs to, with its own input and output prefix.
struct RugnuxInputRun : RugnuxCli {
explicit RugnuxInputRun(const RugnuxCli &cli) : RugnuxCli(cli) {}
// What PrepareRugnuxInput sets up and RunPreparedInput runs with.
std::future<void> cufft_warmup, context_warmup;
SimulatedLive<JFJochHDF5Reader> hdf5_reader;
SimulatedLive<JFJochCBFReader> cbf_reader;
SimulatedLive<JFJochMarCCDReader> marccd_reader;
SimulatedLive<JFJochSMVReader> smv_reader;
SimulatedLive<JFJochXFELReader> xfel_reader;
SimulatedLive<JFJochXTCReader> xtc_reader;
JFJochStartMessageReader live_reader;
std::unique_ptr<JFJochMultiReader> multi_reader;
std::unique_ptr<BrokerFeed> broker_feed;
JFJochReader *reader_ptr = nullptr;
std::shared_ptr<const JFJochReaderDataset> dataset;
OutputProvenance output_provenance;
DiffractionExperiment experiment;
ProcessConfig config;
std::optional<ProcessFileSink> sink;
// The log file of this input, taken out of the log again once it is done.
std::shared_ptr<spdlog::sinks::sink> log_file_sink;
// Reads the input and sets up the experiment and the run's configuration: RUGNUX_CONTINUE, or the
// exit code of a mode that ends here (refine, azint, calibration, scale).
int PrepareRugnuxInput();
// Runs the input PrepareRugnuxInput set up and writes its files.
int RunPreparedInput();
int Process() {
const int rc = PrepareRugnuxInput();
return rc != RUGNUX_CONTINUE ? rc : RunPreparedInput();
}
};
int RugnuxCli::Parse(int argc, char **argv) {
if (argc == 1) {
print_usage();
exit(EXIT_FAILURE);
@@ -1197,6 +1219,16 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
case OPT_REFINE_D_MIN:
refine_d_min = parse_number_arg<double>(optarg, "--refine-d-min", logger, 0.1, 100.0);
break;
case OPT_SWEEPS:
if (std::strcmp(optarg, "joint") == 0)
sweeps_mode = RugnuxSweepsMode::Joint;
else if (std::strcmp(optarg, "separate") == 0)
sweeps_mode = RugnuxSweepsMode::Separate;
else {
logger.Error("--sweeps expects joint or separate, not {}", optarg);
exit(EXIT_FAILURE);
}
break;
case OPT_EXPORT_UNMERGED:
export_unmerged = true;
break;
@@ -1624,8 +1656,8 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
// The inputs, then a broker URL if the last word is one: told apart by the scheme, so that several
// stills inputs can be given.
std::vector<std::string> input_files(argv + optind, argv + argc);
std::string broker_url;
input_files.assign(argv + optind, argv + argc);
broker_url.clear();
if (!input_files.empty() && (input_files.back().starts_with("http://")
|| input_files.back().starts_with("https://"))) {
broker_url = input_files.back();
@@ -1695,7 +1727,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
exit(EXIT_FAILURE);
}
// Read before anything runs: a file that holds no model ends the run here rather than after it.
std::shared_ptr<const RugnuxModel> model;
model.reset();
if (const std::string path = mr_model.empty() ? model_pdb : mr_model; !path.empty()) {
try {
model = std::make_shared<const RugnuxModel>(ReadRugnuxModelFile(path));
@@ -1711,6 +1743,10 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
exit(EXIT_FAILURE);
}
return RUGNUX_CONTINUE;
}
int RugnuxInputRun::PrepareRugnuxInput() {
input_file = input_files[0];
// -N defaults to 0 = "use every CPU the run may use" (AvailableCpus); resolve it to a concrete count here so every mode
@@ -1735,6 +1771,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
for (const auto &m : early_log->last_raw())
file->log(m);
log_sinks->add_sink(file);
log_file_sink = file;
} catch (const std::exception &e) {
logger.Warning("The log file {} cannot be written: {}", log_file, e.what());
log_file.clear();
@@ -1767,7 +1804,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
data = ReadRugnuxReflectionFile(input_file);
} catch (const std::exception &e) {
logger.Error("Cannot read the reflections {}: {}", input_file, e.what());
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
console->OnStep(refine_score_only ? "Scoring the model" : "Refining the model");
RefinementSettings settings;
@@ -1814,26 +1851,16 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
// Off the critical path, so cuFFT's one-time initialisation overlaps the file open, the mask read
// and the beam-stop pre-scan instead of landing inside the first pass. It also times a plan, which
// shows whether the CUDA JIT cache works.
auto cufft_warmup = std::async(std::launch::async, [&logger] { LogCudaJitCache(logger, WarmUpCuFFT()); });
cufft_warmup = std::async(std::launch::async, [this] { LogCudaJitCache(logger, WarmUpCuFFT()); });
// The same for the CUDA contexts of the other cards (the warm-up above creates card 0's), which
// the pre-scan does not use: created one after another at start-up, they cost a fifth of a second
// per card before the run began.
auto context_warmup = std::async(std::launch::async, create_gpu_contexts, 1);
context_warmup = std::async(std::launch::async, create_gpu_contexts, 1);
// 1. Read Input File. An HDF5 master, one PILATUS miniCBF frame, or one marCCD frame - naming
// any frame of a sweep reads the whole sweep, which is how these are archived and how a user
// thinks of them.
console->OnStep("Reading input");
SimulatedLive<JFJochHDF5Reader> hdf5_reader;
SimulatedLive<JFJochCBFReader> cbf_reader;
SimulatedLive<JFJochMarCCDReader> marccd_reader;
SimulatedLive<JFJochSMVReader> smv_reader;
SimulatedLive<JFJochXFELReader> xfel_reader;
SimulatedLive<JFJochXTCReader> xtc_reader;
JFJochStartMessageReader live_reader;
std::unique_ptr<JFJochMultiReader> multi_reader;
std::unique_ptr<BrokerFeed> broker_feed;
JFJochReader *reader_ptr = nullptr;
// Which format this is. Asked INSIDE the try: these open and read the file, and an input the program
// cannot read has to come out as the message below, not as an unhandled exception with nothing
// printed at all.
@@ -1841,13 +1868,13 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
bool input_is_cxi = false, input_is_xtc = false, input_is_panel_table = false;
try {
// Every input is the format the first one is.
const auto all_inputs = [&input_files](auto can_read) {
const auto all_inputs = [this](auto can_read) {
return std::all_of(input_files.begin(), input_files.end(), can_read);
};
const auto refuse_mixed = [&]() {
logger.Error("The inputs are not all of one format; several inputs must be files of the same "
"kind - Jungfraujoch/NXmx masters, SACLA HDF5, CXI or XTC");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
};
const auto is_foreign = [](const std::string &f) {
return JFJochXFELReader::CanReadPanelTable(f) || JFJochXFELReader::CanReadCXI(f) || JFJochXTCReader::CanRead(f);
@@ -1859,7 +1886,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
|| JFJochSMVReader::CanRead(input_file)) {
logger.Error("{} is one frame of a sweep, which names the whole sweep: give just one of its "
"frames", input_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
if (!all_inputs([&is_foreign](const std::string &f) {
return !is_foreign(f) && !JFJochCBFReader::CanRead(f) && !JFJochMarCCDReader::CanRead(f)
@@ -1872,7 +1899,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
r->ReadFile(f);
} catch (const std::exception &e) {
logger.Error("{} is not a Jungfraujoch/NXmx master that can be read: {}", f, e.what());
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
inputs.push_back(std::move(r));
}
@@ -1891,7 +1918,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
if (!extra_masks.empty() && !input_is_crystfel && !input_is_panel_table && !input_is_cxi) {
logger.Error("--mask-good and --mask-bad are for HDF5/CXI stills (SACLA, CXI or --crystfel-geometry "
"input); {} is not one", input_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
xfel_reader.ExtraMasks(extra_masks);
if (input_is_crystfel) {
@@ -1899,7 +1926,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
reader_ptr = &xfel_reader;
if (!xfel_reader.HasPhotonEnergy() && !wavelength_A) {
logger.Error("Nothing read states the photon energy of {}; give it with --wavelength", input_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
logger.Info("Read {} frames from {} files with the CrystFEL geometry {}",
xfel_reader.GetNumberOfImages(), input_files.size(), crystfel_geometry);
@@ -1913,7 +1940,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
logger.Info("Read {} frames from {} CXI files", xfel_reader.GetNumberOfImages(), input_files.size());
} else if (const auto refusal = JFJochXFELReader::CXIRefusal(input_file)) {
logger.Error("{}", *refusal);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
input_is_cbf = !reader_ptr && JFJochCBFReader::CanRead(input_file);
input_is_marccd = !reader_ptr && !input_is_cbf && JFJochMarCCDReader::CanRead(input_file);
@@ -1925,7 +1952,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
if (gain_per_photon && !reader_ptr && !input_is_xtc && !input_is_smv && !input_is_marccd) {
logger.Error("--gain is for input in detector units (SACLA, CXI, XTC, SMV, marCCD or "
"--crystfel-geometry input); {} is read in the units it states", input_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
if (reader_ptr) {
// read above
@@ -1962,7 +1989,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
logger.Error("The XTC does not state the detector distance{}{}: give {} (the beam centre is "
"measured; with -C the distance is too)", xtc_reader.HasPixelSize() ? "" : ", the pixel size",
xtc_reader.HasWavelength() ? "" : ", the wavelength", options);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
reader_ptr = &xtc_reader;
logger.Info("Read {} XTC still images from {} streams", xtc_reader.GetNumberOfImages(),
@@ -1971,13 +1998,13 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
// The collection is still being written: follow it from the broker.
if (mode != RugnuxMode::MX) {
logger.Error("Following a collection from the broker is for --mode mx only");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
const char *token = std::getenv("JUNGFRAUJOCH_HTTP_TOKEN");
if (token == nullptr) {
logger.Error("{} does not exist yet, and following it from the broker needs the collection's token "
"in the environment variable JUNGFRAUJOCH_HTTP_TOKEN", input_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
broker_feed = std::make_unique<BrokerFeed>(broker_url, token);
// The broker's event stream starts with the start message: at once on a JUNGFRAU, when the
@@ -1989,11 +2016,11 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
const std::string given = std::filesystem::path(input_file).lexically_normal().generic_string();
if (given != master && !given.ends_with("/" + master)) {
logger.Error("The broker at {} is writing {}, not {}", broker_feed->Host(), master, input_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
if (!start.goniometer.has_value() || !start.goniometer->IsScanning()) {
logger.Error("Following a collection from the broker is for rotation data, and {} is not", master);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
live_reader.Open(start, input_file);
broker_feed->Attach(live_reader);
@@ -2008,7 +2035,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
}
} catch (const std::exception &e) {
logger.Error("Error reading input file: {}", e.what());
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
JFJochReader &reader = *reader_ptr;
@@ -2070,20 +2097,19 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
? fmt::format(", {:.3f} of them to a photon; intensities and sigmas in photons", adu_per_photon)
: std::string(", one to a photon"));
const auto dataset = reader.GetDataset();
dataset = reader.GetDataset();
if (!dataset) {
logger.Error("No experiment dataset found in the input file");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
if (rotation_indexing_image_count <= 0) {
logger.Error("Invalid number of rotation indexing images: {}", rotation_indexing_image_count);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
logger.Info("Loaded dataset from {}", input_file);
// Recorded in the reflection files.
OutputProvenance output_provenance;
output_provenance.command_line = provenance.command_line;
DescribeInput(input_file, output_provenance);
output_provenance.collection_date = dataset->arm_date;
@@ -2177,7 +2203,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
}
} catch (const std::exception &e) {
logger.Error("Error reading reference {}: {}", ref_mtz, e.what());
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
}
@@ -2188,7 +2214,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
const auto flags = LoadReferenceMtz(free_flags_file, std::nullopt);
if (!flags.has_free_flags) {
logger.Error("{} carries no R-free flags (no FreeR_flag-like column, no _refln.status)", free_flags_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
free_flags_data = flags.reflections;
logger.Info("R-free flags from {} (column {}): {} of {} free, taken by the merge where these data match "
@@ -2196,7 +2222,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
flags.reflections.size());
} catch (const std::exception &e) {
logger.Error("Error reading {}: {}", free_flags_file, e.what());
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
}
@@ -2216,7 +2242,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
|| input_is_xtc) {
logger.Error("--mode scale needs the integrated reflections in a _process.h5; "
"a raw image sweep holds none. Run --mode mx on it first.");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
const auto total_images = static_cast<int>(reader.GetNumberOfImages());
const int last_image = (end_image < 0 || end_image >= total_images) ? total_images - 1 : end_image;
@@ -2285,7 +2311,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
if (!experiment.GetUnitCell()) {
logger.Error("Experiment unit cell not found, cannot update reflection resolution");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
// The stored reflections have to be in the setting of the cell they are about to be merged in,
// or the merge's per-frame cell gate rejects every observation and the run dies far downstream
@@ -2327,7 +2353,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
"written before that was recorded)",
file_cell->a, file_cell->b, file_cell->c, file_cell->alpha,
file_cell->beta, file_cell->gamma);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
}
@@ -2351,7 +2377,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
"has, or reprocess the images with --mode mx.",
merge_sg->xhm(), uc.a, uc.b, uc.c, uc.alpha, uc.beta, uc.gamma,
violation, MAX_METRIC_VIOLATION);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
}
@@ -2789,12 +2815,12 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
if (image_stride < 0) {
logger.Error("Image stride cannot be negative");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
if (image_stride == 0) {
logger.Error("Image stride cannot be zero");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
// Round UP: the worker loop walks ordinals until start + ordinal * stride reaches end_image, so a
@@ -2809,7 +2835,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
}
// 2. Setup Experiment & Components
DiffractionExperiment experiment(dataset->experiment);
experiment = DiffractionExperiment(dataset->experiment);
// Geometry overrides (default: keep the value stored in the input file). Applied before the
// azimuthal-integration settings are derived, which depend on the geometry.
@@ -2845,20 +2871,20 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
}
if (modules.empty()) {
logger.Error("--module-geometry {} describes fewer than two modules", module_geometry_file);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
for (const auto &m: modules.Get())
if (m.x0 < 0 || m.y0 < 0 || m.x0 + m.width > experiment.GetXPixelsNum()
|| m.y0 + m.height > experiment.GetYPixelsNum()) {
logger.Error("--module-geometry {}: a module lies outside the {} x {} image", module_geometry_file,
experiment.GetXPixelsNum(), experiment.GetYPixelsNum());
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
experiment.Modules(modules);
logger.Info("--module-geometry: {} modules from {}", modules.size(), module_geometry_file);
} catch (const std::exception &e) {
logger.Error("Cannot read --module-geometry {}: {}", module_geometry_file, e.what());
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
}
if (!experiment.GetModules().empty())
@@ -2927,7 +2953,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
result = process.Run(console.get());
} catch (const std::exception &e) {
logger.Error("Processing failed: {}", FailureText(e));
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
g_active_process = nullptr;
@@ -3213,7 +3239,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
if (force_still) {
if (rotation_indexing) {
logger.Error("--force-still conflicts with -R / --single-pass-rotation / --force-rotation-lattice");
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
if (has_goniometer)
logger.Info("--force-still: treating the rotation dataset as independent stills");
@@ -3235,7 +3261,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
"measured over frames the sweep never recorded. Process the sweep whole, or select "
"a contiguous range with -s/-e. --mode azint and --force-still still take a stride.",
image_stride);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
// Scaling and merging are on by default (run_scaling initialised true); --no-merge turns them off
@@ -3495,7 +3521,6 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
// Run the shared full-analysis workflow (rotation indexing + scaling/merging live in
// Rugnux; the experiment above carries all algorithm settings).
ProcessConfig config;
config.mode = ProcessMode::FullAnalysis;
config.provenance = output_provenance;
config.start_image = start_image;
@@ -3548,7 +3573,6 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
config.refine_solvent = refine_solvent;
// When merging, the merged reflections (.mtz/.cif) are the wanted output; skip the large
// _process.h5 unless explicitly requested. Without merging, the _process.h5 is the only output.
std::optional<ProcessFileSink> sink;
if (!output_prefix.empty() && (run_scaling ? write_process_h5_flag : true))
config.image_sink = &sink.emplace(output_prefix);
config.finalist_ledger = finalist_ledger_flag;
@@ -3564,6 +3588,11 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
WarmUpModelValidation(model, static_cast<size_t>(config.nthreads), logger);
simulated_start = std::chrono::steady_clock::now();
return RUGNUX_CONTINUE;
}
int RugnuxInputRun::RunPreparedInput() {
JFJochReader &reader = *reader_ptr;
Rugnux process(reader, experiment, *dataset->pixel_mask, config);
g_active_process = &process;
@@ -3602,11 +3631,11 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
provenance);
console->Fail();
PrintSummary(*console, doc, output_prefix, files_since, provenance.wall_time_s);
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
} catch (const std::exception &e) {
logger.Error("Processing failed: {}", FailureText(e));
sink.reset();
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
g_active_process = nullptr;
@@ -3626,7 +3655,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
logger);
} catch (const std::exception &e) {
logger.Error("Processing failed: {}", FailureText(e));
exit(EXIT_FAILURE);
throw RugnuxInputFailed{EXIT_FAILURE};
}
}
@@ -3770,6 +3799,195 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) {
return EXIT_SUCCESS;
}
int RugnuxCli::Run() {
// The sweeps the inputs are (RugnuxSweeps.h). One input, a stills run and anything the readers refuse
// are read as they were.
const auto describe_file = [this](const std::string &f) {
RugnuxInputDescription d;
if (!crystfel_geometry.empty())
d.foreign = true;
else if (JFJochCBFReader::CanRead(f) || JFJochMarCCDReader::CanRead(f) || JFJochSMVReader::CanRead(f))
d.frame_template = RugnuxFrameTemplate(f);
else
d.foreign = JFJochXFELReader::CanReadPanelTable(f) || JFJochXFELReader::CanReadCXI(f)
|| JFJochXTCReader::CanRead(f);
return d;
};
// What a sweep is read as, for its rotation and wavelength: the reader the run itself would take.
const auto read_dataset = [](const std::string &f) -> std::shared_ptr<const JFJochReaderDataset> {
if (JFJochCBFReader::CanRead(f)) {
JFJochCBFReader r;
r.ReadFiles(f);
return r.GetDataset();
}
if (JFJochMarCCDReader::CanRead(f)) {
JFJochMarCCDReader r;
r.ReadFiles(f);
return r.GetDataset();
}
if (JFJochSMVReader::CanRead(f)) {
JFJochSMVReader r;
r.ReadFiles(f);
return r.GetDataset();
}
JFJochHDF5Reader r;
r.ReadFile(f);
return r.GetDataset();
};
const auto describe_sweep = [&read_dataset](const std::string &f) {
std::shared_ptr<const JFJochReaderDataset> dataset;
try {
dataset = read_dataset(f);
} catch (const std::exception &e) {
throw std::runtime_error(fmt::format("{} cannot be read: {}", f, e.what()));
}
RugnuxInputDescription d;
if (dataset) {
const auto g = dataset->experiment.GetGoniometer();
d.rotation = g.has_value() && g->GetIncrement_deg() != 0.0f;
d.wavelength_A = dataset->experiment.GetWavelength_A();
}
return d;
};
RugnuxSweepPlan plan;
try {
plan = PlanRugnuxSweeps(input_files, describe_file, describe_sweep);
} catch (const std::exception &e) {
logger.Error("{}", e.what());
return EXIT_FAILURE;
}
if (plan.sweeps.size() < 2) {
// Frames of one sweep: any of them names it.
if (plan.sweeps.size() == 1)
input_files = {plan.sweeps[0].input};
try {
return RugnuxInputRun(*this).Process();
} catch (const RugnuxInputFailed &f) {
return f.code;
}
}
for (size_t k = 0; k < plan.sweeps.size(); k++)
logger.Info("Sweep {}: {}", k + 1, plan.sweeps[k].label);
for (const auto &s: RugnuxSiblingSweeps(plan, {".cbf", ".img", ".mccd", ".osc"}))
logger.Info("Also beside these inputs, not given and so not processed: {}", s);
if (start_image != 0 || end_image >= 0) {
logger.Error("-s / -e select images of one sweep; with several sweeps give each sweep's frames instead");
return EXIT_FAILURE;
}
if (force_still) {
logger.Error("--force-still processes one rotation sweep as stills; give the sweeps one at a time");
return EXIT_FAILURE;
}
if (sweeps_mode == RugnuxSweepsMode::Joint) {
logger.Error("{} sweeps: processing them as one dataset is not implemented yet. --sweeps separate "
"processes each sweep alone, into <prefix>_s<k>_*", plan.sweeps.size());
return EXIT_FAILURE;
}
// --sweeps separate: each sweep alone, as if it had been the one input, into <prefix>_s<k>.
int failed = 0;
for (size_t k = 0; k < plan.sweeps.size(); k++) {
RugnuxInputRun sweep(*this);
sweep.input_files = {plan.sweeps[k].input};
// Each sweep's report times that sweep; the first also the start-up before it.
if (k > 0)
sweep.invocation_start = std::chrono::steady_clock::now();
if (!output_prefix.empty())
sweep.output_prefix = output_prefix + "_s" + std::to_string(k + 1);
logger.Info("Sweep {} of {}: {} into {}_*", k + 1, plan.sweeps.size(), plan.sweeps[k].label,
sweep.output_prefix);
int rc;
try {
rc = sweep.Process();
} catch (const RugnuxInputFailed &f) {
rc = f.code;
}
if (sweep.log_file_sink)
log_sinks->remove_sink(sweep.log_file_sink);
if (rc != EXIT_SUCCESS) {
logger.Error("Sweep {} ({}) failed", k + 1, plan.sweeps[k].label);
failed++;
}
}
if (failed > 0)
logger.Error("{} of {} sweeps failed", failed, plan.sweeps.size());
return failed > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}
// The body of main. Settings setters and the pipeline itself throw JFJochException on input the parser
// cannot reject on its own (a resolution limit of zero, a distance of zero, a polarisation above one,
// an unreadable file), so main wraps this and reports rather than letting the exception terminate the
// process with no diagnostic and exit code 134.
static int RunRugnux(int argc, char **argv, bool no_gpu) {
// cuFFT plans are reused through the run and destroyed with it, ahead of the device reset in main.
FFTPlanReuse fft_plans;
// The whole invocation, reported at the end and written into the results report. Started before
// anything else so it covers reading the file and setting up as well as the processing.
const auto invocation_start = std::chrono::steady_clock::now();
RunProvenance provenance;
provenance.command_line = JoinCommandLine(argc, argv);
provenance.gpu_count = get_gpu_count();
#ifdef __APPLE__
// Without CUDA, the Bragg integration's integer pixel work runs on the Apple GPU (MXAnalysisWithoutFPGA).
const std::string metal_device = BraggIntegrationEngineMetal::DeviceName();
#else
const std::string metal_device;
#endif
provenance.gpu_description = get_gpu_description();
// The terminal shows the run as steps (RugnuxConsole) and every Logger writes to the log file
// instead; with -v the whole log is on the terminal as well. -v is looked for here, before the
// options are parsed, because the parser already logs.
bool verbose = false;
for (int i = 1; i < argc; i++)
if (std::strcmp(argv[i], "-v") == 0 || std::strcmp(argv[i], "--verbose") == 0)
verbose = true;
console = std::make_shared<RugnuxConsole>(verbose);
std::atexit(CloseConsoleOnExit);
// Files with an older time than this are not this run's (the margin covers coarse file times).
const auto files_since = std::filesystem::file_time_type::clock::now() - std::chrono::seconds(2);
auto log_sinks = std::make_shared<spdlog::sinks::dist_sink_mt>();
// What is logged before the log file can be opened - its name comes from -o - waits here.
auto early_log = std::make_shared<spdlog::sinks::ringbuffer_sink_mt>(10000);
if (verbose)
log_sinks->add_sink(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
else
log_sinks->add_sink(MakeConsoleSink(console));
log_sinks->add_sink(early_log);
Logger::RouteAll(log_sinks);
// The HDF5 library prints its own error stack on stderr; every failure it reports reaches the
// reader as an error code anyway, which is turned into a message there.
H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr);
PrintBanner(*console);
Logger logger("rugnux");
#ifdef JFJOCH_USE_CUDA
const char *build = "CUDA build";
#else
const char *build = "CPU build";
#endif
logger.Info("rugnux Version {}{} - {}", jfjoch_version(),
jfjoch_git_sha1().empty() ? std::string()
: fmt::format(" (git {} {})", jfjoch_git_sha1().substr(0, 6), jfjoch_git_date()), build);
logger.Info("Build flags: {}", jfjoch_build_cxx_flags().empty() ? "none" : jfjoch_build_cxx_flags());
logger.Info("Build: {}", jfjoch_build_info(true));
logger.Info("Command line: {}", provenance.command_line);
logger.Info("GPU: {}", no_gpu ? std::string("not used (--no-gpu) - running on the CPU")
: provenance.gpu_count > 0 ? fmt::format("{} ({} visible)", provenance.gpu_description, provenance.gpu_count)
: !metal_device.empty() ? fmt::format("{} through Metal, for the Bragg integration only - the rest runs on the CPU", metal_device)
: std::string("none visible - running on the CPU"));
RegisterHDF5Filter();
RugnuxCli cli(no_gpu, invocation_start, provenance, metal_device, files_since, log_sinks, early_log);
if (const int rc = cli.Parse(argc, argv); rc != RUGNUX_CONTINUE)
return rc;
return cli.Run();
}
int main(int argc, char **argv) {
try {
// Before anything creates a CUDA context, which fixes the flags for the device. A thread
+1
View File
@@ -212,6 +212,7 @@ ADD_EXECUTABLE(jfjoch_test
WriteReflectionsTest.cpp
RugnuxResultFilesTest.cpp
RugnuxInputKindTest.cpp
RugnuxSweepsTest.cpp
UnitCellTest.cpp
CCTest.cpp
MultiLatticeSearchTest.cpp
+132
View File
@@ -0,0 +1,132 @@
// 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);
}