Build Packages / Create release (push) Successful in 16s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m27s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m15s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m11s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m6s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m44s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m1s
Build Packages / build:windows:nocuda (push) Successful in 17m29s
Build Packages / build:windows:cuda (push) Successful in 19m58s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m7s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m8s
Build Packages / build:rugnux:windows (push) Successful in 10m58s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m46s
Build Packages / Generate python client (push) Successful in 53s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m13s
Build Packages / Build documentation (push) Successful in 1m36s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m57s
Build Packages / build:rpm (rocky8) (push) Successful in 18m7s
Build Packages / build:rpm (rocky9) (push) Successful in 18m54s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 19m32s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 17m30s
Build Packages / Unit tests (push) Successful in 1h39m2s
* Fixed `jfjoch_broker` cancelling every data collection with a CUDA "out of memory" error after long operation: GPU memory no longer leaks with each collection. * Rugnux scales a rotation sweep until the per-frame scales settle instead of for a fixed three rounds, and says so when they did not - merged intensities, and the space group, resolution cut and frame rejection read off them, change accordingly; `--scaling-iterations` is now the cap on that loop (default 100). * Rugnux places every frame of a marCCD, SMV or miniCBF series at the spindle angle its own header states, so a series with missing frames, or with angles written modulo 360, is no longer read at the wrong geometry or refused. * Every rotation run writes two diagnostic files beside its reflections: `<prefix>_detector.jpg`, the detector projection with the pixel mask and the detected beam-stop shadow drawn on it, and `<prefix>_plot.txt`, one row per image. Reviewed-on: #82 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
69 lines
4.6 KiB
C++
69 lines
4.6 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 <string>
|
|
#include <vector>
|
|
|
|
#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<double> 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<double> 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<double> &frame_scale, const std::vector<int64_t> &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<double> CCHalfFrameFactors(const std::vector<double> &frame_scale,
|
|
const std::vector<int64_t> &n_obs);
|
|
|
|
ResolutionCutoffResult ComputeCCHalfLogisticCutoff(const std::vector<MergedReflection> &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<double> ApplyResolutionCutoff(std::vector<MergedReflection> &merged,
|
|
std::optional<double> manual_limit,
|
|
ResolutionCutoffMethod method,
|
|
double cc_target,
|
|
bool for_search,
|
|
Logger &logger,
|
|
std::optional<double> *fit_limit_out = nullptr);
|