Files
Jungfraujoch/image_analysis/structure_refinement/monomers/update_monomers.py
T
leonarski_f 00662c4110 Refinement: common cofactors and modified RNA nucleotides in the embedded dictionary; heme iron restrained from LINK records
The embedded CCP4 Monomer Library subset grows from 62 to 84 compounds:
- the modified tRNA/rRNA nucleotides PSU, 5MC, 1MA, OMG, OMC, OMU, H2U, M2G, 7MG, 2MG, 5MU (RNA group,
  joined by the library's backbone link "p");
- the cofactors NAD, NAI, NAP, NDP, FAD, FMN, HEM, HEC, SAM, SAH, and dioxygen (OXY). CMO is left out:
  its dictionary has C-O at 1.25 A, where bound CO is near 1.15 A.
links_and_mods.cif gains the links these compounds use: HEC-CYS1/HEC-CYS2 (heme C thioethers),
CYS-HEMB/CYS-HEMC, CYS-FAD and HIS-FAD1/2, with their modifications.

AddMetalLinks now takes a metal atom of any residue, not only a one-atom ion, so a heme iron's LINK records
to His NE2 / Met SD / water become bonds at the library's metal-site distances; the site's coordination counts
the metal's bonds in its own dictionary (a heme's four pyrrole N) as well as the records. A metal within a
ligand is also a metal for the repulsions (1.9 A to N, O, S) and its dictionary bonds are kept with the metal
bonds. Note HEC.cif's own Fe-N(pyrrole) ideal is 1.90 A (HEM: 2.04 A), so a cytochrome c reports a metal-bond
rmsd near 0.1 A.

Test RefinementRestraints_deposited_ligands builds the topology of eleven deposited models cut down to the
ligand and its surroundings (tests/test_data/ligands, 66 KB, from PDB 1EHZ, 8BKH, 6K9I, 2W3V, 5VML, 6F3P,
8CON, 3TK0, 7TCD, 6W4H, 5EPE): every ligand restrained, bond rmsd < 0.03 A, the RNA backbone bonded to each
modified nucleotide, the heme Fe-His bonds, and the HEC thioethers. rugnux --mode refine on the full entries
against their deposited structure factors: R-free within +-0.003 of the build where these were held fixed,
ligand bond rmsd vs the dictionary from 0.004-0.058 A down to 0.002-0.042 A. rugnux grows by 425 KiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit f4d407c8de6cb462320edb21a70665350e436d28)
2026-10-26 13:56:29 +01:00

134 lines
6.3 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
# SPDX-License-Identifier: GPL-3.0-only
"""Copy the subset of the CCP4 Monomer Library (https://github.com/MonomerLibrary/monomers, LGPL-3.0)
that rugnux's restrained refinement carries in its binary into this directory.
update_monomers.py /path/to/monomers
COMPOUNDS below is THE list: a compound named there is restrained by the refinement, everything else
is held fixed. To add one, add its code and run this again; CMake embeds every .cif of this directory
(MonomerLibrary.cpp), so nothing else needs to change.
The compound files are copied verbatim. links_and_mods.cif is cut down to the links and modifications
that can apply to these compounds (the polymer links, the termini, the disulfide, ...), and ener_lib.cif
to its atom-type table (_lib_atom: the van der Waals and ion radii and the hydrogen-bond types).
metals.json, the library's statistics of metal-ligand distances, becomes metals.cif: for each metal ion of
COMPOUNDS and each donor element of METAL_LIGANDS, the distance per coordination number (the restraints of
metal sites the links of links_and_mods.cif do not describe).
Requires the gemmi Python module."""
import json
import os
import shutil
import sys
import gemmi
AMINO_ACIDS = ["ALA", "ARG", "ASN", "ASP", "CYS", "GLN", "GLU", "GLY", "HIS", "ILE",
"LEU", "LYS", "MET", "PHE", "PRO", "SER", "THR", "TRP", "TYR", "VAL", "MSE"]
NUCLEOTIDES = ["DA", "DC", "DG", "DT", "A", "C", "G", "U"]
# The modified nucleotides of tRNA and rRNA most often met, polymer residues joined by the same backbone link.
MODIFIED_NUCLEOTIDES = ["PSU", "5MC", "1MA", "OMG", "OMC", "OMU", "H2U", "M2G", "7MG", "2MG", "5MU"]
NUCLEOTIDE_LIGANDS = ["ATP", "ADP", "AMP", "GTP", "GDP", "GMP"]
# The cofactors most often met, and the oxygen a heme iron binds. (Not carbon monoxide: CMO.cif has C-O at 1.25 A,
# where bound CO is near 1.15 A.)
COFACTORS = ["NAD", "NAI", "NAP", "NDP", "FAD", "FMN", "HEM", "HEC", "SAM", "SAH", "OXY"]
ADDITIVES = ["DMS", "PEG", "PGE", "PG4", "1PE", "GOL", "EDO", "SO4", "PO4", "ACT", "MPD"]
WATER_AND_IONS = ["HOH", "NA", "K", "MG", "CA", "ZN", "MN", "FE", "FE2", "NI", "CU", "CO", "CD",
"CL", "BR", "IOD"]
COMPOUNDS = (AMINO_ACIDS + NUCLEOTIDES + MODIFIED_NUCLEOTIDES + NUCLEOTIDE_LIGANDS + COFACTORS + ADDITIVES +
WATER_AND_IONS)
# The donor elements of metal sites whose distances metals.cif carries.
METAL_LIGANDS = ["O", "N", "S"]
# Groups of the polymers above; links and modifications of other groups (sugars) are left out.
GROUPS = {".", "peptide", "P-peptide", "M-peptide", "DNA/RNA", "NON-POLYMER"}
def compound_path(lib, code):
return os.path.join(lib, code[0].lower(), code + ".cif")
def applies(comp, group):
return (comp == "." or comp in COMPOUNDS) and group in GROUPS
def trim_links_and_mods(lib, out):
doc = gemmi.cif.read(os.path.join(lib, "links_and_mods.cif"))
links = doc.find_block("link_list").find("_chem_link.", ["id", "comp_id_1", "mod_id_1", "group_comp_1",
"comp_id_2", "mod_id_2", "group_comp_2", "name"])
mods = doc.find_block("mod_list").find("_chem_mod.", ["id", "name", "comp_id", "group_id"])
keep_links = [list(r) for r in links if applies(r[1], r[3]) and applies(r[4], r[6])]
keep_mods = [list(r) for r in mods if applies(r[2], r[3])]
keep = {"link_" + r[0] for r in keep_links} | {"mod_" + r[0] for r in keep_mods}
new = gemmi.cif.Document()
for name, tag, rows in (("link_list", "_chem_link.", keep_links), ("mod_list", "_chem_mod.", keep_mods)):
block = new.add_new_block(name)
old = doc.find_block(name).find_loop_item(tag + "id").loop
loop = block.init_loop(tag, [t[len(tag):] for t in old.tags])
for r in rows:
loop.add_row(r)
for block in doc:
if block.name in keep:
new.add_copied_block(block)
new.write_file(out)
return len(keep_links), len(keep_mods)
def trim_ener_lib(lib, out):
doc = gemmi.cif.read(os.path.join(lib, "ener_lib.cif"))
atoms = doc[0].find_loop_item("_lib_atom.type").loop
new = gemmi.cif.Document()
block = new.add_new_block(doc[0].name)
loop = block.init_loop("_lib_atom.", [t[len("_lib_atom."):] for t in atoms.tags])
for i in range(atoms.length()):
loop.add_row([atoms[i, j] for j in range(atoms.width())])
new.write_file(out)
def trim_metals(lib, out):
"""metals.json -> metals.cif: one row per metal ion of COMPOUNDS, ligand element and coordination number,
with the mean distance, its standard deviation and the number of sites behind it."""
with open(os.path.join(lib, "metals.json")) as f:
table = json.load(f)["metal_coordination"]
metals = set()
for code in WATER_AND_IONS:
for atom in gemmi.make_chemcomp_from_block(gemmi.cif.read(compound_path(lib, code))[-1]).atoms:
if atom.el.is_metal:
metals.add(atom.el.name)
new = gemmi.cif.Document()
block = new.add_new_block("metal_coordination")
loop = block.init_loop("_metal_coordination.", ["metal", "ligand", "coordination", "distance", "std", "count"])
n = 0
for metal in sorted(metals):
for ligand in METAL_LIGANDS:
for row in table.get(metal, {}).get(ligand, []):
loop.add_row([metal, ligand, str(row["coord"]), "%.4f" % row["mean"], "%.4f" % row["std"],
str(row["count"])])
n += 1
new.write_file(out)
return n
def main():
if len(sys.argv) != 2:
sys.exit(__doc__)
lib = sys.argv[1]
here = os.path.dirname(os.path.abspath(__file__))
for f in os.listdir(here):
if f.endswith(".cif"):
os.remove(os.path.join(here, f))
for code in COMPOUNDS:
shutil.copyfile(compound_path(lib, code), os.path.join(here, code + ".cif"))
n_links, n_mods = trim_links_and_mods(lib, os.path.join(here, "links_and_mods.cif"))
trim_ener_lib(lib, os.path.join(here, "ener_lib.cif"))
n_metal_rows = trim_metals(lib, os.path.join(here, "metals.cif"))
shutil.copyfile(os.path.join(lib, "COPYING"), os.path.join(here, "COPYING"))
print(f"{len(COMPOUNDS)} compounds, {n_links} links, {n_mods} modifications, {n_metal_rows} metal distances")
if __name__ == "__main__":
main()