OpenStructure
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
scoring.py
Go to the documentation of this file.
1 import os
2 from ost import mol
3 from ost import seq
4 from ost import io
5 from ost import conop
6 from ost import settings
7 from ost import geom
8 from ost.mol.alg import lddt
9 from ost.mol.alg import qsscore
10 from ost.mol.alg import chain_mapping
11 from ost.mol.alg import stereochemistry
12 from ost.mol.alg import dockq
13 from ost.mol.alg.lddt import lDDTScorer
14 from ost.mol.alg.qsscore import QSScorer
15 from ost.mol.alg.contact_score import ContactScorer
16 from ost.mol.alg import Molck, MolckSettings
17 from ost import bindings
18 from ost.bindings import cadscore
19 from ost.bindings import tmtools
20 import numpy as np
21 
23  """Scorer specific for a reference/model pair
24 
25  Finds best possible binding site representation of reference in model given
26  lDDT score. Uses :class:`ost.mol.alg.chain_mapping.ChainMapper` to deal with
27  chain mapping.
28 
29  :param reference: Reference structure
30  :type reference: :class:`ost.mol.EntityView`/:class:`ost.mol.EntityHandle`
31  :param model: Model structure
32  :type model: :class:`ost.mol.EntityView`/:class:`ost.mol.EntityHandle`
33  :param residue_number_alignment: Passed to ChainMapper constructor
34  :type residue_number_alignment: :class:`bool`
35  """
36  def __init__(self, reference, model,
37  residue_number_alignment=False):
39  resnum_alignments=residue_number_alignment)
40  self.ref = self.chain_mapper.target
41  self.mdl = model
42 
43  def ScoreBS(self, ligand, radius = 4.0, lddt_radius=10.0):
44  """Computes binding site lDDT score given *ligand*. Best possible
45  binding site representation is selected by lDDT but other scores such as
46  CA based RMSD and GDT are computed too and returned.
47 
48  :param ligand: Defines the scored binding site, i.e. provides positions
49  to perform proximity search
50  :type ligand: r'((Residue)|(Chain)|(Entity))((View)|(Handle))'
51  :param radius: Reference residues with any atom position within *radius*
52  of *ligand* consitute the scored binding site
53  :type radius: :class:`float`
54  :param lddt_radius: Passed as *inclusion_radius* to
55  :class:`ost.mol.alg.lddt.lDDTScorer`
56  :type lddt_radius: :class:`float`
57  :returns: Object of type :class:`ost.mol.alg.chain_mapping.ReprResult`
58  containing all atom lDDT score and mapping information.
59  None if no representation could be found.
60  """
61 
62  # create view of reference binding site
63  ref_residues_hashes = set() # helper to keep track of added residues
64  for ligand_at in ligand.atoms:
65  close_atoms = self.ref.FindWithin(ligand_at.GetPos(), radius)
66  for close_at in close_atoms:
67  ref_res = close_at.GetResidue()
68  h = ref_res.handle.GetHashCode()
69  if h not in ref_residues_hashes:
70  ref_residues_hashes.add(h)
71 
72  # reason for doing that separately is to guarantee same ordering of
73  # residues as in underlying entity. (Reorder by ResNum seems only
74  # available on ChainHandles)
75  ref_bs = self.ref.CreateEmptyView()
76  for ch in self.ref.chains:
77  for r in ch.residues:
78  if r.handle.GetHashCode() in ref_residues_hashes:
79  ref_bs.AddResidue(r, mol.ViewAddFlag.INCLUDE_ALL)
80 
81  # gogogo
82  bs_repr = self.chain_mapper.GetRepr(ref_bs, self.mdl,
83  inclusion_radius = lddt_radius)
84  if len(bs_repr) >= 1:
85  return bs_repr[0]
86  else:
87  return None
88 
89 
90 class Scorer:
91  """ Helper class to access the various scores available from ost.mol.alg
92 
93  Deals with structure cleanup, chain mapping, interface identification etc.
94  Intermediate results are available as attributes.
95 
96  :param model: Model structure - a deep copy is available as :attr:`model`.
97  Additionally, :func:`ost.mol.alg.Molck` using *molck_settings*
98  is applied.
99  :type model: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
100  :param target: Target structure - a deep copy is available as :attr:`target`.
101  Additionally, :func:`ost.mol.alg.Molck` using *molck_settings*
102  is applied.
103  :type target: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
104  :param resnum_alignments: Whether alignments between chemically equivalent
105  chains in *model* and *target* can be computed
106  based on residue numbers. This can be assumed in
107  benchmarking setups such as CAMEO/CASP.
108  :type resnum_alignments: :class:`bool`
109  :param molck_settings: Settings used for Molck on *model* and *target*, if
110  set to None, a default object is constructed by
111  setting everything except rm_zero_occ_atoms and
112  colored to True in
113  :class:`ost.mol.alg.MolckSettings` constructor.
114  :type molck_settings: :class:`ost.mol.alg.MolckSettings`
115  :param cad_score_exec: Explicit path to voronota-cadscore executable from
116  voronota installation from
117  https://github.com/kliment-olechnovic/voronota. If
118  not given, voronota-cadscore must be in PATH if any
119  of the CAD score related attributes is requested.
120  :type cad_score_exec: :class:`str`
121  :param custom_mapping: Provide custom chain mapping between *model* and
122  *target*. Dictionary with target chain names as key
123  and model chain names as value.
124  :type custom_mapping: :class:`dict`
125  :param usalign_exec: Explicit path to USalign executable used to compute
126  TM-score. If not given, TM-score will be computed
127  with OpenStructure internal copy of USalign code.
128  :type usalign_exec: :class:`str`
129  :param lddt_no_stereochecks: Whether to compute lDDT without stereochemistry
130  checks
131  :type lddt_no_stereochecks: :class:`bool`
132  :param n_max_naive: Parameter for chain mapping. If the number of possible
133  mappings is <= *n_max_naive*, the full
134  mapping solution space is enumerated to find the
135  the optimum. A heuristic is used otherwise. The default
136  of 40320 corresponds to an octamer (8! = 40320).
137  A structure with stoichiometry A6B2 would be
138  6!*2! = 1440 etc.
139  :type n_max_naive: :class:`int`
140  :param oum: Override USalign Mapping. Inject mapping of :class:`Scorer`
141  object into USalign to compute TM-score. Experimental feature
142  with limitations.
143  :type oum: :class:`bool`
144  """
145  def __init__(self, model, target, resnum_alignments=False,
146  molck_settings = None, cad_score_exec = None,
147  custom_mapping=None, usalign_exec = None,
148  lddt_no_stereochecks=False, n_max_naive=40320,
149  oum=False):
150 
151  self._target_orig = target
152  self._model_orig = model
153 
154  if isinstance(self._model_orig, mol.EntityView):
155  self._model = mol.CreateEntityFromView(self._model_orig, False)
156  else:
157  self._model = self._model_orig.Copy()
158 
159  if isinstance(self._target_orig, mol.EntityView):
160  self._target = mol.CreateEntityFromView(self._target_orig, False)
161  else:
162  self._target = self._target_orig.Copy()
163 
164  if molck_settings is None:
165  molck_settings = MolckSettings(rm_unk_atoms=True,
166  rm_non_std=False,
167  rm_hyd_atoms=True,
168  rm_oxt_atoms=True,
169  rm_zero_occ_atoms=False,
170  colored=False,
171  map_nonstd_res=True,
172  assign_elem=True)
173  Molck(self._model, conop.GetDefaultLib(), molck_settings)
174  Molck(self._target, conop.GetDefaultLib(), molck_settings)
175  self._model = self._model.Select("peptide=True or nucleotide=True")
176  self._target = self._target.Select("peptide=True or nucleotide=True")
177 
178  # catch models which have empty chain names
179  for ch in self._model.chains:
180  if ch.GetName().strip() == "":
181  raise RuntimeError("Model chains must have valid chain names")
182 
183  # catch targets which have empty chain names
184  for ch in self._target.chains:
185  if ch.GetName().strip() == "":
186  raise RuntimeError("Target chains must have valid chain names")
187 
188  if resnum_alignments:
189  # In case of resnum_alignments, we have some requirements on
190  # residue numbers in the chain mapping: 1) no ins codes 2) strictly
191  # increasing residue numbers.
192  for ch in self._model.chains:
193  ins_codes = [r.GetNumber().GetInsCode() for r in ch.residues]
194  if len(set(ins_codes)) != 1 or ins_codes[0] != '\0':
195  raise RuntimeError("Residue numbers in each model chain "
196  "must not contain insertion codes if "
197  "resnum_alignments are enabled")
198  nums = [r.GetNumber().GetNum() for r in ch.residues]
199  if not all(i < j for i, j in zip(nums, nums[1:])):
200  raise RuntimeError("Residue numbers in each model chain "
201  "must be strictly increasing if "
202  "resnum_alignments are enabled")
203 
204  for ch in self._target.chains:
205  ins_codes = [r.GetNumber().GetInsCode() for r in ch.residues]
206  if len(set(ins_codes)) != 1 or ins_codes[0] != '\0':
207  raise RuntimeError("Residue numbers in each target chain "
208  "must not contain insertion codes if "
209  "resnum_alignments are enabled")
210  nums = [r.GetNumber().GetNum() for r in ch.residues]
211  if not all(i < j for i, j in zip(nums, nums[1:])):
212  raise RuntimeError("Residue numbers in each target chain "
213  "must be strictly increasing if "
214  "resnum_alignments are enabled")
215 
216  if usalign_exec is not None:
217  if not os.path.exists(usalign_exec):
218  raise RuntimeError(f"USalign exec ({usalign_exec}) "
219  f"not found")
220  if not os.access(usalign_exec, os.X_OK):
221  raise RuntimeError(f"USalign exec ({usalign_exec}) "
222  f"is not executable")
223 
224  self.resnum_alignments = resnum_alignments
225  self.cad_score_exec = cad_score_exec
226  self.usalign_exec = usalign_exec
227  self.lddt_no_stereochecks = lddt_no_stereochecks
228  self.n_max_naive = n_max_naive
229  self.oum = oum
230 
231  # lazily evaluated attributes
232  self._stereochecked_model = None
233  self._stereochecked_target = None
234  self._model_clashes = None
235  self._model_bad_bonds = None
236  self._model_bad_angles = None
237  self._target_clashes = None
238  self._target_bad_bonds = None
239  self._target_bad_angles = None
240  self._chain_mapper = None
241  self._mapping = None
242  self._model_interface_residues = None
243  self._target_interface_residues = None
244  self._aln = None
245  self._stereochecked_aln = None
246  self._pepnuc_aln = None
247 
248  # lazily constructed scorer objects
249  self._lddt_scorer = None
250  self._bb_lddt_scorer = None
251  self._qs_scorer = None
252  self._contact_scorer = None
253 
254  # lazily computed scores
255  self._lddt = None
256  self._local_lddt = None
257  self._bb_lddt = None
258  self._bb_local_lddt = None
259 
260  self._qs_global = None
261  self._qs_best = None
262  self._qs_target_interfaces = None
263  self._qs_model_interfaces = None
264  self._qs_interfaces = None
265  self._per_interface_qs_global = None
266  self._per_interface_qs_best = None
267 
268  self._contact_target_interfaces = None
269  self._contact_model_interfaces = None
270  self._native_contacts = None
271  self._model_contacts = None
272  self._ics_precision = None
273  self._ics_recall = None
274  self._ics = None
275  self._per_interface_ics_precision = None
276  self._per_interface_ics_recall = None
277  self._per_interface_ics = None
278  self._ips_precision = None
279  self._ips_recall = None
280  self._ips = None
281  self._per_interface_ics_precision = None
282  self._per_interface_ics_recall = None
283  self._per_interface_ics = None
284 
285  self._dockq_target_interfaces = None
286  self._dockq_interfaces = None
287  self._fnat = None
288  self._fnonnat = None
289  self._irmsd = None
290  self._lrmsd = None
291  self._nnat = None
292  self._nmdl = None
293  self._dockq_scores = None
294  self._dockq_ave = None
295  self._dockq_wave = None
296  self._dockq_ave_full = None
297  self._dockq_wave_full = None
298 
299  self._mapped_target_pos = None
300  self._mapped_model_pos = None
302  self._n_target_not_mapped = None
303  self._transform = None
304  self._gdtts = None
305  self._gdtha = None
306  self._rmsd = None
307 
308  self._cad_score = None
309  self._local_cad_score = None
310 
311  self._patch_qs = None
312  self._patch_dockq = None
313 
314  self._tm_score = None
315  self._usalign_mapping = None
316 
317  if custom_mapping is not None:
318  self._set_custom_mapping(custom_mapping)
319 
320  @property
321  def model(self):
322  """ Model with Molck cleanup
323 
324  :type: :class:`ost.mol.EntityHandle`
325  """
326  return self._model
327 
328  @property
329  def model_orig(self):
330  """ The original model passed at object construction
331 
332  :type: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
333  """
334  return self._model_orig
335 
336  @property
337  def target(self):
338  """ Target with Molck cleanup
339 
340  :type: :class:`ost.mol.EntityHandle`
341  """
342  return self._target
343 
344  @property
345  def target_orig(self):
346  """ The original target passed at object construction
347 
348  :type: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
349  """
350  return self._target_orig
351 
352  @property
353  def aln(self):
354  """ Alignments of :attr:`model`/:attr:`target` chains
355 
356  Alignments for each pair of chains mapped in :attr:`mapping`.
357  First sequence is target sequence, second sequence the model sequence.
358 
359  :type: :class:`list` of :class:`ost.seq.AlignmentHandle`
360  """
361  if self._aln is None:
362  self._compute_aln()
363  return self._aln
364 
365  @property
366  def stereochecked_aln(self):
367  """ Stereochecked equivalent of :attr:`aln`
368 
369  The alignments may differ, as stereochecks potentially remove residues
370 
371  :type: :class:`list` of :class:`ost.seq.AlignmentHandle`
372  """
373  if self._stereochecked_aln is None:
375  return self._stereochecked_aln
376 
377  @property
378  def pepnuc_aln(self):
379  """ Alignments of :attr:`model_orig`/:attr:`target_orig` chains
380 
381  Selects for peptide and nucleotide residues before sequence
382  extraction. Includes residues that would be removed by molck in
383  structure preprocessing.
384 
385  :type: :class:`list` of :class:`ost.seq.AlignmentHandle`
386  """
387  if self._pepnuc_aln is None:
388  self._compute_pepnuc_aln()
389  return self._pepnuc_aln
390 
391  @property
393  """ View of :attr:`~model` that has stereochemistry checks applied
394 
395  First, a selection for peptide/nucleotide residues is performed,
396  secondly peptide sidechains with stereochemical irregularities are
397  removed (full residue if backbone atoms are involved). Irregularities
398  are clashes or bond lengths/angles more than 12 standard deviations
399  from expected values.
400 
401  :type: :class:`ost.mol.EntityView`
402  """
403  if self._stereochecked_model is None:
404  self._do_stereochecks()
405  return self._stereochecked_model
406 
407  @property
408  def model_clashes(self):
409  """ Clashing model atoms
410 
411  :type: :class:`list` of :class:`ost.mol.alg.stereochemistry.ClashInfo`
412  """
413  if self._model_clashes is None:
414  self._do_stereochecks()
415  return self._model_clashes
416 
417  @property
418  def model_bad_bonds(self):
419  """ Model bonds with unexpected stereochemistry
420 
421  :type: :class:`list` of
422  :class:`ost.mol.alg.stereochemistry.BondViolationInfo`
423  """
424  if self._model_bad_bonds is None:
425  self._do_stereochecks()
426  return self._model_bad_bonds
427 
428  @property
429  def model_bad_angles(self):
430  """ Model angles with unexpected stereochemistry
431 
432  :type: :class:`list` of
433  :class:`ost.mol.alg.stereochemistry.AngleViolationInfo`
434  """
435  if self._model_bad_angles is None:
436  self._do_stereochecks()
437  return self._model_bad_angles
438 
439  @property
441  """ Same as :attr:`~stereochecked_model` for :attr:`~target`
442 
443  :type: :class:`ost.mol.EntityView`
444  """
445  if self._stereochecked_target is None:
446  self._do_stereochecks()
447  return self._stereochecked_target
448 
449  @property
450  def target_clashes(self):
451  """ Clashing target atoms
452 
453  :type: :class:`list` of :class:`ost.mol.alg.stereochemistry.ClashInfo`
454  """
455  if self._target_clashes is None:
456  self._do_stereochecks()
457  return self._target_clashes
458 
459  @property
460  def target_bad_bonds(self):
461  """ Target bonds with unexpected stereochemistry
462 
463  :type: :class:`list` of
464  :class:`ost.mol.alg.stereochemistry.BondViolationInfo`
465  """
466  if self._target_bad_bonds is None:
467  self._do_stereochecks()
468  return self._target_bad_bonds
469 
470  @property
471  def target_bad_angles(self):
472  """ Target angles with unexpected stereochemistry
473 
474  :type: :class:`list` of
475  :class:`ost.mol.alg.stereochemistry.AngleViolationInfo`
476  """
477  if self._target_bad_angles is None:
478  self._do_stereochecks()
479  return self._target_bad_angles
480 
481  @property
482  def chain_mapper(self):
483  """ Chain mapper object for given :attr:`target`
484 
485  :type: :class:`ost.mol.alg.chain_mapping.ChainMapper`
486  """
487  if self._chain_mapper is None:
489  n_max_naive=1e9,
490  resnum_alignments=self.resnum_alignments)
491  return self._chain_mapper
492 
493  @property
494  def mapping(self):
495  """ Full chain mapping result for :attr:`target`/:attr:`model`
496 
497  :type: :class:`ost.mol.alg.chain_mapping.MappingResult`
498  """
499  if self._mapping is None:
500  self._mapping = \
501  self.chain_mapper.GetMapping(self.model,
502  n_max_naive = self.n_max_naive)
503  return self._mapping
504 
505  @property
507  """ Interface residues in :attr:`~model`
508 
509  Thats all residues having a contact with at least one residue from
510  another chain (CB-CB distance <= 8A, CA in case of Glycine)
511 
512  :type: :class:`dict` with chain names as key and and :class:`list`
513  with residue numbers of the respective interface residues.
514  """
515  if self._model_interface_residues is None:
517  self._get_interface_residues(self.model)
518  return self._model_interface_residues
519 
520  @property
522  """ Same as :attr:`~model_interface_residues` for :attr:`~target`
523 
524  :type: :class:`dict` with chain names as key and and :class:`list`
525  with residue numbers of the respective interface residues.
526  """
527  if self._target_interface_residues is None:
530  return self._target_interface_residues
531 
532  @property
533  def lddt_scorer(self):
534  """ lDDT scorer for :attr:`~stereochecked_target` (default parameters)
535 
536  :type: :class:`ost.mol.alg.lddt.lDDTScorer`
537  """
538  if self._lddt_scorer is None:
539  if self.lddt_no_stereochecks:
540  self._lddt_scorer = lDDTScorer(self.target)
541  else:
543  return self._lddt_scorer
544 
545  @property
546  def bb_lddt_scorer(self):
547  """ Backbone only lDDT scorer for :attr:`~target`
548 
549  No stereochecks applied for bb only lDDT which considers CA atoms
550  for peptides and C3' atoms for nucleotides.
551 
552  :type: :class:`ost.mol.alg.lddt.lDDTScorer`
553  """
554  if self._bb_lddt_scorer is None:
555  self._bb_lddt_scorer = lDDTScorer(self.target, bb_only=True)
556  return self._bb_lddt_scorer
557 
558  @property
559  def qs_scorer(self):
560  """ QS scorer constructed from :attr:`~mapping`
561 
562  The scorer object is constructed with default parameters and relates to
563  :attr:`~model` and :attr:`~target` (no stereochecks).
564 
565  :type: :class:`ost.mol.alg.qsscore.QSScorer`
566  """
567  if self._qs_scorer is None:
568  self._qs_scorer = QSScorer.FromMappingResult(self.mapping)
569  return self._qs_scorer
570 
571  @property
572  def contact_scorer(self):
573  if self._contact_scorer is None:
574  self._contact_scorer = ContactScorer.FromMappingResult(self.mapping)
575  return self._contact_scorer
576 
577 
578  @property
579  def lddt(self):
580  """ Global lDDT score in range [0.0, 1.0]
581 
582  Computed based on :attr:`~stereochecked_model`. In case of oligomers,
583  :attr:`~mapping` is used.
584 
585  :type: :class:`float`
586  """
587  if self._lddt is None:
588  self._compute_lddt()
589  return self._lddt
590 
591  @property
592  def local_lddt(self):
593  """ Per residue lDDT scores in range [0.0, 1.0]
594 
595  Computed based on :attr:`~stereochecked_model` but scores for all
596  residues in :attr:`~model` are reported. If a residue has been removed
597  by stereochemistry checks, the respective score is set to 0.0. If a
598  residue is not covered by the target or is in a chain skipped by the
599  chain mapping procedure (happens for super short chains), the respective
600  score is set to None. In case of oligomers, :attr:`~mapping` is used.
601 
602  :type: :class:`dict`
603  """
604  if self._local_lddt is None:
605  self._compute_lddt()
606  return self._local_lddt
607 
608  @property
609  def bb_lddt(self):
610  """ Backbone only global lDDT score in range [0.0, 1.0]
611 
612  Computed based on :attr:`~model` on backbone atoms only. This is CA for
613  peptides and C3' for nucleotides. No stereochecks are performed. In case
614  of oligomers, :attr:`~mapping` is used.
615 
616  :type: :class:`float`
617  """
618  if self._bb_lddt is None:
619  self._compute_bb_lddt()
620  return self._bb_lddt
621 
622  @property
623  def bb_local_lddt(self):
624  """ Backbone only per residue lDDT scores in range [0.0, 1.0]
625 
626  Computed based on :attr:`~model` on backbone atoms only. This is CA for
627  peptides and C3' for nucleotides. No stereochecks are performed. If a
628  residue is not covered by the target or is in a chain skipped by the
629  chain mapping procedure (happens for super short chains), the respective
630  score is set to None. In case of oligomers, :attr:`~mapping` is used.
631 
632  :type: :class:`dict`
633  """
634  if self._bb_local_lddt is None:
635  self._compute_bb_lddt()
636  return self._bb_local_lddt
637 
638  @property
639  def qs_global(self):
640  """ Global QS-score
641 
642  Computed based on :attr:`model` using :attr:`mapping`
643 
644  :type: :class:`float`
645  """
646  if self._qs_global is None:
647  self._compute_qs()
648  return self._qs_global
649 
650  @property
651  def qs_best(self):
652  """ Global QS-score - only computed on aligned residues
653 
654  Computed based on :attr:`model` using :attr:`mapping`. The QS-score
655  computation only considers contacts between residues with a mapping
656  between target and model. As a result, the score won't be lowered in
657  case of additional chains/residues in any of the structures.
658 
659  :type: :class:`float`
660  """
661  if self._qs_best is None:
662  self._compute_qs()
663  return self._qs_best
664 
665  @property
667  """ Interfaces in :attr:`~target` with non-zero contribution to
668  :attr:`~qs_global`/:attr:`~qs_best`
669 
670  Chain names are lexicographically sorted.
671 
672  :type: :class:`list` of :class:`tuple` with 2 elements each:
673  (trg_ch1, trg_ch2)
674  """
675  if self._qs_target_interfaces is None:
676  self._qs_target_interfaces = self.qs_scorer.qsent1.interacting_chains
677  self._qs_target_interfaces = \
678  [(min(x[0],x[1]), max(x[0],x[1])) for x in self._qs_target_interfaces]
679  return self._qs_target_interfaces
680 
681  @property
683  """ Interfaces in :attr:`~model` with non-zero contribution to
684  :attr:`~qs_global`/:attr:`~qs_best`
685 
686  Chain names are lexicographically sorted.
687 
688  :type: :class:`list` of :class:`tuple` with 2 elements each:
689  (mdl_ch1, mdl_ch2)
690  """
691  if self._qs_model_interfaces is None:
692  self._qs_model_interfaces = self.qs_scorer.qsent2.interacting_chains
693  self._qs_model_interfaces = \
694  [(min(x[0],x[1]), max(x[0],x[1])) for x in self._qs_model_interfaces]
695 
696  return self._qs_model_interfaces
697 
698  @property
699  def qs_interfaces(self):
700  """ Interfaces in :attr:`~qs_target_interfaces` that can be mapped
701  to :attr:`~model`.
702 
703  Target chain names are lexicographically sorted.
704 
705  :type: :class:`list` of :class:`tuple` with 4 elements each:
706  (trg_ch1, trg_ch2, mdl_ch1, mdl_ch2)
707  """
708  if self._qs_interfaces is None:
709  self._qs_interfaces = list()
710  flat_mapping = self.mapping.GetFlatMapping()
711  for i in self.qs_target_interfaces:
712  if i[0] in flat_mapping and i[1] in flat_mapping:
713  self._qs_interfaces.append((i[0], i[1],
714  flat_mapping[i[0]],
715  flat_mapping[i[1]]))
716  return self._qs_interfaces
717 
718  @property
720  """ QS-score for each interface in :attr:`qs_interfaces`
721 
722  :type: :class:`list` of :class:`float`
723  """
724  if self._per_interface_qs_global is None:
726  return self._per_interface_qs_global
727 
728  @property
730  """ QS-score for each interface in :attr:`qs_interfaces`
731 
732  Only computed on aligned residues
733 
734  :type: :class:`list` of :class:`float`
735  """
736  if self._per_interface_qs_best is None:
738  return self._per_interface_qs_best
739 
740  @property
741  def native_contacts(self):
742  """ Native contacts
743 
744  A contact is a pair or residues from distinct chains that have
745  a minimal heavy atom distance < 5A. Contacts are specified as
746  :class:`tuple` with two strings in format:
747  <cname>.<rnum>.<ins_code>
748 
749  :type: :class:`list` of :class:`tuple`
750  """
751  if self._native_contacts is None:
752  self._native_contacts = self.contact_scorer.cent1.hr_contacts
753  return self._native_contacts
754 
755  @property
756  def model_contacts(self):
757  """ Same for model
758  """
759  if self._model_contacts is None:
760  self._model_contacts = self.contact_scorer.cent2.hr_contacts
761  return self._model_contacts
762 
763  @property
765  """ Interfaces in :class:`target` which have at least one contact
766 
767  Contact as defined in :attr:`~native_contacts`,
768  chain names are lexicographically sorted.
769 
770  :type: :class:`list` of :class:`tuple` with 2 elements each
771  (trg_ch1, trg_ch2)
772  """
773  if self._contact_target_interfaces is None:
774  tmp = self.contact_scorer.cent1.interacting_chains
775  tmp = [(min(x[0],x[1]), max(x[0],x[1])) for x in tmp]
776  self._contact_target_interfaces = tmp
777  return self._contact_target_interfaces
778 
779  @property
781  """ Interfaces in :class:`model` which have at least one contact
782 
783  Contact as defined in :attr:`~native_contacts`,
784  chain names are lexicographically sorted.
785 
786  :type: :class:`list` of :class:`tuple` with 2 elements each
787  (mdl_ch1, mdl_ch2)
788  """
789  if self._contact_model_interfaces is None:
790  tmp = self.contact_scorer.cent2.interacting_chains
791  tmp = [(min(x[0],x[1]), max(x[0],x[1])) for x in tmp]
792  self._contact_model_interfaces = tmp
793  return self._contact_model_interfaces
794 
795  @property
796  def ics_precision(self):
797  """ Fraction of model contacts that are also present in target
798 
799  :type: :class:`float`
800  """
801  if self._ics_precision is None:
802  self._compute_ics_scores()
803  return self._ics_precision
804 
805  @property
806  def ics_recall(self):
807  """ Fraction of target contacts that are correctly reproduced in model
808 
809  :type: :class:`float`
810  """
811  if self._ics_recall is None:
812  self._compute_ics_scores()
813  return self._ics_recall
814 
815  @property
816  def ics(self):
817  """ ICS (Interface Contact Similarity) score
818 
819  Combination of :attr:`~ics_precision` and :attr:`~ics_recall`
820  using the F1-measure
821 
822  :type: :class:`float`
823  """
824  if self._ics is None:
825  self._compute_ics_scores()
826  return self._ics
827 
828  @property
830  """ Per-interface ICS precision
831 
832  :attr:`~ics_precision` for each interface in
833  :attr:`~contact_target_interfaces`
834 
835  :type: :class:`list` of :class:`float`
836  """
837  if self._per_interface_ics_precision is None:
838  self._compute_ics_scores()
839  return self._per_interface_ics_precision
840 
841 
842  @property
844  """ Per-interface ICS recall
845 
846  :attr:`~ics_recall` for each interface in
847  :attr:`~contact_target_interfaces`
848 
849  :type: :class:`list` of :class:`float`
850  """
851  if self._per_interface_ics_recall is None:
852  self._compute_ics_scores()
853  return self._per_interface_ics_recall
854 
855  @property
856  def per_interface_ics(self):
857  """ Per-interface ICS (Interface Contact Similarity) score
858 
859  :attr:`~ics` for each interface in
860  :attr:`~contact_target_interfaces`
861 
862  :type: :class:`float`
863  """
864 
865  if self._per_interface_ics is None:
866  self._compute_ics_scores()
867  return self._per_interface_ics
868 
869 
870  @property
871  def ips_precision(self):
872  """ Fraction of model interface residues that are also interface
873  residues in target
874 
875  :type: :class:`float`
876  """
877  if self._ips_precision is None:
878  self._compute_ips_scores()
879  return self._ips_precision
880 
881  @property
882  def ips_recall(self):
883  """ Fraction of target interface residues that are also interface
884  residues in model
885 
886  :type: :class:`float`
887  """
888  if self._ips_recall is None:
889  self._compute_ips_scores()
890  return self._ips_recall
891 
892  @property
893  def ips(self):
894  """ IPS (Interface Patch Similarity) score
895 
896  Jaccard coefficient of interface residues in target and their mapped
897  counterparts in model
898 
899  :type: :class:`float`
900  """
901  if self._ips is None:
902  self._compute_ips_scores()
903  return self._ips
904 
905  @property
907  """ Per-interface IPS precision
908 
909  :attr:`~ips_precision` for each interface in
910  :attr:`~contact_target_interfaces`
911 
912  :type: :class:`list` of :class:`float`
913  """
914  if self._per_interface_ips_precision is None:
915  self._compute_ips_scores()
916  return self._per_interface_ips_precision
917 
918 
919  @property
921  """ Per-interface IPS recall
922 
923  :attr:`~ips_recall` for each interface in
924  :attr:`~contact_target_interfaces`
925 
926  :type: :class:`list` of :class:`float`
927  """
928  if self._per_interface_ics_recall is None:
929  self._compute_ips_scores()
930  return self._per_interface_ips_recall
931 
932  @property
933  def per_interface_ips(self):
934  """ Per-interface IPS (Interface Patch Similarity) score
935 
936  :attr:`~ips` for each interface in
937  :attr:`~contact_target_interfaces`
938 
939  :type: :class:`list` of :class:`float`
940  """
941 
942  if self._per_interface_ips is None:
943  self._compute_ips_scores()
944  return self._per_interface_ips
945 
946  @property
948  """ Interfaces in :attr:`target` that are relevant for DockQ
949 
950  In principle a subset of :attr:`~contact_target_interfaces` that only
951  contains peptide sequences. Chain names are lexicographically sorted.
952 
953  :type: :class:`list` of :class:`tuple` with 2 elements each:
954  (trg_ch1, trg_ch2)
955  """
956  if self._dockq_target_interfaces is None:
957  self._dockq_target_interfaces = list()
958  pep_seqs = set([s.GetName() for s in self.chain_mapper.polypep_seqs])
959  for interface in self.contact_target_interfaces:
960  if interface[0] in pep_seqs and interface[1] in pep_seqs:
961  self._dockq_target_interfaces.append(interface)
962  return self._dockq_target_interfaces
963 
964  @property
965  def dockq_interfaces(self):
966  """ Interfaces in :attr:`dockq_target_interfaces` that can be mapped
967  to model
968 
969  Target chain names are lexicographically sorted
970 
971  :type: :class:`list` of :class:`tuple` with 4 elements each:
972  (trg_ch1, trg_ch2, mdl_ch1, mdl_ch2)
973  """
974  if self._dockq_interfaces is None:
975  self._dockq_interfaces = list()
976  flat_mapping = self.mapping.GetFlatMapping()
977  for i in self.dockq_target_interfaces:
978  if i[0] in flat_mapping and i[1] in flat_mapping:
979  self._dockq_interfaces.append((i[0], i[1],
980  flat_mapping[i[0]],
981  flat_mapping[i[1]]))
982  return self._dockq_interfaces
983 
984  @property
985  def dockq_scores(self):
986  """ DockQ scores for interfaces in :attr:`~dockq_interfaces`
987 
988  :class:`list` of :class:`float`
989  """
990  if self._dockq_scores is None:
991  self._compute_dockq_scores()
992  return self._dockq_scores
993 
994  @property
995  def fnat(self):
996  """ fnat scores for interfaces in :attr:`~dockq_interfaces`
997 
998  fnat: Fraction of native contacts that are also present in model
999 
1000  :class:`list` of :class:`float`
1001  """
1002  if self._fnat is None:
1003  self._compute_dockq_scores()
1004  return self._fnat
1005 
1006  @property
1007  def nnat(self):
1008  """ N native contacts for interfaces in :attr:`~dockq_interfaces`
1009 
1010  :class:`list` of :class:`int`
1011  """
1012  if self._nnat is None:
1013  self._compute_dockq_scores()
1014  return self._nnat
1015 
1016  @property
1017  def nmdl(self):
1018  """ N model contacts for interfaces in :attr:`~dockq_interfaces`
1019 
1020  :class:`list` of :class:`int`
1021  """
1022  if self._nmdl is None:
1023  self._compute_dockq_scores()
1024  return self._nmdl
1025 
1026  @property
1027  def fnonnat(self):
1028  """ fnonnat scores for interfaces in :attr:`~dockq_interfaces`
1029 
1030  fnat: Fraction of model contacts that are not present in target
1031 
1032  :class:`list` of :class:`float`
1033  """
1034  if self._fnonnat is None:
1035  self._compute_dockq_scores()
1036  return self._fnonnat
1037 
1038  @property
1039  def irmsd(self):
1040  """ irmsd scores for interfaces in :attr:`~dockq_interfaces`
1041 
1042  irmsd: RMSD of interface (RMSD computed on N, CA, C, O atoms) which
1043  consists of each residue that has at least one heavy atom within 10A of
1044  other chain.
1045 
1046  :class:`list` of :class:`float`
1047  """
1048  if self._irmsd is None:
1049  self._compute_dockq_scores()
1050  return self._irmsd
1051 
1052  @property
1053  def lrmsd(self):
1054  """ lrmsd scores for interfaces in :attr:`~dockq_interfaces`
1055 
1056  lrmsd: The interfaces are superposed based on the receptor (rigid
1057  min RMSD superposition) and RMSD for the ligand is reported.
1058  Superposition and RMSD are based on N, CA, C and O positions,
1059  receptor is the chain contributing to the interface with more
1060  residues in total.
1061 
1062  :class:`list` of :class:`float`
1063  """
1064  if self._lrmsd is None:
1065  self._compute_dockq_scores()
1066  return self._lrmsd
1067 
1068  @property
1069  def dockq_ave(self):
1070  """ Average of DockQ scores in :attr:`dockq_scores`
1071 
1072  In its original implementation, DockQ only operates on single
1073  interfaces. Thus the requirement to combine scores for higher order
1074  oligomers.
1075 
1076  :type: :class:`float`
1077  """
1078  if self._dockq_ave is None:
1079  self._compute_dockq_scores()
1080  return self._dockq_ave
1081 
1082  @property
1083  def dockq_wave(self):
1084  """ Same as :attr:`dockq_ave`, weighted by native contacts
1085 
1086  :type: :class:`float`
1087  """
1088  if self._dockq_wave is None:
1089  self._compute_dockq_scores()
1090  return self._dockq_wave
1091 
1092  @property
1093  def dockq_ave_full(self):
1094  """ Same as :attr:`~dockq_ave` but penalizing for missing interfaces
1095 
1096  Interfaces that are not covered in model are added as 0.0
1097  in average computation.
1098 
1099  :type: :class:`float`
1100  """
1101  if self._dockq_ave_full is None:
1102  self._compute_dockq_scores()
1103  return self._dockq_ave_full
1104 
1105  @property
1106  def dockq_wave_full(self):
1107  """ Same as :attr:`~dockq_ave_full`, but weighted
1108 
1109  Interfaces that are not covered in model are added as 0.0 in
1110  average computations and the respective weights are derived from
1111  number of contacts in respective target interface.
1112  """
1113  if self._dockq_wave_full is None:
1114  self._compute_dockq_scores()
1115  return self._dockq_wave_full
1116 
1117  @property
1119  """ Mapped representative positions in target
1120 
1121  Thats CA positions for peptide residues and C3' positions for
1122  nucleotides. Has same length as :attr:`~mapped_model_pos` and mapping
1123  is based on :attr:`~mapping`.
1124 
1125  :type: :class:`ost.geom.Vec3List`
1126  """
1127  if self._mapped_target_pos is None:
1128  self._extract_mapped_pos()
1129  return self._mapped_target_pos
1130 
1131  @property
1132  def mapped_model_pos(self):
1133  """ Mapped representative positions in model
1134 
1135  Thats CA positions for peptide residues and C3' positions for
1136  nucleotides. Has same length as :attr:`~mapped_target_pos` and mapping
1137  is based on :attr:`~mapping`.
1138 
1139  :type: :class:`ost.geom.Vec3List`
1140  """
1141  if self._mapped_model_pos is None:
1142  self._extract_mapped_pos()
1143  return self._mapped_model_pos
1144 
1145  @property
1147  """ :attr:`~mapped_model_pos` with :attr:`~transform` applied
1148 
1149  :type: :class:`ost.geom.Vec3List`
1150  """
1151  if self._transformed_mapped_model_pos is None:
1154  self._transformed_mapped_model_pos.ApplyTransform(self.transform)
1155  return self._transformed_mapped_model_pos
1156 
1157  @property
1159  """ Number of target residues which have no mapping to model
1160 
1161  :type: :class:`int`
1162  """
1163  if self._n_target_not_mapped is None:
1164  self._extract_mapped_pos()
1165  return self._n_target_not_mapped
1166 
1167  @property
1168  def transform(self):
1169  """ Transform: :attr:`~mapped_model_pos` onto :attr:`~mapped_target_pos`
1170 
1171  Computed using Kabsch minimal rmsd algorithm
1172 
1173  :type: :class:`ost.geom.Mat4`
1174  """
1175  if self._transform is None:
1176  try:
1177  res = mol.alg.SuperposeSVD(self.mapped_model_pos,
1178  self.mapped_target_pos)
1179  self._transform = res.transformation
1180  except:
1181  self._transform = geom.Mat4()
1182  return self._transform
1183 
1184  @property
1185  def gdtts(self):
1186  """ GDT with thresholds: 8.0A, 4.0A, 2.0A and 1.0A
1187 
1188  Computed on :attr:`~transformed_mapped_model_pos` and
1189  :attr:`mapped_target_pos`
1190 
1191  :type: :class:`float`
1192  """
1193  if self._gdtts is None:
1194  n = \
1195  self.mapped_target_pos.GetGDTTS(self.transformed_mapped_model_pos,
1196  norm=False)
1197  n_full = 4*len(self.mapped_target_pos) + 4*self.n_target_not_mapped
1198  if n_full > 0:
1199  self._gdtts = float(n) / n_full
1200  else:
1201  self._gdtts = 0.0
1202  return self._gdtts
1203 
1204  @property
1205  def gdtha(self):
1206  """ GDT with thresholds: 4.0A, 2.0A, 1.0A and 0.5A
1207 
1208  Computed on :attr:`~transformed_mapped_model_pos` and
1209  :attr:`mapped_target_pos`
1210 
1211  :type: :class:`float`
1212  """
1213  if self._gdtha is None:
1214  n = \
1215  self.mapped_target_pos.GetGDTHA(self.transformed_mapped_model_pos,
1216  norm=False)
1217  n_full = 4*len(self.mapped_target_pos) + 4*self.n_target_not_mapped
1218  if n_full > 0:
1219  self._gdtha = float(n) / n_full
1220  else:
1221  self._gdtha = 0.0
1222  return self._gdtha
1223 
1224  @property
1225  def rmsd(self):
1226  """ RMSD
1227 
1228  Computed on :attr:`~transformed_mapped_model_pos` and
1229  :attr:`mapped_target_pos`
1230 
1231  :type: :class:`float`
1232  """
1233  if self._rmsd is None:
1234  self._rmsd = \
1235  self.mapped_target_pos.GetRMSD(self.transformed_mapped_model_pos)
1236  return self._rmsd
1237 
1238  @property
1239  def cad_score(self):
1240  """ The global CAD atom-atom (AA) score
1241 
1242  Computed based on :attr:`~model`. In case of oligomers, :attr:`~mapping`
1243  is used.
1244 
1245  :type: :class:`float`
1246  """
1247  if self._cad_score is None:
1248  self._compute_cad_score()
1249  return self._cad_score
1250 
1251  @property
1252  def local_cad_score(self):
1253  """ The per-residue CAD atom-atom (AA) scores
1254 
1255  Computed based on :attr:`~model`. In case of oligomers, :attr:`~mapping`
1256  is used.
1257 
1258  :type: :class:`dict`
1259  """
1260  if self._local_cad_score is None:
1261  self._compute_cad_score()
1262  return self._local_cad_score
1263 
1264  @property
1265  def patch_qs(self):
1266  """ Patch QS-scores for each residue in :attr:`model_interface_residues`
1267 
1268  Representative patches for each residue r in chain c are computed as
1269  follows:
1270 
1271  * mdl_patch_one: All residues in c with CB (CA for GLY) positions within
1272  8A of r and within 12A of residues from any other chain.
1273  * mdl_patch_two: Closest residue x to r in any other chain gets
1274  identified. Patch is then constructed by selecting all residues from
1275  any other chain within 8A of x and within 12A from any residue in c.
1276  * trg_patch_one: Chain name and residue number based mapping from
1277  mdl_patch_one
1278  * trg_patch_two: Chain name and residue number based mapping from
1279  mdl_patch_two
1280 
1281  Results are stored in the same manner as
1282  :attr:`model_interface_residues`, with corresponding scores instead of
1283  residue numbers. Scores for residues which are not
1284  :class:`mol.ChemType.AMINOACIDS` are set to None. Additionally,
1285  interface patches are derived from :attr:`model`. If they contain
1286  residues which are not covered by :attr:`target`, the score is set to
1287  None too.
1288 
1289  :type: :class:`dict` with chain names as key and and :class:`list`
1290  with scores of the respective interface residues.
1291  """
1292  if self._patch_qs is None:
1294  return self._patch_qs
1295 
1296  @property
1297  def patch_dockq(self):
1298  """ Same as :attr:`patch_qs` but for DockQ scores
1299  """
1300  if self._patch_dockq is None:
1302  return self._patch_dockq
1303 
1304  @property
1305  def tm_score(self):
1306  """ TM-score computed with USalign
1307 
1308  USalign executable can be specified with usalign_exec kwarg at Scorer
1309  construction, an OpenStructure internal copy of the USalign code is
1310  used otherwise.
1311 
1312  :type: :class:`float`
1313  """
1314  if self._tm_score is None:
1315  self._compute_tmscore()
1316  return self._tm_score
1317 
1318  @property
1319  def usalign_mapping(self):
1320  """ Mapping computed with USalign
1321 
1322  Dictionary with target chain names as key and model chain names as
1323  values. No guarantee that all chains are mapped. USalign executable
1324  can be specified with usalign_exec kwarg at Scorer construction, an
1325  OpenStructure internal copy of the USalign code is used otherwise.
1326 
1327  :type: :class:`dict`
1328  """
1329  if self._usalign_mapping is None:
1330  self._compute_tmscore()
1331  return self._usalign_mapping
1332 
1333  def _aln_helper(self, target, model):
1334  # perform required alignments - cannot take the alignments from the
1335  # mapping results as we potentially remove stuff there as compared
1336  # to self.model and self.target
1337  trg_seqs = dict()
1338  for ch in target.chains:
1339  cname = ch.GetName()
1340  s = ''.join([r.one_letter_code for r in ch.residues])
1341  s = seq.CreateSequence(ch.GetName(), s)
1342  s.AttachView(target.Select(f"cname={cname}"))
1343  trg_seqs[ch.GetName()] = s
1344  mdl_seqs = dict()
1345  for ch in model.chains:
1346  cname = ch.GetName()
1347  s = ''.join([r.one_letter_code for r in ch.residues])
1348  s = seq.CreateSequence(cname, s)
1349  s.AttachView(model.Select(f"cname={cname}"))
1350  mdl_seqs[ch.GetName()] = s
1351 
1352  alns = list()
1353  trg_pep_chains = [s.GetName() for s in self.chain_mapper.polypep_seqs]
1354  trg_nuc_chains = [s.GetName() for s in self.chain_mapper.polynuc_seqs]
1355  trg_pep_chains = set(trg_pep_chains)
1356  trg_nuc_chains = set(trg_nuc_chains)
1357  for trg_ch, mdl_ch in self.mapping.GetFlatMapping().items():
1358  if mdl_ch in mdl_seqs and trg_ch in trg_seqs:
1359  if trg_ch in trg_pep_chains:
1360  stype = mol.ChemType.AMINOACIDS
1361  elif trg_ch in trg_nuc_chains:
1362  stype = mol.ChemType.NUCLEOTIDES
1363  else:
1364  raise RuntimeError("Chain name inconsistency... ask "
1365  "Gabriel")
1366  alns.append(self.chain_mapper.Align(trg_seqs[trg_ch],
1367  mdl_seqs[mdl_ch],
1368  stype))
1369  alns[-1].AttachView(0, trg_seqs[trg_ch].GetAttachedView())
1370  alns[-1].AttachView(1, mdl_seqs[mdl_ch].GetAttachedView())
1371  return alns
1372 
1373  def _compute_pepnuc_aln(self):
1374  query = "peptide=true or nucleotide=true"
1375  pep_nuc_target = self.target_orig.Select(query)
1376  pep_nuc_model = self.model_orig.Select(query)
1377  self._pepnuc_aln = self._aln_helper(pep_nuc_target, pep_nuc_model)
1378 
1379  def _compute_aln(self):
1380  self._aln = self._aln_helper(self.target, self.model)
1381 
1382  def _compute_stereochecked_aln(self):
1384  self.stereochecked_model)
1385 
1386  def _compute_lddt(self):
1387  # lDDT requires a flat mapping with mdl_ch as key and trg_ch as value
1388  flat_mapping = self.mapping.GetFlatMapping(mdl_as_key=True)
1389 
1390  # make alignments accessible by mdl seq name
1391  stereochecked_alns = dict()
1392  for aln in self.stereochecked_aln:
1393  mdl_seq = aln.GetSequence(1)
1394  stereochecked_alns[mdl_seq.name] = aln
1395  alns = dict()
1396  for aln in self.aln:
1397  mdl_seq = aln.GetSequence(1)
1398  alns[mdl_seq.name] = aln
1399 
1400  # score variables to be set
1401  lddt_score = None
1402  local_lddt = None
1403 
1404  if self.lddt_no_stereochecks:
1405  lddt_chain_mapping = dict()
1406  for mdl_ch, trg_ch in flat_mapping.items():
1407  if mdl_ch in alns:
1408  lddt_chain_mapping[mdl_ch] = trg_ch
1409  lddt_score = self.lddt_scorer.lDDT(self.model,
1410  chain_mapping = lddt_chain_mapping,
1411  residue_mapping = alns,
1412  check_resnames=False,
1413  local_lddt_prop="lddt")[0]
1414  local_lddt = dict()
1415  for r in self.model.residues:
1416  cname = r.GetChain().GetName()
1417  if cname not in local_lddt:
1418  local_lddt[cname] = dict()
1419  if r.HasProp("lddt"):
1420  score = round(r.GetFloatProp("lddt"), 3)
1421  local_lddt[cname][r.GetNumber()] = score
1422  else:
1423  # not covered by trg or skipped in chain mapping procedure
1424  # the latter happens if its part of a super short chain
1425  local_lddt[cname][r.GetNumber()] = None
1426 
1427  else:
1428  lddt_chain_mapping = dict()
1429  for mdl_ch, trg_ch in flat_mapping.items():
1430  if mdl_ch in stereochecked_alns:
1431  lddt_chain_mapping[mdl_ch] = trg_ch
1432  lddt_score = self.lddt_scorer.lDDT(self.stereochecked_model,
1433  chain_mapping = lddt_chain_mapping,
1434  residue_mapping = stereochecked_alns,
1435  check_resnames=False,
1436  local_lddt_prop="lddt")[0]
1437  local_lddt = dict()
1438  for r in self.model.residues:
1439  cname = r.GetChain().GetName()
1440  if cname not in local_lddt:
1441  local_lddt[cname] = dict()
1442  if r.HasProp("lddt"):
1443  score = round(r.GetFloatProp("lddt"), 3)
1444  local_lddt[cname][r.GetNumber()] = score
1445  else:
1446  # rsc => residue stereo checked...
1447  mdl_res = self.stereochecked_model.FindResidue(cname, r.GetNumber())
1448  if mdl_res.IsValid():
1449  # not covered by trg or skipped in chain mapping procedure
1450  # the latter happens if its part of a super short chain
1451  local_lddt[cname][r.GetNumber()] = None
1452  else:
1453  # opt 1: removed by stereochecks => assign 0.0
1454  # opt 2: removed by stereochecks AND not covered by ref
1455  # => assign None
1456  # fetch trg residue from non-stereochecked aln
1457  trg_r = None
1458  if cname in flat_mapping:
1459  for col in alns[cname]:
1460  if col[0] != '-' and col[1] != '-':
1461  if col.GetResidue(1).number == r.number:
1462  trg_r = col.GetResidue(0)
1463  break
1464  if trg_r is None:
1465  local_lddt[cname][r.GetNumber()] = None
1466  else:
1467  local_lddt[cname][r.GetNumber()] = 0.0
1468 
1469  self._lddt = lddt_score
1470  self._local_lddt = local_lddt
1471 
1472  def _compute_bb_lddt(self):
1473 
1474  # make alignments accessible by mdl seq name
1475  alns = dict()
1476  for aln in self.aln:
1477  mdl_seq = aln.GetSequence(1)
1478  alns[mdl_seq.name] = aln
1479 
1480  # lDDT requires a flat mapping with mdl_ch as key and trg_ch as value
1481  flat_mapping = self.mapping.GetFlatMapping(mdl_as_key=True)
1482  lddt_chain_mapping = dict()
1483  for mdl_ch, trg_ch in flat_mapping.items():
1484  if mdl_ch in alns:
1485  lddt_chain_mapping[mdl_ch] = trg_ch
1486 
1487  lddt_score = self.bb_lddt_scorer.lDDT(self.model,
1488  chain_mapping = lddt_chain_mapping,
1489  residue_mapping = alns,
1490  check_resnames=False,
1491  local_lddt_prop="bb_lddt")[0]
1492  local_lddt = dict()
1493  for r in self.model.residues:
1494  cname = r.GetChain().GetName()
1495  if cname not in local_lddt:
1496  local_lddt[cname] = dict()
1497  if r.HasProp("bb_lddt"):
1498  score = round(r.GetFloatProp("bb_lddt"), 3)
1499  local_lddt[cname][r.GetNumber()] = score
1500  else:
1501  # not covered by trg or skipped in chain mapping procedure
1502  # the latter happens if its part of a super short chain
1503  local_lddt[cname][r.GetNumber()] = None
1504 
1505  self._bb_lddt = lddt_score
1506  self._bb_local_lddt = local_lddt
1507 
1508  def _compute_qs(self):
1509  qs_score_result = self.qs_scorer.Score(self.mapping.mapping)
1510  self._qs_global = qs_score_result.QS_global
1511  self._qs_best = qs_score_result.QS_best
1512 
1513  def _compute_per_interface_qs_scores(self):
1514  self._per_interface_qs_global = list()
1515  self._per_interface_qs_best = list()
1516 
1517  for interface in self.qs_interfaces:
1518  trg_ch1 = interface[0]
1519  trg_ch2 = interface[1]
1520  mdl_ch1 = interface[2]
1521  mdl_ch2 = interface[3]
1522  qs_res = self.qs_scorer.ScoreInterface(trg_ch1, trg_ch2,
1523  mdl_ch1, mdl_ch2)
1524  self._per_interface_qs_best.append(qs_res.QS_best)
1525  self._per_interface_qs_global.append(qs_res.QS_global)
1526 
1527  def _compute_ics_scores(self):
1528  contact_scorer_res = self.contact_scorer.ScoreICS(self.mapping.mapping)
1529  self._ics_precision = contact_scorer_res.precision
1530  self._ics_recall = contact_scorer_res.recall
1531  self._ics = contact_scorer_res.ics
1532  self._per_interface_ics_precision = list()
1533  self._per_interface_ics_recall = list()
1534  self._per_interface_ics = list()
1535  flat_mapping = self.mapping.GetFlatMapping()
1536  for trg_int in self.contact_target_interfaces:
1537  trg_ch1 = trg_int[0]
1538  trg_ch2 = trg_int[1]
1539  if trg_ch1 in flat_mapping and trg_ch2 in flat_mapping:
1540  mdl_ch1 = flat_mapping[trg_ch1]
1541  mdl_ch2 = flat_mapping[trg_ch2]
1542  res = self.contact_scorer.ScoreICSInterface(trg_ch1, trg_ch2,
1543  mdl_ch1, mdl_ch2)
1544  self._per_interface_ics_precision.append(res.precision)
1545  self._per_interface_ics_recall.append(res.recall)
1546  self._per_interface_ics.append(res.ics)
1547  else:
1548  self._per_interface_ics_precision.append(None)
1549  self._per_interface_ics_recall.append(None)
1550  self._per_interface_ics.append(None)
1551 
1552  def _compute_ips_scores(self):
1553  contact_scorer_res = self.contact_scorer.ScoreIPS(self.mapping.mapping)
1554  self._ips_precision = contact_scorer_res.precision
1555  self._ips_recall = contact_scorer_res.recall
1556  self._ips = contact_scorer_res.ips
1557 
1558  self._per_interface_ips_precision = list()
1559  self._per_interface_ips_recall = list()
1560  self._per_interface_ips = list()
1561  flat_mapping = self.mapping.GetFlatMapping()
1562  for trg_int in self.contact_target_interfaces:
1563  trg_ch1 = trg_int[0]
1564  trg_ch2 = trg_int[1]
1565  if trg_ch1 in flat_mapping and trg_ch2 in flat_mapping:
1566  mdl_ch1 = flat_mapping[trg_ch1]
1567  mdl_ch2 = flat_mapping[trg_ch2]
1568  res = self.contact_scorer.ScoreIPSInterface(trg_ch1, trg_ch2,
1569  mdl_ch1, mdl_ch2)
1570  self._per_interface_ips_precision.append(res.precision)
1571  self._per_interface_ips_recall.append(res.recall)
1572  self._per_interface_ips.append(res.ips)
1573  else:
1574  self._per_interface_ips_precision.append(None)
1575  self._per_interface_ips_recall.append(None)
1576  self._per_interface_ips.append(None)
1577 
1578  def _compute_dockq_scores(self):
1579  # lists with values in contact_target_interfaces
1580  self._dockq_scores = list()
1581  self._fnat = list()
1582  self._fnonnat = list()
1583  self._irmsd = list()
1584  self._lrmsd = list()
1585  self._nnat = list()
1586  self._nmdl = list()
1587 
1588  dockq_alns = dict()
1589  for aln in self.aln:
1590  trg_s = aln.GetSequence(0)
1591  mdl_s = aln.GetSequence(1)
1592  dockq_alns[(trg_s.GetName(), mdl_s.GetName())] = aln
1593 
1594  for interface in self.dockq_interfaces:
1595  trg_ch1 = interface[0]
1596  trg_ch2 = interface[1]
1597  mdl_ch1 = interface[2]
1598  mdl_ch2 = interface[3]
1599  aln1 = dockq_alns[(trg_ch1, mdl_ch1)]
1600  aln2 = dockq_alns[(trg_ch2, mdl_ch2)]
1601  res = dockq.DockQ(self.model, self.target, mdl_ch1, mdl_ch2,
1602  trg_ch1, trg_ch2, ch1_aln=aln1,
1603  ch2_aln=aln2)
1604  self._fnat.append(res["fnat"])
1605  self._fnonnat.append(res["fnonnat"])
1606  self._irmsd.append(res["irmsd"])
1607  self._lrmsd.append(res["lrmsd"])
1608  self._dockq_scores.append(res["DockQ"])
1609  self._nnat.append(res["nnat"])
1610  self._nmdl.append(res["nmdl"])
1611 
1612  # keep track of native counts in target interfaces which are
1613  # not covered in model in order to compute
1614  # dockq_ave_full/dockq_wave_full in the end
1615  not_covered_counts = list()
1616  proc_trg_interfaces = set([(x[0], x[1]) for x in self.dockq_interfaces])
1617  for interface in self.dockq_target_interfaces:
1618  if interface not in proc_trg_interfaces:
1619  # let's run DockQ with trg as trg/mdl in order to get the native
1620  # contacts out - no need to pass alns as the residue numbers
1621  # match for sure
1622  trg_ch1 = interface[0]
1623  trg_ch2 = interface[1]
1624  res = dockq.DockQ(self.target, self.target,
1625  trg_ch1, trg_ch2, trg_ch1, trg_ch2)
1626  not_covered_counts.apend(res["nnat"])
1627 
1628  # there are 4 types of combined scores
1629  # - simple average
1630  # - average weighted by native_contacts
1631  # - the two above including nonmapped_contact_interfaces => set DockQ to 0.0
1632  scores = np.array([self._dockq_scores])
1633  weights = np.array([self._nnat])
1634  if len(scores) > 0:
1635  self._dockq_ave = np.mean(scores)
1636  else:
1637  self._dockq_ave = 0.0
1638  self._dockq_wave = np.sum(np.multiply(weights/np.sum(weights), scores))
1639  scores = np.append(scores, [0.0]*len(not_covered_counts))
1640  weights = np.append(weights, not_covered_counts)
1641  if len(scores) > 0:
1642  self._dockq_ave_full = np.mean(scores)
1643  else:
1644  self._dockq_ave_full = 0.0
1645  self._dockq_wave_full = np.sum(np.multiply(weights/np.sum(weights),
1646  scores))
1647 
1648  def _extract_mapped_pos(self):
1651  self._n_target_not_mapped = 0
1652  processed_trg_chains = set()
1653  for trg_ch, mdl_ch in self.mapping.GetFlatMapping().items():
1654  processed_trg_chains.add(trg_ch)
1655  aln = self.mapping.alns[(trg_ch, mdl_ch)]
1656  for col in aln:
1657  if col[0] != '-' and col[1] != '-':
1658  trg_res = col.GetResidue(0)
1659  mdl_res = col.GetResidue(1)
1660  trg_at = trg_res.FindAtom("CA")
1661  mdl_at = mdl_res.FindAtom("CA")
1662  if not trg_at.IsValid():
1663  trg_at = trg_res.FindAtom("C3'")
1664  if not mdl_at.IsValid():
1665  mdl_at = mdl_res.FindAtom("C3'")
1666  self._mapped_target_pos.append(trg_at.GetPos())
1667  self._mapped_model_pos.append(mdl_at.GetPos())
1668  elif col[0] != '-':
1669  self._n_target_not_mapped += 1
1670  # count number of trg residues from non-mapped chains
1671  for ch in self.mapping.target.chains:
1672  if ch.GetName() not in processed_trg_chains:
1673  self._n_target_not_mapped += len(ch.residues)
1674 
1675  def _compute_cad_score(self):
1676  if not self.resnum_alignments:
1677  raise RuntimeError("CAD score computations rely on residue numbers "
1678  "that are consistent between target and model "
1679  "chains, i.e. only work if resnum_alignments "
1680  "is True at Scorer construction.")
1681  try:
1682  cad_score_exec = \
1683  settings.Locate("voronota-cadscore",
1684  explicit_file_name=self.cad_score_exec)
1685  except Exception as e:
1686  raise RuntimeError("voronota-cadscore must be in PATH for CAD "
1687  "score scoring") from e
1688  cad_bin_dir = os.path.dirname(cad_score_exec)
1689  m = self.mapping.GetFlatMapping(mdl_as_key=True)
1690  cad_result = cadscore.CADScore(self.model, self.target,
1691  mode = "voronota",
1692  label="localcad",
1693  old_regime=False,
1694  cad_bin_path=cad_bin_dir,
1695  chain_mapping=m)
1696 
1697  local_cad = dict()
1698  for r in self.model.residues:
1699  cname = r.GetChain().GetName()
1700  if cname not in local_cad:
1701  local_cad[cname] = dict()
1702  if r.HasProp("localcad"):
1703  score = round(r.GetFloatProp("localcad"), 3)
1704  local_cad[cname][r.GetNumber()] = score
1705  else:
1706  local_cad[cname][r.GetNumber()] = None
1707 
1708  self._cad_score = cad_result.globalAA
1709  self._local_cad_score = local_cad
1710 
1711  def _get_repr_view(self, ent):
1712  """ Returns view with representative peptide atoms => CB, CA for GLY
1713 
1714  Ensures that each residue has exactly one atom with assertions
1715 
1716  :param ent: Entity for which you want the representative view
1717  :param ent: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
1718  :returns: :class:`ost.mol.EntityView` derived from ent
1719  """
1720  repr_ent = ent.Select("(aname=\"CB\" or (rname=\"GLY\" and aname=\"CA\"))")
1721  for r in repr_ent.residues:
1722  assert(len(r.atoms) == 1)
1723  return repr_ent
1724 
1725  def _get_interface_residues(self, ent):
1726  """ Get interface residues
1727 
1728  Thats all residues having a contact with at least one residue from another
1729  chain (CB-CB distance <= 8A, CA in case of Glycine)
1730 
1731  :param ent: Model for which to extract interface residues
1732  :type ent: :class:`ost.mol.EntityView`
1733  :returns: :class:`dict` with chain names as key and and :class:`list`
1734  with residue numbers of the respective interface residues.
1735  """
1736  # select for representative positions => CB, CA for GLY
1737  repr_ent = self._get_repr_view(ent)
1738  result = {ch.GetName(): list() for ch in ent.chains}
1739  for ch in ent.chains:
1740  cname = ch.GetName()
1741  sel = repr_ent.Select(f"(cname={cname} and 8 <> [cname!={cname}])")
1742  result[cname] = [r.GetNumber() for r in sel.residues]
1743  return result
1744 
1745  def _do_stereochecks(self):
1746  """ Perform stereochemistry checks on model and target
1747  """
1748  data = stereochemistry.GetDefaultStereoData()
1749  l_data = stereochemistry.GetDefaultStereoLinkData()
1750 
1751  a, b, c, d = stereochemistry.StereoCheck(self.model, stereo_data = data,
1752  stereo_link_data = l_data)
1753  self._stereochecked_model = a
1754  self._model_clashes = b
1755  self._model_bad_bonds = c
1756  self._model_bad_angles = d
1757 
1758  a, b, c, d = stereochemistry.StereoCheck(self.target, stereo_data = data,
1759  stereo_link_data = l_data)
1760  self._stereochecked_target = a
1761  self._target_clashes = b
1762  self._target_bad_bonds = c
1763  self._target_bad_angles = d
1764 
1765 
1766  def _get_interface_patches(self, mdl_ch, mdl_rnum):
1767  """ Select interface patches representative for specified residue
1768 
1769  The patches for specified residue r in chain c are selected as follows:
1770 
1771  * mdl_patch_one: All residues in c with CB (CA for GLY) positions within 8A
1772  of r and within 12A of residues from any other chain.
1773  * mdl_patch_two: Closest residue x to r in any other chain gets identified.
1774  Patch is then constructed by selecting all residues from any other chain
1775  within 8A of x and within 12A from any residue in c.
1776  * trg_patch_one: Chain name and residue number based mapping from
1777  mdl_patch_one
1778  * trg_patch_two: Chain name and residue number based mapping from
1779  mdl_patch_two
1780 
1781  :param mdl_ch: Name of chain in *self.model* of residue of interest
1782  :type mdl_ch: :class:`str`
1783  :param mdl_rnum: Residue number of residue of interest
1784  :type mdl_rnum: :class:`ost.mol.ResNum`
1785  :returns: Tuple with 5 elements: 1) :class:`bool` flag whether all residues
1786  in *mdl* patches are covered in *trg* 2) mtl_patch_one
1787  3) mdl_patch_two 4) trg_patch_one 5) trg_patch_two
1788  """
1789  # select for representative positions => CB, CA for GLY
1790  repr_mdl = self._get_repr_view(self.model.Select("peptide=true"))
1791 
1792  # get position for specified residue
1793  r = self.model.FindResidue(mdl_ch, mdl_rnum)
1794  if not r.IsValid():
1795  raise RuntimeError(f"Cannot find residue {mdl_rnum} in chain {mdl_ch}")
1796  if r.GetName() == "GLY":
1797  at = r.FindAtom("CA")
1798  else:
1799  at = r.FindAtom("CB")
1800  if not at.IsValid():
1801  raise RuntimeError("Cannot find interface views for res without CB/CA")
1802  r_pos = at.GetPos()
1803 
1804  # mdl_patch_one contains residues from the same chain as r
1805  # => all residues within 8A of r and within 12A of any other chain
1806 
1807  # q1 selects for everything in same chain and within 8A of r_pos
1808  q1 = f"(cname={mdl_ch} and 8 <> {{{r_pos[0]},{r_pos[1]},{r_pos[2]}}})"
1809  # q2 selects for everything within 12A of any other chain
1810  q2 = f"(12 <> [cname!={mdl_ch}])"
1811  mdl_patch_one = self.model.CreateEmptyView()
1812  sel = repr_mdl.Select(" and ".join([q1, q2]))
1813  for r in sel.residues:
1814  mdl_r = self.model.FindResidue(r.GetChain().GetName(), r.GetNumber())
1815  mdl_patch_one.AddResidue(mdl_r, mol.ViewAddFlag.INCLUDE_ALL)
1816 
1817  # mdl_patch_two contains residues from all other chains. In detail:
1818  # the closest residue to r is identified in any other chain, and the
1819  # patch is filled with residues that are within 8A of that residue and
1820  # within 12A of chain from r
1821  sel = repr_mdl.Select(f"(cname!={mdl_ch})")
1822  close_stuff = sel.FindWithin(r_pos, 8)
1823  min_pos = None
1824  min_dist = 42.0
1825  for close_at in close_stuff:
1826  dist = geom.Distance(r_pos, close_at.GetPos())
1827  if dist < min_dist:
1828  min_pos = close_at.GetPos()
1829  min_dist = dist
1830 
1831  # q1 selects for everything not in mdl_ch but within 8A of min_pos
1832  q1 = f"(cname!={mdl_ch} and 8 <> {{{min_pos[0]},{min_pos[1]},{min_pos[2]}}})"
1833  # q2 selects for everything within 12A of mdl_ch
1834  q2 = f"(12 <> [cname={mdl_ch}])"
1835  mdl_patch_two = self.model.CreateEmptyView()
1836  sel = repr_mdl.Select(" and ".join([q1, q2]))
1837  for r in sel.residues:
1838  mdl_r = self.model.FindResidue(r.GetChain().GetName(), r.GetNumber())
1839  mdl_patch_two.AddResidue(mdl_r, mol.ViewAddFlag.INCLUDE_ALL)
1840 
1841  # transfer mdl residues to trg
1842  flat_mapping = self.mapping.GetFlatMapping(mdl_as_key=True)
1843  full_trg_coverage = True
1844  trg_patch_one = self.target.CreateEmptyView()
1845  for r in mdl_patch_one.residues:
1846  trg_r = None
1847  mdl_cname = r.GetChain().GetName()
1848  if mdl_cname in flat_mapping:
1849  aln = self.mapping.alns[(flat_mapping[mdl_cname], mdl_cname)]
1850  for col in aln:
1851  if col[0] != '-' and col[1] != '-':
1852  if col.GetResidue(1).GetNumber() == r.GetNumber():
1853  trg_r = col.GetResidue(0)
1854  break
1855  if trg_r is not None:
1856  trg_patch_one.AddResidue(trg_r.handle,
1857  mol.ViewAddFlag.INCLUDE_ALL)
1858  else:
1859  full_trg_coverage = False
1860 
1861  trg_patch_two = self.target.CreateEmptyView()
1862  for r in mdl_patch_two.residues:
1863  trg_r = None
1864  mdl_cname = r.GetChain().GetName()
1865  if mdl_cname in flat_mapping:
1866  aln = self.mapping.alns[(flat_mapping[mdl_cname], mdl_cname)]
1867  for col in aln:
1868  if col[0] != '-' and col[1] != '-':
1869  if col.GetResidue(1).GetNumber() == r.GetNumber():
1870  trg_r = col.GetResidue(0)
1871  break
1872  if trg_r is not None:
1873  trg_patch_two.AddResidue(trg_r.handle,
1874  mol.ViewAddFlag.INCLUDE_ALL)
1875  else:
1876  full_trg_coverage = False
1877 
1878  return (full_trg_coverage, mdl_patch_one, mdl_patch_two,
1879  trg_patch_one, trg_patch_two)
1880 
1881  def _compute_patchqs_scores(self):
1882  self._patch_qs = dict()
1883  for cname, rnums in self.model_interface_residues.items():
1884  scores = list()
1885  for rnum in rnums:
1886  score = None
1887  r = self.model.FindResidue(cname, rnum)
1888  if r.IsValid() and r.GetChemType() == mol.ChemType.AMINOACIDS:
1889  full_trg_coverage, mdl_patch_one, mdl_patch_two, \
1890  trg_patch_one, trg_patch_two = \
1891  self._get_interface_patches(cname, rnum)
1892  if full_trg_coverage:
1893  score = self._patchqs(mdl_patch_one, mdl_patch_two,
1894  trg_patch_one, trg_patch_two)
1895  scores.append(score)
1896  self._patch_qs[cname] = scores
1897 
1898  def _compute_patchdockq_scores(self):
1899  self._patch_dockq = dict()
1900  for cname, rnums in self.model_interface_residues.items():
1901  scores = list()
1902  for rnum in rnums:
1903  score = None
1904  r = self.model.FindResidue(cname, rnum)
1905  if r.IsValid() and r.GetChemType() == mol.ChemType.AMINOACIDS:
1906  full_trg_coverage, mdl_patch_one, mdl_patch_two, \
1907  trg_patch_one, trg_patch_two = \
1908  self._get_interface_patches(cname, rnum)
1909  if full_trg_coverage:
1910  score = self._patchdockq(mdl_patch_one, mdl_patch_two,
1911  trg_patch_one, trg_patch_two)
1912  scores.append(score)
1913  self._patch_dockq[cname] = scores
1914 
1915  def _patchqs(self, mdl_patch_one, mdl_patch_two, trg_patch_one, trg_patch_two):
1916  """ Score interface residue patches with QS-score
1917 
1918  In detail: Construct two entities with two chains each. First chain
1919  consists of residues from <x>_patch_one and second chain consists of
1920  <x>_patch_two. The returned score is the QS-score between the two
1921  entities
1922 
1923  :param mdl_patch_one: Interface patch representing scored residue
1924  :type mdl_patch_one: :class:`ost.mol.EntityView`
1925  :param mdl_patch_two: Interface patch representing scored residue
1926  :type mdl_patch_two: :class:`ost.mol.EntityView`
1927  :param trg_patch_one: Interface patch representing scored residue
1928  :type trg_patch_one: :class:`ost.mol.EntityView`
1929  :param trg_patch_two: Interface patch representing scored residue
1930  :type trg_patch_two: :class:`ost.mol.EntityView`
1931  :returns: PatchQS score
1932  """
1933  qs_ent_mdl = self._qs_ent_from_patches(mdl_patch_one, mdl_patch_two)
1934  qs_ent_trg = self._qs_ent_from_patches(trg_patch_one, trg_patch_two)
1935 
1936  alnA = seq.CreateAlignment()
1937  s = ''.join([r.one_letter_code for r in mdl_patch_one.residues])
1938  alnA.AddSequence(seq.CreateSequence("A", s))
1939  s = ''.join([r.one_letter_code for r in trg_patch_one.residues])
1940  alnA.AddSequence(seq.CreateSequence("A", s))
1941 
1942  alnB = seq.CreateAlignment()
1943  s = ''.join([r.one_letter_code for r in mdl_patch_two.residues])
1944  alnB.AddSequence(seq.CreateSequence("B", s))
1945  s = ''.join([r.one_letter_code for r in trg_patch_two.residues])
1946  alnB.AddSequence(seq.CreateSequence("B", s))
1947  alns = {("A", "A"): alnA, ("B", "B"): alnB}
1948 
1949  scorer = QSScorer(qs_ent_mdl, [["A"], ["B"]], qs_ent_trg, alns)
1950  score_result = scorer.Score([["A"], ["B"]])
1951 
1952  return score_result.QS_global
1953 
1954  def _patchdockq(self, mdl_patch_one, mdl_patch_two, trg_patch_one,
1955  trg_patch_two):
1956  """ Score interface residue patches with DockQ
1957 
1958  In detail: Construct two entities with two chains each. First chain
1959  consists of residues from <x>_patch_one and second chain consists of
1960  <x>_patch_two. The returned score is the QS-score between the two
1961  entities
1962 
1963  :param mdl_patch_one: Interface patch representing scored residue
1964  :type mdl_patch_one: :class:`ost.mol.EntityView`
1965  :param mdl_patch_two: Interface patch representing scored residue
1966  :type mdl_patch_two: :class:`ost.mol.EntityView`
1967  :param trg_patch_one: Interface patch representing scored residue
1968  :type trg_patch_one: :class:`ost.mol.EntityView`
1969  :param trg_patch_two: Interface patch representing scored residue
1970  :type trg_patch_two: :class:`ost.mol.EntityView`
1971  :returns: DockQ score
1972  """
1973  m = self._qs_ent_from_patches(mdl_patch_one, mdl_patch_two)
1974  t = self._qs_ent_from_patches(trg_patch_one, trg_patch_two)
1975  dockq_result = dockq.DockQ(t, m, "A", "B", "A", "B")
1976  if dockq_result["nnat"] > 0:
1977  return dockq_result["DockQ"]
1978  return 0.0
1979 
1980  def _qs_ent_from_patches(self, patch_one, patch_two):
1981  """ Constructs Entity with two chains named "A" and "B""
1982 
1983  Blindly adds all residues from *patch_one* to chain A and residues from
1984  patch_two to chain B.
1985  """
1986  ent = mol.CreateEntity()
1987  ed = ent.EditXCS()
1988  added_ch = ed.InsertChain("A")
1989  for r in patch_one.residues:
1990  added_r = ed.AppendResidue(added_ch, r.GetName())
1991  added_r.SetChemClass(str(r.GetChemClass()))
1992  for a in r.atoms:
1993  ed.InsertAtom(added_r, a.handle)
1994  added_ch = ed.InsertChain("B")
1995  for r in patch_two.residues:
1996  added_r = ed.AppendResidue(added_ch, r.GetName())
1997  added_r.SetChemClass(str(r.GetChemClass()))
1998  for a in r.atoms:
1999  ed.InsertAtom(added_r, a.handle)
2000  return ent
2001 
2002  def _set_custom_mapping(self, mapping):
2003  """ sets self._mapping with a full blown MappingResult object
2004 
2005  :param mapping: mapping with trg chains as key and mdl ch as values
2006  :type mapping: :class:`dict`
2007  """
2008 
2009  chain_mapper = self.chain_mapper
2010  chem_mapping, chem_group_alns, mdl = \
2011  chain_mapper.GetChemMapping(self.model)
2012 
2013  # now that we have a chem mapping, lets do consistency checks
2014  # - check whether chain names are unique and available in structures
2015  # - check whether the mapped chains actually map to the same chem groups
2016  if len(mapping) != len(set(mapping.keys())):
2017  raise RuntimeError(f"Expect unique trg chain names in mapping. Got "
2018  f"{mapping.keys()}")
2019  if len(mapping) != len(set(mapping.values())):
2020  raise RuntimeError(f"Expect unique mdl chain names in mapping. Got "
2021  f"{mapping.values()}")
2022 
2023  trg_chains = set([ch.GetName() for ch in chain_mapper.target.chains])
2024  mdl_chains = set([ch.GetName() for ch in mdl.chains])
2025  for k,v in mapping.items():
2026  if k not in trg_chains:
2027  raise RuntimeError(f"Target chain \"{k}\" is not available "
2028  f"in target processed for chain mapping "
2029  f"({trg_chains})")
2030  if v not in mdl_chains:
2031  raise RuntimeError(f"Model chain \"{v}\" is not available "
2032  f"in model processed for chain mapping "
2033  f"({mdl_chains})")
2034 
2035  for trg_ch, mdl_ch in mapping.items():
2036  trg_group_idx = None
2037  mdl_group_idx = None
2038  for idx, group in enumerate(chain_mapper.chem_groups):
2039  if trg_ch in group:
2040  trg_group_idx = idx
2041  break
2042  for idx, group in enumerate(chem_mapping):
2043  if mdl_ch in group:
2044  mdl_group_idx = idx
2045  break
2046  if trg_group_idx is None or mdl_group_idx is None:
2047  raise RuntimeError("Could not establish a valid chem grouping "
2048  "of chain names provided in custom mapping.")
2049 
2050  if trg_group_idx != mdl_group_idx:
2051  raise RuntimeError(f"Chem group mismatch in custom mapping: "
2052  f"target chain \"{trg_ch}\" groups with the "
2053  f"following chemically equivalent target "
2054  f"chains: "
2055  f"{chain_mapper.chem_groups[trg_group_idx]} "
2056  f"but model chain \"{mdl_ch}\" maps to the "
2057  f"following target chains: "
2058  f"{chain_mapper.chem_groups[mdl_group_idx]}")
2059 
2060  pairs = set([(trg_ch, mdl_ch) for trg_ch, mdl_ch in mapping.items()])
2061  ref_mdl_alns = \
2062  chain_mapping._GetRefMdlAlns(chain_mapper.chem_groups,
2063  chain_mapper.chem_group_alignments,
2064  chem_mapping,
2065  chem_group_alns,
2066  pairs = pairs)
2067 
2068  # translate mapping format
2069  final_mapping = list()
2070  for ref_chains in chain_mapper.chem_groups:
2071  mapped_mdl_chains = list()
2072  for ref_ch in ref_chains:
2073  if ref_ch in mapping:
2074  mapped_mdl_chains.append(mapping[ref_ch])
2075  else:
2076  mapped_mdl_chains.append(None)
2077  final_mapping.append(mapped_mdl_chains)
2078 
2079  alns = dict()
2080  for ref_group, mdl_group in zip(chain_mapper.chem_groups,
2081  final_mapping):
2082  for ref_ch, mdl_ch in zip(ref_group, mdl_group):
2083  if ref_ch is not None and mdl_ch is not None:
2084  aln = ref_mdl_alns[(ref_ch, mdl_ch)]
2085  trg_view = chain_mapper.target.Select(f"cname={ref_ch}")
2086  mdl_view = mdl.Select(f"cname={mdl_ch}")
2087  aln.AttachView(0, trg_view)
2088  aln.AttachView(1, mdl_view)
2089  alns[(ref_ch, mdl_ch)] = aln
2090 
2091  self._mapping = chain_mapping.MappingResult(chain_mapper.target, mdl,
2092  chain_mapper.chem_groups,
2093  chem_mapping,
2094  final_mapping, alns)
2095 
2096  def _compute_tmscore(self):
2097  res = None
2098  if self.usalign_exec is None:
2099  if self.oum:
2100  flat_mapping = self.mapping.GetFlatMapping()
2101  res = res = bindings.WrappedMMAlign(self.model, self.target,
2102  mapping=flat_mapping)
2103  else:
2104  res = bindings.WrappedMMAlign(self.model, self.target)
2105  else:
2106  if self.oum:
2107  flat_mapping = self.mapping.GetFlatMapping()
2108  res = tmtools.USAlign(self.model, self.target,
2109  usalign = self.usalign_exec,
2110  custom_chain_mapping = flat_mapping)
2111  else:
2112  res = tmtools.USAlign(self.model, self.target,
2113  usalign = self.usalign_exec)
2114 
2115  self._tm_score = res.tm_score
2116  self._usalign_mapping = dict()
2117  for a,b in zip(res.ent1_mapped_chains, res.ent2_mapped_chains):
2118  self._usalign_mapping[b] = a
void Molck(ost::mol::EntityHandle &ent, ost::conop::CompoundLibPtr lib, const MolckSettings &settings, bool prune=true)
Real Distance(const Vec3 &p1, const Vec3 &p2)
returns the distance between two points
Definition: vecmat3_op.hh:199
definition of EntityView
Definition: entity_view.hh:86