Files
Jungfraujoch/common/ScalingSettings.h
T
leonarski_fandClaude Opus 5 a29c36600f Beam-stop shadow detection, and a low-resolution limit for scaling
rugnux finds the beam stop and its holder in a projection of 60 images and
marks them in the pixel mask as bit 9 (--detect-beam-stop[=N|off], on by
default). Reflections behind the stop are attenuated but not flagged, so they
integrate low with a plausible sigma and nothing downstream catches them: the
signal-box gate requires 100% valid pixels and shadow pixels are valid, the
background clip is high-side only, and the |zeta| cut applies only to the
space-group search merge.

The detection compares each pixel's background against the typical background
at the same radius on two channels. An azimuthal one (the ring median) finds
the holder arm, which is a minority of its ring; a radial one (the background
just outside) finds the disk, which the ring median cannot see because inside a
fully blocked ring the median is the shadow itself. Pixels are pooled over a
5x5 box and tested only where the background has actually been counted, so
low-background data no longer masks the whole detector. Recorded reflections
are carved back out - a beam stop cannot block a reflection that was measured.

Bit 9 belongs to the run that found it, not to the dataset: it is cleared when
a run starts, so a mask read back from a file that carries one starts clear.
The user mask (bit 8) is left alone.

Scaling and merging gain a low-resolution limit, default 50 A
(--scaling-low-resolution <num>, 0 removes it), applied per observation before
scaling so it also protects the per-frame scale fit and the space-group search.
50 A is the value XDS configurations use; rugnux_vs_xds.py now matches both of
XDS's resolution limits instead of only the high one, so the lowest shell is
the same shell in the two programs.

The viewer draws the detected shadow in coral with a "Show beam stop" switch in
the side panel, exposes the low-resolution limit in the settings dock, and
offers detection in its processing jobs. Adding an image marker meant giving
the reader a MIN_REAL_PXL_VALUE, because several places classify a pixel by
range rather than by equality and would otherwise read the new marker as a very
negative intensity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:05:31 +02:00

177 lines
10 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 };
class ScalingSettings {
bool refine_wedge = false;
bool merge_friedel = true;
std::optional<double> high_resolution_limit_A;
// Low-resolution limit for scaling and merging. Reflections coarser than this are behind or
// beside the beam stop and are measured on a background the stop has eaten into; past 50 A a
// large share of them come out negative. 50 A is what XDS's own configurations use, so keeping
// it here is also what makes the two comparable at the coarse end.
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 = 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 <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 = 10; // number of resolution shells in the reported statistics table
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);
[[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;
};