Files
Jungfraujoch/tests/LoadReferenceMtzFreeFlagsTest.cpp
T
leonarski_fandClaude Opus 5.5 a8c559bf6b tests: jfjoch_portable_test, quick [portable] checks for the macOS and Windows jobs
A Catch2 executable that builds in the portable configurations (JFJOCH_VIEWER_ONLY /
JFJOCH_RUGNUX_ONLY) and links only what those build - JFJochRugnux, JFJochReader,
JFJochImageAnalysis, JFJochWriter, JFJochCommon - so it can run on the macOS arm64 and
Windows x64 jobs, where the receiver/broker/FPGA/HLS sources are not built and there is no GPU.
EXCLUDE_FROM_ALL, so a product build does not pay for it; catch2 is now made available in the
portable configure as well (it is EXCLUDE_FROM_ALL too).

The cases tagged [portable] cover what depends on the architecture, the compiler or the
standard library: bitshuffle/LZ4/zstd, HDF5 read-back (legacy/VDS/integrated, the direct-chunk
path), miniCBF/marCCD/SMV header parsing, CBOR, CPU spot finding, azimuthal mapping, Bragg
prediction/integration, gemmi MTZ/mmCIF. New in tests/PortableTest.cpp:
- a golden FNV-1a hash of two frames of compression_benchmark.h5, decoded from the raw chunk by
  the hperf and the classic bitshuffle and through the HDF5 filter (x86 hashes
  affea29c511b6ec2 / e46913009c95a1f1);
- a golden hash of a bitshuffle/LZ4 encode (the writer must produce the same bytes everywhere);
- the shipped bitshuffle block selector against the classic reference over elem 1/2/4/8 and
  block tails;
- the FFTW indexer, named explicitly, on a synthetic orthorhombic lattice (the existing FFT
  indexer lattice tests run only under CUDA);
- a 4-frame end-to-end rugnux run on the git-LFS rotation dataset (HDF5 via external links, CPU
  spot finding, indexing, integration); SKIPs when LFS was not pulled.
All 45 take ~4 s on Linux (~2 s without the LFS case), CPU-only build.

M_PI replaced by PI (common/JFJochMath.h) in the two tagged files that used it, as MSVC does not
define M_PI. The CBF gzip test shells out to gzip and is left untagged.

CI: build and run jfjoch_portable_test "[portable]" in build-windows (both variants),
build-rugnux-windows, build-macos-viewer and build-rugnux-macos, after the build and before
packaging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
2026-09-27 23:44:53 +02:00

97 lines
4.0 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 <filesystem>
#include <map>
#include <tuple>
#include <vector>
#include <gemmi/mtz.hpp>
#include "../image_analysis/LoadFCalcFromMtz.h"
namespace {
// Write a small P4(3)2(1)2 MTZ with an IMEAN column and a FreeR_flag column whose free reflections
// carry `free_value` (and the rest its complement). ~1 in 10 reflections is flagged free.
std::string WriteMtzWithFreeR(const std::string& path, int free_value) {
gemmi::Mtz mtz(true);
mtz.spacegroup = gemmi::find_spacegroup_by_number(96);
mtz.set_cell_for_all(gemmi::UnitCell(78.0, 78.0, 37.0, 90.0, 90.0, 90.0));
mtz.add_dataset("test");
mtz.add_column("IMEAN", 'J', -1, -1, false);
mtz.add_column("FreeR_flag", 'I', -1, -1, false);
std::vector<float> data;
int nref = 0;
for (int h = 1; h <= 10; ++h)
for (int k = 1; k <= 10; ++k)
for (int l = 1; l <= 6; ++l) {
const bool is_free = ((h * 7 + k * 13 + l * 17) % 10 == 0);
const int flag = is_free ? free_value : (free_value == 0 ? 1 : 0);
data.insert(data.end(), {static_cast<float>(h), static_cast<float>(k),
static_cast<float>(l), 100.0f, static_cast<float>(flag)});
++nref;
}
mtz.nreflections = nref;
mtz.data = std::move(data);
mtz.write_to_file(path);
return path;
}
bool ExpectedFree(int h, int k, int l) { return (h * 7 + k * 13 + l * 17) % 10 == 0; }
}
TEST_CASE("LoadReferenceMtz imports FreeR (CCP4 convention: test set = 0)", "[reference_mtz][portable]") {
const auto path = (std::filesystem::temp_directory_path() / "rugnux_freer_ccp4.mtz").string();
WriteMtzWithFreeR(path, /*free_value=*/0);
const auto ref = LoadReferenceMtz(path);
std::filesystem::remove(path);
REQUIRE(ref.has_free_flags);
CHECK(ref.n_free > 0);
CHECK(ref.n_free < static_cast<int>(ref.reflections.size()) / 2); // free is the minority
for (const auto& r : ref.reflections)
CHECK(r.rfree_flag == ExpectedFree(r.h, r.k, r.l));
}
TEST_CASE("LoadReferenceMtz imports FreeR (phenix convention: test set = 1) via the complement", "[reference_mtz]") {
// Here flag 0 is the majority (work), so the loader must take the complement and treat 1 as free.
const auto path = (std::filesystem::temp_directory_path() / "rugnux_freer_phenix.mtz").string();
WriteMtzWithFreeR(path, /*free_value=*/1);
const auto ref = LoadReferenceMtz(path);
std::filesystem::remove(path);
REQUIRE(ref.has_free_flags);
CHECK(ref.n_free > 0);
CHECK(ref.n_free < static_cast<int>(ref.reflections.size()) / 2);
for (const auto& r : ref.reflections)
CHECK(r.rfree_flag == ExpectedFree(r.h, r.k, r.l));
}
TEST_CASE("LoadReferenceMtz without a FreeR column reports no free flags", "[reference_mtz]") {
const auto path = (std::filesystem::temp_directory_path() / "rugnux_no_freer.mtz").string();
gemmi::Mtz mtz(true);
mtz.spacegroup = gemmi::find_spacegroup_by_number(96);
mtz.set_cell_for_all(gemmi::UnitCell(78.0, 78.0, 37.0, 90.0, 90.0, 90.0));
mtz.add_dataset("test");
mtz.add_column("IMEAN", 'J', -1, -1, false);
std::vector<float> data;
int nref = 0;
for (int h = 1; h <= 6; ++h)
for (int k = 1; k <= 6; ++k)
for (int l = 1; l <= 4; ++l) {
data.insert(data.end(), {static_cast<float>(h), static_cast<float>(k),
static_cast<float>(l), 100.0f});
++nref;
}
mtz.nreflections = nref;
mtz.data = std::move(data);
mtz.write_to_file(path);
const auto ref = LoadReferenceMtz(path);
std::filesystem::remove(path);
CHECK_FALSE(ref.has_free_flags);
CHECK(ref.n_free == 0);
}