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
76 lines
2.8 KiB
C++
76 lines
2.8 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
|
|
#include <cmath>
|
|
#include <random>
|
|
|
|
#include "../image_analysis/bragg_integration/BackgroundGain.h"
|
|
#include "../common/BraggIntegrationSettings.h"
|
|
#include "../common/JFJochException.h"
|
|
|
|
namespace {
|
|
|
|
constexpr size_t W = 512, H = 512;
|
|
|
|
// A smooth background of 100..2000 photons per pixel, read out as bias + gain * photons + read noise,
|
|
// with a grid of bright 5x5 "Bragg" spots and a masked strip.
|
|
std::vector<int32_t> Frame(double gain, uint32_t seed, std::vector<uint32_t> &mask) {
|
|
std::mt19937 rng(seed);
|
|
std::normal_distribution<double> read_noise(0.0, 3.0);
|
|
std::vector<int32_t> img(W * H);
|
|
mask.assign(W * H, 0);
|
|
for (size_t y = 0; y < H; y++)
|
|
for (size_t x = 0; x < W; x++) {
|
|
const double photons = 100.0 + 1900.0 * (x + y) / static_cast<double>(W + H);
|
|
std::poisson_distribution<int> p(photons);
|
|
double v = (gain > 1.0 ? 20.0 : 0.0) + gain * p(rng) + (gain > 1.0 ? read_noise(rng) : 0.0);
|
|
if (x % 32 >= 14 && x % 32 < 19 && y % 32 >= 14 && y % 32 < 19)
|
|
v += 50.0 * gain * photons;
|
|
img[y * W + x] = static_cast<int32_t>(std::lround(v));
|
|
if (y >= 200 && y < 210)
|
|
mask[y * W + x] = 1;
|
|
}
|
|
return img;
|
|
}
|
|
|
|
}
|
|
|
|
TEST_CASE("BackgroundGain_CCD_gain4") {
|
|
std::vector<background_gain::Levels> frames;
|
|
std::vector<uint32_t> mask;
|
|
for (uint32_t seed = 1; seed <= 3; seed++) {
|
|
const auto img = Frame(4.0, seed, mask);
|
|
frames.push_back(background_gain::MeasureFrame(img, W, H, mask, 1 << 30));
|
|
}
|
|
const auto g = background_gain::Gain(frames);
|
|
REQUIRE(g.has_value());
|
|
CHECK_THAT(*g, Catch::Matchers::WithinAbs(4.0, 0.25));
|
|
}
|
|
|
|
TEST_CASE("BackgroundGain_photon_counting") {
|
|
std::vector<uint32_t> mask;
|
|
const auto img = Frame(1.0, 7, mask);
|
|
const auto g = background_gain::Gain({background_gain::MeasureFrame(img, W, H, mask, 1 << 30)});
|
|
REQUIRE(g.has_value());
|
|
CHECK_THAT(*g, Catch::Matchers::WithinAbs(1.0, 0.1));
|
|
}
|
|
|
|
TEST_CASE("BackgroundGain_saturated_and_empty") {
|
|
std::vector<uint32_t> mask;
|
|
const auto img = Frame(4.0, 11, mask);
|
|
// Everything at or above the saturation limit: nothing to measure.
|
|
CHECK_FALSE(background_gain::Gain({background_gain::MeasureFrame(img, W, H, mask, 1)}).has_value());
|
|
CHECK_FALSE(background_gain::Gain({}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BackgroundGain_settings") {
|
|
BraggIntegrationSettings s;
|
|
CHECK(s.GetBackgroundPixelGain() == 1.0f);
|
|
s.BackgroundPixelGain(4.5f);
|
|
CHECK(s.GetBackgroundPixelGain() == 4.5f);
|
|
CHECK_THROWS_AS(s.BackgroundPixelGain(0.5f), JFJochException);
|
|
}
|