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)
118 lines
6.4 KiB
C++
118 lines
6.4 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <complex>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#include "gemmi/grid.hpp"
|
|
#include "gemmi/model.hpp"
|
|
#include "gemmi/symmetry.hpp"
|
|
#include "gemmi/unitcell.hpp"
|
|
|
|
#include "../../common/Reflection.h" // MergedReflection
|
|
#include "RefinementRestraints.h" // RestraintSet
|
|
#include "RigidBodyRefine.h" // SymmetryComposition
|
|
|
|
// The X-ray side of restrained refinement (Refinement.h): the observed amplitudes, the model's structure
|
|
// factors and the maximum-likelihood target that compares them, with its gradient with respect to every
|
|
// atom's position and B.
|
|
|
|
// The reflections a refinement fits and scores: the observed amplitudes, in the group's reciprocal ASU, that
|
|
// the refinement grid composes (SymmetryComposition) - every observed one in practice.
|
|
struct RefinementReflections {
|
|
std::vector<gemmi::Miller> hkl;
|
|
std::vector<float> fo, sigma;
|
|
std::vector<char> free, centric;
|
|
std::vector<float> epsilon;
|
|
std::vector<double> inv_d2;
|
|
size_t Size() const { return hkl.size(); }
|
|
};
|
|
|
|
// The French-Wilson amplitudes of `merged` (NaN dropped) into the ASU of `sg`, those the grid of
|
|
// RigidBodyZoneGrid(cell, sg, d_min) composes.
|
|
RefinementReflections PrepareRefinementReflections(const std::vector<MergedReflection> &merged,
|
|
const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg,
|
|
double d_min);
|
|
|
|
// What turns the atoms' structure factors F into the model the target compares with the data, per reflection:
|
|
// F_model = k_total (F + fmask),
|
|
// with k_total the overall scale and anisotropic B and fmask the bulk solvent already scaled by k_sol
|
|
// exp(-b_sol s^2/4); and the two parameters of the likelihood, D and the variance. Fixed for one macro-cycle.
|
|
struct XrayRowModel {
|
|
std::vector<float> k_total;
|
|
std::vector<std::complex<float>> fmask;
|
|
std::vector<float> d, var;
|
|
};
|
|
|
|
// The ML parameters per reflection from the free reflections: D and beta fitted by maximum likelihood in
|
|
// resolution bins of the free set and interpolated between the bins' centres; the variance of a reflection is
|
|
// epsilon beta + sigma^2. `fmodel` is k_total (F + fmask), without D.
|
|
// Following Lunin & Skovoroda (1995) Acta Cryst. A51, 880-887, and Afonine et al. (2012) Acta Cryst. D68, 352-367
|
|
void EstimateLikelihoodParameters(const RefinementReflections &refl, const std::vector<std::complex<float>> &fmodel,
|
|
XrayRowModel &rows);
|
|
|
|
// R-work and R-free of k_total (F + fmask) against the observations.
|
|
struct RFactors {
|
|
double r_work = 1, r_free = 1;
|
|
int n_work = 0, n_free = 0;
|
|
};
|
|
RFactors ComputeRFactors(const RefinementReflections &refl, const std::vector<std::complex<float>> &fmodel);
|
|
|
|
// What evaluates the targets for a set of atom positions and B: the CPU below, or a GPU (RefinementGPU.h). The
|
|
// atoms are the model's, all of them, in model order; the grid and the reflections are fixed when the engine is
|
|
// made, the row model each macro-cycle, the blur where SetBlur is called.
|
|
class RefinementEngine {
|
|
public:
|
|
virtual ~RefinementEngine() = default;
|
|
// The blur the atoms are gridded with (DensityCalculator's), and the matching unblur of their transform.
|
|
// Every atom's B must stay above what the blur was chosen for (RefinementBlur) while it is in use.
|
|
virtual void SetBlur(double blur) = 0;
|
|
virtual void SetRowModel(const XrayRowModel &rows) = 0;
|
|
// F of the atoms at the reflections.
|
|
virtual void Fcalc(const gemmi::Model &model, std::vector<std::complex<float>> &f) = 0;
|
|
// The bulk-solvent mask's structure factors at the reflections (unscaled).
|
|
virtual void Fmask(const gemmi::Model &model, std::vector<std::complex<float>> &f) = 0;
|
|
// The sum of -ln L over the working reflections and, with `grad`, its derivatives with respect to every
|
|
// atom's x, y, z (Cartesian, A) and B (A^2), in model order.
|
|
virtual double Evaluate(const gemmi::Model &model, std::vector<std::array<double, 4>> *grad) = 0;
|
|
virtual bool OnGPU() const = 0;
|
|
|
|
// Restraint set `which` (0: the geometry and the repulsions, 1: the B similarity) and its sum of E at xyzb
|
|
// (x, y, z, B per atom) with, given `grad`, dE/d(x, y, z, B) per atom. On the CPU unless the engine has its own.
|
|
virtual void SetRestraints(int which, const RestraintSet &set) { restraints_[which] = set; }
|
|
virtual double EvaluateRestraints(int which, const std::vector<std::array<double, 4>> &xyzb,
|
|
std::vector<std::array<double, 4>> *grad) {
|
|
if (grad != nullptr)
|
|
grad->assign(xyzb.size(), {0, 0, 0, 0});
|
|
return ::EvaluateRestraints(restraints_[which], xyzb, grad);
|
|
}
|
|
|
|
private:
|
|
RestraintSet restraints_[2];
|
|
};
|
|
|
|
// The blur for a model whose smallest B may go as low as b_floor: DensityCalculator's
|
|
// set_refmac_compatible_blur() for that B, on the refinement grid of d_min.
|
|
double RefinementBlur(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min, double b_floor);
|
|
|
|
// The CPU engine: the density of one copy of the atoms on the grid of RigidBodyZoneGrid(cell, sg, d_min)
|
|
// (PutModelDensityOnGrid), its transform and the crystal's symmetry composed in reciprocal space
|
|
// (SymmetryComposition). The gradient by the FFT method: the derivative of the target with respect to each
|
|
// structure factor, taken back to real space by one inverse transform, is a map; each atom's derivatives are
|
|
// that map summed against the derivatives of its own density.
|
|
// Following Agarwal (1978) Acta Cryst. A34, 791-809
|
|
std::unique_ptr<RefinementEngine> MakeRefinementEngineCPU(const gemmi::UnitCell &cell,
|
|
const gemmi::SpaceGroup &sg, double d_min,
|
|
const RefinementReflections &refl, size_t nthreads);
|
|
|
|
// The derivative map of the X-ray target on a grid (the FFT method's map), summed against each atom's density
|
|
// derivatives: dT/d(x, y, z, B) per atom. `map` is u fastest on the grid of RigidBodyZoneGrid(), the atoms
|
|
// gridded with `blur`. Exposed for the tests.
|
|
void AtomGradientsFromMap(const gemmi::Grid<float> &map, const gemmi::Model &model, double blur, size_t nthreads,
|
|
std::vector<std::array<double, 4>> &grad);
|