Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m11s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m32s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m26s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m11s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 23m32s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 21m45s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m20s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m36s
Build Packages / Unit tests (push) Successful in 2h7m18s
Build Packages / Validation Battery (push) Successful in 33m19s
* Rugnux: Stills processing needs only the unit cell and space group beyond what the file records - detector distance, beam centre, tilt, panel placement, saturation level, gain and polarisation plane are found from the images - and is up to twice as fast. * Rugnux: Stills can be indexed without a GPU, with a CPU implementation of the fast-feedback indexer (macOS, computers without a GPU). * Rugnux: Rotation and stills data are integrated in the detector's own units with one gain model, improving data from CCD detectors. * Web frontend: The Rugnux live page shows the placed model with its electron density and anomalous difference map in a Mol* viewer. Reviewed-on: #90 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
265 lines
11 KiB
C++
265 lines
11 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 <chrono>
|
|
#include <cmath>
|
|
#include <fstream>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <thread>
|
|
|
|
#include <Eigen/Geometry>
|
|
|
|
#include "../common/CUDAWrapper.h"
|
|
#include "../common/DiffractionExperiment.h"
|
|
#include "../image_analysis/indexing/FFBIDXIndexerCPU.h"
|
|
|
|
#ifdef JFJOCH_USE_CUDA
|
|
#include "../image_analysis/indexing/FFBIDXIndexer.h"
|
|
#endif
|
|
|
|
namespace {
|
|
// A still of a test cell in a fixed random orientation: the reciprocal lattice points to 2 A that lie
|
|
// within 0.001 1/A of the Ewald sphere of a 1 A beam along z, and then random spots in the same
|
|
// resolution range. The seed is fixed, and the engine scaled by hand, so the frame is the same
|
|
// with every standard library.
|
|
std::vector<Coord> SyntheticStill(const UnitCell &uc, uint32_t seed, size_t noise_spots) {
|
|
std::mt19937 rng(seed);
|
|
auto draw = [&rng]() {
|
|
return 2.0f * static_cast<float>(rng()) / static_cast<float>(std::mt19937::max()) - 1.0f;
|
|
};
|
|
const Eigen::Matrix3f rot = Eigen::Quaternionf(draw(), draw(), draw(), draw()).normalized().toRotationMatrix();
|
|
|
|
const CrystalLattice l(uc);
|
|
auto rotate = [&rot](const Coord &v) {
|
|
const Eigen::Vector3f x = rot * Eigen::Vector3f(v.x, v.y, v.z);
|
|
return Coord(x.x(), x.y(), x.z());
|
|
};
|
|
const Coord astar = rotate(l.Astar()), bstar = rotate(l.Bstar()), cstar = rotate(l.Cstar());
|
|
|
|
const Coord k0(0, 0, 1.0f);
|
|
std::vector<Coord> spots;
|
|
for (int h = -40; h <= 40; h++)
|
|
for (int k = -40; k <= 40; k++)
|
|
for (int m = -40; m <= 40; m++) {
|
|
const Coord r = h * astar + k * bstar + m * cstar;
|
|
if (r.Length() > 0.5f || r.Length() < 0.02f)
|
|
continue;
|
|
if (std::abs((r + k0).Length() - 1.0f) < 0.001f)
|
|
spots.push_back(r);
|
|
}
|
|
while (noise_spots > 0) {
|
|
const Coord r(0.5f * draw(), 0.5f * draw(), 0.5f * draw());
|
|
if (r.Length() > 0.5f || r.Length() < 0.02f)
|
|
continue;
|
|
spots.push_back(r);
|
|
noise_spots--;
|
|
}
|
|
return spots;
|
|
}
|
|
|
|
// Fraction of spots within 0.1 of a lattice point (in Miller index units)
|
|
float IndexedFraction(const std::vector<Coord> &spots, const CrystalLattice &l) {
|
|
size_t n = 0;
|
|
for (const auto &s: spots) {
|
|
const float h = s * l.Vec0(), k = s * l.Vec1(), m = s * l.Vec2();
|
|
const float dh = h - std::round(h), dk = k - std::round(k), dm = m - std::round(m);
|
|
if (dh * dh + dk * dk + dm * dm < 0.01f)
|
|
n++;
|
|
}
|
|
return static_cast<float>(n) / static_cast<float>(spots.size());
|
|
}
|
|
|
|
bool SameLengths(const UnitCell &a, const UnitCell &b, float rel_tol) {
|
|
std::array<double, 3> x = {a.a, a.b, a.c}, y = {b.a, b.b, b.c};
|
|
std::sort(x.begin(), x.end());
|
|
std::sort(y.begin(), y.end());
|
|
for (int i = 0; i < 3; i++)
|
|
if (std::abs(x[i] - y[i]) > rel_tol * y[i])
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// The cell vectors as rows
|
|
Eigen::Matrix3f CellMatrix(const UnitCell &uc) {
|
|
const CrystalLattice l(uc);
|
|
Eigen::Matrix3f m;
|
|
m << l.Vec0().x, l.Vec0().y, l.Vec0().z, l.Vec1().x, l.Vec1().y, l.Vec1().z,
|
|
l.Vec2().x, l.Vec2().y, l.Vec2().z;
|
|
return m;
|
|
}
|
|
|
|
const std::vector<UnitCell> &TestCells() {
|
|
static const std::vector<UnitCell> cells = {
|
|
{30, 40, 50, 90, 90, 90},
|
|
{80, 80, 90, 90, 90, 120},
|
|
{40, 45, 80, 90, 82.5, 90},
|
|
{60, 60, 60, 90, 90, 90}
|
|
};
|
|
return cells;
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Portable_FFBIDXIndexerCPU_SyntheticStill", "[portable][Indexing]") {
|
|
for (size_t i = 0; i < TestCells().size(); i++) {
|
|
const UnitCell &uc = TestCells()[i];
|
|
const auto lattice_spots = SyntheticStill(uc, 1234 + i, 0);
|
|
auto spots = SyntheticStill(uc, 1234 + i, lattice_spots.size() / 5);
|
|
INFO("cell " << i << ", " << lattice_spots.size() << " lattice spots");
|
|
REQUIRE(lattice_spots.size() > 30);
|
|
|
|
DiffractionExperiment experiment;
|
|
experiment.SetUnitCell(uc);
|
|
FFBIDXIndexerCPU indexer;
|
|
indexer.Setup(experiment);
|
|
const auto result = indexer.Run(spots);
|
|
REQUIRE(!result.lattice.empty());
|
|
CHECK(SameLengths(result.lattice[0].GetUnitCell(), uc, 0.01f));
|
|
CHECK(IndexedFraction(lattice_spots, result.lattice[0]) > 0.95f);
|
|
}
|
|
}
|
|
|
|
// Time per call of the CPU indexer, alone and with Refine(), on one thread and on 16 at once
|
|
TEST_CASE("FFBIDXIndexerCPU_Benchmark", "[.][ffbidx_bench]") {
|
|
const UnitCell uc(40, 45, 80, 90, 82.5, 90);
|
|
DiffractionExperiment experiment;
|
|
experiment.SetUnitCell(uc);
|
|
|
|
for (const size_t n: {30, 80, 250}) {
|
|
auto spots = SyntheticStill(uc, 99, 0);
|
|
const auto lattice_spots = spots.size();
|
|
spots = SyntheticStill(uc, 99, n > lattice_spots ? n - lattice_spots : 0);
|
|
spots.resize(n);
|
|
|
|
const int calls = 20;
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
for (int i = 0; i < calls; i++)
|
|
FFBIDXFindCells(CellMatrix(uc), spots, n);
|
|
const double find_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count() / calls;
|
|
|
|
FFBIDXIndexerCPU indexer;
|
|
indexer.Setup(experiment);
|
|
t0 = std::chrono::steady_clock::now();
|
|
for (int i = 0; i < calls; i++)
|
|
indexer.Run(spots);
|
|
const double run_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count() / calls;
|
|
|
|
const int threads = 16;
|
|
t0 = std::chrono::steady_clock::now();
|
|
std::vector<std::thread> workers;
|
|
for (int t = 0; t < threads; t++)
|
|
workers.emplace_back([&] {
|
|
FFBIDXIndexerCPU w;
|
|
w.Setup(experiment);
|
|
for (int i = 0; i < calls; i++)
|
|
w.Run(spots);
|
|
});
|
|
for (auto &w: workers)
|
|
w.join();
|
|
const double wall_s = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
|
|
|
WARN(n << " spots: FindCells " << find_ms << " ms, with Refine " << run_ms << " ms; "
|
|
<< threads << " threads " << threads * calls / wall_s << " calls/s");
|
|
}
|
|
}
|
|
|
|
#ifdef JFJOCH_USE_CUDA
|
|
|
|
namespace {
|
|
// The GPU library's raw output, before Refine(), with the configuration FFBIDXIndexer uses
|
|
FFBIDXCells RunGPU(const Eigen::Matrix3f &cell, const std::vector<Coord> &spots) {
|
|
fast_feedback::config_runtime<float> crt{};
|
|
fast_feedback::config_persistent<float> cpers{
|
|
.max_output_cells = 32,
|
|
.max_input_cells = 1,
|
|
.max_spots = static_cast<unsigned>(spots.size()),
|
|
.num_candidate_vectors = 32,
|
|
.redundant_computations = true,
|
|
};
|
|
fast_feedback::refine::indexer<float> indexer{cpers, crt};
|
|
indexer.iCellM() = cell;
|
|
for (size_t i = 0; i < spots.size(); i++) {
|
|
indexer.spotX(i) = spots[i].x;
|
|
indexer.spotY(i) = spots[i].y;
|
|
indexer.spotZ(i) = spots[i].z;
|
|
}
|
|
indexer.index(1, spots.size());
|
|
return {indexer.oCellM(), indexer.oScoreV()};
|
|
}
|
|
|
|
// Cell (rows) and spots of one of the library's test files: the first line is the cell, then a
|
|
// spot per line; '#' starts a comment
|
|
std::pair<Eigen::Matrix3f, std::vector<Coord>> ReadSimpleData(const std::string &path) {
|
|
std::ifstream f(path);
|
|
REQUIRE(f.good());
|
|
std::vector<float> v;
|
|
std::string line;
|
|
while (std::getline(f, line)) {
|
|
if (line.empty() || line[0] == '#')
|
|
continue;
|
|
std::istringstream s(line);
|
|
float x;
|
|
while (s >> x)
|
|
v.push_back(x);
|
|
}
|
|
REQUIRE(v.size() >= 12);
|
|
Eigen::Matrix3f cell;
|
|
cell << v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8];
|
|
std::vector<Coord> spots;
|
|
for (size_t i = 9; i + 2 < v.size(); i += 3)
|
|
spots.emplace_back(v[i], v[i + 1], v[i + 2]);
|
|
return {cell, spots};
|
|
}
|
|
|
|
float RelativeDifference(const Eigen::Matrix3f &a, const Eigen::Matrix3f &b) {
|
|
return (a - b).norm() / b.norm();
|
|
}
|
|
|
|
// CPU and GPU agree when the best scores agree and the CPU's best cell is one of the GPU's best
|
|
// cells - the samples are the same, so a matching cell agrees to rounding, and two cells within
|
|
// rounding of each other in score may come out in either order
|
|
void CompareWithGPU(const std::string &name, const Eigen::Matrix3f &cell, const std::vector<Coord> &spots) {
|
|
const auto gpu = RunGPU(cell, spots);
|
|
const auto cpu = FFBIDXFindCells(cell, spots, spots.size());
|
|
|
|
float best_match = INFINITY;
|
|
for (int k = 0; k < gpu.scores.size(); k++)
|
|
best_match = std::min(best_match, RelativeDifference(cpu.cells.block<3, 3>(0, 0), gpu.cells.block<3, 3>(3 * k, 0)));
|
|
|
|
int top_matched = 0;
|
|
for (int j = 0; j < cpu.scores.size(); j++)
|
|
for (int k = 0; k < gpu.scores.size(); k++)
|
|
if (RelativeDifference(cpu.cells.block<3, 3>(3 * j, 0), gpu.cells.block<3, 3>(3 * k, 0)) < 1e-3f) {
|
|
top_matched++;
|
|
break;
|
|
}
|
|
|
|
UNSCOPED_INFO(name << ": " << spots.size() << " spots, score CPU " << cpu.scores(0) << " GPU " << gpu.scores(0)
|
|
<< ", best cell to GPU " << best_match << ", " << top_matched << "/32 cells matched");
|
|
CHECK(std::abs(cpu.scores(0) - gpu.scores(0)) <= 1e-3f * std::abs(gpu.scores(0)));
|
|
CHECK(best_match < 1e-3f);
|
|
CHECK(top_matched >= 24);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("FFBIDXIndexerCPU_MatchesGPU", "[Indexing][GPU]") {
|
|
if (get_gpu_count() == 0)
|
|
SKIP("no GPU");
|
|
|
|
for (int i = 0; i < 9; i++) {
|
|
for (const std::string kind: {"local", "peakfinder8", "radial"}) {
|
|
const std::string name = "image" + std::to_string(i) + "_" + kind + ".txt";
|
|
const auto [cell, spots] = ReadSimpleData(std::string(FFBIDX_SIMPLE_DATA_DIR) + "/" + name);
|
|
CompareWithGPU(name, cell, spots);
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < TestCells().size(); i++) {
|
|
const auto spots = SyntheticStill(TestCells()[i], 1234 + i, 10);
|
|
CompareWithGPU("synthetic " + std::to_string(i), CellMatrix(TestCells()[i]), spots);
|
|
}
|
|
}
|
|
|
|
#endif
|