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
51 lines
2.4 KiB
C++
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
|