// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include "../../common/Logger.h" #include "../../common/Reflection.h" // MergedReflection #include "../../common/ScalingSettings.h" // ResolutionCutoffMethod // Automatic high-resolution cutoff from the CC1/2 fall-off of the merged half-sets (DIALS-style). // The merge itself, the error model and the per-image _process.h5 are left untouched - only the // written reflections and the reported shell table should be trimmed to the returned d_cut. // // Method (see docs/rugnux_resolution_cutoff_design.md): bin CC1/2 against s = 1/d^2 in fine bins, // fit a logistic CC1/2(s) = 1/(1+exp(k*(s-s0))) to the contiguous-from-low-res fall-off, take the s // where the fit crosses cc_target, then extend by one mean (10-shell) shell width in s ("one shell // too far", generous). Where the fitted crossing lands past the bins the fit was made over - a // fall-off region too ragged for a logistic to follow, so the crossing is an extrapolation rather // than something the bins show - the crossing is read off the bins themselves instead, and // everything after it is unchanged. d_cut is nullopt when the fit is degenerate (too few bins, flat) // or CC1/2 never falls below cc_target inside the measured range - the caller then keeps the full // range. cc_target in (0,1); merged must carry finite I_half[0]/I_half[1] to contribute. struct ResolutionCutoffResult { std::optional d_cut; // high-resolution limit (A); nullopt => keep the full range // Where CC1/2 crosses cc_target, BEFORE the deliberate one-shell extension - i.e. the // resolution the data are judged to reach, as opposed to the (coarser in s, finer in d) limit the // reflections are actually written to. This is the number to quote. Set whenever the fit produced // a crossing inside the measured range, even when no cut was applied. std::optional d_fit; std::string note; // human-readable description of the decision (for logging) }; // The run's typical per-frame scale, G_ref = sum G^3 / sum G^2 over the observations (n_obs[f] of // them on frame f): precision-weighted, so frames the crystal barely diffracted on cannot drag it // down however many of them there are, where a median of the frames is itself a dead frame on a // sweep that spent most of its turn out of the beam. The one definition of "typical frame" - the // scaling loop pins its gauge to it, the CC1/2 weight and every frame guard measure against it. A // frame without a finite positive scale counts as G = 1; 1 when no frame carries an observation. double TypicalFrameScale(const std::vector &frame_scale, const std::vector &n_obs); // The per-frame factor of the CC1/2 weight (MergedReflection::cc_weight): max(1, (G_ref/G)^2), the // variance an observation from a frame at scale G carries over the same observation at the typical // scale G_ref (TypicalFrameScale). std::vector CCHalfFrameFactors(const std::vector &frame_scale, const std::vector &n_obs); ResolutionCutoffResult ComputeCCHalfLogisticCutoff(const std::vector &merged, double cc_target, Logger &logger); // Resolve the effective high-resolution limit and trim `merged` to it, in one place shared by the // stills merge, the rotation merge and the offline --scale path. A manual limit (manual_limit) wins; // otherwise, unless this is a P1 space-group search merge (for_search), the CCHalfLogistic method // takes the auto CC1/2 cutoff. Reflections beyond the resolved limit are erased from `merged`. // Returns the applied limit (nullopt => the full range was kept). The decision is logged as before. // fit_limit_out, when given, receives ResolutionCutoffResult::d_fit - the CC1/2 crossing without the // one-shell extension, for reporting. Left untouched when no automatic fit ran (a manual limit, a // search merge, or the method turned off), so a caller can tell "not fitted" from "fitted". std::optional ApplyResolutionCutoff(std::vector &merged, std::optional manual_limit, ResolutionCutoffMethod method, double cc_target, bool for_search, Logger &logger, std::optional *fit_limit_out = nullptr);