From e26083f039b2a186a04d270e271c4822f4f061a3 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 27 Aug 2026 16:28:11 +0200 Subject: [PATCH] rugnux pre-scan: measure the width on settled geometry, and skip the unread projection Two things the pre-scan did regardless of what was asked of it. The spot width is read off the geometry as it stands. Candidates are excluded within MIN_BEAM_DISTANCE_PX of the beam centre and stratified into resolution bands about it, so with --estimate-beam-center the width was measured against the centre the pass was in the middle of correcting - a placeholder header centre both selects a different set of spots and labels them at the wrong resolution - and spot_width_measured_ then stopped the corrected pass from ever revisiting it. The width is now left to the second pass where one follows, which starts from the corrected centre; a run with no second pass measures it as before, a provisional radius being better than none. ShadowFinder::AddImage projects the whole image and ran for every frame of the shadow sample whether or not a shadow was wanted - with --detect-beam-stop=off, or on the second pass where the mask already holds one, that is the pass's largest per-frame cost paid for a result nobody reads. Now gated on want_shadow. The frame set is unchanged, so the mask is still built from the frames it was validated on. Gating it exposed the read-failure guard below, which asked the shadow finder how many frames it had seen. With the finder no longer fed, a pass that read every frame for the beam centre alone would have reported "no image could be read" and returned before placing it. Frames read are now counted directly. Inert on the shipped defaults, as intended: --estimate-beam-center is off by default, so the width deferral cannot fire, and the projection is wanted on the first pass. Verified by building the parent commit and this one and running eight crystals of the rotation battery through both at -N 10: every column identical - space group, unique reflections, observations, completeness, R_meas, CC1/2, - on all eight. Warm-cache wall clock 1m03s against 1m02s, i.e. a wash; the saving only falls on passes that were discarding the result anyway. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016L1qig74oYQzfUJJZbbxFh --- rugnux/Rugnux.cpp | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 4d226c7e..ed1e51b6 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -366,7 +366,18 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru const bool want_beam_center = config_.estimate_beam_center && !beam_center_placed_; // The spot width is a property of the crystal, not of the geometry, so the second pass reuses the // radius the first one measured rather than re-reading the frames. - const bool want_width = config_.adaptive_integration_radius && !spot_width_measured_; + // + // It is READ OFF the geometry as it stands, though, and a beam centre this same pass is about to + // correct leaves that geometry provisional: candidates are excluded within MIN_BEAM_DISTANCE_PX + // of the centre and stratified into resolution bands about it, so a placeholder header centre + // both measures a different set of spots and labels them at the wrong resolution - and + // spot_width_measured_ would then stop the corrected pass from ever revisiting it. Where a + // second pass follows, leave the width to that one, which starts from the corrected centre. A + // run with no second pass measures it here: a provisional radius beats none. + const bool second_pass_follows = want_beam_center && config_.rotation_indexing + && config_.two_pass_rotation && experiment_.IsRotationIndexing(); + const bool want_width = config_.adaptive_integration_radius && !spot_width_measured_ + && !second_pass_follows; if (!want_shadow && !want_beam_center && !want_width) return; @@ -486,6 +497,10 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru // Images the width ended up being measured on, for the log: the tiers below stop as soon as the // answer has settled, so this is a property of the crystal and worth reporting. size_t width_images = 0; + // Frames that could actually be read. Counted here rather than asked of the shadow finder, which + // is only fed when the shadow is wanted and would report zero for a pass that read every frame + // for the beam centre alone. + std::atomic frames_read{0}; // Read the sample on several workers. The reader serialises on the HDF5 lock, but the // decompression, the projection and the spot finding - which is all of the cost on a large @@ -568,7 +583,13 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru msg.image = img->image; msg.number = ordinal; msg.original_number = image_idx; - if (shadow_set.contains(ordinal)) + frames_read.fetch_add(1, std::memory_order_relaxed); + // Gated on want_shadow, not on the frame set alone: AddImage projects the + // whole image, and with --detect-beam-stop=off (or a shadow already in the + // mask) that is the pass's largest per-frame cost paid for a result nobody + // reads. The set still decides WHICH frames feed it, so the mask is built + // from the frames it always was. + if (want_shadow && shadow_set.contains(ordinal)) finder.AddImage(msg, w.shadow_buffer, t); // The width is measured on the projection's own even spread over the sweep, @@ -644,7 +665,7 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru } } - if (finder.GetFrameCount() == 0) { + if (frames_read.load() == 0) { logger.Warning("Pre-scan: no image could be read. Skipping."); return; }