// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #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 &input, RingFitUncertainty *unc = nullptr); };