Files
Jungfraujoch/rugnux/RugnuxCommandLine.cpp
T
leonarski_fandClaude Opus 5.5 4bf41983ed rugnux: the model is parsed by the caller and handed in as RugnuxModel
The analysis read the --model file itself, four to five times per run
(the model kind, every validation, molecular replacement, the stills
reference intensities) and located the PAE sidecar from the file name.
The library is to do no file I/O, so the model now arrives in memory:

- RugnuxModel (image_analysis/structure_refinement/RugnuxModel.h): the
  gemmi structure as read, whether it is ModelCIF, the PAE matrix where a
  prediction came with one, and the name the report gives it.
- ParseRugnuxModel / ReadRugnuxModelFile (writer/RugnuxModelFile.h, in
  JFJochRugnuxFiles): bytes in, gzip by magic, PDB/mmCIF by content,
  _ma_qa_metric detection as before; the file reader takes the PAE JSON
  from the same place PaePathFor named.
- ProcessConfig::model replaces model_path; PrepareSearchModel,
  RunMolecularReplacement, ValidateAgainstModel and
  ModelReferenceIntensities take the model; result fields name it by
  model_name.
- rugnux reads the model before the run starts and stops with an error
  where the file holds no model (it used to run and report the model as
  not validated). The viewer reads it when the job is set up.
- The broker parses an uploaded model at once and answers 400 where it is
  not one; it keeps the parsed model in memory, with no temporary
  directory or file. It takes no PAE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:11:58 +02:00

254 lines
13 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "RugnuxCommandLine.h"
#include "../common/DiffractionExperiment.h"
#include <cmath>
#include <sstream>
#include <vector>
namespace {
std::string quote_if_needed(const std::string &s) {
if (s.find_first_of(" \t\"'") == std::string::npos)
return s;
std::string out = "\"";
for (char c: s) {
if (c == '"' || c == '\\')
out += '\\';
out += c;
}
out += '"';
return out;
}
const char *indexing_alg_flag(IndexingAlgorithmEnum a) {
switch (a) {
case IndexingAlgorithmEnum::FFBIDX: return "ffbidx";
case IndexingAlgorithmEnum::FFT: return "cufft"; // the GPU implementation; rugnux's -X fft is the algorithm
case IndexingAlgorithmEnum::FFTW: return "fftw";
case IndexingAlgorithmEnum::None: return "none";
case IndexingAlgorithmEnum::Auto:
default: return "auto";
}
}
const char *refine_flag(GeomRefinementAlgorithmEnum r) {
switch (r) {
case GeomRefinementAlgorithmEnum::None: return "none";
case GeomRefinementAlgorithmEnum::OrientationOnly: return "orientation";
case GeomRefinementAlgorithmEnum::Flex: return "flex";
case GeomRefinementAlgorithmEnum::BeamCenter:
default: return "beam_and_lattice";
}
}
std::string num(double v) {
std::ostringstream o;
o << v;
return o.str();
}
}
std::string RugnuxCommandLine(const ProcessConfig &config,
const DiffractionExperiment &experiment,
const std::string &input_file,
const std::string &calibrant_name) {
std::vector<std::string> args;
const bool azint = (config.mode == ProcessMode::AzimuthalIntegration);
const bool calibration = (config.mode == ProcessMode::Calibration);
args.emplace_back("rugnux");
if (azint) {
args.emplace_back("--mode");
args.emplace_back("azint");
} else if (calibration) {
args.emplace_back("--mode");
args.emplace_back("calibration");
}
auto add = [&](const std::string &flag, const std::string &val) {
args.push_back(flag);
args.push_back(val);
};
if (!config.output_prefix.empty())
add("-o", config.output_prefix);
// Every CPU the run may use is what rugnux takes without -N, on whatever machine runs the command.
if (!config.nthreads_auto)
add("-N", std::to_string(config.nthreads));
if (!azint && !calibration) {
if (!config.reference_path.empty()) {
add("-z", config.reference_path);
if (!config.reference_column.empty())
add("--reference-column", config.reference_column);
}
if (config.model)
add(config.force_mr ? "--mr" : "--model", config.model->name);
}
if (config.start_image != 0)
add("-s", std::to_string(config.start_image));
if (config.end_image >= 0)
add("-e", std::to_string(config.end_image));
if (config.stride != 1)
add("-t", std::to_string(config.stride));
// Beam-stop detection is ON by default in the CLI, so emit only a deviation from that. getopt takes
// an optional argument only when attached (=N), never as a separate token.
if (!config.detect_beam_stop.has_value())
args.emplace_back("--detect-beam-stop=off");
else if (*config.detect_beam_stop != 60)
args.push_back("--detect-beam-stop=" + std::to_string(*config.detect_beam_stop));
if (calibration) {
if (!calibrant_name.empty())
add("--calibrant", calibrant_name);
add("--calibration", config.calibration_method == CalibrationMethod::Rings ? "rings" : "spots");
if (!config.calibration_refine_tilt)
args.emplace_back("--no-refine-tilt");
// The rings method reads the ring at every azimuth, and the GUI may have raised the sector
// count for this run, so emit the value actually used rather than relying on the CLI default.
if (config.calibration_method == CalibrationMethod::Rings)
add("--azim-phi-bins",
std::to_string(experiment.GetAzimuthalIntegrationSettings().GetAzimuthalBinCount()));
} else if (azint) {
const auto a = experiment.GetAzimuthalIntegrationSettings();
add("--azim-min-q", num(a.GetLowQ_recipA()));
// An unset maximum Q means "to the detector edge"; emitting the resolved number would pin it
// to this run's geometry, so leave the flag out and let it resolve again.
if (const auto high_q = a.GetRequestedHighQ_recipA())
add("--azim-max-q", num(*high_q));
add("--azim-q-spacing", num(a.GetQSpacing_recipA()));
add("--azim-phi-bins", std::to_string(a.GetAzimuthalBinCount()));
add("--polarization-correction", a.IsPolarizationCorrection() ? "on" : "off");
add("--solid-angle-correction", a.IsSolidAngleCorrection() ? "on" : "off");
} else {
const auto &sf = config.spot_finding;
add("--spot-sigma", num(sf.signal_to_noise_threshold));
add("--spot-threshold", std::to_string(sf.photon_count_threshold));
// Emit the explicit flag rather than relying on the default, so the command line reproduces
// this run even if the default changes.
args.emplace_back(sf.adaptive_threshold ? "--adaptive-spots" : "--no-adaptive-spots");
if (sf.adaptive_threshold)
add("--spot-false-pixels", num(sf.false_pixels_per_frame));
// min-pix is chosen per image unless an explicit value is given, so emit --min-pix-per-spot only
// when a fixed min-pix was selected; its absence selects the adaptive per-image path.
if (sf.min_pix_per_spot.has_value())
add("--min-pix-per-spot", std::to_string(*sf.min_pix_per_spot));
// Same for the spot-finding limit: absent means "as far as the detector reaches".
if (sf.high_resolution_limit.has_value())
add("--spot-high-resolution", num(*sf.high_resolution_limit));
// --max-spots pins the budget, which is otherwise measured from the data.
if (!config.measure_spot_budget)
add("--max-spots", std::to_string(experiment.GetMaxSpotCount()));
const auto idx = experiment.GetIndexingSettings();
add("-X", indexing_alg_flag(idx.GetAlgorithm()));
add("-r", refine_flag(idx.GetGeomRefinementAlgorithm()));
if (const auto sg = experiment.GetSpaceGroupNumber())
add("-S", std::to_string(*sg));
if (const auto uc = experiment.GetUnitCell()) {
std::ostringstream o;
o << uc->a << "," << uc->b << "," << uc->c << "," << uc->alpha << "," << uc->beta << "," << uc->gamma;
add("-C", o.str());
}
if (const auto bw = experiment.GetBandwidthFWHM())
add("--bandwidth", num(*bw));
const auto bragg = experiment.GetBraggIntegrationSettings();
// An --integration-radius would switch off the radius the pre-scan measures.
if (!config.adaptive_integration_radius) {
std::ostringstream radii;
radii << bragg.GetR1() << "," << bragg.GetR2() << "," << bragg.GetR3();
add("--integration-radius", radii.str());
}
if (bragg.GetStencilKSigma() > 0.0f)
add("--integration-stencil", num(bragg.GetStencilKSigma()));
// Background ring: the CLI defaults to the 4 sigma high-side clip, so emit a flag only when the
// GUI chose otherwise. The trim is the alternative estimator (it clears the clip), and a clip of
// 0 with no trim is the plain ring mean, which needs the flag to be reproduced.
if (bragg.GetBackgroundTrimFraction() > 0.0f)
add("--background-trim", num(bragg.GetBackgroundTrimFraction()));
else if (bragg.GetBackgroundClipNSigma() != BraggIntegrationSettings().GetBackgroundClipNSigma())
add("--background-clip", num(bragg.GetBackgroundClipNSigma()));
// Air by default, so emit the flag only for the other media - which nothing in the file can
// state, and which therefore has to travel with the command line to reproduce the run.
if (bragg.GetFlightPath() == FlightPathMedium::Helium)
add("--flight-path", "helium");
else if (bragg.GetFlightPath() == FlightPathMedium::Vacuum)
add("--flight-path", "vacuum");
if (const auto max_hkl = bragg.GetMaxHKL())
add("--max-hkl", std::to_string(*max_hkl));
// Unset means "to the detector edge"; emitting the resolved number would pin it to this run's
// geometry, so leave the flag out and let it resolve again.
if (const auto d_min = bragg.GetDMinLimit_A())
add("--integration-high-resolution", num(*d_min));
if (config.rotation_indexing) {
if (config.two_pass_rotation)
// -R takes an optional argument, which getopt only accepts attached (-R100), never as a
// separate token - so emit it joined or the copied command line will not re-parse.
args.push_back("-R" + std::to_string(config.rotation_indexing_image_count));
else
args.emplace_back("--single-pass-rotation");
} else if (experiment.GetGoniometer().has_value()) {
// rotation dataset processed as stills -> the user overrode the default with --force-still
args.emplace_back("--force-still");
}
// Stills geometry-refinement two-pass (--refine-geometry). getopt takes its optional argument
// only when attached (=N), never as a separate token, so emit it joined. The CLI defaults it ON
// for a stills-with-cell run, so emit =off when the GUI turned it off in that same case, to
// reproduce the GUI's choice in the copied command line.
const bool stills_with_cell = !config.rotation_indexing && experiment.GetUnitCell().has_value();
if (config.refine_geometry.has_value())
args.push_back("--refine-geometry=" + std::to_string(*config.refine_geometry));
else if (stills_with_cell)
args.emplace_back("--refine-geometry=off");
// Rotation two-pass geometry post-refine. The CLI defaults it ON for a rotation run, so emit the
// disable flag only when the GUI turned it off on a rotation dataset (to reproduce that choice).
if (config.rotation_indexing && !config.rotation_postrefine_geometry)
args.emplace_back("--rotation-no-postrefine");
if (config.rotation_scale.has_value())
add("--rotation-scale", num(*config.rotation_scale));
// Merging is on by default; emit --no-merge only when it was turned off.
if (config.run_scaling) {
const auto sc = experiment.GetScalingSettings();
if (!sc.GetMergeFriedel())
args.emplace_back("-A");
if (!sc.GetStillsPartialityRefine())
args.emplace_back("--simple-stills");
if (!sc.GetExpectedVarianceMerge())
args.emplace_back("--no-expected-variance-merge");
// When merging, the CLI skips the large _process.h5 unless asked; emit the flag when it is
// wanted so a copied command matches the GUI's "Save _process.h5" choice. (write_merged has
// no CLI equivalent - the CLI always writes the .mtz/.cif when merging.)
if (config.write_process_h5)
args.emplace_back("--write-process-h5");
// The low-resolution limit is on by default at the same value in both, so emit it only when
// it differs - including 0, which is how the CLI spells "no limit".
const auto d_max = sc.GetLowResolutionLimit_A();
const auto d_max_default = ScalingSettings().GetLowResolutionLimit_A();
if (d_max != d_max_default)
args.push_back("--scaling-low-resolution=" + num(d_max.value_or(0.0)));
// An infinite d_max is the default of a run with no low limit, so d_min alone restates it.
if (const auto r = sc.GetReportResolutionRange_A())
args.push_back("--report-resolution=" + num(r->d_min)
+ (std::isfinite(r->d_max) ? "," + num(r->d_max) : ""));
} else {
args.emplace_back("--no-merge");
}
}
args.push_back(input_file);
std::ostringstream cmd;
for (size_t i = 0; i < args.size(); i++) {
if (i)
cmd << ' ';
cmd << quote_if_needed(args[i]);
}
return cmd.str();
}