rugnux: --report-resolution, a second statistics table at a reference range

Comparing a run with another program's table has meant running rugnux AT that
program's resolution range (--scaling-high-resolution), which is a different
run: the range moves the cut, the space-group decision and everything after
them, so the comparison buys itself a different answer. --report-resolution
<dmin>[,<dmax>] instead leaves the run alone and adds a second table to
section 3 of the report - the REFRES_* keys and a shell table - binned from the
same merged reflections over the range given, with the completeness
denominator enumerated over that range and the shells in equal steps of 1/d^2
so they read row for row against a CORRECT.LP at the same range. Report-only:
the merged files and every decision are byte-identical with and without it.

The table holds only what the run kept. Where the reference range is finer
than the run's own limit, the shells past it are printed as not merged (with
their possible count) rather than as zeros, REFRES_SHELLS_PAST_LIMIT counts
them so a consumer can tell "not merged" from a measured zero, REFRES_
COMPLETENESS counts their reflections as missing, and the other overall numbers
are over the shells the run reached; nothing is read from the observations the
run judged to carry no signal. REFRES_ISA is the error model refitted on the
reflections of the table alone, in XDS's convention (rotation only; the stills
model is fitted over the whole range already).

On the rotation path the statistics block of MergeAndStats becomes a lambda
over a shell grid, called once for the run's own grid and once for the
reference one; the reference call floors every observation-level count at the
cut by group d, the rule the erase applied. The stills MergeStats takes a
declared range, whose bounds are the grid's whether or not any reflection
reaches them. Both --mode mx and --mode scale report it, the viewer's command
line echoes it, and the docs describe the keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 10:58:42 +02:00
co-authored by Claude Fable 5.1
parent 26a80caac4
commit 8a0da57941
15 changed files with 661 additions and 267 deletions
+41
View File
@@ -259,6 +259,47 @@ TEST_CASE("MergeStats_NoReferenceCellLeavesCompletenessUnmeasured") {
}
}
// The reference-range table (--report-resolution) is binned over the range it is GIVEN, whether or
// not the data reach it: its last shell ends at the declared d_min, and past the run's own limit
// nothing is read from anywhere - the reflections the run did not keep count as missing.
TEST_CASE("MergeStats_ReferenceRangeIsBinnedOverTheDeclaredRange") {
const auto &sg = TestSpaceGroup();
const auto merged = MeasuredBetween(sg, TEST_CELL, 2.0, 20.0, true); // what the run kept
DiffractionExperiment x;
x.SetSpaceGroup(sg);
MergeOnTheFly merge(x);
merge.ReferenceCell(TEST_CELL);
const auto own = merge.MergeStats(merged, {});
// A range coarser than the run's own: a complete subset, on a grid ending at the declared bound.
const auto within = merge.MergeStats(merged, {}, {}, std::nullopt, ReportResolutionRange{3.0, 50.0});
CHECK(within.shells.back().d_min == Catch::Approx(3.0));
CHECK(within.shells.front().d_max == Catch::Approx(50.0));
int coarser_than_3 = 0;
for (const auto &m: merged)
if (m.d > 3.0f) ++coarser_than_3;
CHECK(within.overall.unique_reflections == coarser_than_3);
CHECK(within.overall.unique_reflections < own.overall.unique_reflections);
CHECK(Completeness(within.overall) > 99.0);
// A range finer than the run kept: the grid still ends at 1.5 A, the outer shell is empty and its
// possible reflections are in the denominator, and the measured range says where the data stop.
const auto beyond = merge.MergeStats(merged, {}, {}, std::nullopt, ReportResolutionRange{1.5, 50.0});
CHECK(beyond.shells.back().d_min == Catch::Approx(1.5));
CHECK(beyond.overall.unique_reflections == own.overall.unique_reflections);
CHECK(beyond.overall.possible_unique_reflections > own.overall.possible_unique_reflections);
CHECK(Completeness(beyond.overall) < Completeness(own.overall));
CHECK(beyond.shells.back().unique_reflections == 0);
CHECK(beyond.shells.back().possible_unique_reflections > 0);
CHECK(beyond.overall.d_min == Catch::Approx(own.overall.d_min));
// A range the data never reach at all is still a grid, not an error.
const auto empty = merge.MergeStats(merged, {}, {}, std::nullopt, ReportResolutionRange{1.0, 1.5});
CHECK(empty.overall.unique_reflections == 0);
CHECK(empty.overall.possible_unique_reflections > 0);
CHECK(empty.overall.d_min == 0.0f);
}
// ---------------------------------------------------------------- the automatic resolution cutoff
namespace {
// Half-set pairs spread uniformly in s = 1/d^2 over [s_from, s_to), either correlated with each