From dcfcff5bc44e2695dbbffbf4d9c9b34ef173736f Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Mon, 31 Aug 2026 19:35:27 +0200 Subject: [PATCH] report: the measured tilt, and one geometry instead of two Adds REFINED_DETECTOR_TILT - rot1/rot2 in degrees as rotation indexing measured them - so the value can be checked against a powder calibration, which measures the tilt from ring shape and is the better instrument for it. rot3 is not carried: a rotation about the beam is an exact null of a single-axis rotation experiment, so the fit cannot move it and printing it would suggest otherwise. Also repairs two defects in the commit that added DETECTOR_TILT, found in review. The geometry was taken from the caller's copy of the experiment, which is the state BEFORE the run, while BEAM_CENTRE and DETECTOR_DISTANCE come from result.used_*, which is the state after - so on a rotation two-pass the report mixed a post-refined centre with the file's rotations and DIRECT_BEAM was off by the whole post-refinement shift. JFJOCH_DATASET_SETTINGS carried the same mixture, which is a geometry no pass ever ran at. The tilt and the direct beam are now taken from experiment_ alongside the other three, so the block describes one geometry. And the prose claimed the tilt "is not refined here". It is: rotation indexing refines rot1/rot2 on every rotation run. What is true is that the result is never written back onto the geometry, which is why it needs a key of its own. M_PI -> PI (common/JFJochMath.h). rugnux is in the forced-Windows viewer-only set and M_PI is not portable to MSVC; the file did not include either. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N --- docs/CHANGELOG.md | 1 + rugnux/ResultReport.cpp | 33 +++++++++++++++++++++++---------- rugnux/Rugnux.cpp | 15 +++++++++++++++ rugnux/Rugnux.h | 11 +++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 538022332..53fc10e73 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,7 @@ * `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have. * Twinning is no longer reported when the L-test contradicts it. * The `rugnux` report gives the detector tilt and the direct beam beside the beam centre, and carries the tilt in the `dataset_settings` block. +* The `rugnux` report gives the detector tilt rotation indexing measured, so it can be checked against a powder calibration. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. ### 1.0.0-rc.165 diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index 6bee76691..0350ba035 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -7,6 +7,7 @@ #include +#include "../common/JFJochMath.h" // PI - M_PI is not portable to MSVC #include "../common/GitInfo.h" #include "../common/time_utc.h" #include "../image_analysis/scale_merge/AnisotropyAnalysis.h" @@ -96,20 +97,31 @@ std::string RenderResultReport(const std::string &output_prefix, // beam lands, and the two were previously indistinguishable in this report because only one of // them was printed. Degrees here; the JSON below carries radians, as the API spells it. { - const auto &g = experiment.GetDiffractionGeometry(); - constexpr double DEG = 180.0 / M_PI; - const auto direct = g.GetDirectBeam_pxl(); - Key(os, "DETECTOR_TILT", fmt::format("{:.4f} {:.4f} {:.4f}", g.GetPoniRot1_rad() * DEG, - g.GetPoniRot2_rad() * DEG, g.GetPoniRot3_rad() * DEG)); - Key(os, "DIRECT_BEAM", fmt::format("{:.2f} {:.2f}", direct.first, direct.second)); + Key(os, "DETECTOR_TILT", fmt::format("{:.4f} {:.4f} {:.4f}", result.used_detector_tilt_deg[0], + result.used_detector_tilt_deg[1], + result.used_detector_tilt_deg[2])); + Key(os, "DIRECT_BEAM", fmt::format("{:.2f} {:.2f}", result.used_direct_beam_x_pxl, + result.used_direct_beam_y_pxl)); + if (result.refined_detector_tilt_deg) + Key(os, "REFINED_DETECTOR_TILT", + fmt::format("{:.4f} {:.4f}", (*result.refined_detector_tilt_deg)[0], + (*result.refined_detector_tilt_deg)[1])); } os << "\n" << " The distance and beam centre above are the ones this result was integrated at, which on\n" << " a rotation run is the post-refined geometry rather than the values in the input file.\n" - << " DETECTOR_TILT is rot1/rot2/rot3 in degrees, as the run used them - it is not refined here.\n" + << " DETECTOR_TILT is rot1/rot2/rot3 in degrees, as the run INTEGRATED at them. Rotation\n" + << " indexing does refine rot1/rot2, but the result is never written back onto the geometry,\n" + << " so what it measured is reported separately as REFINED_DETECTOR_TILT.\n" << " BEAM_CENTRE is the PONI and DIRECT_BEAM is where the beam actually lands; they differ by\n" << " distance*tan(tilt)/pixel and are identical only when the tilt is zero. Quote whichever the\n" << " program you are feeding expects, and check which one it means.\n"; + if (result.refined_detector_tilt_deg) + os << " REFINED_DETECTOR_TILT is rot1/rot2 in degrees as rotation indexing MEASURED them, and is\n" + << " NOT what this run integrated at - nothing writes a refined tilt back onto the geometry.\n" + << " It is here to be checked against a powder calibration, which measures the tilt from ring\n" + << " SHAPE and is the better instrument for it. rot3 is omitted because a rotation about the\n" + << " beam is an exact null of this experiment and the fit cannot move it.\n"; // The same geometry once more, as the object jfjoch_broker takes it in: the four required // properties of dataset_settings in broker/jfjoch_api.yaml, spelled the way the API spells them. @@ -122,12 +134,13 @@ std::string RenderResultReport(const std::string &output_prefix, // poni_rot1/2/3_rad, and a block that omits them describes a FLAT detector - a different // geometry from the one this run used, silently, on every tilted setup. Omitted when zero // because the API's own default is 0.0, so the shorter block means the same thing. - const auto &g = experiment.GetDiffractionGeometry(); + constexpr double RAD = PI / 180.0; + const auto &t = result.used_detector_tilt_deg; std::string rot; - if (g.GetPoniRot1_rad() != 0.0f || g.GetPoniRot2_rad() != 0.0f || g.GetPoniRot3_rad() != 0.0f) + if (t[0] != 0.0 || t[1] != 0.0 || t[2] != 0.0) rot = fmt::format(R"(, "poni_rot1_rad": {:.6f}, "poni_rot2_rad": {:.6f}, )" R"("poni_rot3_rad": {:.6f})", - g.GetPoniRot1_rad(), g.GetPoniRot2_rad(), g.GetPoniRot3_rad()); + t[0] * RAD, t[1] * RAD, t[2] * RAD); Key(os, "JFJOCH_DATASET_SETTINGS", fmt::format(R"({{"beam_x_pxl": {:.2f}, "beam_y_pxl": {:.2f}, "detector_distance_mm": {:.3f}, )" R"("incident_energy_keV": {:.4f}{}}})", diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index c6a12b579..b51a8b1b2 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -3125,6 +3125,12 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b end_msg.refined_poni_rot1 = rot->geom.GetPoniRot1_rad(); end_msg.refined_poni_rot2 = rot->geom.GetPoniRot2_rad(); end_msg.refined_poni_rot3 = rot->geom.GetPoniRot3_rad(); + // rot3 is not carried: a rotation about the beam is an exact null of this experiment + // (measured, smallest eigenvalue -5e-16), so the fit never moves it and reporting it would + // suggest it had been determined. + result.refined_detector_tilt_deg = std::array{ + rot->geom.GetPoniRot1_rad() * 180.0 / PI, + rot->geom.GetPoniRot2_rad() * 180.0 / PI}; if (rot->axis) end_msg.refined_rotation_axis = rot->axis->GetAxis(); end_msg.rotation_lattice_type = LatticeMessage{ @@ -4555,5 +4561,14 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b result.used_beam_x_pxl = experiment_.GetBeamX_pxl(); result.used_beam_y_pxl = experiment_.GetBeamY_pxl(); result.used_distance_mm = experiment_.GetDetectorDistance_mm(); + { + const auto &g = experiment_.GetDiffractionGeometry(); + constexpr double DEG = 180.0 / PI; + result.used_detector_tilt_deg = {g.GetPoniRot1_rad() * DEG, g.GetPoniRot2_rad() * DEG, + g.GetPoniRot3_rad() * DEG}; + const auto direct = g.GetDirectBeam_pxl(); + result.used_direct_beam_x_pxl = direct.first; + result.used_direct_beam_y_pxl = direct.second; + } return result; } diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 5c695ecc7..3ed88f6c7 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -206,6 +206,17 @@ struct ProcessResult { // Truncating cannot be undone and refinement can downweight, so the data are cut generously and this // is the number to quote. Empty when no automatic cut ran (a manual limit, or the method turned off). std::optional resolution_fit_A; + // Detector tilt (PONI rot1/rot2, degrees) as rotation indexing MEASURED it, which is not what the + // run integrated at - nothing writes a refined tilt back onto the experiment. Reported so the value + // can be checked against a powder calibration or handed to another program; empty on stills and + // when no rotation lattice was finalized. + std::optional> refined_detector_tilt_deg; + // The tilt this pass integrated at, and where the beam actually lands under it - taken from the + // same experiment_ as used_beam_*/used_distance_mm, so the four describe ONE geometry. Reading any + // of them off the caller's pre-run copy instead would mix a post-refined centre with the file's + // rotations, which is a geometry no pass ever ran at. + std::array used_detector_tilt_deg{}; + double used_direct_beam_x_pxl = 0.0, used_direct_beam_y_pxl = 0.0; std::optional consensus_cell; bool rotation_lattice_found = false; MeanProcessingTime mean_processing_time{};