calibration: a fit that hands back the file's own tilt is not a calibration
A powder calibration is run because the file's geometry is in doubt, so a fit that quietly returns part of that file has answered nothing - and it is indistinguishable from one that worked, down to the residual and the sigmas arranged around it. On one of four LaB6 exposures of one detector the tilt came out at 2.93x its own sigma, a hundredth under the significance gate, so it was declined and pinned - at the master's hardcoded rot1 -0.08, rot2 -0.22 deg. That is eight times the tilt just refused, on no evidence, and worth 10 px of PONI at 190 mm. rugnux printed it to four decimal places, wrote the .poni, and exited 0. Judge the result on provenance instead of on any residual: a geometry is a measurement only if every parameter in it came from this data. Two ways out of the fits do not qualify - a covariance that never conditioned, so the fit cannot say what it determined, and a declined tilt pinned at a non-zero value from the file. A declined tilt over a file stating no tilt still qualifies, because reporting no tilt is then exactly what was measured; so does --no-refine-tilt, because a hold that was asked for is a stated choice and not a silent substitution. No single number separates the four. rms is 2.465 px against 1.44-1.64; the significance of all four lies between 2.93 and 4.47, so the gate is nearly a coin flip at these distances and moving it would only recalibrate on one population; and the failed fit has the TIGHTEST parameter sigmas of the set, because pinning the tilt removes the tilt/centre correlation that inflates a good fit's. The spot cross-check reads 13.5 px against 0.98-2.66, but 10.4 px of that is the pinned tilt moving the PONI - the same defect one step downstream, not independent evidence. On a failure rugnux says so, writes no .poni - a PONI file states where the detector is and has no field in which to say it does not know - writes the JSON with converged false and the reason beside it, and exits non-zero. The re-binning pass now prefers a converged refit over a non-converged one whatever its residual, so a tilt an earlier pass measured is not what a later one gets pinned at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,54 @@ CalibrationResult Summarize(const DiffractionGeometry &fitted,
|
||||
|
||||
} // namespace
|
||||
|
||||
// Is the geometry about to be returned a measurement of this data, or does it carry a number out of the
|
||||
// input file wearing the appearance of one? A calibration is run precisely because that file's geometry
|
||||
// is in doubt, so handing part of it back - to four decimal places, with a residual and a set of sigmas
|
||||
// arranged around it - is the one failure a caller cannot see. Nothing downstream can see it either: a
|
||||
// .poni states where the detector is and has no field for how that was arrived at.
|
||||
//
|
||||
// Two ways out of the fits below produce such a geometry. Neither is a threshold; both are statements
|
||||
// about where a number came from.
|
||||
//
|
||||
// The covariance never conditioned. RingOptimizer reports valid = false when the normal matrix is
|
||||
// singular at the solution - some direction in parameter space costs the fit nothing at all - and a
|
||||
// fit that cannot say what it determined has not determined it.
|
||||
//
|
||||
// The tilt was declined, and the value pinned in its place is not zero. Declining is the statement
|
||||
// that these rings cannot tell a tilt from a shift of the beam centre. Pinning then writes the INPUT
|
||||
// FILE's tilt into the answer, which those same rings support no better, and which is not even the
|
||||
// tilt that was just refused: measured on a 190 mm powder exposure, the gate rejected a tilt of
|
||||
// 0.026 deg at 2.9 sigma and returned the file's 0.22 deg - eight times larger, on no evidence, and
|
||||
// worth 10 px of PONI at that distance. Where the file's tilt is zero the two agree and the output is
|
||||
// honest: the fit found no tilt and reports none, which is what a long-distance fit should do. Where
|
||||
// it is not zero, the answer carries an angle from the header at whatever size the header stated.
|
||||
//
|
||||
// --no-refine-tilt is not this. There the user asked for the file's tilt to be held, and a held tilt
|
||||
// that was asked for is a stated choice rather than a silent substitution.
|
||||
void JudgeCalibration(CalibrationResult &result, const DiffractionGeometry &header,
|
||||
bool refine_tilt) {
|
||||
if (!result.uncertainty.valid) {
|
||||
result.converged = false;
|
||||
result.reason = "the fit is degenerate at its solution - its covariance does not condition, so "
|
||||
"it cannot say what it determined";
|
||||
return;
|
||||
}
|
||||
if (refine_tilt && !result.tilt_refined
|
||||
&& (header.GetPoniRot1_rad() != 0.0f || header.GetPoniRot2_rad() != 0.0f)) {
|
||||
constexpr double RAD_TO_DEG = 180.0 / PI;
|
||||
const double poni_pxl = std::hypot(header.GetPoniRot1_rad(), header.GetPoniRot2_rad())
|
||||
* result.geometry.GetDetectorDistance_mm() / result.geometry.GetPixelSize_mm();
|
||||
result.converged = false;
|
||||
result.reason = fmt::format(
|
||||
"the tilt was declined at {:.1f}x its own sigma and pinned at the input file's "
|
||||
"rot1 {:+.4f} deg, rot2 {:+.4f} deg - a tilt these rings measured no better than the "
|
||||
"one they refused, and worth {:.1f} px of PONI at this distance",
|
||||
result.tilt_significance,
|
||||
header.GetPoniRot1_rad() * RAD_TO_DEG, header.GetPoniRot2_rad() * RAD_TO_DEG,
|
||||
poni_pxl);
|
||||
}
|
||||
}
|
||||
|
||||
float TiltSignificance(const DiffractionGeometry &geom, const RingFitUncertainty &unc) {
|
||||
if (!unc.valid)
|
||||
return 0.0f;
|
||||
@@ -328,6 +376,7 @@ CalibrationResult CalibrateFromProfile(const std::vector<float> &profile,
|
||||
result.tilt_significance = significance;
|
||||
result.seed_distance_mm = best_provenance.seed_mm;
|
||||
result.header_distance_mm = geom.GetDetectorDistance_mm();
|
||||
JudgeCalibration(result, geom, refine_tilt);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -365,6 +414,7 @@ CalibrationResult CalibrateFromSpots(const std::vector<SpotToSave> &spots,
|
||||
result.tilt_refined = tilt_refined;
|
||||
result.tilt_significance = significance;
|
||||
result.header_distance_mm = geom.GetDetectorDistance_mm();
|
||||
JudgeCalibration(result, geom, refine_tilt);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -462,6 +512,11 @@ void WriteCalibrationJson(const std::string &path, const DiffractionExperiment &
|
||||
}
|
||||
|
||||
nlohmann::json calibration;
|
||||
// The verdict first, because everything under it is only worth reading once it is known which of the
|
||||
// two this file is. A caller that reads nothing else must still not mistake a non-fit for a fit.
|
||||
calibration["converged"] = result.converged;
|
||||
if (!result.converged)
|
||||
calibration["not_converged_reason"] = result.reason;
|
||||
calibration["calibrant"] = calibrant;
|
||||
calibration["method"] = method;
|
||||
calibration["ring_points"] = result.ring_points;
|
||||
|
||||
Reference in New Issue
Block a user