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)
This commit is contained in:
1 parent
e58453a693
commit
b4b68d368f
19 files changed
+4220
No files matched your search
@@ -508,6 +508,51 @@ molecular replacement; the error itself is AlphaFold's: J. Jumper, R. Evans, A.
|
||||
accurate protein structure prediction with AlphaFold" (2021), Nature 596, 583-589
|
||||
[doi:10.1038/s41586-021-03819-2](https://doi.org/10.1038/s41586-021-03819-2).
|
||||
|
||||
**Structure refinement** (`--refine`) — the placed model is refined against the maximum-likelihood
|
||||
amplitude target, the Rice distribution of an observed amplitude given the model's (its centric
|
||||
counterpart for a centric reflection): N. S. Pannu and R. J. Read, "Improved structure refinement
|
||||
through maximum likelihood" (1996), Acta Cryst. A52, 659-668
|
||||
[doi:10.1107/S0108767396004370](https://doi.org/10.1107/S0108767396004370); G. N. Murshudov,
|
||||
A. A. Vagin and E. J. Dodson, "Refinement of macromolecular structures by the maximum-likelihood method"
|
||||
(1997), Acta Cryst. D53, 240-255 [doi:10.1107/S0907444996012255](https://doi.org/10.1107/S0907444996012255).
|
||||
Its two parameters, D and the variance, are fitted by maximum likelihood on the free reflections in
|
||||
resolution bins: V. Y. Lunin and T. P. Skovoroda, "R-free likelihood-based estimates of errors for phases
|
||||
calculated from atomic models" (1995), Acta Cryst. A51, 880-887
|
||||
[doi:10.1107/S010876739500688X](https://doi.org/10.1107/S010876739500688X); V. Y. Lunin, P. V. Afonine and
|
||||
A. G. Urzhumtsev, "Likelihood-based refinement. I. Irremovable model errors" (2002), Acta Cryst. A58,
|
||||
270-282 [doi:10.1107/S0108767302001046](https://doi.org/10.1107/S0108767302001046). The gradient with
|
||||
respect to every atom is taken by the FFT method - the derivatives of the target with respect to the
|
||||
structure factors transformed into a map and summed against each atom's density derivatives:
|
||||
R. C. Agarwal, "A new least-squares refinement technique based on the fast Fourier transform algorithm"
|
||||
(1978), Acta Cryst. A34, 791-809 [doi:10.1107/S0567739478001618](https://doi.org/10.1107/S0567739478001618).
|
||||
The positions and B factors are minimised by L-BFGS (Ceres): D. C. Liu and J. Nocedal, "On the limited
|
||||
memory BFGS method for large scale optimization" (1989), Math. Program. 45, 503-528
|
||||
[doi:10.1007/BF01589116](https://doi.org/10.1007/BF01589116). The geometry restraints are those of the CCP4
|
||||
Monomer Library, a subset of which rugnux carries (see THIRD_PARTY_NOTICES.md): A. A. Vagin,
|
||||
R. A. Steiner, A. A. Lebedev, L. Potterton, S. McNicholas, F. Long and G. N. Murshudov, "REFMAC5
|
||||
dictionary: organization of prior chemical knowledge and guidelines for its use" (2004), Acta Cryst. D60,
|
||||
2184-2195 [doi:10.1107/S0907444904023510](https://doi.org/10.1107/S0907444904023510); applied to the model
|
||||
through GEMMI's topology (links, modifications, planes, chiral volumes, torsions): M. Wojdyr, "GEMMI: A
|
||||
library for structural biology" (2022), J. Open Source Softw. 7, 4200
|
||||
[doi:10.21105/joss.04200](https://doi.org/10.21105/joss.04200). The torsion derivatives follow
|
||||
A. Blondel and M. Karplus, "New formulation for derivatives of torsion angles and improper torsion angles
|
||||
in molecular mechanics: elimination of singularities" (1996), J. Comput. Chem. 17, 1132-1141
|
||||
[doi:10.1002/(SICI)1096-987X(19960715)17:9<1132::AID-JCC5>3.0.CO;2-T](https://doi.org/10.1002/(SICI)1096-987X(19960715)17:9%3C1132::AID-JCC5%3E3.0.CO;2-T).
|
||||
The B factors of bonded atoms are restrained to each other, as REFMAC5 restrains them (Murshudov et al.
|
||||
1997, above). The weight of the X-ray term against the geometry starts from the ratio of the two
|
||||
gradients and is chosen among several full refinements by R-free with the geometry held within bounds,
|
||||
as phenix.refine chooses it: P. V. Afonine, R. W. Grosse-Kunstleve, N. Echols, J. J. Headd,
|
||||
N. W. Moriarty, M. Mustyakimov, T. C. Terwilliger, A. Urzhumtsev, P. H. Zwart and P. D. Adams,
|
||||
"Towards automated crystallographic structure refinement with phenix.refine" (2012), Acta Cryst. D68,
|
||||
352-367 [doi:10.1107/S0907444912001308](https://doi.org/10.1107/S0907444912001308). R-free itself:
|
||||
A. T. Brünger, "Free R value: a novel statistical quantity for assessing the accuracy of crystal
|
||||
structures" (1992), Nature 355, 472-475 [doi:10.1038/355472a0](https://doi.org/10.1038/355472a0).
|
||||
|
||||
**Unmodelled density** — the blobs `--refine` lists are the connected regions of the difference map above
|
||||
a cutoff, found by flood fill as Coot's "unmodelled blobs" finds them, in GEMMI's implementation
|
||||
(`find_blobs_by_flood_fill`): P. Emsley, B. Lohkamp, W. G. Scott and K. Cowtan, "Features and development
|
||||
of Coot" (2010), Acta Cryst. D66, 486-501 [doi:10.1107/S0907444910007493](https://doi.org/10.1107/S0907444910007493).
|
||||
|
||||
## Software and computing methods
|
||||
|
||||
Decoding bitshuffle+LZ4 images on the GPU, rather than decompressing them on the host and uploading
|
||||
|
||||
@@ -18,6 +18,14 @@ ADD_LIBRARY(JFJochStructureRefinement STATIC
|
||||
$<$<BOOL:${JFJOCH_CUDA_AVAILABLE}>:ModelScaleGPU.cu> ModelScaleGPU.h
|
||||
SigmaA.cpp SigmaA.h
|
||||
MonomerLibrary.cpp MonomerLibrary.h
|
||||
RefinementMath.h
|
||||
RefinementRestraints.cpp RefinementRestraints.h
|
||||
RefinementXray.cpp RefinementXray.h
|
||||
Refinement.cpp Refinement.h
|
||||
DensityBlobs.cpp DensityBlobs.h
|
||||
$<$<BOOL:${JFJOCH_CUDA_AVAILABLE}>:RefinementGPU.cpp>
|
||||
$<$<BOOL:${JFJOCH_CUDA_AVAILABLE}>:RefinementGPU.cu>
|
||||
RefinementGPU.h RefinementGPUEngine.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/MonomerLibraryData.cpp)
|
||||
|
||||
# The subset of the CCP4 Monomer Library that refinement restrains with (monomers/, chosen by
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "DensityBlobs.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "gemmi/blob.hpp"
|
||||
#include "gemmi/neighbor.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double CUTOFF_SIGMA = 3.0;
|
||||
constexpr double PEAK_SIGMA = 4.0;
|
||||
// Closer than this to a model atom, the density is the model's own atom out of place.
|
||||
constexpr double MODEL_ERROR_DISTANCE = 1.6;
|
||||
// Waters and ions sit at least this far from the model's atoms.
|
||||
constexpr double CONTACT_MIN = 2.2;
|
||||
// An ion's peak, in sigma, where the anomalous map gives no help.
|
||||
constexpr double ION_PEAK_SIGMA = 8.0;
|
||||
constexpr double ION_ANOMALOUS_SIGMA = 4.0;
|
||||
// The smallest blob called ligand-like, A^3: at high resolution a strong single atom's blob already spans four of
|
||||
// the resolution's spheres.
|
||||
constexpr double LIGAND_MIN_VOLUME = 8.0;
|
||||
|
||||
bool Polar(const gemmi::Element &el) {
|
||||
return el == gemmi::El::N || el == gemmi::El::O;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *DensityBlobKindName(DensityBlob::Kind kind) {
|
||||
switch (kind) {
|
||||
case DensityBlob::Kind::Water: return "WATER";
|
||||
case DensityBlob::Kind::Ion: return "ION";
|
||||
case DensityBlob::Kind::Ligand: return "LIGAND";
|
||||
case DensityBlob::Kind::ModelError: return "MODEL_ERROR";
|
||||
case DensityBlob::Kind::Unassigned: break;
|
||||
}
|
||||
return "UNASSIGNED";
|
||||
}
|
||||
|
||||
std::vector<DensityBlob> FindDensityBlobs(const gemmi::Grid<float> &difference, double rms, const gemmi::Structure &model,
|
||||
double d_min, const gemmi::Grid<float> *anomalous, double anomalous_rms) {
|
||||
std::vector<DensityBlob> out;
|
||||
if (!(rms > 0) || model.models.empty())
|
||||
return out;
|
||||
gemmi::Grid<float> map = difference;
|
||||
for (float &v : map.data)
|
||||
v = static_cast<float>(v / rms);
|
||||
gemmi::BlobCriteria criteria;
|
||||
criteria.cutoff = CUTOFF_SIGMA;
|
||||
criteria.min_volume = 0.0;
|
||||
criteria.min_score = 0.0;
|
||||
criteria.min_peak = PEAK_SIGMA;
|
||||
const std::vector<gemmi::Blob> blobs = gemmi::find_blobs_by_flood_fill(map, criteria);
|
||||
|
||||
gemmi::Model copy = model.models[0];
|
||||
gemmi::UnitCell cell = map.unit_cell;
|
||||
cell.set_cell_images_from_spacegroup(map.spacegroup);
|
||||
gemmi::NeighborSearch ns(copy, cell, 5.0);
|
||||
ns.populate(false);
|
||||
// A single atom's blob at this resolution: a sphere of half the resolution's radius. Four of them is ligand size.
|
||||
const double atom_volume = 4.0 / 3.0 * 3.14159265358979 * std::pow(0.5 * d_min, 3);
|
||||
|
||||
for (const gemmi::Blob &b : blobs) {
|
||||
DensityBlob d;
|
||||
d.peak_sigma = b.peak_value;
|
||||
d.volume = b.volume;
|
||||
d.score = b.score;
|
||||
d.centroid = b.centroid;
|
||||
// the copy of the blob beside the atom nearest to it
|
||||
if (gemmi::NeighborSearch::Mark *m = ns.find_nearest_atom(b.centroid, 12.0)) {
|
||||
const gemmi::CRA cra = m->to_cra(copy);
|
||||
const gemmi::FTransform t = ns.get_image_transformation(m->image_idx);
|
||||
const gemmi::Fractional fc = cell.fractionalize(b.centroid);
|
||||
const gemmi::Fractional fi = t.apply(cell.fractionalize(cra.atom->pos));
|
||||
const gemmi::Fractional shifted(fc.x - std::round(fc.x - fi.x), fc.y - std::round(fc.y - fi.y),
|
||||
fc.z - std::round(fc.z - fi.z));
|
||||
d.centroid = cell.orthogonalize(gemmi::FTransform(t.inverse()).apply(shifted));
|
||||
d.nearest_atom_distance = d.centroid.dist(cra.atom->pos);
|
||||
d.nearest_atom = cra.atom->name + " " + cra.residue->name + " " + cra.chain->name +
|
||||
cra.residue->seqid.str();
|
||||
} else
|
||||
d.nearest_atom_distance = INFINITY;
|
||||
ns.for_each(d.centroid, '\0', 3.4, [&](gemmi::NeighborSearch::Mark &m, double dist_sq) {
|
||||
const double r = std::sqrt(dist_sq);
|
||||
if (Polar(m.element) && r >= 2.4)
|
||||
d.polar_contacts++;
|
||||
if ((Polar(m.element) || m.element == gemmi::El::S) && r >= 1.8 && r <= 2.6)
|
||||
d.metal_contacts++;
|
||||
});
|
||||
if (anomalous != nullptr && anomalous_rms > 0)
|
||||
d.anomalous_sigma = anomalous->interpolate_value(b.peak_pos) / anomalous_rms;
|
||||
|
||||
if (d.nearest_atom_distance < MODEL_ERROR_DISTANCE)
|
||||
d.kind = DensityBlob::Kind::ModelError;
|
||||
else if (d.nearest_atom_distance >= 1.8 && d.metal_contacts >= 2 &&
|
||||
(d.anomalous_sigma >= ION_ANOMALOUS_SIGMA || d.peak_sigma >= ION_PEAK_SIGMA))
|
||||
d.kind = DensityBlob::Kind::Ion;
|
||||
else if (d.volume > std::max(4 * atom_volume, LIGAND_MIN_VOLUME) && d.nearest_atom_distance >= CONTACT_MIN)
|
||||
d.kind = DensityBlob::Kind::Ligand;
|
||||
else if (d.nearest_atom_distance >= CONTACT_MIN && d.polar_contacts >= 1)
|
||||
d.kind = DensityBlob::Kind::Water;
|
||||
out.push_back(d);
|
||||
}
|
||||
std::sort(out.begin(), out.end(), [](const DensityBlob &a, const DensityBlob &b) { return a.score > b.score; });
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gemmi/grid.hpp"
|
||||
#include "gemmi/model.hpp"
|
||||
|
||||
// What a model leaves unexplained: the connected regions of positive mFo-DFc density above a cutoff ("blobs"),
|
||||
// each placed beside the model, measured and given a likely reading - a water, an ion, something ligand-sized,
|
||||
// or an error of the model itself (a misplaced or missing atom, an alternative conformation).
|
||||
//
|
||||
// The readings are rules on what is measured, meant to point a person at the density, not to identify a
|
||||
// compound: a blob called LIGAND is density of ligand size and shape where the model has nothing, which a
|
||||
// buffer molecule or an unmodelled side chain can be as well as the compound that was soaked.
|
||||
struct DensityBlob {
|
||||
enum class Kind { Water, Ion, Ligand, ModelError, Unassigned };
|
||||
Kind kind = Kind::Unassigned;
|
||||
gemmi::Position centroid; // the copy nearest the model
|
||||
double peak_sigma = 0; // the highest point, in the map's rms
|
||||
double volume = 0; // A^3 above the cutoff
|
||||
double score = 0; // the density above the cutoff integrated, sigma A^3
|
||||
double nearest_atom_distance = 0;
|
||||
std::string nearest_atom; // e.g. "OG1 THR A42"
|
||||
int polar_contacts = 0; // N or O of the model at 2.4-3.4 A
|
||||
int metal_contacts = 0; // N, O or S of the model at 1.8-2.6 A
|
||||
double anomalous_sigma = 0; // the anomalous difference map at the peak, 0 where there is none
|
||||
};
|
||||
|
||||
const char *DensityBlobKindName(DensityBlob::Kind kind);
|
||||
|
||||
// The blobs of `difference` (an mFo-DFc map of the whole cell, rms `rms`) above 3 sigma that peak above 4 sigma,
|
||||
// strongest (by score) first. `model`: what the map was made from, in the map's cell. `anomalous`: the anomalous
|
||||
// difference map on any grid of the same cell, or null. Following the flood fill of Coot's unmodelled blobs as
|
||||
// gemmi implements it (find_blobs_by_flood_fill).
|
||||
std::vector<DensityBlob> FindDensityBlobs(const gemmi::Grid<float> &difference, double rms, const gemmi::Structure &model,
|
||||
double d_min, const gemmi::Grid<float> *anomalous = nullptr,
|
||||
double anomalous_rms = 0);
|
||||
@@ -0,0 +1,781 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "Refinement.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <ceres/gradient_problem.h>
|
||||
#include <ceres/gradient_problem_solver.h>
|
||||
#include <ceres/jet.h>
|
||||
#include <ceres/rotation.h>
|
||||
|
||||
#include "gemmi/fourier.hpp" // get_size_for_hkl, get_f_phi_on_grid
|
||||
#include "gemmi/modify.hpp" // remove_hydrogens
|
||||
#include "gemmi/scaling.hpp"
|
||||
#include "gemmi/stats.hpp" // calculate_data_statistics
|
||||
|
||||
#include "../../common/JFJochException.h"
|
||||
#include "../../common/JFJochMath.h"
|
||||
#include "../../common/Logger.h"
|
||||
#include "ModelFFT.h"
|
||||
#include "ModelScaling.h"
|
||||
#include "RefinementXray.h"
|
||||
#include "SigmaA.h"
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
#include "../../common/CUDAWrapper.h"
|
||||
#include "RefinementGPU.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double B_FLOOR = 2.0; // A^2: no atom's B goes below it
|
||||
// nor above twice the model's largest B, within these bounds (the GPU's gather is reserved for it)
|
||||
constexpr double B_CEILING_MIN = 100.0, B_CEILING_MAX = 300.0;
|
||||
// How far below the smallest B of the model a macro-cycle's B refinement may take an atom; the blur of the
|
||||
// cycle is chosen for that.
|
||||
constexpr double B_STEP_DOWN = 10.0;
|
||||
constexpr double REPULSION_MARGIN = 0.5; // A: pairs this much beyond their contact distance are listed too
|
||||
// The geometry the weight is chosen within (Afonine et al. 2012 select among weights by R-free with the
|
||||
// geometry held to a target in the same way).
|
||||
constexpr double MAX_RMSD_BONDS = 0.02, MAX_RMSD_ANGLES = 2.0;
|
||||
// R-free differences smaller than this do not decide between two weights: the tighter geometry is taken.
|
||||
constexpr double R_FREE_TIE = 0.001;
|
||||
|
||||
std::string Joined(const std::vector<std::string> &names) {
|
||||
std::string out;
|
||||
for (const std::string &n : names)
|
||||
out += (out.empty() ? "" : " ") + n;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<gemmi::Atom *> AtomList(gemmi::Model &model) {
|
||||
std::vector<gemmi::Atom *> atoms;
|
||||
for (gemmi::Chain &ch : model.chains)
|
||||
for (gemmi::Residue &r : ch.residues)
|
||||
for (gemmi::Atom &a : r.atoms)
|
||||
atoms.push_back(&a);
|
||||
return atoms;
|
||||
}
|
||||
|
||||
// The model as refinement takes it: the first model only, without hydrogens and atoms of unknown element,
|
||||
// every B isotropic (an anisotropic one becomes its equivalent isotropic B) and inside the allowed range.
|
||||
gemmi::Structure PrepareModel(const gemmi::Structure &placed, const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg) {
|
||||
gemmi::Structure st = placed;
|
||||
st.models.resize(1);
|
||||
st.cell = cell;
|
||||
st.spacegroup_hm = sg.xhm();
|
||||
gemmi::remove_hydrogens(st.models[0]);
|
||||
for (gemmi::Chain &ch : st.models[0].chains)
|
||||
for (gemmi::Residue &r : ch.residues) {
|
||||
std::erase_if(r.atoms, [](const gemmi::Atom &a) { return a.element == gemmi::El::X; });
|
||||
for (gemmi::Atom &a : r.atoms) {
|
||||
if (a.aniso.nonzero()) {
|
||||
a.b_iso = static_cast<float>(gemmi::u_to_b() * a.aniso.trace() / 3.0);
|
||||
a.aniso = {0, 0, 0, 0, 0, 0};
|
||||
}
|
||||
a.b_iso = static_cast<float>(std::clamp<double>(a.b_iso, B_FLOOR, B_CEILING_MAX));
|
||||
}
|
||||
}
|
||||
gemmi::remove_empty_children(st);
|
||||
st.setup_cell_images();
|
||||
return st;
|
||||
}
|
||||
|
||||
// What every stage works on: the model, the reflections, the engine and the row model of the cycle.
|
||||
struct Context {
|
||||
gemmi::Structure st;
|
||||
std::vector<gemmi::Atom *> atoms;
|
||||
gemmi::UnitCell cell;
|
||||
const gemmi::SpaceGroup *sg = nullptr;
|
||||
double d_min = 0;
|
||||
RefinementReflections refl;
|
||||
std::unique_ptr<RefinementEngine> engine;
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
std::unique_ptr<ModelScaleGPUPool> scale_gpu;
|
||||
#endif
|
||||
size_t nthreads = 1;
|
||||
XrayRowModel rows;
|
||||
std::vector<std::complex<float>> fcalc, fmask;
|
||||
double k_sol = 0, b_sol = 0;
|
||||
double b_floor = B_FLOOR; // the cycle's lowest allowed B
|
||||
double b_ceiling = B_CEILING_MAX;
|
||||
// Riding hydrogens: per atom the parent it rides on (-1: none), and where it sits from the parent.
|
||||
std::vector<int> rider_parent;
|
||||
std::vector<gemmi::Position> rider_offset;
|
||||
int evaluations = 0;
|
||||
|
||||
gemmi::Model &Model() { return st.models[0]; }
|
||||
|
||||
// The hydrogens moved with their parents, at the offsets they were last placed at, and given their B.
|
||||
void MoveRiders() {
|
||||
for (size_t i = 0; i < rider_parent.size(); i++)
|
||||
if (rider_parent[i] >= 0) {
|
||||
atoms[i]->pos = atoms[rider_parent[i]]->pos + rider_offset[i];
|
||||
atoms[i]->b_iso = atoms[rider_parent[i]]->b_iso;
|
||||
}
|
||||
}
|
||||
// What the target says of a hydrogen is said of its parent.
|
||||
void FoldRiders(std::vector<std::array<double, 4>> &g) const {
|
||||
for (size_t i = 0; i < rider_parent.size(); i++)
|
||||
if (rider_parent[i] >= 0)
|
||||
for (int c = 0; c < 4; c++)
|
||||
g[rider_parent[i]][c] += g[i][c];
|
||||
}
|
||||
|
||||
std::vector<std::array<double, 4>> XyzB() const {
|
||||
std::vector<std::array<double, 4>> out(atoms.size());
|
||||
for (size_t i = 0; i < atoms.size(); i++)
|
||||
out[i] = {atoms[i]->pos.x, atoms[i]->pos.y, atoms[i]->pos.z, atoms[i]->b_iso};
|
||||
return out;
|
||||
}
|
||||
std::vector<gemmi::Position> Positions() const {
|
||||
std::vector<gemmi::Position> out;
|
||||
for (const gemmi::Atom *a : atoms)
|
||||
out.push_back(a->pos);
|
||||
return out;
|
||||
}
|
||||
void SetPositions(const std::vector<gemmi::Position> &p) {
|
||||
for (size_t i = 0; i < atoms.size(); i++)
|
||||
atoms[i]->pos = p[i];
|
||||
}
|
||||
std::vector<double> B() const {
|
||||
std::vector<double> out;
|
||||
for (const gemmi::Atom *a : atoms)
|
||||
out.push_back(a->b_iso);
|
||||
return out;
|
||||
}
|
||||
void SetB(const std::vector<double> &b) {
|
||||
for (size_t i = 0; i < atoms.size(); i++)
|
||||
atoms[i]->b_iso = static_cast<float>(b[i]);
|
||||
}
|
||||
|
||||
// A new macro-cycle: the blur for the B the cycle may reach, the bulk-solvent mask of the model as it is,
|
||||
// the scale and the solvent fitted to the working set, and the likelihood's parameters from the free set.
|
||||
// Returns the R-factors of the model as it stands.
|
||||
RFactors Refit() {
|
||||
double b_min = b_ceiling;
|
||||
for (const gemmi::Atom *a : atoms)
|
||||
b_min = std::min<double>(b_min, a->b_iso);
|
||||
b_floor = std::max(B_FLOOR, b_min - B_STEP_DOWN);
|
||||
engine->SetBlur(RefinementBlur(cell, *sg, d_min, b_floor));
|
||||
engine->Fcalc(Model(), fcalc);
|
||||
engine->Fmask(Model(), fmask);
|
||||
|
||||
gemmi::AsuData<std::complex<float>> fc, fm;
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fo;
|
||||
fc.unit_cell_ = fm.unit_cell_ = fo.unit_cell_ = cell;
|
||||
fc.spacegroup_ = fm.spacegroup_ = fo.spacegroup_ = sg;
|
||||
for (size_t m = 0; m < refl.Size(); m++) {
|
||||
fc.v.push_back({refl.hkl[m], fcalc[m]});
|
||||
fm.v.push_back({refl.hkl[m], fmask[m]});
|
||||
if (!refl.free[m])
|
||||
fo.v.push_back({refl.hkl[m], {refl.fo[m], 1.0f}});
|
||||
}
|
||||
gemmi::Scaling<float> scaling(cell, sg);
|
||||
scaling.use_solvent = true;
|
||||
scaling.prepare_points(fc, fo, &fm);
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
if (scale_gpu)
|
||||
scale_gpu->Fit(scaling, SolventBMax(Model()), nthreads);
|
||||
else
|
||||
#endif
|
||||
FitModelScale(scaling, {.b_hi = SolventBMax(Model())}, nthreads);
|
||||
k_sol = scaling.k_sol;
|
||||
b_sol = scaling.b_sol;
|
||||
rows.k_total.resize(refl.Size());
|
||||
rows.fmask.resize(refl.Size());
|
||||
std::vector<std::complex<float>> fmodel(refl.Size());
|
||||
for (size_t m = 0; m < refl.Size(); m++) {
|
||||
rows.k_total[m] = static_cast<float>(scaling.get_overall_scale_factor(refl.hkl[m]));
|
||||
rows.fmask[m] = static_cast<float>(scaling.get_solvent_scale(cell.calculate_stol_sq(refl.hkl[m]))) * fmask[m];
|
||||
fmodel[m] = rows.k_total[m] * (fcalc[m] + rows.fmask[m]);
|
||||
}
|
||||
EstimateLikelihoodParameters(refl, fmodel, rows);
|
||||
engine->SetRowModel(rows);
|
||||
return ComputeRFactors(refl, fmodel);
|
||||
}
|
||||
|
||||
// F_model with the cycle's scale and solvent, for the atoms as they are now.
|
||||
std::vector<std::complex<float>> FModel() {
|
||||
engine->Fcalc(Model(), fcalc);
|
||||
std::vector<std::complex<float>> fmodel(refl.Size());
|
||||
for (size_t m = 0; m < refl.Size(); m++)
|
||||
fmodel[m] = rows.k_total[m] * (fcalc[m] + rows.fmask[m]);
|
||||
return fmodel;
|
||||
}
|
||||
};
|
||||
|
||||
ceres::GradientProblemSolver::Options SolverOptions(int iterations) {
|
||||
ceres::GradientProblemSolver::Options options;
|
||||
options.line_search_direction_type = ceres::LBFGS;
|
||||
options.max_num_iterations = iterations;
|
||||
options.function_tolerance = 1e-7;
|
||||
options.gradient_tolerance = 1e-10;
|
||||
options.parameter_tolerance = 1e-9;
|
||||
options.logging_type = ceres::SILENT;
|
||||
return options;
|
||||
}
|
||||
|
||||
// The positions or the B factors of the refinable atoms, against w times the X-ray target plus a restraint set.
|
||||
// Following Liu & Nocedal (1989) Math. Program. 45, 503-528 for the minimiser (Ceres' L-BFGS)
|
||||
class AtomTarget : public ceres::FirstOrderFunction {
|
||||
public:
|
||||
enum class Mode { Xyz, B };
|
||||
AtomTarget(Context &ctx, Mode mode, const std::vector<int> ¶m_atoms, double weight, int restraint_set)
|
||||
: ctx_(ctx), mode_(mode), param_atoms_(param_atoms), weight_(weight), restraint_set_(restraint_set) {}
|
||||
|
||||
int NumParameters() const override {
|
||||
return static_cast<int>(param_atoms_.size()) * (mode_ == Mode::Xyz ? 3 : 1);
|
||||
}
|
||||
|
||||
bool Evaluate(const double *p, double *cost, double *gradient) const override {
|
||||
for (size_t k = 0; k < param_atoms_.size(); k++) {
|
||||
gemmi::Atom &a = *ctx_.atoms[param_atoms_[k]];
|
||||
if (mode_ == Mode::Xyz)
|
||||
a.pos = gemmi::Position(p[3 * k], p[3 * k + 1], p[3 * k + 2]);
|
||||
else
|
||||
a.b_iso = static_cast<float>(std::clamp(p[k], ctx_.b_floor, ctx_.b_ceiling));
|
||||
}
|
||||
ctx_.MoveRiders();
|
||||
ctx_.evaluations++;
|
||||
std::vector<std::array<double, 4>> gx, gr;
|
||||
const double xray = ctx_.engine->Evaluate(ctx_.Model(), gradient != nullptr ? &gx : nullptr);
|
||||
if (gradient != nullptr)
|
||||
ctx_.FoldRiders(gx);
|
||||
double restraints = 0;
|
||||
if (restraint_set_ >= 0)
|
||||
restraints = ctx_.engine->EvaluateRestraints(restraint_set_, ctx_.XyzB(), gradient != nullptr ? &gr : nullptr);
|
||||
*cost = weight_ * xray + restraints;
|
||||
if (gradient != nullptr)
|
||||
for (size_t k = 0; k < param_atoms_.size(); k++) {
|
||||
const int i = param_atoms_[k];
|
||||
if (mode_ == Mode::Xyz)
|
||||
for (int c = 0; c < 3; c++)
|
||||
gradient[3 * k + c] = weight_ * gx[i][c] + (restraint_set_ >= 0 ? gr[i][c] : 0.0);
|
||||
else {
|
||||
// held at a bound, B does not move further out
|
||||
const bool held = (p[k] <= ctx_.b_floor && gx[i][3] > 0) || (p[k] >= ctx_.b_ceiling && gx[i][3] < 0);
|
||||
gradient[k] = held ? 0.0 : weight_ * gx[i][3] + (restraint_set_ >= 0 ? gr[i][3] : 0.0);
|
||||
}
|
||||
}
|
||||
return std::isfinite(*cost);
|
||||
}
|
||||
|
||||
private:
|
||||
Context &ctx_;
|
||||
Mode mode_;
|
||||
const std::vector<int> ¶m_atoms_;
|
||||
double weight_;
|
||||
int restraint_set_;
|
||||
};
|
||||
|
||||
void Minimise(Context &ctx, AtomTarget::Mode mode, const std::vector<int> ¶m_atoms, double weight,
|
||||
int restraint_set, int iterations) {
|
||||
if (param_atoms.empty())
|
||||
return;
|
||||
std::vector<double> p;
|
||||
for (int i : param_atoms) {
|
||||
const gemmi::Atom &a = *ctx.atoms[i];
|
||||
if (mode == AtomTarget::Mode::Xyz) {
|
||||
p.push_back(a.pos.x);
|
||||
p.push_back(a.pos.y);
|
||||
p.push_back(a.pos.z);
|
||||
} else
|
||||
p.push_back(a.b_iso);
|
||||
}
|
||||
ceres::GradientProblem problem(std::make_unique<AtomTarget>(ctx, mode, param_atoms, weight, restraint_set));
|
||||
ceres::GradientProblemSolver::Summary summary;
|
||||
ceres::Solve(SolverOptions(iterations), problem, p.data(), &summary);
|
||||
// the atoms where the solver ended, not where its last line search probed
|
||||
for (size_t k = 0; k < param_atoms.size(); k++) {
|
||||
gemmi::Atom &a = *ctx.atoms[param_atoms[k]];
|
||||
if (mode == AtomTarget::Mode::Xyz)
|
||||
a.pos = gemmi::Position(p[3 * k], p[3 * k + 1], p[3 * k + 2]);
|
||||
else
|
||||
a.b_iso = static_cast<float>(std::clamp(p[k], ctx.b_floor, ctx.b_ceiling));
|
||||
}
|
||||
ctx.MoveRiders();
|
||||
}
|
||||
|
||||
// Groups of atoms that move as rigid bodies (one per chain) against the X-ray target: per body an angle-axis
|
||||
// rotation about its centroid, scaled by its rms radius so that a unit moves a typical atom by an angstrom as a
|
||||
// unit of translation does, then a translation.
|
||||
class RigidBodiesTarget : public ceres::FirstOrderFunction {
|
||||
public:
|
||||
RigidBodiesTarget(Context &ctx, const std::vector<std::vector<int>> &bodies)
|
||||
: ctx_(ctx), bodies_(bodies), base_(ctx.Positions()) {
|
||||
for (const std::vector<int> &b : bodies) {
|
||||
gemmi::Position c;
|
||||
for (int i : b)
|
||||
c += base_[i];
|
||||
c *= 1.0 / static_cast<double>(b.size());
|
||||
double r2 = 0;
|
||||
for (int i : b)
|
||||
r2 += c.dist_sq(base_[i]);
|
||||
centre_.push_back(c);
|
||||
radius_.push_back(std::max(1.0, std::sqrt(r2 / static_cast<double>(b.size()))));
|
||||
}
|
||||
}
|
||||
int NumParameters() const override { return static_cast<int>(6 * bodies_.size()); }
|
||||
|
||||
bool Evaluate(const double *q, double *cost, double *gradient) const override {
|
||||
Place(q);
|
||||
ctx_.evaluations++;
|
||||
std::vector<std::array<double, 4>> g;
|
||||
*cost = ctx_.engine->Evaluate(ctx_.Model(), gradient != nullptr ? &g : nullptr);
|
||||
if (gradient == nullptr)
|
||||
return std::isfinite(*cost);
|
||||
using Jet = ceres::Jet<double, 3>;
|
||||
for (size_t b = 0; b < bodies_.size(); b++) {
|
||||
const Jet aa[3] = {Jet(q[6 * b] / radius_[b], 0), Jet(q[6 * b + 1] / radius_[b], 1),
|
||||
Jet(q[6 * b + 2] / radius_[b], 2)};
|
||||
double dq[6] = {0, 0, 0, 0, 0, 0};
|
||||
for (int i : bodies_[b]) {
|
||||
const gemmi::Position d = base_[i] - centre_[b];
|
||||
const Jet p[3] = {Jet(d.x), Jet(d.y), Jet(d.z)};
|
||||
Jet rp[3];
|
||||
ceres::AngleAxisRotatePoint(aa, p, rp);
|
||||
for (int c = 0; c < 3; c++) {
|
||||
for (int k = 0; k < 3; k++)
|
||||
dq[k] += g[i][c] * rp[c].v[k] / radius_[b];
|
||||
dq[3 + c] += g[i][c];
|
||||
}
|
||||
}
|
||||
std::copy(dq, dq + 6, gradient + 6 * b);
|
||||
}
|
||||
return std::isfinite(*cost);
|
||||
}
|
||||
|
||||
void Place(const double *q) const {
|
||||
for (size_t b = 0; b < bodies_.size(); b++) {
|
||||
const double aa[3] = {q[6 * b] / radius_[b], q[6 * b + 1] / radius_[b], q[6 * b + 2] / radius_[b]};
|
||||
for (int i : bodies_[b]) {
|
||||
const gemmi::Position d = base_[i] - centre_[b];
|
||||
const double p[3] = {d.x, d.y, d.z};
|
||||
double rp[3];
|
||||
ceres::AngleAxisRotatePoint(aa, p, rp);
|
||||
ctx_.atoms[i]->pos = gemmi::Position(rp[0] + centre_[b].x + q[6 * b + 3], rp[1] + centre_[b].y + q[6 * b + 4],
|
||||
rp[2] + centre_[b].z + q[6 * b + 5]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Context &ctx_;
|
||||
const std::vector<std::vector<int>> &bodies_;
|
||||
std::vector<gemmi::Position> base_;
|
||||
std::vector<gemmi::Position> centre_;
|
||||
std::vector<double> radius_;
|
||||
};
|
||||
|
||||
// One B shift per group of atoms (a residue), against the X-ray target.
|
||||
class GroupBTarget : public ceres::FirstOrderFunction {
|
||||
public:
|
||||
GroupBTarget(Context &ctx, const std::vector<std::vector<int>> &groups)
|
||||
: ctx_(ctx), groups_(groups), base_(ctx.B()) {}
|
||||
int NumParameters() const override { return static_cast<int>(groups_.size()); }
|
||||
|
||||
bool Evaluate(const double *q, double *cost, double *gradient) const override {
|
||||
Place(q);
|
||||
ctx_.evaluations++;
|
||||
std::vector<std::array<double, 4>> g;
|
||||
*cost = ctx_.engine->Evaluate(ctx_.Model(), gradient != nullptr ? &g : nullptr);
|
||||
if (gradient != nullptr)
|
||||
for (size_t k = 0; k < groups_.size(); k++) {
|
||||
gradient[k] = 0;
|
||||
for (int i : groups_[k]) {
|
||||
const double b = base_[i] + q[k];
|
||||
if (b > ctx_.b_floor && b < ctx_.b_ceiling)
|
||||
gradient[k] += g[i][3];
|
||||
}
|
||||
}
|
||||
return std::isfinite(*cost);
|
||||
}
|
||||
|
||||
void Place(const double *q) const {
|
||||
for (size_t k = 0; k < groups_.size(); k++)
|
||||
for (int i : groups_[k])
|
||||
ctx_.atoms[i]->b_iso = static_cast<float>(std::clamp(base_[i] + q[k], ctx_.b_floor, ctx_.b_ceiling));
|
||||
}
|
||||
|
||||
private:
|
||||
Context &ctx_;
|
||||
const std::vector<std::vector<int>> &groups_;
|
||||
std::vector<double> base_;
|
||||
};
|
||||
|
||||
// The placement stage: rigid bodies, then group B.
|
||||
void RefinePlacement(Context &ctx, RefinementResult &result) {
|
||||
std::vector<std::vector<int>> bodies, groups;
|
||||
size_t i = 0;
|
||||
for (const gemmi::Chain &ch : ctx.Model().chains) {
|
||||
std::vector<int> body;
|
||||
for (const gemmi::Residue &r : ch.residues) {
|
||||
std::vector<int> group;
|
||||
for (size_t k = 0; k < r.atoms.size(); k++, i++) {
|
||||
group.push_back(static_cast<int>(i));
|
||||
if (!r.is_water())
|
||||
body.push_back(static_cast<int>(i));
|
||||
}
|
||||
groups.push_back(group);
|
||||
}
|
||||
// A chain of a few atoms (an ion, a ligand of its own chain) stays where it is.
|
||||
if (body.size() >= 50)
|
||||
bodies.push_back(body);
|
||||
}
|
||||
result.rigid_bodies = static_cast<int>(bodies.size());
|
||||
if (!bodies.empty()) {
|
||||
std::vector<double> q(6 * bodies.size(), 0.0);
|
||||
auto target = std::make_unique<RigidBodiesTarget>(ctx, bodies);
|
||||
RigidBodiesTarget *rb = target.get();
|
||||
ceres::GradientProblem problem(std::move(target));
|
||||
ceres::GradientProblemSolver::Summary summary;
|
||||
ceres::Solve(SolverOptions(30), problem, q.data(), &summary);
|
||||
rb->Place(q.data());
|
||||
}
|
||||
ctx.Refit();
|
||||
{
|
||||
std::vector<double> q(groups.size(), 0.0);
|
||||
auto target = std::make_unique<GroupBTarget>(ctx, groups);
|
||||
GroupBTarget *gb = target.get();
|
||||
ceres::GradientProblem problem(std::move(target));
|
||||
ceres::GradientProblemSolver::Summary summary;
|
||||
ceres::Solve(SolverOptions(30), problem, q.data(), &summary);
|
||||
gb->Place(q.data());
|
||||
}
|
||||
}
|
||||
|
||||
// The X-ray weight that balances the two gradients: the norm of the geometry's gradient (taken on the model
|
||||
// shaken by 0.1 A, where a refined model's geometry would have none) over the norm of the X-ray target's.
|
||||
// Following Brunger (1992)-style gradient balancing as phenix.refine starts its weight search (Afonine et al.
|
||||
// (2012) Acta Cryst. D68, 352-367)
|
||||
double BalancedWeight(Context &ctx, const std::vector<int> &refinable) {
|
||||
std::vector<std::array<double, 4>> gx, gr;
|
||||
ctx.engine->Evaluate(ctx.Model(), &gx);
|
||||
std::vector<std::array<double, 4>> xyzb = ctx.XyzB();
|
||||
std::mt19937 rng(1);
|
||||
std::normal_distribution<double> shake(0.0, 0.1);
|
||||
for (std::array<double, 4> &a : xyzb)
|
||||
for (int c = 0; c < 3; c++)
|
||||
a[c] += shake(rng);
|
||||
ctx.engine->EvaluateRestraints(0, xyzb, &gr);
|
||||
double nx = 0, ng = 0;
|
||||
for (int i : refinable)
|
||||
for (int c = 0; c < 3; c++) {
|
||||
nx += gx[i][c] * gx[i][c];
|
||||
ng += gr[i][c] * gr[i][c];
|
||||
}
|
||||
return nx > 0 ? std::sqrt(ng / nx) : 1.0;
|
||||
}
|
||||
|
||||
// The model's map coefficients and its difference map, as model validation makes them (sigma_A weighted).
|
||||
void MakeMaps(Context &ctx, const std::vector<MergedReflection> &merged, double wavelength_A, RefinementResult &result) {
|
||||
ctx.Refit();
|
||||
const std::vector<std::complex<float>> fmodel = ctx.FModel();
|
||||
const gemmi::GroupOps gops = ctx.sg->operations();
|
||||
std::vector<SigmaAReflection> sa;
|
||||
for (size_t m = 0; m < ctx.refl.Size(); m++)
|
||||
sa.push_back({ctx.refl.fo[m], std::abs(fmodel[m]), ctx.refl.inv_d2[m], static_cast<int>(ctx.refl.epsilon[m]),
|
||||
static_cast<bool>(ctx.refl.centric[m]), static_cast<bool>(ctx.refl.free[m])});
|
||||
const SigmaAResult sigma_a = EstimateSigmaA(sa, ctx.cell);
|
||||
result.mean_fom = sigma_a.mean_fom;
|
||||
|
||||
auto maps = std::make_shared<ModelMaps>();
|
||||
MtzTable &mtz = maps->coefficients;
|
||||
mtz.space_group_name = ctx.sg->xhm();
|
||||
mtz.unit_cell = UnitCell{static_cast<float>(ctx.cell.a), static_cast<float>(ctx.cell.b), static_cast<float>(ctx.cell.c),
|
||||
static_cast<float>(ctx.cell.alpha), static_cast<float>(ctx.cell.beta),
|
||||
static_cast<float>(ctx.cell.gamma)};
|
||||
mtz.datasets = {{"HKL_base", "HKL_base", "HKL_base", 0.0f},
|
||||
{"refinement", "refinement", "refinement", static_cast<float>(wavelength_A)}};
|
||||
const std::pair<const char *, char> columns[] = {{"H", 'H'}, {"K", 'H'}, {"L", 'H'}, {"FP", 'F'}, {"FC", 'F'},
|
||||
{"PHIC", 'P'}, {"FWT", 'F'}, {"PHWT", 'P'}, {"DELFWT", 'F'},
|
||||
{"PHDELWT", 'P'}, {"FOM", 'W'}, {"FREE", 'I'}};
|
||||
for (const auto &[label, type] : columns)
|
||||
mtz.columns.push_back({label, type, type == 'H' ? 0 : 1, {}});
|
||||
gemmi::AsuData<std::complex<float>> diff;
|
||||
diff.unit_cell_ = ctx.cell;
|
||||
diff.spacegroup_ = ctx.sg;
|
||||
for (size_t m = 0; m < ctx.refl.Size(); m++) {
|
||||
const double fo = ctx.refl.fo[m], fc = std::abs(fmodel[m]);
|
||||
const double mw = sigma_a.weight[m].m, d = sigma_a.weight[m].d;
|
||||
const double fwt = ctx.refl.centric[m] ? mw * fo : 2 * mw * fo - d * fc;
|
||||
const double delfwt = mw * fo - d * fc;
|
||||
const double phi = std::arg(fmodel[m]);
|
||||
double phi_deg = phi * 180.0 / PI;
|
||||
if (phi_deg < 0)
|
||||
phi_deg += 360.0;
|
||||
const float row[] = {static_cast<float>(ctx.refl.hkl[m][0]), static_cast<float>(ctx.refl.hkl[m][1]),
|
||||
static_cast<float>(ctx.refl.hkl[m][2]), static_cast<float>(fo), static_cast<float>(fc),
|
||||
static_cast<float>(phi_deg), static_cast<float>(fwt), static_cast<float>(phi_deg),
|
||||
static_cast<float>(delfwt), static_cast<float>(phi_deg), static_cast<float>(mw),
|
||||
ctx.refl.free[m] ? 0.0f : 1.0f};
|
||||
for (size_t c = 0; c < mtz.columns.size(); c++)
|
||||
mtz.columns[c].values.push_back(row[c]);
|
||||
diff.v.push_back({ctx.refl.hkl[m], std::polar(static_cast<float>(delfwt), static_cast<float>(phi))});
|
||||
}
|
||||
result.maps = maps;
|
||||
const std::array<int, 3> size = gemmi::get_size_for_hkl(diff, {{0, 0, 0}}, 3.0);
|
||||
result.difference_map = std::make_shared<gemmi::Grid<float>>(MapFromFPhi(gemmi::get_f_phi_on_grid<float>(diff, size, true)));
|
||||
result.difference_map_rms = gemmi::calculate_data_statistics(result.difference_map->data).rms;
|
||||
|
||||
// The anomalous difference map: F(+) - F(-) on the model's phase turned back by 90 degrees, read on the +
|
||||
// rows of an anomalous merge only (as model validation reads it).
|
||||
// Following ANODE, Thorn & Sheldrick (2011) J. Appl. Cryst. 44, 1285-1287
|
||||
const gemmi::ReciprocalAsu asu(ctx.sg);
|
||||
std::map<gemmi::Miller, float> danom;
|
||||
for (const MergedReflection &r : merged) {
|
||||
if (!std::isfinite(r.F_plus) || !std::isfinite(r.F_minus))
|
||||
continue;
|
||||
const gemmi::Miller h{{r.h, r.k, r.l}};
|
||||
if (gops.is_reflection_centric(h))
|
||||
continue;
|
||||
const auto [hasu, plus] = asu.to_asu_sign(h, gops);
|
||||
if (plus)
|
||||
danom[hasu] = r.F_plus - r.F_minus;
|
||||
}
|
||||
if (!danom.empty()) {
|
||||
gemmi::AsuData<std::complex<float>> anom;
|
||||
anom.unit_cell_ = ctx.cell;
|
||||
anom.spacegroup_ = ctx.sg;
|
||||
for (size_t m = 0; m < ctx.refl.Size(); m++) {
|
||||
const auto it = danom.find(ctx.refl.hkl[m]);
|
||||
if (it != danom.end())
|
||||
anom.v.push_back({ctx.refl.hkl[m], it->second * std::polar(1.0f, static_cast<float>(std::arg(fmodel[m]) - PI / 2))});
|
||||
}
|
||||
if (!anom.v.empty()) {
|
||||
result.anomalous_map = std::make_shared<gemmi::Grid<float>>(
|
||||
MapFromFPhi(gemmi::get_f_phi_on_grid<float>(anom, gemmi::get_size_for_hkl(anom, {{0, 0, 0}}, 3.0), true)));
|
||||
result.anomalous_map_rms = gemmi::calculate_data_statistics(result.anomalous_map->data).rms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The engine for the model as it is now: on the GPU where it is wanted and takes the case within its budget,
|
||||
// on the CPU otherwise.
|
||||
void MakeEngine(Context &ctx, bool gpu, const RefinementSettings &settings, Logger &logger) {
|
||||
ctx.engine.reset();
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
ctx.scale_gpu.reset();
|
||||
if (gpu && get_gpu_count() > 0) {
|
||||
try {
|
||||
const int device = get_gpu();
|
||||
const size_t card = get_gpu_total_memory(device);
|
||||
const size_t usable = card / 5 * 4;
|
||||
const size_t budget = usable > settings.gpu_resident_bytes ? usable - settings.gpu_resident_bytes : 0;
|
||||
size_t bytes = 0;
|
||||
ctx.engine = MakeRefinementEngineGPU(device, ctx.cell, *ctx.sg, ctx.d_min, ctx.refl, ctx.Model(),
|
||||
ctx.b_ceiling, budget, &bytes);
|
||||
if (ctx.engine)
|
||||
ctx.scale_gpu = std::make_unique<ModelScaleGPUPool>(device, ctx.refl.Size(), 1);
|
||||
else
|
||||
logger.Info("Refinement: on the CPU - the GPU cannot take this case or {:.1f} GB is over its budget",
|
||||
bytes / 1e9);
|
||||
} catch (const JFJochException &e) {
|
||||
logger.Warning("Refinement: the GPU failed ({}), on the CPU", e.what());
|
||||
ctx.engine.reset();
|
||||
ctx.scale_gpu.reset();
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void) gpu;
|
||||
(void) logger;
|
||||
#endif
|
||||
if (!ctx.engine)
|
||||
ctx.engine = MakeRefinementEngineCPU(ctx.cell, *ctx.sg, ctx.d_min, ctx.refl, settings.nthreads);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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) {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
RefinementResult result;
|
||||
Context ctx;
|
||||
ctx.st = PrepareModel(placed, cell, sg);
|
||||
ctx.cell = cell;
|
||||
ctx.sg = &sg;
|
||||
ctx.nthreads = settings.nthreads;
|
||||
ctx.atoms = AtomList(ctx.Model());
|
||||
result.atoms = static_cast<int>(ctx.atoms.size());
|
||||
double b_max = 0;
|
||||
for (const gemmi::Atom *a : ctx.atoms)
|
||||
b_max = std::max<double>(b_max, a->b_iso);
|
||||
ctx.b_ceiling = std::clamp(2 * b_max, B_CEILING_MIN, B_CEILING_MAX);
|
||||
if (ctx.atoms.empty()) {
|
||||
result.failure_reason = "the model has no atoms";
|
||||
return result;
|
||||
}
|
||||
double max_inv_d2 = 0;
|
||||
for (const MergedReflection &r : merged)
|
||||
if (std::isfinite(r.F) && r.d > 0)
|
||||
max_inv_d2 = std::max(max_inv_d2, 1.0 / (double(r.d) * r.d));
|
||||
if (max_inv_d2 <= 0) {
|
||||
result.failure_reason = "no amplitudes";
|
||||
return result;
|
||||
}
|
||||
// just past the finest reflection, so that prepare_asu_data()'s strict limit keeps it
|
||||
ctx.d_min = 0.999 / std::sqrt(max_inv_d2);
|
||||
result.d_min = 1.0 / std::sqrt(max_inv_d2);
|
||||
ctx.refl = PrepareRefinementReflections(merged, cell, sg, ctx.d_min);
|
||||
if (ctx.refl.Size() < 100) {
|
||||
result.failure_reason = "too few reflections";
|
||||
return result;
|
||||
}
|
||||
|
||||
MakeEngine(ctx, settings.gpu, settings, logger);
|
||||
result.on_gpu = ctx.engine->OnGPU();
|
||||
|
||||
RFactors r = ctx.Refit();
|
||||
result.n_work = r.n_work;
|
||||
result.n_free = r.n_free;
|
||||
result.r_work_start = r.r_work;
|
||||
result.r_free_start = r.r_free;
|
||||
logger.Info("Refinement: {} atoms, {} reflections to {:.2f} A ({} free), on the {}; as given R-work {:.4f}, R-free {:.4f}",
|
||||
ctx.atoms.size(), ctx.refl.Size(), result.d_min, r.n_free, result.on_gpu ? "GPU" : "CPU", r.r_work,
|
||||
r.r_free);
|
||||
|
||||
RefinePlacement(ctx, result);
|
||||
r = ctx.Refit();
|
||||
result.r_work_placement = r.r_work;
|
||||
result.r_free_placement = r.r_free;
|
||||
logger.Info("Refinement, placement ({} rigid bodies, B per residue): R-work {:.4f}, R-free {:.4f}",
|
||||
result.rigid_bodies, r.r_work, r.r_free);
|
||||
|
||||
if (settings.restrained) {
|
||||
RefinementTopology topo = BuildRefinementTopology(ctx.st, settings.riding_hydrogens);
|
||||
// the hydrogens it added: the atoms and the engine made again for them
|
||||
ctx.atoms = AtomList(ctx.Model());
|
||||
ctx.rider_parent = topo.rider_parent;
|
||||
auto place_riders = [&]() {
|
||||
PlaceRidingHydrogens(topo);
|
||||
ctx.rider_offset.assign(ctx.atoms.size(), gemmi::Position());
|
||||
for (size_t i = 0; i < ctx.atoms.size(); i++)
|
||||
if (ctx.rider_parent[i] >= 0) {
|
||||
ctx.rider_offset[i] = ctx.atoms[i]->pos - ctx.atoms[ctx.rider_parent[i]]->pos;
|
||||
ctx.atoms[i]->b_iso = ctx.atoms[ctx.rider_parent[i]]->b_iso;
|
||||
}
|
||||
};
|
||||
place_riders();
|
||||
if (static_cast<int>(ctx.atoms.size()) != result.atoms) {
|
||||
MakeEngine(ctx, result.on_gpu, settings, logger);
|
||||
result.on_gpu = ctx.engine->OnGPU();
|
||||
}
|
||||
ctx.Refit();
|
||||
result.restrained_residues = topo.restrained_residues;
|
||||
result.fixed_residues = topo.fixed_residues;
|
||||
result.fixed_names = topo.fixed_names;
|
||||
std::vector<int> refinable;
|
||||
for (size_t i = 0; i < ctx.atoms.size(); i++)
|
||||
if (topo.refinable[i])
|
||||
refinable.push_back(static_cast<int>(i));
|
||||
result.refined_atoms = static_cast<int>(refinable.size());
|
||||
logger.Info("Refinement: {} residues restrained, {} held fixed{}; {} bonds, {} angles, {} torsions, {} chiral "
|
||||
"volumes, {} planes",
|
||||
topo.restrained_residues, topo.fixed_residues,
|
||||
topo.fixed_names.empty() ? std::string() : " (" + Joined(topo.fixed_names) + ")",
|
||||
topo.bonds, topo.angles, topo.torsions, topo.chirals, topo.planes);
|
||||
|
||||
auto restraints_now = [&]() {
|
||||
RestraintSet set = topo.geometry;
|
||||
set.Append(BuildRepulsions(topo, ctx.Model(), cell, sg, REPULSION_MARGIN));
|
||||
set.Finalize(ctx.atoms.size());
|
||||
return set;
|
||||
};
|
||||
ctx.engine->SetRestraints(1, topo.adp);
|
||||
{
|
||||
const RestraintSet set = restraints_now();
|
||||
ctx.engine->SetRestraints(0, set);
|
||||
RestraintSet repulsions = BuildRepulsions(topo, ctx.Model(), cell, sg, 0.0);
|
||||
result.geometry_start = MeasureGeometry(topo.geometry, repulsions, ctx.XyzB());
|
||||
}
|
||||
const double w0 = BalancedWeight(ctx, refinable);
|
||||
logger.Info("Refinement: geometry as given rmsd bonds {:.4f} A, angles {:.2f} deg, {} clashes; balanced X-ray "
|
||||
"weight {:.3g}", result.geometry_start.rmsd_bonds, result.geometry_start.rmsd_angles,
|
||||
result.geometry_start.clashes, w0);
|
||||
|
||||
const std::vector<gemmi::Position> pos0 = ctx.Positions();
|
||||
const std::vector<double> b0 = ctx.B();
|
||||
int best = -1;
|
||||
std::vector<gemmi::Position> best_pos;
|
||||
std::vector<double> best_b;
|
||||
// One full refinement from the start: macro-cycles of the positions at X-ray weight w and, where adp_w is
|
||||
// not 0, the B factors at X-ray weight adp_w; kept where it beats the best so far. The rungs come in order
|
||||
// of trust - tighter geometry, then fewer parameters - and a later one displaces an earlier only by more
|
||||
// than the tie in R-free.
|
||||
auto trial = [&](double w, double adp_w) {
|
||||
ctx.SetPositions(pos0);
|
||||
ctx.SetB(b0);
|
||||
for (int cycle = 0; cycle < settings.macro_cycles; cycle++) {
|
||||
place_riders();
|
||||
ctx.Refit();
|
||||
ctx.engine->SetRestraints(0, restraints_now());
|
||||
Minimise(ctx, AtomTarget::Mode::Xyz, refinable, w, 0, settings.xyz_iterations);
|
||||
if (adp_w > 0)
|
||||
Minimise(ctx, AtomTarget::Mode::B, refinable, adp_w, 1, settings.b_iterations);
|
||||
}
|
||||
place_riders();
|
||||
RefinementWeightTrial t;
|
||||
t.weight = w;
|
||||
t.adp_weight = adp_w;
|
||||
const RFactors rf = ctx.Refit();
|
||||
t.r_work = rf.r_work;
|
||||
t.r_free = rf.r_free;
|
||||
t.geometry = MeasureGeometry(topo.geometry, BuildRepulsions(topo, ctx.Model(), cell, sg, 0.0), ctx.XyzB());
|
||||
t.within_bounds = t.geometry.rmsd_bonds <= MAX_RMSD_BONDS && t.geometry.rmsd_angles <= MAX_RMSD_ANGLES;
|
||||
logger.Info("Refinement, X-ray weight {:.3g}, B {}: R-work {:.4f}, R-free {:.4f}, rmsd bonds {:.4f} A, "
|
||||
"angles {:.2f} deg{}",
|
||||
w, adp_w > 0 ? fmt::format("refined at weight {:.3g}", adp_w) : std::string("held"), rf.r_work,
|
||||
rf.r_free, t.geometry.rmsd_bonds, t.geometry.rmsd_angles,
|
||||
t.within_bounds ? "" : " (geometry out of bounds)");
|
||||
result.trials.push_back(t);
|
||||
const int k = static_cast<int>(result.trials.size()) - 1;
|
||||
const bool better = best < 0 ? true
|
||||
: t.within_bounds && (!result.trials[best].within_bounds ||
|
||||
t.r_free < result.trials[best].r_free - R_FREE_TIE);
|
||||
if (better) {
|
||||
best = k;
|
||||
best_pos = ctx.Positions();
|
||||
best_b = ctx.B();
|
||||
}
|
||||
};
|
||||
// the positions' weight, with the B factors as they are
|
||||
for (double ladder : settings.weight_ladder)
|
||||
trial(w0 * ladder, 0.0);
|
||||
// then the B factors refined too, at that weight, against their own weight
|
||||
const double w = result.trials[best].weight;
|
||||
for (double adp : settings.adp_ladder)
|
||||
trial(w, w * adp);
|
||||
ctx.SetPositions(best_pos);
|
||||
ctx.SetB(best_b);
|
||||
place_riders();
|
||||
const RefinementWeightTrial &chosen = result.trials[best];
|
||||
result.restrained = true;
|
||||
result.weight = chosen.weight;
|
||||
result.adp_weight = chosen.adp_weight;
|
||||
result.r_work = chosen.r_work;
|
||||
result.r_free = chosen.r_free;
|
||||
result.geometry = chosen.geometry;
|
||||
}
|
||||
|
||||
MakeMaps(ctx, merged, wavelength_A, result);
|
||||
result.blobs = FindDensityBlobs(*result.difference_map, result.difference_map_rms, ctx.st, result.d_min,
|
||||
result.anomalous_map.get(), result.anomalous_map_rms);
|
||||
result.k_sol = ctx.k_sol;
|
||||
result.b_sol = ctx.b_sol;
|
||||
result.model = std::make_shared<gemmi::Structure>(ctx.st);
|
||||
result.ok = true;
|
||||
result.seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
|
||||
logger.Info("Refinement: done in {:.1f} s, {} target evaluations{}", result.seconds, ctx.evaluations,
|
||||
result.restrained ? fmt::format("; R-work {:.4f}, R-free {:.4f}, weight {:.3g}", result.r_work,
|
||||
result.r_free, result.weight)
|
||||
: std::string());
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,283 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "RefinementGPU.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "gemmi/dencalc.hpp"
|
||||
#include "gemmi/it92.hpp"
|
||||
#include "gemmi/solmask.hpp" // SolventMasker, refmac_radius_for_bulk_solvent
|
||||
|
||||
#include "RefinementGPUEngine.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using Table = gemmi::IT92<float>;
|
||||
|
||||
// One atom's density as the gather takes it, isotropic, with `blur`.
|
||||
ModelDensityAtom DensityAtom(const gemmi::Atom &atom, double b_iso, const gemmi::DensityCalculator<Table, float> &dc) {
|
||||
using CReal = Table::Coef::coef_type;
|
||||
const auto &coef = Table::get(atom.element, atom.charge, atom.serial);
|
||||
const CReal b = static_cast<CReal>(b_iso + dc.blur);
|
||||
const auto precal = coef.precalculate_density_iso(b, 0.0f);
|
||||
const double radius = dc.estimate_radius(precal, b);
|
||||
ModelDensityAtom a{};
|
||||
a.occ = atom.occ;
|
||||
a.aniso = 0;
|
||||
a.radius = static_cast<float>(radius);
|
||||
a.max_r2 = static_cast<float>(radius * radius);
|
||||
for (int k = 0; k < 5; k++) {
|
||||
a.a[k] = precal.a[k];
|
||||
a.b[k][0] = precal.b[k];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
std::vector<const gemmi::Atom *> Atoms(const gemmi::Model &model) {
|
||||
std::vector<const gemmi::Atom *> atoms;
|
||||
for (const gemmi::Chain &ch : model.chains)
|
||||
for (const gemmi::Residue &r : ch.residues)
|
||||
for (const gemmi::Atom &a : r.atoms)
|
||||
atoms.push_back(&a);
|
||||
return atoms;
|
||||
}
|
||||
|
||||
class RefinementEngineGPU : public RefinementEngine {
|
||||
public:
|
||||
RefinementEngineGPU(int device, const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min,
|
||||
const RefinementReflections &refl, const gemmi::Model &model, double b_max)
|
||||
: device_(device), cell_(cell), sg_(sg), d_min_(d_min) {
|
||||
const gemmi::Grid<float> grid = RigidBodyZoneGrid(cell, sg, d_min);
|
||||
setup_.grid.nu = grid.nu;
|
||||
setup_.grid.nv = grid.nv;
|
||||
setup_.grid.nw = grid.nw;
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++) {
|
||||
setup_.grid.orth[3 * i + j] = cell.orth.mat[i][j];
|
||||
setup_.grid.frac[3 * i + j] = cell.frac.mat[i][j];
|
||||
}
|
||||
setup_.volume = cell.volume;
|
||||
const gemmi::GroupOps gops = sg.operations();
|
||||
setup_.den = gemmi::Op::DEN;
|
||||
setup_.centring = static_cast<double>(gops.cen_ops.size());
|
||||
for (const gemmi::Op &op : gops.sym_ops) {
|
||||
std::array<int, 12> o{};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++)
|
||||
o[3 * i + j] = op.rot[i][j];
|
||||
o[9 + i] = op.tran[i];
|
||||
}
|
||||
setup_.sym_ops.push_back(o);
|
||||
}
|
||||
for (const gemmi::Op::Tran &cen : gops.cen_ops)
|
||||
for (const gemmi::Op &op : gops.sym_ops) {
|
||||
ModelMaskOp m{};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++)
|
||||
m.rot[3 * i + j] = static_cast<double>(op.rot[i][j]) / gemmi::Op::DEN;
|
||||
m.tran[i] = static_cast<double>(op.tran[i] + cen[i]) / gemmi::Op::DEN;
|
||||
}
|
||||
setup_.mask_ops.push_back(m);
|
||||
}
|
||||
for (size_t m = 0; m < refl.Size(); m++) {
|
||||
setup_.rows.push_back({refl.hkl[m][0], refl.hkl[m][1], refl.hkl[m][2]});
|
||||
setup_.inv_d2.push_back(refl.inv_d2[m]);
|
||||
setup_.fo.push_back(refl.fo[m]);
|
||||
setup_.work.push_back(!refl.free[m]);
|
||||
setup_.centric.push_back(refl.centric[m]);
|
||||
}
|
||||
|
||||
// The adjoint of the composition on cuFFT's half-u spectrum: the term of row m and operator o, at
|
||||
// k = hR, goes to k where k_u > 0, conjugated to -k where k_u < 0, and to both on the planes k_u = 0
|
||||
// and k_u = nu/2, which hold their own Friedel mates.
|
||||
const int nu = grid.nu, nv = grid.nv, nw = grid.nw;
|
||||
auto point = [&](int ku, int kv, int kw) {
|
||||
return ku + (nu / 2 + 1) * (gemmi::modulo(kv, nv) + nv * gemmi::modulo(kw, nw));
|
||||
};
|
||||
std::vector<std::pair<int, int>> entries;
|
||||
const int n_ops = static_cast<int>(gops.sym_ops.size());
|
||||
for (size_t m = 0; m < refl.Size(); m++)
|
||||
for (int o = 0; o < n_ops; o++) {
|
||||
const gemmi::Miller k = gops.sym_ops[o].apply_to_hkl(refl.hkl[m]);
|
||||
const int term = (static_cast<int>(m) * n_ops + o) * 2;
|
||||
if (k[0] > 0 && 2 * k[0] != nu)
|
||||
entries.emplace_back(point(k[0], k[1], k[2]), term);
|
||||
else if (k[0] < 0 && -2 * k[0] != nu)
|
||||
entries.emplace_back(point(-k[0], -k[1], -k[2]), term + 1);
|
||||
else {
|
||||
const int ku = std::abs(k[0]);
|
||||
entries.emplace_back(point(ku, k[1], k[2]), term);
|
||||
entries.emplace_back(point(ku, -k[1], -k[2]), term + 1);
|
||||
}
|
||||
}
|
||||
std::stable_sort(entries.begin(), entries.end(),
|
||||
[](const auto &a, const auto &b) { return a.first < b.first; });
|
||||
for (size_t e = 0; e < entries.size(); e++) {
|
||||
if (e == 0 || entries[e].first != entries[e - 1].first) {
|
||||
setup_.scatter_point.push_back(entries[e].first);
|
||||
setup_.scatter_start.push_back(static_cast<int>(e));
|
||||
}
|
||||
setup_.scatter_term.push_back(entries[e].second);
|
||||
}
|
||||
setup_.scatter_start.push_back(static_cast<int>(entries.size()));
|
||||
|
||||
// The gather's reserve for every atom at b_max with the largest blur a refinement sets (B down to 0).
|
||||
dc_ = Calculator(RefinementBlur(cell, sg, d_min, 0.0));
|
||||
std::vector<ModelDensityAtom> widest;
|
||||
for (const gemmi::Atom *a : Atoms(model))
|
||||
widest.push_back(DensityAtom(*a, b_max, dc_));
|
||||
setup_.max_atoms = widest.size();
|
||||
setup_.max_pairs = ModelDensityGPU::PairBound(setup_.grid, widest);
|
||||
supported_ = !setup_.rows.empty() && ModelDensityGPU::Supports(setup_.grid, widest) &&
|
||||
setup_.mask_ops.size() <= ModelMaskGPU::MAX_OPS;
|
||||
if (supported_) {
|
||||
fft_work_ = RefinementGPUEngine::FFTWorkBytes(device, setup_.grid);
|
||||
// the restraints: about thirty records and slots per atom
|
||||
bytes_ = RefinementGPUEngine::DeviceBytes(setup_, fft_work_, 30 * widest.size(), 60 * widest.size());
|
||||
}
|
||||
}
|
||||
|
||||
bool Supported() const { return supported_; }
|
||||
size_t Bytes() const { return bytes_; }
|
||||
void Reserve() { engine_ = std::make_unique<RefinementGPUEngine>(device_, setup_, fft_work_); }
|
||||
|
||||
void SetBlur(double blur) override {
|
||||
dc_ = Calculator(blur);
|
||||
last_b_.clear();
|
||||
engine_->SetBlur(blur);
|
||||
}
|
||||
|
||||
void SetRowModel(const XrayRowModel &rows) override {
|
||||
std::vector<std::array<float, 2>> fmask(rows.fmask.size());
|
||||
for (size_t m = 0; m < fmask.size(); m++)
|
||||
fmask[m] = {rows.fmask[m].real(), rows.fmask[m].imag()};
|
||||
engine_->SetRowModel(rows.k_total, fmask, rows.d, rows.var);
|
||||
}
|
||||
|
||||
bool OnGPU() const override { return true; }
|
||||
|
||||
void Fcalc(const gemmi::Model &model, std::vector<std::complex<float>> &f) override {
|
||||
SetAtoms(model);
|
||||
std::vector<std::array<float, 2>> out;
|
||||
engine_->Fcalc(out);
|
||||
f.resize(out.size());
|
||||
for (size_t m = 0; m < out.size(); m++)
|
||||
f[m] = {out[m][0], out[m][1]};
|
||||
}
|
||||
|
||||
void Fmask(const gemmi::Model &model, std::vector<std::complex<float>> &f) override {
|
||||
const gemmi::SolventMasker masker(gemmi::AtomicRadiiSet::Refmac);
|
||||
std::vector<ModelMaskAtom> mask_atoms;
|
||||
for (const gemmi::Atom *a : Atoms(model)) {
|
||||
if ((masker.ignore_hydrogen && a->is_hydrogen()) || (masker.ignore_zero_occupancy_atoms && a->occ <= 0))
|
||||
continue;
|
||||
const gemmi::Fractional f0 = cell_.fractionalize(a->pos);
|
||||
double frac[3] = {f0.x, f0.y, f0.z};
|
||||
for (double &x : frac) {
|
||||
x -= std::floor(x);
|
||||
if (x >= 1.0)
|
||||
x = 0.0;
|
||||
}
|
||||
mask_atoms.push_back({frac[0], frac[1], frac[2],
|
||||
masker.constant_r + masker.rprobe +
|
||||
gemmi::refmac_radius_for_bulk_solvent(a->element.elem)});
|
||||
}
|
||||
std::vector<std::array<float, 2>> out;
|
||||
engine_->Fmask(mask_atoms, out);
|
||||
f.resize(out.size());
|
||||
for (size_t m = 0; m < out.size(); m++)
|
||||
f[m] = {out[m][0], out[m][1]};
|
||||
}
|
||||
|
||||
double Evaluate(const gemmi::Model &model, std::vector<std::array<double, 4>> *grad) override {
|
||||
SetAtoms(model);
|
||||
std::vector<std::array<float, 4>> g;
|
||||
const double value = engine_->Evaluate(grad != nullptr ? &g : nullptr);
|
||||
if (grad != nullptr) {
|
||||
grad->resize(g.size());
|
||||
for (size_t i = 0; i < g.size(); i++)
|
||||
(*grad)[i] = {g[i][0], g[i][1], g[i][2], g[i][3]};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void SetRestraints(int which, const RestraintSet &set) override {
|
||||
engine_->SetRestraints(which, set.records, set.plane_atoms, set.slot_start, set.slot_index, set.slots);
|
||||
}
|
||||
|
||||
double EvaluateRestraints(int which, const std::vector<std::array<double, 4>> &xyzb,
|
||||
std::vector<std::array<double, 4>> *grad) override {
|
||||
std::vector<std::array<float, 4>> x(xyzb.size()), g;
|
||||
for (size_t i = 0; i < xyzb.size(); i++)
|
||||
x[i] = {static_cast<float>(xyzb[i][0]), static_cast<float>(xyzb[i][1]), static_cast<float>(xyzb[i][2]),
|
||||
static_cast<float>(xyzb[i][3])};
|
||||
const double value = engine_->EvaluateRestraints(which, x, grad != nullptr ? &g : nullptr);
|
||||
if (grad != nullptr) {
|
||||
grad->resize(g.size());
|
||||
for (size_t i = 0; i < g.size(); i++)
|
||||
(*grad)[i] = {g[i][0], g[i][1], g[i][2], g[i][3]};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
gemmi::DensityCalculator<Table, float> Calculator(double blur) const {
|
||||
gemmi::DensityCalculator<Table, float> dc;
|
||||
dc.d_min = d_min_;
|
||||
dc.rate = 1.5;
|
||||
dc.grid.unit_cell = cell_;
|
||||
dc.grid.spacegroup = &sg_;
|
||||
dc.blur = blur;
|
||||
return dc;
|
||||
}
|
||||
|
||||
// The atoms' densities (made again only for the atoms whose B moved) and grid positions, to the device.
|
||||
void SetAtoms(const gemmi::Model &model) {
|
||||
const std::vector<const gemmi::Atom *> atoms = Atoms(model);
|
||||
if (last_b_.size() != atoms.size()) {
|
||||
last_b_.assign(atoms.size(), NAN);
|
||||
atoms_.resize(atoms.size());
|
||||
}
|
||||
std::vector<std::array<float, 4>> pos(atoms.size());
|
||||
const int n3[3] = {setup_.grid.nu, setup_.grid.nv, setup_.grid.nw};
|
||||
for (size_t i = 0; i < atoms.size(); i++) {
|
||||
if (!(atoms[i]->b_iso == last_b_[i])) {
|
||||
atoms_[i] = DensityAtom(*atoms[i], atoms[i]->b_iso, dc_);
|
||||
last_b_[i] = atoms[i]->b_iso;
|
||||
}
|
||||
const gemmi::Fractional f = cell_.fractionalize(atoms[i]->pos);
|
||||
pos[i] = {static_cast<float>(f.x * n3[0]), static_cast<float>(f.y * n3[1]), static_cast<float>(f.z * n3[2]), 0};
|
||||
}
|
||||
engine_->SetAtoms(atoms_, pos);
|
||||
}
|
||||
|
||||
int device_;
|
||||
gemmi::UnitCell cell_;
|
||||
const gemmi::SpaceGroup &sg_;
|
||||
double d_min_;
|
||||
RefinementGPUSetup setup_;
|
||||
gemmi::DensityCalculator<Table, float> dc_;
|
||||
bool supported_ = false;
|
||||
size_t fft_work_ = 0, bytes_ = 0;
|
||||
std::unique_ptr<RefinementGPUEngine> engine_;
|
||||
std::vector<float> last_b_;
|
||||
std::vector<ModelDensityAtom> atoms_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<RefinementEngine> MakeRefinementEngineGPU(int device, const gemmi::UnitCell &cell,
|
||||
const gemmi::SpaceGroup &sg, double d_min,
|
||||
const RefinementReflections &refl,
|
||||
const gemmi::Model &model, double b_max, size_t budget,
|
||||
size_t *device_bytes) {
|
||||
auto engine = std::make_unique<RefinementEngineGPU>(device, cell, sg, d_min, refl, model, b_max);
|
||||
if (device_bytes != nullptr)
|
||||
*device_bytes = engine->Bytes();
|
||||
if (!engine->Supported() || engine->Bytes() > budget)
|
||||
return nullptr;
|
||||
engine->Reserve();
|
||||
return engine;
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "RefinementGPUEngine.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include <cub/cub.cuh>
|
||||
#include <cufft.h>
|
||||
|
||||
#include "../../common/JFJochException.h"
|
||||
#include "../indexing/CUDAMemHelpers.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void cuda_err(cudaError_t val) {
|
||||
if (val != cudaSuccess)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, cudaGetErrorString(val));
|
||||
}
|
||||
|
||||
void cufft_err(cufftResult val) {
|
||||
if (val != CUFFT_SUCCESS)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "cuFFT error " + std::to_string(static_cast<int>(val)));
|
||||
}
|
||||
|
||||
constexpr int THREADS = 256;
|
||||
constexpr int WARP = 32;
|
||||
constexpr double FOUR_PI2 = 4 * refine_math::PI * refine_math::PI;
|
||||
|
||||
int blocks(size_t n) {
|
||||
return static_cast<int>((n + THREADS - 1) / THREADS);
|
||||
}
|
||||
|
||||
__device__ __forceinline__ int imod(int a, int n) {
|
||||
const int r = a % n;
|
||||
return r < 0 ? r + n : r;
|
||||
}
|
||||
|
||||
// gemmi's unsafe_expapprox(), as ModelDensityGPU.cu has it.
|
||||
__device__ __forceinline__ float expapprox(float x) {
|
||||
const float val = 12102203.1615614f * x + 1065353216.f;
|
||||
const int vali = static_cast<int>(val);
|
||||
const float a = __int_as_float(vali & 0x7F800000);
|
||||
const float b = __int_as_float((vali & 0x7FFFFF) | 0x3F800000);
|
||||
return a * (0.509871020f + b * (0.312146713f + b * (0.166617139f + b * (-2.190619930e-3f + b * 1.3555747234e-2f))));
|
||||
}
|
||||
|
||||
// exp(+2 pi i h.t) of row h and operator o, exact for the fractions a translation is made of.
|
||||
__device__ __forceinline__ void op_phase(const int *h, const int *tran, int den, double &c, double &s) {
|
||||
sincospi(2.0 * (h[0] * tran[0] + h[1] * tran[1] + h[2] * tran[2]) / den, &s, &c);
|
||||
}
|
||||
|
||||
// F of the atoms at each row, composed from the half-u transform of one copy (ModelStructureFactorsGPU.cu's
|
||||
// compose_kernel), and with `evaluate` the target's term of a working row and the coefficient
|
||||
// conj(dT/dF) n_cen unblur the gradient's map is made of.
|
||||
__global__ void compose_target_kernel(const float2 *f1, float norm, int nu, int nv, int nw, const int *ops, int n_ops,
|
||||
int den, const int *row_hkl, const double *row_scale, int rows, bool evaluate,
|
||||
const float *k_total, const float2 *fmask, const float *ml_d, const float *ml_var,
|
||||
const float *fo, const char *work, const char *centric, float2 *f, double *nll,
|
||||
float2 *coef) {
|
||||
const int m = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (m >= rows)
|
||||
return;
|
||||
const int h[3] = {row_hkl[3 * m], row_hkl[3 * m + 1], row_hkl[3 * m + 2]};
|
||||
double sr = 0, si = 0;
|
||||
for (int o = 0; o < n_ops; o++) {
|
||||
const int *rot = ops + 12 * o, *tran = rot + 9;
|
||||
int k[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
k[i] = (rot[i] * h[0] + rot[3 + i] * h[1] + rot[6 + i] * h[2]) / den;
|
||||
const bool conj = k[0] < 0;
|
||||
if (conj)
|
||||
for (int i = 0; i < 3; i++)
|
||||
k[i] = -k[i];
|
||||
const float2 out = f1[k[0] + static_cast<size_t>(nu / 2 + 1) * (imod(k[1], nv) + static_cast<size_t>(nv) * imod(k[2], nw))];
|
||||
const double vr = out.x * norm, vi = (conj ? out.y : -out.y) * norm;
|
||||
double s, c;
|
||||
op_phase(h, tran, den, c, s);
|
||||
sr += c * vr - s * vi;
|
||||
si += c * vi + s * vr;
|
||||
}
|
||||
const double scale = row_scale[m];
|
||||
sr *= scale;
|
||||
si *= scale;
|
||||
f[m] = make_float2(static_cast<float>(sr), static_cast<float>(si));
|
||||
if (!evaluate)
|
||||
return;
|
||||
double term = 0, cr = 0, ci = 0;
|
||||
if (work[m]) {
|
||||
const double k = k_total[m];
|
||||
const double mr = k * (sr + fmask[m].x), mi = k * (si + fmask[m].y);
|
||||
const double fc = sqrt(mr * mr + mi * mi);
|
||||
double dfc = 0;
|
||||
term = refine_math::AmplitudeNegLogLikelihood<double>(fo[m], fc, ml_d[m], ml_var[m], centric[m], dfc);
|
||||
if (fc > 0) {
|
||||
// dT/dF = k dT/d|F_model| F_model / |F_model|; the coefficient is its conjugate times the scale
|
||||
const double g = k * dfc / fc * scale;
|
||||
cr = g * mr;
|
||||
ci = -g * mi;
|
||||
}
|
||||
}
|
||||
nll[m] = term;
|
||||
coef[m] = make_float2(static_cast<float>(cr), static_cast<float>(ci));
|
||||
}
|
||||
|
||||
// The gradient's coefficients on the half-u spectrum: each point the sum, in order, of the terms
|
||||
// (coef[row] exp(2 pi i h.t_op)) / 2 that land on it, conjugated where they land as a Friedel mate.
|
||||
__global__ void scatter_kernel(const float2 *coef, const int *row_hkl, const int *ops, int n_ops, int den,
|
||||
const int *point, const int *start, const int *term, int points, float2 *spectrum) {
|
||||
const int s = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (s >= points)
|
||||
return;
|
||||
double sr = 0, si = 0;
|
||||
for (int e = start[s]; e < start[s + 1]; e++) {
|
||||
const int t = term[e];
|
||||
const bool conj = t & 1;
|
||||
const int m = (t >> 1) / n_ops, o = (t >> 1) % n_ops;
|
||||
double c, sn;
|
||||
op_phase(row_hkl + 3 * m, ops + 12 * o + 9, den, c, sn);
|
||||
const double ar = 0.5 * (coef[m].x * c - coef[m].y * sn), ai = 0.5 * (coef[m].x * sn + coef[m].y * c);
|
||||
sr += ar;
|
||||
si += conj ? -ai : ai;
|
||||
}
|
||||
spectrum[point[s]] = make_float2(static_cast<float>(sr), static_cast<float>(si));
|
||||
}
|
||||
|
||||
// F_mask at each row, read at h directly (ModelStructureFactorsGPU.cu's fmask_kernel).
|
||||
__global__ void fmask_kernel(const float2 *fm, float norm, const int *row_hkl, int rows, int nu, int nv, int nw,
|
||||
float2 *fmask) {
|
||||
const int m = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (m >= rows)
|
||||
return;
|
||||
int h = row_hkl[3 * m], k = row_hkl[3 * m + 1], l = row_hkl[3 * m + 2];
|
||||
const bool conj = h < 0;
|
||||
if (conj) {
|
||||
h = -h;
|
||||
k = -k;
|
||||
l = -l;
|
||||
}
|
||||
const float2 out = fm[h + static_cast<size_t>(nu / 2 + 1) * (imod(k, nv) + static_cast<size_t>(nv) * imod(l, nw))];
|
||||
const float re = out.x * norm, im = -out.y * norm;
|
||||
fmask[m] = make_float2(re, conj ? -im : im);
|
||||
}
|
||||
|
||||
// dT/d(x, y, z, B) of every atom: the derivative map summed against the derivatives of the atom's density over
|
||||
// its box, one warp per atom, the lanes' sums added in a fixed tree. The box, the points within the radius and
|
||||
// their offsets from the centre are the density gather's (ModelDensityGPU.cu).
|
||||
__global__ void atom_gradient_kernel(const ModelDensityAtom *atoms, const float4 *pos, const int *box, int n,
|
||||
ModelDensityGPU::Geometry g, const float *map, float map_scale, float4 *grad) {
|
||||
const int a = (blockIdx.x * blockDim.x + threadIdx.x) / WARP;
|
||||
const int lane = threadIdx.x % WARP;
|
||||
if (a >= n)
|
||||
return;
|
||||
const ModelDensityAtom at = atoms[a];
|
||||
const float4 p = pos[a];
|
||||
const float nctr[3] = {p.x, p.y, p.z};
|
||||
int lo[3], span[3];
|
||||
for (int k = 0; k < 3; k++) {
|
||||
lo[k] = __float2int_rn(nctr[k]) - box[3 * a + k];
|
||||
span[k] = 2 * box[3 * a + k] + 1;
|
||||
}
|
||||
const int total = span[0] * span[1] * span[2];
|
||||
float gx = 0, gy = 0, gz = 0, gb = 0;
|
||||
for (int t = lane; t < total; t += WARP) {
|
||||
const int u = lo[0] + t % span[0], v = lo[1] + t / span[0] % span[1], w = lo[2] + t / (span[0] * span[1]);
|
||||
const float fx = nctr[0] - static_cast<float>(u);
|
||||
const float fy = nctr[1] - static_cast<float>(v);
|
||||
const float fz = nctr[2] - static_cast<float>(w);
|
||||
const float z = g.m22 * fz;
|
||||
const float y = g.m11 * fy + g.m12 * fz;
|
||||
const float x = g.m00 * fx + (g.m01 * fy + g.m02 * fz);
|
||||
const float r2 = (y * y + z * z) + x * x;
|
||||
if (r2 > at.max_r2)
|
||||
continue;
|
||||
const float value = map[imod(u, g.nu) + static_cast<size_t>(g.nu) * (imod(v, g.nv) + static_cast<size_t>(g.nv) * imod(w, g.nw))];
|
||||
float dr2 = 0, db = 0;
|
||||
for (int q = 0; q < 5; q++) {
|
||||
const float b = at.b[q][0];
|
||||
const float e = at.a[q] * expapprox(fmaxf(b * r2, -88.f));
|
||||
dr2 += e * b;
|
||||
db += e * b * (1.5f + b * r2);
|
||||
}
|
||||
gx += value * dr2 * x;
|
||||
gy += value * dr2 * y;
|
||||
gz += value * dr2 * z;
|
||||
gb += value * db;
|
||||
}
|
||||
for (int off = WARP / 2; off > 0; off /= 2) {
|
||||
gx += __shfl_down_sync(0xffffffff, gx, off);
|
||||
gy += __shfl_down_sync(0xffffffff, gy, off);
|
||||
gz += __shfl_down_sync(0xffffffff, gz, off);
|
||||
gb += __shfl_down_sync(0xffffffff, gb, off);
|
||||
}
|
||||
if (lane == 0) {
|
||||
const float s = 2.0f * at.occ * map_scale;
|
||||
grad[a] = make_float4(s * gx, s * gy, s * gz, at.occ * map_scale * gb / static_cast<float>(FOUR_PI2));
|
||||
}
|
||||
}
|
||||
|
||||
using V3f = refine_math::V3<float>;
|
||||
|
||||
__device__ V3f xyz(const float4 *p, int i) {
|
||||
const float4 q = p[i];
|
||||
return {q.x, q.y, q.z};
|
||||
}
|
||||
|
||||
// Every restraint's E, and its gradient into its slots, one thread per restraint.
|
||||
__global__ void restraint_kernel(const RestraintRecord *records, int n, const int *plane_atoms, const float4 *xyzb,
|
||||
float4 *contrib, double *energy) {
|
||||
const int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n)
|
||||
return;
|
||||
const RestraintRecord &r = records[i];
|
||||
V3f g[4] = {};
|
||||
int na = 0;
|
||||
float e = 0;
|
||||
switch (static_cast<RestraintKind>(r.kind)) {
|
||||
case RestraintKind::Bond:
|
||||
e = refine_math::BondTerm(xyz(xyzb, r.atom[0]), xyz(xyzb, r.atom[1]), r.ideal, r.sigma, g);
|
||||
na = 2;
|
||||
break;
|
||||
case RestraintKind::Angle:
|
||||
e = refine_math::AngleTerm(xyz(xyzb, r.atom[0]), xyz(xyzb, r.atom[1]), xyz(xyzb, r.atom[2]), r.ideal,
|
||||
r.sigma, g);
|
||||
na = 3;
|
||||
break;
|
||||
case RestraintKind::Torsion:
|
||||
e = refine_math::TorsionTerm(xyz(xyzb, r.atom[0]), xyz(xyzb, r.atom[1]), xyz(xyzb, r.atom[2]),
|
||||
xyz(xyzb, r.atom[3]), r.ideal, r.sigma, r.period, g);
|
||||
na = 4;
|
||||
break;
|
||||
case RestraintKind::Chiral:
|
||||
e = refine_math::ChiralTerm(xyz(xyzb, r.atom[0]), xyz(xyzb, r.atom[1]), xyz(xyzb, r.atom[2]),
|
||||
xyz(xyzb, r.atom[3]), r.ideal, r.sigma, g);
|
||||
na = 4;
|
||||
break;
|
||||
case RestraintKind::Repulsion: {
|
||||
const V3f q = xyz(xyzb, r.atom[1]);
|
||||
const V3f image{r.rot[0] * q.x + r.rot[1] * q.y + r.rot[2] * q.z + r.tran[0],
|
||||
r.rot[3] * q.x + r.rot[4] * q.y + r.rot[5] * q.z + r.tran[1],
|
||||
r.rot[6] * q.x + r.rot[7] * q.y + r.rot[8] * q.z + r.tran[2]};
|
||||
e = refine_math::RepulsionTerm(xyz(xyzb, r.atom[0]), image, r.ideal, r.sigma, g);
|
||||
const V3f gi = g[1];
|
||||
g[1] = {r.rot[0] * gi.x + r.rot[3] * gi.y + r.rot[6] * gi.z,
|
||||
r.rot[1] * gi.x + r.rot[4] * gi.y + r.rot[7] * gi.z,
|
||||
r.rot[2] * gi.x + r.rot[5] * gi.y + r.rot[8] * gi.z};
|
||||
na = 2;
|
||||
break;
|
||||
}
|
||||
case RestraintKind::Plane: {
|
||||
V3f px[RefinementGPUEngine::MAX_PLANE_ATOMS], pg[RefinementGPUEngine::MAX_PLANE_ATOMS];
|
||||
const int m = r.atom[1];
|
||||
for (int k = 0; k < m; k++)
|
||||
px[k] = xyz(xyzb, plane_atoms[r.atom[0] + k]);
|
||||
e = refine_math::PlaneTerm(px, m, r.sigma, pg);
|
||||
for (int k = 0; k < m; k++)
|
||||
contrib[r.slot + k] = make_float4(pg[k].x, pg[k].y, pg[k].z, 0.0f);
|
||||
break;
|
||||
}
|
||||
case RestraintKind::BSimilarity: {
|
||||
const float rr = (xyzb[r.atom[0]].w - xyzb[r.atom[1]].w) / r.sigma;
|
||||
e = rr * rr;
|
||||
contrib[r.slot] = make_float4(0, 0, 0, 2 * rr / r.sigma);
|
||||
contrib[r.slot + 1] = make_float4(0, 0, 0, -2 * rr / r.sigma);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int k = 0; k < na; k++)
|
||||
contrib[r.slot + k] = make_float4(g[k].x, g[k].y, g[k].z, 0.0f);
|
||||
energy[i] = e;
|
||||
}
|
||||
|
||||
// Each atom's restraint gradient: its slots summed in order.
|
||||
__global__ void slot_gather_kernel(const float4 *contrib, const int *start, const int *index, int n, float4 *grad) {
|
||||
const int a = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (a >= n)
|
||||
return;
|
||||
float4 s = make_float4(0, 0, 0, 0);
|
||||
for (int k = start[a]; k < start[a + 1]; k++) {
|
||||
const float4 c = contrib[index[k]];
|
||||
s.x += c.x;
|
||||
s.y += c.y;
|
||||
s.z += c.z;
|
||||
s.w += c.w;
|
||||
}
|
||||
grad[a] = s;
|
||||
}
|
||||
|
||||
CudaFFTShape ForwardShape(const ModelDensityGrid &g) {
|
||||
return {.n = {g.nw, g.nv, g.nu}, .idist = g.nu * g.nv * g.nw, .odist = (g.nu / 2 + 1) * g.nv * g.nw,
|
||||
.type = CUFFT_R2C};
|
||||
}
|
||||
|
||||
CudaFFTShape InverseShape(const ModelDensityGrid &g) {
|
||||
return {.n = {g.nw, g.nv, g.nu}, .idist = (g.nu / 2 + 1) * g.nv * g.nw, .odist = g.nu * g.nv * g.nw,
|
||||
.type = CUFFT_C2R};
|
||||
}
|
||||
|
||||
class DeviceGuard {
|
||||
public:
|
||||
explicit DeviceGuard(int device) {
|
||||
cuda_err(cudaGetDevice(&previous_));
|
||||
cuda_err(cudaSetDevice(device));
|
||||
}
|
||||
~DeviceGuard() { cudaSetDevice(previous_); }
|
||||
DeviceGuard(const DeviceGuard &) = delete;
|
||||
DeviceGuard &operator=(const DeviceGuard &) = delete;
|
||||
|
||||
private:
|
||||
int previous_ = 0;
|
||||
};
|
||||
|
||||
size_t GridPoints(const ModelDensityGrid &g) {
|
||||
return static_cast<size_t>(g.nu) * g.nv * g.nw;
|
||||
}
|
||||
|
||||
size_t ComplexPoints(const ModelDensityGrid &g) {
|
||||
return static_cast<size_t>(g.nu / 2 + 1) * g.nv * g.nw;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void Upload(CudaDevicePtr<T> &dst, const std::vector<T> &src, cudaStream_t stream) {
|
||||
if (!src.empty())
|
||||
cuda_err(cudaMemcpyAsync(dst, src.data(), src.size() * sizeof(T), cudaMemcpyHostToDevice, stream));
|
||||
}
|
||||
|
||||
// A device buffer of at least n elements, kept between calls and grown when it is too small.
|
||||
template <typename T>
|
||||
void Ensure(CudaDevicePtr<T> &p, size_t &capacity, size_t n) {
|
||||
if (n <= capacity && p)
|
||||
return;
|
||||
capacity = std::max<size_t>(n, 1);
|
||||
p = CudaDevicePtr<T>(capacity, CudaAlloc::Synchronous);
|
||||
}
|
||||
|
||||
// cub's temporary storage to reduce n doubles.
|
||||
size_t ReduceBytes(size_t n) {
|
||||
size_t bytes = 0;
|
||||
cuda_err(cub::DeviceReduce::Sum(nullptr, bytes, static_cast<double *>(nullptr), static_cast<double *>(nullptr),
|
||||
static_cast<int>(std::max<size_t>(n, 1))));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct RefinementGPUEngineImpl {
|
||||
struct Restraints {
|
||||
int records = 0, atoms = 0;
|
||||
size_t cap_records = 0, cap_planes = 0, cap_start = 0, cap_index = 0, cap_slots = 0, cap_reduce = 0,
|
||||
cap_atoms = 0, cap_xyzb = 0, reduce_bytes = 0;
|
||||
CudaDevicePtr<RestraintRecord> rec;
|
||||
CudaDevicePtr<int> plane_atoms, slot_start, slot_index;
|
||||
CudaDevicePtr<float4> contrib, xyzb, grad;
|
||||
CudaDevicePtr<double> energy, sum;
|
||||
CudaDevicePtr<char> reduce_temp;
|
||||
};
|
||||
|
||||
RefinementGPUSetup setup;
|
||||
size_t work_bytes;
|
||||
int device = 0;
|
||||
CudaStream stream;
|
||||
int n_atoms = 0;
|
||||
|
||||
CudaDevicePtr<float4> pos;
|
||||
CudaDevicePtr<int> box;
|
||||
CudaDevicePtr<ModelMaskAtom> mask_atoms;
|
||||
CudaDevicePtr<float> grid;
|
||||
CudaDevicePtr<float2> spectrum;
|
||||
CudaDevicePtr<char> fft_work;
|
||||
CudaDevicePtr<int> ops, row_hkl;
|
||||
CudaDevicePtr<double> row_scale;
|
||||
CudaDevicePtr<float> k_total, ml_d, ml_var, fo;
|
||||
CudaDevicePtr<float2> fmask_scaled, f, fm, coef;
|
||||
CudaDevicePtr<char> work, centric;
|
||||
CudaDevicePtr<double> nll, nll_sum;
|
||||
CudaDevicePtr<char> reduce_temp;
|
||||
size_t reduce_bytes = 0;
|
||||
CudaDevicePtr<int> scatter_point, scatter_start, scatter_term;
|
||||
CudaDevicePtr<float4> grad;
|
||||
CudaDevicePtr<ModelDensityAtom> atoms_copy; // the density gather keeps its own; this one is the gradient's
|
||||
std::unique_ptr<ModelDensityGPU> density;
|
||||
std::unique_ptr<ModelMaskGPU> mask;
|
||||
CudaFFTPlan forward, inverse;
|
||||
ModelDensityGPU::Geometry geom{};
|
||||
Restraints restraints[2];
|
||||
|
||||
RefinementGPUEngineImpl(int dev, const RefinementGPUSetup &s, size_t fft_work_bytes) : setup(s), work_bytes(fft_work_bytes), device(dev) {
|
||||
const auto sync = CudaAlloc::Synchronous;
|
||||
const size_t na = std::max<size_t>(setup.max_atoms, 1), nr = std::max<size_t>(setup.rows.size(), 1);
|
||||
pos = CudaDevicePtr<float4>(na, sync);
|
||||
box = CudaDevicePtr<int>(3 * na, sync);
|
||||
mask_atoms = CudaDevicePtr<ModelMaskAtom>(na, sync);
|
||||
atoms_copy = CudaDevicePtr<ModelDensityAtom>(na, sync);
|
||||
grad = CudaDevicePtr<float4>(na, sync);
|
||||
grid = CudaDevicePtr<float>(GridPoints(setup.grid), sync);
|
||||
spectrum = CudaDevicePtr<float2>(ComplexPoints(setup.grid), sync);
|
||||
fft_work = CudaDevicePtr<char>(std::max<size_t>(work_bytes, 1), sync);
|
||||
ops = CudaDevicePtr<int>(std::max<size_t>(12 * setup.sym_ops.size(), 1), sync);
|
||||
row_hkl = CudaDevicePtr<int>(3 * nr, sync);
|
||||
row_scale = CudaDevicePtr<double>(nr, sync);
|
||||
k_total = CudaDevicePtr<float>(nr, sync);
|
||||
ml_d = CudaDevicePtr<float>(nr, sync);
|
||||
ml_var = CudaDevicePtr<float>(nr, sync);
|
||||
fo = CudaDevicePtr<float>(nr, sync);
|
||||
fmask_scaled = CudaDevicePtr<float2>(nr, sync);
|
||||
f = CudaDevicePtr<float2>(nr, sync);
|
||||
fm = CudaDevicePtr<float2>(nr, sync);
|
||||
coef = CudaDevicePtr<float2>(nr, sync);
|
||||
work = CudaDevicePtr<char>(nr, sync);
|
||||
centric = CudaDevicePtr<char>(nr, sync);
|
||||
nll = CudaDevicePtr<double>(nr, sync);
|
||||
nll_sum = CudaDevicePtr<double>(1, sync);
|
||||
reduce_bytes = ReduceBytes(nr);
|
||||
reduce_temp = CudaDevicePtr<char>(std::max<size_t>(reduce_bytes, 1), sync);
|
||||
scatter_point = CudaDevicePtr<int>(std::max<size_t>(setup.scatter_point.size(), 1), sync);
|
||||
scatter_start = CudaDevicePtr<int>(std::max<size_t>(setup.scatter_start.size(), 1), sync);
|
||||
scatter_term = CudaDevicePtr<int>(std::max<size_t>(setup.scatter_term.size(), 1), sync);
|
||||
density = std::make_unique<ModelDensityGPU>(stream, setup.max_atoms, setup.max_pairs,
|
||||
ModelDensityGPU::Bricks(setup.grid.nu, setup.grid.nv, setup.grid.nw));
|
||||
mask = std::make_unique<ModelMaskGPU>(stream, GridPoints(setup.grid));
|
||||
ModelMaskGrid mask_grid;
|
||||
mask_grid.nu = setup.grid.nu;
|
||||
mask_grid.nv = setup.grid.nv;
|
||||
mask_grid.nw = setup.grid.nw;
|
||||
std::copy(setup.grid.orth, setup.grid.orth + 9, mask_grid.orth);
|
||||
mask_grid.volume = setup.volume;
|
||||
mask->SetGrid(mask_grid, setup.mask_ops);
|
||||
|
||||
std::vector<int> flat_ops;
|
||||
for (const auto &o : setup.sym_ops)
|
||||
flat_ops.insert(flat_ops.end(), o.begin(), o.end());
|
||||
std::vector<int> hkl;
|
||||
for (const auto &h : setup.rows)
|
||||
hkl.insert(hkl.end(), h.begin(), h.end());
|
||||
Upload(ops, flat_ops, stream);
|
||||
Upload(row_hkl, hkl, stream);
|
||||
Upload(fo, setup.fo, stream);
|
||||
Upload(work, setup.work, stream);
|
||||
Upload(centric, setup.centric, stream);
|
||||
Upload(scatter_point, setup.scatter_point, stream);
|
||||
Upload(scatter_start, setup.scatter_start, stream);
|
||||
Upload(scatter_term, setup.scatter_term, stream);
|
||||
|
||||
const double inv_n[3] = {1.0 / setup.grid.nu, 1.0 / setup.grid.nv, 1.0 / setup.grid.nw};
|
||||
geom.nu = setup.grid.nu;
|
||||
geom.nv = setup.grid.nv;
|
||||
geom.nw = setup.grid.nw;
|
||||
geom.m00 = static_cast<float>(setup.grid.orth[0] * inv_n[0]);
|
||||
geom.m01 = static_cast<float>(setup.grid.orth[1] * inv_n[1]);
|
||||
geom.m02 = static_cast<float>(setup.grid.orth[2] * inv_n[2]);
|
||||
geom.m11 = static_cast<float>(setup.grid.orth[4] * inv_n[1]);
|
||||
geom.m12 = static_cast<float>(setup.grid.orth[5] * inv_n[2]);
|
||||
geom.m22 = static_cast<float>(setup.grid.orth[8] * inv_n[2]);
|
||||
|
||||
forward = CudaFFTPlan(ForwardShape(setup.grid), stream, fft_work);
|
||||
inverse = CudaFFTPlan(InverseShape(setup.grid), stream, fft_work);
|
||||
if (forward.WorkBytes() > work_bytes || inverse.WorkBytes() > work_bytes)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "refinement: cuFFT work area over the reserve");
|
||||
cuda_err(cudaStreamSynchronize(stream));
|
||||
}
|
||||
|
||||
~RefinementGPUEngineImpl() { cudaStreamSynchronize(stream); }
|
||||
|
||||
float Norm() const { return static_cast<float>(setup.volume / static_cast<double>(GridPoints(setup.grid))); }
|
||||
|
||||
// The atoms' density on the grid and its transform into the spectrum.
|
||||
void Transform() {
|
||||
density->Compute(pos, grid);
|
||||
cufft_err(forward.R2C(grid, reinterpret_cast<cufftComplex *>(spectrum.get())));
|
||||
}
|
||||
|
||||
void Compose(bool evaluate) {
|
||||
const int rows = static_cast<int>(setup.rows.size());
|
||||
if (rows == 0)
|
||||
return;
|
||||
compose_target_kernel<<<blocks(rows), THREADS, 0, stream>>>(
|
||||
spectrum, Norm(), setup.grid.nu, setup.grid.nv, setup.grid.nw, ops, static_cast<int>(setup.sym_ops.size()),
|
||||
setup.den, row_hkl, row_scale, rows, evaluate, k_total, fmask_scaled, ml_d, ml_var, fo, work, centric, f,
|
||||
nll, coef);
|
||||
cuda_err(cudaGetLastError());
|
||||
}
|
||||
};
|
||||
|
||||
size_t RefinementGPUEngine::FFTWorkBytes(int device, const ModelDensityGrid &grid) {
|
||||
DeviceGuard guard(device);
|
||||
return std::max(CudaFFTPlan::WorkBytes(ForwardShape(grid)), CudaFFTPlan::WorkBytes(InverseShape(grid)));
|
||||
}
|
||||
|
||||
size_t RefinementGPUEngine::DeviceBytes(const RefinementGPUSetup &s, size_t fft_work_bytes, size_t max_records,
|
||||
size_t max_slots) {
|
||||
const size_t na = s.max_atoms + 1, nr = s.rows.size() + 1;
|
||||
size_t bytes = na * (2 * sizeof(float4) + 3 * sizeof(int) + sizeof(ModelMaskAtom) + sizeof(ModelDensityAtom));
|
||||
bytes += GridPoints(s.grid) * sizeof(float) + ComplexPoints(s.grid) * sizeof(float2) + fft_work_bytes;
|
||||
bytes += nr * (3 * sizeof(int) + 2 * sizeof(double) + 4 * sizeof(float) + 4 * sizeof(float2) + 2);
|
||||
bytes += (s.scatter_point.size() + s.scatter_start.size() + s.scatter_term.size()) * sizeof(int);
|
||||
bytes += ModelDensityGPU::DeviceBytes(s.max_atoms, s.max_pairs, ModelDensityGPU::Bricks(s.grid.nu, s.grid.nv, s.grid.nw));
|
||||
bytes += ModelMaskGPU::DeviceBytes(GridPoints(s.grid));
|
||||
bytes += max_records * (sizeof(RestraintRecord) + sizeof(double)) + max_slots * (sizeof(float4) + sizeof(int)) +
|
||||
na * (2 * sizeof(float4) + sizeof(int));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
RefinementGPUEngine::RefinementGPUEngine(int device, const RefinementGPUSetup &setup, size_t fft_work_bytes) {
|
||||
DeviceGuard guard(device);
|
||||
impl_ = std::make_unique<RefinementGPUEngineImpl>(device, setup, fft_work_bytes);
|
||||
}
|
||||
|
||||
RefinementGPUEngine::~RefinementGPUEngine() {
|
||||
if (impl_) {
|
||||
DeviceGuard guard(impl_->device);
|
||||
impl_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void RefinementGPUEngine::SetBlur(double blur) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
std::vector<double> scale(e.setup.rows.size());
|
||||
for (size_t m = 0; m < scale.size(); m++)
|
||||
scale[m] = e.setup.centring * std::exp(blur * 0.25 * e.setup.inv_d2[m]);
|
||||
Upload(e.row_scale, scale, e.stream);
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
}
|
||||
|
||||
void RefinementGPUEngine::SetRowModel(const std::vector<float> &k_total, const std::vector<std::array<float, 2>> &fmask,
|
||||
const std::vector<float> &d, const std::vector<float> &var) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
Upload(e.k_total, k_total, e.stream);
|
||||
if (!fmask.empty())
|
||||
cuda_err(cudaMemcpyAsync(e.fmask_scaled, fmask.data(), fmask.size() * sizeof(float2), cudaMemcpyHostToDevice,
|
||||
e.stream));
|
||||
Upload(e.ml_d, d, e.stream);
|
||||
Upload(e.ml_var, var, e.stream);
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
}
|
||||
|
||||
void RefinementGPUEngine::SetAtoms(const std::vector<ModelDensityAtom> &atoms, const std::vector<std::array<float, 4>> &pos) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
if (atoms.size() > e.setup.max_atoms || pos.size() != atoms.size())
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "refinement: atoms over the reserve");
|
||||
e.density->SetAtoms(e.setup.grid, atoms);
|
||||
e.n_atoms = static_cast<int>(atoms.size());
|
||||
const std::vector<int> b = ModelDensityGPU::Boxes(e.setup.grid, atoms);
|
||||
if (!atoms.empty()) {
|
||||
cuda_err(cudaMemcpyAsync(e.pos, pos.data(), pos.size() * sizeof(float4), cudaMemcpyHostToDevice, e.stream));
|
||||
cuda_err(cudaMemcpyAsync(e.atoms_copy, atoms.data(), atoms.size() * sizeof(ModelDensityAtom),
|
||||
cudaMemcpyHostToDevice, e.stream));
|
||||
Upload(e.box, b, e.stream);
|
||||
}
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
}
|
||||
|
||||
void RefinementGPUEngine::Fcalc(std::vector<std::array<float, 2>> &f) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
e.Transform();
|
||||
e.Compose(false);
|
||||
f.resize(e.setup.rows.size());
|
||||
if (!f.empty())
|
||||
cuda_err(cudaMemcpyAsync(f.data(), e.f, f.size() * sizeof(float2), cudaMemcpyDeviceToHost, e.stream));
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
}
|
||||
|
||||
void RefinementGPUEngine::Fmask(const std::vector<ModelMaskAtom> &mask_atoms, std::vector<std::array<float, 2>> &f) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
if (mask_atoms.size() > e.setup.max_atoms)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "refinement: mask atoms over the reserve");
|
||||
if (!mask_atoms.empty())
|
||||
cuda_err(cudaMemcpyAsync(e.mask_atoms, mask_atoms.data(), mask_atoms.size() * sizeof(ModelMaskAtom),
|
||||
cudaMemcpyHostToDevice, e.stream));
|
||||
e.mask->Compute(e.mask_atoms, static_cast<int>(mask_atoms.size()), e.grid);
|
||||
cufft_err(e.forward.R2C(e.grid, reinterpret_cast<cufftComplex *>(e.spectrum.get())));
|
||||
const int rows = static_cast<int>(e.setup.rows.size());
|
||||
f.resize(rows);
|
||||
if (rows > 0) {
|
||||
fmask_kernel<<<blocks(rows), THREADS, 0, e.stream>>>(e.spectrum, e.Norm(), e.row_hkl, rows, e.setup.grid.nu,
|
||||
e.setup.grid.nv, e.setup.grid.nw, e.fm);
|
||||
cuda_err(cudaGetLastError());
|
||||
cuda_err(cudaMemcpyAsync(f.data(), e.fm, rows * sizeof(float2), cudaMemcpyDeviceToHost, e.stream));
|
||||
}
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
}
|
||||
|
||||
double RefinementGPUEngine::Evaluate(std::vector<std::array<float, 4>> *grad) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
const int rows = static_cast<int>(e.setup.rows.size());
|
||||
e.Transform();
|
||||
e.Compose(true);
|
||||
double sum = 0;
|
||||
if (rows > 0) {
|
||||
size_t temp = e.reduce_bytes;
|
||||
cuda_err(cub::DeviceReduce::Sum(e.reduce_temp.get(), temp, e.nll.get(), e.nll_sum.get(), rows, e.stream));
|
||||
cuda_err(cudaMemcpyAsync(&sum, e.nll_sum, sizeof(double), cudaMemcpyDeviceToHost, e.stream));
|
||||
}
|
||||
if (grad != nullptr) {
|
||||
const ModelDensityGrid &g = e.setup.grid;
|
||||
cuda_err(cudaMemsetAsync(e.spectrum, 0, ComplexPoints(g) * sizeof(float2), e.stream));
|
||||
const int points = static_cast<int>(e.setup.scatter_point.size());
|
||||
if (points > 0) {
|
||||
scatter_kernel<<<blocks(points), THREADS, 0, e.stream>>>(e.coef, e.row_hkl, e.ops,
|
||||
static_cast<int>(e.setup.sym_ops.size()), e.setup.den,
|
||||
e.scatter_point, e.scatter_start, e.scatter_term,
|
||||
points, e.spectrum);
|
||||
cuda_err(cudaGetLastError());
|
||||
}
|
||||
cufft_err(e.inverse.C2R(reinterpret_cast<cufftComplex *>(e.spectrum.get()), e.grid));
|
||||
if (e.n_atoms > 0) {
|
||||
const size_t threads = static_cast<size_t>(e.n_atoms) * WARP;
|
||||
atom_gradient_kernel<<<blocks(threads), THREADS, 0, e.stream>>>(e.atoms_copy, e.pos, e.box, e.n_atoms, e.geom,
|
||||
e.grid, e.Norm(), e.grad);
|
||||
cuda_err(cudaGetLastError());
|
||||
}
|
||||
grad->resize(e.n_atoms);
|
||||
if (e.n_atoms > 0)
|
||||
cuda_err(cudaMemcpyAsync(grad->data(), e.grad, e.n_atoms * sizeof(float4), cudaMemcpyDeviceToHost, e.stream));
|
||||
}
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
return sum;
|
||||
}
|
||||
|
||||
void RefinementGPUEngine::SetRestraints(int which, const std::vector<RestraintRecord> &records,
|
||||
const std::vector<int> &plane_atoms, const std::vector<int> &slot_start,
|
||||
const std::vector<int> &slot_index, int slots) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
RefinementGPUEngineImpl::Restraints &r = e.restraints[which];
|
||||
for (const RestraintRecord &rec : records)
|
||||
if (rec.kind == static_cast<int>(RestraintKind::Plane) && rec.atom[1] > MAX_PLANE_ATOMS)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "refinement: a plane of too many atoms");
|
||||
r.records = static_cast<int>(records.size());
|
||||
r.atoms = static_cast<int>(slot_start.empty() ? 0 : slot_start.size() - 1);
|
||||
Ensure(r.rec, r.cap_records, records.size());
|
||||
Ensure(r.energy, r.cap_reduce, records.size());
|
||||
Ensure(r.plane_atoms, r.cap_planes, plane_atoms.size());
|
||||
Ensure(r.slot_start, r.cap_start, slot_start.size());
|
||||
Ensure(r.slot_index, r.cap_index, slot_index.size());
|
||||
Ensure(r.contrib, r.cap_slots, static_cast<size_t>(slots));
|
||||
Ensure(r.xyzb, r.cap_xyzb, r.atoms);
|
||||
Ensure(r.grad, r.cap_atoms, r.atoms);
|
||||
if (!r.sum)
|
||||
r.sum = CudaDevicePtr<double>(1, CudaAlloc::Synchronous);
|
||||
const size_t bytes = ReduceBytes(r.cap_reduce);
|
||||
if (!r.reduce_temp || bytes > r.reduce_bytes) {
|
||||
r.reduce_bytes = bytes;
|
||||
r.reduce_temp = CudaDevicePtr<char>(std::max<size_t>(bytes, 1), CudaAlloc::Synchronous);
|
||||
}
|
||||
Upload(r.rec, records, e.stream);
|
||||
Upload(r.plane_atoms, plane_atoms, e.stream);
|
||||
Upload(r.slot_start, slot_start, e.stream);
|
||||
Upload(r.slot_index, slot_index, e.stream);
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
}
|
||||
|
||||
double RefinementGPUEngine::EvaluateRestraints(int which, const std::vector<std::array<float, 4>> &xyzb,
|
||||
std::vector<std::array<float, 4>> *grad) {
|
||||
RefinementGPUEngineImpl &e = *impl_;
|
||||
DeviceGuard guard(e.device);
|
||||
RefinementGPUEngineImpl::Restraints &r = e.restraints[which];
|
||||
if (static_cast<int>(xyzb.size()) != r.atoms)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "refinement: restraint atoms do not match");
|
||||
if (r.atoms > 0)
|
||||
cuda_err(cudaMemcpyAsync(r.xyzb, xyzb.data(), xyzb.size() * sizeof(float4), cudaMemcpyHostToDevice, e.stream));
|
||||
double sum = 0;
|
||||
if (r.records > 0) {
|
||||
restraint_kernel<<<blocks(r.records), THREADS, 0, e.stream>>>(r.rec, r.records, r.plane_atoms, r.xyzb, r.contrib,
|
||||
r.energy);
|
||||
cuda_err(cudaGetLastError());
|
||||
size_t temp = r.reduce_bytes;
|
||||
cuda_err(cub::DeviceReduce::Sum(r.reduce_temp.get(), temp, r.energy.get(), r.sum.get(), r.records, e.stream));
|
||||
cuda_err(cudaMemcpyAsync(&sum, r.sum, sizeof(double), cudaMemcpyDeviceToHost, e.stream));
|
||||
}
|
||||
if (grad != nullptr) {
|
||||
grad->resize(r.atoms);
|
||||
if (r.atoms > 0) {
|
||||
if (r.records > 0) {
|
||||
slot_gather_kernel<<<blocks(r.atoms), THREADS, 0, e.stream>>>(r.contrib, r.slot_start, r.slot_index,
|
||||
r.atoms, r.grad);
|
||||
cuda_err(cudaGetLastError());
|
||||
} else
|
||||
cuda_err(cudaMemsetAsync(r.grad, 0, r.atoms * sizeof(float4), e.stream));
|
||||
cuda_err(cudaMemcpyAsync(grad->data(), r.grad, r.atoms * sizeof(float4), cudaMemcpyDeviceToHost, e.stream));
|
||||
}
|
||||
}
|
||||
cuda_err(cudaStreamSynchronize(e.stream));
|
||||
return sum;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
// Restrained refinement's targets on a GPU (CUDA builds only): the RefinementEngine of RefinementXray.h with
|
||||
// everything per evaluation on the device - the atoms' density (ModelDensityGPU's gather), its transform, the
|
||||
// symmetry composed in reciprocal space, the likelihood target, the derivative map by the inverse transform
|
||||
// of the target's derivatives and every atom's gradient read off it, and the geometry restraints. The host
|
||||
// passes positions in and gets one value and one gradient per atom back.
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "RefinementXray.h"
|
||||
|
||||
// The engine for refining `model` against `refl` on `device`, or null where the GPU cannot take this case (an
|
||||
// atom's box wider than the cell, too many symmetry operators) or its memory is over `budget` bytes.
|
||||
// `b_max`: the largest B an atom may reach, which the gather's reserve is sized for. Throws JFJochException on a
|
||||
// CUDA failure.
|
||||
std::unique_ptr<RefinementEngine> MakeRefinementEngineGPU(int device, const gemmi::UnitCell &cell,
|
||||
const gemmi::SpaceGroup &sg, double d_min,
|
||||
const RefinementReflections &refl,
|
||||
const gemmi::Model &model, double b_max, size_t budget,
|
||||
size_t *device_bytes = nullptr);
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
// The device half of restrained refinement on a GPU (RefinementGPU.h, CUDA builds only). The host half works
|
||||
// out with gemmi everything that depends on the cell, the group and the reflections; this half grids the atoms,
|
||||
// transforms, composes the symmetry, evaluates the likelihood target, takes its derivative back to every atom
|
||||
// by the FFT method, and evaluates the geometry restraints. Every kernel is deterministic - gathers and fixed
|
||||
// reduction trees, no floating-point atomics - so the same input gives the same bits on every run.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ModelDensityGPU.h"
|
||||
#include "ModelMaskGPU.h"
|
||||
#include "RefinementMath.h"
|
||||
|
||||
// Everything an engine needs that does not depend on the atoms.
|
||||
struct RefinementGPUSetup {
|
||||
ModelDensityGrid grid; // the structure-factor grid, u fastest
|
||||
double volume = 0;
|
||||
std::vector<std::array<int, 12>> sym_ops; // gemmi's Op: rot row-major and tran, in units of 1 / den
|
||||
int den = 24;
|
||||
double centring = 1; // the number of centring vectors
|
||||
std::vector<ModelMaskOp> mask_ops; // every operator with every centring vector
|
||||
std::vector<std::array<int, 3>> rows; // the reflections, in the order their numbers come back
|
||||
std::vector<double> inv_d2;
|
||||
std::vector<float> fo;
|
||||
std::vector<char> work, centric;
|
||||
// The adjoint of the composition, as a gather: the half-u spectrum points the gradient's coefficients
|
||||
// land on (ascending), and for each the terms (row * ops + op) * 2 + conj adding to it, in order.
|
||||
std::vector<int> scatter_point, scatter_start, scatter_term;
|
||||
size_t max_atoms = 0, max_pairs = 0;
|
||||
};
|
||||
|
||||
struct RefinementGPUEngineImpl;
|
||||
|
||||
class RefinementGPUEngine {
|
||||
public:
|
||||
static size_t DeviceBytes(const RefinementGPUSetup &setup, size_t fft_work_bytes, size_t max_records,
|
||||
size_t max_slots);
|
||||
static size_t FFTWorkBytes(int device, const ModelDensityGrid &grid);
|
||||
|
||||
RefinementGPUEngine(int device, const RefinementGPUSetup &setup, size_t fft_work_bytes);
|
||||
~RefinementGPUEngine();
|
||||
|
||||
// n_cen exp(blur s^2 / 4) per reflection: prepare_asu_data()'s unblur.
|
||||
void SetBlur(double blur);
|
||||
// Per macro-cycle: F_model = k_total (F + fmask), and the likelihood's D and variance.
|
||||
void SetRowModel(const std::vector<float> &k_total, const std::vector<std::array<float, 2>> &fmask,
|
||||
const std::vector<float> &d, const std::vector<float> &var);
|
||||
|
||||
// The atoms, as ModelStructureFactorsGPU takes them: densities, grid positions (fractional times the grid
|
||||
// size, not wrapped) and the boxes of ModelDensityGPU::Boxes().
|
||||
void SetAtoms(const std::vector<ModelDensityAtom> &atoms, const std::vector<std::array<float, 4>> &pos);
|
||||
// F of the atoms at the rows.
|
||||
void Fcalc(std::vector<std::array<float, 2>> &f);
|
||||
// The bulk-solvent mask's F at the rows (the mask atoms fractional, wrapped).
|
||||
void Fmask(const std::vector<ModelMaskAtom> &mask_atoms, std::vector<std::array<float, 2>> &f);
|
||||
// -ln L over the working rows for the atoms last set and, with `grad`, dT/d(x, y, z, B) per atom.
|
||||
double Evaluate(std::vector<std::array<float, 4>> *grad);
|
||||
|
||||
// Restraint set `which` (0 or 1), as RestraintSet holds it.
|
||||
void SetRestraints(int which, const std::vector<RestraintRecord> &records, const std::vector<int> &plane_atoms,
|
||||
const std::vector<int> &slot_start, const std::vector<int> &slot_index, int slots);
|
||||
// The sum of E of set `which` at `xyzb` (x, y, z, B per atom) and dE/d(x, y, z, B) per atom.
|
||||
double EvaluateRestraints(int which, const std::vector<std::array<float, 4>> &xyzb,
|
||||
std::vector<std::array<float, 4>> *grad);
|
||||
|
||||
// The most atoms of a plane restraint the kernel takes.
|
||||
static constexpr int MAX_PLANE_ATOMS = 32;
|
||||
|
||||
private:
|
||||
std::unique_ptr<RefinementGPUEngineImpl> impl_;
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
// The per-term arithmetic of restrained refinement (Refinement.h), written once for the CPU and the GPU:
|
||||
// the maximum-likelihood amplitude target of one reflection and the geometry restraints, each with its
|
||||
// derivatives. Plain functions on plain numbers, so the CUDA engine (RefinementGPU.cu) and the CPU path
|
||||
// (RefinementXray.cpp, RefinementRestraints.cpp) evaluate the same expressions.
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#if defined(__CUDACC__)
|
||||
#define REFINE_HD __host__ __device__
|
||||
#else
|
||||
#define REFINE_HD
|
||||
#endif
|
||||
|
||||
// What a restraint is, for the evaluators on either side (RefinementRestraints.h builds them).
|
||||
enum class RestraintKind : int { Bond = 0, Angle, Torsion, Chiral, Plane, Repulsion, BSimilarity };
|
||||
|
||||
// One restraint. E = ((value - ideal) / sigma)^2, summed over a set.
|
||||
struct RestraintRecord {
|
||||
int kind = 0;
|
||||
int atom[4] = {0, 0, 0, 0}; // Plane: atom[0] the first of its atoms in plane_atoms, atom[1] their number
|
||||
float ideal = 0; // A, degrees, A^3 (chiral) or A (repulsion: the contact distance)
|
||||
float sigma = 1;
|
||||
int period = 0; // Torsion
|
||||
int slot = 0; // the first of its gradient slots, one per atom
|
||||
// Repulsion: where the second atom is, as the image x' = rot x + tran of its position (Cartesian);
|
||||
// the identity elsewhere.
|
||||
float rot[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
|
||||
float tran[3] = {0, 0, 0};
|
||||
};
|
||||
|
||||
namespace refine_math {
|
||||
|
||||
constexpr double PI = 3.14159265358979323846;
|
||||
constexpr double DEG = PI / 180.0;
|
||||
|
||||
// ln I0(x) and I1(x)/I0(x) for x >= 0, from the polynomial approximations of Abramowitz & Stegun (1964),
|
||||
// Handbook of Mathematical Functions, 9.8.1-9.8.4 (relative error below 2e-7); for large x through the
|
||||
// exponentially scaled form, so nothing overflows.
|
||||
template <typename T>
|
||||
REFINE_HD inline void BesselI0I1(T x, T &log_i0, T &i1_over_i0) {
|
||||
if (x < T(3.75)) {
|
||||
const T t = (x / T(3.75)) * (x / T(3.75));
|
||||
const T i0 = T(1.0) + t * (T(3.5156229) + t * (T(3.0899424) + t * (T(1.2067492) +
|
||||
t * (T(0.2659732) + t * (T(0.0360768) + t * T(0.0045813))))));
|
||||
const T i1 = x * (T(0.5) + t * (T(0.87890594) + t * (T(0.51498869) + t * (T(0.15084934) +
|
||||
t * (T(0.02658733) + t * (T(0.00301532) + t * T(0.00032411)))))));
|
||||
log_i0 = log(i0);
|
||||
i1_over_i0 = i1 / i0;
|
||||
} else {
|
||||
const T t = T(3.75) / x;
|
||||
// sqrt(x) exp(-x) I0(x) and sqrt(x) exp(-x) I1(x)
|
||||
const T s0 = T(0.39894228) + t * (T(0.01328592) + t * (T(0.00225319) + t * (T(-0.00157565) +
|
||||
t * (T(0.00916281) + t * (T(-0.02057706) + t * (T(0.02635537) + t * (T(-0.01647633) +
|
||||
t * T(0.00392377))))))));
|
||||
const T s1 = T(0.39894228) + t * (T(-0.03988024) + t * (T(-0.00362018) + t * (T(0.00163801) +
|
||||
t * (T(-0.01031555) + t * (T(0.02282967) + t * (T(-0.02895312) + t * (T(0.01787654) +
|
||||
t * T(-0.00420059))))))));
|
||||
log_i0 = x + log(s0) - T(0.5) * log(x);
|
||||
i1_over_i0 = s1 / s0;
|
||||
}
|
||||
}
|
||||
|
||||
// The likelihood of one observed amplitude given the model amplitude Fc - the Rice distribution for an
|
||||
// acentric reflection, its centric counterpart otherwise - with D the fraction of the model that is
|
||||
// right and var the variance the rest of the structure and the measurement leave (epsilon beta plus the
|
||||
// observation's own sigma^2). Returns -ln L without the terms that depend on neither Fc nor the
|
||||
// parameters, and sets d(-ln L)/dFc.
|
||||
// Following Pannu & Read (1996) Acta Cryst. A52, 659-668 and Murshudov, Vagin & Dodson (1997) Acta Cryst.
|
||||
// D53, 240-255
|
||||
template <typename T>
|
||||
REFINE_HD inline T AmplitudeNegLogLikelihood(T fo, T fc, T d, T var, bool centric, T &dfc) {
|
||||
if (!centric) {
|
||||
const T x = T(2.0) * d * fo * fc / var;
|
||||
T log_i0, ratio;
|
||||
BesselI0I1(x, log_i0, ratio);
|
||||
dfc = (T(2.0) * d * d * fc - ratio * T(2.0) * d * fo) / var;
|
||||
return log(var) + (fo * fo + d * d * fc * fc) / var - log_i0;
|
||||
}
|
||||
const T x = d * fo * fc / var;
|
||||
// ln cosh x = x + ln(1 + exp(-2x)) - ln 2 for x >= 0
|
||||
const T log_cosh = x + log(T(1.0) + exp(T(-2.0) * x)) - T(0.6931471805599453);
|
||||
dfc = (d * d * fc - tanh(x) * d * fo) / var;
|
||||
return T(0.5) * log(var) + (fo * fo + d * d * fc * fc) / (T(2.0) * var) - log_cosh;
|
||||
}
|
||||
|
||||
// --- geometry restraints: E = ((value - ideal) / sigma)^2 and dE/dx for each atom ---
|
||||
|
||||
template <typename T>
|
||||
struct V3 {
|
||||
T x, y, z;
|
||||
};
|
||||
|
||||
template <typename T> REFINE_HD inline V3<T> operator-(V3<T> a, V3<T> b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
||||
template <typename T> REFINE_HD inline V3<T> operator+(V3<T> a, V3<T> b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
||||
template <typename T> REFINE_HD inline V3<T> operator*(T s, V3<T> a) { return {s * a.x, s * a.y, s * a.z}; }
|
||||
template <typename T> REFINE_HD inline T Dot(V3<T> a, V3<T> b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
template <typename T> REFINE_HD inline V3<T> Cross(V3<T> a, V3<T> b) {
|
||||
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
|
||||
}
|
||||
template <typename T> REFINE_HD inline T Norm(V3<T> a) { return sqrt(Dot(a, a)); }
|
||||
|
||||
// A bond length.
|
||||
template <typename T>
|
||||
REFINE_HD inline T BondTerm(V3<T> a, V3<T> b, T ideal, T sigma, V3<T> g[2]) {
|
||||
const V3<T> d = a - b;
|
||||
const T len = Norm(d);
|
||||
const T r = (len - ideal) / sigma;
|
||||
const T f = len > T(0) ? T(2.0) * r / (sigma * len) : T(0);
|
||||
g[0] = f * d;
|
||||
g[1] = (-f) * d;
|
||||
return r * r;
|
||||
}
|
||||
|
||||
// A bond angle at b, ideal and sigma in degrees.
|
||||
template <typename T>
|
||||
REFINE_HD inline T AngleTerm(V3<T> a, V3<T> b, V3<T> c, T ideal, T sigma, V3<T> g[3]) {
|
||||
const V3<T> u = a - b, v = c - b;
|
||||
const T lu = Norm(u), lv = Norm(v);
|
||||
const V3<T> cr = Cross(u, v);
|
||||
const T theta = atan2(Norm(cr), Dot(u, v));
|
||||
const T s = T(DEG);
|
||||
const T r = (theta - ideal * s) / (sigma * s);
|
||||
// dtheta/du = (cos(theta) u/|u| - v/|v|) / (|u| sin(theta)), and the same with u and v swapped
|
||||
const T sin_t = fmax(sin(theta), T(1e-6)), cos_t = cos(theta);
|
||||
const T f = T(2.0) * r / (sigma * s);
|
||||
const V3<T> du = (T(1.0) / (lu * sin_t)) * ((cos_t / lu) * u - (T(1.0) / lv) * v);
|
||||
const V3<T> dv = (T(1.0) / (lv * sin_t)) * ((cos_t / lv) * v - (T(1.0) / lu) * u);
|
||||
g[0] = f * du;
|
||||
g[2] = f * dv;
|
||||
g[1] = (-f) * (du + dv);
|
||||
return r * r;
|
||||
}
|
||||
|
||||
// The dihedral angle p0-p1-p2-p3 in radians, and its derivatives with respect to the four positions.
|
||||
// Following Blondel & Karplus (1996) J. Comput. Chem. 17, 1132-1141
|
||||
template <typename T>
|
||||
REFINE_HD inline T Dihedral(V3<T> p0, V3<T> p1, V3<T> p2, V3<T> p3, V3<T> d[4]) {
|
||||
const V3<T> b1 = p1 - p0, b2 = p2 - p1, b3 = p3 - p2;
|
||||
const V3<T> m = Cross(b1, b2), n = Cross(b2, b3);
|
||||
const T lb2 = Norm(b2);
|
||||
const T mm = fmax(Dot(m, m), T(1e-12)), nn = fmax(Dot(n, n), T(1e-12));
|
||||
const T phi = atan2(lb2 * Dot(b1, n), Dot(m, n));
|
||||
d[0] = (-lb2 / mm) * m;
|
||||
d[3] = (lb2 / nn) * n;
|
||||
const T bb = lb2 * lb2;
|
||||
const T p = Dot(b1, b2) / bb, q = Dot(b3, b2) / bb;
|
||||
d[1] = (-(T(1.0) + p)) * d[0] + q * d[3];
|
||||
d[2] = (-T(1.0)) * (d[0] + d[1] + d[3]);
|
||||
return phi;
|
||||
}
|
||||
|
||||
// A torsion restrained to ideal (degrees) modulo 360/period; period 0 is read as 1.
|
||||
template <typename T>
|
||||
REFINE_HD inline T TorsionTerm(V3<T> p0, V3<T> p1, V3<T> p2, V3<T> p3, T ideal, T sigma, int period, V3<T> g[4]) {
|
||||
V3<T> d[4];
|
||||
const T phi = Dihedral(p0, p1, p2, p3, d) / T(DEG);
|
||||
const T step = T(360.0) / T(period > 0 ? period : 1);
|
||||
T delta = phi - ideal;
|
||||
delta -= step * floor(delta / step + T(0.5));
|
||||
const T r = delta / sigma;
|
||||
const T f = T(2.0) * r / (sigma * T(DEG));
|
||||
for (int i = 0; i < 4; i++)
|
||||
g[i] = f * d[i];
|
||||
return r * r;
|
||||
}
|
||||
|
||||
// A chiral volume (c the centre): (a1 - c) . ((a2 - c) x (a3 - c)).
|
||||
template <typename T>
|
||||
REFINE_HD inline T ChiralTerm(V3<T> c, V3<T> a1, V3<T> a2, V3<T> a3, T ideal, T sigma, V3<T> g[4]) {
|
||||
const V3<T> x1 = a1 - c, x2 = a2 - c, x3 = a3 - c;
|
||||
const T vol = Dot(x1, Cross(x2, x3));
|
||||
const T r = (vol - ideal) / sigma;
|
||||
const T f = T(2.0) * r / sigma;
|
||||
g[1] = f * Cross(x2, x3);
|
||||
g[2] = f * Cross(x3, x1);
|
||||
g[3] = f * Cross(x1, x2);
|
||||
g[0] = (-T(1.0)) * (g[1] + g[2] + g[3]);
|
||||
return r * r;
|
||||
}
|
||||
|
||||
// A repulsion below the contact distance d0 (none above it).
|
||||
template <typename T>
|
||||
REFINE_HD inline T RepulsionTerm(V3<T> a, V3<T> b, T d0, T sigma, V3<T> g[2]) {
|
||||
const V3<T> d = a - b;
|
||||
const T len = Norm(d);
|
||||
if (len >= d0 || len <= T(0)) {
|
||||
g[0] = g[1] = V3<T>{0, 0, 0};
|
||||
return T(0);
|
||||
}
|
||||
const T r = (d0 - len) / sigma;
|
||||
const T f = T(-2.0) * r / (sigma * len);
|
||||
g[0] = f * d;
|
||||
g[1] = (-f) * d;
|
||||
return r * r;
|
||||
}
|
||||
|
||||
// The unit eigenvector of the smallest eigenvalue of a symmetric 3x3 matrix (a00 a11 a22 a01 a02 a12), by
|
||||
// cyclic Jacobi rotations.
|
||||
template <typename T>
|
||||
REFINE_HD inline V3<T> SmallestEigenvector(const T m_in[6]) {
|
||||
T a[3][3] = {{m_in[0], m_in[3], m_in[4]}, {m_in[3], m_in[1], m_in[5]}, {m_in[4], m_in[5], m_in[2]}};
|
||||
T v[3][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
|
||||
for (int sweep = 0; sweep < 12; sweep++)
|
||||
for (int p = 0; p < 2; p++)
|
||||
for (int q = p + 1; q < 3; q++) {
|
||||
if (fabs(a[p][q]) < T(1e-30))
|
||||
continue;
|
||||
const T theta = (a[q][q] - a[p][p]) / (T(2.0) * a[p][q]);
|
||||
const T t = (theta >= 0 ? T(1.0) : T(-1.0)) / (fabs(theta) + sqrt(theta * theta + T(1.0)));
|
||||
const T c = T(1.0) / sqrt(t * t + T(1.0)), s = t * c;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
const T akp = a[k][p], akq = a[k][q];
|
||||
a[k][p] = c * akp - s * akq;
|
||||
a[k][q] = s * akp + c * akq;
|
||||
}
|
||||
for (int k = 0; k < 3; k++) {
|
||||
const T apk = a[p][k], aqk = a[q][k];
|
||||
a[p][k] = c * apk - s * aqk;
|
||||
a[q][k] = s * apk + c * aqk;
|
||||
}
|
||||
for (int k = 0; k < 3; k++) {
|
||||
const T vkp = v[k][p], vkq = v[k][q];
|
||||
v[k][p] = c * vkp - s * vkq;
|
||||
v[k][q] = s * vkp + c * vkq;
|
||||
}
|
||||
}
|
||||
int i = 0;
|
||||
if (a[1][1] < a[i][i])
|
||||
i = 1;
|
||||
if (a[2][2] < a[i][i])
|
||||
i = 2;
|
||||
return {v[0][i], v[1][i], v[2][i]};
|
||||
}
|
||||
|
||||
// A plane: the distances of n atoms from their least-squares plane. The plane is optimal, so the
|
||||
// derivative of each atom's distance carries no term from the plane moving: dE/dx_i = 2 (n.(x_i - c)) n / sigma^2.
|
||||
template <typename T>
|
||||
REFINE_HD inline T PlaneTerm(const V3<T> *x, int n, T sigma, V3<T> *g) {
|
||||
V3<T> c{0, 0, 0};
|
||||
for (int i = 0; i < n; i++)
|
||||
c = c + x[i];
|
||||
c = (T(1.0) / T(n)) * c;
|
||||
T m[6] = {0, 0, 0, 0, 0, 0};
|
||||
for (int i = 0; i < n; i++) {
|
||||
const V3<T> d = x[i] - c;
|
||||
m[0] += d.x * d.x;
|
||||
m[1] += d.y * d.y;
|
||||
m[2] += d.z * d.z;
|
||||
m[3] += d.x * d.y;
|
||||
m[4] += d.x * d.z;
|
||||
m[5] += d.y * d.z;
|
||||
}
|
||||
const V3<T> nv = SmallestEigenvector(m);
|
||||
T e = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const T r = Dot(nv, x[i] - c) / sigma;
|
||||
e += r * r;
|
||||
g[i] = (T(2.0) * r / sigma) * nv;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
} // namespace refine_math
|
||||
@@ -0,0 +1,568 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "RefinementRestraints.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "gemmi/neighbor.hpp"
|
||||
#include "gemmi/polyheur.hpp" // setup_entities
|
||||
#include "gemmi/riding_h.hpp" // place_hydrogens_on_all_atoms
|
||||
#include "gemmi/topo.hpp"
|
||||
|
||||
#include "MonomerLibrary.h"
|
||||
#include "RefinementMath.h"
|
||||
|
||||
using refine_math::V3;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float CHIRAL_SIGMA = 0.2f; // A^3
|
||||
constexpr float REPULSION_SIGMA = 0.2f; // A
|
||||
constexpr float HBOND_CONTACT = 2.6f; // A
|
||||
constexpr float METAL_CONTACT = 1.9f; // A, to N, O or S
|
||||
// B similarity: main chain and the rest, for bonded pairs and pairs bonded to a common atom.
|
||||
constexpr float B_SIGMA_MAIN_BOND = 1.5f, B_SIGMA_MAIN_ANGLE = 2.0f;
|
||||
constexpr float B_SIGMA_SIDE_BOND = 3.0f, B_SIGMA_SIDE_ANGLE = 4.5f;
|
||||
|
||||
bool IsMainChain(const gemmi::Atom &a) {
|
||||
static const std::set<std::string> names = {"N", "CA", "C", "O", "OXT", "P", "OP1", "OP2", "O5'", "C5'",
|
||||
"C4'", "C3'", "O3'"};
|
||||
return names.count(a.name) > 0;
|
||||
}
|
||||
|
||||
bool IsMetal(const gemmi::Element &el) {
|
||||
return el.is_metal();
|
||||
}
|
||||
|
||||
// The radius van der Waals contacts are made of, without hydrogens: the dictionary's for the atom's
|
||||
// energy type, or an element's typical value where the residue is not described.
|
||||
float VdwRadius(const gemmi::MonLib &monlib, const std::string &resname, const gemmi::Atom &a) {
|
||||
auto cc = monlib.monomers.find(resname);
|
||||
if (cc != monlib.monomers.end()) {
|
||||
auto it = cc->second.find_atom(a.name);
|
||||
if (it != cc->second.atoms.end()) {
|
||||
auto e = monlib.ener_lib.atoms.find(it->chem_type);
|
||||
if (e != monlib.ener_lib.atoms.end() && std::isfinite(e->second.vdw_radius))
|
||||
return static_cast<float>(e->second.vdw_radius);
|
||||
}
|
||||
}
|
||||
switch (a.element.elem) {
|
||||
case gemmi::El::C: return 1.70f;
|
||||
case gemmi::El::N: return 1.55f;
|
||||
case gemmi::El::O: return 1.52f;
|
||||
case gemmi::El::S: return 1.80f;
|
||||
case gemmi::El::P: return 1.80f;
|
||||
default: return 1.70f;
|
||||
}
|
||||
}
|
||||
|
||||
bool CanHydrogenBond(const gemmi::MonLib &monlib, const std::string &resname, const gemmi::Atom &a) {
|
||||
if (a.element != gemmi::El::N && a.element != gemmi::El::O)
|
||||
return false;
|
||||
auto cc = monlib.monomers.find(resname);
|
||||
if (cc != monlib.monomers.end()) {
|
||||
auto it = cc->second.find_atom(a.name);
|
||||
if (it != cc->second.atoms.end()) {
|
||||
auto e = monlib.ener_lib.atoms.find(it->chem_type);
|
||||
if (e != monlib.ener_lib.atoms.end())
|
||||
return e->second.hb_type == 'D' || e->second.hb_type == 'A' || e->second.hb_type == 'B' ||
|
||||
e->second.hb_type == 'H';
|
||||
}
|
||||
}
|
||||
return true; // N or O of an undescribed residue
|
||||
}
|
||||
|
||||
V3<double> At(const std::vector<std::array<double, 4>> &xyzb, int i) {
|
||||
return {xyzb[i][0], xyzb[i][1], xyzb[i][2]};
|
||||
}
|
||||
|
||||
V3<double> Image(const RestraintRecord &r, V3<double> p) {
|
||||
return {r.rot[0] * p.x + r.rot[1] * p.y + r.rot[2] * p.z + r.tran[0],
|
||||
r.rot[3] * p.x + r.rot[4] * p.y + r.rot[5] * p.z + r.tran[1],
|
||||
r.rot[6] * p.x + r.rot[7] * p.y + r.rot[8] * p.z + r.tran[2]};
|
||||
}
|
||||
|
||||
// The gradient at an image back on the atom it is an image of: rot^T g.
|
||||
V3<double> FromImage(const RestraintRecord &r, V3<double> g) {
|
||||
return {r.rot[0] * g.x + r.rot[3] * g.y + r.rot[6] * g.z,
|
||||
r.rot[1] * g.x + r.rot[4] * g.y + r.rot[7] * g.z,
|
||||
r.rot[2] * g.x + r.rot[5] * g.y + r.rot[8] * g.z};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RestraintSet::Add(RestraintRecord r, int n_atoms_of_r) {
|
||||
r.slot = slots;
|
||||
slots += n_atoms_of_r;
|
||||
records.push_back(r);
|
||||
}
|
||||
|
||||
void RestraintSet::Append(const RestraintSet &other) {
|
||||
const int plane_offset = static_cast<int>(plane_atoms.size());
|
||||
for (RestraintRecord r : other.records) {
|
||||
r.slot += slots;
|
||||
if (r.kind == static_cast<int>(RestraintKind::Plane))
|
||||
r.atom[0] += plane_offset;
|
||||
records.push_back(r);
|
||||
}
|
||||
plane_atoms.insert(plane_atoms.end(), other.plane_atoms.begin(), other.plane_atoms.end());
|
||||
slots += other.slots;
|
||||
}
|
||||
|
||||
void RestraintSet::Finalize(size_t n_atoms) {
|
||||
slot_atom.assign(slots, -1);
|
||||
for (const RestraintRecord &r : records) {
|
||||
if (r.kind == static_cast<int>(RestraintKind::Plane)) {
|
||||
for (int k = 0; k < r.atom[1]; k++)
|
||||
slot_atom[r.slot + k] = plane_atoms[r.atom[0] + k];
|
||||
} else {
|
||||
const int n = r.kind == static_cast<int>(RestraintKind::Bond) ||
|
||||
r.kind == static_cast<int>(RestraintKind::Repulsion) ||
|
||||
r.kind == static_cast<int>(RestraintKind::BSimilarity) ? 2
|
||||
: r.kind == static_cast<int>(RestraintKind::Angle) ? 3 : 4;
|
||||
for (int k = 0; k < n; k++)
|
||||
slot_atom[r.slot + k] = r.atom[k];
|
||||
}
|
||||
}
|
||||
slot_start.assign(n_atoms + 1, 0);
|
||||
for (int a : slot_atom)
|
||||
if (a >= 0)
|
||||
slot_start[a + 1]++;
|
||||
for (size_t a = 0; a < n_atoms; a++)
|
||||
slot_start[a + 1] += slot_start[a];
|
||||
slot_index.assign(slot_start[n_atoms], 0);
|
||||
std::vector<int> fill(slot_start.begin(), slot_start.end() - 1);
|
||||
for (int s = 0; s < slots; s++)
|
||||
if (slot_atom[s] >= 0)
|
||||
slot_index[fill[slot_atom[s]]++] = s;
|
||||
}
|
||||
|
||||
double RestraintValue(const RestraintSet &set, const RestraintRecord &r, const std::vector<std::array<double, 4>> &xyzb) {
|
||||
V3<double> g[4];
|
||||
switch (static_cast<RestraintKind>(r.kind)) {
|
||||
case RestraintKind::Bond:
|
||||
return refine_math::Norm(At(xyzb, r.atom[0]) - At(xyzb, r.atom[1]));
|
||||
case RestraintKind::Angle: {
|
||||
const V3<double> u = At(xyzb, r.atom[0]) - At(xyzb, r.atom[1]), v = At(xyzb, r.atom[2]) - At(xyzb, r.atom[1]);
|
||||
return std::atan2(refine_math::Norm(refine_math::Cross(u, v)), refine_math::Dot(u, v)) / refine_math::DEG;
|
||||
}
|
||||
case RestraintKind::Torsion:
|
||||
return refine_math::Dihedral(At(xyzb, r.atom[0]), At(xyzb, r.atom[1]), At(xyzb, r.atom[2]),
|
||||
At(xyzb, r.atom[3]), g) / refine_math::DEG;
|
||||
case RestraintKind::Chiral: {
|
||||
const V3<double> c = At(xyzb, r.atom[0]);
|
||||
return refine_math::Dot(At(xyzb, r.atom[1]) - c,
|
||||
refine_math::Cross(At(xyzb, r.atom[2]) - c, At(xyzb, r.atom[3]) - c));
|
||||
}
|
||||
case RestraintKind::Repulsion:
|
||||
return refine_math::Norm(At(xyzb, r.atom[0]) - Image(r, At(xyzb, r.atom[1])));
|
||||
case RestraintKind::BSimilarity:
|
||||
return xyzb[r.atom[0]][3] - xyzb[r.atom[1]][3];
|
||||
case RestraintKind::Plane:
|
||||
break;
|
||||
}
|
||||
(void) set;
|
||||
return NAN;
|
||||
}
|
||||
|
||||
double EvaluateRestraints(const RestraintSet &set, const std::vector<std::array<double, 4>> &xyzb,
|
||||
std::vector<std::array<double, 4>> *grad) {
|
||||
double sum = 0;
|
||||
std::vector<V3<double>> px, pg;
|
||||
for (const RestraintRecord &r : set.records) {
|
||||
V3<double> g[4] = {};
|
||||
int n = 0;
|
||||
double e = 0;
|
||||
const double ideal = r.ideal, sigma = r.sigma;
|
||||
switch (static_cast<RestraintKind>(r.kind)) {
|
||||
case RestraintKind::Bond:
|
||||
e = refine_math::BondTerm(At(xyzb, r.atom[0]), At(xyzb, r.atom[1]), ideal, sigma, g);
|
||||
n = 2;
|
||||
break;
|
||||
case RestraintKind::Angle:
|
||||
e = refine_math::AngleTerm(At(xyzb, r.atom[0]), At(xyzb, r.atom[1]), At(xyzb, r.atom[2]), ideal, sigma, g);
|
||||
n = 3;
|
||||
break;
|
||||
case RestraintKind::Torsion:
|
||||
e = refine_math::TorsionTerm(At(xyzb, r.atom[0]), At(xyzb, r.atom[1]), At(xyzb, r.atom[2]),
|
||||
At(xyzb, r.atom[3]), ideal, sigma, r.period, g);
|
||||
n = 4;
|
||||
break;
|
||||
case RestraintKind::Chiral:
|
||||
e = refine_math::ChiralTerm(At(xyzb, r.atom[0]), At(xyzb, r.atom[1]), At(xyzb, r.atom[2]),
|
||||
At(xyzb, r.atom[3]), ideal, sigma, g);
|
||||
n = 4;
|
||||
break;
|
||||
case RestraintKind::Repulsion:
|
||||
e = refine_math::RepulsionTerm(At(xyzb, r.atom[0]), Image(r, At(xyzb, r.atom[1])), ideal, sigma, g);
|
||||
g[1] = FromImage(r, g[1]);
|
||||
n = 2;
|
||||
break;
|
||||
case RestraintKind::Plane: {
|
||||
const int m = r.atom[1];
|
||||
px.resize(m);
|
||||
pg.resize(m);
|
||||
for (int k = 0; k < m; k++)
|
||||
px[k] = At(xyzb, set.plane_atoms[r.atom[0] + k]);
|
||||
e = refine_math::PlaneTerm(px.data(), m, sigma, pg.data());
|
||||
if (grad != nullptr)
|
||||
for (int k = 0; k < m; k++) {
|
||||
std::array<double, 4> &d = (*grad)[set.plane_atoms[r.atom[0] + k]];
|
||||
d[0] += pg[k].x;
|
||||
d[1] += pg[k].y;
|
||||
d[2] += pg[k].z;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RestraintKind::BSimilarity: {
|
||||
const double rr = (xyzb[r.atom[0]][3] - xyzb[r.atom[1]][3]) / sigma;
|
||||
e = rr * rr;
|
||||
if (grad != nullptr) {
|
||||
(*grad)[r.atom[0]][3] += 2 * rr / sigma;
|
||||
(*grad)[r.atom[1]][3] -= 2 * rr / sigma;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
sum += e;
|
||||
if (grad != nullptr)
|
||||
for (int k = 0; k < n; k++) {
|
||||
std::array<double, 4> &d = (*grad)[r.atom[k]];
|
||||
d[0] += g[k].x;
|
||||
d[1] += g[k].y;
|
||||
d[2] += g[k].z;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens) {
|
||||
RefinementTopology out;
|
||||
gemmi::Model &model = st.models.at(0);
|
||||
out.monlib = std::make_shared<gemmi::MonLib>(EmbeddedMonomerLibrary());
|
||||
gemmi::MonLib &monlib = *out.monlib;
|
||||
gemmi::setup_entities(st);
|
||||
gemmi::Logger quiet; // missing atoms and unknown residues are what the fixed residues report
|
||||
quiet.threshold = 0;
|
||||
out.topo = gemmi::prepare_topology(st, monlib, 0,
|
||||
hydrogens ? gemmi::HydrogenChange::ReAddButWater : gemmi::HydrogenChange::NoChange,
|
||||
/*reorder=*/false, quiet, /*ignore_unknown_links=*/true, /*use_cispeps=*/true);
|
||||
gemmi::Topo *topo = out.topo.get();
|
||||
|
||||
// the atoms as they are now, hydrogens added
|
||||
std::unordered_map<const gemmi::Atom *, int> index;
|
||||
std::vector<const gemmi::Residue *> residue_of;
|
||||
for (const gemmi::Chain &ch : model.chains)
|
||||
for (const gemmi::Residue &r : ch.residues)
|
||||
for (const gemmi::Atom &a : r.atoms) {
|
||||
index[&a] = static_cast<int>(residue_of.size());
|
||||
residue_of.push_back(&r);
|
||||
}
|
||||
const size_t n = residue_of.size();
|
||||
out.refinable.assign(n, 0);
|
||||
out.vdw_radius.assign(n, 1.7f);
|
||||
out.hbond.assign(n, 0);
|
||||
out.metal.assign(n, 0);
|
||||
out.altloc.assign(n, 0);
|
||||
out.element.assign(n, gemmi::El::X);
|
||||
out.rider_parent.assign(n, -1);
|
||||
for (const gemmi::Topo::Bond &b : topo->bonds)
|
||||
for (int k = 0; k < 2; k++)
|
||||
if (b.atoms[k]->is_hydrogen() && !b.atoms[1 - k]->is_hydrogen())
|
||||
out.rider_parent[index.at(b.atoms[k])] = index.at(b.atoms[1 - k]);
|
||||
// A hydrogen gemmi could not place, or with no parent, scatters nothing.
|
||||
for (gemmi::Chain &ch : model.chains)
|
||||
for (gemmi::Residue &r : ch.residues)
|
||||
for (gemmi::Atom &a : r.atoms)
|
||||
if (a.is_hydrogen() && (out.rider_parent[index.at(&a)] < 0 || !std::isfinite(a.pos.x))) {
|
||||
a.occ = 0;
|
||||
out.rider_parent[index.at(&a)] = -1;
|
||||
if (!std::isfinite(a.pos.x))
|
||||
a.pos = r.atoms.front().pos;
|
||||
}
|
||||
|
||||
// A residue is refined where the dictionary describes it and every one of its atoms.
|
||||
std::set<std::string> fixed_names;
|
||||
for (const gemmi::Topo::ChainInfo &ci : topo->chain_infos)
|
||||
for (const gemmi::Topo::ResInfo &ri : ci.res_infos) {
|
||||
const gemmi::Residue &res = *ri.res;
|
||||
bool known = ri.orig_chemcomp != nullptr && InEmbeddedMonomerLibrary(res.name);
|
||||
for (const gemmi::Atom &a : res.atoms)
|
||||
known = known && ri.get_final_chemcomp(a.altloc).has_atom(a.name);
|
||||
if (known)
|
||||
out.restrained_residues++;
|
||||
else {
|
||||
out.fixed_residues++;
|
||||
fixed_names.insert(res.name);
|
||||
}
|
||||
for (const gemmi::Atom &a : res.atoms) {
|
||||
const auto it = index.find(&a);
|
||||
if (it == index.end())
|
||||
continue;
|
||||
const int i = it->second;
|
||||
out.refinable[i] = known && a.occ > 0 && !a.is_hydrogen();
|
||||
out.vdw_radius[i] = VdwRadius(monlib, res.name, a);
|
||||
out.hbond[i] = res.is_water() || CanHydrogenBond(monlib, res.name, a);
|
||||
out.metal[i] = res.atoms.size() == 1 && IsMetal(a.element);
|
||||
out.altloc[i] = a.altloc;
|
||||
out.element[i] = a.element.elem;
|
||||
}
|
||||
}
|
||||
out.fixed_names.assign(fixed_names.begin(), fixed_names.end());
|
||||
|
||||
auto idx = [&](const gemmi::Atom *a) { return index.at(a); };
|
||||
auto has_h = [](std::initializer_list<const gemmi::Atom *> atoms) {
|
||||
for (const gemmi::Atom *a : atoms)
|
||||
if (a->is_hydrogen())
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
auto any_refinable = [&](std::initializer_list<int> atoms) {
|
||||
for (int a : atoms)
|
||||
if (out.refinable[a])
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
std::vector<std::set<int>> excluded(n);
|
||||
std::vector<std::vector<int>> bonded(n);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
excluded[i].insert(static_cast<int>(i));
|
||||
|
||||
for (const gemmi::Topo::Bond &b : topo->bonds) {
|
||||
if (b.asu == gemmi::Asu::Different || has_h({b.atoms[0], b.atoms[1]}))
|
||||
continue;
|
||||
const int i = idx(b.atoms[0]), j = idx(b.atoms[1]);
|
||||
excluded[i].insert(j);
|
||||
excluded[j].insert(i);
|
||||
bonded[i].push_back(j);
|
||||
bonded[j].push_back(i);
|
||||
if (!any_refinable({i, j}) || !(b.restr->esd > 0))
|
||||
continue;
|
||||
RestraintRecord r;
|
||||
r.kind = static_cast<int>(RestraintKind::Bond);
|
||||
r.atom[0] = i;
|
||||
r.atom[1] = j;
|
||||
r.ideal = static_cast<float>(b.restr->value);
|
||||
r.sigma = static_cast<float>(b.restr->esd);
|
||||
out.geometry.Add(r, 2);
|
||||
out.bonds++;
|
||||
RestraintRecord rb;
|
||||
rb.kind = static_cast<int>(RestraintKind::BSimilarity);
|
||||
rb.atom[0] = i;
|
||||
rb.atom[1] = j;
|
||||
const bool main = IsMainChain(*b.atoms[0]) && IsMainChain(*b.atoms[1]);
|
||||
rb.sigma = main ? B_SIGMA_MAIN_BOND : B_SIGMA_SIDE_BOND;
|
||||
out.adp.Add(rb, 2);
|
||||
}
|
||||
for (const gemmi::Topo::Angle &an : topo->angles) {
|
||||
if (has_h({an.atoms[0], an.atoms[1], an.atoms[2]}))
|
||||
continue;
|
||||
const int i = idx(an.atoms[0]), j = idx(an.atoms[1]), k = idx(an.atoms[2]);
|
||||
excluded[i].insert(k);
|
||||
excluded[k].insert(i);
|
||||
if (!any_refinable({i, j, k}) || !(an.restr->esd > 0))
|
||||
continue;
|
||||
RestraintRecord r;
|
||||
r.kind = static_cast<int>(RestraintKind::Angle);
|
||||
r.atom[0] = i;
|
||||
r.atom[1] = j;
|
||||
r.atom[2] = k;
|
||||
r.ideal = static_cast<float>(an.restr->value);
|
||||
r.sigma = static_cast<float>(an.restr->esd);
|
||||
out.geometry.Add(r, 3);
|
||||
out.angles++;
|
||||
RestraintRecord rb;
|
||||
rb.kind = static_cast<int>(RestraintKind::BSimilarity);
|
||||
rb.atom[0] = i;
|
||||
rb.atom[1] = k;
|
||||
const bool main = IsMainChain(*an.atoms[0]) && IsMainChain(*an.atoms[2]);
|
||||
rb.sigma = main ? B_SIGMA_MAIN_ANGLE : B_SIGMA_SIDE_ANGLE;
|
||||
out.adp.Add(rb, 2);
|
||||
}
|
||||
for (const gemmi::Topo::Torsion &t : topo->torsions) {
|
||||
const std::string &label = t.restr->label;
|
||||
if (has_h({t.atoms[0], t.atoms[1], t.atoms[2], t.atoms[3]}))
|
||||
continue;
|
||||
if (!(t.restr->esd > 0) || label.rfind("phi", 0) == 0 || label.rfind("psi", 0) == 0 ||
|
||||
label.rfind("C2e", 0) == 0 || label.rfind("C3e", 0) == 0)
|
||||
continue;
|
||||
const int a = idx(t.atoms[0]), b = idx(t.atoms[1]), c = idx(t.atoms[2]), d = idx(t.atoms[3]);
|
||||
if (!any_refinable({a, b, c, d}))
|
||||
continue;
|
||||
RestraintRecord r;
|
||||
r.kind = static_cast<int>(RestraintKind::Torsion);
|
||||
r.atom[0] = a;
|
||||
r.atom[1] = b;
|
||||
r.atom[2] = c;
|
||||
r.atom[3] = d;
|
||||
r.ideal = static_cast<float>(t.restr->value);
|
||||
r.sigma = static_cast<float>(t.restr->esd);
|
||||
r.period = t.restr->period;
|
||||
out.geometry.Add(r, 4);
|
||||
out.torsions++;
|
||||
}
|
||||
for (const gemmi::Topo::Chirality &ch : topo->chirs) {
|
||||
if (ch.restr->sign == gemmi::ChiralityType::Both || has_h({ch.atoms[0], ch.atoms[1], ch.atoms[2], ch.atoms[3]}))
|
||||
continue;
|
||||
const int c = idx(ch.atoms[0]), a1 = idx(ch.atoms[1]), a2 = idx(ch.atoms[2]), a3 = idx(ch.atoms[3]);
|
||||
if (!any_refinable({c, a1, a2, a3}))
|
||||
continue;
|
||||
const double volume = topo->ideal_chiral_abs_volume(ch);
|
||||
RestraintRecord r;
|
||||
r.kind = static_cast<int>(RestraintKind::Chiral);
|
||||
r.atom[0] = c;
|
||||
r.atom[1] = a1;
|
||||
r.atom[2] = a2;
|
||||
r.atom[3] = a3;
|
||||
r.ideal = static_cast<float>(ch.restr->sign == gemmi::ChiralityType::Negative ? -volume : volume);
|
||||
r.sigma = CHIRAL_SIGMA;
|
||||
out.geometry.Add(r, 4);
|
||||
out.chirals++;
|
||||
}
|
||||
for (const gemmi::Topo::Plane &p_all : topo->planes) {
|
||||
gemmi::Topo::Plane p = p_all;
|
||||
std::erase_if(p.atoms, [](const gemmi::Atom *a) { return a->is_hydrogen(); });
|
||||
if (p.atoms.size() < 4 || !(p.restr->esd > 0))
|
||||
continue;
|
||||
bool any = false;
|
||||
for (const gemmi::Atom *a : p.atoms)
|
||||
any = any || out.refinable[idx(a)];
|
||||
if (!any)
|
||||
continue;
|
||||
RestraintRecord r;
|
||||
r.kind = static_cast<int>(RestraintKind::Plane);
|
||||
r.atom[0] = static_cast<int>(out.geometry.plane_atoms.size());
|
||||
r.atom[1] = static_cast<int>(p.atoms.size());
|
||||
r.sigma = static_cast<float>(p.restr->esd);
|
||||
for (const gemmi::Atom *a : p.atoms)
|
||||
out.geometry.plane_atoms.push_back(idx(a));
|
||||
out.geometry.Add(r, r.atom[1]);
|
||||
out.planes++;
|
||||
}
|
||||
// and those three bonds away, whose distance the torsions and planes govern
|
||||
std::vector<std::set<int>> within3 = excluded;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
for (int j : excluded[i])
|
||||
for (int k : bonded[j])
|
||||
within3[i].insert(k);
|
||||
out.excluded.resize(n);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
out.excluded[i].assign(within3[i].begin(), within3[i].end());
|
||||
out.geometry.Finalize(n);
|
||||
out.adp.Finalize(n);
|
||||
return out;
|
||||
}
|
||||
|
||||
RestraintSet BuildRepulsions(const RefinementTopology &topo, const gemmi::Model &model, const gemmi::UnitCell &cell_in,
|
||||
const gemmi::SpaceGroup &sg, double margin) {
|
||||
RestraintSet out;
|
||||
gemmi::UnitCell cell = cell_in;
|
||||
cell.set_cell_images_from_spacegroup(&sg);
|
||||
gemmi::Model copy = model;
|
||||
float max_radius = 0;
|
||||
for (float r : topo.vdw_radius)
|
||||
max_radius = std::max(max_radius, r);
|
||||
const double reach = 2 * max_radius + margin;
|
||||
gemmi::NeighborSearch ns(copy, cell, std::max(reach, 5.0));
|
||||
ns.populate(false);
|
||||
|
||||
std::vector<std::vector<int>> first(copy.chains.size());
|
||||
std::vector<gemmi::Position> pos;
|
||||
for (size_t c = 0; c < copy.chains.size(); c++)
|
||||
for (const gemmi::Residue &r : copy.chains[c].residues) {
|
||||
first[c].push_back(static_cast<int>(pos.size()));
|
||||
for (const gemmi::Atom &a : r.atoms)
|
||||
pos.push_back(a.pos);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < pos.size(); i++) {
|
||||
const gemmi::Fractional fi = cell.fractionalize(pos[i]);
|
||||
ns.for_each(pos[i], '\0', reach, [&](gemmi::NeighborSearch::Mark &m, double) {
|
||||
const int j = first[m.chain_idx][m.residue_idx] + m.atom_idx;
|
||||
if (topo.rider_parent[i] >= 0 || topo.rider_parent[j] >= 0 || topo.element[i] == gemmi::El::H ||
|
||||
topo.element[j] == gemmi::El::H)
|
||||
return;
|
||||
if (!topo.refinable[i] && !topo.refinable[j])
|
||||
return;
|
||||
if (m.image_idx == 0 && (static_cast<int>(i) >= j ||
|
||||
std::binary_search(topo.excluded[i].begin(), topo.excluded[i].end(), j)))
|
||||
return;
|
||||
if (m.image_idx != 0 && static_cast<int>(i) > j)
|
||||
return;
|
||||
if (topo.altloc[i] && topo.altloc[j] && topo.altloc[i] != topo.altloc[j])
|
||||
return;
|
||||
// The image of j nearest to i, as a Cartesian x -> R x + t.
|
||||
const gemmi::FTransform ft = ns.get_image_transformation(m.image_idx);
|
||||
const gemmi::Fractional fj = ft.apply(cell.fractionalize(pos[j]));
|
||||
const gemmi::Vec3 shift(std::round(fi.x - fj.x), std::round(fi.y - fj.y), std::round(fi.z - fj.z));
|
||||
if (m.image_idx == 0 && (shift.x != 0 || shift.y != 0 || shift.z != 0))
|
||||
return; // the same copy's own lattice translate: the identity term covers the contact
|
||||
const gemmi::Mat33 rot = cell.orth.mat.multiply(ft.mat).multiply(cell.frac.mat);
|
||||
const gemmi::Vec3 tran = cell.orth.mat.multiply(ft.vec + shift);
|
||||
const gemmi::Position image(rot.multiply(pos[j]) + tran);
|
||||
const double d = pos[i].dist(image);
|
||||
|
||||
float d0 = topo.vdw_radius[i] + topo.vdw_radius[j];
|
||||
if (topo.metal[i] || topo.metal[j]) {
|
||||
const gemmi::El other = topo.element[topo.metal[i] ? j : static_cast<int>(i)];
|
||||
if (other == gemmi::El::N || other == gemmi::El::O || other == gemmi::El::S)
|
||||
d0 = METAL_CONTACT;
|
||||
} else if (topo.hbond[i] && topo.hbond[j])
|
||||
d0 = HBOND_CONTACT;
|
||||
if (!(d < d0 + margin))
|
||||
return;
|
||||
RestraintRecord r;
|
||||
r.kind = static_cast<int>(RestraintKind::Repulsion);
|
||||
r.atom[0] = static_cast<int>(i);
|
||||
r.atom[1] = j;
|
||||
r.ideal = d0;
|
||||
r.sigma = REPULSION_SIGMA;
|
||||
for (int a = 0; a < 3; a++) {
|
||||
for (int b = 0; b < 3; b++)
|
||||
r.rot[3 * a + b] = static_cast<float>(rot[a][b]);
|
||||
r.tran[a] = static_cast<float>(tran.at(a));
|
||||
}
|
||||
out.Add(r, 2);
|
||||
});
|
||||
}
|
||||
out.Finalize(pos.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
void PlaceRidingHydrogens(RefinementTopology &topo) {
|
||||
gemmi::place_hydrogens_on_all_atoms(*topo.topo);
|
||||
}
|
||||
|
||||
GeometryStatistics MeasureGeometry(const RestraintSet &geometry, const RestraintSet &repulsions,
|
||||
const std::vector<std::array<double, 4>> &xyzb) {
|
||||
GeometryStatistics s;
|
||||
double sb = 0, sa = 0, sc = 0;
|
||||
int nc = 0;
|
||||
for (const RestraintRecord &r : geometry.records) {
|
||||
const RestraintKind kind = static_cast<RestraintKind>(r.kind);
|
||||
if (kind == RestraintKind::Bond) {
|
||||
const double d = RestraintValue(geometry, r, xyzb) - r.ideal;
|
||||
sb += d * d;
|
||||
s.bonds++;
|
||||
} else if (kind == RestraintKind::Angle) {
|
||||
const double d = RestraintValue(geometry, r, xyzb) - r.ideal;
|
||||
sa += d * d;
|
||||
s.angles++;
|
||||
} else if (kind == RestraintKind::Chiral) {
|
||||
const double d = RestraintValue(geometry, r, xyzb) - r.ideal;
|
||||
sc += d * d;
|
||||
nc++;
|
||||
}
|
||||
}
|
||||
for (const RestraintRecord &r : repulsions.records)
|
||||
if (RestraintValue(repulsions, r, xyzb) < r.ideal - 0.4)
|
||||
s.clashes++;
|
||||
s.rmsd_bonds = s.bonds > 0 ? std::sqrt(sb / s.bonds) : 0;
|
||||
s.rmsd_angles = s.angles > 0 ? std::sqrt(sa / s.angles) : 0;
|
||||
s.rmsd_chirals = nc > 0 ? std::sqrt(sc / nc) : 0;
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "gemmi/model.hpp"
|
||||
#include "gemmi/monlib.hpp"
|
||||
#include "gemmi/topo.hpp"
|
||||
#include "gemmi/unitcell.hpp"
|
||||
|
||||
#include "RefinementMath.h" // RestraintRecord
|
||||
|
||||
// The geometry side of restrained refinement (Refinement.h): what the dictionary says each refinable atom's
|
||||
// surroundings should look like, as flat lists of restraints the CPU and the GPU evaluate alike
|
||||
// (RefinementMath.h). Atoms are numbered in model order, as ModelPositions() lists them.
|
||||
|
||||
// A list of restraints and how their gradients reach the atoms: each restraint writes one gradient slot per
|
||||
// atom it involves, and atom a sums the slots slot_index[slot_start[a] ... slot_start[a + 1]) in that order -
|
||||
// a gather, so that a GPU, summing in the same order, needs no atomics and gives the same bits every run.
|
||||
struct RestraintSet {
|
||||
std::vector<RestraintRecord> records;
|
||||
std::vector<int> plane_atoms;
|
||||
int slots = 0;
|
||||
std::vector<int> slot_start; // n_atoms + 1
|
||||
std::vector<int> slot_index;
|
||||
|
||||
void Add(RestraintRecord r, int n_atoms_of_r);
|
||||
void Append(const RestraintSet &other); // other's records after these; Finalize() again afterwards
|
||||
void Finalize(size_t n_atoms);
|
||||
// Which atom each slot belongs to (Finalize() works it out); -1 for none.
|
||||
std::vector<int> slot_atom;
|
||||
};
|
||||
|
||||
// The value of one restraint's own quantity (a distance, an angle, a volume ...) at `xyzb`, for statistics.
|
||||
double RestraintValue(const RestraintSet &set, const RestraintRecord &r, const std::vector<std::array<double, 4>> &xyzb);
|
||||
|
||||
// The sum of E over the set at `xyzb` (x, y, z, B per atom) and, with `grad`, dE/d(x, y, z, B) per atom (added
|
||||
// to it). On the CPU, in double.
|
||||
double EvaluateRestraints(const RestraintSet &set, const std::vector<std::array<double, 4>> &xyzb,
|
||||
std::vector<std::array<double, 4>> *grad);
|
||||
|
||||
// What refinement knows of a model's chemistry.
|
||||
struct RefinementTopology {
|
||||
RestraintSet geometry; // bonds, angles, torsions, chiral volumes, planes
|
||||
RestraintSet adp; // B of bonded and angle-related atoms alike
|
||||
std::vector<char> refinable; // per atom: its residue is described by the dictionary
|
||||
// Per atom: the atoms the repulsion leaves out - itself and those up to three bonds away (sorted).
|
||||
std::vector<std::vector<int>> excluded;
|
||||
std::vector<float> vdw_radius; // A
|
||||
std::vector<char> hbond; // donor, acceptor or both (water included)
|
||||
std::vector<char> metal; // a metal ion
|
||||
std::vector<char> altloc;
|
||||
std::vector<gemmi::El> element;
|
||||
// Per atom: the heavy atom a hydrogen rides on, -1 for every other atom.
|
||||
std::vector<int> rider_parent;
|
||||
// The topology itself and the dictionary it points into, for placing the hydrogens again.
|
||||
std::shared_ptr<gemmi::MonLib> monlib;
|
||||
std::shared_ptr<gemmi::Topo> topo;
|
||||
int restrained_residues = 0;
|
||||
int fixed_residues = 0;
|
||||
std::vector<std::string> fixed_names; // the residue names held fixed, each once
|
||||
int bonds = 0, angles = 0, torsions = 0, chirals = 0, planes = 0;
|
||||
};
|
||||
|
||||
// The topology of the first model of `st` (hydrogens already removed) from the embedded monomer library
|
||||
// (MonomerLibrary.h), through gemmi's prepare_topology(): links between residues (peptide, cis-peptide,
|
||||
// nucleic-acid backbone, disulfides, the model's LINK records the library describes) and their
|
||||
// modifications included. A residue the library does not describe, or with an atom it does not know, is
|
||||
// held fixed: it stays in the structure factors and the bulk-solvent mask, and it is restrained against.
|
||||
// Torsions: those with an esd, except the main-chain phi/psi and the sugar puckers given as alternatives.
|
||||
// With `hydrogens`, the dictionary's hydrogens are added to the residues it describes (waters excepted) and
|
||||
// placed; they ride on their parent atoms and take part in no restraint.
|
||||
RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens = false);
|
||||
|
||||
// The riding hydrogens placed again on their parents as they are now (gemmi's place_hydrogens_on_all_atoms).
|
||||
void PlaceRidingHydrogens(RefinementTopology &topo);
|
||||
|
||||
// The repulsions of the model as it is now: every pair of atoms, symmetry mates included, closer than its
|
||||
// contact distance plus `margin` and not excluded, where at least one of the two is refinable and the two are
|
||||
// not alternative conformations of each other. Contact distances: van der Waals radii summed, 2.6 A where
|
||||
// both can hydrogen-bond, 1.9 A between a metal and N, O or S.
|
||||
RestraintSet BuildRepulsions(const RefinementTopology &topo, const gemmi::Model &model, const gemmi::UnitCell &cell,
|
||||
const gemmi::SpaceGroup &sg, double margin);
|
||||
|
||||
// The usual summary of a model's geometry.
|
||||
struct GeometryStatistics {
|
||||
double rmsd_bonds = 0, rmsd_angles = 0, rmsd_chirals = 0;
|
||||
int bonds = 0, angles = 0;
|
||||
int clashes = 0; // repulsions overlapping by more than 0.4 A
|
||||
};
|
||||
GeometryStatistics MeasureGeometry(const RestraintSet &geometry, const RestraintSet &repulsions,
|
||||
const std::vector<std::array<double, 4>> &xyzb);
|
||||
@@ -0,0 +1,396 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "RefinementXray.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
|
||||
#include <fftw3.h>
|
||||
|
||||
#include "gemmi/dencalc.hpp"
|
||||
#include "gemmi/it92.hpp"
|
||||
#include "gemmi/math.hpp"
|
||||
|
||||
#include "../../common/ParallelFor.h"
|
||||
#include "ModelFFT.h"
|
||||
#include "ModelGrid.h"
|
||||
#include "RefinementMath.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using Table = gemmi::IT92<float>;
|
||||
|
||||
// The free reflections a likelihood bin is fitted on, at least.
|
||||
constexpr size_t FREE_PER_BIN = 100;
|
||||
constexpr size_t MAX_BINS = 30;
|
||||
|
||||
// The density calculator the refinement grids atoms with: DensityCalculator's settings for the grid of
|
||||
// RigidBodyZoneGrid(), and the given blur.
|
||||
gemmi::DensityCalculator<Table, float> Calculator(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg,
|
||||
double d_min, double blur) {
|
||||
gemmi::DensityCalculator<Table, float> dc;
|
||||
dc.d_min = d_min;
|
||||
dc.rate = 1.5; // RigidBodyRefine.cpp's GRID_RATE, which RigidBodyZoneGrid sizes the grid with
|
||||
dc.grid.unit_cell = cell;
|
||||
dc.grid.spacegroup = &sg;
|
||||
dc.blur = blur;
|
||||
return dc;
|
||||
}
|
||||
|
||||
// -ln L summed over rows `idx` at D and beta.
|
||||
double BinNegLogLikelihood(const RefinementReflections &refl, const std::vector<double> &fc,
|
||||
const std::vector<size_t> &idx, double d, double beta) {
|
||||
double sum = 0, dfc = 0;
|
||||
for (size_t i : idx) {
|
||||
const double var = refl.epsilon[i] * beta + double(refl.sigma[i]) * refl.sigma[i];
|
||||
sum += refine_math::AmplitudeNegLogLikelihood<double>(refl.fo[i], fc[i], d, var, refl.centric[i], dfc);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
// The minimum of f over [lo, hi] by golden-section search.
|
||||
template <typename F>
|
||||
double GoldenMinimum(F f, double lo, double hi, int iterations) {
|
||||
const double g = 0.5 * (std::sqrt(5.0) - 1.0);
|
||||
double a = lo, b = hi;
|
||||
double c = b - g * (b - a), d = a + g * (b - a);
|
||||
double fc = f(c), fd = f(d);
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
if (fc < fd) {
|
||||
b = d;
|
||||
d = c;
|
||||
fd = fc;
|
||||
c = b - g * (b - a);
|
||||
fc = f(c);
|
||||
} else {
|
||||
a = c;
|
||||
c = d;
|
||||
fc = fd;
|
||||
d = a + g * (b - a);
|
||||
fd = f(d);
|
||||
}
|
||||
}
|
||||
return 0.5 * (a + b);
|
||||
}
|
||||
|
||||
class RefinementEngineCPU : public RefinementEngine {
|
||||
public:
|
||||
RefinementEngineCPU(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min,
|
||||
const RefinementReflections &refl, size_t nthreads)
|
||||
: cell_(cell), sg_(sg), d_min_(d_min), refl_(refl), nthreads_(nthreads),
|
||||
grid_(RigidBodyZoneGrid(cell, sg, d_min)), composition_(grid_, d_min, refl.hkl),
|
||||
orbit_leaders_(OrbitLeaders(grid_, nthreads)) {}
|
||||
|
||||
void SetBlur(double blur) override { blur_ = blur; }
|
||||
void SetRowModel(const XrayRowModel &rows) override { rows_ = rows; }
|
||||
bool OnGPU() const override { return false; }
|
||||
|
||||
void Fcalc(const gemmi::Model &model, std::vector<std::complex<float>> &f) override {
|
||||
std::vector<std::complex<double>> fd;
|
||||
Compose(model, fd);
|
||||
f.assign(fd.begin(), fd.end());
|
||||
}
|
||||
|
||||
void Fmask(const gemmi::Model &model, std::vector<std::complex<float>> &f) override {
|
||||
gemmi::Grid<float> mask = grid_;
|
||||
PutMaskOnGrid(mask, model, orbit_leaders_, nthreads_);
|
||||
const gemmi::FPhiGrid<float> fm = MapToFPhi(mask);
|
||||
f.clear();
|
||||
for (const gemmi::Miller &h : refl_.hkl)
|
||||
f.push_back(fm.get_value_by_hkl(h));
|
||||
}
|
||||
|
||||
double Evaluate(const gemmi::Model &model, std::vector<std::array<double, 4>> *grad) override {
|
||||
std::vector<std::complex<double>> f;
|
||||
Compose(model, f);
|
||||
const size_t n = f.size();
|
||||
// The target and, per reflection, dT/dF of the atoms' structure factor (dT/dRe F + i dT/dIm F).
|
||||
std::vector<std::complex<double>> dt_df(n, 0.0);
|
||||
double sum = 0;
|
||||
for (size_t m = 0; m < n; m++) {
|
||||
if (refl_.free[m])
|
||||
continue;
|
||||
const double k = rows_.k_total[m];
|
||||
const std::complex<double> fm = k * (f[m] + std::complex<double>(rows_.fmask[m]));
|
||||
const double fc = std::abs(fm);
|
||||
double dfc = 0;
|
||||
sum += refine_math::AmplitudeNegLogLikelihood<double>(refl_.fo[m], fc, rows_.d[m], rows_.var[m],
|
||||
refl_.centric[m], dfc);
|
||||
if (fc > 0)
|
||||
dt_df[m] = k * dfc * fm / fc;
|
||||
}
|
||||
if (grad == nullptr)
|
||||
return sum;
|
||||
|
||||
// The FFT method's map, dT/drho(x) = (V/N) Re sum_m sum_ops conj(dT/dF_m) scale_m exp(2 pi i h.t)
|
||||
// exp(2 pi i (hR).x), as the inverse real transform of a half-l coefficient grid: a term A at index k
|
||||
// goes in as A/2 at k, or as conj(A)/2 at -k where k is in the half not stored; on the planes l = 0 and
|
||||
// l = nw/2, which hold both, as both.
|
||||
const int nu = grid_.nu, nv = grid_.nv, nw = grid_.nw, nwh = nw / 2 + 1;
|
||||
const size_t ncomplex = static_cast<size_t>(nu) * nv * nwh;
|
||||
std::complex<float> *in = reinterpret_cast<std::complex<float> *>(fftwf_alloc_complex(ncomplex));
|
||||
float *out = fftwf_alloc_real(static_cast<size_t>(nu) * nv * nw);
|
||||
std::fill(in, in + ncomplex, std::complex<float>(0, 0));
|
||||
const std::vector<SymmetryComposition::Term> &terms = composition_.Terms();
|
||||
const size_t ops = composition_.Ops();
|
||||
auto add = [&](int ku, int kv, int kw, std::complex<double> value) {
|
||||
in[gemmi::modulo(ku, nu) + static_cast<size_t>(nu) * (gemmi::modulo(kv, nv) + static_cast<size_t>(nv) * kw)] +=
|
||||
std::complex<float>(value);
|
||||
};
|
||||
for (size_t m = 0; m < n; m++) {
|
||||
if (dt_df[m] == 0.0)
|
||||
continue;
|
||||
const double scale = composition_.Centring() * std::exp(blur_ * 0.25 * composition_.InvD2()[m]);
|
||||
const std::complex<double> c = std::conj(dt_df[m]) * scale;
|
||||
for (size_t o = 0; o < ops; o++) {
|
||||
const SymmetryComposition::Term &t = terms[m * ops + o];
|
||||
const std::complex<double> a = 0.5 * c * t.phase;
|
||||
const gemmi::Miller &k = t.k;
|
||||
if (k[2] > 0 && 2 * k[2] != nw)
|
||||
add(k[0], k[1], k[2], a);
|
||||
else if (k[2] < 0 && -2 * k[2] != nw)
|
||||
add(-k[0], -k[1], -k[2], std::conj(a));
|
||||
else {
|
||||
const int l = k[2] < 0 ? -k[2] : k[2];
|
||||
add(k[0], k[1], l, a);
|
||||
add(-k[0], -k[1], l, std::conj(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
MapFromFPhiExecute(nu, nv, nw, in, out);
|
||||
gemmi::Grid<float> map = grid_;
|
||||
const float norm = static_cast<float>(cell_.volume / static_cast<double>(map.point_count()));
|
||||
for (size_t i = 0; i < map.data.size(); i++)
|
||||
map.data[i] = out[i] * norm;
|
||||
fftwf_free(in);
|
||||
fftwf_free(out);
|
||||
AtomGradientsFromMap(map, model, blur_, nthreads_, *grad);
|
||||
return sum;
|
||||
}
|
||||
|
||||
private:
|
||||
void Compose(const gemmi::Model &model, std::vector<std::complex<double>> &f) const {
|
||||
gemmi::DensityCalculator<Table, float> dc = Calculator(cell_, sg_, d_min_, blur_);
|
||||
PutModelDensityOnGrid(dc, model, {}, nthreads_); // one copy: the symmetry is composed below
|
||||
composition_.Compose(MapToFPhi(dc.grid), blur_, f, nullptr, nthreads_);
|
||||
}
|
||||
|
||||
gemmi::UnitCell cell_;
|
||||
const gemmi::SpaceGroup &sg_;
|
||||
double d_min_;
|
||||
const RefinementReflections &refl_;
|
||||
size_t nthreads_;
|
||||
gemmi::Grid<float> grid_;
|
||||
SymmetryComposition composition_;
|
||||
std::vector<size_t> orbit_leaders_;
|
||||
double blur_ = 0;
|
||||
XrayRowModel rows_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
RefinementReflections PrepareRefinementReflections(const std::vector<MergedReflection> &merged,
|
||||
const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg,
|
||||
double d_min) {
|
||||
const gemmi::GroupOps gops = sg.operations();
|
||||
const gemmi::ReciprocalAsu asu(&sg);
|
||||
// ASU, last one wins where a reflection is listed twice (both hands of an anomalous merge)
|
||||
std::map<gemmi::Miller, const MergedReflection *> by_hkl;
|
||||
for (const MergedReflection &r : merged) {
|
||||
if (!std::isfinite(r.F) || r.F < 0)
|
||||
continue;
|
||||
gemmi::Miller h{{r.h, r.k, r.l}};
|
||||
if (!asu.is_in(h))
|
||||
h = asu.to_asu(h, gops).first;
|
||||
by_hkl[h] = &r;
|
||||
}
|
||||
std::vector<gemmi::Miller> hkl;
|
||||
for (const auto &[h, r] : by_hkl)
|
||||
hkl.push_back(h);
|
||||
const SymmetryComposition composition(RigidBodyZoneGrid(cell, sg, d_min), d_min, hkl);
|
||||
RefinementReflections out;
|
||||
for (size_t i = 0; i < hkl.size(); i++) {
|
||||
if (composition.Row()[i] < 0)
|
||||
continue;
|
||||
const MergedReflection &r = *by_hkl[hkl[i]];
|
||||
out.hkl.push_back(hkl[i]);
|
||||
out.fo.push_back(r.F);
|
||||
out.sigma.push_back(std::isfinite(r.sigmaF) ? r.sigmaF : 0.0f);
|
||||
out.free.push_back(r.rfree_flag);
|
||||
out.centric.push_back(gops.is_reflection_centric(hkl[i]));
|
||||
out.epsilon.push_back(static_cast<float>(gops.epsilon_factor(hkl[i])));
|
||||
out.inv_d2.push_back(cell.calculate_1_d2(hkl[i]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void EstimateLikelihoodParameters(const RefinementReflections &refl, const std::vector<std::complex<float>> &fmodel,
|
||||
XrayRowModel &rows) {
|
||||
const size_t n = refl.Size();
|
||||
std::vector<double> fc(n);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
fc[i] = std::abs(fmodel[i]);
|
||||
std::vector<size_t> free;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (refl.free[i] && fc[i] > 0)
|
||||
free.push_back(i);
|
||||
// Too few free reflections to bin: the working set, which says the model is better than it is, but
|
||||
// still gives the likelihood a scale.
|
||||
if (free.size() < FREE_PER_BIN)
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (!refl.free[i] && fc[i] > 0)
|
||||
free.push_back(i);
|
||||
std::sort(free.begin(), free.end(), [&](size_t a, size_t b) { return refl.inv_d2[a] < refl.inv_d2[b]; });
|
||||
const size_t bins = std::clamp<size_t>(free.size() / FREE_PER_BIN, 1, MAX_BINS);
|
||||
|
||||
std::vector<double> centre(bins), bin_d(bins), bin_beta(bins);
|
||||
for (size_t b = 0; b < bins; b++) {
|
||||
const std::vector<size_t> idx(free.begin() + b * free.size() / bins, free.begin() + (b + 1) * free.size() / bins);
|
||||
double s2 = 0, sfo = 0, sfc2 = 0, sfofc = 0;
|
||||
for (size_t i : idx) {
|
||||
s2 += refl.inv_d2[i];
|
||||
sfo += double(refl.fo[i]) * refl.fo[i] / refl.epsilon[i];
|
||||
sfc2 += fc[i] * fc[i] / refl.epsilon[i];
|
||||
sfofc += refl.fo[i] * fc[i] / refl.epsilon[i];
|
||||
}
|
||||
centre[b] = s2 / static_cast<double>(idx.size());
|
||||
double d = sfc2 > 0 ? std::max(sfofc / sfc2, 0.05) : 1.0;
|
||||
double beta = std::max((sfo - d * d * sfc2) / static_cast<double>(idx.size()), 1e-3 * sfo / idx.size());
|
||||
// Alternating one-dimensional minimisations of -ln L over ln D and ln beta.
|
||||
for (int round = 0; round < 4; round++) {
|
||||
const double ld = GoldenMinimum([&](double x) { return BinNegLogLikelihood(refl, fc, idx, std::exp(x), beta); },
|
||||
std::log(d) - 2.0, std::log(std::min(d * 7.4, 3.0)), 40);
|
||||
d = std::exp(ld);
|
||||
const double lb = GoldenMinimum([&](double x) { return BinNegLogLikelihood(refl, fc, idx, d, std::exp(x)); },
|
||||
std::log(beta) - 3.0, std::log(beta) + 3.0, 40);
|
||||
beta = std::exp(lb);
|
||||
}
|
||||
bin_d[b] = d;
|
||||
bin_beta[b] = beta;
|
||||
}
|
||||
rows.d.resize(n);
|
||||
rows.var.resize(n);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
// linear in 1/d^2 between the bins' centres, flat beyond the outermost
|
||||
const double s = refl.inv_d2[i];
|
||||
double d = bin_d[0], beta = bin_beta[0];
|
||||
if (s >= centre[bins - 1]) {
|
||||
d = bin_d[bins - 1];
|
||||
beta = bin_beta[bins - 1];
|
||||
} else if (s > centre[0]) {
|
||||
const size_t b = std::upper_bound(centre.begin(), centre.end(), s) - centre.begin();
|
||||
const double t = (s - centre[b - 1]) / (centre[b] - centre[b - 1]);
|
||||
d = bin_d[b - 1] + t * (bin_d[b] - bin_d[b - 1]);
|
||||
beta = bin_beta[b - 1] + t * (bin_beta[b] - bin_beta[b - 1]);
|
||||
}
|
||||
rows.d[i] = static_cast<float>(d);
|
||||
rows.var[i] = static_cast<float>(refl.epsilon[i] * beta + double(refl.sigma[i]) * refl.sigma[i]);
|
||||
}
|
||||
}
|
||||
|
||||
RFactors ComputeRFactors(const RefinementReflections &refl, const std::vector<std::complex<float>> &fmodel) {
|
||||
double nw = 0, dw = 0, nf = 0, df = 0;
|
||||
RFactors r;
|
||||
for (size_t i = 0; i < refl.Size(); i++) {
|
||||
const double diff = std::fabs(refl.fo[i] - std::abs(fmodel[i]));
|
||||
if (refl.free[i]) {
|
||||
nf += diff;
|
||||
df += refl.fo[i];
|
||||
r.n_free++;
|
||||
} else {
|
||||
nw += diff;
|
||||
dw += refl.fo[i];
|
||||
r.n_work++;
|
||||
}
|
||||
}
|
||||
r.r_work = dw > 0 ? nw / dw : 1;
|
||||
r.r_free = df > 0 ? nf / df : 1;
|
||||
return r;
|
||||
}
|
||||
|
||||
double RefinementBlur(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min, double b_floor) {
|
||||
// set_refmac_compatible_blur(): u_to_b / 1.1 * spacing^2 - b_min, not below 0, the spacing the one asked for
|
||||
const gemmi::DensityCalculator<Table, float> dc = Calculator(cell, sg, d_min, 0);
|
||||
const double spacing = dc.requested_grid_spacing();
|
||||
return std::max(0.0, gemmi::u_to_b() / 1.1 * spacing * spacing - b_floor);
|
||||
}
|
||||
|
||||
std::unique_ptr<RefinementEngine> MakeRefinementEngineCPU(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg,
|
||||
double d_min, const RefinementReflections &refl,
|
||||
size_t nthreads) {
|
||||
return std::make_unique<RefinementEngineCPU>(cell, sg, d_min, refl, nthreads);
|
||||
}
|
||||
|
||||
void AtomGradientsFromMap(const gemmi::Grid<float> &map, const gemmi::Model &model, double blur, size_t nthreads,
|
||||
std::vector<std::array<double, 4>> &grad) {
|
||||
const gemmi::DensityCalculator<Table, float> dc = Calculator(map.unit_cell, *map.spacegroup, 1.0, blur);
|
||||
std::vector<const gemmi::Atom *> atoms;
|
||||
for (const gemmi::Chain &ch : model.chains)
|
||||
for (const gemmi::Residue &r : ch.residues)
|
||||
for (const gemmi::Atom &a : r.atoms)
|
||||
atoms.push_back(&a);
|
||||
grad.assign(atoms.size(), {0, 0, 0, 0});
|
||||
const int nu = map.nu, nv = map.nv, nw = map.nw;
|
||||
// As ModelGrid.cpp walks a density box: the orthogonalization over the grid size in single precision.
|
||||
const gemmi::UpperTriangularMat33 &om = map.orth_n;
|
||||
const float m00 = static_cast<float>(om.a11), m01 = static_cast<float>(om.a12), m02 = static_cast<float>(om.a13),
|
||||
m11 = static_cast<float>(om.a22), m12 = static_cast<float>(om.a23), m22 = static_cast<float>(om.a33);
|
||||
constexpr double FOUR_PI2 = 4 * refine_math::PI * refine_math::PI;
|
||||
ParallelChunks(static_cast<int>(atoms.size()), nthreads, [&](int lo, int hi) {
|
||||
for (int i = lo; i < hi; i++) {
|
||||
const gemmi::Atom &atom = *atoms[i];
|
||||
const float b = static_cast<float>(atom.b_iso + blur);
|
||||
const auto precal = Table::get(atom.element, atom.charge, atom.serial).precalculate_density_iso(b, 0.0f);
|
||||
const double radius = dc.estimate_radius(precal, b);
|
||||
const float max_r2 = static_cast<float>(radius * radius);
|
||||
const gemmi::Fractional fpos = map.unit_cell.fractionalize(atom.pos);
|
||||
const double n3[3] = {fpos.x * nu, fpos.y * nv, fpos.z * nw};
|
||||
float nctr[3];
|
||||
int c[3], d[3];
|
||||
for (int k = 0; k < 3; k++) {
|
||||
nctr[k] = static_cast<float>(n3[k]);
|
||||
c[k] = gemmi::iround(n3[k]);
|
||||
}
|
||||
d[0] = std::min(static_cast<int>(std::ceil(radius / map.spacing[0])), nu - 1);
|
||||
d[1] = std::min(static_cast<int>(std::ceil(radius / map.spacing[1])), nv - 1);
|
||||
d[2] = std::min(static_cast<int>(std::ceil(radius / map.spacing[2])), nw - 1);
|
||||
double gx = 0, gy = 0, gz = 0, gb = 0;
|
||||
for (int w = c[2] - d[2]; w <= c[2] + d[2]; w++) {
|
||||
const float fz = nctr[2] - static_cast<float>(w);
|
||||
const float z = m22 * fz;
|
||||
const int w_ = gemmi::modulo(w, nw);
|
||||
for (int v = c[1] - d[1]; v <= c[1] + d[1]; v++) {
|
||||
const float fy = nctr[1] - static_cast<float>(v);
|
||||
const float y = m11 * fy + m12 * fz;
|
||||
const float yz = y * y + z * z;
|
||||
if (yz > max_r2)
|
||||
continue;
|
||||
const float x_yz = m01 * fy + m02 * fz;
|
||||
const size_t row = static_cast<size_t>(nu) * (gemmi::modulo(v, nv) + static_cast<size_t>(nv) * w_);
|
||||
for (int u = c[0] - d[0]; u <= c[0] + d[0]; u++) {
|
||||
const float x = m00 * (nctr[0] - static_cast<float>(u)) + x_yz;
|
||||
const float r2 = yz + x * x;
|
||||
if (r2 > max_r2)
|
||||
continue;
|
||||
const double value = map.data[row + gemmi::modulo(u, nu)];
|
||||
double dr2 = 0, db = 0;
|
||||
for (size_t q = 0; q < std::size(precal.a); q++) {
|
||||
const double e = precal.a[q] * gemmi::unsafe_expapprox(std::max(precal.b[q] * r2, -88.f));
|
||||
dr2 += e * precal.b[q];
|
||||
db += e * precal.b[q] * (1.5 + precal.b[q] * r2);
|
||||
}
|
||||
gx += value * dr2 * x;
|
||||
gy += value * dr2 * y;
|
||||
gz += value * dr2 * z;
|
||||
gb += value * db;
|
||||
}
|
||||
}
|
||||
}
|
||||
// rho = occ sum_q a_q exp(b_q r^2): d rho / d(centre) = occ 2 (sum a_q b_q e_q) (centre - point), and
|
||||
// with b_q = -4 pi^2 / (B_q + B + blur), d rho / dB = occ sum a_q e_q b_q (1.5 + b_q r^2) / (4 pi^2).
|
||||
grad[i] = {2.0 * atom.occ * gx, 2.0 * atom.occ * gy, 2.0 * atom.occ * gz, atom.occ * gb / FOUR_PI2};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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);
|
||||
@@ -184,6 +184,7 @@ ADD_EXECUTABLE(jfjoch_test
|
||||
MolecularReplacementTest.cpp
|
||||
ModelMaskGPUTest.cpp
|
||||
ModelDensityGPUTest.cpp
|
||||
RefinementTest.cpp
|
||||
SearchSpaceGroupTest.cpp
|
||||
SearchSpaceGroupTwinTest.cpp
|
||||
SyntheticMergedReflections.h
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include <gemmi/symmetry.hpp>
|
||||
|
||||
#include "../common/CUDAWrapper.h"
|
||||
#include "../common/Logger.h"
|
||||
#include "../image_analysis/structure_refinement/MonomerLibrary.h"
|
||||
#include "../image_analysis/structure_refinement/Refinement.h"
|
||||
#include "../image_analysis/structure_refinement/RefinementMath.h"
|
||||
#include "../image_analysis/structure_refinement/RefinementRestraints.h"
|
||||
#include "../image_analysis/structure_refinement/RefinementXray.h"
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
#include "../image_analysis/structure_refinement/RefinementGPU.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// Copies of residue `code` at the embedded dictionary's ideal coordinates (hydrogens left out), each turned and
|
||||
// moved to a place of its own in the cell.
|
||||
gemmi::Structure residue_crystal(const std::vector<std::string> &codes, const gemmi::UnitCell &cell, const char *group,
|
||||
unsigned seed) {
|
||||
std::mt19937 rng(seed);
|
||||
std::uniform_real_distribution<double> frac(0.0, 1.0), angle(-3.1, 3.1);
|
||||
gemmi::Structure st;
|
||||
st.cell = cell;
|
||||
st.spacegroup_hm = group;
|
||||
st.setup_cell_images();
|
||||
gemmi::Model model(1);
|
||||
gemmi::Chain chain("A");
|
||||
int seq = 1;
|
||||
for (const std::string &code : codes) {
|
||||
const gemmi::ChemComp &cc = EmbeddedMonomerLibrary().monomers.at(code);
|
||||
gemmi::Residue res;
|
||||
res.name = code;
|
||||
res.seqid = gemmi::SeqId(seq++, ' ');
|
||||
res.het_flag = 'H';
|
||||
res.entity_type = gemmi::EntityType::NonPolymer;
|
||||
const gemmi::Position centre = cell.orthogonalize(gemmi::Fractional(frac(rng), frac(rng), frac(rng)));
|
||||
const double ax = angle(rng), ay = angle(rng), az = angle(rng);
|
||||
const gemmi::Mat33 rot = gemmi::Mat33(1, 0, 0, 0, std::cos(ax), -std::sin(ax), 0, std::sin(ax), std::cos(ax))
|
||||
.multiply(gemmi::Mat33(std::cos(ay), 0, std::sin(ay), 0, 1, 0, -std::sin(ay), 0, std::cos(ay)))
|
||||
.multiply(gemmi::Mat33(std::cos(az), -std::sin(az), 0, std::sin(az), std::cos(az), 0, 0, 0, 1));
|
||||
for (const gemmi::ChemComp::Atom &ca : cc.atoms) {
|
||||
if (ca.is_hydrogen())
|
||||
continue;
|
||||
gemmi::Atom a;
|
||||
a.name = ca.id;
|
||||
a.element = ca.el;
|
||||
a.pos = gemmi::Position(rot.multiply(ca.xyz)) + centre;
|
||||
a.b_iso = 20.0f;
|
||||
a.occ = 1.0f;
|
||||
res.atoms.push_back(a);
|
||||
}
|
||||
chain.residues.push_back(res);
|
||||
}
|
||||
model.chains.push_back(chain);
|
||||
st.models.push_back(model);
|
||||
return st;
|
||||
}
|
||||
|
||||
// Atoms of a few elements scattered over the cell.
|
||||
gemmi::Structure random_crystal(const gemmi::UnitCell &cell, const char *group, int n, unsigned seed) {
|
||||
std::mt19937 rng(seed);
|
||||
std::uniform_real_distribution<double> frac(0.0, 1.0), b(10.0, 40.0);
|
||||
const gemmi::El elements[] = {gemmi::El::C, gemmi::El::N, gemmi::El::O, gemmi::El::S};
|
||||
gemmi::Structure st;
|
||||
st.cell = cell;
|
||||
st.spacegroup_hm = group;
|
||||
st.setup_cell_images();
|
||||
gemmi::Model model(1);
|
||||
gemmi::Chain chain("A");
|
||||
gemmi::Residue res;
|
||||
res.name = "UNK";
|
||||
res.seqid = gemmi::SeqId(1, ' ');
|
||||
for (int i = 0; i < n; i++) {
|
||||
gemmi::Atom a;
|
||||
a.name = "X";
|
||||
a.element = elements[i % 4];
|
||||
a.pos = cell.orthogonalize(gemmi::Fractional(frac(rng), frac(rng), frac(rng)));
|
||||
a.b_iso = static_cast<float>(b(rng));
|
||||
a.occ = 1.0f;
|
||||
res.atoms.push_back(a);
|
||||
}
|
||||
chain.residues.push_back(res);
|
||||
model.chains.push_back(chain);
|
||||
st.models.push_back(model);
|
||||
return st;
|
||||
}
|
||||
|
||||
// Every reflection of the group's ASU to d_min, amplitude 1, one in twenty free.
|
||||
std::vector<MergedReflection> asu_reflections(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min) {
|
||||
std::vector<MergedReflection> out;
|
||||
const gemmi::ReciprocalAsu asu(&sg);
|
||||
const gemmi::GroupOps gops = sg.operations();
|
||||
const gemmi::Miller lim = cell.get_hkl_limits(d_min);
|
||||
int i = 0;
|
||||
for (int h = -lim[0]; h <= lim[0]; h++)
|
||||
for (int k = -lim[1]; k <= lim[1]; k++)
|
||||
for (int l = -lim[2]; l <= lim[2]; l++) {
|
||||
const gemmi::Miller m{{h, k, l}};
|
||||
if ((h == 0 && k == 0 && l == 0) || !asu.is_in(m) || gops.is_systematically_absent(m))
|
||||
continue;
|
||||
const double d = cell.calculate_d(m);
|
||||
if (d < d_min)
|
||||
continue;
|
||||
MergedReflection r;
|
||||
r.h = h;
|
||||
r.k = k;
|
||||
r.l = l;
|
||||
r.d = static_cast<float>(d);
|
||||
r.F = 1.0f;
|
||||
r.sigmaF = 0.05f;
|
||||
r.rfree_flag = (i++ % 20) == 0;
|
||||
out.push_back(r);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// The observations set to |F| of `truth`, and a row model with no scale, no solvent and the likelihood's
|
||||
// parameters estimated against `model`.
|
||||
void set_observations(RefinementEngine &engine, RefinementReflections &refl, const gemmi::Model &truth,
|
||||
const gemmi::Model &model) {
|
||||
std::vector<std::complex<float>> f;
|
||||
engine.Fcalc(truth, f);
|
||||
for (size_t m = 0; m < refl.Size(); m++)
|
||||
refl.fo[m] = std::abs(f[m]);
|
||||
engine.Fcalc(model, f);
|
||||
XrayRowModel rows;
|
||||
rows.k_total.assign(refl.Size(), 1.0f);
|
||||
rows.fmask.assign(refl.Size(), {0, 0});
|
||||
EstimateLikelihoodParameters(refl, f, rows);
|
||||
engine.SetRowModel(rows);
|
||||
}
|
||||
|
||||
gemmi::Model shaken(const gemmi::Model &model, double sigma, double b_sigma, unsigned seed) {
|
||||
std::mt19937 rng(seed);
|
||||
std::normal_distribution<double> d(0, sigma), db(0, b_sigma);
|
||||
gemmi::Model out = model;
|
||||
for (gemmi::Chain &ch : out.chains)
|
||||
for (gemmi::Residue &r : ch.residues)
|
||||
for (gemmi::Atom &a : r.atoms) {
|
||||
a.pos += gemmi::Position(d(rng), d(rng), d(rng));
|
||||
a.b_iso = static_cast<float>(std::max(5.0, a.b_iso + db(rng)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<gemmi::Atom *> atoms_of(gemmi::Model &model) {
|
||||
std::vector<gemmi::Atom *> out;
|
||||
for (gemmi::Chain &ch : model.chains)
|
||||
for (gemmi::Residue &r : ch.residues)
|
||||
for (gemmi::Atom &a : r.atoms)
|
||||
out.push_back(&a);
|
||||
return out;
|
||||
}
|
||||
|
||||
// The engine's gradient of a few atoms against central differences of its own target.
|
||||
void check_xray_gradient(RefinementEngine &engine, gemmi::Model model, double tolerance) {
|
||||
std::vector<std::array<double, 4>> grad;
|
||||
engine.Evaluate(model, &grad);
|
||||
std::vector<gemmi::Atom *> atoms = atoms_of(model);
|
||||
const double hx = 2e-3, hb = 2e-2;
|
||||
double max_g = 0;
|
||||
for (const auto &g : grad)
|
||||
for (int c = 0; c < 4; c++)
|
||||
max_g = std::max(max_g, std::fabs(g[c]));
|
||||
for (size_t i = 0; i < atoms.size(); i += atoms.size() / 7) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
const gemmi::Position p0 = atoms[i]->pos;
|
||||
const float b0 = atoms[i]->b_iso;
|
||||
const double h = c < 3 ? hx : hb;
|
||||
auto set = [&](double s) {
|
||||
atoms[i]->pos = p0;
|
||||
atoms[i]->b_iso = b0;
|
||||
if (c < 3)
|
||||
atoms[i]->pos.at(c) += s;
|
||||
else
|
||||
atoms[i]->b_iso = static_cast<float>(b0 + s);
|
||||
};
|
||||
set(h);
|
||||
const double fp = engine.Evaluate(model, nullptr);
|
||||
set(-h);
|
||||
const double fm = engine.Evaluate(model, nullptr);
|
||||
set(0);
|
||||
const double fd = (fp - fm) / (2 * h);
|
||||
CAPTURE(i, c, fd, grad[i][c], max_g);
|
||||
CHECK(std::fabs(fd - grad[i][c]) < tolerance * std::max(std::fabs(fd), 0.05 * max_g));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("RefinementLikelihood_derivative", "[Refinement]") {
|
||||
for (bool centric : {false, true})
|
||||
for (double fc : {0.3, 2.0, 9.0}) {
|
||||
double dfc = 0, dummy = 0;
|
||||
const double f0 = refine_math::AmplitudeNegLogLikelihood<double>(4.0, fc, 0.8, 2.5, centric, dfc);
|
||||
const double h = 1e-5;
|
||||
const double fp = refine_math::AmplitudeNegLogLikelihood<double>(4.0, fc + h, 0.8, 2.5, centric, dummy);
|
||||
const double fm = refine_math::AmplitudeNegLogLikelihood<double>(4.0, fc - h, 0.8, 2.5, centric, dummy);
|
||||
CAPTURE(centric, fc, f0);
|
||||
CHECK(dfc == Catch::Approx((fp - fm) / (2 * h)).epsilon(1e-5));
|
||||
}
|
||||
// the Bessel approximation on both sides of its switch
|
||||
double l0, r0, l1, r1;
|
||||
refine_math::BesselI0I1(3.75 * (1 - 1e-12), l0, r0);
|
||||
refine_math::BesselI0I1(3.75 * (1 + 1e-12), l1, r1);
|
||||
CHECK(l0 == Catch::Approx(l1).epsilon(1e-5));
|
||||
CHECK(r0 == Catch::Approx(r1).epsilon(1e-5));
|
||||
}
|
||||
|
||||
TEST_CASE("RefinementMonomerLibrary_embedded", "[Refinement]") {
|
||||
const gemmi::MonLib &monlib = EmbeddedMonomerLibrary();
|
||||
for (const char *code : {"ALA", "PRO", "MSE", "DA", "U", "ATP", "GOL", "SO4", "HOH", "ZN"})
|
||||
CHECK(InEmbeddedMonomerLibrary(code));
|
||||
CHECK_FALSE(InEmbeddedMonomerLibrary("XYZ"));
|
||||
for (const char *link : {"TRANS", "CIS", "PTRANS", "p", "SS"})
|
||||
CHECK(monlib.links.count(link) == 1);
|
||||
CHECK(monlib.modifications.count("NH3") == 1);
|
||||
CHECK(monlib.ener_lib.atoms.count("CH1") == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("RefinementRestraints_ideal_residues", "[Refinement]") {
|
||||
const gemmi::UnitCell cell(60, 65, 70, 90, 90, 90);
|
||||
gemmi::Structure st = residue_crystal({"PHE", "DA", "ATP", "HOH", "ZN"}, cell, "P 21 21 21", 3);
|
||||
const RefinementTopology topo = BuildRefinementTopology(st);
|
||||
CHECK(topo.restrained_residues == 5);
|
||||
CHECK(topo.fixed_residues == 0);
|
||||
CHECK(topo.bonds > 60);
|
||||
CHECK(topo.planes >= 3);
|
||||
CHECK(topo.chirals >= 4);
|
||||
std::vector<std::array<double, 4>> xyzb;
|
||||
for (gemmi::Atom *a : atoms_of(st.models[0]))
|
||||
xyzb.push_back({a->pos.x, a->pos.y, a->pos.z, a->b_iso});
|
||||
const GeometryStatistics g = MeasureGeometry(topo.geometry, {}, xyzb);
|
||||
CHECK(g.rmsd_bonds < 0.02);
|
||||
CHECK(g.rmsd_angles < 2.5);
|
||||
CHECK(g.rmsd_chirals < 0.2);
|
||||
}
|
||||
|
||||
TEST_CASE("RefinementRestraints_gradient", "[Refinement]") {
|
||||
const gemmi::UnitCell cell(14, 15, 16, 90, 90, 90);
|
||||
gemmi::Structure st = residue_crystal({"TYR", "DG"}, cell, "P 21 21 21", 5);
|
||||
st.models[0] = shaken(st.models[0], 0.15, 3.0, 2);
|
||||
const RefinementTopology topo = BuildRefinementTopology(st);
|
||||
// the geometry, the B similarity and the repulsions (symmetry mates included, at a contact distance that
|
||||
// makes every pair within reach count)
|
||||
RestraintSet set = topo.geometry;
|
||||
set.Append(topo.adp);
|
||||
RefinementTopology wide = topo;
|
||||
for (float &r : wide.vdw_radius)
|
||||
r = 2.5f;
|
||||
std::fill(wide.hbond.begin(), wide.hbond.end(), 0);
|
||||
const RestraintSet rep = BuildRepulsions(wide, st.models[0], cell, *st.find_spacegroup(), 0.0);
|
||||
REQUIRE(rep.records.size() > 10);
|
||||
bool image = false;
|
||||
for (const RestraintRecord &r : rep.records)
|
||||
image = image || r.rot[0] != 1.0f || r.tran[0] != 0.0f || r.tran[1] != 0.0f || r.tran[2] != 0.0f;
|
||||
CAPTURE(rep.records.size());
|
||||
CHECK(image);
|
||||
set.Append(rep);
|
||||
set.Finalize(atoms_of(st.models[0]).size());
|
||||
|
||||
std::vector<std::array<double, 4>> xyzb;
|
||||
for (gemmi::Atom *a : atoms_of(st.models[0]))
|
||||
xyzb.push_back({a->pos.x, a->pos.y, a->pos.z, a->b_iso});
|
||||
// each kind of restraint on its own, against central differences
|
||||
for (int kind = 0; kind <= static_cast<int>(RestraintKind::BSimilarity); kind++) {
|
||||
RestraintSet one;
|
||||
for (const RestraintRecord &r : set.records)
|
||||
if (r.kind == kind) {
|
||||
RestraintRecord copy = r;
|
||||
if (kind == static_cast<int>(RestraintKind::Plane)) {
|
||||
copy.atom[0] = static_cast<int>(one.plane_atoms.size());
|
||||
for (int k = 0; k < r.atom[1]; k++)
|
||||
one.plane_atoms.push_back(set.plane_atoms[r.atom[0] + k]);
|
||||
}
|
||||
one.Add(copy, kind == static_cast<int>(RestraintKind::Plane) ? r.atom[1] : 4);
|
||||
}
|
||||
CAPTURE(kind, one.records.size());
|
||||
std::vector<std::array<double, 4>> grad(xyzb.size(), {0, 0, 0, 0});
|
||||
EvaluateRestraints(one, xyzb, &grad);
|
||||
const double h = 1e-6;
|
||||
for (size_t i = 0; i < xyzb.size(); i++)
|
||||
for (int c = 0; c < 4; c++) {
|
||||
auto x = xyzb;
|
||||
x[i][c] += h;
|
||||
const double fp = EvaluateRestraints(one, x, nullptr);
|
||||
x[i][c] -= 2 * h;
|
||||
const double fm = EvaluateRestraints(one, x, nullptr);
|
||||
CAPTURE(i, c);
|
||||
CHECK(grad[i][c] == Catch::Approx((fp - fm) / (2 * h)).epsilon(1e-4).margin(1e-3));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
if (get_gpu_count() > 0) {
|
||||
// the GPU's float evaluation of the same set
|
||||
std::vector<MergedReflection> merged = asu_reflections(cell, *st.find_spacegroup(), 3.0);
|
||||
RefinementReflections refl = PrepareRefinementReflections(merged, cell, *st.find_spacegroup(), 3.0);
|
||||
auto gpu = MakeRefinementEngineGPU(0, cell, *st.find_spacegroup(), 3.0, refl, st.models[0], 100.0, SIZE_MAX);
|
||||
REQUIRE(gpu);
|
||||
gpu->SetRestraints(0, set);
|
||||
std::vector<std::array<double, 4>> ggpu;
|
||||
const double egpu = gpu->EvaluateRestraints(0, xyzb, &ggpu);
|
||||
std::vector<std::array<double, 4>> gcpu(xyzb.size(), {0, 0, 0, 0});
|
||||
const double ecpu = EvaluateRestraints(set, xyzb, &gcpu);
|
||||
CHECK(egpu == Catch::Approx(ecpu).epsilon(1e-4));
|
||||
for (size_t i = 0; i < xyzb.size(); i++)
|
||||
for (int c = 0; c < 4; c++)
|
||||
CHECK(ggpu[i][c] == Catch::Approx(gcpu[i][c]).epsilon(2e-3).margin(2e-2));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("RefinementXray_gradient_CPU", "[Refinement]") {
|
||||
for (const char *group : {"P 1", "P 21 21 21", "C 1 2 1", "P 61 2 2"}) {
|
||||
CAPTURE(group);
|
||||
const gemmi::SpaceGroup &sg = *gemmi::find_spacegroup_by_name(group);
|
||||
const gemmi::UnitCell cell = sg.crystal_system() == gemmi::CrystalSystem::Hexagonal
|
||||
? gemmi::UnitCell(30, 30, 45, 90, 90, 120)
|
||||
: gemmi::UnitCell(28, 31, 34, 90, sg.number == 5 ? 104 : 90, 90);
|
||||
gemmi::Structure truth = random_crystal(cell, group, 240, 7);
|
||||
gemmi::Model model = shaken(truth.models[0], 0.3, 4.0, 8);
|
||||
RefinementReflections refl = PrepareRefinementReflections(asu_reflections(cell, sg, 2.2), cell, sg, 2.2);
|
||||
REQUIRE(refl.Size() > 500);
|
||||
auto cpu = MakeRefinementEngineCPU(cell, sg, 2.2, refl, 4);
|
||||
cpu->SetBlur(RefinementBlur(cell, sg, 2.2, 5.0));
|
||||
set_observations(*cpu, refl, truth.models[0], model);
|
||||
check_xray_gradient(*cpu, model, 0.03);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
TEST_CASE("RefinementXray_gradient_GPU", "[Refinement]") {
|
||||
if (get_gpu_count() == 0)
|
||||
return;
|
||||
for (const char *group : {"P 1", "P 21 21 21", "C 1 2 1", "P 61 2 2"}) {
|
||||
CAPTURE(group);
|
||||
const gemmi::SpaceGroup &sg = *gemmi::find_spacegroup_by_name(group);
|
||||
const gemmi::UnitCell cell = sg.crystal_system() == gemmi::CrystalSystem::Hexagonal
|
||||
? gemmi::UnitCell(30, 30, 45, 90, 90, 120)
|
||||
: gemmi::UnitCell(28, 31, 34, 90, sg.number == 5 ? 104 : 90, 90);
|
||||
gemmi::Structure truth = random_crystal(cell, group, 240, 7);
|
||||
gemmi::Model model = shaken(truth.models[0], 0.3, 4.0, 8);
|
||||
RefinementReflections refl = PrepareRefinementReflections(asu_reflections(cell, sg, 2.2), cell, sg, 2.2);
|
||||
auto cpu = MakeRefinementEngineCPU(cell, sg, 2.2, refl, 4);
|
||||
cpu->SetBlur(RefinementBlur(cell, sg, 2.2, 5.0));
|
||||
set_observations(*cpu, refl, truth.models[0], model);
|
||||
// the GPU engine sees the same observations: it is made after they are set
|
||||
auto gpu = MakeRefinementEngineGPU(0, cell, sg, 2.2, refl, model, 300.0, SIZE_MAX);
|
||||
REQUIRE(gpu);
|
||||
gpu->SetBlur(RefinementBlur(cell, sg, 2.2, 5.0));
|
||||
std::vector<std::complex<float>> f;
|
||||
cpu->Fcalc(model, f);
|
||||
XrayRowModel rows;
|
||||
rows.k_total.assign(refl.Size(), 1.0f);
|
||||
rows.fmask.assign(refl.Size(), {0, 0});
|
||||
EstimateLikelihoodParameters(refl, f, rows);
|
||||
cpu->SetRowModel(rows);
|
||||
gpu->SetRowModel(rows);
|
||||
|
||||
std::vector<std::complex<float>> fg;
|
||||
gpu->Fcalc(model, fg);
|
||||
double num = 0, den = 0;
|
||||
for (size_t m = 0; m < f.size(); m++) {
|
||||
num += std::abs(f[m] - fg[m]);
|
||||
den += std::abs(f[m]);
|
||||
}
|
||||
CHECK(num / den < 1e-3);
|
||||
|
||||
std::vector<std::array<double, 4>> gc, gg;
|
||||
const double tc = cpu->Evaluate(model, &gc);
|
||||
const double tg = gpu->Evaluate(model, &gg);
|
||||
CHECK(tg == Catch::Approx(tc).epsilon(1e-4));
|
||||
double max_g = 0;
|
||||
for (const auto &g : gc)
|
||||
for (int c = 0; c < 4; c++)
|
||||
max_g = std::max(max_g, std::fabs(g[c]));
|
||||
for (size_t i = 0; i < gc.size(); i++)
|
||||
for (int c = 0; c < 4; c++) {
|
||||
CAPTURE(i, c);
|
||||
CHECK(std::fabs(gg[i][c] - gc[i][c]) < 0.01 * max_g);
|
||||
}
|
||||
check_xray_gradient(*gpu, model, 0.05);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("Refinement_restrained_recovers_model", "[Refinement]") {
|
||||
const gemmi::UnitCell cell(36, 40, 44, 90, 90, 90);
|
||||
const gemmi::SpaceGroup &sg = *gemmi::find_spacegroup_by_name("P 21 21 21");
|
||||
gemmi::Structure truth = residue_crystal({"PHE", "TRP", "DA", "DG", "TYR", "HOH", "HOH", "HOH"}, cell,
|
||||
"P 21 21 21", 11);
|
||||
gemmi::Structure start = truth;
|
||||
start.models[0] = shaken(truth.models[0], 0.25, 5.0, 12);
|
||||
std::vector<MergedReflection> merged = asu_reflections(cell, sg, 1.8);
|
||||
{
|
||||
RefinementReflections refl = PrepareRefinementReflections(merged, cell, sg, 1.8 * 0.999);
|
||||
auto cpu = MakeRefinementEngineCPU(cell, sg, 1.8 * 0.999, refl, 4);
|
||||
cpu->SetBlur(RefinementBlur(cell, sg, 1.8, 5.0));
|
||||
std::vector<std::complex<float>> f;
|
||||
cpu->Fcalc(truth.models[0], f);
|
||||
std::map<gemmi::Miller, double> amplitude;
|
||||
for (size_t m = 0; m < refl.Size(); m++)
|
||||
amplitude[refl.hkl[m]] = std::abs(f[m]);
|
||||
std::mt19937 rng(3);
|
||||
std::normal_distribution<double> noise(0.0, 0.03);
|
||||
for (MergedReflection &r : merged) {
|
||||
const auto it = amplitude.find(gemmi::Miller{{r.h, r.k, r.l}});
|
||||
r.F = it == amplitude.end() ? NAN : static_cast<float>(it->second * (1 + noise(rng)));
|
||||
r.sigmaF = 0.03f * r.F + 0.01f;
|
||||
}
|
||||
}
|
||||
Logger logger("RefinementTest");
|
||||
for (bool gpu : {false, true}) {
|
||||
if (gpu && get_gpu_count() == 0)
|
||||
continue;
|
||||
CAPTURE(gpu);
|
||||
RefinementSettings settings;
|
||||
settings.gpu = gpu;
|
||||
settings.nthreads = 4;
|
||||
settings.macro_cycles = 3;
|
||||
settings.weight_ladder = {0.0625, 0.25};
|
||||
settings.adp_ladder = {4.0};
|
||||
settings.riding_hydrogens = false; // the data were made without them
|
||||
const RefinementResult r = RefineModel(start, merged, cell, sg, 1.0, logger, settings);
|
||||
REQUIRE(r.ok);
|
||||
CHECK(r.on_gpu == gpu);
|
||||
CHECK(r.restrained_residues == 8);
|
||||
CHECK(r.r_free < 0.6 * r.r_free_start);
|
||||
CHECK(r.geometry.rmsd_bonds < r.geometry_start.rmsd_bonds);
|
||||
double rms = 0;
|
||||
std::vector<gemmi::Atom *> refined = atoms_of(r.model->models[0]);
|
||||
std::erase_if(refined, [](const gemmi::Atom *a) { return a->is_hydrogen(); }); // the riding hydrogens
|
||||
const std::vector<gemmi::Atom *> ref = atoms_of(truth.models[0]);
|
||||
REQUIRE(refined.size() == ref.size());
|
||||
for (size_t i = 0; i < ref.size(); i++)
|
||||
rms += refined[i]->pos.dist_sq(ref[i]->pos);
|
||||
rms = std::sqrt(rms / ref.size());
|
||||
CHECK(rms < 0.15);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ TARGET_LINK_LIBRARIES(jfjoch_extract_hkl JFJochReader)
|
||||
|
||||
# rugnux, the single offline analysis CLI, lives in rugnux/ next to its library.
|
||||
|
||||
# Developer utility, not installed: refines a model against a finished run's merged MTZ (Refinement.h), for
|
||||
# measuring the refinement on battery outputs without processing anything again.
|
||||
ADD_EXECUTABLE(rugnux_refine_test rugnux_refine_test.cpp)
|
||||
TARGET_LINK_LIBRARIES(rugnux_refine_test JFJochStructureRefinement JFJochCommon gemmi)
|
||||
|
||||
# In-place re-compress /entry/data/data of a _data file from bitshuffle/LZ4 to bitshuffle/zstd.
|
||||
ADD_EXECUTABLE(jfjoch_recompress jfjoch_recompress.cpp)
|
||||
TARGET_LINK_LIBRARIES(jfjoch_recompress JFJochHDF5Wrappers)
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in new issue
Block a user