diff --git a/docs/RUGNUX_REPORT.md b/docs/RUGNUX_REPORT.md index e5e0c1523..76e0cefcd 100644 --- a/docs/RUGNUX_REPORT.md +++ b/docs/RUGNUX_REPORT.md @@ -622,7 +622,9 @@ the correction surfaces and the error model it was judged on were all fitted wit merge that may be written and removes one takes its frames' reflections out — they are in no later merge and in no file written from the integrated reflections — and makes itself again from the start without them; the run's own batches are then judged again on what is left, which can remove more. The stretch -stays in this table at the scale, CC and ΔCC1/2 it was removed on. +stays in this table at the scale, CC and ΔCC1/2 it was removed on. Its warning names it as the frames to process +on their own (`-s -e `, file images): it may be another crystal, radiation-damaged or +dead data. **What ΔCC1/2 cannot do**, because the report must not imply otherwise: diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index bb5a96391..de84cfdab 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -2700,6 +2700,20 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, cost = fmt::format(" - kept at their own reduced weight; removing them would change " "CC1/2 by {:+.4f} +/- {:.4f} over the reflections they touch", -r.delta_cc_half, r.delta_cc_half_se); + // A stretch the merge removed whole (RotationScaleMerge::TakenOut) is out of the data, and + // may still be data of its own: named as the frames to process alone. Stretches removed one + // after the other can overlap; the range is all of them that this one touches. + int s0 = r.last_image + 1, s1 = r.first_image - 1; + for (const auto &[f0, f1] : sq.removed_stretches) + if (rejected && f0 <= r.last_image && r.first_image <= f1) { + s0 = std::min(s0, f0); + s1 = std::max(s1, f1); + } + if (s0 <= s1) + cost += fmt::format(". The stretch removed whole may be another crystal, radiation-damaged " + "or dead data; -s {} -e {} processes it on its own", + result.first_image + s0 * result.image_stride, + result.first_image + s1 * result.image_stride + 1); Warn(doc, PathologyCode::SWEEP_GAPS, fmt::format( "Frames {}-{} {} ({:.1f} deg, scale {:.2f} and CC {:.2f} of the run, {:.0f}% scaled){}", r.first_image, r.last_image, SweepQualityReasonText(r.reason), diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 3773244c0..7b14d00e8 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -565,6 +565,9 @@ struct RugnuxOutputs { struct ProcessResult { bool cancelled = false; uint64_t images_processed = 0; + // The file image of processed-image ordinal f is first_image + f * image_stride (-s, --stride). + int first_image = 0; + int image_stride = 1; double processing_time_s = 0.0; // Wall time of the per-image loop alone; processing_time_s additionally covers the first-pass // indexing and the scaling/merging around it, which no per-stage timer sees. diff --git a/rugnux/RugnuxImagePass.cpp b/rugnux/RugnuxImagePass.cpp index 268acd068..af8a6d07f 100644 --- a/rugnux/RugnuxImagePass.cpp +++ b/rugnux/RugnuxImagePass.cpp @@ -482,6 +482,8 @@ void Rugnux::ImagePass(PipelineLocals &p) { std::chrono::steady_clock::now() - image_loop_start).count(); result.images_processed = finished_count.load(); + result.first_image = start_image; + result.image_stride = config_.stride; // fast_live: a cut inside the canonical pass's image loop ends its sweep at the images read so far, and // the pass merges those, a partial sweep, as a pre-pass merges its share. Any other pass a cut stops. const bool partial_sweep = config_.fast_live && cancelled_ && p.write_output && !geometry_prepass diff --git a/tests/ResultReportTest.cpp b/tests/ResultReportTest.cpp index 9f1d5d3a3..6764a6be9 100644 --- a/tests/ResultReportTest.cpp +++ b/tests/ResultReportTest.cpp @@ -163,6 +163,29 @@ TEST_CASE("ResultReport_Render", "[Diagnostics]") { != std::string::npos); } +// A rejected range the merge removed whole (RotationScaleMerge::TakenOut) names the frames to process on +// their own, as -s / -e give them: file images, the end exclusive, through the run's own -s and stride. +TEST_CASE("ResultReport_RemovedStretchFramesToProcessAlone", "[Diagnostics]") { + DiffractionExperiment x(DetJF(1)); + x.ImagesPerTrigger(1210); + ProcessResult result; + result.images_processed = 600; + result.first_image = 10; + result.image_stride = 2; + result.consensus_cell = UnitCell{.a = 79.0f, .b = 79.0f, .c = 38.0f, + .alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f}; + result.space_group = *gemmi::find_spacegroup_by_number(96); + result.has_merge_statistics = true; + result.merge_statistics.sweep_quality = TestSweepQuality(); + CHECK(RenderResultReport("prefix", "in.h5", x, result).find("processes it on its own") == std::string::npos); + + // Removed one after the other, the two overlap: the range is both. + result.merge_statistics.sweep_quality.removed_stretches = {{120, 149}, {100, 130}}; + const auto text = RenderResultReport("prefix", "in.h5", x, result); + CHECK(text.find("over the reflections they touch. The stretch removed whole may be another crystal, " + "radiation-damaged or dead data; -s 210 -e 309 processes it on its own") != std::string::npos); +} + // The reference-range table: REFRES_* keys beside the run's own, the shells past the run's own limit // reported as not merged rather than as zeros, and no REFRES_ key at all when no range was given. TEST_CASE("ResultReport_ReferenceRangeTable", "[Diagnostics]") {