diff --git a/docs/RUGNUX_REPORT.md b/docs/RUGNUX_REPORT.md index 234195189..cd26dee8f 100644 --- a/docs/RUGNUX_REPORT.md +++ b/docs/RUGNUX_REPORT.md @@ -66,7 +66,8 @@ each half is averaged with 1/sigma^2 weights, and CC1/2 is the plain Pearson cor half-set means, every reflection counted alike. The same holds for `REFRES_CC_HALF=`, the CC1/2 and CC\* columns of every shell table and the mmCIF. The automatic resolution cut is fitted to a different, internal form of CC1/2, in which a reflection measured only on frames the crystal barely diffracted on -counts less (see `FITTED_RESOLUTION`); on a sweep without a weak stretch the two agree. +counts less (`FITTED_RESOLUTION_INTERNAL`); `FITTED_RESOLUTION` is read off the standard one. On a sweep +without a weak stretch the two agree. **`R_MERGE=`** and **`R_PIM=`** are the other two merging R-factors over the same observations as `R_MEAS=`: R_merge without the multiplicity correction, and the precision-indicating R_pim (Weiss, @@ -246,8 +247,9 @@ The keys most scripts read, by section; the rest of this page explains the ones | `MOSAICITY_DEG` | Rotation only ([below](#mosaicity)) | | `MERGE` | Section 3: `PERFORMED` or `NOT_PERFORMED` (`--no-merge`) | | `INCLUDE_RESOLUTION_RANGE` | `dmax dmin` of the written reflections (Å), as XDS's keyword | -| `FITTED_RESOLUTION` | Where the CC1/2 fall-off crosses the `--resolution-cc-target`; absent where the curve is not a fall-off (a `RESOLUTION_FIT` warning says why). It is read off the automatic cut's own fit, which weights each reflection by the scale of the frames it was measured on so that a weak stretch of the sweep does not drag it in; on such a sweep the unweighted `CC_HALF` of the shell table can cross the target earlier | -| `FITTED_RESOLUTION_PLAIN`, `RESOLUTION_CUT_PLAIN` | Diagnostic, rotation only: the same automatic fit made on the unweighted CC1/2 of the shell table - its crossing, and the limit the cut would have written from it. Nothing is cut at them; they show what the weighting of the fit is worth on this run | +| `FITTED_RESOLUTION` | Where the fall-off of the standard CC1/2 (the `CC_HALF` of the shell table) crosses the `--resolution-cc-target` - the number to quote; absent where the curve is not a fall-off (a `RESOLUTION_FIT` warning says why) | +| `FITTED_RESOLUTION_INTERNAL` | Rotation only, internal: the crossing of the automatic cut's own fit, which weights each reflection by the scale of the frames it was measured on so that a weak stretch of the sweep does not drag it in. The written range (`INCLUDE_RESOLUTION_RANGE`) is cut from this one; on a sweep without a weak stretch it equals `FITTED_RESOLUTION` | +| `RESOLUTION_CUT_PLAIN` | Diagnostic, rotation only: the limit the cut would have written from the fit on the standard CC1/2. Nothing is cut at it | | `UNIQUE_REFLECTIONS`, `TOTAL_OBSERVATIONS`, `COMPLETENESS`, `MULTIPLICITY` | Over the written range; completeness in percent | | `I_OVER_SIGMA`, `R_MEAS`, `CC_HALF`, `WILSON_B` | The overall merging statistics; the shell table beside them is in the comment lines. `R_MEAS` (and `R_MERGE`, `R_PIM`) rotation only, `R_SPLIT` stills only | | `SIGANO`, `CC_ANOM`, `FRIEDELS_LAW` | The anomalous signal, and whether Friedel pairs were merged (`FALSE` with `-A`) | diff --git a/image_analysis/scale_merge/ResolutionCutoff.h b/image_analysis/scale_merge/ResolutionCutoff.h index 7c73aa658..c788c1e3c 100644 --- a/image_analysis/scale_merge/ResolutionCutoff.h +++ b/image_analysis/scale_merge/ResolutionCutoff.h @@ -50,7 +50,7 @@ std::vector CCHalfFrameFactors(const std::vector &frame_scale, const std::vector &n_obs); // weighted: each reflection's pair enters with its cc_weight, the internal form the cut is decided on; -// false fits the plain (reported) CC1/2 instead - a diagnostic only (FITTED_RESOLUTION_PLAIN). +// false fits the plain (reported) CC1/2 instead - the quoted FITTED_RESOLUTION; not applied as the cut. ResolutionCutoffResult ComputeCCHalfLogisticCutoff(const std::vector &merged, double cc_target, Logger &logger, bool weighted = true); diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index 76bf2b7e8..315567c94 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -408,16 +408,19 @@ namespace { Add(s, KeyReal((prefix + "CC_HALF").c_str(), po.cc_half, "{:.4f}")); } - // The three helpers below judge the automatic cut's fit against the shells, so they read the curve the - // fit was made on - the internal weighted CC1/2 (cc_half_internal) - not the plain one the shell table - // prints. On a sweep without a weak stretch the two are the same number. + // The three helpers below judge the quoted fit against the shells, so they read the curve the fit was + // made on: `plain` the standard CC1/2 the shell table prints (cc_half), else the internal weighted one + // (cc_half_internal). On a sweep without a weak stretch the two are the same number. + double ShellCC(const MergeStatisticsShell &sh, bool plain) { + return plain ? sh.cc_half : sh.cc_half_internal; + } // The finest shell whose CC1/2 still reaches the cut's own target: where the table says the signal // is, for the completeness quoted beside the written range. - double FinestSupportedShell(const MergeStatistics &ms, double target) { + double FinestSupportedShell(const MergeStatistics &ms, double target, bool plain) { double finest = 0.0; for (const auto &sh : ms.shells) - if (std::isfinite(sh.cc_half_internal) && sh.cc_half_internal >= target && (finest == 0.0 || sh.d_min < finest)) + if (std::isfinite(ShellCC(sh, plain)) && ShellCC(sh, plain) >= target && (finest == 0.0 || sh.d_min < finest)) finest = sh.d_min; return finest; // 0.0 = no shell reaches the target at all } @@ -429,12 +432,12 @@ namespace { // shell ABOVE the target instead withheld the number on 68 of 100 battery datasets whose fit sat // 0.01-0.02 A inside the first sub-target shell. 0.0 when no shell reaches the target; the finest // shell when none falls below it. - double CrossingShellLimit(const MergeStatistics &ms, double target) { + double CrossingShellLimit(const MergeStatistics &ms, double target, bool plain) { double finest = 0.0; for (const auto &sh : ms.shells) { - if (!std::isfinite(sh.cc_half_internal)) + if (!std::isfinite(ShellCC(sh, plain))) continue; - if (sh.cc_half_internal < target) + if (ShellCC(sh, plain) < target) return finest == 0.0 ? 0.0 : sh.d_min; finest = sh.d_min; } @@ -447,12 +450,12 @@ namespace { // resolution can be read off it. Returns that shell's d_min, 0.0 when the curve never climbs // back. A wobble that stays ABOVE the target is not this - a fall-off with a noisy shell in it // is still a fall-off. - double CCHalfClimbsBackAboveTarget(const MergeStatistics &ms, double target) { + double CCHalfClimbsBackAboveTarget(const MergeStatistics &ms, double target, bool plain) { bool below = false; for (const auto &sh : ms.shells) { - if (!std::isfinite(sh.cc_half_internal)) + if (!std::isfinite(ShellCC(sh, plain))) continue; - if (sh.cc_half_internal < target) + if (ShellCC(sh, plain) < target) below = true; else if (below) return sh.d_min; @@ -1513,9 +1516,14 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, // be quotable when the curve it was read off never behaved like a fall-off. A fit finer // than the finest shell that still reaches the target asserts a resolution the shell // table on this very page refutes. - const double finest_supported = FinestSupportedShell(ms, cc_target); - const double crossing_limit = CrossingShellLimit(ms, cc_target); - const double climbs_back = CCHalfClimbsBackAboveTarget(ms, cc_target); + // The number to quote is the fit on the standard CC1/2 the shell table prints (rotation: the + // plain fit); the cut applied is made on the internal weighted fit, reported as + // FITTED_RESOLUTION_INTERNAL where the two exist. Stills have only the one, already unweighted. + const bool plain = result.resolution_fit_plain_A.has_value(); + const std::optional quoted_fit = plain ? result.resolution_fit_plain_A : result.resolution_fit_A; + const double finest_supported = FinestSupportedShell(ms, cc_target, plain); + const double crossing_limit = CrossingShellLimit(ms, cc_target, plain); + const double climbs_back = CCHalfClimbsBackAboveTarget(ms, cc_target, plain); std::string fit_suppressed; // Only where there are shells to judge the fit against; with no table there is no // evidence either way, and suppressing on no evidence is its own false claim. @@ -1525,26 +1533,26 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, "CC1/2 falls below the cut's target of {:.2f} and then climbs back above it" " at {:.2f} A, so this curve is not a fall-off and no one resolution can be" " read off it", cc_target, climbs_back); - else if (result.resolution_fit_A) { + else if (quoted_fit) { if (crossing_limit == 0.0) fit_suppressed = fmt::format( "CC1/2 never reaches the cut's target of {:.2f} in any shell, so there is" " no fall-off to fit", cc_target); - else if (*result.resolution_fit_A < crossing_limit - 1e-6) + else if (*quoted_fit < crossing_limit - 1e-6) fit_suppressed = fmt::format( "the fit reads {:.2f} A, finer than the shell where CC1/2 falls below" " {:.2f} ({:.2f} A), so the curve it was read off is not a fall-off", - *result.resolution_fit_A, cc_target, crossing_limit); + *quoted_fit, cc_target, crossing_limit); else - fitted_resolution = *result.resolution_fit_A; + fitted_resolution = *quoted_fit; } } if (fitted_resolution) Add(s, KeyReal("FITTED_RESOLUTION", *fitted_resolution, "{:.2f}")); - // Diagnostic: the same automatic fit on the plain CC1/2 (rotation), its crossing and the limit - // it would have written. Not applied; printed as fitted, without the suppression above. - if (result.resolution_fit_plain_A) - Add(s, KeyReal("FITTED_RESOLUTION_PLAIN", *result.resolution_fit_plain_A, "{:.2f}")); + // Internal: the fit the applied cut was made on (the weighted CC1/2), printed as fitted, without + // the suppression above; and the limit the plain fit would have written. + if (plain && result.resolution_fit_A) + Add(s, KeyReal("FITTED_RESOLUTION_INTERNAL", *result.resolution_fit_A, "{:.2f}")); if (result.resolution_cut_plain_A) Add(s, KeyReal("RESOLUTION_CUT_PLAIN", *result.resolution_cut_plain_A, "{:.2f}")); @@ -1688,9 +1696,9 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, " FITTED_RESOLUTION\n is where the CC1/2 fall-off crosses {:.2f}, and is the number" " to quote. The data are kept\n one shell past it on purpose: an included shell can" " still be downweighted by refinement,\n while a truncated one cannot be put back." - " The fit weights each reflection by the scale of the\n frames it was measured on," - " so a weak stretch of the sweep does not drag it in; the CC1/2 in\n the shell table" - " is the standard unweighted one, and can read lower where there is such a stretch.", + " It is read off the standard CC1/2 of the shell table; the\n cut itself weights" + " each reflection by the scale of the frames it was measured on, so a weak\n stretch" + " of the sweep does not drag it in (FITTED_RESOLUTION_INTERNAL).", cc_target))); if (completeness_narrower && o.possible_unique_reflections > 0) Add(s, Prose(fmt::format( diff --git a/tests/MergeScaleTest.cpp b/tests/MergeScaleTest.cpp index cbaf1cf1b..c1c1571ec 100644 --- a/tests/MergeScaleTest.cpp +++ b/tests/MergeScaleTest.cpp @@ -396,7 +396,7 @@ TEST_CASE("ResolutionCutoff_ScaledUpNoiseDoesNotHideTheFallOff") { CHECK(*rc.d_fit == Catch::Approx(2.0).margin(0.15)); // Counted as equals, the same reflections hide it: the curve reads noise from the first bin on. - // The plain fit (the FITTED_RESOLUTION_PLAIN diagnostic) is that, with the weights left in place. + // The plain fit (the quoted FITTED_RESOLUTION) is that, with the weights left in place. const auto plain = ComputeCCHalfLogisticCutoff(merged, 0.30, logger, /*weighted=*/false); for (auto &m : merged) m.cc_weight = 1.0f; const auto unweighted = ComputeCCHalfLogisticCutoff(merged, 0.30, logger); diff --git a/tests/ResultReportTest.cpp b/tests/ResultReportTest.cpp index 344180b1c..9f1d5d3a3 100644 --- a/tests/ResultReportTest.cpp +++ b/tests/ResultReportTest.cpp @@ -792,18 +792,20 @@ TEST_CASE("ResultReport_FittedResolutionSuppressed", "[Diagnostics]") { != std::string::npos); result.resolution_fit_A = 0.95; - // The fit is judged against the curve it was made on, the internal weighted CC1/2, not the plain one - // the table prints: a weak stretch can pull the plain shell CC1/2 under the target where the - // weighted one, and the fit, stay above it. + // Without a plain fit (stills) the one fit is quoted, judged against the internal CC1/2 it was made + // on: a weak stretch can pull the plain shell CC1/2 under the target where the weighted one stays. ProcessResult weak = result; weak.merge_statistics.shells.front().cc_half = 0.25; CHECK(RenderResultReport("p", "in.h5", x, weak).find("\nFITTED_RESOLUTION= 0.95\n") != std::string::npos); - // The fit on the plain CC1/2 is a diagnostic beside it, printed as fitted. - weak.resolution_fit_plain_A = 1.10; - weak.resolution_cut_plain_A = 1.02; - const auto weak_text = RenderResultReport("p", "in.h5", x, weak); - CHECK(weak_text.find("\nFITTED_RESOLUTION_PLAIN= 1.10\n") != std::string::npos); - CHECK(weak_text.find("\nRESOLUTION_CUT_PLAIN= 1.02\n") != std::string::npos); + // With a plain fit (rotation) that is the number quoted, and the weighted fit the cut was made on is + // reported as internal, printed as fitted. + ProcessResult two = result; + two.resolution_fit_plain_A = 1.10; + two.resolution_cut_plain_A = 1.02; + const auto two_text = RenderResultReport("p", "in.h5", x, two); + CHECK(two_text.find("\nFITTED_RESOLUTION= 1.10\n") != std::string::npos); + CHECK(two_text.find("\nFITTED_RESOLUTION_INTERNAL= 0.95\n") != std::string::npos); + CHECK(two_text.find("\nRESOLUTION_CUT_PLAIN= 1.02\n") != std::string::npos); // CC1/2 that drops through the target and climbs back above it is not a fall-off either, and the // finest shell reaching the target is then noise correlating with noise - so a fit the test above