BraggPrediction.h claimed the buffer "GROWS to whatever a frame actually predicts, so a large cell is never truncated here". Only the two GPU Calc overrides call GrowCapacity; both CPU predictors stop at max_reflections. The cap is applied inside the h/k/l walk and before the resolution test, so what survives is the low-|h| block, not the reflections nearest the Ewald sphere - a cell large enough to overflow 20000 gives different merged reflections with and without a GPU. Documented rather than silently claimed otherwise. Also removed a paragraph describing a once-per-predictor overflow warning that no longer exists, and fixed the rugnux_cli.cpp path in HDF5.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
4.9 KiB
C++
88 lines
4.9 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||
// SPDX-License-Identifier: GPL-3.0-only
|
||
|
||
#pragma once
|
||
|
||
#include <vector>
|
||
|
||
#include "../../common/CrystalLattice.h"
|
||
#include "../../common/DiffractionExperiment.h"
|
||
#include "../../common/Reflection.h"
|
||
|
||
struct BraggPredictionSettings {
|
||
float high_res_A = 1.5;
|
||
float ewald_dist_cutoff = 0.0005;
|
||
// Per-index half-widths of the box the predictor walks: h runs -max_h..+max_h, and so on. One limit
|
||
// per axis rather than one cube, because each index is bounded by its OWN axis (|h| <= a/d_min), so
|
||
// a cube sized for the longest axis walks the short ones far past anything the resolution cut can
|
||
// keep - on a 149/83/226 A cell that is ~16x the candidates a per-axis box generates.
|
||
int max_h = 100;
|
||
int max_k = 100;
|
||
int max_l = 100;
|
||
char centering = 'P';
|
||
float wedge_deg = 0.1f;
|
||
float mosaicity_deg = 0.2f;
|
||
float min_zeta = 0.05;
|
||
float mosaicity_multiplier = 4.0;
|
||
// Relative X-ray bandwidth Δλ/λ expressed as a Gaussian sigma (0 = monochromatic).
|
||
// When > 0 the Ewald-shell acceptance is thickened radially per reflection by
|
||
// σ_bw = |recip_z|·bandwidth_sigma (= bλ/2d²), so the 1/d² pink-beam smear no
|
||
// longer clips high-resolution reflections.
|
||
float bandwidth_sigma = 0.0f;
|
||
};
|
||
|
||
class BraggPrediction {
|
||
protected:
|
||
// Not const: on the GPU path the buffer grows to fit a frame that predicts more than it holds.
|
||
int max_reflections;
|
||
std::vector<Reflection> reflections;
|
||
|
||
// Make room for `count` reflections. Overridden where device buffers have to follow. Called only
|
||
// when a frame predicted more than the current capacity, so a run pays for it a handful of times.
|
||
//
|
||
// NOTE: only the GPU Calc overrides call this. BraggPrediction::Calc and BraggPredictionRot::Calc
|
||
// stop filling at max_reflections instead, silently - and because that cap is applied inside the
|
||
// h/k/l walk, before the resolution test, what survives is the low-|h| block rather than the
|
||
// reflections nearest the Ewald sphere. A cell large enough to overflow 20000 therefore yields
|
||
// different merged reflections on a CPU-only build than on a GPU one.
|
||
virtual void GrowCapacity(int count);
|
||
|
||
// Deterministically cap Calc's output at kPredictionOutput: if more were predicted, keep the ones
|
||
// closest to the Ewald sphere (smallest excitation error), ties broken by hkl. Returns the kept
|
||
// count. Below the cap it is a no-op. Call at the end of every Calc override.
|
||
int TruncateToOutput(int count);
|
||
public:
|
||
// The prediction buffer holds up to kPredictionCapacity reflections so a strong lattice does not
|
||
// overflow it. Calc returns at most kPredictionOutput, the number that flows downstream and is serialized - kept low so the
|
||
// per-image reflection list stays within the frame transport headroom.
|
||
// Starting size. On the GPU path the buffer grows to whatever a frame actually predicts
|
||
// (GrowCapacity), so a large cell is not truncated there; the CPU path still caps at this value,
|
||
// see the note on GrowCapacity. It used to be a hard cap on both, and overflowing it was lossy
|
||
// and NON-DETERMINISTIC - the GPU kernels claim slots with an atomicAdd, so which reflections
|
||
// survived depended on block scheduling and changed between runs of the same command.
|
||
static constexpr int kPredictionCapacity = 20000;
|
||
// How many reflections may flow downstream per image. Sized for a large unit cell: a ~2.8e6 A^3
|
||
// cell predicts up to ~44000 per frame at 2.4 A. Truncating below what the frame really has costs
|
||
// more than it saves - the selection keeps the smallest excitation errors, i.e. the nearly
|
||
// fully-recorded reflections, and the rotation combine rebuilds a full FROM the partials it drops
|
||
// (measured on such a crystal: CC1/2 98 -> 60, ISa 8.6 -> 1.8). DiffractionExperiment's image-buffer
|
||
// headroom is derived from this, so the transport can carry what the analysis produces.
|
||
static constexpr int kPredictionOutput = 65536;
|
||
// What the ONLINE path may carry per image. The acquisition transports every reflection list through
|
||
// a fixed-size image-buffer slot, and the slot size divides a fixed total - so sizing the slot for
|
||
// kPredictionOutput would cut the number of slots, and with it the receiver's ability to absorb a
|
||
// burst, by about three. Online keeps the transport-sized limit it has always had; offline, which
|
||
// has no such budget, keeps the full one. DiffractionExperiment's buffer headroom derives from THIS.
|
||
static constexpr int kOnlineMaxReflections = 10000;
|
||
|
||
explicit BraggPrediction(int max_reflections = kPredictionCapacity);
|
||
|
||
virtual ~BraggPrediction() = default;
|
||
virtual int Calc(const DiffractionExperiment &experiment, const CrystalLattice &lattice,
|
||
const BraggPredictionSettings &settings);
|
||
const std::vector<Reflection> &GetReflections() const;
|
||
};
|
||
|
||
|
||
|