Merge broker-rugnux-live into rc176: rugnux in jfjoch_broker as a live thread during a DECTRIS rotation collection (off by default), the rugnux library without file I/O (results in ProcessResult::outputs, the CLI writes them through JFJochRugnuxOutput), END carrying the merged table, map coefficients and placed model

Branch reviewed at e16db07ff (review: owner's decisions accepted - _maps.mtz instead of .ccp4 in the
viewer, -o "" computes everything incl. MR/validation, Ctrl+C/error paths may leave fewer files, the
writer links gemmi/zlib) and merged at 55184eb09 with its review fixes (END size guard, frame drop as
in Lite, outputs released before re-runs, pool priority, docs, frontend gating).

Conflicts: rugnux/Rugnux.h (kept rc176's host_memory_budget_bytes, output_prefix removed as on the
branch), rugnux/RugnuxPipeline.h (MtzTable future + rc176's GPUMerge pointer), MolecularReplacement.h/.cpp
(rc176's GPU cost model, packed transform and split functions with the branch's RugnuxModel signature;
search_model with rc176's other_budgets, result.model_name). tests/RugnuxLargeTest.cpp: rc176's
host-budget test writes its files through WriteRugnuxOutputs now that the library writes none.
Generated code (broker/gen, frontend client, Python client docs, Redoc) regenerated from the merged
jfjoch_api.yaml with update_version.sh (version unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-17 21:01:38 +02:00
commit 78c2a69267
130 files changed
+7612 -1010

No files matched your search

@@ -18,15 +18,11 @@
#include <tuple>
#include <random>
#include <unordered_map>
#include <fstream>
#include <fftw3.h>
#include <nlohmann/json.hpp>
#include <gemmi/mmread_gz.hpp> // read_structure_gz
#include <gemmi/modify.hpp> // remove_hydrogens, remove_alternative_conformations
#include <gemmi/polyheur.hpp> // setup_entities, remove_ligands_and_waters
#include <gemmi/read_cif.hpp> // read_cif_gz
#include <gemmi/dencalc.hpp> // DensityCalculator
#include <gemmi/it92.hpp> // IT92 x-ray form factors
#include <gemmi/scaling.hpp> // Scaling (bulk solvent + anisotropic B)
@@ -906,19 +902,10 @@ void finish_search_model(SearchModel &m) {
} // namespace
SearchModel PrepareSearchModel(const std::string &path) {
SearchModel PrepareSearchModel(const RugnuxModel &model_in) {
SearchModel out;
bool modelcif = false;
try {
out.st = gemmi::read_structure_gz(path, gemmi::CoorFormat::Detect);
if (out.st.input_format == gemmi::CoorFormat::Mmcif) {
const gemmi::cif::Document doc = gemmi::read_cif_gz(path);
modelcif = !doc.blocks.empty() && doc.blocks[0].has_mmcif_category("_ma_qa_metric");
}
} catch (const std::exception &e) {
out.failure_reason = std::string("cannot read the model: ") + e.what();
return out;
}
const bool modelcif = model_in.modelcif;
out.st = model_in.structure;
if (out.st.models.empty()) {
out.failure_reason = "the model has no atoms";
return out;
@@ -992,39 +979,15 @@ SearchModel PrepareSearchModel(const std::string &path) {
// --- domains from the predicted aligned error ---
std::string PaePathFor(const std::string &model_path) {
const size_t slash = model_path.find_last_of("/\\");
const std::string dir = slash == std::string::npos ? std::string() : model_path.substr(0, slash + 1);
const std::string name = model_path.substr(dir.size());
const size_t at = name.rfind("-model_v");
if (at == std::string::npos)
return {};
const size_t dot = name.find('.', at);
const std::string version = name.substr(at + 8, dot == std::string::npos ? std::string::npos : dot - at - 8);
return dir + name.substr(0, at) + "-predicted_aligned_error_v" + version + ".json";
}
// A prediction cut into domains by its predicted aligned error before molecular replacement, as Oeffner et
// al. (2022) Acta Cryst. D78, 1303-1314 do; here by average linkage.
std::vector<std::vector<int>> PaeDomains(const std::string &pae_path, const SearchModel &model) {
std::vector<std::vector<int>> PaeDomains(const std::vector<std::vector<double>> &pae, const SearchModel &model) {
// Two domains a prediction does not place relative to each other with confidence are this far apart in
// mean PAE (within one domain it is a few angstroms); a domain is at least this many residues.
constexpr double PAE_DOMAIN_A = 10.0;
constexpr int DOMAIN_MIN_RESIDUES = 40;
// The clustering starts from runs of this many consecutive residues kept.
constexpr int PIECE = 5;
std::vector<std::vector<double>> pae;
try {
std::ifstream in(pae_path);
if (!in)
return {};
const nlohmann::json j = nlohmann::json::parse(in);
const nlohmann::json &m = j.is_array() ? j.at(0).at("predicted_aligned_error") : j.at("predicted_aligned_error");
for (const auto &row : m)
pae.push_back(row.get<std::vector<double>>());
} catch (const std::exception &) {
return {};
}
const size_t n = pae.size();
if (n != static_cast<size_t>(model.residues_read)
|| std::any_of(pae.begin(), pae.end(), [n](const std::vector<double> &r) { return r.size() != n; }))
@@ -3576,7 +3539,7 @@ std::vector<TranslationPeak> SplitTranslationFunction(const std::vector<Molecula
}
#endif
MolecularReplacementResult RunMolecularReplacement(const std::string &model_path, const std::vector<MRMerge> &merges,
MolecularReplacementResult RunMolecularReplacement(const RugnuxModel &input, const std::vector<MRMerge> &merges,
Logger &logger, size_t nthreads, size_t gpu_resident_bytes) {
const auto t0 = std::chrono::steady_clock::now();
// What the search may hold on the card: the budget model validation keeps to. What the stream-ordered pool
@@ -3596,25 +3559,24 @@ MolecularReplacementResult RunMolecularReplacement(const std::string &model_path
#else
(void) gpu_resident_bytes;
#endif
SearchModel model = PrepareSearchModel(model_path);
SearchModel model = PrepareSearchModel(input);
if (!model.failure_reason.empty()) {
MolecularReplacementResult result;
result.model_path = model_path;
result.model_name = input.name;
result.residues_read = model.residues_read;
result.failure_reason = model.failure_reason;
return result;
}
model.description = "the whole model";
// Only a prediction has a predicted aligned error.
const std::string pae_path = model.predicted ? PaePathFor(model_path) : std::string();
const std::vector<std::vector<int>> domains = pae_path.empty() ? std::vector<std::vector<int>>()
: PaeDomains(pae_path, model);
const bool pae = model.predicted && input.pae.has_value();
const std::vector<std::vector<int>> domains = pae ? PaeDomains(*input.pae, model) : std::vector<std::vector<int>>();
const double t_model = seconds_since(t0);
if (!pae_path.empty())
logger.Info("Molecular replacement: {} rigid domain(s) in the predicted aligned error {}", domains.size(), pae_path);
if (pae)
logger.Info("Molecular replacement: {} rigid domain(s) in the predicted aligned error", domains.size());
MolecularReplacementResult result = search_model(model, merges, logger, nthreads, gpu_budget, other_budgets);
result.model_path = model_path;
result.model_name = input.name;
result.pae_domains = static_cast<int>(domains.size());
result.t_model += t_model;
// Where the whole model is placed nowhere and its prediction falls apart into domains whose relative
@@ -3664,7 +3626,7 @@ MolecularReplacementResult RunMolecularReplacement(const std::string &model_path
found[best].candidates[found[best].chosen].space_group.xhm());
} else if (best >= 0 && agree) {
MolecularReplacementResult chosen = std::move(found[best]);
chosen.model_path = model_path;
chosen.model_name = input.name;
chosen.pae_domains = result.pae_domains;
chosen.t_model = result.t_model;
chosen.t_transform = result.t_transform;