OpenStructure
Loading...
Searching...
No Matches
stereochemistry.py
Go to the documentation of this file.
1"""
2.. note::
3
4 This is a new implementation of the stereochemistry checks, introduced in
5 OpenStructure 2.4, with support for nucleotides. The
6 :doc:`previous stereochemistry checks <stereochemistry_deprecated>` that come
7 with `Mariani et al. <https://dx.doi.org/10.1093/bioinformatics/btt473>`_ are
8 considered deprecated.
9"""
10
11import os
12import json
13import datetime
14
15import numpy as np
16
17import ost
18from ost import geom
19from ost import mol
20
21
23 """ Returns string to uniquely identify atom
24
25 format: <chain_name>.<resnum>.<resnum_inscode>.<atom_name>
26 """
27 r = a.GetResidue()
28 ch = r.GetChain()
29 num = r.number.num
30 ins_code = r.number.ins_code.strip("\u0000")
31 return f"{ch.name}.{r.number.num}.{ins_code}.{a.name}"
32
33
34def _PotentialDisulfid(a_one, a_two):
35 """ Returns whether two atoms can potentially build a disulfid bond
36
37 Assumes that they're from two distinct residues
38 """
39 if a_one.GetName() == "SG" and a_two.GetName() == "SG":
40 if a_one.GetResidue().GetName() == "CYS":
41 if a_two.GetResidue().GetName() == "CYS":
42 return True
43 return False
44
45
46def _GetAngles(bonds):
47 """ Returns list of angles based on bonds
48
49 Returns list of tuples, each tuple has three atom handles
50 representing angles
51 """
52 angles = list()
53 done = set()
54 for bond in bonds:
55 h1 = bond.first.GetHashCode()
56 h2 = bond.second.GetHashCode()
57 for a in bond.first.GetBondPartners():
58 h0 = a.GetHashCode()
59 if h0 != h2:
60 if ((h0, h1, h2)) not in done and (h2, h1, h0) not in done:
61 angles.append((a, bond.first, bond.second))
62 done.add((h0, h1, h2))
63 for a in bond.second.GetBondPartners():
64 h3 = a.GetHashCode()
65 if h3 != h1:
66 if ((h1, h2, h3)) not in done and (h3, h2, h1) not in done:
67 angles.append((bond.first, bond.second, a))
68 done.add((h1, h2, h3))
69 return angles
70
71
72def _GetResidueType(atoms):
73 """ Identifies type in StereoLinkData
74
75 :param atoms: Atoms that define a bond or angle
76 :type atoms: :class:`list` of :class:`AtomHandle`
77 :returns: :class:`str` with which the respective parameters can be
78 accessed in default stereo link data, None if no match is found
79 """
80 residues = [a.GetResidue().handle for a in atoms]
81 chem_types = list(set([str(r.GetChemType()) for r in residues]))
82
83 if len(chem_types) == 1 and chem_types[0] == 'N':
84 return "NA"
85 elif len(chem_types) == 1 and chem_types[0] == 'A':
86 # in both cases, bond or angle, there should be exactly two residues
87 # involved
88 tmp = list()
89 r_hashes = set()
90 for r in residues:
91 h = r.GetHashCode()
92 if h not in r_hashes:
93 r_hashes.add(h)
94 tmp.append(r)
95 residues = tmp
96 if len(residues) != 2:
97 return None
98
99 # need to be sorted
100 if residues[0].GetNumber() > residues[1].GetNumber():
101 r0 = residues[1]
102 r1 = residues[0]
103 else:
104 r0 = residues[0]
105 r1 = residues[1]
106
107 if r1.GetName() == "GLY":
108 return "GLY"
109 elif r1.GetName() == "PRO":
110 a = r0.FindAtom("CA")
111 b = r0.FindAtom("C")
112 c = r1.FindAtom("N")
113 d = r1.FindAtom("CA")
114 if a.IsValid() and b.IsValid() and c.IsValid() and d.IsValid():
115 omega = geom.DihedralAngle(a.GetPos(), b.GetPos(),
116 c.GetPos(), d.GetPos())
117 if abs(omega) < 1.57:
118 return "PRO_CIS"
119 else:
120 return "PRO_TRANS"
121 else:
122 return "PEPTIDE"
123
124 return None
125
126
128 """ Parse stereochemistry data for bonds
129
130 That is expected distances and standard deviations from a
131 :class:`gemmi.Document`. Concatenates results form all loops with tags:
132 _chem_comp_bond.comp_id, _chem_comp_bond.atom_id_1,
133 _chem_comp_bond.atom_id_2, _chem_comp_bond.value_dist,
134 _chem_comp_bond.value_dist_esd
135
136 :param doc: Gemmi doc representing cif file opened with
137 gemmi.cif.read_file(filepath)
138 :type doc: :class:`gemmi.Document`
139 :returns: :class:`dict` with one key per compound, the respective value
140 is again a dict with key f"{at_1}_{at_2}" and value
141 [dist, dist_std].
142 """
143 data = dict()
144 for block in doc:
145 comp_id = block.find_values("_chem_comp_bond.comp_id")
146 at_1 = block.find_values("_chem_comp_bond.atom_id_1")
147 at_2 = block.find_values("_chem_comp_bond.atom_id_2")
148 dist = block.find_values("_chem_comp_bond.value_dist")
149 dist_std = block.find_values("_chem_comp_bond.value_dist_esd")
150 if None not in [comp_id, at_1, at_2, dist, dist_std]:
151 for a, b, c, d, e in zip(comp_id, at_1, at_2, dist, dist_std):
152 if a not in data:
153 data[a] = dict()
154 key = '_'.join([b.strip('\"'), c.strip('\"')])
155 data[a][key] = [float(d), float(e)]
156 return data
157
158
160 """ Parse stereochemistry data for angles
161
162 That is expected distances and standard deviations from a
163 :class:`gemmi.Document`. Concatenates results form all loops with tags:
164 _chem_comp_angle.comp_id, _chem_comp_angle.atom_id_1,
165 _chem_comp_angle.atom_id_2, _chem_comp_angle.atom_id_2,
166 _chem_comp_angle.value_angle, _chem_comp_angle.value_angle_esd
167
168 :param doc: Gemmi doc representing cif file opened with
169 gemmi.cif.read_file(filepath)
170 :type doc: :class:`gemmi.Document`
171 :returns: :class:`dict` with one key per compound, the respective value
172 is again a dict with key f"{at_1}_{at_2}_{at_3}" and value
173 [angle, angle_std].
174 """
175 data = dict()
176 for block in doc:
177 comp_id = block.find_values("_chem_comp_angle.comp_id")
178 at_1 = block.find_values("_chem_comp_angle.atom_id_1")
179 at_2 = block.find_values("_chem_comp_angle.atom_id_2")
180 at_3 = block.find_values("_chem_comp_angle.atom_id_3")
181 angle = block.find_values("_chem_comp_angle.value_angle")
182 angle_std = block.find_values("_chem_comp_angle.value_angle_esd")
183 if None not in [comp_id, at_1, at_2, at_3, angle, angle_std]:
184 for a, b, c, d, e, f in zip(comp_id, at_1, at_2, at_3, angle,
185 angle_std):
186 if a not in data:
187 data[a] = dict()
188 key = '_'.join([b.strip('\"'), c.strip('\"'), d.strip('\"')])
189 data[a][key] = [float(e), float(f)]
190 return data
191
192
193def StereoDataFromMON_LIB(mon_lib_path, compounds=None):
194 """ Parses stereochemistry parameters from CCP4 MON_LIB
195
196 CCP4 `MON_LIB <https://www.ccp4.ac.uk/html/mon_lib.html>`_ contains
197 data on ideal bond lengths/angles for compounds.
198
199 Original data (several updates in the meantime) come from:
200
201 * Amino acid bond lengths and angles: Engh and Huber, Acta Cryst.
202 A47, 392-400 (1991).
203 * Purine and pyrimidine bond lengths and angles: O. Kennard & R. Taylor
204 (1982), J. Am. Soc. Chem. vol. 104, pp. 3209-3212.
205 * Sugar-phosphate backbone bond lengths and bond angles: W. Saenger’s
206 Principles of Nucleic Acid Structure (1983), Springer-Verlag, pp. 70,86.
207
208 This function adds a dependency to the
209 `gemmi <https://github.com/project-gemmi/gemmi/>`_ library to read cif
210 files.
211
212 :param mon_lib_path: Path to CCP4 MON_LIB
213 :type mon_lib_path: :class:`str`
214 :param compounds: Compounds to parse - parses proteinogenic amino acids
215 and nucleotides if not given.
216 :type compounds: :class:`list`
217 :returns: :class:`dict` with stereochemistry parameters
218 """
219 if compounds is None:
220 compounds = ['ALA', 'ARG', 'ASN', 'ASP', 'CYS', 'GLN', 'GLU', 'GLY',
221 'HIS', 'ILE', 'LEU', 'LYS', 'MET', 'MSE', 'PHE', 'PRO',
222 'SER', 'THR', 'TRP', 'TYR', 'VAL', 'DA', 'A', 'DC', 'C',
223 'DG', 'G', 'DU', 'U', 'DT', 'DI', 'I']
224
225 cif_paths = list()
226 for c in compounds:
227 p = os.path.join(mon_lib_path, c[0].lower(), c + ".cif")
228 if not os.path.exists(p):
229 raise RuntimeError(f"Tried to find cif file for compound {c} "
230 f"in specified MON_LIB ({mon_lib_path})."
231 f"Expected file ({p}) does not exist.")
232 cif_paths.append(p)
233
234 # hide import to avoid it as dependency for the whole module
235 from gemmi import cif
236 # construct return dict from first element and subsequently
237 # add the remainder
238 doc = cif.read_file(cif_paths[0])
239 data = {"bond_data": _ParseBondData(doc),
240 "angle_data": _ParseAngleData(doc)}
241 for cp in cif_paths[1:]:
242 doc = cif.read_file(cp)
243 bond_data = _ParseBondData(doc)
244 angle_data = _ParseAngleData(doc)
245 data["bond_data"].update(bond_data)
246 data["angle_data"].update(angle_data)
247
248 # add license info
249 copying_str = f"This data has been derived from the CCP4 MON_LIB on "
250 copying_str += f"{datetime.datetime.now()}. MON_LIB is licensed under "
251 copying_str += f"GNU LESSER GENERAL PUBLIC LICENSE Version 3. Consult the "
252 copying_str += f"latest CCP4 for the full license text."
253 data["COPYING"] = copying_str
254
255 return data
256
257
258def GetBondParam(a1, a2, stereo_data = None, stereo_link_data = None):
259 """ Returns mean and standard deviation for bond
260
261 :param a1: First atom that defines bond
262 :type a1: :class:`ost.mol.AtomView`/:class:`ost.mol.AtomHandle`
263 :param a2: Second atom that defines bond
264 :type a2: :class:`ost.mol.AtomView`/:class:`ost.mol.AtomHandle`
265 :param stereo_data: Stereochemistry data, use return value of
266 :func:`GetDefaultStereoData` if not given.
267 If you call this function repeatedly, you
268 really should provide *stereo_data*!
269 :type stereo_data: :class:`dict`
270 :param stereo_link_data: Stereochemistry data, use return value of
271 :func:`GetDefaultStereoLinkData` if not given.
272 If you call this function repeatedly, you
273 really should provide *stereo_link_data*!
274 :type stereo_link_data: :class:`dict`
275 :returns: :class:`tuple` with mean and standard deviation. Values are None
276 if respective bond is not found in *stereo_data*
277 """
278 if stereo_data is None:
279 stereo_data = GetDefaultStereoData()
280 if stereo_link_data is None:
281 stereo_link_data = GetDefaultStereoLinkData()
282
283 residue_data = None
284 if a1.GetResidue().GetHashCode() == a2.GetResidue().GetHashCode():
285 # intra residue case
286 rname = a1.GetResidue().GetName()
287 if rname in stereo_data["bond_data"]:
288 residue_data = stereo_data["bond_data"][rname]
289 else:
290 # inter residue case
291 residue_type = _GetResidueType([a1, a2])
292 if residue_type is not None:
293 residue_data = stereo_link_data["bond_data"][residue_type]
294
295 if residue_data is not None:
296 a1name = a1.GetName()
297 a2name = a2.GetName()
298 key = a1name + "_" + a2name
299 if key in residue_data:
300 return (residue_data[key][0], residue_data[key][1])
301 key = a2name + "_" + a1name
302 if key in residue_data:
303 return (residue_data[key][0], residue_data[key][1])
304
305 return (None, None)
306
307
308def GetAngleParam(a1, a2, a3, stereo_data = None, stereo_link_data = None):
309 """ Returns mean and standard deviation for angle
310
311 :param a1: First atom that defines angle
312 :type a1: :class:`ost.mol.AtomView`/:class:`ost.mol.AtomHandle`
313 :param a2: Second atom that defines angle
314 :type a2: :class:`ost.mol.AtomView`/:class:`ost.mol.AtomHandle`
315 :param a3: Third atom that defines angle
316 :type a3: :class:`ost.mol.AtomView`/:class:`ost.mol.AtomHandle`
317 :param stereo_data: Stereochemistry data, use return value of
318 :func:`GetDefaultStereoData` if not given.
319 If you call this function repeatedly, you
320 really should provide *stereo_data*!
321 :type stereo_data: :class:`dict`
322 :param stereo_link_data: Stereochemistry data, use return value of
323 :func:`GetDefaultStereoLinkData` if not given.
324 If you call this function repeatedly, you
325 really should provide *stereo_link_data*!
326 :type stereo_link_data: :class:`dict`
327 :returns: :class:`tuple` with mean and standard deviation. Values are None
328 if respective angle is not found in *stereo_data*
329 """
330 if stereo_data is None:
331 stereo_data = GetDefaultStereoData()
332 if stereo_link_data is None:
333 stereo_link_data = GetDefaultStereoLinkData()
334 h1 = a1.GetResidue().handle.GetHashCode()
335 h2 = a2.GetResidue().handle.GetHashCode()
336 h3 = a3.GetResidue().handle.GetHashCode()
337 residue_data = None
338 if h1 == h2 and h2 == h3:
339 # intra residue case
340 rname = a1.GetResidue().GetName()
341 if rname in stereo_data["angle_data"]:
342 residue_data = stereo_data["angle_data"][rname]
343 else:
344 # inter residue case
345 residue_type = _GetResidueType([a1, a2, a3])
346 if residue_type in stereo_link_data["angle_data"]:
347 residue_data = stereo_link_data["angle_data"][residue_type]
348
349 if residue_data is not None:
350 a1name = a1.GetName()
351 a2name = a2.GetName()
352 a3name = a3.GetName()
353 key = a1name + "_" + a2name + "_" + a3name
354 if key in residue_data:
355 return (residue_data[key][0], residue_data[key][1])
356 key = a3name + "_" + a2name + "_" + a1name
357 if key in residue_data:
358 return (residue_data[key][0], residue_data[key][1])
359 return (None, None)
360
361
363 """ Object to hold info on clashing atom
364
365 Constructor arguments are available as attributes:
366
367 * a (:class:`ost.mol.AtomHandle`)
368 """
369 def __init__(self, a):
370 self.a = a
371
372 def ToJSON(self):
373 """ Return JSON serializable dict
374
375 Clashing atom is represented by a string in format:
376 <chain_name>.<resnum>.<resnum_inscode>.<atom_name>
377 """
378 return {"a": _AtomToQualifiedName(self.a)}
379
380
382 """ Object to hold info on bond violation
383
384 Constructor arguments are available as attributes:
385
386 * a1 (:class:`ost.mol.AtomHandle`)
387 * a2 (:class:`ost.mol.AtomHandle`)
388 * length (:class:`float`)
389 * exp_length (:class:`float`)
390 * std (:class:`float`)
391 """
392 def __init__(self, a1, a2, length, exp_length, std):
393 self.a1 = a1
394 self.a2 = a2
395 self.length = length
396 self.exp_length = exp_length
397 self.std = std
398
399 def ToJSON(self, decimals = 3):
400 """ Return JSON serializable dict
401
402 Atoms are represented by a string in format:
403 <chain_name>.<resnum>.<resnum_inscode>.<atom_name>
404 """
405 return {"a1": _AtomToQualifiedName(self.a1),
406 "a2": _AtomToQualifiedName(self.a2),
407 "length": round(self.length, decimals),
408 "exp_length": round(self.exp_length, decimals),
409 "std": round(self.std, decimals)}
410
411
413 """ Object to hold info on angle violation
414
415 Constructor arguments are available as attributes:
416
417 * a1 (:class:`ost.mol.AtomHandle`)
418 * a2 (:class:`ost.mol.AtomHandle`)
419 * a3 (:class:`ost.mol.AtomHandle`)
420 * angle (:class:`float`)
421 * exp_angle (:class:`float`)
422 * std (:class:`float`)
423 """
424 def __init__(self, a1, a2, a3, angle, exp_angle, std):
425 self.a1 = a1
426 self.a2 = a2
427 self.a3 = a3
428 self.angle = angle
429 self.exp_angle = exp_angle
430 self.std = std
431
432 def ToJSON(self, decimals = 3):
433 """ Return JSON serializable dict
434
435 Atoms are represented by a string in format:
436 <chain_name>.<resnum>.<resnum_inscode>.<atom_name>
437 """
438 return {"a1": _AtomToQualifiedName(self.a1),
439 "a2": _AtomToQualifiedName(self.a2),
440 "a3": _AtomToQualifiedName(self.a3),
441 "angle": round(self.angle, decimals),
442 "exp_angle": round(self.exp_angle, decimals),
443 "std": round(self.std, decimals)}
444
445
446def GetClashes(ent, vdw_radii = None, tolerance = 1.5, disulfid_dist = 2.03,
447 disulfid_tolerance = 1.0):
448 """ Identifies clashing atoms
449
450 A clash between two non-bonded atoms is defined as their distance d being
451 below the sum of their vdw radii with some subtracted tolerance value.
452
453 The default values are not very sensitive.
454
455 :param ent: Entity for which you want to identify clashing atoms
456 :type ent: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
457 :param vdw_radii: Element based van der Waals radii. Only atoms of these
458 elements will be considered. If not given, default values
459 for all elements occuring in proteins/nucleotides are
460 used. Must be provided as :class:`dict`, where they key
461 are elements (capitalized) and value the respective radii
462 in Angstrom.
463 :type vdw_radii: :class:`dict`
464 :param tolerance: Tolerance value
465 :param disulfid_dist: Summed vdw radius that is used if two Sulfurs that can
466 potentially build a disulfid bond interact
467 :type disulfid_dist: :class:`float`
468 :param disulfid_tolerance: The respective tolerance
469 :type disulfid_dist: :class:`float`
470 :returns: A :class:`list` of :class:`ClashInfo`
471 """
472
473 if vdw_radii is None:
474 vdw_radii = {"C": 1.70, "N": 1.55, "O": 1.52, "P": 1.80, "S": 1.80}
475
476 for ele in vdw_radii.keys():
477 if ele.upper() != ele:
478 raise RuntimeError(f"Elements in vdw_radii must be upper case. "
479 f"Got {ele}")
480
481 # it would be elegant to just do a selection by the ele property. However,
482 # thats case sensitive. So the element could be Cl but the vdw radii
483 # are all caps.
484 elements = set([ele.upper() for ele in vdw_radii.keys()])
485 for a in ent.atoms:
486 if a.GetElement().upper() in elements:
487 a.SetIntProp("clash_check", 1)
488 clash_ent = ent.Select("gaclash_check:0=1")
489
490 max_radius = max(vdw_radii.values())
491 max_radius = max(max_radius, 0.5*disulfid_dist)
492 min_tolerance = min(tolerance, disulfid_tolerance)
493 radius = 2*max_radius-min_tolerance
494
495 return_list = list()
496 for a in clash_ent.atoms:
497 a_hash = a.handle.GetHashCode()
498 close_atoms = clash_ent.FindWithin(a.GetPos(), radius)
499 for ca in close_atoms:
500 ca_hash = ca.handle.GetHashCode()
501 if a_hash != ca_hash and not mol.BondExists(a.handle, ca.handle):
502 d = geom.Distance(a.GetPos(), ca.GetPos())
503 if _PotentialDisulfid(a, ca):
504 thresh = disulfid_dist - disulfid_tolerance
505 else:
506 thresh = vdw_radii[a.GetElement().upper()]
507 thresh += vdw_radii[ca.GetElement().upper()]
508 thresh -= tolerance
509 if d < thresh:
510 return_list.append(ClashInfo(a.handle))
511 break
512 return return_list
513
514
515def GetBadBonds(ent, stereo_data = None, stereo_link_data = None, tolerance=12):
516 """ Identify unrealistic bonds
517
518 :param ent: Entity for which you want to identify unrealistic bonds
519 :type ent: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
520 :param stereo_data: Stereochemistry data, use return value of
521 :func:`GetDefaultStereoData` if not given.
522 :type stereo_data: :class:`dict`
523 :param stereo_link_data: Stereochemistry data, use return value of
524 :func:`GetDefaultStereoLinkData` if not given.
525 :type stereo_link_data: :class:`dict`
526 :param tolerance: Bonds that devaiate more than *tolerance* times standard
527 deviation from expected mean are considered bad
528 :type tolerance: :class:`int`
529 :returns: :class:`list` :class:`BondViolationInfo`
530
531 """
532 if stereo_data is None:
533 stereo_data = GetDefaultStereoData()
534 if stereo_link_data is None:
535 stereo_link_data = GetDefaultStereoLinkData()
536 return_list = list()
537 for b in ent.bonds:
538 a1 = b.first
539 a2 = b.second
540 mean, std = GetBondParam(a1, a2, stereo_data = stereo_data,
541 stereo_link_data = stereo_link_data)
542 if None not in [mean, std]:
543 l = b.length
544 if abs(mean-l) > tolerance*std:
545 return_list.append(BondViolationInfo(a1, a2, l, mean, std))
546 return return_list
547
548
549def GetBadAngles(ent, stereo_data = None, stereo_link_data = None,
550 tolerance = 12):
551 """ Identify unrealistic angles
552
553 :param ent: Entity for which you want to identify unrealistic angles
554 :type ent: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
555 :param stereo_data: Stereochemistry data, use return value of
556 :func:`GetDefaultStereoData` if not given.
557 :type stereo_data: :class:`dict`
558 :param stereo_link_data: Stereochemistry data, use return value of
559 :func:`GetDefaultStereoLinkData` if not given.
560 :type stereo_link_data: :class:`dict`
561 :param tolerance: Angles that devaiate more than *tolerance* times standard
562 deviation from expected mean are considered bad
563 :type tolerance: :class:`int`
564 :returns: :class:`list` of :class:`AngleViolationInfo`
565 """
566 if stereo_data is None:
567 stereo_data = GetDefaultStereoData()
568 if stereo_link_data is None:
569 stereo_link_data = GetDefaultStereoLinkData()
570 return_list = list()
571 for a in _GetAngles(ent.bonds):
572 mean, std = GetAngleParam(a[0], a[1], a[2], stereo_data = stereo_data,
573 stereo_link_data = stereo_link_data)
574 if None not in [mean, std]:
575 angle = geom.Angle(a[0].GetPos() - a[1].GetPos(),
576 a[2].GetPos() - a[1].GetPos())
577 angle = angle/np.pi*180 # stereo params are in degrees
578 diff = abs(mean-angle)
579 if diff > tolerance*std:
580 return_list.append(AngleViolationInfo(a[0], a[1], a[2], angle,
581 mean, std))
582 return return_list
583
584
585def StereoCheck(ent, stereo_data = None, stereo_link_data = None):
586 """ Remove atoms with stereochemical problems
587
588 Selects for peptide/nucleotides and calls :func:`GetClashes`,
589 :func:`GetBadBonds` and :func:`GetBadAngles` with default
590 parameters.
591
592 * Amino acids: Remove full residue if backbone atom is involved in
593 stereochemistry issue ("N", "CA", "C", "O"). Remove sidechain if any of
594 the sidechain atoms is involved in stereochemistry issues.
595 * Nucleotides: Remove full residue if backbone atom is involved in
596 stereochemistry issue ("P", "OP1", "OP2", "OP3", "O5'", "C5'", "C4'",
597 "C3'", "C2'", "C1'", "O4'", "O3'", "O2'"). Remove sidechain (base) if any
598 of the sidechain atoms is involved in stereochemistry issues.
599
600 :param ent: Entity to be stereochecked
601 :type ent: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
602 :param stereo_data: Stereochemistry data, use return value of
603 :func:`GetDefaultStereoData` if not given.
604 :type stereo_data: :class:`dict`
605 :param stereo_link_data: Stereochemistry data, use return value of
606 :func:`GetDefaultStereoLinkData` if not given.
607 :type stereo_link_data: :class:`dict`
608 :returns: Tuple with four elements: 1) :class:`ost.mol.EntityView` of
609 *ent* processed as described above 2) Return value of
610 :func:`GetClashes` 3) return value of :func:`GetBadBonds`
611 4) return value of :func:`GetBadAngles`
612 """
613 if stereo_data is None:
614 stereo_data = GetDefaultStereoData()
615
616 sel = ent.Select("peptide=true or nucleotide=true")
617 clashes = GetClashes(sel)
618 bad_bonds = GetBadBonds(sel, stereo_data = stereo_data)
619 bad_angles = GetBadAngles(sel, stereo_data = stereo_data)
620
621 # set stereo problems as properties on an atom level
622 for clash in clashes:
623 clash.a.SetIntProp("stereo_problem", 1)
624
625 for bond in bad_bonds:
626 bond.a1.SetIntProp("stereo_problem", 1)
627 bond.a2.SetIntProp("stereo_problem", 1)
628
629 for angle in bad_angles:
630 angle.a1.SetIntProp("stereo_problem", 1)
631 angle.a2.SetIntProp("stereo_problem", 1)
632 angle.a3.SetIntProp("stereo_problem", 1)
633
634 # set stereo problems as properties on a residue level
635 bad_ent = ent.Select("gastereo_problem:0=1")
636 if len(bad_ent.residues) > 0:
637 pep_bb = set(["N", "CA", "C", "O"])
638 nuc_bb = set(["P", "OP1", "OP2", "OP3", "O5'", "C5'", "C4'", "C3'",
639 "C2'", "C1'", "O4'", "O3'", "O2'"])
640
641 for r in bad_ent.residues:
642 bad_atoms = set([a.GetName() for a in r.atoms])
643 r.SetIntProp("stereo_problem", 1)
644 if r.GetChemType() == mol.ChemType.NUCLEOTIDES:
645 if len(nuc_bb.intersection(bad_atoms)) > 0:
646 r.SetIntProp("stereo_problem_bb", 1)
647 elif r.GetChemType() == mol.ChemType.AMINOACIDS:
648 if len(pep_bb.intersection(bad_atoms)) > 0:
649 r.SetIntProp("stereo_problem_bb", 1)
650
651 # explicitely add " as OpenStructure query language would not
652 # understand ' otherwise
653 nuc_bb = [f"\"{name}\"" for name in nuc_bb]
654
655 pep_query = f"(peptide=true and grstereo_problem:0=0) or "
656 pep_query += f"(peptide=true and grstereo_problem_bb:0=0 and "
657 pep_query += f"aname={','.join(pep_bb)})"
658 nuc_query = f"(nucleotide=true and grstereo_problem:0=0) or "
659 nuc_query += f"(nucleotide=true and grstereo_problem_bb:0=0 and "
660 nuc_query += f"aname={','.join(nuc_bb)})"
661 query = pep_query + " or " + nuc_query
662 return_view = sel.Select(query)
663 else:
664 return_view = sel
665
666 return return_view, clashes, bad_bonds, bad_angles
667
668
670 """ Get default stereo data derived from CCP4 MON_LIB
671
672 Used as default if not provided in :func:`GetBadBonds`, :func:`GetBadAngles`
673 and :func:`StereoCheck`.
674
675 MON_LIB is licensed under GNU LESSER GENERAL PUBLIC LICENSE Version 3.
676 Consult the latest CCP4 for the full license text.
677 """
678 data_path = os.path.join(ost.GetSharedDataPath(), "stereo_data.json")
679 with open(data_path, 'r') as fh:
680 return json.load(fh)
681
682
684 """ Get default stereo data for links between compounds
685
686 Hardcoded from arbitrary sources, see comments in the code.
687
688 :returns: Data for peptide bonds, nucleotide links and disulfid bonds that
689 are used as default if not provided in :func:`GetBadBonds`,
690 :func:`GetBadAngles` and :func:`StereoCheck`.
691 """
692 data = {"bond_data": dict(),
693 "angle_data": dict()}
694
695 # data for nucleotides - deliberately stolen from
696 # geostd (https://github.com/phenix-project/geostd) which is basically
697 # the Phenix equivalent for MON_LIB
698 # used file: $GEOSTD_DIR/rna_dna/chain_link_rna2p.cif
699 # Reason to not use the same data origin as peptides is that in CCP4
700 # there is a bit a more fine grained differentiation of NA types
701 # which makes things more complicated.
702 data["bond_data"]["NA"] = dict()
703 data["bond_data"]["NA"]["O3'_P"] = [1.607, 0.015]
704
705 data["angle_data"]["NA"] = dict()
706 data["angle_data"]["NA"]["O3'_P_O5'"] = [104.000, 1.500]
707 data["angle_data"]["NA"]["O3'_P_OP1"] = [108.000, 3.000]
708 data["angle_data"]["NA"]["O3'_P_OP2"] = [108.000, 3.000]
709 data["angle_data"]["NA"]["C3'_O3'_P"] = [120.200, 1.500]
710
711 # data for peptides - deliberately stolen from standard_geometry.cif file
712 # which is shipped with CCP4
713 # (_standard_geometry.version "Fri Feb 22 17:25:15 GMT 2013").
714 data["bond_data"]["PEPTIDE"] = dict()
715 data["bond_data"]["PEPTIDE"]["C_N"] = [1.336, 0.023]
716 data["bond_data"]["PEPTIDE"]["SG_SG"] = [2.033, 0.016]
717
718 data["bond_data"]["GLY"] = dict()
719 data["bond_data"]["GLY"]["C_N"] = [1.326, 0.018]
720
721 data["bond_data"]["PRO_CIS"] = dict()
722 data["bond_data"]["PRO_CIS"]["C_N"] = [1.338, 0.019]
723 data["bond_data"]["PRO_TRANS"] = dict()
724 data["bond_data"]["PRO_TRANS"]["C_N"] = [1.338, 0.019]
725
726 data["angle_data"]["PEPTIDE"] = dict()
727 data["angle_data"]["PEPTIDE"]["CA_C_N"] = [117.2, 2.2]
728 data["angle_data"]["PEPTIDE"]["O_C_N"] = [122.7, 1.6]
729 data["angle_data"]["PEPTIDE"]["C_N_CA"] = [121.7, 2.5]
730
731 data["angle_data"]["GLY"] = dict()
732 data["angle_data"]["GLY"]["CA_C_N"] = [116.2, 2.0]
733 data["angle_data"]["GLY"]["O_C_N"] = [123.2, 1.7]
734 data["angle_data"]["GLY"]["C_N_CA"] = [122.3, 2.1]
735
736 data["angle_data"]["PRO_TRANS"] = dict()
737 data["angle_data"]["PRO_TRANS"]["CA_C_N"] = [117.1, 2.8]
738 data["angle_data"]["PRO_TRANS"]["O_C_N"] = [121.1, 1.9]
739 data["angle_data"]["PRO_TRANS"]["C_N_CA"] = [119.3, 1.5]
740 data["angle_data"]["PRO_TRANS"]["C_N_CD"] = [128.4, 2.1]
741
742 data["angle_data"]["PRO_CIS"] = dict()
743 data["angle_data"]["PRO_CIS"]["CA_C_N"] = [117.1, 2.8]
744 data["angle_data"]["PRO_CIS"]["O_C_N"] = [121.1, 1.9]
745 data["angle_data"]["PRO_CIS"]["C_N_CA"] = [127.0, 2.4]
746 data["angle_data"]["PRO_CIS"]["C_N_CD"] = [120.6, 2.2]
747
748 return data
__init__(self, a1, a2, a3, angle, exp_angle, std)
__init__(self, a1, a2, length, exp_length, std)
Real DLLEXPORT_OST_GEOM Angle(const Line2 &l1, const Line2 &l2)
Real DihedralAngle(const Vec3 &p1, const Vec3 &p2, const Vec3 &p3, const Vec3 &p4)
Get dihedral angle for p1-p2-p3-p4.
Real DLLEXPORT_OST_GEOM Distance(const Line2 &l, const Vec2 &v)
GetClashes(ent, vdw_radii=None, tolerance=1.5, disulfid_dist=2.03, disulfid_tolerance=1.0)
GetBadBonds(ent, stereo_data=None, stereo_link_data=None, tolerance=12)
GetBondParam(a1, a2, stereo_data=None, stereo_link_data=None)
StereoCheck(ent, stereo_data=None, stereo_link_data=None)
GetBadAngles(ent, stereo_data=None, stereo_link_data=None, tolerance=12)
StereoDataFromMON_LIB(mon_lib_path, compounds=None)
GetAngleParam(a1, a2, a3, stereo_data=None, stereo_link_data=None)
String DLLEXPORT_OST_BASE GetSharedDataPath()