From 7d5fb15413245b1a2a964c6bdb110309c78de4cd Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 3 Jul 2026 12:20:45 +0200 Subject: [PATCH] Unify still processing on one --force-still flag; scaling follows rotation indexing Fold the two overlapping knobs into one. Rotation-vs-stills is now a single decision (IsRotationIndexing() = goniometer && rotation-indexing) that drives BOTH indexing and scaling/merging - there is no independent scaling-model switch: - ScalingSettings: drop combine_3d and force_still_processing (and Combine3D/GetCombine3D, ForceStillProcessing/GetForceStillProcessing). The scaling stage reads experiment.IsRotationIndexing() instead - rotation indexing implies RotationScaleMerge, no rotation indexing implies per-image ScaleOnTheFly. - jfjoch_process: merge --process-as-stills and --force-still-processing into one --force-still (turns rotation processing off entirely: still indexing + still scaling). The rotation-specific scaling defaults (scale-fulls, smooth-G, capture uncertainty, outlier rejection) now key on rotation_indexing. - jfjoch_scale: replace -P fixed|rot3d with --force-still; rotation is auto-detected from the goniometer (RotationIndexing set accordingly) so it matches jfjoch_process. - jfjoch_viewer: the "process as stills" toggle already sets RotationIndexing; drop the now redundant Combine3D/ForceStillProcessing calls. Rotation path unchanged (lyso 16.4/99.6%/87.3%); --force-still routes to ScaleOnTheFly; the -R + --force-still conflict still errors. CUDA + non-CUDA + viewer all build. Co-Authored-By: Claude Opus 4.8 --- common/ScalingSettings.cpp | 18 -------- common/ScalingSettings.h | 17 +------ process/JFJochProcess.cpp | 2 +- process/JFJochProcessCommandLine.cpp | 5 +- tools/jfjoch_process.cpp | 51 ++++++++------------- tools/jfjoch_scale.cpp | 34 +++++++------- viewer/widgets/JFJochViewerSettingsDock.cpp | 4 +- 7 files changed, 41 insertions(+), 90 deletions(-) diff --git a/common/ScalingSettings.cpp b/common/ScalingSettings.cpp index 1cc0e88b..c3e75ee5 100644 --- a/common/ScalingSettings.cpp +++ b/common/ScalingSettings.cpp @@ -5,11 +5,6 @@ #include "ScalingSettings.h" -ScalingSettings& ScalingSettings::ForceStillProcessing(bool input) { - force_still_processing = input; - return *this; -} - ScalingSettings& ScalingSettings::RefineB(bool input) { refine_b = input; return *this; @@ -51,10 +46,6 @@ bool ScalingSettings::GetRefineWedge() const { return refine_wedge; } -bool ScalingSettings::GetForceStillProcessing() const { - return force_still_processing; -} - std::optional ScalingSettings::GetHighResolutionLimit_A() const { return high_resolution_limit_A; } @@ -131,15 +122,6 @@ ScalingSettings &ScalingSettings::OutlierRejectNsigma(double input) { return *this; } -ScalingSettings &ScalingSettings::Combine3D(bool input) { - combine_3d = input; - return *this; -} - -bool ScalingSettings::GetCombine3D() const { - return combine_3d; -} - ScalingSettings &ScalingSettings::ScaleFulls(bool input) { scale_fulls = input; return *this; diff --git a/common/ScalingSettings.h b/common/ScalingSettings.h index 183ca7c2..3a4256d3 100644 --- a/common/ScalingSettings.h +++ b/common/ScalingSettings.h @@ -9,10 +9,6 @@ enum class IntensityFormat { Text, mmCIF, MTZ}; class ScalingSettings { - // Force per-image stills scaling (ScaleOnTheFly, fixed partiality) even for a rotation dataset, - // instead of the rotation self-scale + 3D combine + merge (RotationScaleMerge). Off by default. - bool force_still_processing = false; - bool refine_b = false; double max_b = 200.0; double min_b = -50.0; @@ -31,13 +27,8 @@ class ScalingSettings { double min_cc_for_image = 0.0; double outlier_reject_nsigma = 0.0; // per-observation merge outlier rejection (XDS/DIALS-style); 0 = off, e.g. 6 enables - // Rotation processing (RotationScaleMerge): recompute partiality, weight-sum each reflection's - // per-frame partials into one counting-limited full, scale the fulls and merge. Set by the tool for - // rotation data unless force_still_processing. - bool combine_3d = false; - - // Scale fulls: after the rot3d combine, refit a per-frame scale on the combined fulls (XDS - // order). A no-op without rot3d. + // Scale fulls: after the rotation 3D combine, refit a per-frame scale on the combined fulls (XDS + // order). Only used by the rotation path (RotationScaleMerge). bool scale_fulls = false; // Absorption surface: after scaling the rot3d fulls, fit a smooth multiplicative correction @@ -56,7 +47,6 @@ class ScalingSettings { bool scaling_regularize = false; public: - ScalingSettings& ForceStillProcessing(bool input); ScalingSettings& RefineB(bool input); ScalingSettings& RefineRotationWedge(bool input); ScalingSettings& RotationWedgeForScaling(std::optional input); @@ -68,7 +58,6 @@ public: ScalingSettings& CaptureUncertaintyCoeff(double input); ScalingSettings& MinCCForImage(double min_cc_for_image); ScalingSettings& OutlierRejectNsigma(double input); - ScalingSettings& Combine3D(bool input); ScalingSettings& ScaleFulls(bool input); ScalingSettings& AbsorptionIter(int input); ScalingSettings& SmoothGDegrees(double input); @@ -93,7 +82,6 @@ public: [[nodiscard]] bool GetMergeFriedel() const; - [[nodiscard]] bool GetForceStillProcessing() const; [[nodiscard]] std::optional GetHighResolutionLimit_A() const; [[nodiscard]] double GetMinPartiality() const; @@ -101,7 +89,6 @@ public: [[nodiscard]] double GetCaptureUncertaintyCoeff() const; [[nodiscard]] double GetMinCCForImage() const; [[nodiscard]] double GetOutlierRejectNsigma() const; - [[nodiscard]] bool GetCombine3D() const; [[nodiscard]] bool GetScaleFulls() const; [[nodiscard]] int GetAbsorptionIter() const; [[nodiscard]] double GetSmoothGDegrees() const; diff --git a/process/JFJochProcess.cpp b/process/JFJochProcess.cpp index 604d3198..75545757 100644 --- a/process/JFJochProcess.cpp +++ b/process/JFJochProcess.cpp @@ -440,7 +440,7 @@ ProcessResult JFJochProcess::Run(JFJochProcessObserver *observer) { // (forced) mosaicity is handled by the recompute. It does NOT support external-reference scaling, // B-factor refinement, an absorption surface, or wedge refinement - reject those combinations. const auto &rot_ss = experiment_.GetScalingSettings(); - const bool is_rotation = rot_ss.GetCombine3D(); // set for rotation data unless force-still + const bool is_rotation = experiment_.IsRotationIndexing(); // rotation indexing -> rotation scaling/merge std::optional rsm; if (is_rotation) { if (!config_.reference_data.empty() || rot_ss.GetRefineB() || rot_ss.GetAbsorptionIter() > 0 diff --git a/process/JFJochProcessCommandLine.cpp b/process/JFJochProcessCommandLine.cpp index ac398eff..d09d035a 100644 --- a/process/JFJochProcessCommandLine.cpp +++ b/process/JFJochProcessCommandLine.cpp @@ -111,13 +111,14 @@ std::string JFJochProcessCommandLine(const ProcessConfig &config, 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"); } if (config.run_scaling) { args.emplace_back("-M"); const auto sc = experiment.GetScalingSettings(); - if (sc.GetForceStillProcessing()) - args.emplace_back("--force-still-processing"); if (!sc.GetMergeFriedel()) args.emplace_back("-A"); if (sc.GetRefineB()) diff --git a/tools/jfjoch_process.cpp b/tools/jfjoch_process.cpp index d07f6c1a..c4054e2f 100644 --- a/tools/jfjoch_process.cpp +++ b/tools/jfjoch_process.cpp @@ -45,8 +45,8 @@ void print_usage() { std::cout << std::endl; std::cout << " Indexing" << std::endl; - std::cout << " (A dataset with a rotation goniometer axis is processed as rotation data by default; use --process-as-stills to override)" << std::endl; - std::cout << " --process-as-stills Process a rotation (goniometer) dataset as independent stills instead of rotation" << std::endl; + std::cout << " (A dataset with a rotation goniometer axis is processed as rotation data by default; use --force-still to override)" << std::endl; + std::cout << " --force-still Process a rotation (goniometer) dataset as independent stills (still indexing + per-image ScaleOnTheFly) instead of rotation" << std::endl; std::cout << " -R, --two-pass-rotation[=num] Two-pass offline rotation indexing (default for goniometer data; optional first-pass image count, default: 100)" << std::endl; std::cout << " --single-pass-rotation[=num] Use online-like single-pass rotation indexing (optional: min angular range deg)" << std::endl; std::cout << " --redo-rotation-spots Redo spot finding for two-pass rotation indexing" << std::endl; @@ -63,7 +63,6 @@ void print_usage() { std::cout << " --no-scale-fulls Disable the rot3d scale-fulls refit (it is on by default for rot3d)" << std::endl; std::cout << " --write-process-h5 Also write the (large) _process.h5 when merging (default: only .mtz/.cif when merging)" << std::endl; std::cout << " --smooth-g[=deg] rot3d: smooth per-frame scale G over a deg-degree rotation range (XDS DELPHI-like) before the combine (default: 5 for rot3d; 0 = off)" << std::endl; - std::cout << " --force-still-processing Scale a rotation dataset as independent stills (per-image ScaleOnTheFly, fixed partiality) instead of the default RotationScaleMerge (recompute partiality, 3D combine, scale-fulls, merge)" << std::endl; std::cout << " -A, --anomalous Anomalous mode (don't merge Friedel pairs)" << std::endl; std::cout << " -B, --refine-bfactor Refine per image B-factor" << std::endl; std::cout << " --scaling-high-resolution High resolution limit for spot finding (default: no limit)" << std::endl; @@ -116,8 +115,7 @@ enum { OPT_DETECT_ICE_RINGS, OPT_NO_SCALE_FULLS, OPT_WRITE_PROCESS_H5, - OPT_PROCESS_AS_STILLS, - OPT_FORCE_STILL_PROCESSING, + OPT_FORCE_STILL, OPT_AZIM_MIN_Q, OPT_AZIM_MAX_Q, OPT_AZIM_PHI_BINS @@ -136,7 +134,6 @@ static option long_options[] = { {"reference-column", required_argument, nullptr, OPT_REFERENCE_COLUMN}, {"dump-observations", required_argument, nullptr, OPT_DUMP_OBSERVATIONS}, {"space-group", required_argument, nullptr, 'S'}, - {"force-still-processing", no_argument, nullptr, OPT_FORCE_STILL_PROCESSING}, {"anomalous", no_argument, nullptr, 'A'}, {"refine-bfactor", no_argument, nullptr, 'B'}, {"scale-merge", no_argument, nullptr, 'M'}, @@ -148,7 +145,7 @@ static option long_options[] = { {"two-pass-rotation", optional_argument, nullptr, 'R'}, {"single-pass-rotation", optional_argument, nullptr, OPT_SINGLE_PASS_ROTATION}, - {"process-as-stills", no_argument, nullptr, OPT_PROCESS_AS_STILLS}, + {"force-still", no_argument, nullptr, OPT_FORCE_STILL}, {"azim-q-spacing", required_argument, nullptr, 'q'}, {"azim-min-q", required_argument, nullptr, OPT_AZIM_MIN_Q}, {"azim-max-q", required_argument, nullptr, OPT_AZIM_MAX_Q}, @@ -305,7 +302,7 @@ int main(int argc, char **argv) { bool verbose = false; bool rotation_indexing = false; - bool process_as_stills = false; // --process-as-stills: force per-frame stills on a rotation dataset + bool force_still = false; // --force-still: process a rotation dataset as stills (indexing + scaling) bool two_pass_rotation = true; bool reuse_rotation_spots = true; int rotation_indexing_image_count = 100; @@ -340,8 +337,6 @@ int main(int argc, char **argv) { GeomRefinementAlgorithmEnum refinement_algorithm = GeomRefinementAlgorithmEnum::BeamCenter; std::optional intensity_format; // --scaling-output override; default lives in ScalingSettings (mmCIF) - bool force_still_processing = false; // --force-still-processing: scale a rotation dataset as stills - bool combine_3d = false; // -P rot3d: weight-sum per-frame partials into fulls before merging float d_min_spot_finding = 1.5; std::optional d_min_scale_merge; @@ -501,9 +496,6 @@ int main(int argc, char **argv) { case 'S': space_group_number = atoi(optarg); break; - case OPT_FORCE_STILL_PROCESSING: - force_still_processing = true; - break; case OPT_SPOT_SIGMA: sigma_spot_finding = atof(optarg); logger.Info("Noise threshold level for spot finding set to {:.2f} sigma", sigma_spot_finding); @@ -592,8 +584,8 @@ int main(int argc, char **argv) { exit(EXIT_FAILURE); } break; - case OPT_PROCESS_AS_STILLS: - process_as_stills = true; + case OPT_FORCE_STILL: + force_still = true; break; case 'q': q_spacing = atof(optarg); @@ -760,29 +752,24 @@ int main(int argc, char **argv) { logger.Info("X-ray bandwidth FWHM set to {:.4f}", experiment.GetBandwidthFWHM().value()); // Rotation vs stills. A dataset collected on a rotation goniometer is processed as rotation data - // (two-pass indexing) by default; --process-as-stills forces per-frame stills. The rotation flags + // (two-pass indexing) by default; --force-still forces per-frame stills. The rotation flags // (-R / --single-pass-rotation / --force-rotation-lattice) still request rotation explicitly and // choose the pass/lattice; at this point they show up as rotation_indexing already being set. const bool has_goniometer = experiment.GetGoniometer().has_value(); - if (process_as_stills) { + if (force_still) { if (rotation_indexing) { - logger.Error("--process-as-stills conflicts with -R / --single-pass-rotation / --force-rotation-lattice"); + logger.Error("--force-still conflicts with -R / --single-pass-rotation / --force-rotation-lattice"); exit(EXIT_FAILURE); } if (has_goniometer) - logger.Info("--process-as-stills: treating the rotation dataset as independent stills"); + logger.Info("--force-still: treating the rotation dataset as independent stills"); } else if (!rotation_indexing && has_goniometer) { rotation_indexing = true; two_pass_rotation = true; logger.Info("Dataset has a rotation goniometer axis: processing as rotation data (two-pass " - "indexing). Use --process-as-stills to treat it as stills."); + "indexing). Use --force-still to treat it as stills."); } - // Rotation data goes through RotationScaleMerge (3D combine of the per-frame partials into fulls) - // unless --force-still-processing asks for per-image stills scaling instead. - if (rotation_indexing && !force_still_processing) - combine_3d = true; - // Configure Indexing IndexingSettings indexing_settings; indexing_settings.Algorithm(indexing_algorithm); @@ -797,15 +784,13 @@ int main(int argc, char **argv) { if (detect_ice_rings.has_value()) experiment.DetectIceRings(detect_ice_rings.value()); - // Scale-fulls refits the per-frame scale on the rot3d combined fulls; it is on by default for - // rot3d (where it lifts ISa substantially) and off otherwise. --no-scale-fulls overrides. - const bool scale_fulls = scale_fulls_arg.value_or(combine_3d); + // 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. + const bool scale_fulls = scale_fulls_arg.value_or(rotation_indexing); ScalingSettings scaling_settings; - scaling_settings.ForceStillProcessing(force_still_processing); - scaling_settings.Combine3D(combine_3d); scaling_settings.ScaleFulls(scale_fulls); - scaling_settings.SmoothGDegrees(smooth_g_deg_arg.value_or(combine_3d ? SMOOTH_G_DEFAULT_DEG : 0.0)); + scaling_settings.SmoothGDegrees(smooth_g_deg_arg.value_or(rotation_indexing ? SMOOTH_G_DEFAULT_DEG : 0.0)); if (d_min_scale_merge) scaling_settings.HighResolutionLimit_A(d_min_scale_merge.value()); scaling_settings.MergeFriedel(!anomalous_mode); @@ -814,11 +799,11 @@ int main(int argc, char **argv) { // Capture-aware systematic sigma defaults ON (1.0) for the rot3d combine - it down-weights the // over-extrapolated under-captured fulls and, with the mosaicity fix, lifts rotation ISa/anomalous // substantially. Off for non-rot3d (no combine). An explicit --capture-uncertainty always wins. - scaling_settings.CaptureUncertaintyCoeff(capture_uncertainty_arg.value_or(combine_3d ? 1.0 : 0.0)); + scaling_settings.CaptureUncertaintyCoeff(capture_uncertainty_arg.value_or(rotation_indexing ? 1.0 : 0.0)); scaling_settings.ForcedMosaicity(forced_mosaicity_arg); scaling_settings.MinCCForImage(min_image_cc / 100.0); // --min-image-cc is in percent; the setting is a fraction scaling_settings.OutlierRejectNsigma( - outlier_reject_nsigma.value_or(combine_3d ? REJECT_OUTLIERS_DEFAULT_NSIGMA : 0.0)); + outlier_reject_nsigma.value_or(rotation_indexing ? REJECT_OUTLIERS_DEFAULT_NSIGMA : 0.0)); if (intensity_format) scaling_settings.FileFormat(intensity_format.value()); diff --git a/tools/jfjoch_scale.cpp b/tools/jfjoch_scale.cpp index 73501796..86191bdf 100644 --- a/tools/jfjoch_scale.cpp +++ b/tools/jfjoch_scale.cpp @@ -40,8 +40,8 @@ void print_usage() { std::cout << " -S, --space-group Space group number" << std::endl; std::cout << " --scaling-high-resolution High resolution limit for scaling/merging (default: 0.0; no limit)" << std::endl; - std::cout << " -P, --partiality Scaling/merge model fixed|rot3d (default: fixed); rot3d = RotationScaleMerge" << - std::endl; + std::cout << " --force-still Scale as independent stills (ScaleOnTheFly) even for a rotation dataset " + "(default: rotation data uses RotationScaleMerge)" << std::endl; std::cout << " -A, --anomalous Anomalous mode (don't merge Friedel pairs)" << std::endl; std::cout << " -B, --refine-bfactor Refine per image B-factor" << std::endl; std::cout << " --min-partiality Minimum partiality to accept reflection (default: 0.02)" << @@ -60,7 +60,8 @@ enum { OPT_SCALING_ITERATIONS, OPT_SCALING_OUTPUT, OPT_SCALING_HIGH_RESOLUTION, - OPT_REFERENCE_COLUMN + OPT_REFERENCE_COLUMN, + OPT_FORCE_STILL }; @@ -73,7 +74,7 @@ static option long_options[] = { {"reference-mtz", required_argument, nullptr, 'z'}, {"reference-column", required_argument, nullptr, OPT_REFERENCE_COLUMN}, {"space-group", required_argument, nullptr, 'S'}, - {"partiality", required_argument, nullptr, 'P'}, + {"force-still", no_argument, nullptr, OPT_FORCE_STILL}, {"anomalous", no_argument, nullptr, 'A'}, {"refine-bfactor", no_argument, nullptr, 'B'}, {"scaling-high-resolution", required_argument, nullptr, OPT_SCALING_HIGH_RESOLUTION}, @@ -107,7 +108,7 @@ int main(int argc, char **argv) { int64_t scaling_iter = 3; std::optional intensity_format; // --scaling-output override; default lives in ScalingSettings (mmCIF) - bool combine_3d = false; // -P rot3d: weight-sum per-frame partials into fulls before merging + bool force_still = false; // --force-still: scale a rotation dataset as stills std::optional d_min_scale_merge; @@ -118,7 +119,7 @@ int main(int argc, char **argv) { int opt; int option_index = 0; - const char *short_opts = "vo:N:s:e:z:S:P:AB"; + const char *short_opts = "vo:N:s:e:z:S:AB"; while ((opt = getopt_long(argc, argv, short_opts, long_options, &option_index)) != -1) { switch (opt) { @@ -146,16 +147,8 @@ int main(int argc, char **argv) { case 'S': space_group_number = atoi(optarg); break; - case 'P': - if (strcmp(optarg, "fixed") == 0) - combine_3d = false; // stills: per-image ScaleOnTheFly - else if (strcmp(optarg, "rot3d") == 0) - combine_3d = true; // rotation: RotationScaleMerge - else { - logger.Error("Invalid scaling model (use fixed|rot3d): {}", optarg); - print_usage(); - exit(EXIT_FAILURE); - } + case OPT_FORCE_STILL: + force_still = true; break; case 'A': anomalous_mode = true; @@ -300,8 +293,13 @@ int main(int argc, char **argv) { experiment.SpaceGroupNumber(space_group_number); experiment.NumTriggers(1); + // Rotation indexing drives rotation scaling/merge downstream (IsRotationIndexing()); a rotation + // (goniometer) dataset uses RotationScaleMerge unless --force-still asks for per-image stills scaling. + IndexingSettings indexing_settings; + indexing_settings.RotationIndexing(experiment.GetGoniometer().has_value() && !force_still); + experiment.ImportIndexingSettings(indexing_settings); + ScalingSettings scaling_settings; - scaling_settings.Combine3D(combine_3d); if (d_min_scale_merge) scaling_settings.HighResolutionLimit_A(d_min_scale_merge.value()); scaling_settings.MergeFriedel(!anomalous_mode); @@ -333,7 +331,7 @@ int main(int argc, char **argv) { // Rotation (rot3d): the dedicated RotationScaleMerge does the whole self-scale -> 3D combine -> merge // (on the GPU when present). It does not support external-reference scaling, B-factor refinement or // wedge refinement. Everything else (stills, reference scaling) uses ScaleOnTheFly + MergeOnTheFly. - const bool is_rotation = combine_3d; // -P rot3d + const bool is_rotation = experiment.IsRotationIndexing(); if (is_rotation) { if (!reference_data.empty() || experiment.GetScalingSettings().GetRefineB() || experiment.GetRefineRotationWedgeInScaling() diff --git a/viewer/widgets/JFJochViewerSettingsDock.cpp b/viewer/widgets/JFJochViewerSettingsDock.cpp index a52840fd..8550e8b7 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.cpp +++ b/viewer/widgets/JFJochViewerSettingsDock.cpp @@ -584,9 +584,7 @@ void JFJochViewerSettingsDock::ApplyProcessingMode() { // Rotation good-path unless the dataset is stills or the user forces "Process as stills". const bool rotation_data = experiment_.GetGoniometer().has_value(); const bool rotation_mode = rotation_data && stills_ && !stills_->isChecked(); - indexing_.RotationIndexing(rotation_mode); - scaling_.ForceStillProcessing(!rotation_mode); - scaling_.Combine3D(rotation_mode); + indexing_.RotationIndexing(rotation_mode); // rotation indexing -> rotation scaling/merge downstream scaling_.ScaleFulls(rotation_mode); EmitSpotFinding(); // carries indexing_ (incl. RotationIndexing) to the worker emit scalingChanged(scaling_);