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);