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 <noreply@anthropic.com>
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-07 15:06:01 +01:00
1 parent a6050681dc
commit 270ae432d8
11 files changed
+387 -267

No files matched your search

+4 -43
View File
@@ -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<MtzTable> coefficients;
if (o.maps)
coefficients = o.maps->coefficients;
std::shared_ptr<const RugnuxStructure> refined;
if (o.refined_model) {
std::optional<MtzTable> refined_coefficients;
if (o.refined_maps)
refined_coefficients = o.refined_maps->coefficients;
refined = std::make_shared<RugnuxStructure>(o.refined_model->model, o.refined_model->cell,
o.refined_model->space_group,
std::move(refined_coefficients));
}
live_status.Structure(std::make_shared<RugnuxStructure>(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<uint8_t[]>(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);
}
}
+51
View File
@@ -3,6 +3,14 @@
#include "RugnuxStructure.h"
#include <memory>
#include "spdlog/fmt/fmt.h"
#include "../common/JFJochException.h"
#include "../frame_serialize/CBORStream2Serializer.h"
#include "../rugnux/Rugnux.h"
#include <algorithm>
#include <cmath>
#include <complex>
@@ -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<MtzTable> coefficients;
if (maps)
coefficients = maps->coefficients;
std::shared_ptr<const RugnuxStructure> refined_structure;
if (refined) {
std::optional<MtzTable> refined_coefficients;
if (refined_maps)
refined_coefficients = refined_maps->coefficients;
refined_structure = std::make_shared<RugnuxStructure>(refined->model, refined->cell, refined->space_group,
std::move(refined_coefficients));
}
status.Structure(std::make_shared<RugnuxStructure>(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<uint8_t[]>(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);
}
}
+17
View File
@@ -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);
+14 -43
View File
@@ -385,7 +385,7 @@ const DiffractionExperiment &StillsGPUAnalysis::GetMergeExperiment() const {
return merge_experiment;
}
const std::optional<StillsModelResult> &StillsGPUAnalysis::GetModel() const {
const std::optional<ModelStepResult> &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<MtzTable> coefficients;
if (r.maps)
coefficients = r.maps->coefficients;
std::shared_ptr<const RugnuxStructure> refined;
if (r.refinement && r.refinement->ok && r.refinement->model) {
std::optional<MtzTable> refined_coefficients;
if (r.refinement->maps)
refined_coefficients = r.refinement->maps->coefficients;
refined = std::make_shared<RugnuxStructure>(r.refinement->model, r.placed_model->cell,
r.placed_model->space_group, std::move(refined_coefficients));
}
status->Structure(std::make_shared<RugnuxStructure>(r.placed_model->model, r.placed_model->cell,
r.placed_model->space_group, std::move(coefficients)),
std::move(refined));
std::optional<PlacedModelOutput> 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<uint8_t[]>(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);
}
+7 -7
View File
@@ -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<StillsMergeOutput> &GetMerged() const;
[[nodiscard]] const DiffractionExperiment &GetMergeExperiment() const;
// The model step on the merge of the collection, where one was made.
[[nodiscard]] const std::optional<StillsModelResult> &GetModel() const;
[[nodiscard]] const std::optional<ModelStepResult> &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<StillsMergeOutput> merged;
std::future<void> 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<const RugnuxModel> 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<StillsModelResult> model_placed; // the last step that placed the model, for the next to start from
std::optional<StillsModelResult> model_final;
std::optional<ModelStepResult> model_placed; // the last step that placed the model, for the next to start from
std::optional<ModelStepResult> model_final;
std::future<void> 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<std::atomic<bool>> 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();
+2 -2
View File
@@ -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
+110 -43
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "StillsModel.h"
#include "ModelStep.h"
#include <chrono>
#include <cmath>
@@ -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<double>(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<gemmi::SpaceGroup> &collection, const UnitCell &consensus,
bool MergeIntoBasis(std::vector<MergedReflection> &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<MergedReflection> &merged, const UnitCell &cell,
const gemmi::SpaceGroup &space_group, const RugnuxModel &model,
const gemmi::Structure &placed, double wavelength_A,
const std::vector<float> &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<MergedReflection> &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<gemmi::SpaceGroup> &collection, const UnitCell &consensus,
const RugnuxModel &model) {
if (collection)
return {collection, ""};
@@ -43,47 +91,53 @@ StillsModelGroup StillsModelSpaceGroup(const std::optional<gemmi::SpaceGroup> &c
return {*sg, ""};
}
StillsModelResult PlaceAndRefineStillsModel(const std::vector<MergedReflection> &merged, const UnitCell &cell,
ModelStepResult RunModelStep(const std::vector<MergedReflection> &merged, const UnitCell &cell,
const gemmi::SpaceGroup &space_group, const RugnuxModel &model,
double wavelength_A, bool merge_friedel,
const std::vector<float> &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<MergedReflection> 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<MergedReflection> &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<MergedReflection> 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<MergedReflection>
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<MergedReflection>
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<MergedReflection>
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;
+101
View File
@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <atomic>
#include <functional>
#include <optional>
#include <string>
#include <vector>
#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<MergedReflection> &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<MergedReflection> &merged, const UnitCell &cell,
const gemmi::SpaceGroup &space_group, const RugnuxModel &model,
const gemmi::Structure &placed, double wavelength_A,
const std::vector<float> &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<MergedReflection> &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<gemmi::SpaceGroup> space_group;
std::string reason;
};
ModelStepGroup ModelStepSpaceGroup(const std::optional<gemmi::SpaceGroup> &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<bool> *cut = nullptr;
// Called as each part starts: "Model validation", "Molecular replacement", "Refinement"
std::function<void(const std::string &)> 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<MergedReflection> 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<ModelValidationResult> validation; // its maps moved to `maps`
std::optional<MolecularReplacementResult> mr;
// Where the model was placed: the maps of the placement and the model as placed, in the merge's frame
std::shared_ptr<ModelMaps> maps;
std::optional<PlacedModelOutput> placed_model;
std::optional<RefinementResult> refinement; // its model, maps and blobs where it ran
std::vector<std::string> 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<MergedReflection> &merged, const UnitCell &cell,
const gemmi::SpaceGroup &space_group, const RugnuxModel &model,
double wavelength_A, bool merge_friedel,
const std::vector<float> &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);
+11 -41
View File
@@ -3,6 +3,7 @@
#include <cstdlib>
#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<size_t>(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<size_t>(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<size_t>(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<MergedReflection> &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";
-76
View File
@@ -1,76 +0,0 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <atomic>
#include <functional>
#include <optional>
#include <string>
#include <vector>
#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<gemmi::SpaceGroup> space_group;
std::string reason;
};
StillsModelGroup StillsModelSpaceGroup(const std::optional<gemmi::SpaceGroup> &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<bool> *cut = nullptr;
// Called as each part starts: "Model validation", "Molecular replacement", "Refinement"
std::function<void(const std::string &)> 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<MergedReflection> 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<ModelValidationResult> validation; // its maps moved to `maps`
std::optional<MolecularReplacementResult> mr;
// Where the model was placed: the maps of the placement and the model as placed, in the merge's frame
std::shared_ptr<ModelMaps> maps;
std::optional<PlacedModelOutput> placed_model;
std::optional<RefinementResult> refinement; // its model, maps and blobs where it ran
std::vector<std::string> 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<MergedReflection> &merged, const UnitCell &cell,
const gemmi::SpaceGroup &space_group, const RugnuxModel &model,
double wavelength_A, bool merge_friedel,
const std::vector<float> &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);
+70 -12
View File
@@ -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<bool> 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<std::string>{"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);