The "median rocking width -> estimated mosaicity" line took an intensity-weighted second moment of the frame-centre angles with max(0, I) weights, per event, then a median over events. For a two-frame event that moment is exactly zero whenever only one frame has I > 0 - probability 2/3 for a reflection carrying no signal - so on a noise-dominated dataset the median lands in the degenerate spike and prints 0.0000. Simulated against a known width it is wrong by 0.23x to 13x, in both directions, and on a pure-noise null it returns a plausible-looking 0.06 deg. est_mosaicity_deg was read nowhere, so nothing downstream was affected; the number only misled whoever read the log. It was built to measure a signal for a mosaicity refinement that was then abandoned, and the estimator that replaced it is the per-image one that already drives prediction. Report instead the frames per rocking event, which is what the block could honestly say: near 2.0 the reflections barely rock, so the observed angle this refinement is fitted to is under-determined. It is a geometry count, so noise cannot inflate it. Also drop phi_rms_deg, which is never assigned anywhere, and Partial::zeta, which is only written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
65 lines
3.9 KiB
C++
65 lines
3.9 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 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
|
|
// 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);
|