Report: a stretch removed whole is named as the frames to process on their own

The SWEEP_GAPS warning of a rejected range that the merge removed whole and
took out of the data (RotationScaleMerge::TakenOut) now says what the frames
may be - another crystal, radiation-damaged or dead data - and gives them as
the -s / -e range that processes them on their own: file images, the end
exclusive, through the run's own -s and stride (ProcessResult::first_image,
image_stride). Stretches removed one after the other can overlap; the range
is all of them the warning's range touches.

Processed alone, such stretches range from dead frames to valid data of
another crystal; the user is the one who can tell which is wanted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqXYdL4qBGYRiSXSGXfCLu
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-05 13:31:40 +01:00
1 parent d04f91fd74
commit 48dcf8218a
5 files changed
+45 -1

No files matched your search

+3 -1
View File
@@ -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 <first> -e <last + 1>`, file images): it may be another crystal, radiation-damaged or
dead data.
**What ΔCC1/2 cannot do**, because the report must not imply otherwise:
+14
View File
@@ -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),
+3
View File
@@ -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.
+2
View File
@@ -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
+23
View File
@@ -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]") {