Files
Jungfraujoch/image_analysis/structure_refinement/Refinement.h
T
leonarski_f b4b68d368f Restrained refinement on the GPU: ML amplitude target, FFT gradient, dictionary restraints, R1 + R2 driver, blobs
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)
2026-10-25 07:20:32 +01:00

104 lines
4.9 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "gemmi/grid.hpp"
#include "gemmi/model.hpp"
#include "gemmi/symmetry.hpp"
#include "gemmi/unitcell.hpp"
#include "../../common/Reflection.h" // MergedReflection
#include "DensityBlobs.h"
#include "ModelValidation.h" // ModelMaps
#include "RefinementRestraints.h" // GeometryStatistics
class Logger;
// Refinement of an atomic model already placed against the data (ValidateAgainstModel's placed model, or a
// molecular-replacement placement), in two stages:
//
// 1. Placement: each chain as a rigid body, then one B shift per residue, then the bulk solvent and the
// scale. Needs no dictionary.
// 2. Restrained: every atom's position and isotropic B, against the maximum-likelihood amplitude target with
// the geometry the dictionary describes (MonomerLibrary.h: bonds, angles, planes, chiral volumes,
// torsions, repulsions; B similarity of bonded atoms). Residues it does not describe are held fixed and
// stay in the structure factors and the bulk-solvent mask. The weight of the X-ray term against the
// geometry is chosen from a ladder of full refinements, by R-free among those whose geometry stays within
// bounds; then whether, and at what weight, to refine the B factors as well, the same way.
//
// The library reads and writes nothing: the model and the reflections come in, the refined model, the
// statistics and the map coefficients go out. Nothing it finds feeds back into processing decisions.
struct RefinementSettings {
bool restrained = true; // stage 2; false stops after the placement stage
int macro_cycles = 5;
int xyz_iterations = 25; // L-BFGS iterations per macro-cycle
int b_iterations = 15;
// The positions' X-ray weight, times the gradient-balanced one (BalancedWeight), tried in this order.
std::vector<double> weight_ladder = {0.0078125, 0.015625, 0.03125, 0.0625};
// The X-ray weights of the B refinement tried next, times the positions' chosen weight.
std::vector<double> adp_ladder = {1.0, 4.0, 16.0};
bool riding_hydrogens = true; // the dictionary's hydrogens added, riding on their parents
bool gpu = true;
size_t nthreads = 1;
size_t gpu_resident_bytes = 0; // held on the card by what runs beside
};
// One rung of the weight ladder.
struct RefinementWeightTrial {
double weight = 0; // the X-ray term's weight
double adp_weight = 0; // the B refinement's X-ray weight; 0: the B factors were held
double r_work = 1, r_free = 1;
GeometryStatistics geometry;
bool within_bounds = false; // the geometry bounds the choice is made inside
};
struct RefinementResult {
bool ok = false;
std::string failure_reason;
bool on_gpu = false;
double seconds = 0;
double d_min = 0;
int n_work = 0, n_free = 0;
// The model as given, scaled to these reflections (hydrogens removed, B made isotropic).
double r_work_start = 1, r_free_start = 1;
// After the placement stage.
int rigid_bodies = 0;
double r_work_placement = 1, r_free_placement = 1;
// After the restrained stage (NaN where it did not run).
bool restrained = false;
double r_work = NAN, r_free = NAN;
double weight = 0;
double adp_weight = 0; // 0: the B factors kept as placed
std::vector<RefinementWeightTrial> trials;
GeometryStatistics geometry_start, geometry;
int atoms = 0, refined_atoms = 0;
int restrained_residues = 0, fixed_residues = 0;
std::vector<std::string> fixed_names;
double k_sol = 0, b_sol = 0;
double mean_fom = 0;
std::shared_ptr<gemmi::Structure> model; // the refined model, in the data's cell and frame
std::shared_ptr<ModelMaps> maps; // its 2mFo-DFc and mFo-DFc coefficients (as _maps.mtz holds them)
// The mFo-DFc map of the refined model and its rms, for finding what it leaves unexplained (DensityBlobs.h).
std::shared_ptr<gemmi::Grid<float>> difference_map;
double difference_map_rms = 0;
// The anomalous difference map on the refined model's phases, where the merge kept the Bijvoet pairs.
std::shared_ptr<gemmi::Grid<float>> anomalous_map;
double anomalous_map_rms = 0;
// What the refined model leaves unexplained in the difference map (FindDensityBlobs).
std::vector<DensityBlob> blobs;
};
// `placed`: the model in the data's cell and space group. `merged`: the merge, with French-Wilson amplitudes
// and free flags, in the same group.
RefinementResult RefineModel(const gemmi::Structure &placed, const std::vector<MergedReflection> &merged,
const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double wavelength_A,
Logger &logger, const RefinementSettings &settings);