Files
Jungfraujoch/image_analysis/geom_refinement/RingOptimizer.h
T
leonarski_fandClaude Opus 5 fba5435c38 calibration: report what the ring fit knows about its own answer
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
2026-08-31 16:11:18 +02:00

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);
};