report: the measured tilt, and one geometry instead of two
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 9m28s
Build Packages / build:windows:nocuda (push) Successful in 17m21s
Build Packages / build:windows:cuda (push) Successful in 19m41s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 19m57s
Build Packages / build:viewer-tgz:cpu (push) Successful in 20m58s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 22m43s
Build Packages / build:viewer-tgz:cuda (push) Successful in 23m28s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 27m47s
Build Packages / build:rugnux:windows (push) Successful in 11m26s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m24s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 22m12s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 26m56s
Build Packages / build:rpm (rocky9) (push) Successful in 23m46s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 23m48s
Build Packages / build:rpm (rocky8) (push) Successful in 29m28s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m32s
Build Packages / XDS test (durin plugin) (push) Successful in 11m33s
Build Packages / DIALS test (push) Successful in 26m43s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 27m55s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m18s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m56s
Build Packages / Unit tests (push) Successful in 2h43m15s

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 <cmath> either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
2026-08-31 19:35:27 +02:00
co-authored by Claude Opus 5
parent 7c00731992
commit dcfcff5bc4
4 changed files with 50 additions and 10 deletions
+1
View File
@@ -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
+23 -10
View File
@@ -7,6 +7,7 @@
#include <spdlog/fmt/fmt.h>
#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}{}}})",
+15
View File
@@ -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<double, 2>{
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;
}
+11
View File
@@ -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<double> 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<std::array<double, 2>> 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<double, 3> used_detector_tilt_deg{};
double used_direct_beam_x_pxl = 0.0, used_direct_beam_y_pxl = 0.0;
std::optional<UnitCell> consensus_cell;
bool rotation_lattice_found = false;
MeanProcessingTime mean_processing_time{};