Files
Jungfraujoch/tests/LoadReferenceSfCifTest.cpp
T
leonarski_fandClaude Opus 5.5 ae444437f4 rugnux: accept a PDB structure-factor mmCIF as the -z reference
-z (now also --reference; --reference-mtz still works) takes an SF-mmCIF
(e.g. a deposited -sf.cif, gzipped or not) as well as an MTZ. The format is
recognised by content: a file starting with "MTZ " is an MTZ, anything else
is parsed as CIF. The first merged reflection block with the requested
column (or, by default, one the auto choice accepts) is converted to a
gemmi::Mtz in memory with GEMMI's CifToMtz and then read by the unchanged
MTZ loader, so the in-memory reference is exactly what the MTZ path yields.
Unmerged (_diffrn_refln) and anomalous-only blocks are passed over; the log
names the block used.

The R-free set comes from _refln.status (f -> FreeR_flag 0, o -> 1: the
CCP4 convention the loader already reads) and is preferred to
_refln.pdbx_r_free_flag, whose convention varies by program; a status
column with no 'f' is ignored and pdbx_r_free_flag is used instead.

Checked on two open-arm sets in the deposited setting (one with F_meas_au
only, one with intensity_meas): reference loaded with the deposited cell and
group, the inherited free set agrees with the deposited status 'f' on every
common reflection, and the merge correlates at CC 0.99 with the deposition.

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

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]") {
// 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);
}