From aabac1541aeb5fae2cb10b4b42195ee5455c0221 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 9 Oct 2026 12:08:33 +0200 Subject: [PATCH] ModelValidation: score the candidate changes of basis at the same time Each candidate setting of the model was scored one after another on the coarse shell (a density, an FFT and an isotropic scale fit, 0.2-0.5 s each), and a run with a model in another setting scores them in both of its validations. They share nothing but what they read, so they now run on ParallelFor, each on its own copy of the model, and are read back in their own order: the log lines, the ranking and the tie-break (first of equal R wins) are those of the serial loop. Measured (16-core workstation, loaded): 8sa8 3 candidates 0.64 s -> 0.22 s; 9ea5 3 candidates 1.47 s -> 0.50 s; 8xtg 1.45 s -> 0.40 s. p.mtz, the maps, the map MTZ and the placed model md5-identical. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi --- .../structure_refinement/ModelValidation.cpp | 22 ++++++++++++++----- .../structure_refinement/ModelValidation.h | 4 ++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/image_analysis/structure_refinement/ModelValidation.cpp b/image_analysis/structure_refinement/ModelValidation.cpp index 3673483dc..3bbf9af2d 100644 --- a/image_analysis/structure_refinement/ModelValidation.cpp +++ b/image_analysis/structure_refinement/ModelValidation.cpp @@ -32,6 +32,7 @@ #include "../../common/CorrelationCoefficient.h" #include "../../common/JFJochMath.h" // PI (M_PI is not standard, and MSVC does not define it) #include "../../common/Logger.h" +#include "../../common/ParallelFor.h" #include "../scale_merge/ReindexAmbiguity.h" // ReindexReflections #include "../scale_merge/CrystalSetting.h" // CellMappingOperators #include "ModelFFT.h" @@ -481,13 +482,15 @@ ModelValidationResult Validate(const std::vector &merged, "is not how the data describe this lattice; scoring {} change(s) of basis", st.cell.a, st.cell.b, st.cell.c, st.cell.alpha, st.cell.beta, st.cell.gamma, sg->xhm(), frames.size() - 1); - size_t best = 0; - double best_r = 0; - for (size_t i = 0; i < frames.size(); i++) { + // The candidates are scored at the same time, each on its own copy of the model, and read + // in their own order below, so the ranking and its tie-break are those of one after another. + std::vector frame_r(frames.size(), 0.0); + std::vector frame_sg(frames.size(), nullptr); + ParallelFor(static_cast(frames.size()), nthreads, [&](int i) { gemmi::Structure trial = st; const gemmi::SpaceGroup *trial_sg = sg; if (i > 0 && !change_model_basis(trial, trial_sg, frames[i])) - continue; // no origin shift names the transformed group; not a basis we can take + return; // no origin shift names the transformed group; not a basis we can take refractionalize_into(trial, data_cell); trial.setup_cell_images(); // Each frame at the data's best indexing: the alternative indexings are only probed @@ -499,8 +502,17 @@ ModelValidationResult Validate(const std::vector &merged, if (same_point_group) for (const gemmi::Op &law : reindex_ops) r = std::min(r, frame_probe_r(trial, trial_sg, ReindexReflections(obs, law), d_min)); + frame_r[i] = r; + frame_sg[i] = trial_sg; + }); + size_t best = 0; + double best_r = 0; + for (size_t i = 0; i < frames.size(); i++) { + if (frame_sg[i] == nullptr) + continue; + const double r = frame_r[i]; logger.Info("Model validation: {:<12} -> {:<12} R {:.4f} on the coarse shell", - frames[i].triplet(), trial_sg->xhm(), r); + frames[i].triplet(), frame_sg[i]->xhm(), r); if (i == 0 || r < best_r) { best_r = r; best = i; diff --git a/image_analysis/structure_refinement/ModelValidation.h b/image_analysis/structure_refinement/ModelValidation.h index 47bffdd26..bcfcad445 100644 --- a/image_analysis/structure_refinement/ModelValidation.h +++ b/image_analysis/structure_refinement/ModelValidation.h @@ -219,8 +219,8 @@ struct ModelValidationResult { // the runner-up beats the lead the same model in a random orientation takes, since a random model // also picks a winner. Holohedral crystals (no twin laws) are unaffected either way. // -// nthreads is what the null's replicates run on - they are independent of each other and of the real -// model, so they run at once. Nothing else here is threaded, and the answer does not depend on it. +// nthreads is what the null's replicates and the candidate changes of basis run on - each is +// independent of the others, so they run at once. The answer does not depend on it. // // report_shell_d_min is the merge statistics' own shell bounds, coarse to fine, and it is what // cc_model_shells is binned on. Sharing the grid is the point: a reader has to be able to put a