The ring fit reported the SCATTER of its measurements (rms, and the beam-centre standard error that follows from it) but nothing about how well the fit pinned each parameter. Those two part company exactly where a calibration is worth doubting: as the rings run out, the tilt and the beam centre stop being separable - both displace a ring's radius as cos(phi) and only the way that amplitude scales with radius tells them apart - so the fit can sit tightly on the few points it has while being free to spend tens of pixels of beam centre on a tilt the data do not support. Take the covariance of the converged problem from Ceres and report it. Measured on a LaB6 distance series, the fitted tilt is 50 sigma at 110 mm and 0.1 sigma at 500 mm, where only two rings reach the detector; at 500 mm the fit quotes its own beam centre to +-180 px and its tilt to +-2.9 deg on a 0.35 deg value, and the correlation between them is 1.000. Nothing acts on this yet - it is printed so the next change can gate on it. Ceres returns the bare (J'J)^-1 of an unweighted problem, so it is scaled by chi2 per degree of freedom; that leaves the sigmas in pixels, mm and radians whatever unit the residual is stated in. Cost is one 5x5 SVD per run, below the noise of the surrounding I/O. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfuDvf5ipV3Hi8TiCUKD27
57 lines
2.9 KiB
C++
57 lines
2.9 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <vector>
|
|
#include "../../common/DiffractionGeometry.h"
|
|
|
|
struct RingOptimizerInput {
|
|
float x;
|
|
float y;
|
|
double q_expected;
|
|
};
|
|
|
|
// What the ring fit knows about its own answer, from the covariance of the converged problem.
|
|
//
|
|
// The beam centre and the tilt are not independent: both displace a ring's radius as cos(phi), and
|
|
// only how that amplitude scales with the ring's radius tells them apart, which takes two well-sampled
|
|
// rings. On one ring they are exactly degenerate; on two they are merely badly correlated, and the fit
|
|
// still returns an answer - it just spends tens of pixels of beam centre to buy a tilt the data cannot
|
|
// support. The sigmas below say when that has happened and the correlations say why.
|
|
//
|
|
// Sigmas are in each parameter's own unit and are scaled by the residual variance of THIS fit
|
|
// (chi2_per_dof), so they are the usual "how far could this parameter move before the fit got visibly
|
|
// worse" and not Ceres' bare (J^T J)^-1. A tilt held fixed reports sigma 0 - it was not a parameter.
|
|
struct RingFitUncertainty {
|
|
bool valid = false; // false if the covariance could not be computed - itself a verdict:
|
|
// the problem is exactly rank-deficient at the solution
|
|
double sigma_beam_x_pxl = 0.0;
|
|
double sigma_beam_y_pxl = 0.0;
|
|
double sigma_distance_mm = 0.0;
|
|
double sigma_rot1_rad = 0.0;
|
|
double sigma_rot2_rad = 0.0;
|
|
// The two degenerate pairs. rot1 tips the detector about the vertical, so it trades against the
|
|
// beam centre in x; rot2 tips it about the horizontal and trades against y. |corr| approaching 1
|
|
// is the signature of a tilt the rings do not separate from a beam-centre shift.
|
|
double corr_beam_x_rot1 = 0.0;
|
|
double corr_beam_y_rot2 = 0.0;
|
|
double chi2_per_dof = 0.0; // in the residual's own (q^2) units - a scale, not a goodness of fit
|
|
int free_parameters = 0;
|
|
};
|
|
|
|
class RingOptimizer {
|
|
DiffractionGeometry reference;
|
|
bool refine_tilt;
|
|
public:
|
|
// refine_tilt = false holds the detector tilt (rot1/rot2) at the value the geometry came in with
|
|
// and fits only the beam centre and the distance. A tilted PONI is exact but not every consumer
|
|
// accepts one - XDS has no place to put it - so a calibration meant for such a program is better
|
|
// measured with the tilt pinned than with it refined and then dropped.
|
|
RingOptimizer(const DiffractionGeometry& geom, bool refine_tilt = true);
|
|
// unc, when given, receives the covariance of the converged fit. Computing it is a 5x5 SVD and
|
|
// costs nothing next to the solve, so there is no option to switch it off - pass nullptr instead.
|
|
DiffractionGeometry Run(const std::vector<RingOptimizerInput> &input,
|
|
RingFitUncertainty *unc = nullptr);
|
|
};
|