// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "JFJochException.h" // How the high-resolution cutoff for the written reflections and the reported shell table is chosen // when no explicit --scaling-high-resolution is given. Off = keep the full (detector-edge) range; // CCHalfLogistic = fit the CC1/2 fall-off and cut one shell past cc_target (DIALS-style, generous). enum class ResolutionCutoffMethod { Off, CCHalfLogistic }; class ScalingSettings { bool refine_wedge = false; bool merge_friedel = true; std::optional high_resolution_limit_A; std::optional wedge_for_scaling; std::optional forced_mosaicity; // diagnostic: fix the scaling mosaicity (deg) instead of the per-image seed double min_partiality = 0.02; // Capture-aware systematic uncertainty for the rot3d combine: a full reconstructed from only // a fraction f<1 of its rocking curve is extrapolated, and the unobserved (1-f) carries a // systematic error ~coeff*(1-f)*I that plain counting sigma misses. 0 = off (baseline). double capture_uncertainty_coeff = 0.0; // Full-level captured-fraction floor for the rot3d combine: drop a reconstructed full whose rocking // curve was only fractionally captured (sum of its partials' partiality < this). Distinct from // min_partiality, which gates individual partials; this gates the assembled full. 0 = off (baseline). double min_captured_fraction = 0.0; double min_cc_for_image = 0.0; // Exclude observations whose Lorentz geometry |zeta| falls below this from the DE-NOVO space-group // search merge only (see RotationScaleMerge::search_min_zeta). 0 = off. double search_min_zeta = 0.0; double outlier_reject_nsigma = 0.0; // per-observation merge outlier rejection (XDS/DIALS-style); 0 = off, e.g. 6 enables // 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; // Correction surfaces fitted on the rot3d fulls after scale-fulls: a decay (per-run Debye-Waller B) // and an absorption surface (over the diffracted-beam direction in the goniometer frame). Both are // cross-validated, so they no-op when their systematic is absent - hence ON by default (they only ever // help or do nothing). No-op without rot3d. Set false to disable both. bool correction_surfaces = true; // Absorption-surface refinement iteration count (used when correction_surfaces is on). int absorption_iter = 3; // Physical partiality post-refinement for the STILLS merge (StillsPartialityRefine): refine a per-crystal // orientation tilt against the running merge, recompute each reflection's partiality from the refined // geometry (angular Ewald-proximity model), and re-scale/merge - the "full model" for stills. ON by // default (helps mono stills, neutral on pink beam, tames weak data via a soft prior). rugnux // --simple-stills turns it OFF, reverting to treating every reflection as a full (p = 1, single pass). bool stills_partiality_refine = true; // Expected-variance merge weighting for the STILLS merge (MergeOnTheFly). When combining a reflection's // redundant observations by inverse variance, rebuild the Poisson signal part of each observation's // variance at the reflection's EXPECTED instead of the observation's own intensity. Weighting by an // observation's own sigma^2 biases the inverse-variance mean low at <1 photon (an up-fluctuated // observation gets a larger sigma and is over-downweighted). Default on - it mirrors the rotation combine // (RotationScaleMerge::process_rawrun), which already does this, and is R-free-neutral on strong data and // better on weak. --no-expected-variance-merge restores the old observed-sigma weighting. bool expected_variance_merge = true; // Merge-time ice-ring mask (FindDecorrelatedIceRings): after a first merge, drop a hexagonal-ice ring // whose merged half-set CC1/2 has collapsed below its resolution shoulders and merge again. Separate // from --detect-ice-rings, which flags ice SPOTS for indexing and keeps ice reflections out of the // SCALE fit - so this can be turned on or off on its own without changing how the data were indexed // and scaled. Only consulted when detect_ice_rings is on. // // OFF by default. Deleting reflections is not what the field does - AIMLESS, DIALS, xia2, XDS and // CrystFEL all keep ice-band reflections in the merge, and only autoPROC removes them - and the // deletion did not pay for itself when it was measured against a structure-referenced metric. On the // one crystal in the rotation battery where the mask both fires and the anomalous arbiter can score // it, dropping the ring changed the anomalous peak height by -0.001 +- 0.018 sigma (2% of the mean // site height), while costing 1149 unique reflections whose mean I/sigma was 3.62 against the // dataset's own 3.05 - i.e. it deletes better-than-average data. Overall R_meas, CC1/2 and ISa were // identical to three significant figures either way, and the affected shell went from 82.9% to // 100.0% complete without it. // // It is not useless, which is why the switch stays: over the 37-crystal rotation battery it fires // on 5, changes no space group, and those 5 disagree in sign - it clearly helps the two most // heavily iced (one gains 3.8 R_meas and 4.0 CC1/2 points, the other 23 points of high-shell // CC1/2), is a wash on two and costs a third. It always costs completeness where it fires. So: // off as a default, worth turning on by hand on a badly iced crystal. bool ice_ring_merge_mask = false; // Minimum measured ice strength (iceRingScore, 1 = no ice) before any ice-ring handling is applied // at all. The eleven fixed hexagonal bands cover 16-26% of the unique reflections at typical // resolutions REGARDLESS of whether the crystal has ice, so flagging unconditionally taxes clean // data for nothing - and the merge-time ring mask has been observed to fire on crystals with no // measurable ice. 0 disables the gate (always handle ice, the previous behaviour). // 1.5 is measured, not guessed: over 37 rotation crystals the score lands at 1.00-1.22 on the // thirty with no ice, 1.28-1.44 on four borderline ones whose ice-ring positions show no // azimuthally smooth elevation, and 2.08-2.37 on the three with confirmed ice - and a decoy null // (the identical statistic at ring positions where hexagonal ice cannot be) never exceeded 1.29. float ice_min_score = 1.5f; // The same gate on the SECOND ice channel: spots found on the hexagonal rings over the same q width // of ice-free flanks beside them (1 = spots spread evenly). This is what catches ice in large // crystallites, which diffracts as discrete spots and leaves the radial profile - and so // ice_min_score - flat. Also measured, not guessed: over 36 rotation crystals thirty read // 0.65-1.37 and a clean control 1.04, then 1.63/1.78 and a gap to 2.18-14.6 on the five whose // spots really do pile up on the rings. 0 disables this channel. float ice_min_spot_ratio = 2.0f; // Smooth the per-frame scale G across frames (centered moving average of log G) before the rot3d // combine, so a rocking event's partials share a consistent scale. Given as a ROTATION RANGE in // degrees (like XDS DELPHI), converted to an odd frame window from the oscillation step; this keeps // the smoothing physical (independent of frame slicing). 0 = off. A no-op without rot3d. double smooth_g_deg = 0.0; // Per-batch relative-B on the rot3d fulls (beyond the single global decay slope): bin frames into // rotation-range batches of this width in degrees and refine one relative Debye-Waller B per batch, so // NON-monotonic changes in scattering power across the run (absorption path, crystal slippage, dose // bursts) are corrected the resolution-flat per-frame G and the single decay slope both miss. Cross- // validated + zero-mean-anchored, so a no-op when absent. 0 = off. A no-op without rot3d. double relative_b_deg = 0.0; double rfree_fraction = 0.05; // Automatic high-resolution cutoff for the written reflections + reported shells (not the scaling // or the error model, and not the per-image _process.h5). Applied only when no explicit // high_resolution_limit_A is set - that manual limit always wins and disables the auto-cut. ResolutionCutoffMethod resolution_cutoff = ResolutionCutoffMethod::CCHalfLogistic; double resolution_cc_target = 0.30; // CC1/2 value defining the fall-off limit before the +1 shell int report_shell_count = 10; // number of resolution shells in the reported statistics table public: ScalingSettings& RefineRotationWedge(bool input); ScalingSettings& RotationWedgeForScaling(std::optional input); ScalingSettings& MergeFriedel(bool input); ScalingSettings& HighResolutionLimit_A(double limit); ScalingSettings& HighResolutionLimit_A(std::optional limit); // nullopt clears the limit ScalingSettings& MinPartiality(double min_partiality); ScalingSettings& ForcedMosaicity(std::optional input); ScalingSettings& CaptureUncertaintyCoeff(double input); ScalingSettings& MinCapturedFraction(double input); ScalingSettings& MinCCForImage(double min_cc_for_image); ScalingSettings& SearchMinZeta(double search_min_zeta); ScalingSettings& OutlierRejectNsigma(double input); ScalingSettings& ScaleFulls(bool input); ScalingSettings& AbsorptionIter(int input); ScalingSettings& CorrectionSurfaces(bool input); ScalingSettings& StillsPartialityRefine(bool input); ScalingSettings& ExpectedVarianceMerge(bool input); ScalingSettings& IceRingMergeMask(bool input); ScalingSettings& IceMinScore(float input); ScalingSettings& IceMinSpotRatio(float input); ScalingSettings& SmoothGDegrees(double input); ScalingSettings& RelativeBDegrees(double input); ScalingSettings& RfreeFraction(double input); ScalingSettings& ResolutionCutoff(ResolutionCutoffMethod input); ScalingSettings& ResolutionCCTarget(double input); ScalingSettings& ReportShellCount(int input); [[nodiscard]] bool GetRefineWedge() const; [[nodiscard]] double GetMinMosaicity() const; [[nodiscard]] double GetDefaultMosaicity() const; [[nodiscard]] double GetMaxMosaicity() const; [[nodiscard]] double GetMinWedge() const; [[nodiscard]] std::optional GetRotationWedgeForScaling() const; [[nodiscard]] double GetMaxWedge() const; [[nodiscard]] bool GetMergeFriedel() const; [[nodiscard]] std::optional GetHighResolutionLimit_A() const; [[nodiscard]] double GetMinPartiality() const; [[nodiscard]] std::optional GetForcedMosaicity() const; [[nodiscard]] double GetCaptureUncertaintyCoeff() const; [[nodiscard]] double GetMinCapturedFraction() const; [[nodiscard]] double GetMinCCForImage() const; [[nodiscard]] double GetSearchMinZeta() const; [[nodiscard]] double GetOutlierRejectNsigma() const; [[nodiscard]] bool GetScaleFulls() const; [[nodiscard]] int GetAbsorptionIter() const; [[nodiscard]] bool GetCorrectionSurfaces() const; [[nodiscard]] bool GetStillsPartialityRefine() const; [[nodiscard]] bool GetExpectedVarianceMerge() const; [[nodiscard]] bool GetIceRingMergeMask() const; [[nodiscard]] float GetIceMinScore() const; [[nodiscard]] float GetIceMinSpotRatio() const; [[nodiscard]] double GetSmoothGDegrees() const; [[nodiscard]] double GetRelativeBDegrees() const; [[nodiscard]] double GetRfreeFraction() const; [[nodiscard]] ResolutionCutoffMethod GetResolutionCutoff() const; [[nodiscard]] double GetResolutionCCTarget() const; [[nodiscard]] int GetReportShellCount() const; };