Reflection files record their provenance: commit, command line, input file
The merged .mtz and the unmerged .mtz carry, after the existing "From Rugnux <version>" record, HISTORY records with the git commit and date, the command line, the input file (absolute path) and a cheap identity of it - size and modification time, the master file's for HDF5, nothing hashed - and the collection date. MTZ allows 30 records of 80 characters; a longer value runs on over records starting with '+', cut so that no record ends in a blank a reader would strip. The mmCIF names the commit in _software.description and the command line, input file and identity under _jfjoch_provenance (the local prefix the other private items use; no PDBx item holds them), the first two as text fields so any character survives. OutputProvenance is filled once in the CLI and travels in ProcessConfig to every writer of a reflection file. Reflection data are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
@@ -47,7 +47,7 @@ TEST_CASE("Merged MTZ: the data dataset is id 1 and carries the wavelength", "[w
|
||||
|
||||
const auto experiment = TestExperiment();
|
||||
const auto path = (std::filesystem::temp_directory_path() / "rugnux_merged_wavelength.mtz").string();
|
||||
WriteMtzReflections(reflections, TETRAGONAL_CELL, experiment, path);
|
||||
WriteMtzReflections(reflections, TETRAGONAL_CELL, experiment, OutputProvenance{}, path);
|
||||
const gemmi::Mtz mtz = gemmi::read_mtz_file(path);
|
||||
std::filesystem::remove(path);
|
||||
|
||||
@@ -108,7 +108,7 @@ TEST_CASE("Unmerged MTZ: LP is Lorentz-polarization, QE the sensor efficiency, F
|
||||
}
|
||||
|
||||
const auto path = (std::filesystem::temp_directory_path() / "rugnux_unmerged_qe.mtz").string();
|
||||
WriteUnmergedMtzReflections({outcome}, TETRAGONAL_CELL, experiment, false, path);
|
||||
WriteUnmergedMtzReflections({outcome}, TETRAGONAL_CELL, experiment, false, OutputProvenance{}, path);
|
||||
const gemmi::Mtz mtz = gemmi::read_mtz_file(path);
|
||||
std::filesystem::remove(path);
|
||||
|
||||
@@ -180,8 +180,10 @@ TEST_CASE("Unmerged MTZ: built on several workers, the same rows as on one", "[w
|
||||
|
||||
for (bool sum_partials : {true, false}) {
|
||||
INFO("sum_partials " << sum_partials);
|
||||
const gemmi::Mtz serial = UnmergedMtz(outcomes, TETRAGONAL_CELL, experiment, sum_partials, 1);
|
||||
const gemmi::Mtz parallel = UnmergedMtz(outcomes, TETRAGONAL_CELL, experiment, sum_partials, 8);
|
||||
const gemmi::Mtz serial = UnmergedMtz(outcomes, TETRAGONAL_CELL, experiment, sum_partials,
|
||||
OutputProvenance{}, 1);
|
||||
const gemmi::Mtz parallel = UnmergedMtz(outcomes, TETRAGONAL_CELL, experiment, sum_partials,
|
||||
OutputProvenance{}, 8);
|
||||
REQUIRE(serial.nreflections > 100000);
|
||||
REQUIRE(parallel.nreflections == serial.nreflections);
|
||||
CHECK(std::memcmp(parallel.data.data(), serial.data.data(), serial.data.size() * sizeof(float)) == 0);
|
||||
@@ -260,6 +262,9 @@ TEST_CASE("Merged mmCIF: PDBx keys, merging R-factors and the scaled unmerged bl
|
||||
};
|
||||
OutputProvenance provenance;
|
||||
provenance.collection_date = "2026-07-13T13:48:52.413Z";
|
||||
provenance.command_line = "rugnux -o 'my run' /data/x_master.h5";
|
||||
provenance.input_file = "/data/x_master.h5";
|
||||
provenance.input_identity = "1234 bytes, modified 2026-07-13T14:00:00.000Z";
|
||||
|
||||
const auto path = (std::filesystem::temp_directory_path() / "rugnux_merged_test.cif").string();
|
||||
WriteMmcifReflections(reflections, TETRAGONAL_CELL, experiment, statistics, ErrorModelReport{},
|
||||
@@ -278,6 +283,12 @@ TEST_CASE("Merged mmCIF: PDBx keys, merging R-factors and the scaled unmerged bl
|
||||
== Catch::Approx(std::sqrt(2 * 0.98 / 1.98)).margin(1e-4));
|
||||
CHECK(*merged.find_value("_diffrn.ambient_temp") == "100.0");
|
||||
CHECK(*merged.find_value("_diffrn_detector.pdbx_collection_date") == "2026-07-13");
|
||||
// Provenance: the command line survives its own quotes, as a text field.
|
||||
CHECK(gemmi::cif::as_string(*merged.find_value("_jfjoch_provenance.command_line"))
|
||||
== provenance.command_line);
|
||||
CHECK(gemmi::cif::as_string(*merged.find_value("_jfjoch_provenance.input_file")) == provenance.input_file);
|
||||
CHECK(gemmi::cif::as_string(*merged.find_value("_jfjoch_provenance.input_identity"))
|
||||
== provenance.input_identity);
|
||||
|
||||
gemmi::cif::Table software = merged.find("_software.", {"pdbx_ordinal", "classification"});
|
||||
REQUIRE(software.length() == 2);
|
||||
@@ -303,3 +314,42 @@ TEST_CASE("Merged mmCIF: PDBx keys, merging R-factors and the scaled unmerged bl
|
||||
CHECK(gemmi::cif::as_number(obs[0][3]) == Catch::Approx(200.0));
|
||||
CHECK(gemmi::cif::as_number(obs[1][5]) == Catch::Approx(0.8));
|
||||
}
|
||||
|
||||
TEST_CASE("MTZ history: command line and input, in records of at most 80 characters",
|
||||
"[write_reflections][portable]") {
|
||||
jfjoch_test::SyntheticMergeParams params;
|
||||
params.true_space_group = "P 43 21 2";
|
||||
params.twin_supergroup = "P 43 21 2";
|
||||
params.d_min_A = 5.0;
|
||||
const auto reflections = jfjoch_test::GenerateSyntheticMerged(params);
|
||||
|
||||
OutputProvenance provenance;
|
||||
// A space falls exactly on the first record's last character, which a reader would strip.
|
||||
provenance.command_line = "rugnux -o out " + std::string(56, 'x') + " " + std::string(150, 'x') + "_master.h5";
|
||||
provenance.input_file = "/data/" + std::string(150, 'x') + "_master.h5";
|
||||
provenance.input_identity = "1234 bytes, modified 2026-07-13T14:00:00.000Z";
|
||||
|
||||
const auto path = (std::filesystem::temp_directory_path() / "rugnux_history.mtz").string();
|
||||
WriteMtzReflections(reflections, TETRAGONAL_CELL, TestExperiment(), provenance, path);
|
||||
const gemmi::Mtz mtz = gemmi::read_mtz_file(path);
|
||||
std::filesystem::remove(path);
|
||||
|
||||
REQUIRE(!mtz.history.empty());
|
||||
CHECK(mtz.history.front().rfind("From Rugnux ", 0) == 0);
|
||||
std::string joined;
|
||||
for (const auto &line : mtz.history) {
|
||||
CHECK(line.size() <= 80);
|
||||
joined += line;
|
||||
}
|
||||
// Each value is all there, run on over records that start with '+'.
|
||||
std::string command;
|
||||
for (size_t i = 0; i < mtz.history.size(); ++i)
|
||||
if (mtz.history[i].rfind("Command: ", 0) == 0) {
|
||||
command = mtz.history[i].substr(9);
|
||||
for (size_t j = i + 1; j < mtz.history.size() && mtz.history[j].rfind("+", 0) == 0; ++j)
|
||||
command += mtz.history[j].substr(1);
|
||||
}
|
||||
CHECK(command == provenance.command_line);
|
||||
CHECK(joined.find("Input file: " + provenance.input_identity) != std::string::npos);
|
||||
CHECK(mtz.history.size() <= 30);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user