Files
Jungfraujoch/rugnux/RugnuxCommandLine.cpp
T
leonarski_fandClaude Opus 5 e8a7696ffe Viewer: process a dataset with the same defaults the CLI uses
"Analyze dataset" and `rugnux` with no options are two front ends onto the same
library and are meant to agree, but they decided their defaults separately and
the two lists had drifted. The viewer was missing:

- the de-novo starting point. The CLI discards the cell and space group stored
  in the input file before it does anything; the viewer left them on the
  experiment. Rugnux only searches for a space group when none is set, so the
  search was skipped entirely and the stored group was reported straight back.
  That is self-reinforcing: a finished job's own _process.h5 becomes the active
  snapshot, so a run that ended in P1 pinned every later run to P1 - which is
  what "lysozyme keeps coming out P1 in the viewer" was.
- the polarization factor, so the Lp correction was omitted altogether. The
  missing factor is azimuthal and intensity-proportional, and symmetry mates sit
  at the same 2-theta but different azimuth - the exact "unequal intensities
  forced together" signature the space-group search vetoes as pseudo-symmetry,
  which can land a genuinely de-novo run in P1 on its own.
- five rotation scaling defaults: the smooth-G range, the minimum captured
  fraction, the capture-aware sigma, outlier rejection and --search-min-zeta.
  The CLI's own comments tie the captured-fraction default to a crystal
  recovering its true space group instead of P1.

Put the policy in one place (RugnuxDefaults) and have both front ends start from
it. The CLI now takes its defaults from there and applies user options on top;
its output is unchanged, verified bit-for-bit on four battery crystals.

The stored cell/group is still available: the job dialog offers "Use the stored
unit cell / space group", off by default, shown only when the file has one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:07:14 +02:00

186 lines
8.5 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 <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 "fft";
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) {
std::vector<std::string> args;
const bool azint = (config.mode == ProcessMode::AzimuthalIntegration);
args.emplace_back("rugnux");
if (azint)
args.emplace_back("--azint-only");
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);
add("-N", std::to_string(config.nthreads));
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));
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));
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();
std::ostringstream radii;
radii << bragg.GetR1() << "," << bragg.GetR2() << "," << bragg.GetR3();
add("--integration-radius", radii.str());
// Background trim defaults to 0.10 in the CLI, so emit it whenever the GUI value differs (a
// custom fraction, or 0 when the box is unchecked) to reproduce the GUI's choice faithfully.
if (bragg.GetBackgroundTrimFraction() != 0.10f)
add("--background-trim", num(bragg.GetBackgroundTrimFraction()));
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");
if (!config.reuse_rotation_spots)
args.emplace_back("--redo-rotation-spots");
} 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");
// 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");
} 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();
}