Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands. * `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion. * Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants. * `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing. * A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed. * `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing. * Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences. * The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to. * The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after. * The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution. * `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have. * Twinning is no longer reported when the L-test contradicts it. * The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's. * `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. Reviewed-on: #76 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
233 lines
11 KiB
C++
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();
|
|
}
|