Solvent build: write each new water beside the model, not a symmetry mate

A difference peak is taken once per symmetry-equivalent set (the map's
asymmetric unit) and was to be moved to its copy nearest the model's own
atoms (NearestCopy) before a water is built there. That step took the
nearest atom's NeighborSearch mark, whose position is already the
symmetry image, and applied the image's operator to it a second time:
outside the identity image the water was written beside a symmetry mate
or a unit cell away (by a lattice translation for a two-fold, beside
another mate for a three-, four- or six-fold).

The copy is now taken against the atom as the model has it: the
symmetry operator and lattice translation that put the peak nearest to
that atom (UnitCell::find_nearest_image / find_nearest_pbc_position). The
water's environment, and so the refinement, is the same; only the
coordinates written move. New test: a peak beside a four-fold mate in
P4 is written 2.8 A from the model's water (the old step wrote it a
lattice vector away).

5reo (open arm, C2), --refine-solvent, share of the added waters whose
nearest non-water, non-hydrogen model atom over all copies is in the
written copy: from the model without waters 67/155 (43%) before,
154/155 after; from the deposited model 27/43 before, 42/43 after
(the one left a near tie moved by refinement). R-work/R-free
0.2046/0.2335 -> 0.2046/0.2334 and 0.1979/0.2172 unchanged; 156 and 43
waters added either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqXYdL4qBGYRiSXSGXfCLu
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-05 15:55:15 +01:00
1 parent caf2461bfa
commit c4df332618
3 files changed
+27 -9

No files matched your search

+3 -1
View File
@@ -476,7 +476,9 @@ higher, one 0.0016 lower). `--refine-score-only` builds nothing, and the build a
restrains the model's own (§15.5).
- *Candidates* are the local maxima of mFo-DFc from 3 σ (phenix.refine's peak cutoff), each symmetry-equivalent
set once, moved to the copy nearest the model. A peak closer than 1.2 Å to a model atom is that atom's own
set once, moved to the copy nearest the model - the symmetry operator and lattice translation that put it
closest to an atom of the model as written, so a water built there is written beside the model, not beside a
symmetry mate or in another unit cell. A peak closer than 1.2 Å to a model atom is that atom's own
residual and is passed over.
- *Not solvent* - reported in `<prefix>_solvent.json` with the reason, never filled: a peak 1.2-1.9 Å from a model
atom (a missing or displaced atom); one beside a model atom (within 2.6 Å) that sits in mFo-DFc of -3 σ or
@@ -132,18 +132,15 @@ std::vector<Peak> DifferencePeaks(const gemmi::Grid<float> &g, double rms, doubl
return out;
}
// The copy of `p` nearest to the model's own atoms (not their symmetry mates); false where no atom is within
// `reach`.
// The copy of `p` - the symmetry operator and lattice translation - nearest to the model's own atoms (not their
// symmetry mates); false where no atom is within `reach`. The mark's position is already the atom's image, so the
// copy is taken by the atom as the model has it: p's copy nearest to that atom.
bool NearestCopy(gemmi::NeighborSearch &ns, const gemmi::UnitCell &cell, double reach, gemmi::Position &p) {
gemmi::NeighborSearch::Mark *m = ns.find_nearest_atom(p, reach);
if (m == nullptr)
return false;
const gemmi::FTransform t = ns.get_image_transformation(m->image_idx);
const gemmi::Fractional fc = cell.fractionalize(p);
const gemmi::Fractional fi = t.apply(cell.fractionalize(m->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));
p = cell.orthogonalize(gemmi::FTransform(t.inverse()).apply(shifted));
const gemmi::Position atom = m->to_cra(*ns.model).atom->pos;
p = cell.find_nearest_pbc_position(atom, p, cell.find_nearest_image(atom, p, gemmi::Asu::Any).sym_idx);
return true;
}
+19
View File
@@ -101,6 +101,25 @@ TEST_CASE("SolventBuild_peak_with_and_without_partner", "[Solvent]") {
CHECK(r.edits.at(0).site == "HOH W1");
}
// A peak beside a symmetry mate of the model's water, in another unit cell, is written beside the water itself: the
// copy of the peak - symmetry operator and lattice translation - nearest to the model's own atoms.
TEST_CASE("SolventBuild_peak_written_beside_the_model", "[Solvent]") {
gemmi::Structure st = box();
st.spacegroup_hm = "P 4";
st.setup_cell_images();
add_atom(st, "HOH", "O", gemmi::El::O, {6, 4, 6});
// the water's mate by -y,x,z is at 20 6 6; the peak 2.8 A from it, only there (the map drawn without symmetry)
const gemmi::Position peak(20, 6, 8.8);
const gemmi::Grid<float> diff = map_with(gemmi::UnitCell(EDGE, EDGE, EDGE, 90, 90, 90), {peak}, 6.0f);
const gemmi::Grid<float> map = map_with(st.cell, {}, 0.0f, 1.5f);
const SolventMaps maps{&map, 1.0, &diff, 1.0, nullptr, 0};
const SolventRound r = PickSolvent(st, maps, {}, {}, {}, 300, 1);
REQUIRE(r.added == 1);
const SolventEdit &e = r.edits.at(0);
CHECK(e.pos.dist(gemmi::Position(6, 4, 6)) == Catch::Approx(2.8).margin(0.25));
}
// A sulfate-like cluster - a peak with four more at 1.5 A on a tetrahedron - beside a water is no water: the centre
// is reported as a tetrahedral group, its corners as atoms of one molecule; nothing is added.
TEST_CASE("SolventBuild_tetrahedral_cluster_refused", "[Solvent]") {