Files
Jungfraujoch/image_analysis/structure_refinement/RefinementXray.h
T
leonarski_f 8718065a7c Refinement GPU: the bulk-solvent mask on its own fine grid on the card
The score draws the mask on a grid of its own (min(d_min/3, 0.4 A), BulkSolventMask) on the host; on a GPU
engine that was a CPU mask and transform every cycle, per trial slot on one thread. Now the engine masks
on that grid itself:

- RefinementGPUSetup::mask_grid (BulkSolventMask::MaskGrid, the grid the host mask uses); where it is not
  the structure factors' grid the engine reserves a grid, a spectrum and an R2C plan for it (counted in
  DeviceBytes and the FFT work area), and Fmask masks (ModelMaskGPU), transforms and reads the rows there.
  Refit takes the engine's Fmask on a GPU, BulkSolventMask on the CPU.
- ModelMaskGPU's shrink: each edge point marks its stencil as solvent instead of every macromolecule point
  searching its stencil for an edge - the same idempotent stores to the same points, so the same mask to
  the bit (ModelMaskGPU_* tests unchanged), and a thin shell of points visited instead of half the grid.
- BulkSolventMask finds its symmetry orbits at its first mask: a GPU refinement never draws one on the host
  (1.1 s of trial-slot setup on the 3.6 A set).

The card's mask is gemmi's but at points exactly on an atom's radius (ModelMaskGPU.h): R-work/R-free of the
14 parity sets on the GPU are the CPU engine's to 4 digits. Refinement deterministic run to run on the GPU
(7 timing sets, refined model and report identical over 5 runs of successive builds that did not change
results).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit 89b5d00907c9ad99e8b389f6272a3724f981cd10)
2026-10-26 01:38:24 +01:00

177 lines
9.5 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 <chrono>
#include <cstdint>
#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
class Logger;
// 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 flat bulk-solvent model's mask (SolventMasker with the Refmac radii, PutMaskOnGrid) and its structure factors
// at the reflections, on a grid of its own rather than the atoms' density grid: the mask's edge is set by the
// probe and shrink radii (1.0 and 0.8 A), which a grid at d_min / 3 cannot resolve at low resolution - at 3 A
// its 1 A step leaves no grid point within the shrink radius and the shrink is skipped. The step is therefore
// d_min / 3 or MASK_SPACING_MAX, whichever is finer - where the mask grid stays within MASK_POINTS_MAX points; a
// cell too large for that gets the spacing that fills it. The R-factors of low-resolution models settle at a
// step of 0.4 A (within 0.001 of 0.3 A) and move by up to 0.005 between 0.4 and 0.8 A.
// The flat solvent model of Fokine & Urzhumtsev (2002) Acta Cryst. D58, 1387-1392
class BulkSolventMask {
public:
static constexpr double MASK_SPACING_MAX = 0.4; // A
static constexpr double MASK_POINTS_MAX = 1 << 26;
BulkSolventMask(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min, size_t nthreads);
// The mask's grid (sizes and spacing set, values not allocated) - also the GPU engine's (RefinementGPU.cpp).
static gemmi::Grid<float> MaskGrid(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min);
// Fmask (unscaled) of `model` at `hkl`.
void Fmask(const gemmi::Model &model, const std::vector<gemmi::Miller> &hkl,
std::vector<std::complex<float>> &f) const;
double Spacing() const { return spacing_; }
private:
gemmi::Grid<float> grid_;
mutable std::vector<size_t> orbit_leaders_; // made at the first Fmask()
size_t nthreads_;
double spacing_;
};
// 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, size_t nthreads = 1);
// 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);
// x, y, z and B of every atom of `model`, in model order.
std::vector<std::array<double, 4>> ModelXyzB(const gemmi::Model &model);
// 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);
}
// Evaluate() and EvaluateRestraints(which) at the atoms of `model` as they are, in one go: returns the X-ray
// target, and the restraints' sum in `restraints`.
virtual double EvaluateWithRestraints(const gemmi::Model &model, int which, std::vector<std::array<double, 4>> *grad,
std::vector<std::array<double, 4>> *restraint_grad, double &restraints) {
const double xray = Evaluate(model, grad);
restraints = EvaluateRestraints(which, ModelXyzB(model), restraint_grad);
return xray;
}
private:
RestraintSet restraints_[2];
};
// The time a refinement spends in one stage, summed over every call (and over threads) until Log(): the scope of
// an instance is one call, and an NVTX range of the stage's name.
class RefinementStage {
public:
explicit RefinementStage(const char *name);
~RefinementStage();
RefinementStage(const RefinementStage &) = delete;
RefinementStage &operator=(const RefinementStage &) = delete;
// "TIMING refine.<stage> <seconds> <calls>" per stage at debug level, then the sums start again.
static void Log(Logger &logger);
private:
const char *name_;
std::chrono::steady_clock::time_point start_;
uint64_t nvtx_;
};
// 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);