rugnux: the run's own N_obs and R_meas stop at the same cut as N_uniq

The own statistics table's grid bottoms 0.1% below the finest kept reflection,
and the observation-level counts (N_obs, R_meas, the Bijvoet split behind SigAno
and CCanom) re-walk the fulls, which still hold every ingested group. Groups in
that band just below the auto cut were erased from the merge, so they were not
in N_uniq, but their observations still counted in N_obs and R_meas of the
finest shell. The own table now floors those counts at the cut by group d, the
rule the erase applies - the same floor the reference-range table already used
(now passed as a double, so the comparison is exactly the erase's).

Report-only: on an auto-cut cubic in-house set the written MTZ, HKL, P1 MTZ, unmerged
MTZ and image table are byte-identical; TOTAL_OBSERVATIONS 378754 -> 377933,
MULTIPLICITY 40.31 -> 40.22 (now equal to the reference-range table's), finest
shell N_obs 60873 -> 60052, R_meas 1136.9% -> 1129.2%, CCanom -0.6% -> -0.3%;
overall R_MEAS, CC1/2, SigAno unchanged at printed precision; the mmCIF's
pdbx_number_measured_all / pdbx_redundancy / last shell row follow. A
detector-limited lysozyme run (no auto cut) is byte-identical throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-20 18:45:03 +02:00
co-authored by Claude Opus 5
parent 6a26591786
commit 0ef6b3f00b
@@ -4088,13 +4088,15 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
// grid_d_max, and the completeness denominator is enumerated over exactly that. The fulls
// hold every group that was ingested, cut or not, so a grid reaching past the cut needs
// d_floor - the cut, as the erase above applied it, by group d - to keep the observations
// of the groups the run did not keep out of every count; the run's own grid ends at the
// finest kept reflection and needs none. ----
// of the groups the run did not keep out of every count. The run's own grid needs it too:
// it bottoms 0.1% below the finest kept reflection, and the groups in that band were erased
// from the merge, so without the floor they would count in N_obs and R_meas but not in
// N_uniq. ----
const int n_shells = report_shell_count;
struct AnomExport { float Ip = NAN, sIp = NAN, Im = NAN, sIm = NAN; };
std::unordered_map<uint64_t, AnomExport> anom_export; // filled by the run's own table only
auto statistics_over = [&](const std::vector<MergedReflection> &merged, float grid_d_min,
float grid_d_max, float d_max_label, float d_floor,
float grid_d_max, float d_max_label, double d_floor,
bool own_table) -> MergeStatistics {
ResolutionShells shells(grid_d_min, grid_d_max, n_shells);
const auto shell_mean_1_d2 = shells.GetShellMeanOneOverResSq();
@@ -4369,7 +4371,8 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
// The innermost shell is labelled by the bound it reaches, except when that bound is infinite
// and the coarsest reflection measured is the only finite thing to say.
const float d_max_label = std::isfinite(d_max_declared) ? d_max_declared : sd_max;
result.statistics = statistics_over(result.merged, d_min_pad, d_max_declared, d_max_label, 0.0f, true);
result.statistics = statistics_over(result.merged, d_min_pad, d_max_declared, d_max_label,
effective_d_min.value_or(0.0), true);
MergeStatistics &out = result.statistics;
out.overall.d_min = sd_min; out.overall.d_max = sd_max;
@@ -4398,7 +4401,7 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
in_min = std::min(in_min, m.d); in_max = std::max(in_max, m.d);
}
MergeStatistics ref = statistics_over(result.merged, d_lo, d_hi, std::isfinite(d_hi) ? d_hi : in_max,
static_cast<float>(effective_d_min.value_or(0.0)), false);
effective_d_min.value_or(0.0), false);
ref.overall.d_min = in_min < in_max ? in_min : 0.0f;
ref.overall.d_max = in_max;
result.reference_statistics = std::move(ref);