RefineModel (Refinement.h) refines a placed model against the merge, reading and writing nothing: - placement stage (R1): every chain of 50+ atoms as a rigid body at once (L-BFGS on six parameters per chain, their gradient from the per-atom one through Ceres jets), then one B shift per residue; - restrained stage (R2): positions and isotropic B of every atom of a residue the embedded dictionary describes, the rest held fixed but kept in Fcalc and the mask; riding hydrogens added by gemmi's topology, moved with their parents, re-placed every macro-cycle. Macro-cycles refit the mask, the scale and bulk solvent (FitModelScale / ModelScaleGPUPool) and the likelihood's D and beta (ML on the free set in resolution bins), rebuild the repulsion pairs (symmetry mates included), then L-BFGS on positions and, where chosen, on B. - weights: a ladder of full refinements at multiples of the gradient-balanced X-ray weight, chosen by R-free within bond/angle bounds (ties to the tighter geometry), then the B refinement tried at its own weights and kept only where it beats B held by more than the tie. The target and its gradient (RefinementXray, RefinementGPU): the Rice/centric amplitude likelihood (RefinementMath.h, shared host/device code), the gradient by Agarwal's FFT method - the adjoint of the reciprocal-space symmetry composition into a half-l (CPU) or half-u (cuFFT) spectrum, one inverse real transform, then each atom's density derivatives (xyz and B) summed over its box. On the GPU everything per evaluation stays on the device: ModelDensityGPU's gather, r2c, a fused compose + likelihood kernel, a CSR gather for the adjoint (no atomics), c2r, one warp per atom for the gradient; the restraints are one kernel per restraint plus a per-atom gather of gradient slots; cub reductions. Deterministic. Restraints (RefinementRestraints): bonds, angles, torsions (with esd, no phi/psi, no alternative sugar puckers), chiral volumes, least-squares planes from gemmi's Topo; repulsions beyond 1-4 (vdW radii from the dictionary's energy library, 2.6 A for H-bond pairs, 1.9 A metal-N/O/S); B similarity of bonded and angle-related atoms. DensityBlobs (D1): gemmi's flood-fill blobs of mFo-DFc above 3 sigma peaking above 4 sigma, placed beside the model and labelled WATER / ION / LIGAND / MODEL_ERROR by distance, contacts, volume and the anomalous map. tools/rugnux_refine_test refines a model against a finished run's p.mtz (developer utility, not installed). Tests: likelihood derivative, the embedded library, restraint values on ideal residues and every restraint kind's gradient against finite differences (CPU, and the GPU against the CPU), the X-ray gradient against finite differences in P1, P212121, C2 and P6122 (CPU and GPU), and a synthetic refinement that recovers a shaken model. Credits in docs/ACKNOWLEDGEMENT.md and at the algorithms. Measured on the open arm's battery outputs against REFMAC 5.8, 10 cycles, same p.mtz, free set and start model (R-free): 5reo 0.2228 vs 0.2236, 8dyz 0.1733 vs 0.1651, 9chw 0.1995 vs 0.1969 (CPU engine). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu (cherry picked from commit 8efe6f2322b3bfdae580bb34f256e67730dfc3c2)
152 lines
6.6 KiB
C++
152 lines
6.6 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
// Refinement (image_analysis/structure_refinement/Refinement.h) of a model against a rugnux merged MTZ, without
|
|
// processing anything: for measuring the refinement on finished runs (a battery run's p.mtz and p_model.cif)
|
|
// against another program on the same files. Prints the outcome, and one JSON line for scripts.
|
|
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include <gemmi/mmread_gz.hpp>
|
|
#include <gemmi/mtz.hpp>
|
|
#include <gemmi/to_cif.hpp>
|
|
#include <gemmi/to_mmcif.hpp>
|
|
#include <fstream>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "../common/CUDAWrapper.h"
|
|
#include "../common/Logger.h"
|
|
#include "../image_analysis/structure_refinement/Refinement.h"
|
|
|
|
namespace {
|
|
|
|
void usage() {
|
|
std::cout << "Usage: rugnux_refine_test [options] <merged.mtz> <model.cif|.pdb>\n"
|
|
" -N <threads> CPU threads (default 8)\n"
|
|
" --no-gpu the CPU engine\n"
|
|
" --placement-only rigid bodies and group B only\n"
|
|
" --cycles <n> macro-cycles per weight (default as in RefinementSettings)\n"
|
|
" --xyz-iterations <n>, --b-iterations <n> L-BFGS iterations per macro-cycle\n"
|
|
" --ladder <a,b,...> the weight ladder, times the gradient-balanced weight\n"
|
|
" -o <file.cif> write the refined model\n";
|
|
}
|
|
|
|
std::vector<double> ParseList(const std::string &s) {
|
|
std::vector<double> out;
|
|
std::stringstream ss(s);
|
|
std::string item;
|
|
while (std::getline(ss, item, ','))
|
|
out.push_back(std::stod(item));
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char **argv) {
|
|
RefinementSettings settings;
|
|
settings.nthreads = 8;
|
|
std::string out_path;
|
|
std::vector<std::string> files;
|
|
for (int i = 1; i < argc; i++) {
|
|
const std::string a = argv[i];
|
|
if (a == "-N" && i + 1 < argc)
|
|
settings.nthreads = std::stoul(argv[++i]);
|
|
else if (a == "--no-gpu") {
|
|
settings.gpu = false;
|
|
hide_gpus();
|
|
} else if (a == "--placement-only")
|
|
settings.restrained = false;
|
|
else if (a == "--cycles" && i + 1 < argc)
|
|
settings.macro_cycles = std::stoi(argv[++i]);
|
|
else if (a == "--xyz-iterations" && i + 1 < argc)
|
|
settings.xyz_iterations = std::stoi(argv[++i]);
|
|
else if (a == "--adp-ladder" && i + 1 < argc)
|
|
settings.adp_ladder = ParseList(argv[++i]);
|
|
else if (a == "--b-iterations" && i + 1 < argc)
|
|
settings.b_iterations = std::stoi(argv[++i]);
|
|
else if (a == "--ladder" && i + 1 < argc)
|
|
settings.weight_ladder = ParseList(argv[++i]);
|
|
else if (a == "-o" && i + 1 < argc)
|
|
out_path = argv[++i];
|
|
else if (!a.empty() && a[0] == '-') {
|
|
usage();
|
|
return EXIT_FAILURE;
|
|
} else
|
|
files.push_back(a);
|
|
}
|
|
if (files.size() != 2) {
|
|
usage();
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
gemmi::Mtz mtz = gemmi::read_mtz_file(files[0]);
|
|
const gemmi::SpaceGroup *sg = mtz.spacegroup;
|
|
if (sg == nullptr) {
|
|
std::cerr << "No space group in " << files[0] << std::endl;
|
|
return EXIT_FAILURE;
|
|
}
|
|
const int ih = mtz.column_with_label("H")->idx, ik = mtz.column_with_label("K")->idx,
|
|
il = mtz.column_with_label("L")->idx, iF = mtz.column_with_label("F")->idx,
|
|
isF = mtz.column_with_label("SIGF")->idx, ifree = mtz.column_with_label("FreeR_flag")->idx;
|
|
const gemmi::Mtz::Column *fp = mtz.column_with_label("F(+)"), *fm = mtz.column_with_label("F(-)");
|
|
std::vector<MergedReflection> merged;
|
|
for (size_t r = 0; r < static_cast<size_t>(mtz.nreflections); r++) {
|
|
const float *row = &mtz.data[r * mtz.columns.size()];
|
|
MergedReflection m;
|
|
m.h = static_cast<int>(row[ih]);
|
|
m.k = static_cast<int>(row[ik]);
|
|
m.l = static_cast<int>(row[il]);
|
|
m.F = row[iF];
|
|
m.sigmaF = row[isF];
|
|
m.rfree_flag = row[ifree] == 0;
|
|
m.d = static_cast<float>(mtz.cell.calculate_d({{m.h, m.k, m.l}}));
|
|
if (fp != nullptr && fm != nullptr) {
|
|
m.F_plus = row[fp->idx];
|
|
m.F_minus = row[fm->idx];
|
|
}
|
|
merged.push_back(m);
|
|
}
|
|
gemmi::Structure st = gemmi::read_structure_gz(files[1]);
|
|
|
|
Logger logger("rugnux_refine_test");
|
|
const RefinementResult r = RefineModel(st, merged, mtz.cell, *sg, 1.0, logger, settings);
|
|
if (!r.ok) {
|
|
std::cerr << "Refinement failed: " << r.failure_reason << std::endl;
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (!out_path.empty())
|
|
{
|
|
std::ofstream f(out_path);
|
|
gemmi::cif::write_cif_to_stream(f, gemmi::make_mmcif_document(*r.model));
|
|
}
|
|
|
|
int kinds[5] = {0, 0, 0, 0, 0};
|
|
for (const DensityBlob &b : r.blobs)
|
|
kinds[static_cast<int>(b.kind)]++;
|
|
nlohmann::json blobs = nlohmann::json::array();
|
|
for (const DensityBlob &b : r.blobs)
|
|
blobs.push_back({{"kind", DensityBlobKindName(b.kind)}, {"x", b.centroid.x}, {"y", b.centroid.y},
|
|
{"z", b.centroid.z}, {"peak", b.peak_sigma}, {"volume", b.volume}, {"score", b.score},
|
|
{"nearest", b.nearest_atom}, {"distance", b.nearest_atom_distance},
|
|
{"anomalous", b.anomalous_sigma}});
|
|
nlohmann::json j = {{"d_min", r.d_min}, {"gpu", r.on_gpu}, {"seconds", r.seconds},
|
|
{"r_work_start", r.r_work_start}, {"r_free_start", r.r_free_start},
|
|
{"r_work_placement", r.r_work_placement}, {"r_free_placement", r.r_free_placement},
|
|
{"r_work", r.r_work}, {"r_free", r.r_free}, {"weight", r.weight}, {"adp_weight", r.adp_weight},
|
|
{"rmsd_bonds", r.geometry.rmsd_bonds}, {"rmsd_angles", r.geometry.rmsd_angles},
|
|
{"rmsd_bonds_start", r.geometry_start.rmsd_bonds},
|
|
{"rmsd_angles_start", r.geometry_start.rmsd_angles}, {"clashes", r.geometry.clashes},
|
|
{"atoms", r.atoms}, {"refined_atoms", r.refined_atoms}, {"fixed_residues", r.fixed_residues},
|
|
{"blobs", blobs}};
|
|
nlohmann::json trials = nlohmann::json::array();
|
|
for (const RefinementWeightTrial &t : r.trials)
|
|
trials.push_back({{"weight", t.weight}, {"adp_weight", t.adp_weight}, {"r_work", t.r_work}, {"r_free", t.r_free},
|
|
{"rmsd_bonds", t.geometry.rmsd_bonds}, {"rmsd_angles", t.geometry.rmsd_angles}});
|
|
j["trials"] = trials;
|
|
std::cout << "JSON " << j.dump() << std::endl;
|
|
return EXIT_SUCCESS;
|
|
}
|