Files
Jungfraujoch/rugnux/RugnuxCommandLine.cpp
T
leonarski_fandClaude Opus 5 03456dcddd calibration: an option to hold the detector tilt fixed
--mode calibration fits five parameters - beam centre, distance and the two
PONI tilts - and a program that cannot express a tilted detector has nowhere to
put the last two. Dropping them after the fact is worse than never fitting
them: the centre and the distance of a tilted fit have already absorbed the
tilt, so the flattened geometry is right nowhere.

rugnux --no-refine-tilt, the "Refine detector tilt" tick box on the viewer's
Calib page and RingOptimizer's refine_tilt argument hold rot1/rot2 at the value
the geometry came in with and fit the remaining three. That is the best
flat-detector answer, and the one such a program would refine to itself.

Measured on a five-distance calibrant series. At short distance the tilt is
real and reproducible - three independent fits agreeing to 0.01 deg, radial rms
1.4 -> 0.4 px - and its direct beam agrees with the background beam-centre
estimator to 0.05 px, so the tilted model is the physically right one. The
pinned fit then displaces the centre 2.6 px to absorb the tilt and lands within
0.03 px of the same place at every distance.

Past ~300 mm, where only two rings reach the detector, the tilt is instead
under-determined: it comes out with the opposite sign to every short-distance
fit and drags the PONI 28 px while the rms does not move (0.960 against 0.962).
The existing degeneracy guard only fires on a strictly single ring, so it does
not catch that; declining a tilt that does not pay for itself in rms is left
for a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxrrPcxodNiXzhNiECCVp5
2026-08-30 20:44:12 +02:00

233 lines
11 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,
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);
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));
// 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));
// Measuring the beam centre is OFF by default in the CLI, and the flag fits the spindle with it.
if (config.estimate_beam_center) {
args.emplace_back("--estimate-beam-center");
if (!config.fit_spindle)
args.emplace_back("--no-fit-spindle");
}
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));
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());
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()));
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)));
} 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();
}