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 <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 09:17:23 +01:00
1 parent d3d426aa8a
commit c5fc3fa1d6
4 files changed
+24 -3

No files matched your search

+3
View File
@@ -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<uint32_t> 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;
+15 -2
View File
@@ -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<uint32_t> 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<ShadowFinder> 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<const std::vector<float>>(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);
+3
View File
@@ -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<uint32_t> 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
+3 -1
View File
@@ -86,8 +86,10 @@ void JFJochProcessController::run_(QString file_path, DiffractionExperiment expe
const RugnuxMeasuredGain g = RugnuxMeasureGain(reader, static_cast<size_t>(std::max(1, config.nthreads)));
const std::optional<float> gain = g.peaks ? std::optional<float>(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