Each round of the build was a refinement of its own - the whole weight search again, and a rejected round cost a full refinement - so the build took 2-4 times the refinement. Now the atom list carries placeholder waters from the start (occupancy 0: no density, no mask, no contact, not refined; a fifteenth of the heavy atoms, at least 64), and the build runs where the weight trajectory ends: a round reads the maps and the audit of that model, puts waters into placeholders, takes waters out (occupancy 0) and relabels sites in place (RetypeAtom: element, flags, metal bonds, exclusions; the GPU engine refreshes an atom's density on an occupancy or element change), in every trial slot; then the solvent alone is refined at the trajectory's weights (B refined, 10 iterations a cycle) and the round is kept where that lowers R-free, else undone. At most two rounds; the final ladder then refines what they kept with the rest. The output model is the view with the placeholders moved into their chains (SolventView) and the relabelled metals' inner-sphere records. One topology for the whole refinement, so the metal records are decided once by construction (the outer_records plumbing of the previous commit is gone again). Contacts() leaves out atoms at occupancy 0; FindSpecialPositions() too (placeholders at the origin sat on the 2-folds of I222/P43212/C2 and were projected there - R-free 0.44 on 36gk until fixed); waters put on a special position get their site constraint. The audit runs on the threads (its density tables made up front), and MakeMaps makes its three maps at once. Timing (--mode refine on the depositions, GPU, each set's three runs under one lock, load 15-24 from other agents), refinement seconds without the build / with the old build / with this one, R-free old / new: 1hr2 3.5/11.3/4.6 0.2462/0.2490, 2r8s 2.8/11.2/4.6 0.2238/0.2244, 193l 1.0/3.1/1.8 0.1713/0.1713, 1exr 1.6/5.2/1.9 0.2076/0.2084, 1k4c 2.5/4.9/3.2 0.2106/0.2106, 36gk 3.7/12.4/6.7 0.2559/0.2543, 7l6j 3.6/16.3/6.5 0.1530/0.1546, 8agq 2.1/6.7/2.7 0.1753/0.1761, 6hwj 1.9/8.1/3.6 0.2222/0.2232, 8sa8 8.5/36.2/14.1 0.1714/0.1710, 6f3p 4.8/22.3/8.3 0.1629/0.1630, 7os3 1.4/4.9/2.6 0.2122/0.2115. Build overhead 0.2-0.9x the refinement (median 0.7), was 1.0-3.7x (median 2.3); R-free within 0.001 on 8/12. Tried and dropped (P3.md): the trajectory again after the rounds (never gained), a weight ladder after the build (never displaced), building after the ladder instead (same), accepting rounds within the R-free tie (worse on 4), a full step as the round's judge (noisier), one macro-cycle for the solvent (6f3p's rounds refused). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVeDQqN9qRD57vnShzTC37
104 lines
6.0 KiB
C++
104 lines
6.0 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "gemmi/model.hpp"
|
|
|
|
#include "SolventSites.h" // SolventSite, SolventMaps
|
|
|
|
// One round of the solvent build that RefineModel runs within its weight trajectory (Refinement.h): waters placed at the
|
|
// peaks of the difference map a refined model leaves, the model's waters the maps no longer support taken out, and
|
|
// waters and ions relabelled.
|
|
//
|
|
// A peak of mFo-DFc becomes a water where it has a hydrogen-bond partner and nothing too close - on a special
|
|
// position exactly on it, at the occupancy that makes its coinciding copies one water; a peak that
|
|
// cannot be a single solvent atom - one bonded to the model, beside a negative peak on a model atom, where a
|
|
// truncated side chain's missing atoms would be, one of a cluster of peaks at bonded distances, elongated, or
|
|
// denser than the model's S and P - is reported as not solvent and left alone. A water or a single-atom ion whose
|
|
// surroundings and density support another species, not ambiguously (SolventSites.h), is relabelled as it: a water
|
|
// becomes an ion, an ion water or another ion.
|
|
struct SolventEdit {
|
|
enum class Kind { Added, Removed, NotSolvent, Relabelled };
|
|
Kind kind = Kind::NotSolvent;
|
|
int round = 0;
|
|
std::string site; // the water as modelled ("HOH A401"); empty for a peak not taken
|
|
std::string chain; // where an added water goes: the chain of the atom nearest to it
|
|
gemmi::SeqId seqid; // a removed water's residue
|
|
char altloc = '\0';
|
|
gemmi::Position pos;
|
|
double difference_sigma = 0; // mFo-DFc at the site, in its rms
|
|
double map_sigma = 0; // 2mFo-DFc
|
|
double b_iso = 0; // an added water's starting B, a removed one's
|
|
double occupancy = 1; // an added water's: 1 over its site's multiplicity on a special position
|
|
double elongation = 0; // a peak's density: the longest axis over the shortest, rms
|
|
std::string species; // a relabelled site's new residue name (HOH, MG, NA, K, CA, CL); its old one in `site`
|
|
std::string reason; // why: the partners an added water has, what a removed or refused one lacks, the
|
|
// scores a relabelled one has
|
|
};
|
|
|
|
const char *SolventEditKindName(SolventEdit::Kind kind);
|
|
|
|
struct SolventRound {
|
|
std::vector<SolventEdit> edits;
|
|
int added = 0, removed = 0, not_solvent = 0, relabelled = 0;
|
|
std::map<std::string, int> refused; // the peaks neither taken nor reported, by what they lack
|
|
};
|
|
|
|
// The resolution waters are built to at most, following the ordered-solvent defaults of phenix.refine
|
|
// (Afonine et al. (2012) Acta Cryst. D68, 352-367): no picking where the data stop short of 2.8 A.
|
|
constexpr double SOLVENT_BUILD_D_MIN = 2.8;
|
|
|
|
// At most this many rounds, each judged by refining the solvent it changed (a third one gained at most 0.0004 in
|
|
// R-free where it was kept, on twelve sets, and was refused on the others).
|
|
constexpr int SOLVENT_BUILD_ROUNDS = 2;
|
|
|
|
// What the build did, over the rounds kept.
|
|
struct SolventBuildResult {
|
|
bool ran = false;
|
|
int rounds = 0; // the rounds kept
|
|
int added = 0; // waters added and still in the model
|
|
int removed = 0; // waters of the model as given taken out
|
|
int relabelled = 0; // sites relabelled, in the rounds kept
|
|
int not_solvent = 0; // peaks of the final model's difference map that are no solvent
|
|
std::vector<SolventEdit> edits; // the kept rounds' additions and removals, then the final not-solvent peaks
|
|
std::string stop; // why it stopped
|
|
double seconds = 0;
|
|
};
|
|
|
|
// A round on `model` (the refined model the maps were made from, in their cell and space group): `maps` its
|
|
// 2mFo-DFc and mFo-DFc (the anomalous one unused), `sites` its solvent audit (AnalyseSolventSites on the same
|
|
// maps), `built` the waters added by earlier rounds (by name, "HOH A401"), `b_ceiling` the highest B its refinement
|
|
// allowed; `relabelled` the sites relabelled by earlier rounds (by their new name), which are not relabelled again.
|
|
SolventRound PickSolvent(const gemmi::Structure &model, const SolventMaps &maps, const std::vector<SolventSite> &sites,
|
|
const std::set<std::string> &built, const std::set<std::string> &relabelled, double b_ceiling,
|
|
int round);
|
|
|
|
// The N, O and S of a metal's inner sphere in the model's own copy (no symmetry mate): within the library's distance
|
|
// plus INNER_SHELL_MARGIN, at the coordination of those within an octahedral site's - the atoms a metal's records
|
|
// name. `own` is the metal's residue.
|
|
struct InnerSphereDonor {
|
|
int chain = 0, residue = 0, atom = 0; // indices into the model
|
|
double distance = 0;
|
|
};
|
|
std::vector<InnerSphereDonor> InnerSphereDonors(const gemmi::Model &model, const gemmi::Residue &own,
|
|
const gemmi::Atom &metal);
|
|
|
|
// The element of a species by its residue name (HOH: O, MG: Mg, ...).
|
|
gemmi::El SolventSpeciesElement(const std::string &species);
|
|
|
|
// A record (struct_conn metalc, named rugnux_metalcN with N on from `record`) for each of InnerSphereDonors() of the
|
|
// single-atom ion `res` of `chain`, where it is a metal.
|
|
void AddMetalRecords(gemmi::Structure &st, const gemmi::Chain &chain, const gemmi::Residue &res, int &record);
|
|
|
|
// The round's waters taken out and put in: each added one to the chain of its nearest atom, numbered on from that
|
|
// chain's last residue (its `site` is set to the name it gets). A relabelled site gets its new residue name,
|
|
// element and atom name; its metal records go, and a metal gets one for each N, O or S of its inner sphere (within
|
|
// the library's distance plus 0.33 A, as the records of the model as given are taken).
|
|
void ApplySolventEdits(gemmi::Structure &st, std::vector<SolventEdit> &edits);
|