Files
Jungfraujoch/image_analysis/bragg_integration/BackgroundGain.h
T
leonarski_fandClaude Opus 5.5 aec6f6aabc rugnux: background clip scaled by the CCD gain measured in the pre-scan
The background ring's high-side clip, mean + 4*sqrt(mean), assumes counted photons. A CCD reports
ADU: on a Rigaku Saturn a background pixel scatters 4-5x its mean, so the clip sat at ~2 sigma, cut
the upper tail of every ring and added a positive offset to every reflection, compressing the
intensity statistics (a false 422 on one twinned I41 crystal).

No header carries a usable gain (SMV states none, every marCCD file the writer default of 1 ADU per
photon), so it is measured: background_gain::MeasureFrame takes the variance per unit mean of the
difference of pixels two columns apart, per level, clipped, on every fourth row of up to 16 of the
pre-scan's frames; Gain is the pair-weighted median over the levels. The clip becomes
mean + n*sqrt(max(g,1)*mean) (BraggIntegrationSettings::bkg_pixel_gain, CPU and GPU engines).
Only for SMV/marCCD input (ProcessConfig::counts_in_adu, set from the file format); HDF5 and CBF
keep g = 1, the identity. One log line reports the value. No new option.

Measured g: Saturn 944+ 4.7-5.2, Saturn 944 2.7, A200 0.68, ADSC ~0.3, marCCD ~0.3 - so the
ADSC/marCCD/A200 sets keep the photon clip exactly.

Checks (reflection data via gemmi and p.hkl, against the rc175 battery outputs / base build):
identical on 11if, 7qis, 8a1a, myob_x10sa, cytc_x10sa (photon counting), 4bwl, 5epe (ADSC,
marCCD) and 6cee (A200). Saturn sets, battery baseline -> this change:
  3r6o  I4122 R_meas 19.3% ISa 4.4 <|L|> .321 -> I41 (correct) 12.1% 8.1 .384
  3mc4  R_meas 10.2 -> 9.8 %, ISa 12.1 -> 12.7, d_min 1.79 -> 1.75 A, <|L|> .360 -> .391
  5uth  12.6 -> 11.8 %, ISa 10.4 -> 10.1, <|L|> .426 -> .483, offset vs |Fc|^2 0.21 -> 0.00
  5vml  10.2 -> 9.6 %, ISa 11.0 -> 11.6, <|L|> .426 -> .475, offset 0.15 -> 0.05
  5cc8  8.1 -> 7.9 %, ISa 10.8 -> 11.2, <|L|> .469 -> .494
  3meb  14.5 -> 14.6 %, ISa 13.2 -> 13.2, <|L|> .442 -> .457
Space groups unchanged except 3r6o. Pre-scan cost: +0.5 s on a 9 Mpx ADSC sweep, within noise on
a 15 Mpx marCCD sweep, nothing on HDF5/CBF.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
2026-10-09 21:18:12 +02:00

51 lines
2.4 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
// The background noise of a detector that reports ADU rather than counted photons. For a photon
// counter a background pixel's variance equals its mean; a CCD's pixel value is a gain times the
// photons plus read noise, so its variance is g*mean (+ a constant). The gain is not in the headers
// (an SMV file states none, every marCCD file states the writer's default of 1 ADU/photon), so it is
// measured: the variance per unit mean of the difference between two pixels two columns apart, which
// share the background, but are far enough apart that the detector's point spread does not correlate
// them. Bragg peaks and zingers are clipped per level. Taken per level (bins of the pair's mean, a
// factor 1.25 wide) and summarised as the median ratio over all pairs, i.e. over the background that
// holds most of the pixels.
//
// The variance/mean of pixels in a neighbourhood is how XDS fills its GAIN table (INIT step,
// Kabsch (2010) Acta Cryst. D66, 125-132).
namespace background_gain {
constexpr int N_BINS = 64; // levels 1.25^0 .. 1.25^63, i.e. up to ~1.3e6 ADU
constexpr size_t MIN_PAIRS_PER_BIN = 1000;
constexpr size_t MIN_BINS = 3;
// Frames of the pre-scan sample it is measured on, spread over the sample. The answer moves by under
// 1 % between frames of a sweep, so more only cost time.
constexpr size_t GAIN_MAX_FRAMES = 16;
// Per level, over the pairs that survived the clip: the count, the sum of the pair means and the sum
// of the squared differences.
struct Levels {
std::array<uint64_t, N_BINS> n{};
std::array<double, N_BINS> sum_mean{};
std::array<double, N_BINS> sum_diff2{};
};
// One frame. A pixel takes part if the mask is zero there and 1 <= value < saturation (a CCD reads
// its bias in the dark, so a zero is a pixel outside the active area).
Levels MeasureFrame(const std::vector<int32_t> &image, size_t width, size_t height,
const std::vector<uint32_t> &mask, int64_t saturation);
// The gain over frames: the pair-weighted median of the per-level variance/mean, or nothing with
// fewer than MIN_BINS levels holding MIN_PAIRS_PER_BIN pairs.
std::optional<float> Gain(const std::vector<Levels> &frames);
} // namespace background_gain