From b5c6ea7990c0cdc0365c4672d115c5624201f397 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 15 Oct 2026 15:34:11 +0200 Subject: [PATCH] Viewer: "Analyze dataset" starts from the defaults of a bare rugnux The viewer's processing job had drifted from `rugnux `: 60 instead of 100 first-pass rotation images, no adaptive integration radius, a fixed spot budget instead of a measured one, the per-image loop never capped (nthreads_auto), stills radii 4/6/13 instead of 6/8/14, ice-ring flagging taken as off for HDF5 files that do not state it (rugnux: on for rotation), the background radial correction off (rugnux: auto), counts_in_adu never set for marCCD/SMV, 8192 instead of 16384 FFT directions on a CPU-only host, and a stationary goniometer axis processed as rotation. The defaults both front ends share now live in RugnuxDefaults (spot budget, beam-stop frames, rotation first-pass images, stills geometry frames, bare-run spot finding, stills radii, ice-ring default, radial correction auto), and the CLI and the viewer both read them. The CLI's own behaviour is unchanged. The viewer's only intended difference stays: it writes _process.h5. The copied command line now leaves out -N, --max-spots and --integration-radius when the run left those to rugnux, so it reproduces the automatic choices instead of pinning them. Co-Authored-By: Claude Opus 5.5 --- rugnux/RugnuxCommandLine.cpp | 17 +++-- rugnux/RugnuxDefaults.cpp | 50 +++++++++++++ rugnux/RugnuxDefaults.h | 36 +++++++++ rugnux/rugnux_cli.cpp | 75 +++++-------------- tests/RugnuxTest.cpp | 55 ++++++++++++++ viewer/JFJochImageReadingWorker.cpp | 2 + viewer/JFJochImageReadingWorker.h | 3 + viewer/JFJochProcessController.cpp | 2 + viewer/JFJochViewerWindow.cpp | 21 +++--- viewer/widgets/JFJochViewerSettingsDock.cpp | 9 ++- viewer/widgets/JFJochViewerSettingsDock.h | 3 +- viewer/windows/JFJochProcessingJobsWindow.cpp | 37 +++++++-- viewer/windows/JFJochProcessingJobsWindow.h | 7 +- 13 files changed, 231 insertions(+), 86 deletions(-) diff --git a/rugnux/RugnuxCommandLine.cpp b/rugnux/RugnuxCommandLine.cpp index eb7c08495..1de2ddb02 100644 --- a/rugnux/RugnuxCommandLine.cpp +++ b/rugnux/RugnuxCommandLine.cpp @@ -73,7 +73,9 @@ std::string RugnuxCommandLine(const ProcessConfig &config, if (!config.output_prefix.empty()) add("-o", config.output_prefix); - add("-N", std::to_string(config.nthreads)); + // 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); @@ -134,7 +136,9 @@ std::string RugnuxCommandLine(const ProcessConfig &config, // 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())); + // --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())); @@ -151,9 +155,12 @@ std::string RugnuxCommandLine(const ProcessConfig &config, add("--bandwidth", num(*bw)); const auto bragg = experiment.GetBraggIntegrationSettings(); - std::ostringstream radii; - radii << bragg.GetR1() << "," << bragg.GetR2() << "," << bragg.GetR3(); - add("--integration-radius", radii.str()); + // 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 diff --git a/rugnux/RugnuxDefaults.cpp b/rugnux/RugnuxDefaults.cpp index 12ed7d605..09896a2f9 100644 --- a/rugnux/RugnuxDefaults.cpp +++ b/rugnux/RugnuxDefaults.cpp @@ -16,6 +16,15 @@ void ApplyRugnuxExperimentDefaults(DiffractionExperiment &experiment) { // NOT share, because they sit at the same 2-theta but a different azimuth - which is exactly the // "unequal intensities forced together" signature the space-group search vetoes as pseudo-symmetry. experiment.PolarizationFactor(0.99); + + // The background ring's radial curvature correction: auto, applied per image where the ice score + // sees a smooth ring. The library default stays off (the broker keeps it off); offline, over ice + // sets and clean controls, auto was a clear gain where it fired (smooth ice: CC to the model +0.015, + // R shell-scaled -0.019 on one set) and bit-identical where it did not, while forcing it on + // everywhere cost a clean set 0.008 in CC to the model. + BraggIntegrationSettings bis = experiment.GetBraggIntegrationSettings(); + bis.BackgroundRadialCorrection(std::nullopt); + experiment.ImportBraggIntegrationSettings(bis); } void ClearStoredCrystal(DiffractionExperiment &experiment) { @@ -51,3 +60,44 @@ ScalingSettings RugnuxDefaultScalingSettings(bool rotation) { return s; } + +SpotFindingSettings RugnuxDefaultSpotFinding(bool rotation) { + SpotFindingSettings s; + s.enable = true; + s.indexing = true; + s.adaptive_threshold = true; + // The high-resolution limit stays unset - as far as the detector reaches - for rotation as well as + // stills. Rotation used to keep 1.5 A on the strength of an indexing-rate measurement, but over the + // 33-crystal battery that limit changes nothing on 29 crystals, changes no space-group decision at + // all, and on the crystals where it does bite it is the LIMIT that is worse: the one crystal that + // loses appreciable indexing rate without it (99.5 -> 94.2%) comes back with better R_meas, better + // high-resolution CC1/2 and better ISa. Fewer frames, better data. + s.high_resolution_limit = std::nullopt; + s.min_pix_per_spot = rotation ? std::optional(2) : std::nullopt; + return s; +} + +void ApplyRugnuxIntegrationRadii(DiffractionExperiment &experiment, bool rotation) { + if (rotation) + return; + // Stills spots span a range of crystal orientations captured in a single shot, so they land + // wider on the detector than the r1=4 monochromatic-rotation default assumes. A larger signal + // box lets the profile-fit integrator capture the whole spot while its profile weighting keeps + // the extra background from adding noise (a plain box-sum degrades with it). Measured R-free + // gains on serial stills. + // + // r3 = 14 rather than 12: the reported sigma carries the ring mean's own error as + // (dI/dbkg)^2 * bkg/n_ring, and the ring only holds 244 pixels at r3 = 12 against the + // rotation default's 408, so that term is a larger share of the variance here than + // anywhere else. 14 takes the ring to about 410 pixels. What normally stops a ring being + // widened is crowding - a neighbour inside the annulus is excluded from it - and stills + // barely crowd: under a percent of stills reflections have any neighbour within 13 px, + // against most of them on rotation. + BraggIntegrationSettings bis = experiment.GetBraggIntegrationSettings(); + bis.R1(6.0f).R2(8.0f).R3(14.0f); + experiment.ImportBraggIntegrationSettings(bis); +} + +bool RugnuxDetectIceRings(std::optional file_detect_ice_rings, bool rotation) { + return file_detect_ice_rings.value_or(rotation); +} diff --git a/rugnux/RugnuxDefaults.h b/rugnux/RugnuxDefaults.h index a416bb869..4ea9a6fa8 100644 --- a/rugnux/RugnuxDefaults.h +++ b/rugnux/RugnuxDefaults.h @@ -5,6 +5,7 @@ #include "../common/DiffractionExperiment.h" #include "../common/ScalingSettings.h" +#include "../image_analysis/spot_finding/SpotFindingSettings.h" // What "process this dataset with nothing set" means, in one place. // @@ -16,6 +17,25 @@ // stored (P1, on a file written by an earlier P1 run). Both front ends now start here and then apply // their own explicit overrides on top. +// Spots kept per image (the strongest ones) and handed to indexing. Offline reprocessing is not +// bound by the online spot budget, so this is rugnux's own default rather than the 250 the +// DatasetSettings constructor uses for the detector. Measured over the 37-crystal rotation battery, +// lifting it to 1000 leaves low-resolution R_meas better or equal on every crystal (16 better, 0 +// worse, the rest untouched because their frames never reach the cap), with R_meas 14/4 and ISa 14/6 +// in its favour and no measurable cost in wall clock. A run left at this value measures its budget +// from the data instead (ProcessConfig::measure_spot_budget); --max-spots pins it. +constexpr int64_t RUGNUX_MAX_SPOT_COUNT = 1000; + +// Frames the beam-stop pre-scan projects (--detect-beam-stop, on by default). +constexpr int RUGNUX_DETECT_BEAM_STOP_FRAMES = 60; + +// Images the rotation first pass indexes the lattice on (-R). +constexpr int RUGNUX_ROTATION_INDEXING_IMAGES = 100; + +// Strong frames the stills geometry bundle adjustment takes (--refine-geometry), on by default for +// stills with a known cell. +constexpr int RUGNUX_REFINE_GEOMETRY_FRAMES = 200; + // Analysis policy. Output policy (file names, formats, compression) belongs to the caller - the CLI // and the viewer write different things in different places. A front end that exposes one of these as // a user option (the CLI's --polarization) applies it AFTER this call, or it is overwritten here. @@ -31,3 +51,19 @@ void ClearStoredCrystal(DiffractionExperiment &experiment); // Scaling/merging defaults for the workflow. Several differ between rotation and stills because the // rotation path has a 3D combine and the stills path does not; each is explained at its setter. ScalingSettings RugnuxDefaultScalingSettings(bool rotation); + +// Spot finding of a bare run: adaptive detection for both workflows, out to the detector edge. min-pix +// is chosen per image on stills; rotation indexing builds ONE lattice from all frames, so it keeps a +// fixed value. +SpotFindingSettings RugnuxDefaultSpotFinding(bool rotation); + +// Integration radii of a run that did not set them by hand. Stills widen the signal box (see the +// setter); rotation keeps the shipped radii and has r1 measured in the pre-scan instead +// (ProcessConfig::adaptive_integration_radius, which the caller sets to `rotation`). +void ApplyRugnuxIntegrationRadii(DiffractionExperiment &experiment, bool rotation); + +// Ice-ring flagging where the file says nothing about it: on for rotation, off for stills. A rotation +// sweep sits on the same rings for the whole run, so ice there is a coherent systematic worth handling, +// and the ice-presence gate keeps it inert on a clean crystal; a serial stills run has too few spots per +// image to spend any of them on flagging. A file that states it is followed. +bool RugnuxDetectIceRings(std::optional file_detect_ice_rings, bool rotation); diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 03b1a1edb..e011d3dec 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -76,13 +76,6 @@ #include "../image_analysis/bragg_integration/BraggIntegrationEngineMetal.h" #endif -// Spots kept per image (the strongest ones) and handed to indexing. Offline reprocessing is not -// bound by the online spot budget, so this is rugnux's own default rather than the 250 the -// DatasetSettings constructor uses for the detector. Measured over the 37-crystal rotation battery, -// lifting it to 1000 leaves low-resolution R_meas better or equal on every crystal (16 better, 0 -// worse, the rest untouched because their frames never reach the cap), with R_meas 14/4 and ISa 14/6 -// in its favour and no measurable cost in wall clock. It is also what jfjoch_viewer already sends. -constexpr int64_t RUGNUX_MAX_SPOT_COUNT = 1000; // Default rot3d per-frame scale-G smoothing range (XDS DELPHI-like), in degrees of rotation. constexpr double SMOOTH_G_DEFAULT_DEG = 5.0; @@ -941,7 +934,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { // lattice - the setting would appear to do nothing at all on rotation data. bool rotation_postrefine_geometry = true; // default on; --rotation-no-postrefine disables it std::optional rotation_scale; // --rotation-scale: asserted stage calibration - int rotation_indexing_image_count = 100; + int rotation_indexing_image_count = RUGNUX_ROTATION_INDEXING_IMAGES; double prepass_fraction = 0.5; std::optional rotation_indexing_range; std::optional min_indexed_spots; // --min-indexed-spots: spots on the lattice for a frame to count as indexed @@ -973,11 +966,11 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { const gemmi::SpaceGroup *space_group = nullptr; std::optional fixed_reference_unit_cell; std::optional max_spot_count_override; - float sigma_spot_finding = 4.0; - int64_t photon_count_threshold_spot_finding = 10; + float sigma_spot_finding = SpotFindingSettings{}.signal_to_noise_threshold; + int64_t photon_count_threshold_spot_finding = SpotFindingSettings{}.photon_count_threshold; std::optional min_pix_per_spot; // unset -> adaptive per image; a value -> fixed min-pix std::optional adaptive_spots; // unset -> on, for both workflows - float false_pixels_per_frame = 100.0f; + float false_pixels_per_frame = SpotFindingSettings{}.false_pixels_per_frame; std::string ref_mtz; std::string ref_column; std::string model_pdb; // --model: PDB to validate merged intensities against (R-free + maps) @@ -996,7 +989,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { std::optional forced_rotation_lattice; std::optional background_clip_arg; // --background-clip: background-ring high-side sigma clip std::optional background_radial_arg; // set = force on/off, unset = auto (the default) - std::optional detect_beam_stop = 60; // --detect-beam-stop[=N|off]; on by default + std::optional detect_beam_stop = RUGNUX_DETECT_BEAM_STOP_FRAMES; // --detect-beam-stop[=N|off]; on by default bool beam_center_check = true; // --beam-center-check[=off]; on by default std::optional refine_geometry; // --refine-geometry[=N]: stills global geometry-refinement pass bool refine_geometry_disabled = false; // --refine-geometry=off: opt out of the stills default-on @@ -1101,7 +1094,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { } detect_beam_stop = optarg ? parse_number_arg(optarg, "--detect-beam-stop", logger, 1, 1000000) - : 60; + : RUGNUX_DETECT_BEAM_STOP_FRAMES; break; case OPT_BEAM_CENTER_CHECK: beam_center_check = !optarg || std::string(optarg) != "off"; @@ -1116,7 +1109,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { // RefineStillsGeometry cannot overflow int (1e6 is already far past any real dataset). refine_geometry = optarg ? parse_number_arg(optarg, "--refine-geometry", logger, 1, 1000000) - : 200; + : RUGNUX_REFINE_GEOMETRY_FRAMES; break; } case OPT_FORCE_ROTATION_LATTICE: { @@ -2800,7 +2793,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { if (!refine_geometry.has_value() && !refine_geometry_disabled) { const bool is_stills = !(experiment.GetGoniometer().has_value() && !force_still); if (is_stills && experiment.GetUnitCell().has_value()) - refine_geometry = 200; + refine_geometry = RUGNUX_REFINE_GEOMETRY_FRAMES; } experiment.MaxSpotCount(max_spot_count_override.value_or(RUGNUX_MAX_SPOT_COUNT)); @@ -2879,10 +2872,8 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { // stills. A rotation sweep sits on the same rings for the whole run, so ice there is a coherent // systematic worth handling, and the ice-presence gate keeps it inert on a clean crystal; a serial // stills run has too few spots per image to spend any of them on flagging. - if (detect_ice_rings.has_value()) - experiment.DetectIceRings(detect_ice_rings.value()); - else if (!dataset->file_detect_ice_rings.has_value()) - experiment.DetectIceRings(rotation_indexing); + experiment.DetectIceRings(detect_ice_rings.value_or( + RugnuxDetectIceRings(dataset->file_detect_ice_rings, rotation_indexing))); // Scale-fulls refits the per-frame scale on the rotation combined fulls; on by default for rotation // data (where it lifts ISa substantially) and off for stills. --no-scale-fulls overrides. @@ -2956,22 +2947,8 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { experiment.ImportBraggIntegrationSettings(bis); logger.Info("Integration radii set to r1={:.1f} r2={:.1f} r3={:.1f}", r1, r2, r3); } else if (!rotation_indexing) { - // Stills spots span a range of crystal orientations captured in a single shot, so they land - // wider on the detector than the r1=4 monochromatic-rotation default assumes. A larger signal - // box lets the profile-fit integrator capture the whole spot while its profile weighting keeps - // the extra background from adding noise (a plain box-sum degrades with it). Measured R-free - // gains on serial stills. An explicit --integration-radius always wins. - // - // r3 = 14 rather than 12: the reported sigma carries the ring mean's own error as - // (dI/dbkg)^2 * bkg/n_ring, and the ring only holds 244 pixels at r3 = 12 against the - // rotation default's 408, so that term is a larger share of the variance here than - // anywhere else. 14 takes the ring to about 410 pixels. What normally stops a ring being - // widened is crowding - a neighbour inside the annulus is excluded from it - and stills - // barely crowd: under a percent of stills reflections have any neighbour within 13 px, - // against most of them on rotation. - BraggIntegrationSettings bis = experiment.GetBraggIntegrationSettings(); - bis.R1(6.0f).R2(8.0f).R3(14.0f); - experiment.ImportBraggIntegrationSettings(bis); + // An explicit --integration-radius always wins over the stills default. + ApplyRugnuxIntegrationRadii(experiment, rotation_indexing); logger.Info("Stills integration radii default to r1=6.0 r2=8.0 r3=14.0 (override with --integration-radius)"); } @@ -3056,10 +3033,7 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { : std::string("off"))); } - // Auto unless the flag says otherwise. The library default stays off (the broker keeps it off); - // offline, over ice sets and clean controls, auto was a clear gain where it fired (smooth ice: - // CC to the model +0.015, R shell-scaled -0.019 on one set) and bit-identical where it did not, - // while forcing it on everywhere cost a clean set 0.008 in CC to the model. + // Auto (ApplyRugnuxExperimentDefaults) unless the flag says otherwise. { BraggIntegrationSettings bis = experiment.GetBraggIntegrationSettings(); bis.BackgroundRadialCorrection(background_radial_arg); @@ -3097,25 +3071,14 @@ static int RunRugnux(int argc, char **argv, bool no_gpu) { : "auto (per image, where the ice score sees a smooth ring)"); } - SpotFindingSettings spot_settings; - spot_settings.enable = true; - spot_settings.indexing = true; + // Detection defaults differ by workflow (RugnuxDefaultSpotFinding); each flag always wins. + SpotFindingSettings spot_settings = RugnuxDefaultSpotFinding(rotation_indexing); spot_settings.signal_to_noise_threshold = sigma_spot_finding; spot_settings.photon_count_threshold = photon_count_threshold_spot_finding; - // Detection defaults differ by workflow; each is overridden by its flag, which always wins. - // - min-pix: choosing it per image (unset) only means something where each frame is indexed on its - // own. Rotation indexing builds ONE lattice from all frames, so it keeps the fixed value. - // - adaptive detection: on by default for both workflows. - // The high-resolution limit is NOT one of them: unset means "as far as the detector reaches" for - // rotation as well as stills. Rotation used to keep 1.5 A on the strength of an indexing-rate - // measurement, but over the 33-crystal battery that limit changes nothing on 29 crystals, changes - // no space-group decision at all, and on the crystals where it does bite it is the LIMIT that is - // worse: the one crystal that loses appreciable indexing rate without it (99.5 -> 94.2%) comes back - // with better R_meas, better high-resolution CC1/2 and better ISa. Fewer frames, better data. - spot_settings.min_pix_per_spot = min_pix_per_spot; - if (rotation_indexing && !spot_settings.min_pix_per_spot.has_value()) - spot_settings.min_pix_per_spot = 2; - spot_settings.adaptive_threshold = adaptive_spots.value_or(true); + if (min_pix_per_spot.has_value()) + spot_settings.min_pix_per_spot = min_pix_per_spot; + if (adaptive_spots.has_value()) + spot_settings.adaptive_threshold = *adaptive_spots; spot_settings.high_resolution_limit = d_min_spot_finding; spot_settings.false_pixels_per_frame = false_pixels_per_frame; spot_settings.low_resolution_limit = d_max_spot_finding; diff --git a/tests/RugnuxTest.cpp b/tests/RugnuxTest.cpp index 70c8621f7..273532e2a 100644 --- a/tests/RugnuxTest.cpp +++ b/tests/RugnuxTest.cpp @@ -12,6 +12,7 @@ #include "../reader/JFJochHDF5Reader.h" #include "../rugnux/Rugnux.h" #include "../rugnux/RugnuxCommandLine.h" +#include "../rugnux/RugnuxDefaults.h" #include "../image_analysis/bragg_integration/SpotWidth.h" #include "../image_analysis/geom_refinement/Calibrants.h" @@ -281,6 +282,60 @@ TEST_CASE("RugnuxCommandLine_Full", "[process]") { CHECK(mr_cmd.find("--model") == std::string::npos); } +TEST_CASE("RugnuxDefaults_BareRun", "[process]") { + // What both front ends (the rugnux CLI and the viewer's "Analyze dataset") start a run from. + const SpotFindingSettings rot = RugnuxDefaultSpotFinding(true); + const SpotFindingSettings stills = RugnuxDefaultSpotFinding(false); + CHECK(rot.adaptive_threshold); + CHECK(stills.adaptive_threshold); + CHECK(rot.min_pix_per_spot == 2); + CHECK(!stills.min_pix_per_spot.has_value()); + CHECK(!rot.high_resolution_limit.has_value()); + CHECK(rot.indexing); + + DiffractionExperiment x(DetJF(1)); + ApplyRugnuxExperimentDefaults(x); + CHECK(x.GetPolarizationFactor() == 0.99f); + CHECK(!x.GetBraggIntegrationSettings().GetBackgroundRadialCorrection().has_value()); // auto + + DiffractionExperiment rot_x = x, stills_x = x; + ApplyRugnuxIntegrationRadii(rot_x, true); + ApplyRugnuxIntegrationRadii(stills_x, false); + CHECK(rot_x.GetBraggIntegrationSettings().GetR1() == BraggIntegrationSettings().GetR1()); + CHECK(stills_x.GetBraggIntegrationSettings().GetR1() == 6.0f); + CHECK(stills_x.GetBraggIntegrationSettings().GetR3() == 14.0f); + + CHECK(RugnuxDetectIceRings(std::nullopt, true)); + CHECK(!RugnuxDetectIceRings(std::nullopt, false)); + CHECK(!RugnuxDetectIceRings(false, true)); +} + +TEST_CASE("RugnuxCommandLine_AutomaticChoices", "[process]") { + // A run left to rugnux's automatic choices must give back a command line that leaves them automatic: + // -N, --max-spots and --integration-radius each switch one of them off. + DiffractionExperiment x(DetJF(1)); + ProcessConfig config; + config.mode = ProcessMode::FullAnalysis; + config.nthreads = 8; + config.nthreads_auto = true; + config.measure_spot_budget = true; + config.adaptive_integration_radius = true; + config.rotation_indexing = true; + + const std::string cmd = RugnuxCommandLine(config, x, "in.h5"); + CHECK(cmd.find("-N ") == std::string::npos); + CHECK(cmd.find("--max-spots") == std::string::npos); + CHECK(cmd.find("--integration-radius") == std::string::npos); + + config.nthreads_auto = false; + config.measure_spot_budget = false; + config.adaptive_integration_radius = false; + const std::string pinned = RugnuxCommandLine(config, x, "in.h5"); + CHECK(pinned.find("-N 8") != std::string::npos); + CHECK(pinned.find("--max-spots") != std::string::npos); + CHECK(pinned.find("--integration-radius") != std::string::npos); +} + TEST_CASE("RugnuxCommandLine_AzInt", "[process]") { DiffractionExperiment x(DetJF(1)); AzimuthalIntegrationSettings a; diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index 4d8433854..c59cb8485 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -321,6 +321,7 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_ curr_experiment = dataset->experiment; roi_override_.reset(); // new file: use its ROIs, forget any earlier edits file_goniometer_ = dataset->experiment.GetGoniometer(); // before the panel edits it + file_detect_ice_rings_ = dataset->file_detect_ice_rings; curr_experiment.ImportIndexingSettings(indexing_settings); curr_experiment.ImportAzimuthalIntegrationSettings(azint_settings); curr_experiment.ImportBraggIntegrationSettings(bragg_settings); @@ -1108,6 +1109,7 @@ ReprocessingInputs JFJochImageReadingWorker::GetReprocessingInputs() const { in.pixel_mask = *current_image_ptr->Dataset().pixel_mask; in.spot_finding = spot_finding_settings; in.file_goniometer = file_goniometer_; + in.file_detect_ice_rings = file_detect_ice_rings_; if (reference_) { in.reference_data = reference_->reflections; in.reference_has_free_flags = reference_->has_free_flags; diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index 95b8c90d8..2336c893b 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -61,6 +61,8 @@ struct ReprocessingInputs { // The rotation axis the file was opened with, before the settings panel had a say. `experiment` // carries the panel's, which may have cleared it; this is what a fresh `rugnux` run would see. std::optional file_goniometer; + // Whether the file states ice-ring flagging; unset, a run decides it as `rugnux` does. + std::optional file_detect_ice_rings; }; #include "RunData.h" @@ -101,6 +103,7 @@ private: // The rotation axis of the open file, captured before the settings panel can change it — see // ReprocessingInputs::file_goniometer. std::optional file_goniometer_; + std::optional file_detect_ice_rings_; std::map run_labels_; // snapshot id -> editable display label IndexingSettings indexing_settings; AzimuthalIntegrationSettings azint_settings; diff --git a/viewer/JFJochProcessController.cpp b/viewer/JFJochProcessController.cpp index 699c82e09..c2839e7ea 100644 --- a/viewer/JFJochProcessController.cpp +++ b/viewer/JFJochProcessController.cpp @@ -62,9 +62,11 @@ void JFJochProcessController::run_(QString file_path, DiffractionExperiment expe } else if (JFJochMarCCDReader::CanRead(path)) { marccd_reader.ReadFiles(path); reader_ptr = &marccd_reader; + config.counts_in_adu = true; // a CCD: pixel values are ADU, as for rugnux } else if (JFJochSMVReader::CanRead(path)) { smv_reader.ReadFiles(path); reader_ptr = &smv_reader; + config.counts_in_adu = true; } else { hdf5_reader.ReadFile(path); reader_ptr = &hdf5_reader; diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index b532e842d..0b4cc8bf1 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -128,23 +128,20 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString const QRect avail = QGuiApplication::primaryScreen()->availableGeometry(); resize(qMin(1200, avail.width() - 100), qMin(1100, avail.height() - 100)); - SpotFindingSettings spot_finding_settings = DiffractionExperiment::DefaultDataProcessingSettings(); - - // The interactive viewer is an offline tool looking at one dataset at a time, so it starts from the - // self-calibrating settings rather than numbers the user would have to tune: the per-ring adaptive - // threshold, min-pix chosen per image (std::nullopt), and detection out to the detector edge - // (high_resolution_limit unset). All three are switchable in the settings dock. - spot_finding_settings.adaptive_threshold = true; - spot_finding_settings.min_pix_per_spot = std::nullopt; - spot_finding_settings.indexing = true; + // The interactive viewer is an offline tool looking at one dataset at a time, so it starts from what + // a bare `rugnux` uses rather than numbers the user would have to tune: the per-ring adaptive + // threshold, min-pix chosen per image (the settings dock fixes it for rotation, as rugnux does), and + // detection out to the detector edge. All three are switchable in the settings dock. + SpotFindingSettings spot_finding_settings = RugnuxDefaultSpotFinding(false); + // Auto, as in rugnux: the GPU FFT where there is a GPU, FFTW on the CPU otherwise. IndexingSettings indexing_settings; indexing_settings.IndexingThreads(1); indexing_settings.Algorithm(IndexingAlgorithmEnum::Auto); - if (get_gpu_count() == 0) { - indexing_settings.Algorithm(IndexingAlgorithmEnum::FFTW); + // Fewer FFT directions keep single-image indexing interactive on the CPU; a processing job puts the + // rugnux number back (JFJochProcessingJobsWindow::newJob). + if (get_gpu_count() == 0) indexing_settings.FFT_NumVectors(8 * 1024); - } indexing_settings.GeomRefinementAlgorithm(GeomRefinementAlgorithmEnum::BeamCenter); DiffractionExperiment experiment; diff --git a/viewer/widgets/JFJochViewerSettingsDock.cpp b/viewer/widgets/JFJochViewerSettingsDock.cpp index 913b092b8..8ab69f720 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.cpp +++ b/viewer/widgets/JFJochViewerSettingsDock.cpp @@ -509,8 +509,10 @@ QWidget *JFJochViewerSettingsDock::BuildGoniometerSection() { EmitExperiment(); }); connect(axisName_, &QLineEdit::editingFinished, this, [this] { EmitExperiment(); }); - for (auto *f : {axisX_, axisY_, axisZ_, rotStart_, rotIncrement_}) + for (auto *f : {axisX_, axisY_, axisZ_, rotStart_}) connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); }); + // A zero increment is a stationary axis, which is processed as stills. + connect(rotIncrement_, &NumberLineEdit::newValue, this, [this] { ApplyProcessingMode(); EmitExperiment(); }); for (auto *f : {gridNFast_, gridStepX_, gridStepY_}) connect(f, &NumberLineEdit::newValue, this, [this] { UpdateGridSummary(); EmitExperiment(); }); for (auto *b : {gridVertical_, gridSnake_}) @@ -1024,7 +1026,10 @@ void JFJochViewerSettingsDock::ApplyProcessingMode() { // Rotation is the only mode that changes how the data is processed: the rotation good-path // (rotation indexing + Ewald partiality + rot3d combine + scale-fulls). A grid scan is a raster // of stills, so it runs exactly as Still does. - const bool rotation_mode = modeRotation_ && modeRotation_->isChecked(); + // As in rugnux, an axis that does not turn is processed as stills: it records where the head was, + // not that the crystal turned, and rotation indexing would see a zero stride and never index. + const auto gonio = modeRotation_ ? GoniometerFromFields() : std::nullopt; + const bool rotation_mode = gonio.has_value() && gonio->IsScanning(); indexing_.RotationIndexing(rotation_mode); // rotation indexing -> rotation scaling/merge downstream scaling_.ScaleFulls(rotation_mode); EmitSpotFinding(); // carries indexing_ (incl. RotationIndexing) to the worker diff --git a/viewer/widgets/JFJochViewerSettingsDock.h b/viewer/widgets/JFJochViewerSettingsDock.h index fb8ec0908..5cd774391 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.h +++ b/viewer/widgets/JFJochViewerSettingsDock.h @@ -15,6 +15,7 @@ #include "../../reader/JFJochReaderDataset.h" #include "../../reader/JFJochReaderImage.h" #include "../../rugnux/Rugnux.h" // ProcessMode +#include "../../rugnux/RugnuxDefaults.h" #include "../ReferenceMtzInfo.h" #include "PowderCalibrationWidget.h" // CalibrationSelection @@ -70,7 +71,7 @@ private: ScalingSettings scaling_; DiffractionExperiment experiment_; bool have_experiment_ = false; // geometry edits only take effect once a dataset is loaded - int64_t max_spots_ = 1000; + int64_t max_spots_ = RUGNUX_MAX_SPOT_COUNT; // Min-pix is kept as the pair the UI shows - the fixed value AND whether to choose it per image - // because spot_.min_pix_per_spot can only hold one of the two (unset = per image). SyncMinPix() // turns the pair into that field, and per-image only ever applies to stills. diff --git a/viewer/windows/JFJochProcessingJobsWindow.cpp b/viewer/windows/JFJochProcessingJobsWindow.cpp index c63f3ff6a..2a91d6725 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.cpp +++ b/viewer/windows/JFJochProcessingJobsWindow.cpp @@ -4,7 +4,6 @@ #include "JFJochProcessingJobsWindow.h" #include "JFJochCalibrationResultWindow.h" #include "JFJochMergeStatsWindow.h" -#include "../../rugnux/RugnuxDefaults.h" #include "../widgets/ToolbarIcons.h" #include "../../rugnux/RugnuxCommandLine.h" #include "../../common/ThreadAffinity.h" @@ -47,6 +46,14 @@ namespace { return static_cast(AvailableCpus()); } + // Radii left where the settings panel starts them are not a choice, so the run takes rugnux's + // per-workflow default, as `rugnux` does without --integration-radius. + bool radii_set_by_hand(const DiffractionExperiment &experiment) { + const BraggIntegrationSettings shipped; + const auto &b = experiment.GetBraggIntegrationSettings(); + return b.GetR1() != shipped.GetR1() || b.GetR2() != shipped.GetR2() || b.GetR3() != shipped.GetR3(); + } + QTableWidgetItem *fixedItem(const QString &text) { // a non-editable cell auto *cell = new QTableWidgetItem(text); cell->setFlags(cell->flags() & ~Qt::ItemIsEditable); @@ -219,7 +226,7 @@ int JFJochProcessingJobsWindow::askJob(const ReprocessingInputs &inputs, JobSpec "indexing). Needs a known unit cell; a no-op for rotation data."); auto *refine_frames = new QSpinBox(&dlg); refine_frames->setRange(10, 100000); - refine_frames->setValue(200); + refine_frames->setValue(RUGNUX_REFINE_GEOMETRY_FRAMES); refine_frames->setEnabled(stills_with_cell); refine_frames->setToolTip("Number of strongly-indexed frames fed to the joint bundle adjustment."); connect(refine_geometry, &QCheckBox::toggled, refine_frames, &QWidget::setEnabled); @@ -256,7 +263,7 @@ int JFJochProcessingJobsWindow::askJob(const ReprocessingInputs &inputs, JobSpec "attenuated but not flagged, so they otherwise integrate low with a plausible sigma."); auto *beam_stop_frames = new QSpinBox(&dlg); beam_stop_frames->setRange(3, 100000); - beam_stop_frames->setValue(60); + beam_stop_frames->setValue(RUGNUX_DETECT_BEAM_STOP_FRAMES); beam_stop_frames->setToolTip("Frames projected to find the shadow. Fewer leaves the background too " "sparsely counted to tell a shadow from noise."); connect(beam_stop, &QCheckBox::toggled, beam_stop_frames, &QWidget::setEnabled); @@ -343,6 +350,9 @@ ProcessConfig JFJochProcessingJobsWindow::buildConfig(const JobSpec &spec, const ProcessConfig config; config.mode = spec.mode; config.nthreads = spec.threads; + // Left at every CPU, as `rugnux` without -N: the per-image loop is then capped where more workers + // than a card can feed only slow it down. + config.nthreads_auto = spec.threads == default_threads(); config.start_image = spec.start_image; config.end_image = spec.end_image > 0 ? spec.end_image : -1; // 0 => to the end // Files land at output_prefix; leave it empty (write nothing, stats only) when neither output is @@ -370,11 +380,15 @@ ProcessConfig JFJochProcessingJobsWindow::buildConfig(const JobSpec &spec, const } if (spec.mode == ProcessMode::FullAnalysis) { // Rotation indexing follows the panel's rotation axis (= the experiment's indexing - // setting); a rotation run uses 60 first-pass images to find the lattice. + // setting); the first pass finds the lattice on as many images as rugnux's. config.rotation_indexing = inputs.experiment.GetIndexingSettings().GetRotationIndexing(); config.two_pass_rotation = true; - if (config.rotation_indexing) - config.rotation_indexing_image_count = 60; + config.rotation_indexing_image_count = RUGNUX_ROTATION_INDEXING_IMAGES; + // Measured in the pre-scan on rotation data, unless the panel's radii were changed by hand. + config.adaptive_integration_radius = config.rotation_indexing && !radii_set_by_hand(inputs.experiment); + // The spot budget is measured from the data, unless the panel moved it off rugnux's default + // (what --max-spots does on the command line). + config.measure_spot_budget = inputs.experiment.GetMaxSpotCount() == RUGNUX_MAX_SPOT_COUNT; config.run_scaling = spec.scaling; config.reference_data = inputs.reference_data; // enables CCref / reference-based scaling config.reference_has_free_flags = inputs.reference_has_free_flags; @@ -426,6 +440,17 @@ void JFJochProcessingJobsWindow::newJob(ProcessMode mode, CalibrationSelection c const auto polarization = experiment.GetPolarizationFactor(); ApplyRugnuxExperimentDefaults(experiment); experiment.PolarizationFactor(polarization); // the panel's setting, not the rugnux default + if (spec.mode == ProcessMode::FullAnalysis) { + // Neither of these is on the panel, so both are what a bare `rugnux` decides. + experiment.DetectIceRings(RugnuxDetectIceRings(inputs.file_detect_ice_rings, config.rotation_indexing)); + if (!radii_set_by_hand(experiment)) + ApplyRugnuxIntegrationRadii(experiment, config.rotation_indexing); + // The interactive viewer searches fewer FFT directions on the CPU (JFJochViewerWindow); a run + // takes rugnux's. + IndexingSettings indexing = experiment.GetIndexingSettings(); + indexing.FFT_NumVectors(IndexingSettings().GetFFT_NumVectors()); + experiment.ImportIndexingSettings(indexing); + } // Calibrating from the run-summed profile needs the profile to be binned in azimuth; the panel's // bin count is an azimuthal-integration setting and defaults to a plain radial profile, which // carries no information about where the ring centre is. Same fallback as the rugnux CLI. diff --git a/viewer/windows/JFJochProcessingJobsWindow.h b/viewer/windows/JFJochProcessingJobsWindow.h index 8700dc2c3..1b529803e 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.h +++ b/viewer/windows/JFJochProcessingJobsWindow.h @@ -11,6 +11,7 @@ #include #include "../JFJochProcessController.h" +#include "../../rugnux/RugnuxDefaults.h" #include "../JFJochImageReadingWorker.h" // ReprocessingInputs #include "../widgets/PowderCalibrationWidget.h" // CalibrationSelection @@ -89,14 +90,12 @@ private: bool save_h5 = true; // write the per-image _process.h5 (also drives the viewer snapshot) bool save_merged = true; // write the merged .mtz/.cif QString prefix; - bool rotation = false; - int rotation_images = 30; // images used to find the rotation lattice (first pass) bool scaling = false; bool refine_geometry = false; // stills-only global geometry bundle-adjust (needs a known cell) - int refine_geometry_frames = 200; // strong indexed frames fed to that bundle adjustment + int refine_geometry_frames = RUGNUX_REFINE_GEOMETRY_FRAMES; // strong indexed frames fed to that bundle adjustment bool rotation_postrefine = true; // rotation-only two-pass geometry post-refine (default on) bool detect_beam_stop = true; // project frames and mask the beam-stop shadow (default on) - int detect_beam_stop_frames = 60; // frames projected to find it + int detect_beam_stop_frames = RUGNUX_DETECT_BEAM_STOP_FRAMES; // frames projected to find it CalibrationSelection calibration; // Calibration mode: calibrant rings + rings/spots method };