Refinement restraints: metal ions bound to their donors by the model's LINK records

Metal sites were held only by the 1.9 A metal contact of the repulsions; REFMAC restrains the metal LINKs of
the coordinate file (its documentation), which was the one geometry term the 0-cycle comparison did not
share (one 2.8 A set: bond rmsd 0.018 against REFMAC's 0.042).

- monomers/metals.cif: the Monomer Library's own statistics of metal sites (metals.json, LGPL data as the
  rest of monomers/), cut by update_monomers.py to the metal ions of its compound list and O, N, S donors:
  mean distance, spread and number of sites per metal, donor element and coordination number.
- AddMetalLinks (before prepare_topology): every LINK / struct_conn record between a metal ion and an O, N
  or S that no link of the dictionary describes (its ZN-CYS, ZN-HIS, FE-CYS, FE-HIS are taken as they are)
  gets a link of one bond at EmbeddedMetalDistance for the two elements and the ion's coordination - the
  number of its donor records, symmetry mates included - from the rows resting on at least 20 sites (the
  coordination's own row, else the neighbours interpolated), sigma their spread (>= 0.02 A); a His or Cys
  donor loses the proton the bond replaces (DEL-HD1 / DEL-HE2 / CYSmod1, as the library's links do).
  Records only, no distance search; records to a symmetry mate count for the coordination but are not
  restrained (bonds to symmetry mates are not, anywhere).
- The metal bonds are a restraint set of their own (RefinementTopology::metal_bonds), evaluated with the
  geometry and reported apart (GeometryStatistics::rmsd_metal, REFINE_RMSD_METAL_BONDS,
  REFINE_SCORE_RMSD_METAL_BONDS / _BONDS_ALL): their sigma is the spread of real sites, so they sit
  further from ideal than covalent bonds, and the weight ladder's 0.02 A bound stays a bound on the
  covalent ones. refine_parity.py compares REFMAC's bond rmsd (links included) with the combined one, and
  takes --gpu.

Score-only geometry against REFMAC 5.8 0-cycle, all bonds (count): 2.8 A Zn/Fe set 0.0402 (4934) vs 0.042
(4935), was 0.0178; a Zn-Cys set 0.0186 (7102) vs 0.019 (7102); a Ca set 0.0297 vs 0.026 (REFMAC also
refines the sugars and uses one Ca-O distance); a Mg set 0.0114 vs 0.017 (REFMAC restrains 8 symmetry
links and puts Mg-O at 1.99 A, the library's statistics at 2.07). R of the scored model unchanged (+0.0001
on the first from the removed His/Cys protons). Refined R-free with the links vs without: +0.0016, -0.0002,
+0.0005, +0.0004.
Test: RefinementRestraints_metal_links (Zn-Cys4 by the dictionary's link, Mg(H2O)6 by the new ones, both
back at their distances).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit 0fcffc5c71e03e184cff18f07524473462a57b68)
This commit is contained in:
leonarski_f committed 2026-10-26 01:38:24 +01:00
1 parent 9da3851844
commit bcb71136ab
13 files changed
+547 -29

No files matched your search

@@ -94,6 +94,89 @@ V3<double> FromImage(const RestraintRecord &r, V3<double> g) {
r.rot[2] * g.x + r.rot[5] * g.y + r.rot[8] * g.z};
}
// A metal ion: a residue of one atom, of a metal element.
bool IsMetalIon(const gemmi::CRA &cra) {
return cra.residue->atoms.size() == 1 && IsMetal(cra.atom->element);
}
bool IsMetalDonor(const gemmi::Element &el) {
return el == gemmi::El::O || el == gemmi::El::N || el == gemmi::El::S;
}
// The modification a donor's residue takes when it binds a metal: the proton the bond replaces, as the
// library's own ZN-HISND, ZN-HISNE and ZN-CYS links have it.
std::string MetalDonorModification(const std::string &resname, const std::string &atom) {
if (resname == "HIS" && atom == "ND1")
return "DEL-HD1";
if (resname == "HIS" && atom == "NE2")
return "DEL-HE2";
if (resname == "CYS" && atom == "SG")
return "CYSmod1";
return "";
}
// The model's LINK / struct_conn records between a metal ion and an O, N or S that no link of the dictionary
// describes (its ZN-CYS, ZN-HISND, ZN-HISNE, FE-CYS, FE-HISND and FE-HISNE are taken as they are): a link of
// one bond is made for each in `monlib`, at the distance of the library's statistics of metal sites
// (EmbeddedMetalDistance) for the two elements and the site's coordination - the number of records between the
// ion and an O, N or S - and the record is pointed at it, so that prepare_topology() restrains it like any other link.
// Only the bond: no angles at the metal. Sites are taken from the records alone, as REFMAC takes links from
// the coordinate file; a contact without a record stays a repulsion (BuildRepulsions, METAL_CONTACT).
// Records to a symmetry mate are left out, as every bond to a symmetry mate is (BuildRefinementTopology).
// Returns the number of links made.
// Distances as surveyed by Harding (2001) Acta Cryst. D57, 401-411 and (2006) Acta Cryst. D62, 678-682,
// and for Mg, Na and K at nucleic acids by Leonarski, D'Ascenzo & Auffinger (2017) Nucleic Acids Res. 45, 987-1004
int AddMetalLinks(gemmi::Structure &st, gemmi::MonLib &monlib) {
gemmi::Model &model = st.models.at(0);
struct Site {
gemmi::Connection *conn;
gemmi::CRA metal, donor;
bool metal_first; // the metal is partner1
};
std::vector<Site> sites;
std::unordered_map<const gemmi::Atom *, int> coordination;
for (gemmi::Connection &conn : st.connections) {
gemmi::CRA a = model.find_cra(conn.partner1, true), b = model.find_cra(conn.partner2, true);
if (!a.atom || !b.atom)
continue;
const bool a_metal = IsMetalIon(a), b_metal = IsMetalIon(b);
if (a_metal == b_metal)
continue;
const gemmi::CRA &metal = a_metal ? a : b, &donor = a_metal ? b : a;
if (!IsMetalDonor(donor.atom->element))
continue;
// the site's coordination: every record of the ion, a symmetry mate's or one the dictionary describes too
coordination[metal.atom]++;
if (!conn.link_id.empty() || conn.asu == gemmi::Asu::Different ||
std::get<0>(monlib.match_link(*a.residue, a.atom->name, conn.partner1.altloc, *b.residue, b.atom->name,
conn.partner2.altloc)) != nullptr)
continue;
sites.push_back({&conn, metal, donor, a_metal});
}
int made = 0;
for (const Site &site : sites) {
double distance = 0, sigma = 0;
if (!EmbeddedMetalDistance(site.metal.atom->element.elem, site.donor.atom->element.elem,
coordination[site.metal.atom], distance, sigma))
continue;
const gemmi::Connection &conn = *site.conn;
gemmi::ChemLink link;
link.side1.comp = conn.partner1.res_id.name;
link.side2.comp = conn.partner2.res_id.name;
(site.metal_first ? link.side2 : link.side1).mod =
MetalDonorModification(site.donor.residue->name, site.donor.atom->name);
link.id = "metal-" + std::to_string(made);
link.name = link.id;
link.rt.bonds.push_back({gemmi::Restraints::AtomId{1, conn.partner1.atom_name},
gemmi::Restraints::AtomId{2, conn.partner2.atom_name},
gemmi::BondType::Metal, false, distance, sigma, distance, sigma});
monlib.links.emplace(link.id, link);
site.conn->link_id = link.id;
made++;
}
return made;
}
} // namespace
void RestraintSet::Add(RestraintRecord r, int n_atoms_of_r) {
@@ -247,6 +330,7 @@ RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens)
out.monlib = std::make_shared<gemmi::MonLib>(EmbeddedMonomerLibrary());
gemmi::MonLib &monlib = *out.monlib;
gemmi::setup_entities(st);
out.metal_links = AddMetalLinks(st, monlib);
gemmi::Logger quiet; // missing atoms and unknown residues are what the fixed residues report
quiet.threshold = 0;
out.topo = gemmi::prepare_topology(st, monlib, 0,
@@ -349,8 +433,12 @@ RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens)
r.atom[1] = j;
r.ideal = static_cast<float>(b.restr->value);
r.sigma = static_cast<float>(b.restr->esd);
out.geometry.Add(r, 2);
out.bonds++;
if (out.metal[i] || out.metal[j])
out.metal_bonds.Add(r, 2); // a metal's coordination, kept apart from the covalent bonds
else {
out.geometry.Add(r, 2);
out.bonds++;
}
RestraintRecord rb;
rb.kind = static_cast<int>(RestraintKind::BSimilarity);
rb.atom[0] = i;
@@ -454,6 +542,7 @@ RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens)
for (size_t i = 0; i < n; i++)
out.excluded[i].assign(within3[i].begin(), within3[i].end());
out.geometry.Finalize(n);
out.metal_bonds.Finalize(n);
out.adp.Finalize(n);
return out;
}
@@ -549,8 +638,19 @@ void PlaceRidingHydrogens(RefinementTopology &topo) {
}
GeometryStatistics MeasureGeometry(const RestraintSet &geometry, const RestraintSet &repulsions,
const std::vector<std::array<double, 4>> &xyzb) {
const std::vector<std::array<double, 4>> &xyzb, const RestraintSet *metal_bonds) {
GeometryStatistics s;
if (metal_bonds != nullptr) {
double sm = 0, zm = 0;
for (const RestraintRecord &r : metal_bonds->records) {
const double d = RestraintValue(*metal_bonds, r, xyzb) - r.ideal;
sm += d * d;
zm += d * d / (double(r.sigma) * r.sigma);
s.metal_bonds++;
}
s.rmsd_metal = s.metal_bonds > 0 ? std::sqrt(sm / s.metal_bonds) : 0;
s.rmsz_metal = s.metal_bonds > 0 ? std::sqrt(zm / s.metal_bonds) : 0;
}
double sb = 0, sa = 0, sc = 0, zb = 0, za = 0;
int nc = 0;
for (const RestraintRecord &r : geometry.records) {