Files
Jungfraujoch/image_analysis/spot_finding/AdaptiveSpotFinderGPU.h
T
leonarski_f 680c36c20d
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
v1.0.0-rc.166 (#76)
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands.
* `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion.
* Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants.
* `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing.
* A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed.
* `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing.
* Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences.
* The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to.
* The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after.
* The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution.
* `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have.
* Twinning is no longer reported when the L-test contradicts it.
* The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's.
* `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots.
* The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area.

Reviewed-on: #76
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-02 21:17:31 +02:00

124 lines
7.2 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
// GPU adaptive spot finder that FUSES azimuthal integration and spot finding into one image pass.
//
// The CPU adaptive finder (AdaptiveSpotFinderCPU) and the azimuthal integrator both bin every pixel
// into resolution rings and reduce (sum / sum^2 / count). Today azint runs on the GPU while the
// adaptive finder re-does the identical per-ring reduction on the HOST - a wasted second pass over a
// ~10 MP image. This engine does the ring reduction on the GPU and drives BOTH products from it:
// - the azimuthal-integration profile (mean intensity per ring, in flat-field-corrected space), and
// - the per-ring background (mean, sigma, peak-excluded via two sigma-clip passes) that sets the
// self-calibrating spot-detection threshold (in raw photon counts).
// The ring threshold is the photon-count FLOOR and nothing more: this engine derives from the classic
// ImageSpotFinderGPU and intersects the ring mask with that engine's local-box SNR mask, exactly as
// AdaptiveSpotFinderCPU does on the host (the reason the local test is not optional is written out
// there). The result is left in the inherited bit buffer, still on the device, and the inherited
// SpotExtractorGPU builds the spots from it.
//
// Numerically it reproduces AdaptiveSpotFinderCPU: the same three-pass robust background, the same
// per-ring threshold formula (shared via AdaptiveThreshold.h, computed on the host once per frame),
// and the same intersection with the local test. The only differences from the CPU are those inherent
// to a GPU reduction (float per-ring accumulation in atomic order vs the CPU's serial double sums),
// which shift a handful of borderline pixels at most. The corrected sums for the azint profile are
// accumulated in the SAME plain first pass, so one reduction feeds both products.
#include <memory>
#include <vector>
#include "ImageSpotFinderGPU.h"
#include "SpotExtractorGPU.h"
#include "SpotFindingSettings.h"
#include "../../common/AzimuthalIntegrationProfile.h"
#include "../../common/AzimuthalIntegrationMapping.h"
#include "../indexing/CUDAMemHelpers.h"
#include "../indexing/CudaSharedTables.h"
class AdaptiveSpotFinderGPU : public ImageSpotFinderGPU {
const AzimuthalIntegrationMapping &mapping;
const int nbins;
const size_t npix;
int reduce_threads = 256;
int reduce_blocks = 0; // global-atomics fallback
int reduce_blocks_plain = 0; // as many blocks as actually fit, per shared-memory footprint
int reduce_blocks_clip = 0;
int flag_threads = 256;
int flag_blocks = 0;
size_t shared_plain = 0; // per-block shared bytes for the plain pass (raw + corrected rings)
size_t shared_clip = 0; // per-block shared bytes for a sigma-clip pass (raw rings only)
bool use_shared = true; // false -> nbins too large for shared memory, use the global-atomics kernel
// Static mapping inputs: geometry-only, so one copy per GPU shared with every other engine on it
// (see CudaSharedTables.h) rather than one copy per worker thread.
std::shared_ptr<CudaDevicePtr<uint16_t>> gpu_pixel_to_bin;
std::shared_ptr<CudaDevicePtr<float>> gpu_corrections;
// Raw per-ring accumulators (re-zeroed each pass) + derived stats used to clip and threshold.
// double, like the CPU engine's ring accumulators: the ring sigma is the cancelling difference
// sum2/n - m^2, and the block atomics that fill these arrive in an arbitrary order.
CudaDevicePtr<unsigned long long> gpu_sum;
CudaDevicePtr<unsigned long long> gpu_sum2;
CudaDevicePtr<uint32_t> gpu_count;
CudaDevicePtr<float> gpu_mean; // per-ring raw mean (clip predicate)
CudaDevicePtr<float> gpu_sigma; // per-ring raw sigma (clip predicate)
// Corrected per-ring accumulators (plain first pass only) -> azimuthal-integration profile.
CudaDevicePtr<float> gpu_sum_corr;
CudaDevicePtr<float> gpu_sum2_corr;
// Per-ring detection threshold (host-computed, uploaded) and the ring-threshold mask that is
// intersected into the inherited bit buffer.
CudaDevicePtr<float> gpu_thr;
CudaDevicePtr<uint32_t> gpu_ring;
// Host mirrors of the small per-ring transfers.
std::vector<unsigned long long> host_sum; // clipped raw sum } input to the host threshold computation
std::vector<unsigned long long> host_sum2; // clipped raw sum^2 } (exact integers - see the kernel)
std::vector<uint32_t> host_count; // clipped raw count }
std::vector<float> host_thr; // per-ring threshold (empty -> frame had no valid pixels)
std::vector<float> host_bkg; // clipped per-ring mean, NaN where the ring is too sparse to trust
std::vector<float> prof_sum; // plain corrected sum } azimuthal-integration profile
std::vector<float> prof_sum2; // plain corrected sum^2 }
std::vector<uint32_t> prof_count; // plain pixel count }
// Every per-ring array above is a device-to-host copy once per frame. A D2H copy into PAGEABLE
// memory blocks the host until it completes, whatever stream it was issued on - which would stall
// Detect() between the plain pass and the clip passes, with the device then idle while the host
// enqueues them. Pinning the destinations makes the copies genuinely asynchronous, as the
// azimuthal-integration engine already does with its own.
CudaRegisteredVector<unsigned long long> host_sum_reg;
CudaRegisteredVector<unsigned long long> host_sum2_reg;
CudaRegisteredVector<uint32_t> host_count_reg;
CudaRegisteredVector<float> prof_sum_reg;
CudaRegisteredVector<float> prof_sum2_reg;
CudaRegisteredVector<uint32_t> prof_count_reg;
AzimuthalIntegrationProfile last_profile; // filled every Run(), retrievable via GetProfile()
// One reduction pass over the image into the raw accumulators. clip_k <= 0 -> plain pass (all
// valid pixels); clip_k > 0 -> keep only pixels within clip_k sigma of the current gpu_mean.
// accumulate_corrected additionally fills gpu_sum_corr/gpu_sum2_corr for the profile (plain pass).
void ReducePass(const ImagePreprocessorBuffer &image, float clip_k, bool accumulate_corrected);
// Finalize gpu_mean/gpu_sigma from the current raw accumulators (per ring).
void FinalizeStats();
// Host: per-ring threshold from the clipped raw stats and the single knob E (false pixels/frame).
void ComputeThresholds(const SpotFindingSettings &settings);
public:
AdaptiveSpotFinderGPU(const AzimuthalIntegrationMapping &mapping, std::shared_ptr<CudaStream> stream);
~AdaptiveSpotFinderGPU() override = default;
AdaptiveSpotFinderGPU(const AdaptiveSpotFinderGPU &) = delete;
AdaptiveSpotFinderGPU &operator=(const AdaptiveSpotFinderGPU &) = delete;
void Detect(const ImagePreprocessorBuffer &image, const SpotFindingSettings &settings) override;
// The azimuthal profile computed as a byproduct of the last Detect() - lets this engine replace the
// separate azint pass in the analysis pipeline.
[[nodiscard]] const AzimuthalIntegrationProfile &GetProfile() const { return last_profile; }
[[nodiscard]] const std::vector<float> &GetRingBackground() const override { return host_bkg; }
};