Build Packages / build:viewer-tgz:cpu (push) Successful in 20m2s
Build Packages / build:viewer-tgz:cuda (push) Successful in 20m10s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 22m20s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 23m33s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m3s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m7s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m50s
Build Packages / XDS test (durin plugin) (push) Successful in 11m12s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 21m35s
Build Packages / build:rpm (rocky9) (push) Successful in 21m32s
Build Packages / Generate python client (push) Successful in 39s
Build Packages / Build documentation (push) Successful in 1m29s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky8) (push) Successful in 26m3s
Build Packages / DIALS test (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m18s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 21m11s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m17s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m2s
Build Packages / Unit tests (push) Successful in 1h18m44s
Build Packages / build:windows:nocuda (push) Failing after 12m15s
Build Packages / build:windows:cuda (push) Failing after 11m57s
A stage that turns further than commanded is invisible in the file, because the stored omega values ARE the commanded ones - both XDS and rugnux then read the discrepancy as the crystal drifting. Measured on one dataset in 37, a ~1.2 % over-rotation costs it unique 9.9k -> 29k and CC1/2 68 -> 98 % when corrected by hand. No new degree of freedom is added, because the one needed is already there and being thrown away: step A's residual rotates by -angle_rad * axis[] with axis an UNNORMALISED 3-vector, so the length it fits IS the factor by which the stage actually turned. GoniometerAxis::Axis then normalises it away (with the `increment *= len` line sitting commented out). This only reports it. Guarded by the same cross-validation that gates the cell move - a fold that merely soaked up noise cannot raise the flag - and by a 0.5 % tolerance, which is where a direct scan of this factor puts 36 of 37 datasets (all at exactly 1.0000). The known fault reads 1.00604 and warns; clean controls read 0.99958 and 0.99954. It UNDER-reads the true magnitude: the fit only sees reflections already indexed at the nominal angle, per-frame orientation refinement has absorbed part of the error, and the axis components are bounded. Treat it as a detector, not a calibration - nothing here corrects the data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
67 lines
4.1 KiB
C++
67 lines
4.1 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)
|
|
// Implied GONIOMETER ROTATION SCALE: step A's axis vector is unnormalised, so the length it fits is
|
|
// 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). Reported only -
|
|
// nothing here applies it, and it adds no degree of freedom: the parameter was always refined, its
|
|
// length was simply normalised away and thrown out. 1.0 = header and stage agree.
|
|
double rotation_scale = 1.0;
|
|
bool rotation_scale_suspect = false; // |rotation_scale - 1| over the tolerance AND cross-validated
|
|
};
|
|
|
|
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);
|