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
154 lines
4.9 KiB
C++
154 lines
4.9 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 <fstream>
|
|
#include <string>
|
|
|
|
#include <zlib.h>
|
|
|
|
#include "../image_analysis/LoadFCalcFromMtz.h"
|
|
|
|
namespace {
|
|
// A synthetic structure-factor mmCIF in the layout of a PDB deposition: an unmerged block first
|
|
// (which the loader must pass over), then the merged block with intensities, sigmas and the
|
|
// _refln.status test-set marks ('f' free, 'o' working, '-' neither). Cell and space group are
|
|
// synthetic.
|
|
const char* SF_CIF = R"(data_r0testsf_unmerged
|
|
_cell.length_a 50.0
|
|
_cell.length_b 60.0
|
|
_cell.length_c 70.0
|
|
_cell.angle_alpha 90.0
|
|
_cell.angle_beta 90.0
|
|
_cell.angle_gamma 90.0
|
|
_symmetry.space_group_name_H-M 'P 21 21 21'
|
|
loop_
|
|
_diffrn_refln.index_h
|
|
_diffrn_refln.index_k
|
|
_diffrn_refln.index_l
|
|
_diffrn_refln.intensity_net
|
|
_diffrn_refln.intensity_sigma
|
|
1 0 0 5.0 1.0
|
|
1 0 0 6.0 1.0
|
|
#
|
|
data_r0testsf
|
|
_cell.length_a 50.0
|
|
_cell.length_b 60.0
|
|
_cell.length_c 70.0
|
|
_cell.angle_alpha 90.0
|
|
_cell.angle_beta 90.0
|
|
_cell.angle_gamma 90.0
|
|
_symmetry.space_group_name_H-M 'P 21 21 21'
|
|
loop_
|
|
_refln.index_h
|
|
_refln.index_k
|
|
_refln.index_l
|
|
_refln.status
|
|
_refln.intensity_meas
|
|
_refln.intensity_sigma
|
|
0 0 2 o 100.0 5.0
|
|
0 1 1 f 200.0 6.0
|
|
1 1 1 o 300.0 7.0
|
|
1 2 3 f 400.0 8.0
|
|
2 2 2 - 500.0 9.0
|
|
3 1 4 o ? ?
|
|
)";
|
|
|
|
std::string WriteFile(const std::string& name, const std::string& content) {
|
|
const auto path = (std::filesystem::temp_directory_path() / name).string();
|
|
std::ofstream(path) << content;
|
|
return path;
|
|
}
|
|
|
|
std::string WriteGzFile(const std::string& name, const std::string& content) {
|
|
const auto path = (std::filesystem::temp_directory_path() / name).string();
|
|
gzFile f = gzopen(path.c_str(), "wb");
|
|
gzwrite(f, content.data(), static_cast<unsigned>(content.size()));
|
|
gzclose(f);
|
|
return path;
|
|
}
|
|
|
|
void CheckSyntheticReference(const ReferenceMtzData& ref) {
|
|
CHECK(ref.source == "SF-mmCIF block r0testsf");
|
|
CHECK(ref.used_column == "IMEAN");
|
|
CHECK_FALSE(ref.squared);
|
|
REQUIRE(ref.cell.has_value());
|
|
CHECK(ref.cell->a == Catch::Approx(50.0));
|
|
CHECK(ref.cell->b == Catch::Approx(60.0));
|
|
CHECK(ref.cell->c == Catch::Approx(70.0));
|
|
REQUIRE(ref.space_group_number.has_value());
|
|
CHECK(*ref.space_group_number == 19);
|
|
|
|
// The reflection with no intensity ('?') is not a reference reflection.
|
|
REQUIRE(ref.reflections.size() == 5);
|
|
CHECK(ref.reflections[1].h == 0);
|
|
CHECK(ref.reflections[1].k == 1);
|
|
CHECK(ref.reflections[1].l == 1);
|
|
CHECK(ref.reflections[1].I == Catch::Approx(200.0));
|
|
CHECK(ref.reflections[4].I == Catch::Approx(500.0));
|
|
|
|
// 'f' is the test set; 'o' and '-' are not.
|
|
REQUIRE(ref.has_free_flags);
|
|
CHECK(ref.n_free == 2);
|
|
CHECK_FALSE(ref.reflections[0].rfree_flag);
|
|
CHECK(ref.reflections[1].rfree_flag);
|
|
CHECK_FALSE(ref.reflections[2].rfree_flag);
|
|
CHECK(ref.reflections[3].rfree_flag);
|
|
CHECK_FALSE(ref.reflections[4].rfree_flag);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("LoadReferenceMtz reads a structure-factor mmCIF", "[reference_mtz][portable]") {
|
|
// Named without a .cif extension: the format is recognised by content.
|
|
const auto path = WriteFile("rugnux_ref_sf.dat", SF_CIF);
|
|
const auto ref = LoadReferenceMtz(path);
|
|
std::filesystem::remove(path);
|
|
CheckSyntheticReference(ref);
|
|
}
|
|
|
|
TEST_CASE("LoadReferenceMtz reads a gzipped structure-factor mmCIF", "[reference_mtz]") {
|
|
const auto path = WriteGzFile("rugnux_ref_sf.cif.gz", SF_CIF);
|
|
const auto ref = LoadReferenceMtz(path);
|
|
std::filesystem::remove(path);
|
|
CheckSyntheticReference(ref);
|
|
}
|
|
|
|
TEST_CASE("LoadReferenceMtz SF-mmCIF: amplitudes and pdbx_r_free_flag when status marks no test set", "[reference_mtz]") {
|
|
// Only amplitudes: FP is squared to an intensity. The status column has no 'f', so the free set
|
|
// comes from pdbx_r_free_flag (here 1 marks free, as phenix writes it - the minority value).
|
|
const auto path = WriteFile("rugnux_ref_sf_fp.cif", R"(data_r0testsf
|
|
_cell.length_a 50.0
|
|
_cell.length_b 60.0
|
|
_cell.length_c 70.0
|
|
_cell.angle_alpha 90.0
|
|
_cell.angle_beta 90.0
|
|
_cell.angle_gamma 90.0
|
|
_symmetry.space_group_name_H-M 'P 21 21 21'
|
|
loop_
|
|
_refln.index_h
|
|
_refln.index_k
|
|
_refln.index_l
|
|
_refln.status
|
|
_refln.pdbx_r_free_flag
|
|
_refln.F_meas_au
|
|
_refln.F_meas_sigma_au
|
|
0 0 2 o 0 10.0 1.0
|
|
0 1 1 o 1 20.0 1.0
|
|
1 1 1 o 0 30.0 1.0
|
|
1 2 3 o 0 40.0 1.0
|
|
2 2 2 o 0 50.0 1.0
|
|
)");
|
|
const auto ref = LoadReferenceMtz(path);
|
|
std::filesystem::remove(path);
|
|
|
|
CHECK(ref.used_column == "FP");
|
|
CHECK(ref.squared);
|
|
REQUIRE(ref.reflections.size() == 5);
|
|
CHECK(ref.reflections[2].I == Catch::Approx(900.0));
|
|
REQUIRE(ref.has_free_flags);
|
|
CHECK(ref.n_free == 1);
|
|
CHECK(ref.reflections[1].rfree_flag);
|
|
}
|