diff --git a/docs/RUGNUX_CLI.md b/docs/RUGNUX_CLI.md index 9a73a5532..6081bc20d 100644 --- a/docs/RUGNUX_CLI.md +++ b/docs/RUGNUX_CLI.md @@ -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 +`_s_*` (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; `_log.txt` is written either way | | `--simulate-live ` | 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 `_s_*`; `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 diff --git a/rugnux/CMakeLists.txt b/rugnux/CMakeLists.txt index 396d610f5..9f1b96385 100644 --- a/rugnux/CMakeLists.txt +++ b/rugnux/CMakeLists.txt @@ -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 diff --git a/rugnux/RugnuxSweeps.cpp b/rugnux/RugnuxSweeps.cpp new file mode 100644 index 000000000..1f88a5de4 --- /dev/null +++ b/rugnux/RugnuxSweeps.cpp @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "RugnuxSweeps.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +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(name[end - 1]))) + end--; + if (end == 0) + return {}; + size_t start = end; + while (start > 0 && std::isdigit(static_cast(name[start - 1]))) + start--; + name.replace(start, end - start, std::string(end - start, '#')); + return (p.parent_path() / name).string(); +} + +RugnuxSweepPlan PlanRugnuxSweeps(const std::vector &inputs, + const std::function &describe_file, + const std::function &describe_sweep) { + RugnuxSweepPlan plan; + if (inputs.size() < 2) + return plan; + + std::vector 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 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 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 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 RugnuxSiblingSweeps(const RugnuxSweepPlan &plan, const std::vector &extensions) { + namespace fs = std::filesystem; + std::set 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 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 out; + for (const auto &[name, n]: found) + out.push_back(n > 0 ? fmt::format("{} ({} frame{})", name, n, n == 1 ? "" : "s") : name); + return out; +} diff --git a/rugnux/RugnuxSweeps.h b/rugnux/RugnuxSweeps.h new file mode 100644 index 000000000..eec15b1a7 --- /dev/null +++ b/rugnux/RugnuxSweeps.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include +#include + +// 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 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 &inputs, + const std::function &describe_file, + const std::function &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 RugnuxSiblingSweeps(const RugnuxSweepPlan &plan, const std::vector &extensions); diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 0530a68e4..1f58d54fa 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -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 _s (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 _s_*; processing them as one dataset is to come -v, --verbose Whole log on the terminal (it is always in _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(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(); - // What is logged before the log file can be opened - its name comes from -o - waits here. - auto early_log = std::make_shared(10000); - if (verbose) - log_sinks->add_sink(std::make_shared()); - 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 log_sinks; + std::shared_ptr 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 outlier_reject_nsigma; // merge per-observation outlier rejection + std::vector input_files; + std::string broker_url; + std::shared_ptr 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 log_sinks_, + std::shared_ptr 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 cufft_warmup, context_warmup; + SimulatedLive hdf5_reader; + SimulatedLive cbf_reader; + SimulatedLive marccd_reader; + SimulatedLive smv_reader; + SimulatedLive xfel_reader; + SimulatedLive xtc_reader; + JFJochStartMessageReader live_reader; + std::unique_ptr multi_reader; + std::unique_ptr broker_feed; + JFJochReader *reader_ptr = nullptr; + std::shared_ptr dataset; + OutputProvenance output_provenance; + DiffractionExperiment experiment; + ProcessConfig config; + std::optional sink; + // The log file of this input, taken out of the log again once it is done. + std::shared_ptr 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(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 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 model; + model.reset(); if (const std::string path = mr_model.empty() ? model_pdb : mr_model; !path.empty()) { try { model = std::make_shared(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 hdf5_reader; - SimulatedLive cbf_reader; - SimulatedLive marccd_reader; - SimulatedLive smv_reader; - SimulatedLive xfel_reader; - SimulatedLive xtc_reader; - JFJochStartMessageReader live_reader; - std::unique_ptr multi_reader; - std::unique_ptr 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(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(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 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(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 { + 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 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 _s_*", plan.sweeps.size()); + return EXIT_FAILURE; + } + + // --sweeps separate: each sweep alone, as if it had been the one input, into _s. + 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(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(); + // What is logged before the log file can be opened - its name comes from -o - waits here. + auto early_log = std::make_shared(10000); + if (verbose) + log_sinks->add_sink(std::make_shared()); + 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b44df7a59..63599b77e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -212,6 +212,7 @@ ADD_EXECUTABLE(jfjoch_test WriteReflectionsTest.cpp RugnuxResultFilesTest.cpp RugnuxInputKindTest.cpp + RugnuxSweepsTest.cpp UnitCellTest.cpp CCTest.cpp MultiLatticeSearchTest.cpp diff --git a/tests/RugnuxSweepsTest.cpp b/tests/RugnuxSweepsTest.cpp new file mode 100644 index 000000000..a46541304 --- /dev/null +++ b/tests/RugnuxSweepsTest.cpp @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include + +#include +#include +#include +#include + +#include "../rugnux/RugnuxSweeps.h" + +namespace { + +// Inputs described by name alone: "