Files
Jungfraujoch/image_analysis/spot_finding/AdaptiveSpotFinderGPU.h
T
leonarski_f 538f3504d3
Build Packages / build:windows:nocuda (push) Successful in 20m4s
Build Packages / Unit tests (push) Skipped
Build Packages / build:viewer-tgz:cpu (push) Successful in 16m5s
Build Packages / build:viewer-tgz:cuda (push) Successful in 17m26s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m46s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 20m17s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 26m13s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 23m17s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m11s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m30s
Build Packages / build:rpm (rocky8) (push) Successful in 24m34s
Build Packages / build:rpm (rocky9) (push) Successful in 21m30s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 23m33s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 20m18s
Build Packages / DIALS test (push) Successful in 18m23s
Build Packages / XDS test (durin plugin) (push) Successful in 11m30s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m16s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m2s
Build Packages / Generate python client (push) Successful in 49s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 29m45s
v1.0.0.rc-161 (#71)
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* **rugnux: significantly better quality of results, and faster.** A large rework of integration, scaling, merging, geometry refinement and space-group determination, together with measurements the program previously made no attempt at - the direct beam before indexing, the beam stop, the goniometer rotation scale, and the stretches of a sweep the crystal did not deliver. A rotation dataset typically gains observations at better <I/sigma> and R_meas, and every `mx` and `scale` run writes a `<prefix>_report.txt` results report modelled on XDS's `CORRECT.LP`. Many defaults moved with it: spot detection is self-calibrating, beam-stop detection and rotation geometry post-refinement are on, resolution limits default to as far as the detector reaches, and ice-ring handling engages only where the crystal is measured to have ice.
* **jfjoch_viewer:** the beam-stop shadow, the detector calibration and the beam-centre measurement are reachable from "Analyze dataset"; the settings panel reports how the sample moved and how polarized the beam was; image rendering and interaction are faster.
* **Performance:** bitshuffle+LZ4 images are decoded on the GPU rather than on the host, with the bitshuffle inverse fused into preprocessing so the decompressed frame is never held in device memory.
* **Broker, writer, packaging and build:** image-slot lifetime and locking fixes, per-image datasets sized by the images actually written, the Debian/Ubuntu broker package renamed to `jfjoch`, and `image_analysis` compiling under MSVC again.

**Breaking change to the rugnux command line:**
* `--azint-only` and `--scale` are **removed**, replaced by `--mode azint` and `--mode scale`; the full pipeline is `--mode mx` and remains the default. A script passing the old flags now fails with the list of valid modes rather than silently running the wrong one.
* `-t`/`--stride` is **refused on rotation data**: skipping frames cuts every reflection's rocking curve, so the combined fulls and their partiality would be measured over frames the sweep never recorded. Select a contiguous range with `-s`/`-e` instead. `--mode azint` and `--force-still` still take a stride.

**Breaking changes to OpenAPI** - regenerate the client (`jfjoch-client` 1.0.0-rc.161, `frontend/src/client`) or read the affected fields as optional:
* `image_scale_b` is removed from the `plot_type` enum, so a client requesting that plot now gets an error rather than a curve.
* `azim_int_settings.high_q_recipA`, `spot_finding_settings.high_resolution_limit` and `spot_finding_settings.low_resolution_limit` are no longer `required`. All three mean "no limit at that end" when unset and are omitted from the response instead of carrying a placeholder value, which raises in a client generated from an rc.160-or-earlier spec. A value of 0 is still accepted and means the same thing.

**Breaking changes to the stored formats** - a consumer reading these fields must treat them as optional:
* The per-image image-scale B factor is no longer computed, so `/entry/MX/imageScaleBFactor` is absent from newly written HDF5 files and the corresponding key is absent from the CBOR DataMessage and END blocks. Files written by rc.160 and earlier still contain it and still open; nothing in the pipeline reads it any more.
* `_reflns.jfjoch_diffrn_ISa` now carries the whole-range `1/sqrt(a*b)` that XDS's ISa denotes, and the error-model `a` and `b` are reported in XDS's convention; the strong-reflection asymptote moves to `_reflns.jfjoch_diffrn_ISa_asymptotic`. **A file written by an earlier version carries the asymptote under the plain `ISa` name.**

Reviewed-on: #71
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-08-13 17:03:10 +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).
// It then flags strong pixels (value >= ring threshold) into a packed bit buffer and hands that
// buffer - still on the device - to SpotExtractorGPU, which builds the spots there.
//
// 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 raw-count detection 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 "ImageSpotFinder.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 ImageSpotFinder {
const AzimuthalIntegrationMapping &mapping;
std::shared_ptr<CudaStream> stream;
const int nbins;
const size_t npix;
int reduce_threads = 128;
int reduce_blocks = 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 strong-pixel bit buffer.
CudaDevicePtr<float> gpu_thr;
CudaDevicePtr<uint32_t> gpu_strong;
// 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;
SpotExtractorGPU extractor; // builds the spots from gpu_strong without it leaving the device
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;
void SetResolutionMask(const std::vector<bool> &mask) override;
const std::vector<DiffractionSpot> &ExtractComponents(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; }
};