Move the atomic-model code to image_analysis/structure_refinement/ and WriteModel to writer/
A pure move. ModelValidation, RigidBodyRefine, RigidBodyGPU, ModelFFT, ModelGrid, ModelScaling, ModelMaskGPU, ModelScaleGPU and SigmaA - everything that works on an atomic model - become the JFJochStructureRefinement library, linked by JFJochImageAnalysis. WriteModel (the placed-model mmCIF/PDB writer) goes to writer/ as its own small JFJochModelWriter target, so JFJochWriter, which a writer-only build compiles, does not gain a gemmi dependency. Only include paths and CMake lists change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "ModelGrid.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include "gemmi/solmask.hpp" // SolventMasker
|
||||
|
||||
#include "../../common/ParallelFor.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using Table = gemmi::IT92<float>;
|
||||
|
||||
// The box of grid points gemmi's Grid::use_points_in_box<true>() walks around one atom.
|
||||
struct AtomBox {
|
||||
gemmi::Fractional fpos;
|
||||
int du = 0, dv = 0, dw = 0;
|
||||
double radius = 0;
|
||||
};
|
||||
|
||||
// The box of use_points_around<true>(fpos, radius, ..., false), sized and clamped as gemmi does it.
|
||||
AtomBox MakeAtomBox(const gemmi::Grid<float> &grid, const gemmi::Fractional &fpos, double radius) {
|
||||
AtomBox box;
|
||||
box.fpos = fpos;
|
||||
box.radius = radius;
|
||||
box.du = std::min(static_cast<int>(std::ceil(radius / grid.spacing[0])), grid.nu - 1);
|
||||
box.dv = std::min(static_cast<int>(std::ceil(radius / grid.spacing[1])), grid.nv - 1);
|
||||
box.dw = std::min(static_cast<int>(std::ceil(radius / grid.spacing[2])), grid.nw - 1);
|
||||
return box;
|
||||
}
|
||||
|
||||
// gemmi's use_points_in_box<true>() run over a list of atoms (grid.hpp, do_use_points_in_box), in
|
||||
// parallel over the grid's w planes: a plane is one task, and it walks every atom whose box reaches
|
||||
// it, in list order, visiting only that plane's points. Every point is so handed exactly the calls
|
||||
// gemmi's serial loop over the atoms hands it, in the same order, and the grid comes out bit for bit
|
||||
// the same whatever the thread count. func_for(i) gives atom i's func(point, d2, delta), taken as a
|
||||
// local copy so that the compiler can hold it in registers while the atom's points are written.
|
||||
template <typename FuncFor>
|
||||
void UseAtomBoxesByPlane(gemmi::Grid<float> &grid, const std::vector<AtomBox> &boxes, size_t nthreads,
|
||||
FuncFor func_for) {
|
||||
const int nu = grid.nu, nv = grid.nv, nw = grid.nw;
|
||||
std::vector<std::vector<int>> plane_atoms(nw);
|
||||
for (int i = 0; i < static_cast<int>(boxes.size()); i++) {
|
||||
const int w0 = gemmi::iround(boxes[i].fpos.z * nw);
|
||||
for (int w = w0 - boxes[i].dw; w <= w0 + boxes[i].dw; w++) {
|
||||
std::vector<int> &atoms = plane_atoms[gemmi::modulo(w, nw)];
|
||||
if (atoms.empty() || atoms.back() != i)
|
||||
atoms.push_back(i);
|
||||
}
|
||||
}
|
||||
ParallelFor(nw, nthreads, [&](int plane) {
|
||||
for (int i : plane_atoms[plane]) {
|
||||
const AtomBox &b = boxes[i];
|
||||
const auto func = func_for(i);
|
||||
const double max_dist_sq = b.radius * b.radius;
|
||||
const gemmi::Fractional nctr(b.fpos.x * nu, b.fpos.y * nv, b.fpos.z * nw);
|
||||
const int u_lo = gemmi::iround(nctr.x) - b.du, u_hi = gemmi::iround(nctr.x) + b.du;
|
||||
const int v_lo = gemmi::iround(nctr.y) - b.dv, v_hi = gemmi::iround(nctr.y) + b.dv;
|
||||
const int w_lo = gemmi::iround(nctr.z) - b.dw, w_hi = gemmi::iround(nctr.z) + b.dw;
|
||||
const int u_0 = gemmi::modulo(u_lo, nu), v_0 = gemmi::modulo(v_lo, nv);
|
||||
gemmi::Fractional fdelta(nctr.x - u_lo, 0, 0);
|
||||
// The box's w that fall on this plane, ascending as gemmi walks them: one, unless the box is
|
||||
// wider than the cell.
|
||||
for (int w = w_lo + gemmi::modulo(plane - w_lo, nw); w <= w_hi; w += nw) {
|
||||
fdelta.z = nctr.z - w;
|
||||
for (int v = v_lo, v_ = v_0; v <= v_hi; ++v, v_ = (v_ + 1 == nv ? 0 : v_ + 1)) {
|
||||
fdelta.y = nctr.y - v;
|
||||
gemmi::Position delta(grid.orth_n.multiply(fdelta));
|
||||
const double dist_sq0 = gemmi::sq(delta.y) + gemmi::sq(delta.z);
|
||||
if (dist_sq0 > max_dist_sq)
|
||||
continue;
|
||||
float *t = &grid.data[grid.index_q(u_0, v_, plane)];
|
||||
for (int u = u_lo, u_ = u_0;;) {
|
||||
const double dist_sq = dist_sq0 + gemmi::sq(delta.x);
|
||||
if (!(dist_sq > max_dist_sq))
|
||||
func(*t, dist_sq, delta);
|
||||
if (u >= u_hi)
|
||||
break;
|
||||
++u;
|
||||
++u_;
|
||||
++t;
|
||||
if (u_ == nu) {
|
||||
u_ = 0;
|
||||
t -= nu;
|
||||
}
|
||||
delta.x -= grid.orth_n.a11;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// gemmi's Grid::symmetrize() in parallel over the orbits found by OrbitLeaders(): each orbit is reduced
|
||||
// by one thread, into its leader, with gemmi's operands in gemmi's order, and the value written to its
|
||||
// leader and mates as gemmi writes it. Orbits share no points, so the grid is gemmi's, bit for bit.
|
||||
template <typename Func>
|
||||
void SymmetrizeOrbits(gemmi::Grid<float> &grid, const std::vector<size_t> &leaders, size_t nthreads,
|
||||
Func func) {
|
||||
const std::vector<gemmi::GridOp> ops = grid.get_scaled_ops_except_id();
|
||||
const size_t nu = grid.nu, nv = grid.nv;
|
||||
ParallelChunks(static_cast<int>(leaders.size()), nthreads, [&](int lo, int hi) {
|
||||
std::vector<size_t> mates(ops.size());
|
||||
for (int i = lo; i < hi; i++) {
|
||||
const size_t idx = leaders[i];
|
||||
const int u = static_cast<int>(idx % nu), v = static_cast<int>(idx / nu % nv),
|
||||
w = static_cast<int>(idx / (nu * nv));
|
||||
for (size_t k = 0; k < ops.size(); ++k) {
|
||||
const std::array<int, 3> t = ops[k].apply(u, v, w);
|
||||
mates[k] = grid.index_n(t[0], t[1], t[2]);
|
||||
}
|
||||
float value = grid.data[idx];
|
||||
for (size_t k : mates)
|
||||
value = func(value, grid.data[k]);
|
||||
grid.data[idx] = value;
|
||||
for (size_t k : mates)
|
||||
grid.data[k] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<const gemmi::Atom *> ModelAtoms(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;
|
||||
}
|
||||
|
||||
// One atom's density, added to a grid point as gemmi's do_add_atom_density_to_grid() adds it.
|
||||
struct AtomDensity {
|
||||
using CReal = Table::Coef::coef_type;
|
||||
using IsoSum = decltype(std::declval<const Table::Coef &>().precalculate_density_iso(CReal(), CReal()));
|
||||
using AnisoSum = decltype(std::declval<const Table::Coef &>().precalculate_density_aniso_b(
|
||||
gemmi::SMat33<CReal>(), CReal()));
|
||||
|
||||
bool is_aniso = false;
|
||||
float occ = 0;
|
||||
IsoSum iso{};
|
||||
AnisoSum aniso{};
|
||||
|
||||
void operator()(float &point, double r2, const gemmi::Position &delta) const {
|
||||
if (!is_aniso)
|
||||
point += float(occ * iso.calculate((CReal)r2));
|
||||
else
|
||||
point += float(occ * aniso.calculate(delta));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The points gemmi's Grid::symmetrize() (grid.hpp) reduces the grid's orbits into, ascending. gemmi
|
||||
// walks the points by index and takes the orbit of every point it has not visited yet, so it reduces
|
||||
// each orbit into the orbit's lowest index - the point none of whose mates has a lower one. They depend
|
||||
// on the grid's size and space group only, so a zone finds them once for all its evaluations.
|
||||
std::vector<size_t> OrbitLeaders(const gemmi::Grid<float> &grid, size_t nthreads) {
|
||||
const std::vector<gemmi::GridOp> ops = grid.get_scaled_ops_except_id();
|
||||
if (ops.empty())
|
||||
return {}; // P1: nothing to symmetrize
|
||||
std::vector<std::vector<size_t>> plane_leaders(grid.nw);
|
||||
ParallelFor(grid.nw, nthreads, [&](int w) {
|
||||
for (int v = 0; v < grid.nv; ++v)
|
||||
for (int u = 0; u < grid.nu; ++u) {
|
||||
const size_t idx = grid.index_q(u, v, w);
|
||||
bool lowest = true;
|
||||
for (size_t k = 0; k < ops.size() && lowest; ++k) {
|
||||
const std::array<int, 3> t = ops[k].apply(u, v, w);
|
||||
lowest = grid.index_n(t[0], t[1], t[2]) >= idx;
|
||||
}
|
||||
if (lowest)
|
||||
plane_leaders[w].push_back(idx);
|
||||
}
|
||||
});
|
||||
std::vector<size_t> leaders;
|
||||
for (const std::vector<size_t> &l : plane_leaders)
|
||||
leaders.insert(leaders.end(), l.begin(), l.end());
|
||||
return leaders;
|
||||
}
|
||||
|
||||
// DensityCalculator::put_model_density_on_grid() (gemmi dencalc.hpp, do_add_atom_density_to_grid)
|
||||
// with the atoms and the symmetry spread over the threads as above - the same grid, bit for bit. Each
|
||||
// atom's density coefficients and radius are worked out once, up front, rather than once per plane.
|
||||
void PutModelDensityOnGrid(gemmi::DensityCalculator<Table, float> &dc, const gemmi::Model &model,
|
||||
const std::vector<size_t> &orbit_leaders, size_t nthreads) {
|
||||
using CReal = AtomDensity::CReal;
|
||||
dc.initialize_grid();
|
||||
const std::vector<const gemmi::Atom *> atoms = ModelAtoms(model);
|
||||
const int n = static_cast<int>(atoms.size());
|
||||
std::vector<AtomBox> boxes(n);
|
||||
std::vector<AtomDensity> density(n);
|
||||
ParallelChunks(n, nthreads, [&](int lo, int hi) {
|
||||
for (int i = lo; i < hi; i++) {
|
||||
const gemmi::Atom &atom = *atoms[i];
|
||||
const auto &coef = Table::get(atom.element, atom.charge, atom.serial);
|
||||
const float addend = dc.addends.get(atom.element);
|
||||
const gemmi::Fractional fpos = dc.grid.unit_cell.fractionalize(atom.pos);
|
||||
AtomDensity &d = density[i];
|
||||
d.occ = atom.occ;
|
||||
d.is_aniso = atom.aniso.nonzero();
|
||||
if (!d.is_aniso) {
|
||||
const CReal b = static_cast<CReal>(atom.b_iso + dc.blur);
|
||||
d.iso = coef.precalculate_density_iso(b, addend);
|
||||
boxes[i] = MakeAtomBox(dc.grid, fpos, dc.estimate_radius(d.iso, b));
|
||||
} else {
|
||||
const auto aniso_b = atom.aniso.scaled(CReal(gemmi::u_to_b())).added_kI(CReal(dc.blur));
|
||||
const CReal b_max = std::max(std::max(aniso_b.u11, aniso_b.u22), aniso_b.u33);
|
||||
const double radius = dc.estimate_radius(coef.precalculate_density_iso(b_max, addend), b_max);
|
||||
d.aniso = coef.precalculate_density_aniso_b(aniso_b, addend);
|
||||
boxes[i] = MakeAtomBox(dc.grid, fpos, radius);
|
||||
}
|
||||
}
|
||||
});
|
||||
UseAtomBoxesByPlane(dc.grid, boxes, nthreads, [&](int i) { return density[i]; });
|
||||
SymmetrizeOrbits(dc.grid, orbit_leaders, nthreads, [](float a, float b) { return a + b; });
|
||||
}
|
||||
|
||||
// SolventMasker(AtomicRadiiSet::Refmac).put_mask_on_grid() (gemmi solmask.hpp) with the atoms and the
|
||||
// symmetry spread over the threads as above - the same mask, bit for bit. The island removal and the
|
||||
// shrink that follow are gemmi's own, except that the shrink is skipped where it cannot change a point:
|
||||
// it looks at the grid offsets within rshrink of each point (set_margin_around), and on a grid whose
|
||||
// spacing is coarser than rshrink along all three axes there are none - which is every rigid-body zone -
|
||||
// yet it still walks the whole grid.
|
||||
void PutMaskOnGrid(gemmi::Grid<float> &grid, const gemmi::Model &model, const std::vector<size_t> &orbit_leaders,
|
||||
size_t nthreads) {
|
||||
const gemmi::SolventMasker masker(gemmi::AtomicRadiiSet::Refmac);
|
||||
masker.clear(grid);
|
||||
std::vector<AtomBox> boxes;
|
||||
for (const gemmi::Atom *atom : ModelAtoms(model)) {
|
||||
if ((masker.ignore_hydrogen && atom->is_hydrogen()) ||
|
||||
(masker.ignore_zero_occupancy_atoms && atom->occ <= 0))
|
||||
continue;
|
||||
double r = masker.constant_r + masker.rprobe;
|
||||
r += gemmi::refmac_radius_for_bulk_solvent(atom->element.elem);
|
||||
boxes.push_back(MakeAtomBox(grid, grid.unit_cell.fractionalize(atom->pos), r));
|
||||
}
|
||||
UseAtomBoxesByPlane(grid, boxes, nthreads, [](int) {
|
||||
return [](float &point, double, const gemmi::Position &) { point = 0.f; };
|
||||
});
|
||||
SymmetrizeOrbits(grid, orbit_leaders, nthreads, [](float a, float b) { return a < b ? a : b; });
|
||||
masker.remove_islands(grid);
|
||||
bool shrink_has_offsets = false;
|
||||
for (int i = 0; i < 3; i++)
|
||||
shrink_has_offsets = shrink_has_offsets || static_cast<int>(std::floor(masker.rshrink / grid.spacing[i])) > 0;
|
||||
if (shrink_has_offsets)
|
||||
masker.shrink(grid);
|
||||
}
|
||||
Reference in New Issue
Block a user