Files
Jungfraujoch/image_analysis/spot_finding/SpotFindingSettings.h
T
leonarski_fandClaude Opus 5 ac202a55a1 Spot finding: the strong-pixel limit follows the detector
An image with 65535 or more strong pixels was given up on and reported ZERO
spots - silently, no log line, indistinguishable from a frame that did not
diffract. 65535 is one pixel in 64 of the JUNGFRAU 4M the number was written
for; left fixed while the detectors grew it became one in 276 of an
18-megapixel EIGER, which a strongly diffracting crystal passes on its best
frames. On the strong rotation set just added to the battery it cost 767 of
1800 images: peakCountUnfiltered 0 and resolutionEstimate NaN across two
blocks of the sweep, the two where the crystal diffracts hardest. Make the bar
one pixel in 64 everywhere, and never below the value that stood here, so no
smaller detector loses ground. It lived in three places - the host extractor,
StrongPixelSet, and SpotExtractorGPU's buffer capacity - now one function.

The bar was there for a reason and raising it alone would not have been safe.
sparseccl walks a sliding window of the last two lines and tests every pixel in
it, which is quadratic in how many strong pixels a line pair holds: a handful
for the silicon-tracker hits upstream wrote it for, four thousand for a lit
detector line, and 76 seconds for a fully lit frame. But the pixels arrive in
raster order, so the window need not be walked at all - a pixel's earlier
8-neighbours are the one to its left and the at most three above it, which is
what the GPU extractor already finds by binary search. Keeping the previous
line's range and a forward-only cursor gives the same edge set and the same
unions in the same order, so the labels are identical, and the fully lit frame
now takes 0.16 s. Verified bit-identical on real frames, on fully dense frames,
across occupancy 1e-5 to 5e-2, and on 4000 randomised images including ones
with blank lines; SpotExtractorGPU's host-vs-device parity test passes
untouched.

ImagePreprocessorBufferGPU's gather staging was sized to the old constant, with
a comment tying it to the caller's give-up. Raising that give-up without it
would have run the gather off the end of the device buffer, so it follows the
same limit now.

Byte-identical .hkl on three battery crystals that never reach the bar. On the
strong set, with symmetry, cell and geometry pinned so only the spot list
moves: <I/sigma> better in every resolution shell, CC1/2 97.8 -> 98.5%,
R_meas 30.5 -> 28.4%, ISa 3.36 -> 3.58, indexing rate 0.772 -> 0.824.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBumeJVx4oeXxiBRpkrE5H
2026-08-28 10:38:10 +02:00

63 lines
3.7 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <cstddef>
#include <optional>
#include <cstdint>
// Strong pixels above which the connected-component search gives up on a frame, unlabelled: an image
// with this much of the detector over threshold is not a diffraction pattern, and it is what the
// spot extractor's buffers are sized to. It is no longer a time limit - the search is linear in the
// strong pixels either way, and a fully lit 18-megapixel frame labels in 0.16 s.
//
// The bar has to be a FRACTION of the detector. It stood at a fixed 65535, which is one pixel in 64
// of the JUNGFRAU 4M it was written for; left fixed while the detectors grew it became one pixel in
// 276 of an 18-megapixel EIGER - a bar a strongly diffracting crystal clears on its best frames,
// which were then dropped whole, and in silence. One in 64 everywhere, and never below the value
// that used to stand here, so no smaller detector loses ground.
constexpr uint32_t StrongPixelLimit(size_t pixel_count) {
const auto limit = static_cast<uint32_t>(pixel_count / 64);
return limit > UINT16_MAX ? limit : UINT16_MAX;
}
struct SpotFindingSettings {
bool enable = true;
float signal_to_noise_threshold = 4.0; // STRONG_PIXEL in XDS
int64_t photon_count_threshold = 10; // Threshold in photon counts
// Minimum connected pixels per spot. std::nullopt = choose it per image: on the stills indexing
// path the frame is indexed at min-pix 3/2/1 and the one maximising indexed count x indexed fraction
// is kept (see MXAnalysisWithoutFPGA::Analyze); a value fixes it. Defaults to a concrete value, so
// the online receiver and the FPGA path keep the single-pass fixed behaviour unless set otherwise.
std::optional<int64_t> min_pix_per_spot = 2;
int64_t max_pix_per_spot = 50; // Maximum pixels per spot
// High-resolution limit for spot finding [A]. std::nullopt = as far as the detector reaches, i.e. no
// resolution clipping of the detection at all (DiffractionExperiment::GetDetectorMaxResolution_A
// supplies the number where one is needed, e.g. for the spot plot's shells).
std::optional<float> high_resolution_limit;
// Low-resolution limit for spot finding [A]. std::nullopt = no limit at the low-resolution end, the
// mirror of high_resolution_limit above. Optional rather than a zero sentinel because zero is not a
// natural "no limit" here: every pixel has d above it, so the plain comparison would mask the whole
// image rather than none of it. Defaults to a concrete value, which is where the detection normally
// stops - the direct beam and its halo sit beyond it.
std::optional<float> low_resolution_limit = 50.0;
float cutoff_spot_count_low_res = 5.0;
std::optional<float> high_res_gap_Q_recipA = 1.5; // 0.25 * 2 * pi
// Half-width of the ice-ring exclusion band in q (2*pi/d). Measured hexagonal-ice ring FWHM on the
// JUNGFRAU is ~0.06 q, so the band half-width is ~0.03; 0.02 under-covered the strong low-res rings.
float ice_ring_width_Q_recipA = 0.03;
bool indexing = true;
bool quick_integration = true;
// Self-calibrating detection (offline/rugnux path): when true, the fixed photon_count_threshold is
// replaced by a per-resolution-ring threshold set from the image's own noise (see
// AdaptiveSpotFinderCPU), so the same setting adapts across datasets with no per-dataset tuning.
// false_pixels_per_frame is the one portable knob: the expected number of noise pixels tolerated
// per frame (the threshold's operating point), ~100 for a multi-megapixel detector.
bool adaptive_threshold = false;
float false_pixels_per_frame = 100.0f;
};