1.0.0-rc.174 (#84)
Build Packages / Create release (push) Successful in 17s
Build Packages / build:viewer:macos-arm64:nocuda (push) Successful in 3m22s
Build Packages / build:rugnux:macos-arm64:nocuda (push) Successful in 2m37s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m33s
Build Packages / build:rugnux:linux-x86_64:cuda (push) Successful in 10m39s
Build Packages / build:viewer:linux-x86_64:nocuda (push) Successful in 11m4s
Build Packages / build:viewer:linux-x86_64:cuda (push) Successful in 13m19s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 17m37s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 18m49s
Build Packages / build:viewer:windows-x86_64:nocuda (push) Successful in 19m10s
Build Packages / build:viewer:windows-x86_64:cuda (push) Successful in 24m26s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m31s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 18m54s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m45s
Build Packages / Generate python client (push) Successful in 37s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m20s
Build Packages / Build documentation (push) Successful in 1m32s
Build Packages / build:rugnux:windows-x86_64:cuda (push) Successful in 14m37s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m6s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 19m49s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 20m29s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 17m2s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 14m27s
Build Packages / Unit tests (push) Successful in 1h18m12s

* Rugnux: Performance improvements on GPU and CPU (more of the pre-scan and of scaling on the GPU, faster CPU spot finding and crystal refinement), with unchanged results.
* Rugnux: More robust processing - patches of persistently hot pixels are masked, an inconsistent merge triggers a retry at the measured beam centre, and builds targeting different CPU levels give the same results.
* Rugnux: Improved scaling and merging - reflections with an overloaded pixel are dropped, as in XDS, sparse rotation sweeps are scaled more reliably, and French-Wilson amplitudes use an anisotropic Wilson prior.
* Rugnux: Improved space-group determination - glide planes in groups without a centre of symmetry, screw axes from short or weak axial rows kept when a higher group is adopted, and more reliable decisions on twinned and pseudo-symmetric crystals.
* Rugnux: Improved small-molecule processing - spots that grow wider than the integration disk and split spots are integrated over their measured footprint, sparse lattices are integrated on every frame, and the `.hkl` file holds unmerged scaled reflections (SHELX HKLF 4).
* Rugnux: Reads Rigaku d*TREK SMV images (Saturn CCD), including detector 2theta and encoded pixel overflows; home-source (rotating-anode) datasets were added to the validation battery.
* jfjoch_viewer: Fixed processing failing at the end with "Wrong JPEG library version" on Linux; the merge window shows the space group with proper subscripts and a checklist of crystal pathologies.

Reviewed-on: #84
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
This commit was merged in pull request #84.
This commit is contained in:
2026-10-06 14:03:18 +02:00
committed by leonarski_f
parent 84228bf8be
commit a395f358ef
292 changed files with 11586 additions and 2438 deletions
+45 -10
View File
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include <algorithm>
#include "../common/CUDAWrapper.h"
#ifdef JFJOCH_USE_CUDA
@@ -146,10 +148,16 @@ double CompareCpuVsGpu(IntegratorMode mode, std::optional<float> bandwidth_fwhm,
float stencil_k = 0.0f,
float r1 = 0.0f, float r2 = 0.0f, float r3 = 0.0f,
OverlapMode overlap = OverlapMode::Off, float companion_dx = 0.0f,
bool clip_spots = false, float odd_partiality = 1.0f) {
const DiffractionExperiment experiment =
bool clip_spots = false, float odd_partiality = 1.0f,
const SpotFootprint *footprint = nullptr) {
DiffractionExperiment experiment =
MakeExperiment(mode, bandwidth_fwhm, clip_nsigma, radial, DetJF(2), stencil_k, r1, r2, r3,
overlap);
if (footprint) {
BraggIntegrationSettings settings = experiment.GetBraggIntegrationSettings();
settings.Footprint(*footprint);
experiment.ImportBraggIntegrationSettings(settings);
}
const size_t width = experiment.GetXPixelsNum();
const size_t height = experiment.GetYPixelsNum();
const size_t npixel = experiment.GetPixelsNum();
@@ -165,7 +173,7 @@ double CompareCpuVsGpu(IntegratorMode mode, std::optional<float> bandwidth_fwhm,
ImagePreprocessorBuffer cpu_image(npixel);
for (size_t i = 0; i < npixel; ++i)
cpu_image[i] = scene.image[i];
BraggIntegrationEngineCPU cpu(experiment);
BraggIntegrationEngineCPU cpu(experiment, PixelMask(experiment));
const auto out_cpu = cpu.Run(cpu_image, scene.predicted, scene.predicted.size(), 5);
// GPU under test, identical input uploaded to the device
@@ -175,7 +183,7 @@ double CompareCpuVsGpu(IntegratorMode mode, std::optional<float> bandwidth_fwhm,
gpu_image[i] = scene.image[i];
REQUIRE(cudaMemcpyAsync(gpu_image.getGPUBuffer(), gpu_image.getBuffer().data(),
npixel * sizeof(int32_t), cudaMemcpyHostToDevice, *stream) == cudaSuccess);
BraggIntegrationEngineGPU gpu(experiment, stream);
BraggIntegrationEngineGPU gpu(experiment, stream, PixelMask(experiment));
const auto out_gpu = gpu.Run(gpu_image, scene.predicted, scene.predicted.size(), 5);
// The ok/observed decisions are deterministic geometry, so both engines return the same set in
@@ -192,15 +200,20 @@ double CompareCpuVsGpu(IntegratorMode mode, std::optional<float> bandwidth_fwhm,
ImagePreprocessorBuffer clean_image(npixel);
for (size_t i = 0; i < npixel; ++i)
clean_image[i] = clean_scene.image[i];
BraggIntegrationEngineCPU clean_cpu(experiment);
BraggIntegrationEngineCPU clean_cpu(experiment, PixelMask(experiment));
const auto out_clean = clean_cpu.Run(clean_image, clean_scene.predicted,
clean_scene.predicted.size(), 5);
CHECK(out_cpu.size() < out_clean.size());
// An unreadable pixel the (empty) mask does not explain reads as an overload, which keeps the
// reflection, flagged - so the cost shows in the measured ones.
const auto measured = std::count_if(out_cpu.begin(), out_cpu.end(),
[](const Reflection &r) { return !r.overloaded; });
CHECK(static_cast<size_t>(measured) < out_clean.size());
}
for (size_t i = 0; i < out_cpu.size(); ++i) {
INFO("mode " << static_cast<int>(mode) << " reflection " << i << " hkl " << out_cpu[i].h);
CHECK(out_gpu[i].h == out_cpu[i].h);
CHECK(out_gpu[i].image_number == out_cpu[i].image_number);
CHECK(out_gpu[i].overloaded == out_cpu[i].overloaded);
CHECK(out_gpu[i].bkg == Catch::Approx(out_cpu[i].bkg).epsilon(0.02).margin(0.5));
CHECK(out_gpu[i].I == Catch::Approx(out_cpu[i].I).epsilon(0.03).margin(2.0));
CHECK(out_gpu[i].sigma == Catch::Approx(out_cpu[i].sigma).epsilon(0.03).margin(0.5));
@@ -254,6 +267,28 @@ TEST_CASE("BraggIntegrationEngineGPU_MatchesCPU") {
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, 0.04f, 0.0f, false, 120, 4.0f, 6.0f, 8.0f, 12.0f);
}
SECTION("ProfileEmpirical") { CompareCpuVsGpu(IntegratorMode::ProfileEmpirical, std::nullopt); }
// A measured footprint wider than the r1 disk (SpotFootprint.h): the summation ellipse, the ring
// grown along and across the radius and the footprint-width Gaussian are all reflection-dependent
// geometry the two engines have to classify alike - spaced so the rings stay clear, and crowded
// so the grown neighbour mask and the ring gate come into play.
SpotFootprint fp;
fp.bin_px = 100.0f;
for (int b = 0; b < 8; ++b) {
fp.sigma_rad.push_back(1.5f + 0.2f * b);
fp.sigma_tan.push_back(1.8f + 0.3f * b);
}
SECTION("ProfileGaussian footprint") {
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, std::nullopt, 4.0f, false, 120, 0.0f,
0.0f, 0.0f, 0.0f, OverlapMode::Off, 0.0f, false, 1.0f, &fp);
}
SECTION("ProfileGaussian footprint crowded exclude") {
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, std::nullopt, 4.0f, false, 40, 0.0f,
0.0f, 0.0f, 0.0f, OverlapMode::Exclude, 0.0f, false, 1.0f, &fp);
}
SECTION("ProfileGaussian footprint radial") {
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, std::nullopt, 4.0f, true, 120, 0.0f,
0.0f, 0.0f, 0.0f, OverlapMode::Off, 0.0f, false, 1.0f, &fp);
}
// Overlap treatment: companions 4 px apart put each reflection's centre inside its neighbour's
// signal disk, so the owner map, the excluded pixels and the profile fraction the two modes act on
// all have to come out the same in both engines - the ownership atomic in particular is settled by
@@ -358,11 +393,11 @@ TEST_CASE("BraggIntegrationEngineGPU_ReusedEngineMatchesFresh") {
};
auto stream_fresh = std::make_shared<CudaStream>();
BraggIntegrationEngineGPU fresh(experiment, stream_fresh);
BraggIntegrationEngineGPU fresh(experiment, stream_fresh, PixelMask(experiment));
const auto out_fresh = integrate(fresh, second, stream_fresh);
auto stream_reused = std::make_shared<CudaStream>();
BraggIntegrationEngineGPU reused(experiment, stream_reused);
BraggIntegrationEngineGPU reused(experiment, stream_reused, PixelMask(experiment));
integrate(reused, first, stream_reused);
const auto out_reused = integrate(reused, second, stream_reused);
@@ -397,7 +432,7 @@ TEST_CASE("BraggIntegrationEngineGPU_Benchmark", "[.][bragg_bench]") {
REQUIRE(npixel == width * height);
auto stream = std::make_shared<CudaStream>();
BraggIntegrationEngineGPU gpu(experiment, stream);
BraggIntegrationEngineGPU gpu(experiment, stream, PixelMask(experiment));
for (int spacing : {28, 60}) {
const Scene scene = BuildScene(width, height, spacing);
const size_t nrefl = scene.predicted.size();
@@ -418,7 +453,7 @@ TEST_CASE("BraggIntegrationEngineGPU_Benchmark", "[.][bragg_bench]") {
const auto t1 = std::chrono::steady_clock::now();
const double ms = std::chrono::duration<double, std::milli>(t1 - t0).count() / iters;
BraggIntegrationEngineCPU cpu(experiment);
BraggIntegrationEngineCPU cpu(experiment, PixelMask(experiment));
ImagePreprocessorBuffer cpu_image(npixel);
for (size_t i = 0; i < npixel; ++i) cpu_image[i] = scene.image[i];
const auto c0 = std::chrono::steady_clock::now();