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:
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) {
|
||||
|
||||
Reference in new issue
Block a user