From a574e97b10bf3e8449306e6c4ab51cab3ef60fc7 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Wed, 14 Oct 2026 13:15:13 +0200 Subject: [PATCH] RESULT-CHANGING (owner approved): stills unmerged MTZ written only for the crystals on the run's cell The unmerged MTZ took every integrated still. A still whose cell is not close to the run's - the same test, with the same tolerances, the merge applies - was indexed in another lattice (on few spots, typically an axis multiple), so its H K L do not name the reflections a reading program takes them for: aimless or careless would scale them in as wrong reflections. On two 25k-frame XFEL runs such frames carried 60% of the integrated reflections, and the 824 MB file was the largest single cost after the merge (about 7 s in --mode scale). Rotation runs are unchanged (one lattice). The orientation matrix in the batch headers is likewise taken from the first crystal on the run's cell. Test: Unmerged MTZ: on stills, only the crystals on the run's cell. Co-Authored-By: Claude Opus 5.5 (1M context) --- image_analysis/WriteReflections.cpp | 16 +++++++++++++--- tests/WriteReflectionsTest.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/image_analysis/WriteReflections.cpp b/image_analysis/WriteReflections.cpp index e1c1bce98..faf3b5075 100644 --- a/image_analysis/WriteReflections.cpp +++ b/image_analysis/WriteReflections.cpp @@ -1033,6 +1033,15 @@ gemmi::Mtz UnmergedMtz(const std::vector &outcomes, // removing them turns its test into an assumption. XDS and DIALS draw the line in the same place. const char centering = mtz.spacegroup ? mtz.spacegroup->hm[0] : 'P'; + // On stills only the crystals the merge takes are written: one whose cell is not the run's was indexed + // in another lattice (on few spots, typically an axis multiple), so its indices do not name the + // reflections they would be read as. A rotation run has the one lattice. + const auto &indexing = experiment.GetIndexingSettings(); + const auto merged_crystal = [&](const IntegrationOutcome &outcome) { + return scanning || outcome.latt.GetUnitCell().is_close(unitCell, indexing.GetUnitCellDistTolerance(), + indexing.GetUnitCellAngleTolerance_deg()); + }; + std::vector> fulls; if (scanning && sum_partials) fulls = SumRockingEvents(outcomes, @@ -1057,8 +1066,9 @@ gemmi::Mtz UnmergedMtz(const std::vector &outcomes, add_row(r); } else { for (const auto &outcome : outcomes) - for (const auto &r : outcome.reflections) - add_row(r); + if (merged_crystal(outcome)) + for (const auto &r : outcome.reflections) + add_row(r); } // Every row has its own slot, so they are filled on several workers. @@ -1104,7 +1114,7 @@ gemmi::Mtz UnmergedMtz(const std::vector &outcomes, // the sweep, which is the one matrix POINTLESS also writes into every batch. std::optional lattice_at_zero; for (const auto &outcome : outcomes) { - if (outcome.reflections.empty() || outcome.latt.CalcVolume() <= 1.0f) + if (outcome.reflections.empty() || outcome.latt.CalcVolume() <= 1.0f || !merged_crystal(outcome)) continue; const float mid_deg = phi_start_deg(outcome.reflections.front().image_number) + wedge_deg / 2.0f; lattice_at_zero = gon ? outcome.latt.Multiply(gon->GetTransformationAngle(mid_deg)) : outcome.latt; diff --git a/tests/WriteReflectionsTest.cpp b/tests/WriteReflectionsTest.cpp index 848a3d02c..51512f4ec 100644 --- a/tests/WriteReflectionsTest.cpp +++ b/tests/WriteReflectionsTest.cpp @@ -335,3 +335,29 @@ TEST_CASE("MTZ history: command line and input, in records of at most 80 charact CHECK(joined.find("Input file: " + provenance.input_identity) != std::string::npos); CHECK(mtz.history.size() <= 30); } + +TEST_CASE("Unmerged MTZ: on stills, only the crystals on the run's cell", "[write_reflections][portable]") { + // A still indexed with its c axis doubled names other reflections by the same indices, so its rows + // would be read as the wrong reflections; it is left out, the crystal on the run's cell is written. + const auto experiment = TestExperiment(); // no goniometer: stills + std::vector outcomes(2); + outcomes[0].latt = CrystalLattice(TETRAGONAL_CELL); + outcomes[1].latt = CrystalLattice(UnitCell{47.0f, 47.0f, 126.0f, 90.0f, 90.0f, 90.0f}); + for (int f = 0; f < 2; ++f) + for (int l = 1; l <= 5; ++l) { + Reflection r{}; + r.h = 1; r.k = 2; r.l = l; + r.image_number = static_cast(f); + r.I = 100.0f; + r.sigma = 10.0f; + r.prescaling_corr = 1.0f; + r.partiality = 1.0f; + outcomes[f].reflections.push_back(r); + } + const gemmi::Mtz mtz = UnmergedMtz(outcomes, TETRAGONAL_CELL, experiment, false, OutputProvenance{}, 1); + CHECK(mtz.nreflections == 5); + const auto batch = mtz.column_with_label("BATCH"); + REQUIRE(batch != nullptr); + for (int i = 0; i < mtz.nreflections; ++i) + CHECK((*batch)[i] == 1.0f); +}