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)
134 lines
6.3 KiB
Python
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()
|