Files
Jungfraujoch/tests/RotationIngestGPUTest.cpp
T
leonarski_fandClaude Opus 5.5 f88ff0440d rotation merge: the resident ingest sorts and cuts the reflections on the card
With the GPU resident, the host ingest swept the source reflections four times (background
and h histogram, sort keys, the in-range build, the device staging), bucket-sorted the keys
and cut the raw-hkl runs: 5-6 s of host time per engine on a 78 M-reflection sweep, run
twice beside the quality guard. Now the frames' reflections are copied once, through two
pinned buffers, to the card, where the per-frame background is summed, the reflections
are ordered by two stable radix passes ((image, index), then raw hkl - the one order the
host's total comparator gives), cut into runs by run-length encoding, filtered by the
resolution range and compacted. The observations, the runs and their order are the host
ingest's to the bit; a new GPU test checks the layout against the host's sort and cut.
The geometry smoothing still runs on the host in this step, on arrays read back.

The all-observation arm's engine is cloned after the main engine's device ingest (its
DeviceBytes are known only then) and copies the runs on the card as well. DeviceBytes now
also covers the ingest's own peak (90 bytes per reflection, 64 per run before the cut).

Measured: p.hkl and MTZ data identical on cytc and 7qij.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:38:25 +02:00

183 lines
7.6 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include "../common/CUDAWrapper.h"
#ifdef JFJOCH_USE_CUDA
#include <algorithm>
#include <cmath>
#include <numeric>
#include <random>
#include <vector>
#include "../common/Reflection.h"
#include "../image_analysis/scale_merge/RotationScaleMergeGPU.h"
namespace {
// Frames of reflections with every awkward case the host ingest orders: repeated hkl on one frame (both
// Ewald intersections), the same frame position twice, unusable resolutions and backgrounds, indices
// that fill the sixteen bits of the sort key.
std::vector<std::vector<Reflection>> MakeFrames(int n_frames, uint32_t seed) {
std::mt19937 rng(seed);
std::uniform_int_distribution<int> idx(-5, 5), count(0, 900), pick(0, 19);
std::uniform_real_distribution<float> u(0.0f, 1.0f);
std::vector<std::vector<Reflection>> frames(n_frames);
for (int f = 0; f < n_frames; ++f) {
const int n = (f == 7) ? 0 : count(rng);
for (int j = 0; j < n; ++j) {
Reflection r{};
r.h = idx(rng); r.k = idx(rng); r.l = idx(rng);
if (pick(rng) == 0) r.h = (j % 2) ? 32767 : -32768;
r.image_number = static_cast<float>(f) + 0.25f * static_cast<float>(pick(rng) % 3);
r.d = 1.0f + 0.1f * static_cast<float>(std::abs(r.h % 7) + std::abs(r.k) + std::abs(r.l));
if (pick(rng) == 1) r.d = NAN;
if (pick(rng) == 2) r.d = 0.0f;
r.I = 100.0f * u(rng);
r.sigma = 1.0f + u(rng);
r.bkg = (pick(rng) == 3) ? NAN : 10.0f * u(rng) - 1.0f;
r.var_bkg = u(rng);
r.prescaling_corr = 0.5f + u(rng);
r.qe_corr = 0.9f + 0.1f * u(rng);
r.flight_corr = 1.0f + 0.01f * u(rng);
r.partiality = u(rng);
r.zeta = u(rng);
r.image_scale_corr = u(rng);
r.delta_phi_deg = u(rng) - 0.5f;
r.predicted_x = 1000.0f * u(rng);
r.predicted_y = 1000.0f * u(rng);
r.on_ice_ring = pick(rng) == 4;
r.clipped = pick(rng) == 5;
r.overloaded = pick(rng) == 6;
frames[f].push_back(r);
}
}
return frames;
}
struct HostKey { int16_t h, k, l; float img; int32_t idx; float d; };
} // namespace
TEST_CASE("RotationIngestGPU_SortAndCutMatchHost", "[RotationScale][gpu]") {
if (get_gpu_count() == 0)
SKIP("no GPU");
const int n_frames = 60;
const auto frames = MakeFrames(n_frames, 17);
std::vector<const Reflection *> ptr(n_frames);
std::vector<int32_t> count(n_frames), src_start(n_frames + 1, 0);
for (int f = 0; f < n_frames; ++f) {
ptr[f] = frames[f].data();
count[f] = static_cast<int32_t>(frames[f].size());
src_start[f + 1] = src_start[f] + count[f];
}
const int n = src_start[n_frames];
std::vector<const Reflection *> all(n);
for (int f = 0; f < n_frames; ++f)
for (int j = 0; j < count[f]; ++j) all[src_start[f] + j] = &frames[f][j];
for (const auto &[d_min, d_max] : {std::pair<std::optional<double>, std::optional<double>>{std::nullopt, std::nullopt},
{1.4, 2.1}, {std::nullopt, 1.8}}) {
RotationScaleMergeGPU gpu(0);
gpu.IngestUpload(n_frames, ptr.data(), count.data(), 4);
// The flux meter, summed per frame in reflection order.
std::vector<double> mean(n_frames);
gpu.IngestMeanBackground(mean.data());
for (int f = 0; f < n_frames; ++f) {
double sum = 0.0;
int c = 0;
for (const auto &r : frames[f])
if (std::isfinite(r.bkg) && r.bkg > 0.0f) { sum += r.bkg; ++c; }
if (c > 0) CHECK(mean[f] == sum / c);
else CHECK(std::isnan(mean[f]));
}
std::vector<double> flux(n_frames);
for (int f = 0; f < n_frames; ++f) flux[f] = 0.9 + 0.01 * f;
const int n_keep = gpu.IngestSort(flux.data(), d_min, d_max);
// The host ingest: one total order, its raw-hkl runs, each with its first usable resolution, and
// the runs the range keeps with their observations.
std::vector<HostKey> key(n);
for (int i = 0; i < n; ++i)
key[i] = {static_cast<int16_t>(all[i]->h), static_cast<int16_t>(all[i]->k), static_cast<int16_t>(all[i]->l),
all[i]->image_number, i, all[i]->d};
std::sort(key.begin(), key.end(), [](const HostKey &a, const HostKey &b) {
if (a.h != b.h) return a.h < b.h;
if (a.k != b.k) return a.k < b.k;
if (a.l != b.l) return a.l < b.l;
if (a.img != b.img) return a.img < b.img;
return a.idx < b.idx;
});
const bool limited = d_min || d_max;
std::vector<uint8_t> keep(n, limited ? 0 : 1);
struct Run { int start, count; int32_t h, k, l; float d; };
std::vector<Run> runs;
int n_runs_all = 0;
for (int i = 0; i < n; ) {
int j = i;
float d = NAN;
while (j < n && key[j].h == key[i].h && key[j].k == key[i].k && key[j].l == key[i].l) {
if (!std::isfinite(d) && std::isfinite(key[j].d) && key[j].d > 0.0f) d = key[j].d;
++j;
}
++n_runs_all;
const bool in = !limited || (std::isfinite(d) && d > 0.0f && !(d_min && d < *d_min) && !(d_max && d > *d_max));
if (in) {
runs.push_back({i, j - i, key[i].h, key[i].k, key[i].l, d});
for (int p = i; p < j; ++p) keep[key[p].idx] = 1;
}
i = j;
}
std::vector<int32_t> new_idx(n, -1);
int at = 0;
for (int i = 0; i < n; ++i) if (keep[i]) new_idx[i] = at++;
REQUIRE(n_keep == at);
RotationScaleMergeGPU::IngestLayout L;
gpu.IngestGetLayout(L);
CHECK(L.n_reflections == n);
CHECK(L.n_runs_all == n_runs_all);
REQUIRE(L.run_start.size() == runs.size());
std::vector<int32_t> h(n_keep), k(n_keep), l(n_keep), perm(n_keep);
std::vector<float> dphi(n_keep), part(n_keep), img(n_keep), zeta(n_keep), d(n_keep), corr(n_keep),
I(n_keep), sigma(n_keep);
gpu.IngestGetObservations(h.data(), k.data(), l.data(), dphi.data(), part.data(), img.data(), zeta.data(),
d.data(), corr.data(), I.data(), sigma.data(), perm.data());
int at_run = 0;
for (size_t r = 0; r < runs.size(); ++r) {
CHECK(L.run_start[r] == at_run);
CHECK(L.run_count[r] == runs[r].count);
CHECK(L.run_h[r] == runs[r].h);
CHECK(L.run_k[r] == runs[r].k);
CHECK(L.run_l[r] == runs[r].l);
CHECK((L.run_d[r] == runs[r].d || (std::isnan(L.run_d[r]) && std::isnan(runs[r].d))));
for (int p = 0; p < runs[r].count; ++p)
CHECK(perm[at_run + p] == new_idx[key[runs[r].start + p].idx]);
at_run += runs[r].count;
}
for (int f = 0, s = 0; f < n_frames; ++f) {
int c = 0;
for (int j = 0; j < count[f]; ++j) c += keep[src_start[f] + j];
CHECK(L.frame_start[f] == s);
CHECK(L.frame_count[f] == c);
s += c;
}
for (int i = 0; i < n; ++i) {
if (!keep[i]) continue;
const int o = new_idx[i];
const Reflection &r = *all[i];
CHECK(h[o] == static_cast<int16_t>(r.h));
CHECK(I[o] == r.I);
CHECK(corr[o] == r.image_scale_corr);
CHECK(dphi[o] == r.delta_phi_deg);
CHECK(img[o] == r.image_number);
}
}
}
#endif