From c5fc3fa1d68c0dd7eee2e33b98c00c1db1456ec6 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 5 Nov 2026 09:17:23 +0100 Subject: [PATCH] Beam stop: looked for with the CCD's inactive pixels read as data The pixels outside a CCD's active area, constant over the gain frames, are masked as dead before the run starts. A CCD reads the deep shadow of a beam stop at one value too, so that mask took the shadow's core, and the shadow finder - which reads only unmasked pixels and grows the shadow from where it is deep - then found a fraction of it or nothing. Of the 48 open-arm CCD rotation sets, about half lost most of their shadow: 9.35% -> 1.11% of the detector on PDB 3MEB, 10.79% -> 2.60% on 3R6O, 1.54% -> none on 9UPT; the CCD stills sets lost theirs too. What went unmasked was the penumbra about the core, which is not constant and was integrated as if exposed, and the shadow bit the integration's test of clipped reflections reads. On 7BGU the background centre the finder draws its rings about moved 56 px. The beam-stop step - the shadow finder and that centre - now reads the inactive pixels as the input has them (ProcessConfig::inactive_pixels, passed by the CLI and the viewer where they mask them); everything else keeps them masked as dead. On 11 CCD rotation sets the shadow is again exactly what it was before the inactive mask, the mask's gain on 7BGU stays (d_min 2.42 A, R-free 0.190), 9UPT R-free 0.225 -> 0.219, 2WNN ISa 14.5 -> 15.4, 3MEB d_min 1.65 -> 1.56 A; the CCD stills sets get their shadow back (R-free within 0.001). Photon-counting sets are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UqXYdL4qBGYRiSXSGXfCLu --- rugnux/Rugnux.h | 3 +++ rugnux/RugnuxPreScan.cpp | 17 +++++++++++++++-- rugnux/rugnux_cli.cpp | 3 +++ viewer/JFJochProcessController.cpp | 4 +++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 514a2c0bd..3773244c0 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -202,6 +202,9 @@ struct ProcessConfig { // Neither the input nor the command line states the distance (an XTC): the input's value is a // placeholder, which the stills search does not hold against the others and nothing refines from. bool detector_distance_unknown = false; + // The pixels outside the detector's active area (RugnuxMeasuredGain::constant, non-zero = inactive), which the + // caller has masked as dead; empty where there are none. The beam-stop search reads them as data (PreScan). + std::vector inactive_pixels; // Whether a saturation level the frames show below the file's limit replaces it (DetectSaturation): // for every input but a Jungfraujoch-written file, whose stated limit is trusted. bool detect_saturation = false; diff --git a/rugnux/RugnuxPreScan.cpp b/rugnux/RugnuxPreScan.cpp index 95ff6ce0b..8575cdc4c 100644 --- a/rugnux/RugnuxPreScan.cpp +++ b/rugnux/RugnuxPreScan.cpp @@ -339,9 +339,22 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru const bool measure_width = want_width && !(measured && measured->spots); const bool measure_hot = want_hot && !(measured && measured->defects); + // The beam stop is looked for on the detector as the input has it: the pixels outside the active area + // (ProcessConfig::inactive_pixels) are read as data here, and by the centre its rings are drawn about. A CCD + // reads the deep shadow of a beam stop at one value as well; masked, the finder sees only the shadow's rim + // or nothing, and leaves the penumbra about it unmasked. What it finds there is a shadow, for the + // integration's test of the reflections it clips. Everything else keeps them masked as dead. + PixelMask beam_stop_mask = pixel_mask_; + if (!config_.inactive_pixels.empty()) { + std::vector m = pixel_mask_.GetMask(); + for (size_t i = 0; i < m.size(); i++) + if (config_.inactive_pixels[i] != 0) + m[i] &= ~(1u << PixelMask::ErrorPixelBit); + beam_stop_mask = PixelMask(m); + } std::optional finder; if (find_shadow) - finder.emplace(experiment_, pixel_mask_); + finder.emplace(experiment_, beam_stop_mask); // The spot width is read in the same pass as the projection rather than in a pre-pass of its own. // The image is preprocessed a second time for it: the shadow is accumulated from raw counts and @@ -1049,7 +1062,7 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru TimingMark mark(logger, "pre-scan beam stop"); std::string centre_line; projection = std::make_shared>(finder->GetMeanProjection()); - if (const auto ring_center = FindBeamCenterFromBackground(experiment_, pixel_mask_, + if (const auto ring_center = FindBeamCenterFromBackground(experiment_, beam_stop_mask, *projection)) { finder->BeamCenter(ring_center->beam_x_pxl, ring_center->beam_y_pxl); ring_centre = std::make_pair(ring_center->beam_x_pxl, ring_center->beam_y_pxl); diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index cfaa47511..2a1409d6d 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -2151,6 +2151,7 @@ int RugnuxInputRun::PrepareRugnuxInput() { logger.Error("--gain is for input in detector units; {} is a CBF of counted photons", input_file); exit(EXIT_FAILURE); } + std::vector inactive_pixels; // masked as dead below (RugnuxMeasuredGain::constant) if (counts_in_adu) { const bool float_frames = xfel_input && xfel_reader.StoredFloat(); const float unit = float_frames ? 1.0f / 16.0f : 1.0f; @@ -2181,6 +2182,7 @@ int RugnuxInputRun::PrepareRugnuxInput() { // Outside the active area (RugnuxMeasuredGain::constant). if (g.constant_pixels > 0) { reader.AddDeadPixelMask(g.constant); + inactive_pixels = g.constant; logger.Info("Inactive pixels: {} masked, holding one value on all {} frames of the gain measurement", g.constant_pixels, background_gain::GAIN_MAX_FRAMES); } @@ -3627,6 +3629,7 @@ int RugnuxInputRun::PrepareRugnuxInput() { config.beam_center_check = beam_center_check; config.detector_distance_given = detector_distance_mm.has_value(); config.detector_distance_unknown = detector_distance_unknown; + config.inactive_pixels = std::move(inactive_pixels); config.detector_tilt_given = rot1_rad.has_value() || rot2_rad.has_value(); config.calibration_refine_tilt = calibration_refine_tilt; config.detect_saturation = !saturation_value diff --git a/viewer/JFJochProcessController.cpp b/viewer/JFJochProcessController.cpp index 4361f3753..e6e6aa98f 100644 --- a/viewer/JFJochProcessController.cpp +++ b/viewer/JFJochProcessController.cpp @@ -86,8 +86,10 @@ void JFJochProcessController::run_(QString file_path, DiffractionExperiment expe const RugnuxMeasuredGain g = RugnuxMeasureGain(reader, static_cast(std::max(1, config.nthreads))); const std::optional gain = g.peaks ? std::optional(g.peaks->gain) : g.variance; ApplyRugnuxPixelGain(experiment, config.spot_finding, gain.value_or(1.0f)); - if (g.constant_pixels > 0) + if (g.constant_pixels > 0) { pixel_mask.AddDeadPixelMask(experiment, g.constant); + config.inactive_pixels = g.constant; + } } // Seed the live dataset with the experiment so the chart has geometry context; per-image