Files
Jungfraujoch/image_analysis/geom_refinement/PostRefine.h
T
leonarski_fandClaude Opus 4.8 1b91823eed rugnux: --rotation-post-refine two-pass geometry post-refinement (opt-in)
A first pass integrates and post-refines the detector geometry from the observed
diffraction, applies it to experiment_, and a second pass re-indexes and
re-integrates with it - only the second pass is written, so the _process.h5 / mtz
(and the ice-ring flags, which move with the geometry) reflect the refined values.
Default off; enable with --rotation-post-refine.

PostRefineRotationGeometry refines in two SEPARATE cross-validated steps (a joint
fit of everything is fragile - the same lesson as integration, where refining the
profile width and the scale jointly fails but separately works):
  * Step A: cell scale + rotation axis from the observed rocking centroids phi_obs
    (a distance-independent excitation residual, so it pins the absolute cell scale
    that the positional residual leaves degenerate with distance).
  * Step B: detector distance + beam centre from the observed spot positions, with
    the cell fixed at step A.
Each step commits only if it lowers a held-out (deterministic split-half) residual,
else that part of the geometry is left at nominal.

Rugnux::Run wraps RunPipeline(write_output, geometry_prepass); the pre-pass runs
the first scale/merge so RotationScaleMerge fits a frame-order-smoothed mosaicity,
which is captured and fed to the second pass's Bragg prediction (via
IndexAndRefine::SetPredictionMosaicityOverride) rather than re-derived per image.
observed_x/y were plumbed through the profile integrator earlier for the positional
residual. rugnux_vs_xds.py gains --extra-args to sweep the option over the battery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 12:19:33 +02:00

60 lines
3.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 <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. It refines one
// shared crystal orientation + cell (+ optionally the detector distance) 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.
// Reflections are weighted by their merged I/sigma (strong, well-measured reflections dominate).
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 phi_rms_deg = 0.0; // RMS(phi_obs - phi_pred) at the refined model (fit quality)
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;
bool cell_refined = false; // GEOM step A (cell scale + axis) passed cross-validation
bool detector_refined = false; // GEOM step B (distance + beam) passed cross-validation
double est_mosaicity_deg = 0.0; // mosaicity estimated from the observed rocking widths (0 = n/a)
};
struct PostRefineSettings {
gemmi::CrystalSystem crystal_system = gemmi::CrystalSystem::Triclinic;
bool refine_geometry = false; // XtalOptimizer-equivalent: cell scale + axis (from phi_obs) and detector
// distance + beam centre (from the observed spot positions X,Y), as two
// separate cross-validated steps. 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;
};
// nominal_geom / reference_latt: the current detector geometry and the phi=0 reference lattice (orientation
// + cell) from rotation indexing. outcomes: the per-image integrated reflections (observed_x/y, I, sigma,
// image_number). axis: the goniometer. Returns ok=false (geometry untouched) on failure.
PostRefineResult PostRefineRotationGeometry(const std::vector<IntegrationOutcome> &outcomes,
const GoniometerAxis &axis,
const DiffractionGeometry &nominal_geom,
const CrystalLattice &reference_latt,
const PostRefineSettings &settings,
Logger &logger);