From 270ae432d8fd84c168c8c63092c762f5effad130 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 7 Nov 2026 15:05:51 +0100 Subject: [PATCH] Model step: one set of parts for rotation and live stills rugnux/StillsModel becomes rugnux/ModelStep, and rotation calls its parts instead of keeping copies: the validation of a placement with the rigid body's verdict kept, the refinement of a placed model, and the move of a merge to the model's axes. The receiver side shares how the structures reach the live status and how the result blocks are dropped from an END that does not fit. RunModelStep remains the one call that makes the whole step on a merge; the timing, the deadline and what to reuse from an earlier step stay with the caller. Rotation still drives the parts itself, inside ScaleMergeAndSymmetry, because its decisions move what the run writes: the integrated observations, the twin hypothesis' merges, a rival refined beside the choice, a validation started on a forecast, reference data and free flags. Live stills have none of these, so RunModelStep takes no options for them. Live stills take from rotation what rotation did better: a model written on other axes takes the merge to them (indices, cell, group, free set) and the maps and placed model are made again there, and the cell follows an indexing that is only a near-symmetry. Before, the placement and the merge could end up on different axes. Later merges follow through the earlier step's change of basis. Rotation does not change: end to end with a model on a monoclinic standard specimen, the old and new code wrote identical maps, placed and refined models and blobs, and an MTZ that differs only in the build stamp of its history line. Live stills with a model ran end to end for the first time: placed by the rigid body, warm-started on each later merge, R-free 0.239 placed and 0.196 refined, the files written and the viewer routes serving the model and maps. Co-Authored-By: Claude Opus 5.5 --- receiver/JFJochReceiverRugnux.cpp | 47 +------ receiver/RugnuxStructure.cpp | 51 ++++++++ receiver/RugnuxStructure.h | 17 +++ receiver/StillsGPUAnalysis.cpp | 57 ++------ receiver/StillsGPUAnalysis.h | 14 +- rugnux/CMakeLists.txt | 4 +- rugnux/{StillsModel.cpp => ModelStep.cpp} | 153 ++++++++++++++++------ rugnux/ModelStep.h | 101 ++++++++++++++ rugnux/RugnuxScaleMerge.cpp | 52 ++------ rugnux/StillsModel.h | 76 ----------- tests/ModelValidationTest.cpp | 82 ++++++++++-- 11 files changed, 387 insertions(+), 267 deletions(-) rename rugnux/{StillsModel.cpp => ModelStep.cpp} (52%) create mode 100644 rugnux/ModelStep.h delete mode 100644 rugnux/StillsModel.h diff --git a/receiver/JFJochReceiverRugnux.cpp b/receiver/JFJochReceiverRugnux.cpp index 6ebd1a821..8a1384bb3 100644 --- a/receiver/JFJochReceiverRugnux.cpp +++ b/receiver/JFJochReceiverRugnux.cpp @@ -657,22 +657,8 @@ void JFJochReceiverRugnux::RugnuxThread() { // refined model with its own, for the broker's viewer routes. if (result.outputs && result.outputs->placed_model) { const RugnuxOutputs &o = *result.outputs; - const PlacedModelOutput &p = *o.placed_model; - std::optional coefficients; - if (o.maps) - coefficients = o.maps->coefficients; - std::shared_ptr refined; - if (o.refined_model) { - std::optional refined_coefficients; - if (o.refined_maps) - refined_coefficients = o.refined_maps->coefficients; - refined = std::make_shared(o.refined_model->model, o.refined_model->cell, - o.refined_model->space_group, - std::move(refined_coefficients)); - } - live_status.Structure(std::make_shared(p.model, p.cell, p.space_group, - std::move(coefficients)), - std::move(refined)); + PublishStructures(live_status, *o.placed_model, o.maps.get(), + o.refined_model ? &*o.refined_model : nullptr, o.refined_maps.get()); } live_status.Finish(result.cancelled ? RugnuxLiveState::Cancelled : RugnuxLiveState::Done); reachable.reset(); @@ -790,35 +776,10 @@ void JFJochReceiverRugnux::AddToEndMessage(EndMessage &message) { if (o.blobs) message.blobs = DensityBlobsToMessage(*o.blobs); - // Every pusher serialises END into a buffer of MESSAGE_SIZE_FOR_START_END, and an END that does - // not fit there fails the collection. So it is serialised here first, into a buffer of the same - // size (uninitialised: only the bytes written are touched); one that does not fit goes without - // the blocks. - const auto scratch = std::make_unique_for_overwrite(MESSAGE_SIZE_FOR_START_END); - CBORStream2Serializer serializer(scratch.get(), MESSAGE_SIZE_FOR_START_END); - try { - serializer.SerializeSequenceEnd(message); - } catch (const JFJochException &e) { - const std::string text = fmt::format("the merged data, map coefficients and models do not " - "fit in the end message ({} MiB) and are not written", - MESSAGE_SIZE_FOR_START_END / (1024 * 1024)); - logger.Error("rugnux live: {} - {}", text, e.what()); - live_status.Warning(text); - message.merged.reset(); - message.map_coefficients.reset(); - message.placed_model.reset(); - message.refined_model.reset(); - message.refined_map_coefficients.reset(); - message.blobs.reset(); - } + DropResultBlocksThatDoNotFit(message, &live_status, logger); } catch (const std::exception &e) { logger.Error("rugnux live: the end message goes without the analysis results - {}", e.what()); - message.merged.reset(); - message.map_coefficients.reset(); - message.placed_model.reset(); - message.refined_model.reset(); - message.refined_map_coefficients.reset(); - message.blobs.reset(); + DropResultBlocks(message); } } diff --git a/receiver/RugnuxStructure.cpp b/receiver/RugnuxStructure.cpp index 88493686e..754943316 100644 --- a/receiver/RugnuxStructure.cpp +++ b/receiver/RugnuxStructure.cpp @@ -3,6 +3,14 @@ #include "RugnuxStructure.h" +#include + +#include "spdlog/fmt/fmt.h" + +#include "../common/JFJochException.h" +#include "../frame_serialize/CBORStream2Serializer.h" +#include "../rugnux/Rugnux.h" + #include #include #include @@ -101,3 +109,46 @@ std::string RugnuxStructure::Map(RugnuxMapType type) const { cached = std::move(out); return *cached; } + +void PublishStructures(RugnuxLiveStatus &status, const PlacedModelOutput &placed, const ModelMaps *maps, + const PlacedModelOutput *refined, const ModelMaps *refined_maps) { + std::optional coefficients; + if (maps) + coefficients = maps->coefficients; + std::shared_ptr refined_structure; + if (refined) { + std::optional refined_coefficients; + if (refined_maps) + refined_coefficients = refined_maps->coefficients; + refined_structure = std::make_shared(refined->model, refined->cell, refined->space_group, + std::move(refined_coefficients)); + } + status.Structure(std::make_shared(placed.model, placed.cell, placed.space_group, + std::move(coefficients)), + std::move(refined_structure)); +} + +void DropResultBlocks(EndMessage &message) { + message.merged.reset(); + message.map_coefficients.reset(); + message.placed_model.reset(); + message.refined_model.reset(); + message.refined_map_coefficients.reset(); + message.blobs.reset(); +} + +void DropResultBlocksThatDoNotFit(EndMessage &message, RugnuxLiveStatus *status, Logger &logger) { + const auto scratch = std::make_unique_for_overwrite(MESSAGE_SIZE_FOR_START_END); + CBORStream2Serializer serializer(scratch.get(), MESSAGE_SIZE_FOR_START_END); + try { + serializer.SerializeSequenceEnd(message); + } catch (const JFJochException &e) { + const std::string text = fmt::format("the merged data, map coefficients and models do not fit in the end " + "message ({} MiB) and are not written", + MESSAGE_SIZE_FOR_START_END / (1024 * 1024)); + logger.Error("rugnux live: {} - {}", text, e.what()); + if (status) + status->Warning(text); + DropResultBlocks(message); + } +} diff --git a/receiver/RugnuxStructure.h b/receiver/RugnuxStructure.h index e44b18a32..862b42798 100644 --- a/receiver/RugnuxStructure.h +++ b/receiver/RugnuxStructure.h @@ -11,8 +11,14 @@ #include "gemmi/model.hpp" #include "gemmi/symmetry.hpp" +#include "../common/JFJochMessages.h" +#include "../common/Logger.h" #include "../common/MtzTable.h" #include "../common/UnitCell.h" +#include "../image_analysis/structure_refinement/ModelValidation.h" +#include "RugnuxLiveStatus.h" + +struct PlacedModelOutput; enum class RugnuxMapType { TwoFoFc, FoFc, Anomalous }; @@ -44,3 +50,14 @@ public: // map none of its coefficients. std::string Map(RugnuxMapType type) const; }; + +// The model step's structures, as the placed model with the maps of its placement and the refined one with its own, +// to the live status for the broker's viewer routes. +void PublishStructures(RugnuxLiveStatus &status, const PlacedModelOutput &placed, const ModelMaps *maps, + const PlacedModelOutput *refined, const ModelMaps *refined_maps); + +// The end message's merged data, maps and models, for the writer. Every pusher serialises END into a buffer of +// MESSAGE_SIZE_FOR_START_END, and an END that does not fit there fails the collection; so it is tried here, and one +// that does not fit goes without them, with a warning in the status. +void DropResultBlocks(EndMessage &message); +void DropResultBlocksThatDoNotFit(EndMessage &message, RugnuxLiveStatus *status, Logger &logger); diff --git a/receiver/StillsGPUAnalysis.cpp b/receiver/StillsGPUAnalysis.cpp index e005604d3..6011e6aee 100644 --- a/receiver/StillsGPUAnalysis.cpp +++ b/receiver/StillsGPUAnalysis.cpp @@ -385,7 +385,7 @@ const DiffractionExperiment &StillsGPUAnalysis::GetMergeExperiment() const { return merge_experiment; } -const std::optional &StillsGPUAnalysis::GetModel() const { +const std::optional &StillsGPUAnalysis::GetModel() const { return model_final; } @@ -402,7 +402,7 @@ void StillsGPUAnalysis::MergeThread() { merge->Cell(*c); cell_set = true; if (model) { - const StillsModelGroup g = StillsModelSpaceGroup(merge_experiment.GetGemmiSpaceGroup(), *c, *model); + const ModelStepGroup g = ModelStepSpaceGroup(merge_experiment.GetGemmiSpaceGroup(), *c, *model); if (!g.space_group) { model_left_out = "the model is left out: " + g.reason; logger.Warning("{}", model_left_out); @@ -539,16 +539,17 @@ void StillsGPUAnalysis::ModelThread() { model_running_cut = job.cut; } try { - StillsModelSettings settings; + ModelStepSettings settings; // Beside the analysis on as many threads as a provisional merge, on every thread after the last image settings.nthreads = job.final ? AvailableCpus() : MERGE_THREADS; // The analysis's indexers stay on the card for the broker's life settings.gpu_resident_bytes = indexing_thread_pool ? indexing_thread_pool->DeviceBytes() : 0; + settings.rfree_fraction = merge_experiment.GetScalingSettings().GetRfreeFraction(); settings.cut = job.cut.get(); if (job.final && status) settings.step = [this](const std::string &title) { status->Step(title); }; const DiffractionExperiment &x = merge_experiment; - StillsModelResult r = PlaceAndRefineStillsModel(job.merged, x.GetUnitCell().value(), x.GetSpaceGroupOrP1(), + ModelStepResult r = RunModelStep(job.merged, x.GetUnitCell().value(), x.GetSpaceGroupOrP1(), *model, x.GetWavelength_A(), x.GetScalingSettings().GetMergeFriedel(), job.shells, settings, model_placed ? &*model_placed : nullptr, logger); @@ -563,7 +564,7 @@ void StillsGPUAnalysis::ModelThread() { } } -void StillsGPUAnalysis::PublishModel(const StillsModelResult &r, const std::string &title) { +void StillsGPUAnalysis::PublishModel(const ModelStepResult &r, const std::string &title) { std::string text = fmt::format("Model on the merge of {}: ", title); if (r.placement == "NOT_PLACED") text += "not placed"; @@ -583,24 +584,15 @@ void StillsGPUAnalysis::PublishModel(const StillsModelResult &r, const std::stri if (!status) return; status->Note(text); - if (const auto m = LiveModel(StillsModelReport(r))) + if (const auto m = LiveModel(ModelStepReport(r))) status->Model(*m); if (!r.placed_model) return; - std::optional coefficients; - if (r.maps) - coefficients = r.maps->coefficients; - std::shared_ptr refined; - if (r.refinement && r.refinement->ok && r.refinement->model) { - std::optional refined_coefficients; - if (r.refinement->maps) - refined_coefficients = r.refinement->maps->coefficients; - refined = std::make_shared(r.refinement->model, r.placed_model->cell, - r.placed_model->space_group, std::move(refined_coefficients)); - } - status->Structure(std::make_shared(r.placed_model->model, r.placed_model->cell, - r.placed_model->space_group, std::move(coefficients)), - std::move(refined)); + std::optional refined; + if (r.refinement && r.refinement->ok && r.refinement->model) + refined = PlacedModelOutput{r.refinement->model, r.placed_model->cell, r.placed_model->space_group}; + PublishStructures(*status, *r.placed_model, r.maps.get(), refined ? &*refined : nullptr, + refined && r.refinement->maps ? r.refinement->maps.get() : nullptr); } void StillsGPUAnalysis::FillEndMessage(EndMessage &message) const { @@ -628,27 +620,6 @@ void StillsGPUAnalysis::FillEndMessage(EndMessage &message) const { } else if (const auto &m = GetMerged(); m && GetMergeExperiment().GetUnitCell()) message.merged = MergedMtzTable(m->merge.merged, *GetMergeExperiment().GetUnitCell(), GetMergeExperiment(), OutputProvenance{}); - if (!message.merged) - return; - // Every pusher serialises END into a buffer of MESSAGE_SIZE_FOR_START_END, and an END that does not fit there - // fails the collection. So it is serialised here first, as JFJochReceiverRugnux does for rotation; one that does not fit - // goes without the merged data, map coefficients and models. - const auto scratch = std::make_unique_for_overwrite(MESSAGE_SIZE_FOR_START_END); - CBORStream2Serializer serializer(scratch.get(), MESSAGE_SIZE_FOR_START_END); - try { - serializer.SerializeSequenceEnd(message); - } catch (const JFJochException &e) { - const std::string text = fmt::format("the merged data, map coefficients and models do not fit in the end " - "message ({} MiB) and are not written", - MESSAGE_SIZE_FOR_START_END / (1024 * 1024)); - logger.Error("GPU analysis: {} - {}", text, e.what()); - if (status) - status->Warning(text); - message.merged.reset(); - message.map_coefficients.reset(); - message.placed_model.reset(); - message.refined_model.reset(); - message.refined_map_coefficients.reset(); - message.blobs.reset(); - } + if (message.merged) + DropResultBlocksThatDoNotFit(message, status, logger); } diff --git a/receiver/StillsGPUAnalysis.h b/receiver/StillsGPUAnalysis.h index 00dd45f2a..46b5227d2 100644 --- a/receiver/StillsGPUAnalysis.h +++ b/receiver/StillsGPUAnalysis.h @@ -24,7 +24,7 @@ #include "../common/ThreadSafeFIFO.h" #include "../image_analysis/IndexAndRefine.h" #include "../rugnux/StillsLiveMerge.h" -#include "../rugnux/StillsModel.h" +#include "../rugnux/ModelStep.h" // Serial frames analysed on the GPU (MXAnalysisWithoutFPGA), after a pre-pass on // the first PREPASS_IMAGES (RunStillsPrePass) has decided the beam centre and the mask they are analysed @@ -95,7 +95,7 @@ public: [[nodiscard]] const std::optional &GetMerged() const; [[nodiscard]] const DiffractionExperiment &GetMergeExperiment() const; // The model step on the merge of the collection, where one was made. - [[nodiscard]] const std::optional &GetModel() const; + [[nodiscard]] const std::optional &GetModel() const; // What the analysis found, for the collection's end message: the cell, geometry, and the merged data for the // writer to put beside the master file, in the model's frame where a model was placed against them. void FillEndMessage(EndMessage &message) const; @@ -149,13 +149,13 @@ private: std::optional merged; std::future merge_future; - // Where a model is given, it is placed against each merge, mapped and refined (PlaceAndRefineStillsModel), on a + // Where a model is given, it is placed against each merge, mapped and refined (RunModelStep), on a // thread of its own: the provisional merges' beside the analysis, published to the status as they are made, and the // collection's in Finish, which waits for it until deadline_s after the last image (0: no deadline) and then cuts it - // a refinement running is cut short, a molecular replacement or a refinement not started is left out. A step still // running when the next merge is made is cut the same way, and one not started is replaced by it. Each step starts // from the placement of the last one that placed the model. Without a space group stated, the frames are merged in - // the model's where its cell is the run's consensus (StillsModelSpaceGroup); otherwise the model is left out. + // the model's where its cell is the run's consensus (ModelStepSpaceGroup); otherwise the model is left out. const std::shared_ptr model; const double deadline_s; std::string model_left_out; // why the model is left out; set by the merge thread before any step @@ -173,14 +173,14 @@ private: bool model_stop = false; bool model_cut = false; // the steps from here on are cut too bool model_over = false; // the step on the merge of the collection has ended: nothing is left to cut - std::optional model_placed; // the last step that placed the model, for the next to start from - std::optional model_final; + std::optional model_placed; // the last step that placed the model, for the next to start from + std::optional model_final; std::future model_future; // The step on `out`, the merge of `title` (its reflections moved out, but for the final one); returns its cut. std::shared_ptr> SubmitModel(StillsMergeOutput &out, const std::string &title, bool final); void StopModel(bool cut); void ModelThread(); - void PublishModel(const StillsModelResult &r, const std::string &title); + void PublishModel(const ModelStepResult &r, const std::string &title); void StartPrePass(); void PrePassThread(); diff --git a/rugnux/CMakeLists.txt b/rugnux/CMakeLists.txt index 748515e52..55379b405 100644 --- a/rugnux/CMakeLists.txt +++ b/rugnux/CMakeLists.txt @@ -25,8 +25,8 @@ ADD_LIBRARY(JFJochRugnux STATIC RugnuxLive.h StillsLiveMerge.cpp StillsLiveMerge.h - StillsModel.cpp - StillsModel.h + ModelStep.cpp + ModelStep.h RugnuxRefine.cpp RugnuxRefine.h Rugnux.h diff --git a/rugnux/StillsModel.cpp b/rugnux/ModelStep.cpp similarity index 52% rename from rugnux/StillsModel.cpp rename to rugnux/ModelStep.cpp index 9b3077679..3a80082fa 100644 --- a/rugnux/StillsModel.cpp +++ b/rugnux/ModelStep.cpp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only -#include "StillsModel.h" +#include "ModelStep.h" #include #include @@ -10,23 +10,71 @@ #include "../image_analysis/scale_merge/CrystalSetting.h" #include "../image_analysis/scale_merge/ReindexAmbiguity.h" +#include "../image_analysis/scale_merge/RfreeFlags.h" namespace { double SecondsSince(std::chrono::steady_clock::time_point t0) { return std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); } - bool Cut(const StillsModelSettings &settings) { + bool Cut(const ModelStepSettings &settings) { return settings.cut != nullptr && settings.cut->load(); } - void Step(const StillsModelSettings &settings, const std::string &title) { + void Step(const ModelStepSettings &settings, const std::string &title) { if (settings.step) settings.step(title); } } -StillsModelGroup StillsModelSpaceGroup(const std::optional &collection, const UnitCell &consensus, +bool MergeIntoBasis(std::vector &merged, UnitCell &cell, gemmi::SpaceGroup &space_group, + const gemmi::Op &cob, bool merge_friedel, double rfree_fraction, size_t nthreads) { + const gemmi::SpaceGroup *moved = SpaceGroupInBasis(space_group, cob); + if (moved == nullptr) + return false; + merged = ReindexMergedIntoAsu(merged, HklOperator(cob), *moved, merge_friedel); + cell = CellInBasis(cell, cob); + AssignRfreeFlags(merged, *moved, rfree_fraction, 500, cell, nthreads); + space_group = *moved; + return true; +} + +ModelValidationResult ValidatePlacement(const std::vector &merged, const UnitCell &cell, + const gemmi::SpaceGroup &space_group, const RugnuxModel &model, + const gemmi::Structure &placed, double wavelength_A, + const std::vector &report_shell_d_min, size_t nthreads, + size_t gpu_resident_bytes, const ModelValidationResult *rigid_body, + Logger &logger) { + ModelValidationSchedule placed_elsewhere; + placed_elsewhere.fit_test = false; + placed_elsewhere.gpu_resident_bytes = gpu_resident_bytes; + ModelValidationResult v = ValidateAgainstModel(merged, cell, model, logger, &space_group, + /*probe_indexing_ambiguity=*/false, nthreads, wavelength_A, + report_shell_d_min, placed_elsewhere, nullptr, &placed); + if (v.ok && rigid_body != nullptr) { + v.fit_tested = rigid_body->fit_tested; + v.model_fits = rigid_body->model_fits; + v.null_r_work_mean = rigid_body->null_r_work_mean; + v.null_r_work_sd = rigid_body->null_r_work_sd; + v.r_work_sigma = rigid_body->r_work_sigma; + v.null_replicates = rigid_body->null_replicates; + } + return v; +} + +RefinementResult RefinePlacedModel(const gemmi::Structure &placed, const std::vector &merged, + const UnitCell &cell, const gemmi::SpaceGroup &space_group, double wavelength_A, + const RefinementSettings &settings, Logger &logger) { + try { + return RefineModel(placed, merged, gemmi::UnitCell(cell), space_group, wavelength_A, logger, settings); + } catch (const std::exception &e) { + RefinementResult failed; + failed.failure_reason = std::string("refinement failed: ") + e.what(); + return failed; + } +} + +ModelStepGroup ModelStepSpaceGroup(const std::optional &collection, const UnitCell &consensus, const RugnuxModel &model) { if (collection) return {collection, ""}; @@ -43,47 +91,53 @@ StillsModelGroup StillsModelSpaceGroup(const std::optional &c return {*sg, ""}; } -StillsModelResult PlaceAndRefineStillsModel(const std::vector &merged, const UnitCell &cell, +ModelStepResult RunModelStep(const std::vector &merged, const UnitCell &cell, const gemmi::SpaceGroup &space_group, const RugnuxModel &model, double wavelength_A, bool merge_friedel, const std::vector &report_shell_d_min, - const StillsModelSettings &settings, const StillsModelResult *earlier, + const ModelStepSettings &settings, const ModelStepResult *earlier, Logger &logger) { const auto t0 = std::chrono::steady_clock::now(); - StillsModelResult out; + ModelStepResult out; out.merged = merged; out.cell = cell; out.space_group = space_group; - ModelValidationSchedule placed_elsewhere; // a placement already judged: no null - placed_elsewhere.fit_test = false; - placed_elsewhere.gpu_resident_bytes = settings.gpu_resident_bytes; // Warm: in the earlier step's frame, from where it placed the model if (earlier && earlier->placed_model && earlier->validation) { - const ReindexChoice c = ChooseReindex(out.merged, cell, space_group, + std::vector moved = out.merged; + UnitCell moved_cell = cell; + gemmi::SpaceGroup moved_group = space_group; + const bool setting_moved = !(earlier->to_model_setting == gemmi::Op::identity()); + if (setting_moved) + MergeIntoBasis(moved, moved_cell, moved_group, earlier->to_model_setting, merge_friedel, + settings.rfree_fraction, settings.nthreads); + const ReindexChoice c = ChooseReindex(moved, moved_cell, moved_group, [&](const std::vector &m) { - const double cc = ReferenceIntensityCC(m, earlier->merged, space_group); + const double cc = ReferenceIntensityCC(m, earlier->merged, moved_group); return std::isfinite(cc) ? cc : -1.0; }); - std::vector moved = c.is_identity - ? out.merged - : ReindexMergedIntoAsu(out.merged, c.op, space_group, merge_friedel); + if (!c.is_identity) { + moved = ReindexMergedIntoAsu(moved, c.op, moved_group, merge_friedel); + moved_cell = CellInBasis(moved_cell, c.op); + } Step(settings, "Model validation"); - ModelValidationResult v = ValidateAgainstModel(moved, cell, model, logger, &earlier->space_group, - /*probe_indexing_ambiguity=*/false, settings.nthreads, - wavelength_A, report_shell_d_min, placed_elsewhere, nullptr, - earlier->placed_model->model.get()); + ModelValidationResult v = ValidatePlacement(moved, moved_cell, earlier->space_group, model, + *earlier->placed_model->model, wavelength_A, report_shell_d_min, + settings.nthreads, settings.gpu_resident_bytes, nullptr, logger); if (v.ok && v.placed_model) { logger.Info("Model: placed as on the earlier merge{} - R-work {:.3f}, R-free {:.3f}", c.is_identity ? "" : ", the merge reindexed into its frame", v.r_work, v.r_free); KeepModelVerdict(v, *earlier->validation); out.merged = std::move(moved); + out.cell = moved_cell; out.space_group = earlier->space_group; + out.to_model_setting = earlier->to_model_setting; out.placement = earlier->placement; out.mr = earlier->mr; out.warm = true; out.maps = std::move(v.maps); - out.placed_model = PlacedModelOutput{v.placed_model, cell, out.space_group}; + out.placed_model = PlacedModelOutput{v.placed_model, out.cell, out.space_group}; out.validation = std::move(v); } else logger.Warning("Model: the earlier placement does not validate on this merge ({}) - placed again", @@ -102,9 +156,37 @@ StillsModelResult PlaceAndRefineStillsModel(const std::vector wavelength_A, report_shell_d_min, schedule); if (v.ok && v.model_fits) { out.space_group = *AdoptModelFrame(v, out.merged, space_group, merge_friedel, logger); + // The cell moves with an indexing that is only a near-symmetry, and a model on other axes takes the merge + // to them; the maps and the placed model are then made again, there. As a rotation run does. + bool moved = false; + if (!(v.indexing_op == gemmi::Op::identity())) { + const UnitCell before = out.cell; + out.cell = CellInBasis(out.cell, HklOperator(v.indexing_op)); + moved = !(out.cell == before); + } + gemmi::Op to_model = v.change_of_basis_op; + to_model.tran = {0, 0, 0}; + if (!(to_model == gemmi::Op::identity()) + && MergeIntoBasis(out.merged, out.cell, out.space_group, to_model.inverse(), merge_friedel, + settings.rfree_fraction, settings.nthreads)) { + out.to_model_setting = to_model.inverse(); + moved = true; + } + if (moved) { + ModelValidationSchedule kept_verdict; + kept_verdict.fit_test = false; + kept_verdict.gpu_resident_bytes = settings.gpu_resident_bytes; + ModelValidationResult remade = ValidateAgainstModel(out.merged, out.cell, model, logger, &out.space_group, + /*probe_indexing_ambiguity=*/false, settings.nthreads, + wavelength_A, report_shell_d_min, kept_verdict); + if (remade.ok) { + KeepModelVerdict(remade, v); + v = std::move(remade); + } + } out.placement = "RIGID_BODY"; out.maps = std::move(v.maps); - out.placed_model = PlacedModelOutput{v.placed_model, cell, out.space_group}; + out.placed_model = PlacedModelOutput{v.placed_model, out.cell, out.space_group}; } v.maps.reset(); out.validation = std::move(v); @@ -131,21 +213,11 @@ StillsModelResult PlaceAndRefineStillsModel(const std::vector return ops.is_systematically_absent({{r.h, r.k, r.l}}); }); } - // Its maps and R-factors: the placement validated against the merge, nothing left to decide - ModelValidationResult v = ValidateAgainstModel(out.merged, cell, model, logger, &out.space_group, - /*probe_indexing_ambiguity=*/false, settings.nthreads, - wavelength_A, report_shell_d_min, placed_elsewhere, - nullptr, c.placed.get()); + ModelValidationResult v = ValidatePlacement(out.merged, cell, out.space_group, model, *c.placed, + wavelength_A, report_shell_d_min, settings.nthreads, + settings.gpu_resident_bytes, + out.validation ? &*out.validation : nullptr, logger); if (v.ok && v.placed_model) { - if (out.validation) { - // the rigid body's verdict stays - v.fit_tested = out.validation->fit_tested; - v.model_fits = out.validation->model_fits; - v.null_r_work_mean = out.validation->null_r_work_mean; - v.null_r_work_sd = out.validation->null_r_work_sd; - v.r_work_sigma = out.validation->r_work_sigma; - v.null_replicates = out.validation->null_replicates; - } out.placement = "MR"; out.maps = std::move(v.maps); out.placed_model = PlacedModelOutput{v.placed_model, cell, out.space_group}; @@ -171,18 +243,13 @@ StillsModelResult PlaceAndRefineStillsModel(const std::vector rs.cancel = settings.cut; // The free set is drawn by the merge: a model placed as given has most likely been refined against all of it rs.as_given_competes = out.placement != "RIGID_BODY"; - try { - out.refinement = RefineModel(*out.placed_model->model, out.merged, gemmi::UnitCell(cell), out.space_group, - wavelength_A, logger, rs); - } catch (const std::exception &e) { - out.refinement = RefinementResult(); - out.refinement->failure_reason = std::string("refinement failed: ") + e.what(); - } + out.refinement = RefinePlacedModel(*out.placed_model->model, out.merged, out.cell, out.space_group, wavelength_A, rs, + logger); out.refinement_s = SecondsSince(t1); return out; } -ProcessResult StillsModelReport(const StillsModelResult &result) { +ProcessResult ModelStepReport(const ModelStepResult &result) { ProcessResult r; r.model_validation = result.validation; r.model_prior = result.mr; diff --git a/rugnux/ModelStep.h b/rugnux/ModelStep.h new file mode 100644 index 000000000..1c0319eef --- /dev/null +++ b/rugnux/ModelStep.h @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include +#include +#include +#include + +#include "Rugnux.h" +#include "../image_analysis/structure_refinement/MolecularReplacement.h" +#include "../image_analysis/structure_refinement/Refinement.h" + +// The model step: a model placed against a merge - by its rigid body where it fits, by molecular replacement where +// it does not - then its maps and its refinement. No file is read or written here. +// +// A rotation run takes these parts one at a time, in Rugnux::ScaleMergeAndSymmetry, because each decision there moves what the +// run writes (the integrated observations, the twin hypothesis' merge, a rival refined beside it) and a space group, +// free set or merge it may search beside is its own. RunModelStep takes them in one go on a merge that is all there is +// of the run: the live stills of the broker, which make the step at each provisional merge and after the last frame. +// What to reuse from an earlier step and when to stop is the caller's. + +// The merge moved to the setting `cob` takes its group to: the indices, the cell, the group and, as the free set is a +// function of the written index (RfreeFlags.h), the free set. False, with nothing moved, where it reaches no setting. +bool MergeIntoBasis(std::vector &merged, UnitCell &cell, gemmi::SpaceGroup &space_group, + const gemmi::Op &cob, bool merge_friedel, double rfree_fraction, size_t nthreads); + +// The placement `placed` validated against the merge it was chosen on, for its maps, R-factors and CC(model, data), with +// nothing left for the validation to decide. `rigid_body`, where there is one, keeps its verdict on the model as given. +ModelValidationResult ValidatePlacement(const std::vector &merged, const UnitCell &cell, + const gemmi::SpaceGroup &space_group, const RugnuxModel &model, + const gemmi::Structure &placed, double wavelength_A, + const std::vector &report_shell_d_min, size_t nthreads, + size_t gpu_resident_bytes, const ModelValidationResult *rigid_body, + Logger &logger); + +// The refinement of a placed model. It decides nothing for the run, so one that fails is reported, in the result. +RefinementResult RefinePlacedModel(const gemmi::Structure &placed, const std::vector &merged, + const UnitCell &cell, const gemmi::SpaceGroup &space_group, double wavelength_A, + const RefinementSettings &settings, Logger &logger); + +// The group a merge is merged in where a model is given and no determination of the group was made (serial frames): the collection's, where it states +// one; else the model's own, where the model's cell describes the run's consensus cell on the same axes +// (CellsCorrespond); else none, and `reason` says why the model is left out. +struct ModelStepGroup { + std::optional space_group; + std::string reason; +}; +ModelStepGroup ModelStepSpaceGroup(const std::optional &collection, const UnitCell &consensus, + const RugnuxModel &model); + +struct ModelStepSettings { + size_t nthreads = 1; + size_t gpu_resident_bytes = 0; // held on the card meanwhile (the analysis's indexers) + double rfree_fraction = 0.05; // of a free set drawn again in the model's setting + bool refine = true; + // Set from any thread: the step is cut - a refinement that runs is cut short, keeping the trials that ran + // (RefinementSettings::cancel), and a molecular replacement or a refinement that has not started is left out. + const std::atomic *cut = nullptr; + // Called as each part starts: "Model validation", "Molecular replacement", "Refinement" + std::function step; +}; + +struct ModelStepResult { + // The merge in the model's frame - the indexing the model chose (AdoptModelFrame) on the axes the model is + // written on, labelled in the group the model was placed in (its enantiomorph, where it was taken) - and its cell. + std::vector merged; + UnitCell cell{}; + gemmi::SpaceGroup space_group; + gemmi::Op to_model_setting = gemmi::Op::identity(); // the change of basis that took the merge to those axes + std::string placement = "NOT_PLACED"; // RIGID_BODY | MR | NOT_PLACED + bool warm = false; // placed from an earlier step's placement + std::optional validation; // its maps moved to `maps` + std::optional mr; + // Where the model was placed: the maps of the placement and the model as placed, in the merge's frame + std::shared_ptr maps; + std::optional placed_model; + std::optional refinement; // its model, maps and blobs where it ran + std::vector left_out; // what a cut left out + double placement_s = 0, refinement_s = 0; +}; + +// The model placed against `merged` (in `space_group`, the cell `cell`): by its rigid body where it fits these +// data as given, its indexing ambiguity probed and the merge put in the model's indexing and on its axes; by molecular +// replacement where it does not (or is a prediction); then refined, where placed. `earlier`, the result of a +// step on an earlier merge of the same collection, is the warm start: the merge is put in its frame (the +// indexing a later merge happened to take is decided against it, ChooseReindex), the model goes in where it was +// placed there, and the verdict, the indexing and the molecular replacement's choice are its - no null, probe +// or search is made again. Where the warm placement does not validate, the step is made cold. +ModelStepResult RunModelStep(const std::vector &merged, const UnitCell &cell, + const gemmi::SpaceGroup &space_group, const RugnuxModel &model, + double wavelength_A, bool merge_friedel, + const std::vector &report_shell_d_min, + const ModelStepSettings &settings, const ModelStepResult *earlier, + Logger &logger); + +// The step's outcome in the terms of a ProcessResult (model_validation, model_prior, model_placement and +// refinement), for what reports one (LiveModel). +ProcessResult ModelStepReport(const ModelStepResult &result); diff --git a/rugnux/RugnuxScaleMerge.cpp b/rugnux/RugnuxScaleMerge.cpp index 3e33beedd..40c7ffca5 100644 --- a/rugnux/RugnuxScaleMerge.cpp +++ b/rugnux/RugnuxScaleMerge.cpp @@ -3,6 +3,7 @@ #include #include "Rugnux.h" +#include "ModelStep.h" #include "../image_analysis/structure_refinement/ModelValidation.h" #include "../image_analysis/indexing/SpindleCuspLoss.h" #include "../image_analysis/bragg_integration/SpotWidth.h" @@ -4355,13 +4356,7 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { gemmi::Op to_model = ahead.change_of_basis_op; to_model.tran = {0, 0, 0}; const gemmi::Op cob = to_model.inverse(); - const gemmi::SpaceGroup *in = &sg; - if (const gemmi::SpaceGroup *moved = SpaceGroupInBasis(sg, cob)) { - merged = ReindexMergedIntoAsu(merged, HklOperator(cob), *moved, friedel); - cell = CellInBasis(cell, cob); - AssignRfreeFlags(merged, *moved, rfree_fraction, 500, cell, config_.nthreads); - in = moved; - } + MergeIntoBasis(merged, cell, sg, cob, friedel, rfree_fraction, config_.nthreads); ModelValidationSchedule schedule; schedule.maps_gate = gate; schedule.fit_test = false; // the verdict is the first validation's (KeepModelVerdict) @@ -4369,7 +4364,7 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { schedule.gpu_beside_bytes = beside; ahead.merged = std::move(merged); return ValidateAgainstModel(ahead.merged, cell, *config_.model, ahead.log, - in, /*probe_indexing_ambiguity=*/false, + &sg, /*probe_indexing_ambiguity=*/false, static_cast(config_.nthreads), wavelength, report_shell_d_min, schedule); }); @@ -5458,27 +5453,15 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { const ModelPriorFile *own = model_prior_file ? &*model_prior_file : nullptr; const gemmi::SpaceGroup data_sg = own ? own->space_group : experiment_.GetSpaceGroupOrP1(); const UnitCell cell = own ? own->cell : *result.consensus_cell; - ModelValidationSchedule placed_elsewhere; - placed_elsewhere.fit_test = false; - placed_elsewhere.gpu_resident_bytes = merge_on_card; phase("Validating against model (molecular replacement)"); - ModelValidationResult v = ValidateAgainstModel( - own ? own->merged : sm.merged, cell, *config_.model, logger, &data_sg, - /*probe_indexing_ambiguity=*/false, static_cast(config_.nthreads), - experiment_.GetWavelength_A(), report_shell_d_min, placed_elsewhere, nullptr, c.placed.get()); + ModelValidationResult v = ValidatePlacement( + own ? own->merged : sm.merged, cell, data_sg, *config_.model, *c.placed, + experiment_.GetWavelength_A(), report_shell_d_min, static_cast(config_.nthreads), + merge_on_card, result.model_validation ? &*result.model_validation : nullptr, logger); // Its maps go beside the files of the merge it was validated against. take_maps(own ? result.outputs->model_prior_maps : result.outputs->maps, v); v.maps_suffix = result.model_prior_file_suffix; if (v.ok) { - if (result.model_validation) { - const ModelValidationResult &rb = *result.model_validation; - v.fit_tested = rb.fit_tested; - v.model_fits = rb.model_fits; - v.null_r_work_mean = rb.null_r_work_mean; - v.null_r_work_sd = rb.null_r_work_sd; - v.r_work_sigma = rb.r_work_sigma; - v.null_replicates = rb.null_replicates; - } // Refined where it was chosen: in the group of the model's choice, against that choice's merge. if (own) { model_prior_file->placed = v.placed_model; @@ -5648,15 +5631,8 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { refine_merged.push_back(r); } // processing-inert: a refinement that fails is reported, and the run goes on to its other outputs - RefinementResult refined; - try { - refined = RefineModel(*p.model, refine_merged.empty() ? *placed_merged : refine_merged, - gemmi::UnitCell(p.cell), refine_sg, experiment_.GetWavelength_A(), logger, - settings); - } catch (const std::exception &e) { - refined = RefinementResult(); - refined.failure_reason = std::string("refinement failed: ") + e.what(); - } + RefinementResult refined = RefinePlacedModel(*p.model, refine_merged.empty() ? *placed_merged : refine_merged, + p.cell, refine_sg, experiment_.GetWavelength_A(), settings, logger); // The rival hypothesis (see RivalPlacement), refined the same way, and the two decided on the refined // R-free: each refinement has made its own twin decision against its own null, so what is compared is // the best each hypothesis can do. The validation's choice stands unless the rival beats it by @@ -5665,14 +5641,8 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { phase("Refining the model (rival symmetry hypothesis)"); const RivalPlacement &rv = *rival; const std::vector &rv_merged = rv.prior ? rv.prior->merged : *rv.merged; - RefinementResult other; - try { - other = RefineModel(*rv.placed.model, rv_merged, gemmi::UnitCell(rv.placed.cell), - rv.placed.space_group, experiment_.GetWavelength_A(), logger, settings); - } catch (const std::exception &e) { - other = RefinementResult(); - other.failure_reason = std::string("refinement failed: ") + e.what(); - } + RefinementResult other = RefinePlacedModel(*rv.placed.model, rv_merged, rv.placed.cell, rv.placed.space_group, + experiment_.GetWavelength_A(), settings, logger); ProcessResult::RivalRefinement rr; rr.space_group = rv.placed.space_group.xhm(); rr.merge = rv.prior ? "TWIN_SUBGROUP_MERGE" : "MERGE"; diff --git a/rugnux/StillsModel.h b/rugnux/StillsModel.h deleted file mode 100644 index 56c50ea95..000000000 --- a/rugnux/StillsModel.h +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include -#include -#include -#include -#include - -#include "Rugnux.h" -#include "../image_analysis/structure_refinement/MolecularReplacement.h" -#include "../image_analysis/structure_refinement/Refinement.h" - -// The model step of a merge of serial frames made in memory (StillsLiveMerge, MergeStills): the model placed -// against the merge, its maps and its refinement, as `rugnux --mode scale --model` places it and `rugnux --mode -// refine` refines it. For the live stills of the broker, which makes it at each provisional merge and after the -// last frame. No file is read or written here. - -// The group a merge of serial frames is merged in where a model is given: the collection's, where it states -// one; else the model's own, where the model's cell describes the run's consensus cell on the same axes -// (CellsCorrespond); else none, and `reason` says why the model is left out. -struct StillsModelGroup { - std::optional space_group; - std::string reason; -}; -StillsModelGroup StillsModelSpaceGroup(const std::optional &collection, const UnitCell &consensus, - const RugnuxModel &model); - -struct StillsModelSettings { - size_t nthreads = 1; - size_t gpu_resident_bytes = 0; // held on the card meanwhile (the analysis's indexers) - bool refine = true; - // Set from any thread: the step is cut - a refinement that runs is cut short, keeping the trials that ran - // (RefinementSettings::cancel), and a molecular replacement or a refinement that has not started is left out. - const std::atomic *cut = nullptr; - // Called as each part starts: "Model validation", "Molecular replacement", "Refinement" - std::function step; -}; - -struct StillsModelResult { - // The merge in the model's frame - the indexing the model chose (AdoptModelFrame), labelled in the group - // the model was placed in (its enantiomorph, where it was taken) - and its cell. - std::vector merged; - UnitCell cell{}; - gemmi::SpaceGroup space_group; - std::string placement = "NOT_PLACED"; // RIGID_BODY | MR | NOT_PLACED - bool warm = false; // placed from an earlier step's placement - std::optional validation; // its maps moved to `maps` - std::optional mr; - // Where the model was placed: the maps of the placement and the model as placed, in the merge's frame - std::shared_ptr maps; - std::optional placed_model; - std::optional refinement; // its model, maps and blobs where it ran - std::vector left_out; // what a cut left out - double placement_s = 0, refinement_s = 0; -}; - -// The model placed against `merged` (in `space_group`, the cell `cell`): by its rigid body where it fits these -// data as given, its indexing ambiguity probed and the merge put in the model's indexing; by molecular -// replacement where it does not (or is a prediction); then refined, where placed. `earlier`, the result of a -// step on an earlier merge of the same collection, is the warm start: the merge is put in its frame (the -// indexing a later merge happened to take is decided against it, ChooseReindex), the model goes in where it was -// placed there, and the verdict, the indexing and the molecular replacement's choice are its - no null, probe -// or search is made again. Where the warm placement does not validate, the step is made cold. -StillsModelResult PlaceAndRefineStillsModel(const std::vector &merged, const UnitCell &cell, - const gemmi::SpaceGroup &space_group, const RugnuxModel &model, - double wavelength_A, bool merge_friedel, - const std::vector &report_shell_d_min, - const StillsModelSettings &settings, const StillsModelResult *earlier, - Logger &logger); - -// The step's outcome in the terms of a ProcessResult (model_validation, model_prior, model_placement and -// refinement), for what reports one (LiveModel). -ProcessResult StillsModelReport(const StillsModelResult &result); diff --git a/tests/ModelValidationTest.cpp b/tests/ModelValidationTest.cpp index 2fee6e828..60e676d4c 100644 --- a/tests/ModelValidationTest.cpp +++ b/tests/ModelValidationTest.cpp @@ -34,7 +34,7 @@ #include "../writer/WriteModel.h" #include "../writer/RugnuxModelFile.h" #include "../image_analysis/scale_merge/ReindexAmbiguity.h" -#include "../rugnux/StillsModel.h" +#include "../rugnux/ModelStep.h" namespace { // A synthetic P1 cell with two carbon atoms - enough for a reader to produce a Structure with @@ -2070,12 +2070,70 @@ TEST_CASE("ModelStructureFactorsGPU_MapMatchesCPU", "[ModelValidation][gpu]") { #endif -// The model step of a live serial merge (StillsModel.h). A merge in the other indexing of a P4 crystal is put in the +// A model on other axes than the merge's takes the merge to them: its indices, cell, group and free set, as a rotation run +// takes its files. A later merge of the collection follows it there. +TEST_CASE("ModelStep_MergeTakesTheModelsAxes", "[ModelValidation][RugnuxLive]") { + Logger logger("ModelStep_MergeTakesTheModelsAxes"); + const auto data_model = WriteTemp("step_axes_data_test.pdb", ClusterPdb().c_str()); + auto obs = ModelReferenceIntensities(ReadModel(data_model), {}, {}, 2.5, logger); + REQUIRE(obs.size() > 500); + for (size_t i = 0; i < obs.size(); i++) { + obs[i].F = std::sqrt(std::max(0.0f, obs[i].I)); + obs[i].sigma = 0.02f * obs[i].I + 1.0f; + obs[i].sigmaF = 0.02f * obs[i].F + 0.1f; + obs[i].rfree_flag = (i % 20) == 0; + } + const UnitCell data_cell{.a = 30, .b = 34, .c = 38, .alpha = 90, .beta = 90, .gamma = 90}; + const gemmi::SpaceGroup data_sg = *gemmi::find_spacegroup_by_name("P 21 21 21"); + + // a' = b, b' = c, c' = a + std::string permuted = "CRYST1 34.000 38.000 30.000 90.00 90.00 90.00 P 21 21 21 4\n"; + std::istringstream in(ClusterPdb()); + std::string line; + char buf[96]; + while (std::getline(in, line)) { + if (line.compare(0, 4, "ATOM") != 0) + continue; + std::snprintf(buf, sizeof buf, "%s%8.3f%8.3f%8.3f%s", line.substr(0, 30).c_str(), std::stod(line.substr(38, 8)), + std::stod(line.substr(46, 8)), std::stod(line.substr(30, 8)), line.substr(54).c_str()); + permuted += buf; + permuted += "\n"; + } + permuted += "END\n"; + const auto path = WriteTemp("step_axes_model_test.pdb", permuted.c_str()); + const RugnuxModel model = ReadModel(path); + + ModelStepSettings settings; + settings.refine = false; + const ModelStepResult first = RunModelStep(obs, data_cell, data_sg, model, 1.0, true, {}, settings, nullptr, logger); + CHECK(first.placement == "RIGID_BODY"); + CHECK_FALSE(first.to_model_setting == gemmi::Op::identity()); + CHECK(first.cell.a == Catch::Approx(34.0)); + CHECK(first.cell.b == Catch::Approx(38.0)); + CHECK(first.cell.c == Catch::Approx(30.0)); + REQUIRE(first.placed_model); + CHECK(first.placed_model->cell.a == Catch::Approx(34.0)); + REQUIRE(first.validation); + CHECK(first.validation->r_work < 0.15); + CHECK(first.validation->model_fits); + + const ModelStepResult second = RunModelStep(obs, data_cell, data_sg, model, 1.0, true, {}, settings, &first, logger); + CHECK(second.warm); + CHECK(second.to_model_setting == first.to_model_setting); + CHECK(second.cell.a == Catch::Approx(34.0)); + REQUIRE(second.validation); + CHECK(second.validation->r_work < 0.15); + + std::filesystem::remove(data_model); + std::filesystem::remove(path); +} + +// The model step of a live serial merge (ModelStep.h). A merge in the other indexing of a P4 crystal is put in the // model's by the probe; a later merge of the same collection, again in the other indexing, is put in the earlier // step's frame and placed from where the model was, with no null or probe made again; a cut leaves out what has not // started. The group a merge without one is merged in is the model's where its cell is the run's. -TEST_CASE("StillsModel_PlacesWarmStartsAndCuts", "[ModelValidation][RugnuxLive]") { - Logger logger("StillsModel_PlacesWarmStartsAndCuts"); +TEST_CASE("ModelStep_PlacesWarmStartsAndCuts", "[ModelValidation][RugnuxLive]") { + Logger logger("ModelStep_PlacesWarmStartsAndCuts"); const auto path = WriteTemp("stills_model_test.pdb", ClusterPdb("CRYST1 34.000 34.000 38.000 90.00 90.00 90.00 P 4 4\n").c_str()); const RugnuxModel model = ReadModel(path); @@ -2094,16 +2152,16 @@ TEST_CASE("StillsModel_PlacesWarmStartsAndCuts", "[ModelValidation][RugnuxLive]" const auto other = ReindexMergedIntoAsu(own, laws.front(), p4, true); // The group: the collection's, else the model's on the run's cell, else none - CHECK(StillsModelSpaceGroup(*gemmi::find_spacegroup_by_name("P 4 2 2"), cell, model).space_group->xhm() == "P 4 2 2"); - CHECK(StillsModelSpaceGroup(std::nullopt, cell, model).space_group->xhm() == "P 4"); + CHECK(ModelStepSpaceGroup(*gemmi::find_spacegroup_by_name("P 4 2 2"), cell, model).space_group->xhm() == "P 4 2 2"); + CHECK(ModelStepSpaceGroup(std::nullopt, cell, model).space_group->xhm() == "P 4"); const UnitCell longer{.a = 34, .b = 34, .c = 45, .alpha = 90, .beta = 90, .gamma = 90}; - const auto none = StillsModelSpaceGroup(std::nullopt, longer, model); + const auto none = ModelStepSpaceGroup(std::nullopt, longer, model); CHECK_FALSE(none.space_group); CHECK_FALSE(none.reason.empty()); - StillsModelSettings settings; + ModelStepSettings settings; settings.refine = false; - const StillsModelResult first = PlaceAndRefineStillsModel(other, cell, p4, model, 1.0, true, {}, settings, nullptr, + const ModelStepResult first = RunModelStep(other, cell, p4, model, 1.0, true, {}, settings, nullptr, logger); REQUIRE(first.validation); CHECK(first.placement == "RIGID_BODY"); @@ -2117,7 +2175,7 @@ TEST_CASE("StillsModel_PlacesWarmStartsAndCuts", "[ModelValidation][RugnuxLive]" // A later merge, in the other indexing again, and refined settings.refine = true; - const StillsModelResult second = PlaceAndRefineStillsModel(other, cell, p4, model, 1.0, true, {}, settings, &first, + const ModelStepResult second = RunModelStep(other, cell, p4, model, 1.0, true, {}, settings, &first, logger); REQUIRE(second.validation); CHECK(second.warm); @@ -2132,7 +2190,7 @@ TEST_CASE("StillsModel_PlacesWarmStartsAndCuts", "[ModelValidation][RugnuxLive]" // replacement too std::atomic cut{true}; settings.cut = &cut; - const StillsModelResult cut_placed = PlaceAndRefineStillsModel(other, cell, p4, model, 1.0, true, {}, settings, + const ModelStepResult cut_placed = RunModelStep(other, cell, p4, model, 1.0, true, {}, settings, &first, logger); CHECK_FALSE(cut_placed.refinement); CHECK(cut_placed.left_out == std::vector{"refinement"}); @@ -2145,7 +2203,7 @@ TEST_CASE("StillsModel_PlacesWarmStartsAndCuts", "[ModelValidation][RugnuxLive]" unrelated[i].sigmaF = 0.02f * unrelated[i].F + 0.1f; unrelated[i].rfree_flag = (i % 20) == 0; } - const StillsModelResult cut_unplaced = PlaceAndRefineStillsModel(unrelated, cell, p4, model, 1.0, true, {}, + const ModelStepResult cut_unplaced = RunModelStep(unrelated, cell, p4, model, 1.0, true, {}, settings, nullptr, logger); CHECK(cut_unplaced.placement == "NOT_PLACED"); CHECK_FALSE(cut_unplaced.mr);