Merge ribo-gap into refine-merge: the bulk-solvent mask grid never coarser than 0.75 A, in refinement and validation

Resolved: the ModelGrid.h include in ModelStructureFactorsGPU.cpp (AnisoRadiusB, SolventMaskGrid).
ModelReferenceIntensities keeps its mask on the density's grid: it is a reference at fixed solvent
constants, not a scored mask, and RigidBodyGPU_MatchesCPU and ModelScaleGPU_MatchesGemmiOnAModelsPoints
make their amplitudes from it - with the finer mask those amplitudes moved enough to land the anisotropic
test cluster on a near-tie of gemmi's scale fit (residuals 2.8e-4 against 2e-4, b_star 9.5e-7 against
1.6e-7). ModelStructureFactorsGPU_MatchesCPU at 1.5 A in F 41 3 2 (F_calc 1.28e-4 of the mean |F|
against 1e-4) fails on 89b37f9bc already and is left as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQqN9qRD57vnShzTC37
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-28 01:14:53 +01:00
commit 0486575dec
12 files changed
+164 -78

No files matched your search

+1 -1
View File
@@ -132,7 +132,7 @@ The model electron density is sampled on a grid (IT92 X-ray form factors, with a
### 14.2 Bulk solvent and scaling
A flat bulk-solvent mask around the model is transformed to $F_\mathrm{mask}$, and the model is scaled to the observed amplitudes by an overall least-squares fit of a scale $k$, an anisotropic $B$, and the flat-solvent parameters $k_\mathrm{sol}, B_\mathrm{sol}$:
A flat bulk-solvent mask around the model (the REFMAC radii, on the density's grid, or on one of its own at 0.75 Å where that grid is too coarse for the mask's 0.8 Å shrink to act) is transformed to $F_\mathrm{mask}$, and the model is scaled to the observed amplitudes by an overall least-squares fit of a scale $k$, an anisotropic $B$, and the flat-solvent parameters $k_\mathrm{sol}, B_\mathrm{sol}$:
$
F_\mathrm{model} = k\,e^{-\mathbf{h}^\top \mathbf{B}\,\mathbf{h}/4}\left(F_\mathrm{calc} + k_\mathrm{sol}\,e^{-B_\mathrm{sol}\,s^2}\,F_\mathrm{mask}\right),\quad s^2 = 1/4d^2.
+2 -2
View File
@@ -93,8 +93,8 @@ gradient gather, whatever the number of atoms. The derivative of a riding hydrog
**Scale and solvent.** The flat bulk-solvent mask is drawn with the REFMAC radii (probe 1.0 Å, shrink 0.8 Å)
on a grid of its own, with a spacing of $d_\mathrm{min}/3$ or 0.4 Å, whichever is finer (coarser only where
a cell would need more than $2^{26}$ points, and then never coarser than the density grid, which a cell that
large uses for its mask). On the atoms' density grid the shrink is lost at low
a cell would need more than $2^{26}$ points, and then never coarser than 0.75 Å nor than the density grid,
which a cell that large uses for its mask). On the atoms' density grid the shrink is lost at low
resolution: at 3 Å its 1 Å step leaves no grid point within the shrink radius. $k_\mathrm{sol}$ and
$B_\mathrm{sol}$ are searched over their physical range on the working reflections, and the overall scale
and anisotropic B are fitted at each point of that search (§14.2) - at the points of its fine pass by
+4 -2
View File
@@ -166,7 +166,8 @@ the first card has no room for go on the others, each against its own budget.
as a gather over bricks of the grid, bit-identical from run to run.
[§14.1](CPU_DATA_ANALYSIS_DECISIONS.md#141-model-structure-factors)
- **Bulk-solvent mask** (`ModelMaskGPU.cu`) - the solvent mask from every symmetry image of every
atom, with island removal and the shrink, as gemmi computes it.
atom, with island removal and the shrink, as gemmi computes it - on the density's grid, or on one of its
own at 0.75 A where the density's is too coarse for the 0.8 A shrink to act.
[§14.2](CPU_DATA_ANALYSIS_DECISIONS.md#142-bulk-solvent-and-scaling)
- **Structure factors and maps** (`ModelStructureFactorsGPU.cu`) - F_calc and F_mask from the two grids
by cuFFT, composed over the group's operators and read off at the data's reflections; and the
@@ -269,7 +270,8 @@ and both gradients come back into pinned buffers. The minimiser (L-BFGS) stays o
Once per macro-cycle ([§15.3](CPU_DATA_ANALYSIS_REFINEMENT.md#153-scale-bulk-solvent-and-the-likelihood-parameters)):
- **F_calc and F_mask** - the forward path above without the target, and the bulk-solvent mask drawn on the card
(`ModelMaskGPU.cu`) on **its own fine grid** (d_min/3 or 0.4 A, whichever is finer, at most 2^26 points but never coarser than the density's grid),
(`ModelMaskGPU.cu`) on **its own fine grid** (d_min/3 or 0.4 A, whichever is finer, at most 2^26 points but never coarser than 0.75 A nor than the
density's grid),
with a grid, spectrum and cuFFT plan of its own where that grid is not the density's: one block per
(atom, symmetry image) sets the points within the atom's REFMAC radius, the islands are removed by a
union-find labelling (integer `atomicCAS`, rooted at each component's lowest index, so the partition does not
@@ -501,6 +501,23 @@ void PutMaskOnGrid(gemmi::Grid<float> &grid, const gemmi::Model &model, const st
masker.shrink(grid);
}
gemmi::Grid<float> SolventMaskGrid(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min,
double spacing) {
spacing = std::min({spacing, MASK_SPACING_SHRINK, d_min / 3.0});
gemmi::Grid<float> grid;
grid.unit_cell = cell;
grid.spacegroup = &sg;
// set_size_from_spacing()'s size, without allocating the values
const std::array<int, 3> n = gemmi::good_grid_size({{cell.a / spacing, cell.b / spacing, cell.c / spacing}},
gemmi::GridSizeRounding::Up, &sg);
grid.nu = n[0];
grid.nv = n[1];
grid.nw = n[2];
grid.calculate_spacing();
grid.axis_order = gemmi::AxisOrder::XYZ;
return grid;
}
size_t ModelGridPoints(const gemmi::UnitCell &cell, const gemmi::SpaceGroup *sg, double d_min, double rate) {
const double spacing = d_min / (2 * rate);
const std::array<int, 3> m = gemmi::good_grid_size({{cell.a / spacing, cell.b / spacing, cell.c / spacing}},
@@ -61,6 +61,17 @@ void PutModelDensityOnGrid(gemmi::DensityCalculator<gemmi::IT92<float>, float> &
void PutMaskOnGrid(gemmi::Grid<float> &grid, const gemmi::Model &model, const std::vector<size_t> &orbit_leaders,
size_t nthreads);
// The step a bulk-solvent mask's grid is held below. The mask's shrink (0.8 A, the Refmac radii) reaches no grid point
// on a coarser grid and is skipped, and the macromolecule keeps the whole 1.0 A probe: some 5-10% of the cell's solvent
// lost, 0.015-0.03 of R-free on deposited models. Below the shrink radius the R-factors no longer depend on the step.
constexpr double MASK_SPACING_SHRINK = 0.75; // A
// The grid of a bulk-solvent mask for structure factors to d_min (sizes, cell and group set, no values): at `spacing`,
// but never coarser than MASK_SPACING_SHRINK nor than the density's own grid at rate 1.5 (d_min / 3), which it then
// is - set_size_from_spacing()'s size for d_min / 3.
gemmi::Grid<float> SolventMaskGrid(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min,
double spacing);
// The number of points of the grid a model's structure factors to d_min are computed on at DensityCalculator's
// `rate` (a spacing of d_min / (2 rate)) in this cell and space group: the grid's size, without making it.
size_t ModelGridPoints(const gemmi::UnitCell &cell, const gemmi::SpaceGroup *sg, double d_min, double rate = 1.5);
@@ -13,7 +13,7 @@
#include "gemmi/fourier.hpp" // get_size_for_hkl, get_f_phi_on_grid
#include "gemmi/solmask.hpp" // SolventMasker, refmac_radius_for_bulk_solvent
#include "ModelGrid.h" // AnisoRadiusB
#include "ModelGrid.h" // AnisoRadiusB, SolventMaskGrid
#include "RigidBodyRefine.h" // RigidBodyZoneGrid
namespace {
@@ -123,6 +123,11 @@ ModelStructureFactorsGPU::ModelStructureFactorsGPU(int device, const gemmi::Mode
setup_.grid.frac[3 * i + j] = cell.frac.mat[i][j];
}
setup_.volume = cell.volume;
const gemmi::Grid<float> mask_grid = SolventMaskGrid(cell, sg, d_min, d_min / 3.0);
setup_.mask_grid = setup_.grid;
setup_.mask_grid.nu = mask_grid.nu;
setup_.mask_grid.nv = mask_grid.nv;
setup_.mask_grid.nw = mask_grid.nw;
const gemmi::GroupOps gops = sg.operations();
setup_.den = gemmi::Op::DEN;
@@ -177,7 +182,7 @@ ModelStructureFactorsGPU::ModelStructureFactorsGPU(int device, const gemmi::Mode
const int map_nw = 2 * (map_size_[2] / 2);
setup_.map_points = static_cast<size_t>(map_size_[0]) * map_size_[1] * map_nw;
setup_.map_complex_points = static_cast<size_t>(map_size_[0]) * map_size_[1] * (map_nw / 2 + 1);
fft_work_bytes_ = ModelStructureFactorsGPUEngine::FFTWorkBytes(device_, setup_.grid, map_size_[0], map_size_[1], map_nw);
fft_work_bytes_ = ModelStructureFactorsGPUEngine::FFTWorkBytes(device_, setup_, map_size_[0], map_size_[1], map_nw);
device_bytes_ = ModelStructureFactorsGPUEngine::DeviceBytes(setup_, fft_work_bytes_);
}
@@ -151,6 +151,10 @@ size_t ComplexPoints(const ModelDensityGrid &g) {
return static_cast<size_t>(g.nu / 2 + 1) * g.nv * g.nw;
}
bool SameGrid(const ModelDensityGrid &a, const ModelDensityGrid &b) {
return a.nu == b.nu && a.nv == b.nv && a.nw == b.nw;
}
} // namespace
struct ModelStructureFactorsGPUEngineImpl {
@@ -161,7 +165,7 @@ struct ModelStructureFactorsGPUEngineImpl {
CudaDevicePtr<float4> pos;
CudaDevicePtr<ModelMaskAtom> mask_atoms;
CudaDevicePtr<float> grid; // the density, then the mask; a map's real grid
CudaDevicePtr<float> grid; // the density, then the mask (on a grid of its own size or not); a map's real grid
CudaDevicePtr<float2> spectrum; // their transforms; a map's coefficients, transformed in place
CudaDevicePtr<char> fft_work;
CudaDevicePtr<int> ops, row_hkl;
@@ -169,7 +173,7 @@ struct ModelStructureFactorsGPUEngineImpl {
CudaDevicePtr<float2> f, fmask;
std::unique_ptr<ModelDensityGPU> density;
std::unique_ptr<ModelMaskGPU> mask;
CudaFFTPlan forward;
CudaFFTPlan forward, mask_forward; // the mask's only where its grid is not the density's
// On `dev`, which the caller has made current.
ModelStructureFactorsGPUEngineImpl(int dev, const ModelStructureFactorsGPUSetup &s, size_t work)
@@ -178,8 +182,9 @@ struct ModelStructureFactorsGPUEngineImpl {
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);
mask_atoms = CudaDevicePtr<ModelMaskAtom>(na, sync);
grid = CudaDevicePtr<float>(std::max(GridPoints(setup.grid), setup.map_points), sync);
spectrum = CudaDevicePtr<float2>(std::max(ComplexPoints(setup.grid), setup.map_complex_points), sync);
grid = CudaDevicePtr<float>(std::max({GridPoints(setup.grid), GridPoints(setup.mask_grid), setup.map_points}), sync);
spectrum = CudaDevicePtr<float2>(
std::max({ComplexPoints(setup.grid), ComplexPoints(setup.mask_grid), setup.map_complex_points}), 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);
@@ -188,13 +193,13 @@ struct ModelStructureFactorsGPUEngineImpl {
fmask = CudaDevicePtr<float2>(nr, 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));
mask = std::make_unique<ModelMaskGPU>(stream, GridPoints(setup.mask_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.nu = setup.mask_grid.nu;
mask_grid.nv = setup.mask_grid.nv;
mask_grid.nw = setup.mask_grid.nw;
std::copy(setup.mask_grid.orth, setup.mask_grid.orth + 9, mask_grid.orth);
mask_grid.volume = setup.volume;
mask->SetGrid(mask_grid, setup.mask_ops);
@@ -207,7 +212,10 @@ struct ModelStructureFactorsGPUEngineImpl {
up(row_scale, setup.row_scale.data(), setup.row_scale.size() * sizeof(double));
forward = CudaFFTPlan(ForwardShape(setup.grid), stream, fft_work);
if (forward.WorkBytes() > work_bytes)
if (!SameGrid(setup.mask_grid, setup.grid))
mask_forward = CudaFFTPlan(ForwardShape(setup.mask_grid), stream, fft_work);
if (forward.WorkBytes() > work_bytes ||
(!SameGrid(setup.mask_grid, setup.grid) && mask_forward.WorkBytes() > work_bytes))
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, "structure factors: cuFFT work area over the reserve");
cuda_err(cudaStreamSynchronize(stream));
}
@@ -216,8 +224,8 @@ struct ModelStructureFactorsGPUEngineImpl {
cudaStreamSynchronize(stream);
}
float Norm() const {
return static_cast<float>(setup.volume / static_cast<double>(GridPoints(setup.grid)));
float Norm(const ModelDensityGrid &g) const {
return static_cast<float>(setup.volume / static_cast<double>(GridPoints(g)));
}
};
@@ -228,22 +236,25 @@ size_t ModelStructureFactorsGPUEngine::TotalMemory(int device) {
return total;
}
size_t ModelStructureFactorsGPUEngine::FFTWorkBytes(int device, const ModelDensityGrid &grid, int map_nu, int map_nv,
int map_nw) {
size_t ModelStructureFactorsGPUEngine::FFTWorkBytes(int device, const ModelStructureFactorsGPUSetup &setup, int map_nu,
int map_nv, int map_nw) {
DeviceGuard guard(device);
return std::max(CudaFFTPlan::WorkBytes(ForwardShape(grid)), CudaFFTPlan::WorkBytes(MapShape(map_nu, map_nv, map_nw)));
size_t bytes = std::max(CudaFFTPlan::WorkBytes(ForwardShape(setup.grid)),
CudaFFTPlan::WorkBytes(MapShape(map_nu, map_nv, map_nw)));
if (!SameGrid(setup.mask_grid, setup.grid))
bytes = std::max(bytes, CudaFFTPlan::WorkBytes(ForwardShape(setup.mask_grid)));
return bytes;
}
size_t ModelStructureFactorsGPUEngine::DeviceBytes(const ModelStructureFactorsGPUSetup &s, size_t fft_work_bytes) {
const size_t na = s.max_atoms + 1, nr = s.rows.size() + 1;
const size_t points = GridPoints(s.grid);
size_t bytes = na * (sizeof(float4) + sizeof(ModelMaskAtom));
bytes += std::max(points, s.map_points) * sizeof(float);
bytes += std::max(ComplexPoints(s.grid), s.map_complex_points) * sizeof(float2);
bytes += std::max({GridPoints(s.grid), GridPoints(s.mask_grid), s.map_points}) * sizeof(float);
bytes += std::max({ComplexPoints(s.grid), ComplexPoints(s.mask_grid), s.map_complex_points}) * sizeof(float2);
bytes += fft_work_bytes;
bytes += 12 * s.sym_ops.size() * sizeof(int) + nr * (3 * sizeof(int) + sizeof(double) + 2 * sizeof(float2));
bytes += ModelDensityGPU::DeviceBytes(s.max_atoms, s.max_pairs, ModelDensityGPU::Bricks(s.grid.nu, s.grid.nv, s.grid.nw));
bytes += ModelMaskGPU::DeviceBytes(points);
bytes += ModelMaskGPU::DeviceBytes(GridPoints(s.mask_grid));
return bytes;
}
@@ -282,17 +293,19 @@ void ModelStructureFactorsGPUEngine::Compute(const std::vector<ModelDensityAtom>
e.density->Compute(e.pos, e.grid);
cufft_err(e.forward.R2C(e.grid, reinterpret_cast<cufftComplex *>(e.spectrum.get())));
if (rows > 0) {
compose_kernel<<<blocks(rows), THREADS, 0, e.stream>>>(e.spectrum, e.Norm(), g.nu, g.nv, g.nw, e.ops,
compose_kernel<<<blocks(rows), THREADS, 0, e.stream>>>(e.spectrum, e.Norm(g), g.nu, g.nv, g.nw, e.ops,
static_cast<int>(e.setup.sym_ops.size()), e.setup.den,
e.row_hkl, e.row_scale, rows, e.f);
cuda_err(cudaGetLastError());
}
const ModelDensityGrid &mg = e.setup.mask_grid;
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())));
CudaFFTPlan &mask_forward = SameGrid(mg, g) ? e.forward : e.mask_forward;
cufft_err(mask_forward.R2C(e.grid, reinterpret_cast<cufftComplex *>(e.spectrum.get())));
if (rows > 0) {
fmask_kernel<<<blocks(rows), THREADS, 0, e.stream>>>(e.spectrum, e.Norm(), e.row_hkl, rows, g.nu, g.nv, g.nw,
e.fmask);
fmask_kernel<<<blocks(rows), THREADS, 0, e.stream>>>(e.spectrum, e.Norm(mg), e.row_hkl, rows, mg.nu, mg.nv,
mg.nw, e.fmask);
cuda_err(cudaGetLastError());
}
@@ -19,6 +19,7 @@
// Everything an engine needs that does not depend on the model's coordinates.
struct ModelStructureFactorsGPUSetup {
ModelDensityGrid grid; // the structure-factor grid, u fastest
ModelDensityGrid mask_grid; // the bulk-solvent mask's (SolventMaskGrid, ModelGrid.h): `grid`, or finer
double volume = 0;
// The group's operators as gemmi holds them: rot (row-major) and tran, in units of 1 / den.
std::vector<std::array<int, 12>> sym_ops;
@@ -39,8 +40,9 @@ public:
// The device's total memory.
static size_t TotalMemory(int device);
// The cuFFT work area the structure factors' r2c, and a map of (nu, nv, nw)'s c2r, take on `device`.
static size_t FFTWorkBytes(int device, const ModelDensityGrid &grid, int map_nu, int map_nv, int map_nw);
// The cuFFT work area the structure factors' and the mask's r2c, and a map of (nu, nv, nw)'s c2r, take on `device`.
static size_t FFTWorkBytes(int device, const ModelStructureFactorsGPUSetup &setup, int map_nu, int map_nv,
int map_nw);
// The bytes an engine for `setup` reserves on the device, given its cuFFT work area.
static size_t DeviceBytes(const ModelStructureFactorsGPUSetup &setup, size_t fft_work_bytes);
@@ -896,18 +896,24 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
// The mask is made on a thread of its own, beside the density: both only read the model, each
// fills a grid of its own, and between them they are the whole cost of this step. Both are
// gemmi's gridding spread over the threads (ModelGrid.h), the same grids bit for bit at any
// thread count; the two grids are the same size, so they share the symmetry's orbits.
// thread count. The mask is on the density's grid, and shares its symmetry's orbits, but where that
// grid is too coarse for the mask's shrink (SolventMaskGrid).
// Refmac radii give a slightly lower R than the Cctbx set on our test cases, at no cost.
gemmi::Grid<float> mask_grid;
mask_grid.unit_cell = dc.grid.unit_cell;
mask_grid.spacegroup = dc.grid.spacegroup;
mask_grid.set_size_from_spacing(dc.requested_grid_spacing(), gemmi::GridSizeRounding::Up);
const std::vector<size_t> orbit_leaders = OrbitLeaders(mask_grid, nthreads);
gemmi::Grid<float> mask_grid =
SolventMaskGrid(dc.grid.unit_cell, *dc.grid.spacegroup, to_d, dc.requested_grid_spacing());
mask_grid.data.resize(mask_grid.point_count());
gemmi::Grid<float> sized;
sized.unit_cell = dc.grid.unit_cell;
sized.spacegroup = dc.grid.spacegroup;
sized.set_size_from_spacing(dc.requested_grid_spacing(), gemmi::GridSizeRounding::Up);
const bool same = sized.nu == mask_grid.nu && sized.nv == mask_grid.nv && sized.nw == mask_grid.nw;
const std::vector<size_t> mask_leaders = OrbitLeaders(mask_grid, nthreads);
const std::vector<size_t> density_leaders = same ? mask_leaders : OrbitLeaders(sized, nthreads);
std::future<gemmi::AsuData<std::complex<float>>> fmask = std::async(std::launch::async, [&] {
PutMaskOnGrid(mask_grid, ms.st.models[0], orbit_leaders, nthreads);
PutMaskOnGrid(mask_grid, ms.st.models[0], mask_leaders, nthreads);
return MapToFPhi(mask_grid).prepare_asu_data(to_d, 0);
});
PutModelDensityOnGrid(dc, ms.st.models[0], orbit_leaders, nthreads);
PutModelDensityOnGrid(dc, ms.st.models[0], density_leaders, nthreads);
ms.fcalc = MapToFPhi(dc.grid)
.prepare_asu_data(dc.d_min, dc.blur, false, false, false);
ms.fmask = fmask.get();
@@ -2174,14 +2180,18 @@ ModelValidationResult ReadModelAnomalousMap(const std::vector<MergedReflection>
dc.rate = 1.5;
dc.set_grid_cell_and_spacegroup(st);
dc.set_refmac_compatible_blur(st.models[0]);
gemmi::Grid<float> mask_grid;
mask_grid.unit_cell = dc.grid.unit_cell;
mask_grid.spacegroup = dc.grid.spacegroup;
mask_grid.set_size_from_spacing(dc.requested_grid_spacing(), gemmi::GridSizeRounding::Up);
const std::vector<size_t> orbit_leaders = OrbitLeaders(mask_grid, nthreads);
PutMaskOnGrid(mask_grid, st.models[0], orbit_leaders, nthreads);
gemmi::Grid<float> mask_grid =
SolventMaskGrid(dc.grid.unit_cell, *dc.grid.spacegroup, d_min, dc.requested_grid_spacing());
mask_grid.data.resize(mask_grid.point_count());
gemmi::Grid<float> sized;
sized.unit_cell = dc.grid.unit_cell;
sized.spacegroup = dc.grid.spacegroup;
sized.set_size_from_spacing(dc.requested_grid_spacing(), gemmi::GridSizeRounding::Up);
const bool same = sized.nu == mask_grid.nu && sized.nv == mask_grid.nv && sized.nw == mask_grid.nw;
const std::vector<size_t> mask_leaders = OrbitLeaders(mask_grid, nthreads);
PutMaskOnGrid(mask_grid, st.models[0], mask_leaders, nthreads);
fmask = MapToFPhi(mask_grid).prepare_asu_data(d_min, 0);
PutModelDensityOnGrid(dc, st.models[0], orbit_leaders, nthreads);
PutModelDensityOnGrid(dc, st.models[0], same ? mask_leaders : OrbitLeaders(sized, nthreads), nthreads);
fcalc = MapToFPhi(dc.grid).prepare_asu_data(dc.d_min, dc.blur, false, false, false);
}
@@ -2287,6 +2297,8 @@ std::vector<MergedReflection> ModelReferenceIntensities(const RugnuxModel &model
// and a correlation on raw intensities would be led by them.
constexpr double K_SOL = 0.35;
constexpr double B_SOL = 46.0;
// On the density's grid, unlike the scored masks (SolventMaskGrid): a reference at fixed solvent constants,
// which the tests' amplitudes are made from as well.
gemmi::SolventMasker masker(gemmi::AtomicRadiiSet::Refmac);
gemmi::Grid<float> mask_grid;
mask_grid.unit_cell = dc.grid.unit_cell;
@@ -444,29 +444,11 @@ BulkSolventMask::BulkSolventMask(const gemmi::UnitCell &cell, const gemmi::Space
double BulkSolventMask::MaskSpacing(const gemmi::UnitCell &cell, double d_min) {
const double spacing = std::max(std::min(d_min / 3.0, MASK_SPACING_MAX), std::cbrt(cell.volume / MASK_POINTS_MAX));
return std::min(spacing, d_min / 3.0);
return std::min({spacing, MASK_SPACING_SHRINK, d_min / 3.0});
}
gemmi::Grid<float> BulkSolventMask::MaskGrid(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min) {
// the cap would coarsen it beyond the atoms' grid: that grid, whose points the density holds anyway
if (std::cbrt(cell.volume / MASK_POINTS_MAX) > d_min / 3.0) {
gemmi::Grid<float> grid = RigidBodyZoneGrid(cell, sg, d_min);
std::vector<float>().swap(grid.data);
return grid;
}
gemmi::Grid<float> grid;
grid.unit_cell = cell;
grid.spacegroup = &sg;
const double spacing = MaskSpacing(cell, d_min);
// set_size_from_spacing()'s size, without allocating the values
const std::array<int, 3> n = gemmi::good_grid_size({{cell.a / spacing, cell.b / spacing, cell.c / spacing}},
gemmi::GridSizeRounding::Up, &sg);
grid.nu = n[0];
grid.nv = n[1];
grid.nw = n[2];
grid.calculate_spacing();
grid.axis_order = gemmi::AxisOrder::XYZ;
return grid;
return SolventMaskGrid(cell, sg, d_min, MaskSpacing(cell, d_min));
}
void BulkSolventMask::Fmask(const gemmi::Model &model, const std::vector<gemmi::Miller> &hkl,
@@ -90,11 +90,10 @@ struct XrayRowModel {
// probe and shrink radii (1.0 and 0.8 A), which a grid at d_min / 3 cannot resolve at low resolution - at 3 A
// its 1 A step leaves no grid point within the shrink radius and the shrink is skipped. The step is therefore
// d_min / 3 or MASK_SPACING_MAX, whichever is finer - where the mask grid stays within MASK_POINTS_MAX points; a
// cell too large for that gets the spacing that fills it, but never one coarser than the atoms' density grid
// (RigidBodyZoneGrid, d_min / 3), which it then is: the density's points are on the card anyway, and the mask's
// own grid only ever saved memory. The R-factors of low-resolution models settle at a step of 0.4 A (within 0.001
// of 0.3 A) and move by up to 0.005 between 0.4 and 0.8 A; a cell coarsened by the cap past the shrink radius has
// no shrink at all, its macromolecule swollen by the whole probe, which costs far more.
// cell too large for that gets the spacing that fills it, but never one coarser than MASK_SPACING_SHRINK, where the
// shrink still acts, nor than the atoms' density grid (d_min / 3), which it then is (SolventMaskGrid, ModelGrid.h).
// The R-factors of low-resolution models settle at a step of 0.4 A (within 0.001 of 0.3 A) and move by up to 0.005
// between 0.4 and 0.8 A; past the shrink radius there is no shrink at all, which costs far more.
// The flat solvent model of Fokine & Urzhumtsev (2002) Acta Cryst. D58, 1387-1392
// The probe-and-shrink mask of Jiang & Brunger (1994) J. Mol. Biol. 243, 100-115
class BulkSolventMask {
@@ -104,7 +103,7 @@ public:
BulkSolventMask(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min, size_t nthreads);
// The mask's grid (sizes and spacing set, values not allocated) - also the GPU engine's (RefinementGPU.cpp).
static gemmi::Grid<float> MaskGrid(const gemmi::UnitCell &cell, const gemmi::SpaceGroup &sg, double d_min);
// The step that grid is asked for.
// The step that grid is asked for (before good_grid_size() rounds it finer).
static double MaskSpacing(const gemmi::UnitCell &cell, double d_min);
// Fmask (unscaled) of `model` at `hkl`.
void Fmask(const gemmi::Model &model, const std::vector<gemmi::Miller> &hkl,
+48 -5
View File
@@ -1159,6 +1159,50 @@ TEST_CASE("ModelValidation_RigidBodySameOnAnyNumberOfThreads", "[ModelValidation
// gemmi's but for the last bits of its single-precision walk (a point at a radius' edge can also fall on
// the other side, adding or losing a term at the cutoff). Both must be the same on any number of threads,
// for isotropic and anisotropic atoms and for groups with and without centring.
// The bulk-solvent mask's grid: the density's (d_min / 3) where that is fine enough for the mask's 0.8 A shrink,
// and finer where it is not - there the shrink is skipped on the density's grid and the macromolecule keeps the
// whole probe, so less of the cell is solvent; on the mask's own grid the solvent is what a fine grid finds.
TEST_CASE("SolventMaskGrid_ShrinkActsAtLowResolution", "[ModelValidation]") {
const auto path = WriteTemp("solvent_mask_grid_test.pdb",
ClusterPdb("CRYST1 40.000 50.000 60.000 90.00 90.00 90.00 P 21 21 21 4\n").c_str());
gemmi::Structure st = gemmi::read_structure_gz(path, gemmi::CoorFormat::Detect);
std::filesystem::remove(path);
const gemmi::SpaceGroup *sg = st.find_spacegroup();
REQUIRE(sg != nullptr);
auto sized = [&](double spacing) {
gemmi::Grid<float> g;
g.unit_cell = st.cell;
g.spacegroup = sg;
g.set_size_from_spacing(spacing, gemmi::GridSizeRounding::Up);
return g;
};
auto solvent = [&](gemmi::Grid<float> g) {
g.data.resize(g.point_count());
PutMaskOnGrid(g, st.models[0], OrbitLeaders(g, 2), 2);
double sum = 0;
for (float v : g.data)
sum += v;
return sum / static_cast<double>(g.data.size());
};
// 1.8 A: the density's grid, 0.6 A
const gemmi::Grid<float> fine = SolventMaskGrid(st.cell, *sg, 1.8, 1.8 / 3.0);
const gemmi::Grid<float> density_fine = sized(1.8 / 3.0);
CHECK(fine.nu == density_fine.nu);
CHECK(fine.nv == density_fine.nv);
CHECK(fine.nw == density_fine.nw);
CHECK(fine.data.empty());
// 4 A: the density's grid is 1.33 A, the mask's at most MASK_SPACING_SHRINK
const gemmi::Grid<float> coarse = SolventMaskGrid(st.cell, *sg, 4.0, 4.0 / 3.0);
for (int i = 0; i < 3; i++)
CHECK(coarse.spacing[i] <= MASK_SPACING_SHRINK);
const double on_density = solvent(sized(4.0 / 3.0)), on_mask = solvent(coarse), on_fine = solvent(sized(0.4));
INFO("solvent fraction on the density's grid " << on_density << ", the mask's " << on_mask << ", 0.4 A " << on_fine);
CHECK(on_mask > on_density + 0.01);
CHECK(std::fabs(on_mask - on_fine) < 0.005);
}
TEST_CASE("ModelValidation_ParallelGriddingMatchesGemmi", "[ModelValidation]") {
const char *crysts[] = {
"CRYST1 40.000 50.000 60.000 90.00 90.00 90.00 P 1 1\n",
@@ -1829,7 +1873,8 @@ TEST_CASE("RigidBodyGPU_CallsLeaveTheCallerOnItsCard", "[ModelValidation][gpu]")
// The model's structure factors on the GPU against the CPU path model validation takes - gemmi's density and
// mask on the symmetrized grid, transformed by FFTW and read off by prepare_asu_data() - in the five groups,
// at a resolution where the mask's shrink step does nothing and at one where it does. The same reflections
// at a resolution where the mask has a grid of its own (SolventMaskGrid) and at one where it is on the
// density's. The same reflections
// in the same order; F_calc to rounding; F_mask to the few grid points at an atom's mask radius that float
// and double distances put on different sides. The same atoms twice give the same bits.
TEST_CASE("ModelStructureFactorsGPU_MatchesCPU", "[ModelValidation][gpu]") {
@@ -1843,10 +1888,8 @@ TEST_CASE("ModelStructureFactorsGPU_MatchesCPU", "[ModelValidation][gpu]") {
auto dc = ZoneDensity(st, model, d_min);
dc.put_model_density_on_grid(model);
const auto ref_fc = MapToFPhi(dc.grid).prepare_asu_data(d_min, dc.blur, false, false, false);
gemmi::Grid<float> mask;
mask.unit_cell = st.cell;
mask.spacegroup = sg;
mask.set_size_from_spacing(dc.requested_grid_spacing(), gemmi::GridSizeRounding::Up);
gemmi::Grid<float> mask = SolventMaskGrid(st.cell, *sg, d_min, dc.requested_grid_spacing());
mask.data.resize(mask.point_count());
gemmi::SolventMasker(gemmi::AtomicRadiiSet::Refmac).put_mask_on_grid(mask, model);
const auto ref_fm = MapToFPhi(mask).prepare_asu_data(d_min, 0);