rugnux report: BEAM_CENTRE_OFF warning when the file's beam centre is wrong

The pre-scan's beam-centre check and the post-refinement both measure where
the direct beam lands, but the report only carried the centre the run used.
Section 1 now sets the file's direct beam (FILE_DIRECT_BEAM) beside the
background's (BACKGROUND_DIRECT_BEAM, with its sigma), the shift of the
refined centre from the file's (BEAM_CENTRE_SHIFT) and the tolerance the
geometry asks for (BEAM_CENTRE_TOLERANCE: BeamCenterNeed_pxl, the number the
log's check prints, here on the run's own cell). A shift above the tolerance
is a BEAM_CENTRE_OFF warning naming the file's, refined and background
centres, the difference in px and mm and the value to correct the beamline
to; a run that refined no centre is warned on the background alone where
its fit resolves the difference (3 sigma). The summary gains a Beam centre
line.

The refined centre, not the background, is the trigger wherever one exists:
on a battery set where XDS's refined direct beam and rugnux's agree to 1 px,
the background fit is 12 px away from both. Report-only; no result changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
2026-10-09 17:07:22 +02:00
co-authored by Claude Opus 5.5
parent 341c347a3f
commit f462047038
6 changed files with 144 additions and 2 deletions
+24 -2
View File
@@ -128,6 +128,28 @@ JFJOCH_DATASET_SETTINGS= {"beam_x_pxl": 2078.24, "beam_y_pxl": 2233.92, "detecto
grep '^JFJOCH_DATASET_SETTINGS=' out_report.txt | cut -d' ' -f2- > geometry.json
```
**The beam centre in the file.** Section 1 sets the file's centre beside the two the run measured:
**`FILE_DIRECT_BEAM=`** is where the input file puts the direct beam (its beam centre, under its own
detector tilt), **`BACKGROUND_DIRECT_BEAM=`** where the scattered background puts it, with the fit's
sigma (the pre-scan's beam-centre check), and **`BEAM_CENTRE_SHIFT=`** how far the centre the result was
integrated at (`DIRECT_BEAM=`, the refined one on a rotation run) lies from the file's, in pixels.
**`BEAM_CENTRE_TOLERANCE=`** is how far this crystal on this geometry lets the centre be wrong - the
displacement at which the accumulated reflections of the longest cell axis smear to 70% of their
height, the number the log's `Beam centre check` line states, here on the run's own cell. Where the
shift is larger, a **`BEAM_CENTRE_OFF`** warning gives the file's, the refined and the background
centre, the difference in pixels and millimetres, and the value to correct the beamline (or the
header) to: the run itself integrated at the refined centre, but every other program given the same
file starts from the wrong one. A run that refined no centre (stills) is warned on the background
alone, where its fit resolves the difference (three sigma) and the difference exceeds the tolerance,
worded as a check. The summary's `Beam centre` line states the outcome either way.
```
FILE_DIRECT_BEAM= 1564.31 1702.91
BACKGROUND_DIRECT_BEAM= 1571.30 1699.85 0.62
BEAM_CENTRE_SHIFT= 7.15
BEAM_CENTRE_TOLERANCE= 3.49
```
**Which pass.** A rotation run integrates twice — once at the geometry in the input file (the first
half of the sweep but at least 120°, `--prepass-fraction`), then the whole sweep again at the post-refined geometry — and can integrate a third time if a guard rejects the second pass.
There is **one** report, for the pass that became the canonical output, and `PASS=` /
@@ -156,13 +178,13 @@ and a run that produced a textbook data set read identically for their first thr
`GONIO_SCALE`, `SPINDLE_CAP`, `ANISOTROPY`, `TWINNING`, `PSEUDO_TRANSLATION`,
`LATTICE_TRANSLATION`, `MODEL_HAND`, `MODEL_NOT_VALIDATED`, `REFERENCE_MISMATCH`, `CANCELLED`, `RESOLUTION_FIT`,
`FLIGHT_PATH`, `GEOMETRY_NOT_CONVERGED`, `SCALING_NOT_CONVERGED`, `HARMONIC_CONTAMINATION`,
`SUPERCELL_POSSIBLE`, `MULTIPLE_LATTICES`, `ICE_RINGS`, `POWDER_RINGS`. `NONE` when nothing fired. A code appears if and only if its
`SUPERCELL_POSSIBLE`, `MULTIPLE_LATTICES`, `ICE_RINGS`, `POWDER_RINGS`, `BEAM_CENTRE_OFF`. `NONE` when nothing fired. A code appears if and only if its
warning fired, so the flags and the `WARNING:` lines are two renderings of one list. Thresholds are
deliberately low: a warning is a prompt to check something, not a verdict, and some of them fire on
data that turn out fine — the closed
type for machinery, the open sentence for a person.
- Below them, one line each, the facts a reader needs before reading further: space group, cell,
mosaicity, the powder and ice rings, multiple lattices, resolution, completeness, signal, anomalous
mosaicity, the beam centre, the powder and ice rings, multiple lattices, resolution, completeness, signal, anomalous
signal, anisotropy, pseudo-symmetry, twinning, supercell, radiation damage and the sweep. A line
states a finding that is not a condition too - weak powder rings, a possible supercell no stronger than
correct cells show, an arbitrary indexing choice (`Indexing choice`, a rotation sweep whose cell
+1
View File
@@ -51,6 +51,7 @@ namespace PathologyCode {
constexpr const char *SCALING_NOT_CONVERGED = "SCALING_NOT_CONVERGED"; // the per-frame scales hit their cap still moving
constexpr const char *POWDER_RINGS = "POWDER_RINGS"; // non-ice powder rings hold many spots
constexpr const char *ICE_RINGS = "ICE_RINGS"; // hexagonal-ice rings hold many spots
constexpr const char *BEAM_CENTRE_OFF = "BEAM_CENTRE_OFF"; // the file's beam centre is wrong for this geometry
}
// A closed type plus an open description. The type is what machinery switches on; the text is what a
+67
View File
@@ -42,6 +42,8 @@ namespace {
// UNDECLARED_LATTICE_TRANSLATION_PCT and INDEXING_AMBIGUITY_OPERATORS; INDEXING_RATE is a
// --developer key on rotation runs; POWDER_ICE_* / POWDER_NON_ICE_SPOT_FRACTION;
// the ICE_RINGS and POWDER_RINGS flags.
// 16: FILE_DIRECT_BEAM / BACKGROUND_DIRECT_BEAM / BEAM_CENTRE_SHIFT / BEAM_CENTRE_TOLERANCE in
// section 1 and the BEAM_CENTRE_OFF flag - the file's beam centre against the refined one.
// 8: every line that is not `KEY= value` and not blank starts with `#`, warnings included
// (`# WARNING:`); the header gains RUGNUX_DOWNLOAD and BUILD_CXX_FLAGS, and RUGNUX_GIT is
// the full build-time hash.
@@ -408,6 +410,8 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
const bool rotation = experiment.IsRotationIndexing();
const bool merged = result.has_merge_statistics;
doc.warnings = result.warnings;
// SUMMARY rows filled by the sections that measure them; empty leaves the row out.
std::string beam_centre_row;
// ------------------------------------------------------------------ header block
{
@@ -543,6 +547,67 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
"integrated at is the best one reached, not a settled one (see PASS_DECISION)");
}
// The beam centre against the file's. The file's is read off the experiment the run was
// given - the header, with the header's tilt - and compared with where the result was
// integrated (the refined geometry) and with where the scattered background put the beam. The
// run corrects a wrong centre for itself; every other program reading the same file does not,
// which is why this is worth a warning however well the run went. The tolerance is the one the
// pre-scan's check prints (BeamCenterNeed_pxl), taken on the run's own cell.
{
const auto file_direct = experiment.GetDiffractionGeometry().GetDirectBeam_pxl();
const double pixel_mm = experiment.GetPixelSize_mm();
const double need = result.beam_centre_need_pxl;
const double moved = std::hypot(result.used_direct_beam_x_pxl - file_direct.first,
result.used_direct_beam_y_pxl - file_direct.second);
const auto &bg = result.background_beam_centre;
const double bg_moved = bg ? std::hypot(bg->beam_x_pxl - file_direct.first,
bg->beam_y_pxl - file_direct.second) : NAN;
Add(s, KeyText("FILE_DIRECT_BEAM", fmt::format("{:.2f} {:.2f}", file_direct.first,
file_direct.second)));
if (bg)
Add(s, KeyText("BACKGROUND_DIRECT_BEAM", fmt::format("{:.2f} {:.2f} {:.2f}", bg->beam_x_pxl,
bg->beam_y_pxl, bg->sigma_pxl)));
Add(s, KeyReal("BEAM_CENTRE_SHIFT", moved, "{:.2f}"));
if (std::isfinite(need))
Add(s, KeyReal("BEAM_CENTRE_TOLERANCE", need, "{:.2f}"));
// The refined geometry is the evidence wherever the run refined one: the background fit can
// be pulled by an asymmetric background, and where both exist and disagree the spot
// positions are what the result rests on. Only a run that refined nothing (stills, no
// post-refinement) is warned on the background alone, and only where the fit resolves the
// difference at all (three of its own sigma, as the pre-scan's check reads it).
const bool bg_resolves = bg && bg_moved > 3.0 * bg->sigma_pxl;
const std::string bg_text = bg ? fmt::format("the scattered background at ({:.2f}, {:.2f}) +- {:.2f} px",
bg->beam_x_pxl, bg->beam_y_pxl, bg->sigma_pxl)
: std::string();
if (std::isfinite(need) && moved > need) {
beam_centre_row = fmt::format("the file's is OFF by {:.1f} px ({:.2f} mm) - correct the "
"beamline value", moved, moved * pixel_mm);
Warn(doc, PathologyCode::BEAM_CENTRE_OFF, fmt::format(
"the beam centre in the input file is {:.1f} px ({:.2f} mm) from where this run found "
"the direct beam, more than the {:.1f} px this geometry needs it to be right to: the "
"file puts the direct beam at ({:.2f}, {:.2f}), the refined geometry at ({:.2f}, "
"{:.2f}){}. This run used the refined centre, but any other program given this file "
"starts from the wrong one - correct the beam centre at the beamline (or in the "
"file's header) to ({:.2f}, {:.2f})",
moved, moved * pixel_mm, need, file_direct.first, file_direct.second,
result.used_direct_beam_x_pxl, result.used_direct_beam_y_pxl,
bg ? ", " + bg_text : std::string(),
result.used_direct_beam_x_pxl, result.used_direct_beam_y_pxl));
} else if (std::isfinite(need) && !result.post_refine && bg_resolves && bg_moved > need) {
beam_centre_row = fmt::format("the background puts it {:.1f} px ({:.2f} mm) from the file's - "
"check the beamline value", bg_moved, bg_moved * pixel_mm);
Warn(doc, PathologyCode::BEAM_CENTRE_OFF, fmt::format(
"the beam centre in the input file may be off: the file puts the direct beam at "
"({:.2f}, {:.2f}), {} - {:.1f} px ({:.2f} mm) away, more than the {:.1f} px this "
"geometry needs the centre to be right to. Nothing on this run refined the centre, so "
"this is the background's word alone: check the beam centre at the beamline and "
"correct it if this is confirmed",
file_direct.first, file_direct.second, bg_text, bg_moved, bg_moved * pixel_mm, need));
} else if (std::isfinite(need))
beam_centre_row = fmt::format("the file's is within the {:.1f} px this geometry needs ({:.1f} px "
"from the refined one)", need, moved);
}
Add(s, Blank());
// Where the geometry came from. This is the whole provenance story a user needs, because a
// user acts on exactly one geometry: the one the result was produced with.
@@ -2377,6 +2442,8 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
row("Indexing choice", fmt::format("alternative indexing {} - one sweep is indexed consistently, "
"so it matters only against other data: index against those "
"with -z or --model", result.indexing_ambiguity_operators));
if (!beam_centre_row.empty())
checked_row("Beam centre", {PathologyCode::BEAM_CENTRE_OFF}, beam_centre_row);
if (!result.powder.rings_q_recipA.empty() || result.consensus_cell) {
const auto &pw = result.powder;
checked_row("Powder", {PathologyCode::POWDER_RINGS}, pw.rings_q_recipA.empty() ? std::string("no rings detected")
+9
View File
@@ -434,8 +434,17 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) {
// on the rotation two-pass that is under a third of what the run actually took, the pre-scan and the
// first pass being invisible in it. Time the whole thing here, where every pass is inside.
const auto run_start = std::chrono::steady_clock::now();
DiffractionExperiment header_experiment = experiment_;
ProcessResult result = RunAllPasses(observer);
result.total_time_s = std::chrono::duration<double>(std::chrono::steady_clock::now() - run_start).count();
// For the report's beam-centre warning: what the background measured, and what this crystal asks
// of the centre - at the file's geometry, as the pre-scan's check asks it, but on the cell the run
// determined rather than the long axis that check has to assume.
JoinBeamCenterCapture();
result.background_beam_centre = measured_beam_center_;
if (result.consensus_cell)
header_experiment.SetUnitCell(*result.consensus_cell);
result.beam_centre_need_pxl = BeamCenterNeed_pxl(header_experiment);
return result;
}
+7
View File
@@ -530,6 +530,13 @@ struct ProcessResult {
// did not post-refine (stills, single pass, or the refinement did not converge).
std::optional<PostRefineResult> post_refine;
// The beam centre the scattered background puts the direct beam at (the pre-scan's check), and
// how far the centre may be wrong on this crystal (BeamCenterNeed_pxl, with the run's cell where
// it has one). Report only: the file's centre is read off the experiment the report is given, the
// refined one off used_direct_beam_*. Empty / NaN where the check did not run.
std::optional<BeamCenterEstimate> background_beam_centre;
float beam_centre_need_pxl = NAN;
// Which pass this result is (1-based) and how many ran, plus one line saying which pass became the
// canonical output and on what evidence. pass_count == 1 outside the rotation two-pass.
int pass_number = 1;
+36
View File
@@ -1200,3 +1200,39 @@ TEST_CASE("ResultReport_Checks", "[Diagnostics]") {
CHECK(std::ranges::any_of(c.codes, [&](const std::string &code) {
return std::ranges::find(doc.pathology_flags, code) != doc.pathology_flags.end(); }));
}
TEST_CASE("ResultReport_BeamCentreOff", "[Diagnostics]") {
DiffractionExperiment x(DetJF(1));
x.BeamX_pxl(500.0f).BeamY_pxl(400.0f);
const auto file = x.GetDiffractionGeometry().GetDirectBeam_pxl();
ProcessResult result;
result.post_refine = PostRefineResult{};
result.beam_centre_need_pxl = 2.0f;
result.background_beam_centre = BeamCenterEstimate{.beam_x_pxl = static_cast<float>(file.first) + 5.0f,
.beam_y_pxl = static_cast<float>(file.second),
.sigma_pxl = 0.5f};
// Refined within the tolerance: the background's word is not enough where the centre was refined.
result.used_direct_beam_x_pxl = file.first + 1.0;
result.used_direct_beam_y_pxl = file.second;
auto text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("BEAM_CENTRE_OFF") == std::string::npos);
CHECK(text.find("\nBEAM_CENTRE_SHIFT= 1.00\n") != std::string::npos);
CHECK(text.find("\nBEAM_CENTRE_TOLERANCE= 2.00\n") != std::string::npos);
// Refined 4 px off the file's centre: warned, with both centres, px and mm, and the advice.
result.used_direct_beam_x_pxl = file.first;
result.used_direct_beam_y_pxl = file.second + 4.0;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nPATHOLOGY_FLAGS= BEAM_CENTRE_OFF\n") != std::string::npos);
CHECK(text.find("4.0 px (0.30 mm)") != std::string::npos);
CHECK(text.find("correct the beam centre at the beamline") != std::string::npos);
// Nothing refined (stills): the background alone warns, worded as a check.
result.post_refine.reset();
result.used_direct_beam_x_pxl = file.first;
result.used_direct_beam_y_pxl = file.second;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nPATHOLOGY_FLAGS= BEAM_CENTRE_OFF\n") != std::string::npos);
CHECK(text.find("the background's word alone") != std::string::npos);
}