Files
Jungfraujoch/image_analysis/geom_refinement/PostRefine.h
T
leonarski_f cb5a2f032a
Build Packages / Create release (push) Successful in 23s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 10m6s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 9m6s
Build Packages / build:viewer-tgz:cpu (push) Successful in 11m15s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m21s
Build Packages / build:windows:nocuda (push) Successful in 17m9s
Build Packages / build:windows:cuda (push) Successful in 19m49s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m42s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m0s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m54s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 17m7s
Build Packages / build:rugnux:windows (push) Successful in 10m47s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m4s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m8s
Build Packages / Generate python client (push) Successful in 45s
Build Packages / Build documentation (push) Successful in 1m45s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 19m20s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m43s
Build Packages / build:rpm (rocky8) (push) Successful in 19m31s
Build Packages / build:rpm (rocky9) (push) Successful in 20m16s
Build Packages / Unit tests (push) Successful in 1h41m19s
v1.0.0-rc.170 (#80)
* Fixed a `jfjoch_broker` crash during indexing: sorting no longer misbehaves on non-finite values, and GPU FFT indexer kernel launches are now error-checked.
* rugnux needs about a third less peak memory to scale, merge and post-refine rotation data, with identical results.
* `rugnux --model`: the placed coordinate file carries the space group its own coordinates obey, and says so when that is not the group the reflection files beside it carry.
* `jfjoch_viewer`: fixes in the dataset plots, inspector and layout; spot markers lose their black outline by default (a checkbox under "Image features" restores it) and the highest-pixel markers are white boxes around the pixel.

Reviewed-on: #80
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-16 18:17:46 +02:00

122 lines
7.5 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "../../common/DiffractionGeometry.h"
#include "../../common/CrystalLattice.h"
#include "../../common/GoniometerAxis.h"
#include "../../common/UnitCell.h"
#include "../../common/Logger.h"
#include "../IntegrationOutcome.h"
#include "gemmi/symmetry.hpp"
// Post-integration geometry refinement for rotation data. Unlike the at-indexing XtalOptimizer, this runs
// AFTER integration/merge, where each reflection has an OBSERVED rocking centroid phi_obs (the intensity-
// weighted mean goniometer angle over the frames it spans) and an observed spot position. One JOINT fit
// refines the crystal (orientation, cell, rotation axis) and the detector (distance, beam centre) together
// against two residuals:
// * an Ewald excitation residual evaluated at phi_obs (distance-independent) -> pins the absolute cell
// scale that the positional residual leaves degenerate with the distance. Because phi_obs is the real
// rocking angle (not a frame centre) it is unbiased.
// * the positional detector<->reciprocal residual at each partial's observed spot -> pins the distance.
// Where there are more reflections than the fit's caps, the strongest by I/sigma are the ones kept;
// inside the fit every reflection carries the same weight.
struct PostRefineResult {
bool ok = false;
DiffractionGeometry geom; // refined (distance / beam left as configured)
UnitCell cell{}; // refined unit cell
int events_used = 0;
int obs_used = 0;
double distance_before_mm = 0.0, distance_after_mm = 0.0;
double beam_x_before_px = 0.0, beam_x_after_px = 0.0; // refined beam centre (GEOM mode)
double beam_y_before_px = 0.0, beam_y_after_px = 0.0;
// The joint fit is one decision - the crystal and the detector are refined together and committed
// together - so these two are always equal. Both are kept because the report names them separately.
bool cell_refined = false; // the joint fit passed cross-validation (crystal half)
bool detector_refined = false; // the joint fit passed cross-validation (detector half)
// The fit moved the distance or a cell length by more than one step of its trust region (see
// STEP_BOUND in PostRefine.cpp), so it walked rather than settling beside the geometry it started
// from. Such a move is not refused for its size - it is RATIFIED by re-integrating and re-indexing
// at it, which a second lattice does not survive and a wrong header does (see RunAllPasses).
bool large_move = false;
// Which test refused the fit and what it wanted, where it was refused; empty where it committed.
// Nothing else says it - the geometry that comes out of a refusal is the nominal one whatever
// refused it, so a silent refusal cannot be told from a fit that never wanted to move.
std::string refused_reason;
// GONIOMETER ROTATION SCALE: the factor by which the stage actually turned relative to the angle
// stored in the file (which is the COMMANDED value, hence a stage calibration error is invisible in
// the header). Fitted after the joint fit as a single free parameter, with the crystal and the axis
// direction held at their committed values. Always the fitted value; 1.0 = header and stage agree.
double rotation_scale = 1.0;
// Whether the fit passed every test needed to ACT on it: enough sweep and events, a significant and
// physically relevant size, and the same k from every fifth of the sweep. Only then is it applied.
bool rotation_scale_suspect = false;
};
struct PostRefineSettings {
gemmi::CrystalSystem crystal_system = gemmi::CrystalSystem::Triclinic;
bool refine_geometry = false; // XtalOptimizer-equivalent: the crystal (from phi_obs and the observed
// spot positions) and the detector distance + beam centre, in one
// cross-validated fit. The only supported refinement mode.
double excitation_weight = 1.0; // weight of the phi/excitation residual vs the positional one
int min_events = 50;
int num_threads = 1;
// An independent measurement of the beam centre in pixels, where the run has one (rugnux's pre-scan
// fit to the isotropy of the scattered background, which runs on every rotation run). The fit bounds
// how far it may move the beam from whichever of this and the nominal centre is NEARER; see
// BEAM_BOUND_PXL in PostRefine.cpp for why the nominal centre alone is not enough to bound it.
std::optional<std::array<double, 2>> measured_beam_px;
};
// One integrated partial, flattened across all images. Kept as narrow as the sort and the event
// split allow: on a large cell this array is gigabytes, and the scatter and every level of the
// per-bucket sort move all of it. The goniometer angle is not stored - it is a function of the
// image number alone, and is rebuilt from it where it is needed.
struct PostRefinePartial {
int h, k, l;
float img;
float I, sigma;
float obs_x, obs_y; // observed spot centroid (pixels); NAN if the box sum found no centroid
};
// The gathered form the fit runs on: every usable partial, bucketed by h (bstart = bucket starts,
// order = the buckets largest first) and sorted by (h, k, l, img) inside each bucket. Gathered
// apart from the fit itself so a caller that is finished with the per-image reflection vectors can
// hand them back before the fit's own allocations arrive - on a fine-sliced long axis the vectors
// are several times this array.
struct PostRefineObservations {
std::unique_ptr<PostRefinePartial[]> pts;
size_t n_pts = 0;
std::vector<int32_t> bstart; // size H+1: where each h bucket starts in pts
std::vector<int> order; // size H: bucket indices, largest bucket first
};
// Gather and sort the usable partials out of the per-image outcomes. release_reflections: each
// image's reflection vector is freed the moment it has been read - only for a caller that will
// never touch them again (the rotation geometry pre-pass, whose reflections are never written).
// An allocation failure returns an empty gather (n_pts = 0), which the fit refuses - the same
// silent refusal the fit gives every other failure.
PostRefineObservations GatherPostRefineObservations(std::vector<IntegrationOutcome> &outcomes,
size_t nthreads, bool release_reflections,
Logger &logger);
// nominal_geom / reference_latt: the current detector geometry and the phi=0 reference lattice (orientation
// + cell) from rotation indexing. obs: the gathered per-image integrated reflections (observed_x/y, I,
// sigma, image_number), from GatherPostRefineObservations. axis: the goniometer. Returns ok=false
// (geometry untouched) on failure.
PostRefineResult PostRefineRotationGeometry(PostRefineObservations obs,
const GoniometerAxis &axis,
const DiffractionGeometry &nominal_geom,
const CrystalLattice &reference_latt,
const PostRefineSettings &settings,
Logger &logger);