Files
leonarski_fandClaude Opus 5.5 2a0e91bd6a RESULT-CHANGING: Refinement: the solvent build within the weight search, two rounds each judged by refining the solvent
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
2026-10-27 18:39:47 +01:00

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);