From ef5da293196768a71925343f2d8aa57b08fb52e1 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 1 Sep 2026 23:29:55 +0200 Subject: [PATCH] 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) --- .../gen/model/Powder_calibration_quality.cpp | 60 ++++++++++++- broker/gen/model/Powder_calibration_quality.h | 18 ++++ broker/jfjoch_api.yaml | 11 +++ broker/redoc-static.html | 2 +- docs/CHANGELOG.md | 1 + docs/RUGNUX_CALIBRATION.md | 11 +++ frontend/src/client/types.gen.ts | 13 +++ frontend/src/client/zod.gen.ts | 2 + .../geom_refinement/PowderCalibration.cpp | 55 ++++++++++++ .../geom_refinement/PowderCalibration.h | 15 ++++ rugnux/Rugnux.cpp | 14 ++- rugnux/rugnux_cli.cpp | 22 ++++- tests/CalibrationTest.cpp | 89 +++++++++++++++++++ 13 files changed, 307 insertions(+), 6 deletions(-) diff --git a/broker/gen/model/Powder_calibration_quality.cpp b/broker/gen/model/Powder_calibration_quality.cpp index f68ab9eed..9866c96ca 100644 --- a/broker/gen/model/Powder_calibration_quality.cpp +++ b/broker/gen/model/Powder_calibration_quality.cpp @@ -21,6 +21,10 @@ namespace org::openapitools::server::model Powder_calibration_quality::Powder_calibration_quality() { + m_Converged = false; + m_ConvergedIsSet = false; + m_Not_converged_reason = ""; + m_Not_converged_reasonIsSet = false; m_Calibrant = ""; m_CalibrantIsSet = false; m_Method = ""; @@ -67,7 +71,7 @@ bool Powder_calibration_quality::validate(std::stringstream& msg, const std::str bool success = true; const std::string _pathPrefix = pathPrefix.empty() ? "Powder_calibration_quality" : pathPrefix; - + return success; } @@ -77,6 +81,12 @@ bool Powder_calibration_quality::operator==(const Powder_calibration_quality& rh + ((!convergedIsSet() && !rhs.convergedIsSet()) || (convergedIsSet() && rhs.convergedIsSet() && isConverged() == rhs.isConverged())) && + + + ((!notConvergedReasonIsSet() && !rhs.notConvergedReasonIsSet()) || (notConvergedReasonIsSet() && rhs.notConvergedReasonIsSet() && getNotConvergedReason() == rhs.getNotConvergedReason())) && + + ((!calibrantIsSet() && !rhs.calibrantIsSet()) || (calibrantIsSet() && rhs.calibrantIsSet() && getCalibrant() == rhs.getCalibrant())) && @@ -126,6 +136,10 @@ bool Powder_calibration_quality::operator!=(const Powder_calibration_quality& rh void to_json(nlohmann::json& j, const Powder_calibration_quality& o) { j = nlohmann::json::object(); + if(o.convergedIsSet()) + j["converged"] = o.m_Converged; + if(o.notConvergedReasonIsSet()) + j["not_converged_reason"] = o.m_Not_converged_reason; if(o.calibrantIsSet()) j["calibrant"] = o.m_Calibrant; if(o.methodIsSet()) @@ -157,6 +171,16 @@ void to_json(nlohmann::json& j, const Powder_calibration_quality& o) void from_json(const nlohmann::json& j, Powder_calibration_quality& o) { + if(j.find("converged") != j.end()) + { + j.at("converged").get_to(o.m_Converged); + o.m_ConvergedIsSet = true; + } + if(j.find("not_converged_reason") != j.end()) + { + j.at("not_converged_reason").get_to(o.m_Not_converged_reason); + o.m_Not_converged_reasonIsSet = true; + } if(j.find("calibrant") != j.end()) { j.at("calibrant").get_to(o.m_Calibrant); @@ -225,6 +249,40 @@ void from_json(const nlohmann::json& j, Powder_calibration_quality& o) } +bool Powder_calibration_quality::isConverged() const +{ + return m_Converged; +} +void Powder_calibration_quality::setConverged(bool const value) +{ + m_Converged = value; + m_ConvergedIsSet = true; +} +bool Powder_calibration_quality::convergedIsSet() const +{ + return m_ConvergedIsSet; +} +void Powder_calibration_quality::unsetConverged() +{ + m_ConvergedIsSet = false; +} +std::string Powder_calibration_quality::getNotConvergedReason() const +{ + return m_Not_converged_reason; +} +void Powder_calibration_quality::setNotConvergedReason(std::string const& value) +{ + m_Not_converged_reason = value; + m_Not_converged_reasonIsSet = true; +} +bool Powder_calibration_quality::notConvergedReasonIsSet() const +{ + return m_Not_converged_reasonIsSet; +} +void Powder_calibration_quality::unsetNot_converged_reason() +{ + m_Not_converged_reasonIsSet = false; +} std::string Powder_calibration_quality::getCalibrant() const { return m_Calibrant; diff --git a/broker/gen/model/Powder_calibration_quality.h b/broker/gen/model/Powder_calibration_quality.h index daedd39eb..99d881f05 100644 --- a/broker/gen/model/Powder_calibration_quality.h +++ b/broker/gen/model/Powder_calibration_quality.h @@ -60,6 +60,20 @@ public: ///////////////////////////////////////////// /// Powder_calibration_quality members + /// + /// Whether the geometry beside this is a measurement of the data at all. False means at least one of its parameters was carried over from the input file rather than fitted - which looks exactly like a good fit in every other field here - and see not_converged_reason for which. rugnux --mode calibration writes no .poni file in that case, a PONI file having no field in which to say it, and exits non-zero. + /// + bool isConverged() const; + void setConverged(bool const value); + bool convergedIsSet() const; + void unsetConverged(); + /// + /// What made converged false, in words. Absent when the calibration converged. + /// + std::string getNotConvergedReason() const; + void setNotConvergedReason(std::string const& value); + bool notConvergedReasonIsSet() const; + void unsetNot_converged_reason(); /// /// The powder standard the rings were fitted to, or the unit cell given in its place /// @@ -155,6 +169,10 @@ public: friend void to_json(nlohmann::json& j, const Powder_calibration_quality& o); friend void from_json(const nlohmann::json& j, Powder_calibration_quality& o); protected: + bool m_Converged; + bool m_ConvergedIsSet; + std::string m_Not_converged_reason; + bool m_Not_converged_reasonIsSet; std::string m_Calibrant; bool m_CalibrantIsSet; std::string m_Method; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index a39a20ae5..1c844585f 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -1756,6 +1756,17 @@ components: wrong - the wrong standard named, a header too far out for the rings to be found - looks exactly like one that has not until these are read. properties: + converged: + type: boolean + description: | + Whether the geometry beside this is a measurement of the data at all. False means at least + one of its parameters was carried over from the input file rather than fitted - which looks + exactly like a good fit in every other field here - and see not_converged_reason for which. + rugnux --mode calibration writes no .poni file in that case, a PONI file having no field in + which to say it, and exits non-zero. + not_converged_reason: + type: string + description: What made converged false, in words. Absent when the calibration converged. calibrant: type: string description: The powder standard the rings were fitted to, or the unit cell given in its place diff --git a/broker/redoc-static.html b/broker/redoc-static.html index 9121a8e5f..263ee5e05 100644 --- a/broker/redoc-static.html +++ b/broker/redoc-static.html @@ -988,7 +988,7 @@ then image might be replaced in the buffer between calling /images and /image.cb