Files
Jungfraujoch/common/ScalingSettings.h
T
leonarski_f fca2a77b0c Scaling: the low-resolution limit bounds the scale fit, not the merge
Reflections coarser than --scaling-low-resolution (default 50 A) are now merged and written - what
maps and refinement want of the lowest resolutions - but stay out of every fit of the scale model, by the
same path an ice-ring observation takes (excluded from scaling, kept for merging), under a name of its
own: RotationScaleMerge's per-observation flag is now `unscaled` (UNSCALED_ICE | UNSCALED_LOW_RES; on the
device OBS_UNSCALED = OBS_ON_ICE_FLAG | OBS_LOW_RES_FLAG), set from the reflection on the host and in the
device ingest, and only its ice bit marks a merged reflection as on an ice ring. The stills merge drops
its low-resolution cut; ScaleOnTheFly, StillsPartialityRefine and the reindexing correlation keep it.
The merge statistics run over the whole sphere unless --report-resolution declares a range, so the
reflections the beam stop hides count as missing. Which coarse reflections exist at all is the beam-stop
shadow test's decision (a7610d847).

Why not simply lift the limit: on 5nw5, 35 reflections beyond 50 A bent the scale model enough to move
the merged 20-50 A intensities by +61 % and 10-20 A by +19 % (Wilson B 319 -> 382, REFMAC R-free +0.006).
Kept out of the fit, the merged intensities at 10-50 A are unchanged to 0.2 % and Wilson B is 307.

CPU subset battery against the previous commit (lp-cpu-v2 / lp-cpu-v1): REFMAC 0-cycle R-free 5nw5
+0.0004, 6oel +0.0005, others within 0.0001; rugnux's own R-free/R_model rise where the new >50 A terms
enter it (lyso R_model 0.2714 -> 0.2739; the model is poorest there). Kept beyond 50 A: 7qij 61, 5nw5 35,
6oel 14 unique. In-house photon-counting sets unchanged, except insu_I_x06da_ref, where the delta-CC1/2
frame exclusion now removes 449 instead of 78 frames: refres R_meas 0.246 -> 0.167, ISa 33.5 -> 35.4,
R_free 0.2443 -> 0.2427, anomalous signal 3.31 -> 3.38 (XDS: R_meas 0.349, ISa 25.1). Stills (lcls242_s/t,
lcls222, euxfel83, sacla236) within 0.0002 of the previous commit. GPU (device ingest) and CPU give the
same 5nw5 merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit 32a9fbcb3ca67879ceb051c13f58d61fe85530f4)
2026-10-25 07:20:21 +01:00

193 lines
12 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <optional>
#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 };
// A resolution range in A, d_max coarse and d_min fine; d_max may be infinite (no low-resolution bound).
struct ReportResolutionRange {
double d_min = 0.0;
double d_max = 0.0;
};
class ScalingSettings {
bool refine_wedge = false;
bool merge_friedel = true;
std::optional<double> high_resolution_limit_A;
// Low-resolution limit of the scale FIT. Reflections coarser than it are still merged and written -
// they are what maps and refinement want of the lowest resolutions - but are kept out of every fit
// of the scale model: on a large cell a few dozen of them, measured against a background the beam
// stop depresses, bent the model enough to move the merged 10-50 A intensities by tens of percent.
// Which coarse reflections are measured at all is the beam-stop mask's decision (a reflection whose
// signal disk reaches the shadow is not integrated, MXAnalysisWithoutFPGA). 50 A is XDS's default.
std::optional<double> low_resolution_limit_A = 50.0;
std::optional<double> wedge_for_scaling;
std::optional<double> 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 = 30; // the cap; a surface stops refining once its factors settle
// 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 <I> 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;
// 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. 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 = 9; // resolution shells in the reported statistics table (XDS's count,
// over the same equal-1/d^2 bins, so the two tables read row for row)
// A second statistics table over this REFERENCE range (--report-resolution), binned from the same
// scaled and merged observations as the run's own table and reported beside it, so a run can be
// read against another program's numbers at that program's range. Report-only: the cut, the
// scaling, the error model and every decision are the run's own whether or not it is set.
std::optional<ReportResolutionRange> report_resolution_range;
public:
ScalingSettings& RefineRotationWedge(bool input);
ScalingSettings& RotationWedgeForScaling(std::optional<double> input);
ScalingSettings& MergeFriedel(bool input);
ScalingSettings& HighResolutionLimit_A(double limit);
ScalingSettings& HighResolutionLimit_A(std::optional<double> limit); // nullopt clears the limit
ScalingSettings& LowResolutionLimit_A(std::optional<double> limit); // nullopt clears the limit
ScalingSettings& MinPartiality(double min_partiality);
ScalingSettings& ForcedMosaicity(std::optional<double> 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& 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);
ScalingSettings& ReportResolutionRange_A(std::optional<ReportResolutionRange> input);
[[nodiscard]] bool GetRefineWedge() const;
[[nodiscard]] double GetMinMosaicity() const;
[[nodiscard]] double GetDefaultMosaicity() const;
[[nodiscard]] double GetMaxMosaicity() const;
[[nodiscard]] double GetMinWedge() const;
[[nodiscard]] std::optional<double> GetRotationWedgeForScaling() const;
[[nodiscard]] double GetMaxWedge() const;
[[nodiscard]] bool GetMergeFriedel() const;
[[nodiscard]] std::optional<double> GetHighResolutionLimit_A() const;
[[nodiscard]] std::optional<double> GetLowResolutionLimit_A() const;
[[nodiscard]] double GetMinPartiality() const;
[[nodiscard]] std::optional<double> 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]] 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;
[[nodiscard]] std::optional<ReportResolutionRange> GetReportResolutionRange_A() const;
};