rugnux: an R that compares between runs, beside the one that makes the maps

R_WORK and R_FREE score the data against a model scaled by k_overall, a
symmetry-constrained anisotropic B and a flat bulk solvent. That scale is
deliberate - a free per-shell rescale would reshape each map's radial
amplitude profile and a batch of maps would no longer be comparable - but it
can only bend one way with resolution, so whatever a dataset's own radial
profile does that k*exp(-B s^2) cannot follow is reported as R. Two reductions
of one crystal whose amplitudes differ radially therefore differ in R_FREE for
a reason that has nothing to do with either fitting the model: measured on one
such pair of runs, R_FREE moved a median +0.042 over eleven sets where
phenix.model_vs_data, which scales per shell, moved +0.002.

Report a second reading of the same fit: R_MODEL_SHELL_SCALED, one free scale
per resolution shell of the merge table, with R_MODEL as the same sum without
it and MODEL_RADIAL_MISFIT (RMS of ln k_shell) as the size of the rescale, so
the reason a comparison is or is not readable is on the page. Over all the
reflections, not the free 5%: nothing here is refined, so work and free
estimate the same quantity and the split only costs precision.

Nothing is written from it. On two sets, every output file - both maps, the
anomalous map, the map coefficients, the MTZ, the HKL and the CIF - is byte
for byte what the same commit produced without this change, and R_WORK and
R_FREE are unchanged.

On the pair above, the new R moves a median +0.0005 and holds every set
inside 0.0065 except the one that is a real regression, which it puts first
at +0.0174 where the old metric ranked it seventh. The battery reports it
beside R_free, and scores neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
This commit is contained in:
2026-09-21 14:09:39 +02:00
co-authored by Claude Opus 5
parent 29b5d7cce1
commit 8364cfbc40
10 changed files with 235 additions and 4 deletions
+30 -1
View File
@@ -421,8 +421,11 @@ TEST_CASE("ResultReport_ModelValidationSection", "[Diagnostics]") {
CHECK(dev_text.find("\nMODEL_FIT_NULL_REPLICATES= 5\n") != std::string::npos);
}
CHECK(text.find("\nMODEL_FIT_SIGMA= +4.12\n") != std::string::npos);
// A run that measured no CC(model, data) writes no key for it.
// A run that measured no CC(model, data) writes no key for it, and the shell-scaled R shares
// its shells, so it is absent too.
CHECK(text.find("CC_MODEL_OVERALL=") == std::string::npos);
CHECK(text.find("R_MODEL_SHELL_SCALED=") == std::string::npos);
CHECK(text.find("MODEL_RADIAL_MISFIT=") == std::string::npos);
CHECK(text.find("CC_MODEL_CONFIRMED_TO_D_MIN=") == std::string::npos);
CHECK(text.find("\nMODEL_DECISIONS_TAKEN= ENANTIOMORPH\n") != std::string::npos);
// The hand is ASSUMED from the model, never determined: merged intensities cannot see it.
@@ -473,6 +476,32 @@ TEST_CASE("ResultReport_ModelValidationSection", "[Diagnostics]") {
CHECK(cc_text.find(" 1.80 0.0412 2110 +1.9") != std::string::npos);
CHECK(cc_text.find("The table reads in ONE direction only") != std::string::npos);
// The comparable R rides on the same shells. R_WORK and R_FREE are what they always were - the
// maps' own scale - and the new keys sit beside them rather than in place of them.
ModelValidationResult with_r = with_cc;
with_r.r_model = 0.2011;
with_r.r_model_shell_scaled = 0.1734;
with_r.n_model = 17610;
with_r.radial_misfit = 0.312;
with_r.shell_scale_bins = 3;
result.model_validation = with_r;
const auto r_text = RenderResultReport("p", "in.h5", x, result);
CHECK(r_text.find("\nR_WORK= 0.1823\n") != std::string::npos);
CHECK(r_text.find("\nR_FREE= 0.2145\n") != std::string::npos);
CHECK(r_text.find("\nR_MODEL= 0.2011\n") != std::string::npos);
CHECK(r_text.find("\nR_MODEL_SHELL_SCALED= 0.1734\n") != std::string::npos);
CHECK(r_text.find("\nR_MODEL_REFLECTIONS= 17610\n") != std::string::npos);
CHECK(r_text.find("\nMODEL_RADIAL_MISFIT= 0.312\n") != std::string::npos);
CHECK(r_text.find("the number to compare between runs") != std::string::npos);
// The shell count is a working detail of the rescale, not part of the reading.
CHECK(r_text.find("MODEL_RADIAL_SHELLS=") == std::string::npos);
{
RunProvenance dev;
dev.developer = true;
CHECK(RenderResultReport("p", "in.h5", x, result, dev)
.find("\nMODEL_RADIAL_SHELLS= 3\n") != std::string::npos);
}
// Nothing significant anywhere: the key still has to be written, saying so.
ModelValidationResult no_signal = with_cc;
for (auto &sh : no_signal.cc_model_shells)