From 7c49fefb54efb94cb1720a5d6cdfd6ad5ab40f88 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Mon, 26 Oct 2026 18:03:56 +0100 Subject: [PATCH] Tail: the cell e.s.d.s and the unmerged MTZ table beside the refinement; the refined difference and anomalous maps side by side The cell e.s.d.s and the unmerged MTZ read only the integrated observations and the group, both final once the model has been read, and the refinement reads neither: both now start before the refinement instead of after it. In MakeMaps the difference map is made on its own thread while the anomalous map is made. Outputs identical (MTZ data, CIF/HKL/INS, refined model, maps, blobs, report minus provenance) on the thaumatin and cytochrome C live runs. Live path without the unmerged MTZ (as the broker runs it), step Refinement: thaumatin 2.76 -> 2.40 s, cytochrome C 1.99 -> 1.93 s. With the unmerged MTZ (CLI), Refinement + Writing files: 3.21 -> 2.67 s and 3.33 -> 2.80 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu --- .../structure_refinement/Refinement.cpp | 11 ++-- rugnux/RugnuxScaleMerge.cpp | 52 +++++++++++-------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/image_analysis/structure_refinement/Refinement.cpp b/image_analysis/structure_refinement/Refinement.cpp index 977eccf6f..96c274e52 100644 --- a/image_analysis/structure_refinement/Refinement.cpp +++ b/image_analysis/structure_refinement/Refinement.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -856,9 +857,12 @@ void MakeMaps(Context &ctx, const std::vector &merged, double diff.v.push_back({ctx.refl.hkl[m], std::polar(static_cast(delfwt), static_cast(phi))}); } result.maps = maps; - const std::array size = gemmi::get_size_for_hkl(diff, {{0, 0, 0}}, 3.0); - result.difference_map = std::make_shared>(MapFromFPhi(gemmi::get_f_phi_on_grid(diff, size, true))); - result.difference_map_rms = gemmi::calculate_data_statistics(result.difference_map->data).rms; + // The difference map beside the anomalous one below: neither reads the other. + std::future difference_map = std::async(std::launch::async, [&diff, &result] { + const std::array size = gemmi::get_size_for_hkl(diff, {{0, 0, 0}}, 3.0); + result.difference_map = std::make_shared>(MapFromFPhi(gemmi::get_f_phi_on_grid(diff, size, true))); + result.difference_map_rms = gemmi::calculate_data_statistics(result.difference_map->data).rms; + }); // The anomalous difference map: F(+) - F(-) on the model's phase turned back by 90 degrees, read on the + // rows of an anomalous merge only (as model validation reads it). @@ -890,6 +894,7 @@ void MakeMaps(Context &ctx, const std::vector &merged, double result.anomalous_map_rms = gemmi::calculate_data_statistics(result.anomalous_map->data).rms; } } + difference_map.get(); } // The engine for the model as it is now: on the GPU where it is wanted and takes the case within its budget, diff --git a/rugnux/RugnuxScaleMerge.cpp b/rugnux/RugnuxScaleMerge.cpp index 113e46c6d..c1b35fd2d 100644 --- a/rugnux/RugnuxScaleMerge.cpp +++ b/rugnux/RugnuxScaleMerge.cpp @@ -4754,6 +4754,35 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { } result.model_prior = std::move(mr); } + + // The cell's e.s.d.s (CellEsd.h), from the same integrated observations and at the same point as the + // unmerged MTZ below: the indexing and the group are final here, once the model has been read. Rotation + // only - a stills run keeps no per-image reflections to the end, and its crystals' cells differ by more than + // any one is measured to. Made beside the refinement, which reads neither. + std::future> cell_esd; + if (is_rotation && !geometry_prepass && !superseded && result.consensus_cell) + cell_esd = std::async(std::launch::async, [&outcomes = indexer->GetIntegrationOutcome(), + sg = experiment_.GetSpaceGroupOrP1(), + wavelength = experiment_.GetWavelength_A(), + nthreads = config_.nthreads, &logger = logger] { + TimingMark mark(logger, "cell e.s.d."); + return EstimateCellEsd(GatherCellObservations(outcomes, nthreads), sg, wavelength); + }); + + // The unmerged MTZ (kept with the other outputs, see there) is built from here on, beside the + // refinement and the reflection files: the indexing and the group it is written in are final once the + // model has been read, and nothing after this changes the integrated observations it reads. It gets its + // own copy of the experiment. + if (full && write_output && !geometry_prepass && !superseded && result.consensus_cell + && config_.export_unmerged) { + unmerged_mtz = std::async(std::launch::async, + [&outcomes = indexer->GetIntegrationOutcome(), + cell = *result.consensus_cell, x = experiment_, + &provenance = config_.provenance, nthreads = config_.nthreads] { + return UnmergedMtzTable(outcomes, cell, x, true, provenance, nthreads); + }); + } + if (model_run) { result.model_placement = placed_by_rigid_body ? "RIGID_BODY" : result.model_prior && result.model_prior->chosen >= 0 ? "MR" : "NOT_PLACED"; @@ -4833,13 +4862,8 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { } } - // The cell's e.s.d.s (CellEsd.h), from the same integrated observations and at the same point as the - // unmerged MTZ below: the indexing and the group are final here. Rotation only - a stills run keeps no - // per-image reflections to the end, and its crystals' cells differ by more than any one is measured to. - if (is_rotation && !geometry_prepass && !superseded && result.consensus_cell) { - TimingMark mark(logger, "cell e.s.d."); - const auto obs = GatherCellObservations(indexer->GetIntegrationOutcome(), config_.nthreads); - result.cell_esd = EstimateCellEsd(obs, experiment_.GetSpaceGroupOrP1(), experiment_.GetWavelength_A()); + if (cell_esd.valid()) { + result.cell_esd = cell_esd.get(); if (result.cell_esd) { const CellEsd &e = *result.cell_esd; logger.Info("Cell e.s.d.s from {} observations of {} reflections: {:.5f} {:.5f} {:.5f} {:.4f} {:.4f} {:.4f}" @@ -4850,20 +4874,6 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { } } - // The unmerged MTZ (kept with the other outputs, see there) is built from here on, beside the - // reflection files: the indexing and the group it is written in are final once the model has been - // read, and nothing after this changes the integrated observations it reads. It gets its own copy of - // the experiment. - if (full && write_output && !geometry_prepass && !superseded && result.consensus_cell - && config_.export_unmerged) { - unmerged_mtz = std::async(std::launch::async, - [&outcomes = indexer->GetIntegrationOutcome(), - cell = *result.consensus_cell, x = experiment_, - &provenance = config_.provenance, nthreads = config_.nthreads] { - return UnmergedMtzTable(outcomes, cell, x, true, provenance, nthreads); - }); - } - // Not on the geometry pre-pass, and not on a pass that is going to be re-run. Pass 1 exists to // choose the space group and post-refine the geometry; the pass after it remakes these outputs // seconds later at the refined geometry, and that is the answer anyone reads. The quality guard is