OpenStructure
Loading...
Searching...
No Matches
lddt.py
Go to the documentation of this file.
1import itertools
2import numpy as np
3
4from ost import mol
5from ost import conop
6
7# use cdist of scipy, fallback to (slower) numpy implementation if scipy is not
8# available
9try:
10 from scipy.spatial.distance import cdist
11except:
12 def cdist(p1, p2):
13 x2 = np.sum(p1**2, axis=1) # (m)
14 y2 = np.sum(p2**2, axis=1) # (n)
15 xy = np.matmul(p1, p2.T) # (m, n)
16 x2 = x2.reshape(-1, 1)
17 return np.sqrt(x2 - 2*xy + y2) # (m, n)
18
19def blockwise_cdist(A, B, block_size=1000):
20 """ Memory efficient cdist implementation that performs blockwise operations
21
22 scipy cdist uses 64 bit floats (double) which can scratch at the upper
23 memory end for most machines when number of positions become larger.
24 E.g. ~4000 residues might for example have 35000 atom positions. That's
25 Almost 10GB to hold all pairwise distances in 64bit floats. This function
26 calls cdist blockwise and stores the results in a 32bit float matrix.
27
28 This function is adapted from chatgpt output
29 """
30 A = A.astype(np.float32)
31 B = B.astype(np.float32)
32 M, N = A.shape[0], B.shape[0]
33 D = np.empty((M, N), dtype=np.float32) # Output in float32 to save memory
34 for i in range(0, M, block_size):
35 A_block = A[i:i+block_size]
36 D[i:i+block_size, :] = cdist(A_block, B).astype(np.float32)
37 return D
38
40 """ Defines atoms for custom compounds
41
42 LDDT requires the reference atoms of a compound which are typically
43 extracted from a :class:`ost.conop.CompoundLib`. This lightweight
44 container allows to handle arbitrary compounds which are not
45 necessarily in the compound library.
46
47 :param atom_names: Names of atoms of custom compound
48 :type atom_names: :class:`list` of :class:`str`
49 """
50 def __init__(self, atom_names):
51 self.atom_names = atom_names
52
53 @staticmethod
54 def FromResidue(res):
55 """ Construct custom compound from residue
56
57 :param res: Residue from which reference atom names are extracted,
58 hydrogen/deuterium atoms are filtered out
59 :type res: :class:`ost.mol.ResidueView`/:class:`ost.mol.ResidueHandle`
60 :returns: :class:`CustomCompound`
61 """
62 at_names = [a.name for a in res.atoms if a.element not in ["H", "D"]]
63 if len(at_names) != len(set(at_names)):
64 raise RuntimeError("Duplicate atoms detected in CustomCompound")
65 compound = CustomCompound(at_names)
66 return compound
67
69 """Container for symmetric compounds
70
71 LDDT considers symmetries and selects the one resulting in the highest
72 possible score.
73
74 A symmetry is defined as a renaming operation on one or more atoms that
75 leads to a chemically equivalent residue. Example would be OD1 and OD2 in
76 ASP => renaming OD1 to OD2 and vice versa gives a chemically equivalent
77 residue.
78
79 Use :func:`AddSymmetricCompound` to define a symmetry which can then
80 directly be accessed through the *symmetric_compounds* member.
81 """
82 def __init__(self):
83 self.symmetric_compounds = dict()
84
85 def AddSymmetricCompound(self, name, symmetric_atoms):
86 """Adds symmetry for compound with *name*
87
88 :param name: Name of compound with symmetry
89 :type name: :class:`str`
90 :param symmetric_atoms: Pairs of atom names that define renaming
91 operation, i.e. after applying all switches
92 defined in the tuples, the resulting residue
93 should be chemically equivalent. Atom names
94 must refer to the PDB component dictionary.
95 :type symmetric_atoms: :class:`list` of :class:`tuple`
96 """
97 for pair in symmetric_atoms:
98 if len(pair) != 2:
99 raise RuntimeError("Expect pairs when defining symmetries")
100 self.symmetric_compounds[name] = symmetric_atoms
101
102
104 """Constructs and returns :class:`SymmetrySettings` object for natural amino
105 acids
106 """
107 symmetry_settings = SymmetrySettings()
108
109 # ASP
110 symmetry_settings.AddSymmetricCompound("ASP", [("OD1", "OD2")])
111
112 # GLU
113 symmetry_settings.AddSymmetricCompound("GLU", [("OE1", "OE2")])
114
115 # LEU
116 symmetry_settings.AddSymmetricCompound("LEU", [("CD1", "CD2")])
117
118 # VAL
119 symmetry_settings.AddSymmetricCompound("VAL", [("CG1", "CG2")])
120
121 # ARG
122 symmetry_settings.AddSymmetricCompound("ARG", [("NH1", "NH2")])
123
124 # PHE
125 symmetry_settings.AddSymmetricCompound(
126 "PHE", [("CD1", "CD2"), ("CE1", "CE2")]
127 )
128
129 # TYR
130 symmetry_settings.AddSymmetricCompound(
131 "TYR", [("CD1", "CD2"), ("CE1", "CE2")]
132 )
133
134 # nucleotides
135 nuc_names = ["A", "C", "G", "U", "DA", "DC", "DG", "DT"]
136 for nuc_name in nuc_names:
137 symmetry_settings.AddSymmetricCompound(
138 nuc_name, [("OP1","OP2")]
139 )
140
141 return symmetry_settings
142
143
145 """LDDT scorer object for a specific target
146
147 Sets up everything to score models of that target. LDDT (local distance
148 difference test) is defined as fraction of pairwise distances which exhibit
149 a difference < threshold when considering target and model. In case of
150 multiple thresholds, the average is returned. See
151
152 V. Mariani, M. Biasini, A. Barbato, T. Schwede, lDDT : A local
153 superposition-free score for comparing protein structures and models using
154 distance difference tests, Bioinformatics, 2013
155
156 :param target: The target
157 :type target: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
158 :param compound_lib: Compound library from which a compound for each residue
159 is extracted based on its name. Uses
160 :func:`ost.conop.GetDefaultLib` if not given, raises
161 if this returns no valid compound library. Atoms
162 defined in the compound are searched in the residue and
163 build the reference for scoring. If the residue has
164 atoms with names ["A", "B", "C"] but the corresponding
165 compound only has ["A", "B"], "A" and "B" are
166 considered for scoring. If the residue has atoms
167 ["A", "B"] but the compound has ["A", "B", "C"], "C" is
168 considered missing and does not influence scoring, even
169 if present in the model.
170 :param custom_compounds: Custom compounds defining reference atoms. If
171 given, *custom_compounds* take precedent over
172 *compound_lib*.
173 :type custom_compounds: :class:`dict` with residue names (:class:`str`) as
174 key and :class:`CustomCompound` as value.
175 :type compound_lib: :class:`ost.conop.CompoundLib`
176 :param inclusion_radius: All pairwise distances < *inclusion_radius* are
177 considered for scoring
178 :type inclusion_radius: :class:`float`
179 :param sequence_separation: Only pairwise distances between atoms of
180 residues which are further apart than this
181 threshold are considered. Residue distance is
182 based on resnum. The default (0) considers all
183 pairwise distances except intra-residue
184 distances.
185 :type sequence_separation: :class:`int`
186 :param symmetry_settings: Define residues exhibiting internal symmetry, uses
187 :func:`GetDefaultSymmetrySettings` if not given.
188 :type symmetry_settings: :class:`SymmetrySettings`
189 :param seqres_mapping: Mapping of model residues at the scoring stage
190 happens with residue numbers defining their location
191 in a reference sequence (SEQRES) using one based
192 indexing. If the residue numbers in *target* don't
193 correspond to that SEQRES, you can specify the
194 mapping manually. You can provide a dictionary to
195 specify a reference sequence (SEQRES) for one or more
196 chain(s). Key: chain name, value: alignment
197 (seq1: SEQRES, seq2: sequence of residues in chain).
198 Example: The residues in a chain with name "A" have
199 sequence "YEAH" and residue numbers [42,43,44,45].
200 You can provide an alignment with seq1 "``HELLYEAH``"
201 and seq2 "``----YEAH``". "Y" gets assigned residue
202 number 5, "E" gets assigned 6 and so on no matter
203 what the original residue numbers were.
204 :type seqres_mapping: :class:`dict` (key: :class:`str`, value:
205 :class:`ost.seq.AlignmentHandle`)
206 :param bb_only: Only consider atoms with name "CA" in case of amino acids and
207 "C3'" for Nucleotides. this invalidates *compound_lib*.
208 Raises if any residue in *target* is not
209 `r.chem_class.IsPeptideLinking()` or
210 `r.chem_class.IsNucleotideLinking()`
211 :type bb_only: :class:`bool`
212 :raises: :class:`RuntimeError` if *target* contains compound which is not in
213 *compound_lib*, :class:`RuntimeError` if *symmetry_settings*
214 specifies symmetric atoms that are not present in the according
215 compound in *compound_lib*, :class:`RuntimeError` if
216 *seqres_mapping* is not provided and *target* contains residue
217 numbers with insertion codes or the residue numbers for each chain
218 are not monotonically increasing, :class:`RuntimeError` if
219 *seqres_mapping* is provided but an alignment is invalid
220 (seq1 contains gaps, mismatch in seq1/seq2, seq2 does not match
221 residues in corresponding chains).
222 """
224 self,
225 target,
226 compound_lib=None,
227 custom_compounds=None,
228 inclusion_radius=15,
229 sequence_separation=0,
230 symmetry_settings=None,
231 seqres_mapping=dict(),
232 bb_only=False
233 ):
234
235 if target.atom_count == 0:
236 raise RuntimeError("LDDT: target has no atoms")
237 self.target = target
238 self.inclusion_radius = inclusion_radius
239 self.sequence_separation = sequence_separation
240 if compound_lib is None:
241 compound_lib = conop.GetDefaultLib()
242 if compound_lib is None:
243 raise RuntimeError("No compound_lib given and conop.GetDefaultLib "
244 "returns no valid compound library")
245 self.compound_lib = compound_lib
246 self.custom_compounds = custom_compounds
247 if symmetry_settings is None:
249 else:
250 self.symmetry_settings = symmetry_settings
251
252 # whether to only consider atoms with name "CA" (amino acids) or C3'
253 # (nucleotides), invalidates *compound_lib*
254 self.bb_only=bb_only
255
256 # names of heavy atoms of each unique compound present in *target* as
257 # extracted from *compound_lib*, e.g.
258 # self.compound_anames["GLY"] = ["N", "CA", "C", "O"]
259 self.compound_anames = dict()
260
261 # stores symmetry information for those compounds as defined in
262 # *symmetry_settings*
264
265 # list of len(target.chains) containing all chain names in *target*
266 self.chain_names = list()
267
268 # list of len(target.residues) containing all compound names in *target*
269 self.compound_names = list()
270
271 # list of len(target.residues) defining start pos in internal reference
272 # positions for each residue
273 self.res_start_indices = list()
274
275 # list of len(target.residues) defining residue numbers in target
276 self.res_resnums = list()
277
278 # list of len(target.chains) defining start pos in internal reference
279 # positions for each chain
281
282 # list of len(target.chains) defining start pos in self.compound_names
283 # for each chain
285
286 # maps residues in *target* to indices in
287 # self.compound_names/self.res_start_indices. A residue gets identified
288 # by a tuple (first element: chain name, second element: residue number,
289 # residue number is either the actual residue number in *target* or
290 # given by *seqres_mapping*)
291 self.res_mapper = dict()
292
293 # number of atoms as specified in compounds. not all are necessarily
294 # covered by structure
295 self.n_atoms = None
296
297 # stores an index for each AtomHandle in *target*
298 # (atom hashcode => index)
299 self.atom_indices = dict()
300
301 # store indices of all atoms that have symmetry properties
302 self.symmetric_atoms = set()
303
304 # the actual target positions in a numpy array of shape (self.n_atoms,3)
305 self.positions = None
306
307 # setup members defined above
309 self.symmetry_settings, seqres_mapping, self.bb_only)
310
311 # distance related members are lazily computed as they're affected
312 # by different flavours of LDDT (e.g. LDDT including inter-chain
313 # contacts or not etc.)
314
315 # stores for each atom the other atoms within inclusion_radius
316 self._ref_indices = None
317 # the corresponding distances
318 self._ref_distances = None
319
320 # The following lists will be sparsely populated. We keep for each
321 # symmetry related atom the distances towards all atoms which are NOT
322 # affected by symmetry. So we can evaluate two symmetric versions
323 # against the fixed stuff later on and select the better scoring one.
326
327 # exactly the same as above but without interchain contacts
328 # => single-chain (sc)
329 self._ref_indices_sc = None
333
334 # exactly the same as above but without intrachain contacts
335 # => inter-chain (ic)
336 self._ref_indices_ic = None
340
341 # input parameter checking
343
344 @property
345 def ref_indices(self):
346 if self._ref_indices is None:
347 self._ref_indices, self._ref_distances = \
348 lDDTScorer._SetupDistances(self.target, self.n_atoms,
349 self.atom_indices,
350 self.inclusion_radius)
351 return self._ref_indices
352
353 @property
354 def ref_distances(self):
355 if self._ref_distances is None:
356 self._ref_indices, self._ref_distances = \
357 lDDTScorer._SetupDistances(self.target, self.n_atoms,
358 self.atom_indices,
359 self.inclusion_radius)
360 return self._ref_distances
361
362 @property
364 if self._sym_ref_indices is None:
366 lDDTScorer._NonSymDistances(self.n_atoms, self.symmetric_atoms,
368 return self._sym_ref_indices
369
370 @property
372 if self._sym_ref_distances is None:
374 lDDTScorer._NonSymDistances(self.n_atoms, self.symmetric_atoms,
376 return self._sym_ref_distances
377
378 @property
379 def ref_indices_sc(self):
380 if self._ref_indices_sc is None:
382 lDDTScorer._SetupDistancesSC(self.n_atoms,
386 return self._ref_indices_sc
387
388 @property
390 if self._ref_distances_sc is None:
392 lDDTScorer._SetupDistancesSC(self.n_atoms,
396 return self._ref_distances_sc
397
398 @property
400 if self._sym_ref_indices_sc is None:
402 lDDTScorer._NonSymDistances(self.n_atoms,
403 self.symmetric_atoms,
406 return self._sym_ref_indices_sc
407
408 @property
410 if self._sym_ref_distances_sc is None:
412 lDDTScorer._NonSymDistances(self.n_atoms,
413 self.symmetric_atoms,
416 return self._sym_ref_distances_sc
417
418 @property
419 def ref_indices_ic(self):
420 if self._ref_indices_ic is None:
422 lDDTScorer._SetupDistancesIC(self.n_atoms,
426 return self._ref_indices_ic
427
428 @property
430 if self._ref_distances_ic is None:
432 lDDTScorer._SetupDistancesIC(self.n_atoms,
436 return self._ref_distances_ic
437
438 @property
440 if self._sym_ref_indices_ic is None:
442 lDDTScorer._NonSymDistances(self.n_atoms,
443 self.symmetric_atoms,
446 return self._sym_ref_indices_ic
447
448 @property
450 if self._sym_ref_distances_ic is None:
452 lDDTScorer._NonSymDistances(self.n_atoms,
453 self.symmetric_atoms,
456 return self._sym_ref_distances_ic
457
458 def lDDT(self, model, thresholds = [0.5, 1.0, 2.0, 4.0],
459 local_lddt_prop=None, local_contact_prop=None,
460 chain_mapping=None, no_interchain=False,
461 no_intrachain=False, penalize_extra_chains=False,
462 residue_mapping=None, return_dist_test=False,
463 check_resnames=True, add_mdl_contacts=False,
464 interaction_data=None, set_atom_props=False):
465 """Computes LDDT of *model* - globally and per-residue
466
467 :param model: Model to be scored - models are preferably scored upon
468 performing stereo-chemistry checks in order to punish for
469 non-sensical irregularities. This must be done separately
470 as a pre-processing step. Target contacts that are not
471 covered by *model* are considered not conserved, thus
472 decreasing LDDT score. This also includes missing model
473 chains or model chains for which no mapping is provided in
474 *chain_mapping*.
475 :type model: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
476 :param thresholds: Thresholds of distance differences to be considered
477 as correct - see docs in constructor for more info.
478 default: [0.5, 1.0, 2.0, 4.0]
479 :type thresholds: :class:`list` of :class:`floats`
480 :param local_lddt_prop: If set, per-residue scores will be assigned as
481 generic float property of that name
482 :type local_lddt_prop: :class:`str`
483 :param local_contact_prop: If set, number of expected contacts as well
484 as number of conserved contacts will be
485 assigned as generic int property.
486 Excected contacts will be set as
487 <local_contact_prop>_exp, conserved contacts
488 as <local_contact_prop>_cons. Values
489 are summed over all thresholds.
490 :type local_contact_prop: :class:`str`
491 :param chain_mapping: Mapping of model chains (key) onto target chains
492 (value). This is required if target or model have
493 more than one chain.
494 :type chain_mapping: :class:`dict` with :class:`str` as keys/values
495 :param no_interchain: Whether to exclude interchain contacts
496 :type no_interchain: :class:`bool`
497 :param no_intrachain: Whether to exclude intrachain contacts (i.e. only
498 consider interface related contacts)
499 :type no_intrachain: :class:`bool`
500 :param penalize_extra_chains: Whether to include a fixed penalty for
501 additional chains in the model that are
502 not mapped to the target. ONLY AFFECTS
503 RETURNED GLOBAL SCORE. In detail: adds the
504 number of intra-chain contacts of each
505 extra chain to the expected contacts, thus
506 adding a penalty.
507 :type penalize_extra_chains: :class:`bool`
508 :param residue_mapping: By default, residue mapping is based on residue
509 numbers. That means, a model chain and the
510 respective target chain map to the same
511 underlying reference sequence (SEQRES).
512 Alternatively, you can specify one or
513 several alignment(s) between model and target
514 chains by providing a dictionary. key: Name
515 of chain in model (respective target chain is
516 extracted from *chain_mapping*),
517 value: Alignment with first sequence
518 corresponding to target chain and second
519 sequence to model chain. There is NO reference
520 sequence involved, so the two sequences MUST
521 exactly match the actual residues observed in
522 the respective target/model chains (ATOMSEQ).
523 :type residue_mapping: :class:`dict` with key: :class:`str`,
524 value: :class:`ost.seq.AlignmentHandle`
525 :param return_dist_test: Whether to additionally return the underlying
526 per-residue data for the distance difference
527 test. Adds five objects to the return tuple.
528 First: Number of total contacts summed over all
529 thresholds
530 Second: Number of conserved contacts summed
531 over all thresholds
532 Third: list with length of scored residues.
533 Contains indices referring to model.residues.
534 Fourth: numpy array of size
535 len(scored_residues) containing the number of
536 total contacts,
537 Fifth: numpy matrix of shape
538 (len(scored_residues), len(thresholds))
539 specifying how many for each threshold are
540 conserved.
541 :param check_resnames: On by default. Enforces residue name matches
542 between mapped model and target residues.
543 :type check_resnames: :class:`bool`
544 :param add_mdl_contacts: Adds model contacts - Only using contacts that
545 are within a certain distance threshold in the
546 target does not penalize for added model
547 contacts. If set to True, this flag will also
548 consider target contacts that are within the
549 specified distance threshold in the model but
550 not necessarily in the target. No contact will
551 be added if the respective atom pair is not
552 resolved in the target.
553 :type add_mdl_contacts: :class:`bool`
554 :param interaction_data: Pro param - don't use
555 :type interaction_data: :class:`tuple`
556 :param set_atom_props: If True, sets generic properties on a per atom
557 level if *local_lddt_prop*/*local_contact_prop*
558 are set as well.
559 In other words: this is the only way you can
560 get per-atom LDDT values.
561 :type set_atom_props: :class:`bool`
562
563 :returns: global and per-residue LDDT scores as a tuple -
564 first element is global LDDT score (None if *target* has no
565 contacts) and second element a list of per-residue scores with
566 length len(*model*.residues). None is assigned to residues that
567 are not covered by target. If a residue is covered but has no
568 contacts in *target*, 0.0 is assigned.
569 """
570 if chain_mapping is None:
571 if len(self.chain_names) > 1 or len(model.chains) > 1:
572 raise NotImplementedError("Must provide chain mapping if "
573 "target or model have > 1 chains.")
574 chain_mapping = {model.chains[0].GetName(): self.chain_names[0]}
575 else:
576 # check whether chains specified in mapping exist
577 for model_chain, target_chain in chain_mapping.items():
578 if target_chain not in self.chain_names:
579 raise RuntimeError(f"Target chain specified in "
580 f"chain_mapping ({target_chain}) does "
581 f"not exist. Target has chains: "
582 f"{self.chain_names}")
583 ch = model.FindChain(model_chain)
584 if not ch.IsValid():
585 raise RuntimeError(f"Model chain specified in "
586 f"chain_mapping ({model_chain}) does "
587 f"not exist. Model has chains: "
588 f"{[c.GetName() for c in model.chains]}")
589
590 # data objects defining model data - see _ProcessModel for rough
591 # description
592 pos, res_ref_atom_indices, res_atom_indices, res_atom_hashes, \
593 res_indices, ref_res_indices, symmetries = \
594 self._ProcessModel(model, chain_mapping,
595 residue_mapping = residue_mapping,
596 nirvana_dist = self.inclusion_radius + max(thresholds),
597 check_resnames = check_resnames)
598
599 if no_interchain and no_intrachain:
600 raise RuntimeError("no_interchain and no_intrachain flags are "
601 "mutually exclusive")
602
603 sym_ref_indices = None
604 sym_ref_distances = None
605 ref_indices = None
606 ref_distances = None
607
608 if interaction_data is None:
609 if no_interchain:
610 sym_ref_indices = self.sym_ref_indices_sc
611 sym_ref_distances = self.sym_ref_distances_sc
612 ref_indices = self.ref_indices_scref_indices_sc
613 ref_distances = self.ref_distances_scref_distances_sc
614 elif no_intrachain:
615 sym_ref_indices = self.sym_ref_indices_ic
616 sym_ref_distances = self.sym_ref_distances_ic
617 ref_indices = self.ref_indices_icref_indices_ic
618 ref_distances = self.ref_distances_icref_distances_ic
619 else:
620 sym_ref_indices = self.sym_ref_indices
621 sym_ref_distances = self.sym_ref_distances
622 ref_indices = self.ref_indicesref_indices
623 ref_distances = self.ref_distancesref_distances
624
625 if add_mdl_contacts:
626 ref_indices, ref_distances = \
627 self._AddMdlContacts(model, res_atom_indices, res_atom_hashes,
628 ref_indices, ref_distances,
629 no_interchain, no_intrachain)
630 # recompute symmetry related indices/distances
631 sym_ref_indices, sym_ref_distances = \
632 lDDTScorer._NonSymDistances(self.n_atoms, self.symmetric_atoms,
633 ref_indices, ref_distances)
634 else:
635 sym_ref_indices, sym_ref_distances, ref_indices, ref_distances = \
636 interaction_data
637
638 self._ResolveSymmetries(pos, thresholds, symmetries, sym_ref_indices,
639 sym_ref_distances)
640
641 atom_indices = list(itertools.chain.from_iterable(res_atom_indices))
642
643 per_atom_exp = np.asarray([self._GetNExp(i, ref_indices)
644 for i in atom_indices], dtype=np.int32)
645 per_res_exp = np.asarray([self._GetNExp(res_ref_atom_indices[idx],
646 ref_indices) for idx in range(len(res_indices))], dtype=np.int32)
647
648 per_atom_conserved = self._EvalAtoms(pos, atom_indices, thresholds,
649 ref_indices, ref_distances)
650 per_res_conserved = np.zeros((len(res_atom_indices), len(thresholds)),
651 dtype=np.int32)
652 start_idx = 0
653 for r_idx in range(len(res_atom_indices)):
654 end_idx = start_idx + len(res_atom_indices[r_idx])
655 per_res_conserved[r_idx] = np.sum(per_atom_conserved[start_idx:end_idx,:],
656 axis=0)
657 start_idx = end_idx
658
659 n_thresh = len(thresholds)
660
661 # do per-residue scores
662 per_res_lDDT = [None] * model.GetResidueCount()
663 for idx in range(len(res_indices)):
664 n_exp = n_thresh * per_res_exp[idx]
665 if n_exp > 0:
666 score = np.sum(per_res_conserved[idx,:]) / n_exp
667 per_res_lDDT[res_indices[idx]] = score
668 else:
669 per_res_lDDT[res_indices[idx]] = 0.0
670
671 # do full model score
672 n_distances = sum([len(x) for x in ref_indices])
673 if penalize_extra_chains:
674 n_distances += self._GetExtraModelChainPenalty(model, chain_mapping)
675
676 lDDT_tot = int(n_thresh * n_distances)
677 lDDT_cons = int(np.sum(per_res_conserved))
678 lDDT = None
679 if lDDT_tot > 0:
680 lDDT = float(lDDT_cons) / lDDT_tot
681
682 # set properties if necessary
683 if local_lddt_prop:
684 residues = model.residues
685 for idx in res_indices:
686 residues[idx].SetFloatProp(local_lddt_prop, per_res_lDDT[idx])
687
688 if local_contact_prop:
689 residues = model.residues
690 exp_prop = local_contact_prop + "_exp"
691 conserved_prop = local_contact_prop + "_cons"
692
693 for i, r_idx in enumerate(res_indices):
694 residues[r_idx].SetIntProp(exp_prop,
695 n_thresh * int(per_res_exp[i]))
696 residues[r_idx].SetIntProp(conserved_prop,
697 int(np.sum(per_res_conserved[i,:])))
698
699 if set_atom_props and (local_lddt_prop or local_contact_prop):
700 atom_list = list()
701 residues = model.residues
702 for i, indices in enumerate(res_atom_indices):
703 r = residues[res_indices[i]]
704 r_idx = ref_res_indices[i]
705 res_start_idx = self.res_start_indices[r_idx]
706 anames = self.compound_anames[self.compound_names[r_idx]]
707 for a_i in indices:
708 a = r.FindAtom(anames[a_i - res_start_idx])
709 assert(a.IsValid())
710 atom_list.append(a)
711
712 summed_per_atom_conserved = per_atom_conserved.sum(axis=1)
713 if local_lddt_prop:
714 # the only place where actually need to compute per-atom LDDT
715 # scores
716 for a_idx in range(len(atom_list)):
717 if per_atom_exp[a_idx] != 0:
718 tmp = summed_per_atom_conserved[a_idx] / per_atom_exp[a_idx]
719 tmp = tmp / n_thresh
720 atom_list[a_idx].SetFloatProp(local_lddt_prop, tmp)
721
722 if local_contact_prop:
723 conserved_prop = local_contact_prop + "_cons"
724 exp_prop = local_contact_prop + "_exp"
725 for a_idx in range(len(atom_list)):
726 # do number of conserved contacts
727 tmp = summed_per_atom_conserved[a_idx]
728 atom_list[a_idx].SetIntProp(conserved_prop, tmp)
729 # do number of expected contacts
730 tmp = per_atom_exp[a_idx] * n_thresh
731 atom_list[a_idx].SetIntProp(exp_prop, tmp)
732
733 if return_dist_test:
734 return lDDT, per_res_lDDT, lDDT_tot, lDDT_cons, res_indices, \
735 per_res_exp, per_res_conserved
736 else:
737 return lDDT, per_res_lDDT
738
739 def DRMSD(self, model, dist_cap = 5,
740 chain_mapping=None, no_interchain=False,
741 no_intrachain=False, residue_mapping=None,
742 check_resnames=True, add_mdl_contacts=False,
743 interaction_data=None):
744 """ DRMSD of *model* - globally and per-residue
745
746 Very similar to LDDT as we operate on distance differences for all
747 interatomic distances within the same inclusion radius as in LDDT.
748 DRMSD is the distance rmsd, i.e. the RMSD of distance differences.
749 Distance differences are capped at *dist_cap* which is also the default
750 value for missing distances.
751
752 :param model: Model to be scored - models are preferably scored upon
753 performing stereo-chemistry checks in order to punish for
754 non-sensical irregularities. This must be done separately
755 as a pre-processing step. Target contacts that are not
756 covered by *model* are considered not conserved, thus
757 increasing DRMSD score. This also includes missing model
758 chains or model chains for which no mapping is provided in
759 *chain_mapping*.
760 :type model: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
761 :param dist_cap: Cap for distance differences.
762 :type dist_cap: :class:`float`
763 :param chain_mapping: Mapping of model chains (key) onto target chains
764 (value). This is required if target or model have
765 more than one chain.
766 :type chain_mapping: :class:`dict` with :class:`str` as keys/values
767 :param no_interchain: Whether to exclude interchain contacts
768 :type no_interchain: :class:`bool`
769 :param no_intrachain: Whether to exclude intrachain contacts (i.e. only
770 consider interface related contacts)
771 :type no_intrachain: :class:`bool`
772 :param residue_mapping: By default, residue mapping is based on residue
773 numbers. That means, a model chain and the
774 respective target chain map to the same
775 underlying reference sequence (SEQRES).
776 Alternatively, you can specify one or
777 several alignment(s) between model and target
778 chains by providing a dictionary. key: Name
779 of chain in model (respective target chain is
780 extracted from *chain_mapping*),
781 value: Alignment with first sequence
782 corresponding to target chain and second
783 sequence to model chain. There is NO reference
784 sequence involved, so the two sequences MUST
785 exactly match the actual residues observed in
786 the respective target/model chains (ATOMSEQ).
787 :type residue_mapping: :class:`dict` with key: :class:`str`,
788 value: :class:`ost.seq.AlignmentHandle`
789 :param check_resnames: On by default. Enforces residue name matches
790 between mapped model and target residues.
791 :type check_resnames: :class:`bool`
792 :param add_mdl_contacts: Adds model contacts - Only using contacts that
793 are within a certain distance threshold in the
794 target does not penalize for added model
795 contacts. If set to True, this flag will also
796 consider target contacts that are within the
797 specified distance threshold in the model but
798 not necessarily in the target. No contact will
799 be added if the respective atom pair is not
800 resolved in the target.
801 :type add_mdl_contacts: :class:`bool`
802 :param interaction_data: Pro param - don't use
803 :type interaction_data: :class:`tuple`
804
805 :returns: global and per-residue DRMSD scores as a tuple -
806 first element is global DRMSD score (None if *target* has no
807 contacts) and second element a list of per-residue scores with
808 length len(*model*.residues). None is assigned to residues that
809 are not covered by target. If a residue is covered but has no
810 contacts in *target*, None is assigned.
811 """
812 if chain_mapping is None:
813 if len(self.chain_names) > 1 or len(model.chains) > 1:
814 raise NotImplementedError("Must provide chain mapping if "
815 "target or model have > 1 chains.")
816 chain_mapping = {model.chains[0].GetName(): self.chain_names[0]}
817 else:
818 # check whether chains specified in mapping exist
819 for model_chain, target_chain in chain_mapping.items():
820 if target_chain not in self.chain_names:
821 raise RuntimeError(f"Target chain specified in "
822 f"chain_mapping ({target_chain}) does "
823 f"not exist. Target has chains: "
824 f"{self.chain_names}")
825 ch = model.FindChain(model_chain)
826 if not ch.IsValid():
827 raise RuntimeError(f"Model chain specified in "
828 f"chain_mapping ({model_chain}) does "
829 f"not exist. Model has chains: "
830 f"{[c.GetName() for c in model.chains]}")
831
832 # data objects defining model data - see _ProcessModel for rough
833 # description
834 pos, res_ref_atom_indices, res_atom_indices, res_atom_hashes, \
835 res_indices, ref_res_indices, symmetries = \
836 self._ProcessModel(model, chain_mapping,
837 residue_mapping = residue_mapping,
838 nirvana_dist = self.inclusion_radius + dist_cap,
839 check_resnames = check_resnames)
840
841 if no_interchain and no_intrachain:
842 raise RuntimeError("no_interchain and no_intrachain flags are "
843 "mutually exclusive")
844
845 sym_ref_indices = None
846 sym_ref_distances = None
847 ref_indices = None
848 ref_distances = None
849
850 if interaction_data is None:
851 if no_interchain:
852 sym_ref_indices = self.sym_ref_indices_sc
853 sym_ref_distances = self.sym_ref_distances_sc
854 ref_indices = self.ref_indices_scref_indices_sc
855 ref_distances = self.ref_distances_scref_distances_sc
856 elif no_intrachain:
857 sym_ref_indices = self.sym_ref_indices_ic
858 sym_ref_distances = self.sym_ref_distances_ic
859 ref_indices = self.ref_indices_icref_indices_ic
860 ref_distances = self.ref_distances_icref_distances_ic
861 else:
862 sym_ref_indices = self.sym_ref_indices
863 sym_ref_distances = self.sym_ref_distances
864 ref_indices = self.ref_indicesref_indices
865 ref_distances = self.ref_distancesref_distances
866
867 if add_mdl_contacts:
868 ref_indices, ref_distances = \
869 self._AddMdlContacts(model, res_atom_indices, res_atom_hashes,
870 ref_indices, ref_distances,
871 no_interchain, no_intrachain)
872 # recompute symmetry related indices/distances
873 sym_ref_indices, sym_ref_distances = \
874 lDDTScorer._NonSymDistances(self.n_atoms, self.symmetric_atoms,
875 ref_indices, ref_distances)
876 else:
877 sym_ref_indices, sym_ref_distances, ref_indices, ref_distances = \
878 interaction_data
879
880 self._ResolveSymmetriesSSD(pos, dist_cap, symmetries, sym_ref_indices,
881 sym_ref_distances)
882
883 atom_indices = list(itertools.chain.from_iterable(res_atom_indices))
884
885 per_atom_exp = np.asarray([self._GetNExp(i, ref_indices)
886 for i in atom_indices], dtype=np.int32)
887 per_res_exp = np.asarray([self._GetNExp(res_ref_atom_indices[idx],
888 ref_indices) for idx in range(len(res_indices))], dtype=np.int32)
889 per_atom_ssd = self._EvalAtomsSSD(pos, atom_indices, dist_cap,
890 ref_indices, ref_distances)
891
892 # do per residue scores
893 start_idx = 0
894 per_res_drmsd = [None] * model.GetResidueCount()
895 for r_idx in range(len(res_atom_indices)):
896 end_idx = start_idx + len(res_atom_indices[r_idx])
897 n_tot = per_res_exp[r_idx]
898 if n_tot > 0:
899 ssd = np.sum(per_atom_ssd[start_idx:end_idx])
900 # add penalties from distances involving atoms that are not
901 # present in the model
902 n_missing = n_tot - np.sum(per_atom_exp[start_idx:end_idx])
903 ssd += n_missing*dist_cap*dist_cap
904 per_res_drmsd[res_indices[r_idx]] = np.sqrt(ssd/n_tot)
905 start_idx = end_idx
906
907 # do full model score
908 drmsd = None
909 n_tot = sum([len(x) for x in ref_indices])
910 if n_tot > 0:
911 ssd = np.sum(per_atom_ssd)
912 # add penalties from distances involving atoms that are not
913 # present in the model
914 n_missing = n_tot - np.sum(per_atom_exp)
915 ssd += (dist_cap*dist_cap*n_missing)
916 drmsd = np.sqrt(ssd/n_tot)
917
918 return drmsd, per_res_drmsd
919
920 def GetNChainContacts(self, target_chain, no_interchain=False):
921 """Returns number of contacts expected for a certain chain in *target*
922
923 :param target_chain: Chain in *target* for which you want the number
924 of expected contacts
925 :type target_chain: :class:`str`
926 :param no_interchain: Whether to exclude interchain contacts
927 :type no_interchain: :class:`bool`
928 :raises: :class:`RuntimeError` if specified chain doesnt exist
929 """
930 if target_chain not in self.chain_names:
931 raise RuntimeError(f"Specified chain name ({target_chain}) not in "
932 f"target")
933 ch_idx = self.chain_names.index(target_chain)
934 s = self.chain_start_indices[ch_idx]
935 e = self.n_atoms
936 if ch_idx + 1 < len(self.chain_names):
937 e = self.chain_start_indices[ch_idx+1]
938 if no_interchain:
939 return self._GetNExp(list(range(s, e)), self.ref_indices_scref_indices_sc)
940 else:
941 return self._GetNExp(list(range(s, e)), self.ref_indicesref_indices)
942
943 def _ProcessModel(self, model, chain_mapping, residue_mapping = None,
944 nirvana_dist = 100,
945 check_resnames = True):
946 """ Helper that generates data structures from model
947 """
948
949 # initialize positions with values far in nirvana. If a position is not
950 # set, it should be far away from any position in model.
951 max_pos = model.bounds.GetMax()
952 max_coordinate = abs(max(max_pos[0], max_pos[1], max_pos[2]))
953 max_coordinate += 42 * nirvana_dist
954 pos = np.ones((self.n_atoms, 3), dtype=np.float32) * max_coordinate
955
956 # for each scored residue in model a list of indices describing the
957 # atoms from the reference that should be there
958 res_ref_atom_indices = list()
959
960 # for each scored residue in model a list of indices of atoms that are
961 # actually there
962 res_atom_indices = list()
963
964 # and the respective hash codes
965 # this is required if add_mdl_contacts is set to True
966 res_atom_hashes = list()
967
968 # indices of the scored residues
969 res_indices = list()
970
971 # respective residue indices in reference
972 ref_res_indices = list()
973
974 # Will contain one element per symmetry group
975 symmetries = list()
976
977 current_model_res_idx = -1
978 for ch in model.chains:
979 model_ch_name = ch.GetName()
980 if model_ch_name not in chain_mapping:
981 current_model_res_idx += len(ch.residues)
982 continue # additional model chain which is not mapped
983 target_ch_name = chain_mapping[model_ch_name]
984
985 rnums = self._GetChainRNums(ch, residue_mapping, model_ch_name,
986 target_ch_name)
987
988 for r, rnum in zip(ch.residues, rnums):
989 current_model_res_idx += 1
990 res_mapper_key = (target_ch_name, rnum)
991 if res_mapper_key not in self.res_mapper:
992 continue
993 r_idx = self.res_mapper[res_mapper_key]
994 if check_resnames and r.name != self.compound_names[r_idx]:
995 raise RuntimeError(
996 f"Residue name mismatch for {r}, "
997 f" expect {self.compound_names[r_idx]}"
998 )
999 res_start_idx = self.res_start_indices[r_idx]
1000 rname = self.compound_names[r_idx]
1001 anames = self.compound_anames[rname]
1002 atoms = [r.FindAtom(aname) for aname in anames]
1003 res_ref_atom_indices.append(
1004 list(range(res_start_idx, res_start_idx + len(anames)))
1005 )
1006 res_atom_indices.append(list())
1007 res_atom_hashes.append(list())
1008 res_indices.append(current_model_res_idx)
1009 ref_res_indices.append(r_idx)
1010 for a_idx, a in enumerate(atoms):
1011 if a.IsValid():
1012 p = a.GetPos()
1013 pos[res_start_idx + a_idx][0] = p[0]
1014 pos[res_start_idx + a_idx][1] = p[1]
1015 pos[res_start_idx + a_idx][2] = p[2]
1016 res_atom_indices[-1].append(res_start_idx + a_idx)
1017 res_atom_hashes[-1].append(a.handle.GetHashCode())
1018 if rname in self.compound_symmetric_atoms:
1019 sym_indices = list()
1020 for sym_tuple in self.compound_symmetric_atoms[rname]:
1021 a_one = atoms[sym_tuple[0]]
1022 a_two = atoms[sym_tuple[1]]
1023 if a_one.IsValid() and a_two.IsValid():
1024 sym_indices.append(
1025 (
1026 res_start_idx + sym_tuple[0],
1027 res_start_idx + sym_tuple[1],
1028 )
1029 )
1030 if len(sym_indices) > 0:
1031 symmetries.append(sym_indices)
1032
1033 return (pos, res_ref_atom_indices, res_atom_indices, res_atom_hashes,
1034 res_indices, ref_res_indices, symmetries)
1035
1036
1037 def _GetExtraModelChainPenalty(self, model, chain_mapping):
1038 """Counts n distances in extra model chains to be added as penalty
1039 """
1040 penalty = 0
1041 for chain in model.chains:
1042 ch_name = chain.GetName()
1043 if ch_name not in chain_mapping:
1044 sm = self.symmetry_settings
1045 mdl_sel = model.Select(f"cname={mol.QueryQuoteName(ch_name)}")
1046 dummy_scorer = lDDTScorer(mdl_sel, self.compound_lib,
1047 symmetry_settings = sm,
1048 inclusion_radius = self.inclusion_radius,
1049 bb_only = self.bb_only)
1050 penalty += sum([len(x) for x in dummy_scorer.ref_indices])
1051 return penalty
1052
1053 def _GetChainRNums(self, ch, residue_mapping, model_ch_name,
1054 target_ch_name):
1055 """Map residues in model chain to target residues
1056
1057 There are two options: one is simply using residue numbers,
1058 the other is a custom mapping as given in *residue_mapping*
1059 """
1060 if residue_mapping and model_ch_name in residue_mapping:
1061 # extract residue numbers from target chain
1062 ch_idx = self.chain_names.index(target_ch_name)
1063 start_idx = self.chain_res_start_indices[ch_idx]
1064 if ch_idx < len(self.chain_names) - 1:
1065 end_idx = self.chain_res_start_indices[ch_idx+1]
1066 else:
1067 end_idx = len(self.compound_names)
1068 target_rnums = self.res_resnums[start_idx:end_idx]
1069 # get sequences from alignment and do consistency checks
1070 target_seq = residue_mapping[model_ch_name].GetSequence(0)
1071 model_seq = residue_mapping[model_ch_name].GetSequence(1)
1072 if len(target_seq.GetGaplessString()) != len(target_rnums):
1073 raise RuntimeError(f"Try to perform residue mapping for "
1074 f"model chain {model_ch_name} which "
1075 f"maps to {target_ch_name} in target. "
1076 f"Target sequence in alignment suggests "
1077 f"{len(target_seq.GetGaplessString())} "
1078 f"residues but {len(target_rnums)} are "
1079 f"expected.")
1080 if len(model_seq.GetGaplessString()) != len(ch.residues):
1081 raise RuntimeError(f"Try to perform residue mapping for "
1082 f"model chain {model_ch_name} which "
1083 f"maps to {target_ch_name} in target. "
1084 f"Model sequence in alignment suggests "
1085 f"{len(model_seq.GetGaplessString())} "
1086 f"residues but {len(ch.residues)} are "
1087 f"expected.")
1088 rnums = list()
1089 target_idx = -1
1090 for col in residue_mapping[model_ch_name]:
1091 if col[0] != '-':
1092 target_idx += 1
1093 # handle match
1094 if col[0] != '-' and col[1] != '-':
1095 rnums.append(target_rnums[target_idx])
1096 # insertion in model adds None to rnum
1097 if col[0] == '-' and col[1] != '-':
1098 rnums.append(None)
1099 else:
1100 rnums = [r.GetNumber() for r in ch.residues]
1101
1102 return rnums
1103
1104
1105 def _SetupEnv(self, compound_lib, custom_compounds, symmetry_settings,
1106 seqres_mapping, bb_only):
1107 """Sets target related lDDTScorer members defined in constructor
1108
1109 No distance related members - see _SetupDistances
1110 """
1111 residue_numbers = self._GetTargetResidueNumbers(self.target,
1112 seqres_mapping)
1113 current_idx = 0
1114 positions = list()
1115 for chain in self.target.chains:
1116 ch_name = chain.GetName()
1117 self.chain_names.append(ch_name)
1118 self.chain_start_indices.append(current_idx)
1119 self.chain_res_start_indices.append(len(self.compound_names))
1120 for r, rnum in zip(chain.residues, residue_numbers[ch_name]):
1121 if r.name not in self.compound_anames:
1122 # sets compound info in self.compound_anames and
1123 # self.compound_symmetric_atoms
1124 self._SetupCompound(r, compound_lib, custom_compounds,
1125 symmetry_settings, bb_only)
1126
1127 self.res_start_indices.append(current_idx)
1128 self.res_mapper[(ch_name, rnum)] = len(self.compound_names)
1129 self.compound_names.append(r.name)
1130 self.res_resnums.append(rnum)
1131
1132 atoms = [r.FindAtom(an) for an in self.compound_anames[r.name]]
1133 for a in atoms:
1134 if a.IsValid():
1135 self.atom_indices[a.handle.GetHashCode()] = current_idx
1136 p = a.GetPos()
1137 positions.append(np.asarray([p[0], p[1], p[2]],
1138 dtype=np.float32))
1139 else:
1140 positions.append(np.zeros(3, dtype=np.float32))
1141 current_idx += 1
1142
1143 if r.name in self.compound_symmetric_atoms:
1144 for sym_tuple in self.compound_symmetric_atoms[r.name]:
1145 for a_idx in sym_tuple:
1146 a = atoms[a_idx]
1147 if a.IsValid():
1148 hashcode = a.handle.GetHashCode()
1149 self.symmetric_atoms.add(
1150 self.atom_indices[hashcode]
1151 )
1152 self.positions = np.vstack(positions)
1153 self.n_atoms = current_idx
1154
1155 def _GetTargetResidueNumbers(self, target, seqres_mapping):
1156 """Returns residue numbers for each chain in target as dict
1157
1158 They're either directly extracted from the raw residue number
1159 from the structure or from user provided alignments
1160 """
1161 residue_numbers = dict()
1162 for ch in target.chains:
1163 ch_name = ch.GetName()
1164 rnums = list()
1165 if ch_name in seqres_mapping:
1166 seqres = seqres_mapping[ch_name].GetSequence(0).GetString()
1167 atomseq = seqres_mapping[ch_name].GetSequence(1).GetString()
1168 # SEQRES must not contain gaps
1169 if "-" in seqres:
1170 raise RuntimeError(
1171 "SEQRES in seqres_mapping must not " "contain gaps"
1172 )
1173 atomseq_from_chain = [r.one_letter_code for r in ch.residues]
1174 if atomseq.replace("-", "") != atomseq_from_chain:
1175 raise RuntimeError(
1176 "ATOMSEQ in seqres_mapping must match "
1177 "raw sequence extracted from chain "
1178 "residues"
1179 )
1180 rnum = 0
1181 for seqres_olc, atomseq_olc in zip(seqres, atomseq):
1182 if seqres_olc != "-":
1183 rnum += 1
1184 if atomseq_olc != "-":
1185 if seqres_olc != atomseq_olc:
1186 raise RuntimeError(
1187 f"Residue with number {rnum} in "
1188 f"chain {ch_name} has SEQRES "
1189 f"ATOMSEQ mismatch"
1190 )
1191 rnums.append(mol.ResNum(rnum))
1192 else:
1193 rnums = [r.GetNumber() for r in ch.residues]
1194 assert len(rnums) == len(ch.residues)
1195 residue_numbers[ch_name] = rnums
1196 return residue_numbers
1197
1198 def _SetupCompound(self, r, compound_lib, custom_compounds,
1199 symmetry_settings, bb_only):
1200 """fill self.compound_anames/self.compound_symmetric_atoms
1201 """
1202 if bb_only:
1203 # throw away compound_lib info
1204 if r.chem_class.IsPeptideLinking():
1205 self.compound_anames[r.name] = ["CA"]
1206 elif r.chem_class.IsNucleotideLinking():
1207 self.compound_anames[r.name] = ["C3'"]
1208 else:
1209 raise RuntimeError(f"Only support amino acids and nucleotides "
1210 f"if bb_only is True, failed with {str(r)}")
1211 self.compound_symmetric_atoms[r.name] = list()
1212 else:
1213 atom_names = list()
1214 symmetric_atoms = list()
1215 if custom_compounds is not None and r.GetName() in custom_compounds:
1216 atom_names = list(custom_compounds[r.GetName()].atom_names)
1217 else:
1218 compound = compound_lib.FindCompound(r.name)
1219 if compound is None:
1220 raise RuntimeError(f"no entry for {r} in compound_lib")
1221 for atom_spec in compound.GetAtomSpecs():
1222 if atom_spec.element not in ["H", "D"]:
1223 atom_names.append(atom_spec.name)
1224 if r.name in symmetry_settings.symmetric_compounds:
1225 for pair in symmetry_settings.symmetric_compounds[r.name]:
1226 try:
1227 a = atom_names.index(pair[0])
1228 b = atom_names.index(pair[1])
1229 except:
1230 msg = f"Could not find symmetric atoms "
1231 msg += f"({pair[0]}, {pair[1]}) for {r.name} "
1232 msg += f"as specified in SymmetrySettings in "
1233 msg += f"compound from component dictionary. "
1234 msg += f"Atoms in compound: {atom_names}"
1235 raise RuntimeError(msg)
1236 symmetric_atoms.append((a, b))
1237 self.compound_anames[r.name] = atom_names
1238 if len(symmetric_atoms) > 0:
1239 self.compound_symmetric_atoms[r.name] = symmetric_atoms
1240
1241 def _AddMdlContacts(self, model, res_atom_indices, res_atom_hashes,
1242 ref_indices, ref_distances, no_interchain,
1243 no_intrachain):
1244
1245 # buildup an index map for mdl atoms that are also present in target
1246 in_target = np.zeros(self.n_atoms, dtype=bool)
1247 for i in self.atom_indices.values():
1248 in_target[i] = True
1249 mdl_atom_indices = dict()
1250 for at_indices, at_hashes in zip(res_atom_indices, res_atom_hashes):
1251 for i, h in zip(at_indices, at_hashes):
1252 if in_target[i]:
1253 mdl_atom_indices[h] = i
1254
1255 # get contacts for mdl - the contacts are only from atom pairs that
1256 # are also present in target, as we only provide the respective
1257 # hashes in mdl_atom_indices
1258 mdl_ref_indices, mdl_ref_distances = \
1259 lDDTScorer._SetupDistances(model, self.n_atoms, mdl_atom_indices,
1260 self.inclusion_radius)
1261 if no_interchain:
1262 mdl_ref_indices, mdl_ref_distances = \
1263 lDDTScorer._SetupDistancesSC(self.n_atoms,
1265 mdl_ref_indices,
1266 mdl_ref_distances)
1267
1268 if no_intrachain:
1269 mdl_ref_indices, mdl_ref_distances = \
1270 lDDTScorer._SetupDistancesIC(self.n_atoms,
1272 mdl_ref_indices,
1273 mdl_ref_distances)
1274
1275 # update ref_indices/ref_distances => add mdl contacts
1276 for i in range(self.n_atoms):
1277 mask = np.isin(mdl_ref_indices[i], ref_indices[i],
1278 assume_unique=True, invert=True)
1279 if np.sum(mask) > 0:
1280 added_mdl_indices = mdl_ref_indices[i][mask]
1281 ref_indices[i] = np.append(ref_indices[i],
1282 added_mdl_indices)
1283
1284 # distances need to be recomputed from ref positions
1285 tmp = self.positions.take(added_mdl_indices, axis=0)
1286 np.subtract(tmp, self.positions[i][None, :], out=tmp)
1287 np.square(tmp, out=tmp)
1288 tmp = tmp.sum(axis=1)
1289 np.sqrt(tmp, out=tmp) # distances against all relevant atoms
1290 ref_distances[i] = np.append(ref_distances[i], tmp)
1291
1292 return (ref_indices, ref_distances)
1293
1294
1295
1296 @staticmethod
1297 def _SetupDistances(structure, n_atoms, atom_index_mapping,
1298 inclusion_radius):
1299
1300 """Compute distance related members of lDDTScorer
1301
1302 Brute force all vs all distance computation kills LDDT for large
1303 complexes. Instead of building some KD tree data structure, we make use
1304 of expected spatial proximity of atoms in the same chain. Distances are
1305 computed as follows:
1306
1307 - process each chain individually
1308 - perform crude collision detection
1309 - process potentially interacting chain pairs
1310 - concatenate distances from all processing steps
1311 """
1312 ref_indices = [np.asarray([], dtype=np.int32) for idx in range(n_atoms)]
1313 ref_distances = [np.asarray([], dtype=np.float32) for idx in range(n_atoms)]
1314
1315 indices = [list() for _ in range(n_atoms)]
1316 distances = [list() for _ in range(n_atoms)]
1317 per_chain_pos = list()
1318 per_chain_indices = list()
1319
1320 # Process individual chains
1321 for ch in structure.chains:
1322 pos_list = list()
1323 atom_indices = list()
1324 mask_start = list()
1325 mask_end = list()
1326 r_start_idx = 0
1327 for r_idx, r in enumerate(ch.residues):
1328 n_valid_atoms = 0
1329 for a in r.atoms:
1330 hash_code = a.handle.GetHashCode()
1331 if hash_code in atom_index_mapping:
1332 p = a.GetPos()
1333 pos_list.append(np.asarray([p[0], p[1], p[2]], dtype=np.float32))
1334 atom_indices.append(atom_index_mapping[hash_code])
1335 n_valid_atoms += 1
1336 mask_start.extend([r_start_idx] * n_valid_atoms)
1337 mask_end.extend([r_start_idx + n_valid_atoms] * n_valid_atoms)
1338 r_start_idx += n_valid_atoms
1339
1340 if len(pos_list) == 0:
1341 # nothing to do...
1342 continue
1343
1344 pos = np.vstack(pos_list)
1345 atom_indices = np.asarray(atom_indices, dtype=np.int32)
1346
1347 if atom_indices.shape[0] > 20000:
1348 dists = blockwise_cdist(pos, pos)
1349 else:
1350 dists = cdist(pos, pos)
1351
1352 # apply masks
1353 far_away = 2 * inclusion_radius
1354 for idx in range(atom_indices.shape[0]):
1355 dists[idx, range(mask_start[idx], mask_end[idx])] = far_away
1356
1357 # fish out and store close atoms within inclusion radius
1358 within_mask = dists < inclusion_radius
1359 for idx in range(atom_indices.shape[0]):
1360 indices_to_append = atom_indices[within_mask[idx,:]]
1361 if indices_to_append.shape[0] > 0:
1362 full_at_idx = atom_indices[idx]
1363 indices[full_at_idx].append(indices_to_append)
1364 distances[full_at_idx].append(dists[idx, within_mask[idx,:]])
1365
1366 dists = None
1367
1368 per_chain_pos.append(pos)
1369 per_chain_indices.append(atom_indices)
1370
1371 # perform crude collision detection
1372 min_pos = [p.min(0) for p in per_chain_pos]
1373 max_pos = [p.max(0) for p in per_chain_pos]
1374 chain_pairs = list()
1375 for idx_one in range(len(per_chain_pos)):
1376 for idx_two in range(idx_one + 1, len(per_chain_pos)):
1377 if np.max(min_pos[idx_one] - max_pos[idx_two]) > inclusion_radius:
1378 continue
1379 if np.max(min_pos[idx_two] - max_pos[idx_one]) > inclusion_radius:
1380 continue
1381 chain_pairs.append((idx_one, idx_two))
1382
1383 # process potentially interacting chains
1384 for pair in chain_pairs:
1385 if per_chain_pos[pair[0]].shape[0] > 20000 or per_chain_pos[pair[1]].shape[0] > 20000:
1386 dists = blockwise_cdist(per_chain_pos[pair[0]], per_chain_pos[pair[1]])
1387 else:
1388 dists = cdist(per_chain_pos[pair[0]], per_chain_pos[pair[1]])
1389 within = dists <= inclusion_radius
1390
1391 # process pair[0]
1392 tmp = within.sum(axis=1)
1393 for idx in range(tmp.shape[0]):
1394 if tmp[idx] > 0:
1395 # even though not being a strict requirement, we perform an
1396 # insertion here such that the indices for each atom will be
1397 # sorted after the hstack operation
1398 at_idx = per_chain_indices[pair[0]][idx]
1399 indices_to_insert = per_chain_indices[pair[1]][within[idx,:]]
1400 distances_to_insert = dists[idx, within[idx, :]]
1401 insertion_idx = len(indices[at_idx])
1402 for i in range(insertion_idx):
1403 if indices_to_insert[0] > indices[at_idx][i][0]:
1404 insertion_idx = i
1405 break
1406 indices[at_idx].insert(insertion_idx, indices_to_insert)
1407 distances[at_idx].insert(insertion_idx, distances_to_insert)
1408
1409 # process pair[1]
1410 tmp = within.sum(axis=0)
1411 for idx in range(tmp.shape[0]):
1412 if tmp[idx] > 0:
1413 # even though not being a strict requirement, we perform an
1414 # insertion here such that the indices for each atom will be
1415 # sorted after the hstack operation
1416 at_idx = per_chain_indices[pair[1]][idx]
1417 indices_to_insert = per_chain_indices[pair[0]][within[:, idx]]
1418 distances_to_insert = dists[within[:, idx], idx]
1419 insertion_idx = len(indices[at_idx])
1420 for i in range(insertion_idx):
1421 if indices_to_insert[0] > indices[at_idx][i][0]:
1422 insertion_idx = i
1423 break
1424 indices[at_idx].insert(insertion_idx, indices_to_insert)
1425 distances[at_idx].insert(insertion_idx, distances_to_insert)
1426
1427 dists = None
1428
1429 # concatenate distances from all processing steps
1430 for at_idx in range(n_atoms):
1431 if len(indices[at_idx]) > 0:
1432 ref_indices[at_idx] = np.hstack(indices[at_idx])
1433 ref_distances[at_idx] = np.hstack(distances[at_idx])
1434
1435 return (ref_indices, ref_distances)
1436
1437 @staticmethod
1438 def _SetupDistancesSC(n_atoms, chain_start_indices,
1439 ref_indices, ref_distances):
1440 """Select subset of contacts only covering intra-chain contacts
1441 """
1442 # init
1443 ref_indices_sc = [np.asarray([], dtype=np.int32) for idx in range(n_atoms)]
1444 ref_distances_sc = [np.asarray([], dtype=np.float32) for idx in range(n_atoms)]
1445
1446 n_chains = len(chain_start_indices)
1447 for ch_idx in range(n_chains):
1448 chain_s = chain_start_indices[ch_idx]
1449 chain_e = n_atoms
1450 if ch_idx + 1 < n_chains:
1451 chain_e = chain_start_indices[ch_idx+1]
1452 for i in range(chain_s, chain_e):
1453 if len(ref_indices[i]) > 0:
1454 intra_idx = np.where(np.logical_and(ref_indices[i]>=chain_s,
1455 ref_indices[i]<chain_e))[0]
1456 ref_indices_sc[i] = ref_indices[i][intra_idx]
1457 ref_distances_sc[i] = ref_distances[i][intra_idx]
1458
1459 return (ref_indices_sc, ref_distances_sc)
1460
1461 @staticmethod
1462 def _SetupDistancesIC(n_atoms, chain_start_indices,
1463 ref_indices, ref_distances):
1464 """Select subset of contacts only covering inter-chain contacts
1465 """
1466 # init
1467 ref_indices_ic = [np.asarray([], dtype=np.int32) for idx in range(n_atoms)]
1468 ref_distances_ic = [np.asarray([], dtype=np.float32) for idx in range(n_atoms)]
1469
1470 n_chains = len(chain_start_indices)
1471 for ch_idx in range(n_chains):
1472 chain_s = chain_start_indices[ch_idx]
1473 chain_e = n_atoms
1474 if ch_idx + 1 < n_chains:
1475 chain_e = chain_start_indices[ch_idx+1]
1476 for i in range(chain_s, chain_e):
1477 if len(ref_indices[i]) > 0:
1478 inter_idx = np.where(np.logical_or(ref_indices[i]<chain_s,
1479 ref_indices[i]>=chain_e))[0]
1480 ref_indices_ic[i] = ref_indices[i][inter_idx]
1481 ref_distances_ic[i] = ref_distances[i][inter_idx]
1482
1483 return (ref_indices_ic, ref_distances_ic)
1484
1485 @staticmethod
1486 def _NonSymDistances(n_atoms, symmetric_atoms, ref_indices, ref_distances):
1487 """Transfer indices/distances of non-symmetric atoms and return
1488 """
1489
1490 sym_ref_indices = [np.asarray([], dtype=np.int32) for idx in range(n_atoms)]
1491 sym_ref_distances = [np.asarray([], dtype=np.float32) for idx in range(n_atoms)]
1492
1493 for idx in symmetric_atoms:
1494 indices = list()
1495 distances = list()
1496 for i, d in zip(ref_indices[idx], ref_distances[idx]):
1497 if i not in symmetric_atoms:
1498 indices.append(i)
1499 distances.append(d)
1500 sym_ref_indices[idx] = np.asarray(indices, dtype=np.int32)
1501 sym_ref_distances[idx] = np.asarray(distances, dtype=np.float32)
1502
1503 return (sym_ref_indices, sym_ref_distances)
1504
1505 def _EvalAtom(self, pos, atom_idx, thresholds, ref_indices, ref_distances):
1506 """Computes number of distance differences within given thresholds
1507
1508 returns np.array with len(thresholds) elements
1509 """
1510 a_p = pos[atom_idx, :]
1511 tmp = pos.take(ref_indices[atom_idx], axis=0)
1512 np.subtract(tmp, a_p[None, :], out=tmp)
1513 np.square(tmp, out=tmp)
1514 tmp = tmp.sum(axis=1)
1515 np.sqrt(tmp, out=tmp) # distances against all relevant atoms
1516 np.subtract(ref_distances[atom_idx], tmp, out=tmp)
1517 np.absolute(tmp, out=tmp) # absolute dist diffs
1518 return np.asarray([(tmp <= thresh).sum() for thresh in thresholds],
1519 dtype=np.int32)
1520
1522 self, pos, atom_indices, thresholds, ref_indices, ref_distances
1523 ):
1524 """Calls _EvalAtom for several atoms and sums up the computed number
1525 of distance differences within given thresholds
1526
1527 returns numpy matrix of shape (n_atoms, len(threshold))
1528 """
1529 conserved = np.zeros((len(atom_indices), len(thresholds)),
1530 dtype=np.int32)
1531 for a_idx, a in enumerate(atom_indices):
1532 conserved[a_idx, :] = self._EvalAtom(pos, a, thresholds,
1533 ref_indices, ref_distances)
1534 return conserved
1535
1536 def _EvalResidues(self, pos, thresholds, res_atom_indices, ref_indices,
1537 ref_distances):
1538 """Calls _EvalAtoms for a bunch of residues
1539
1540 residues are defined in *res_atom_indices* as lists of atom indices
1541 returns numpy matrix of shape (n_residues, len(thresholds)).
1542 """
1543 conserved = np.zeros((len(res_atom_indices), len(thresholds)),
1544 dtype=np.int32)
1545 for rai_idx, rai in enumerate(res_atom_indices):
1546 conserved[rai_idx,:] = np.sum(self._EvalAtoms(pos, rai, thresholds,
1547 ref_indices, ref_distances), axis=0)
1548 return conserved
1549
1551 if self.sequence_separation != 0:
1552 raise NotImplementedError("Congratulations! You're the first one "
1553 "requesting a non-default "
1554 "sequence_separation in the new and "
1555 "awesome LDDT implementation. A crate of "
1556 "beer for Gabriel and he'll implement "
1557 "it.")
1558
1559 def _GetNExp(self, atom_idx, ref_indices):
1560 """Returns number of close atoms around one or several atoms
1561 """
1562 if isinstance(atom_idx, int):
1563 return len(ref_indices[atom_idx])
1564 elif isinstance(atom_idx, list):
1565 return sum([len(ref_indices[idx]) for idx in atom_idx])
1566 else:
1567 raise RuntimeError("invalid input type")
1568
1569 def _ResolveSymmetries(self, pos, thresholds, symmetries, sym_ref_indices,
1570 sym_ref_distances):
1571 """Swaps symmetric positions in-place in order to maximize LDDT scores
1572 towards non-symmetric atoms.
1573 """
1574 for sym in symmetries:
1575
1576 atom_indices = list()
1577 for sym_tuple in sym:
1578 atom_indices += [sym_tuple[0], sym_tuple[1]]
1579 tot = self._GetNExp(atom_indices, sym_ref_indices)
1580
1581 if tot == 0:
1582 continue # nothing to do
1583
1584 # score as is
1585 sym_one_conserved = self._EvalAtoms(
1586 pos,
1587 atom_indices,
1588 thresholds,
1589 sym_ref_indices,
1590 sym_ref_distances,
1591 )
1592
1593 # switch positions and score again
1594 for pair in sym:
1595 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
1596
1597 sym_two_conserved = self._EvalAtoms(
1598 pos,
1599 atom_indices,
1600 thresholds,
1601 sym_ref_indices,
1602 sym_ref_distances,
1603 )
1604
1605 sym_one_score = np.sum(sym_one_conserved) / (len(thresholds) * tot)
1606 sym_two_score = np.sum(sym_two_conserved) / (len(thresholds) * tot)
1607
1608 if sym_one_score >= sym_two_score:
1609 # switch back, initial positions were better or equal
1610 # for the equal case: we still switch back to reproduce the old
1611 # LDDT behaviour
1612 for pair in sym:
1613 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
1614
1615 def _EvalAtomSSD(self, pos, atom_idx, dist_cap, ref_indices, ref_distances):
1616 """ Computes summed squared distances
1617
1618 distances are capped at dist_cap
1619 """
1620 a_p = pos[atom_idx, :]
1621 tmp = pos.take(ref_indices[atom_idx], axis=0)
1622 np.subtract(tmp, a_p[None, :], out=tmp)
1623 np.square(tmp, out=tmp)
1624 tmp = tmp.sum(axis=1)
1625 np.sqrt(tmp, out=tmp) # distances against all relevant atoms
1626 np.subtract(ref_distances[atom_idx], tmp, out=tmp) # distance difference
1627 np.square(tmp, out=tmp) # squared distance difference
1628 squared_dist_cap = dist_cap*dist_cap
1629 tmp[tmp > squared_dist_cap] = squared_dist_cap
1630 return tmp.sum()
1631
1633 self, pos, atom_indices, dist_cap, ref_indices, ref_distances
1634 ):
1635 """Calls _EvalAtomSSD for several atoms
1636 """
1637 return np.asarray([self._EvalAtomSSD(pos, a, dist_cap, ref_indices,
1638 ref_distances) for a in atom_indices],
1639 dtype=np.float32)
1640
1641 def _ResolveSymmetriesSSD(self, pos, dist_cap, symmetries, sym_ref_indices,
1642 sym_ref_distances):
1643 """Swaps symmetric positions in-place in order to maximize summed
1644 squared distances towards non-symmetric atoms.
1645 """
1646 for sym in symmetries:
1647
1648 atom_indices = list()
1649 for sym_tuple in sym:
1650 atom_indices += [sym_tuple[0], sym_tuple[1]]
1651 tot = self._GetNExp(atom_indices, sym_ref_indices)
1652
1653 if tot == 0:
1654 continue # nothing to do
1655
1656 # score as is
1657 sym_one_ssd = self._EvalAtomsSSD(
1658 pos,
1659 atom_indices,
1660 dist_cap,
1661 sym_ref_indices,
1662 sym_ref_distances,
1663 )
1664
1665 # switch positions and score again
1666 for pair in sym:
1667 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
1668
1669 sym_two_ssd = self._EvalAtomsSSD(
1670 pos,
1671 atom_indices,
1672 dist_cap,
1673 sym_ref_indices,
1674 sym_ref_distances,
1675 )
1676
1677 sym_one_score = np.sum(sym_one_ssd)
1678 sym_two_score = np.sum(sym_two_ssd)
1679
1680 if sym_one_score < sym_two_score:
1681 # switch back, initial positions were better
1682 for pair in sym:
1683 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
1684
1685
1686def DisableChainPairContacts(scorer, excluded_interfaces):
1687 """Copy interchain reference distances of *scorer* with contacts
1688 for a selection of interfaces switched off
1689
1690 :param scorer: Scorer from which to derive the interchain reference
1691 distances
1692 :type scorer: :class:`lDDTScorer`
1693 :param excluded_interfaces: Interfaces for which contacts should be
1694 disabled. Each element is a pair of chain
1695 names.
1696 :type excluded_interfaces: :class:`list` of :class:`tuple` with two
1697 :class:`str`
1698 :returns: Tuple with 4 elements that are copies of
1699 (scorer.ref_indices_ic, scorer.ref_distances_ic,
1700 scorer.sym_ref_indices_ic, scorer.sym_ref_distances_ic) with
1701 any contact belonging to one of *excluded_interfaces* removed
1702 """
1703 def _chain_range(scorer, ch_idx):
1704 s = scorer.chain_start_indices[ch_idx]
1705 if ch_idx + 1 < len(scorer.chain_start_indices):
1706 e = scorer.chain_start_indices[ch_idx + 1]
1707 else:
1708 e = scorer.n_atoms
1709 return s, e
1710
1711 # Copy to make sure that we don't change anything on underlying
1712 # references
1713 ref_indices = [a.copy() for a in scorer.ref_indices_ic]
1714 ref_distances = [a.copy() for a in scorer.ref_distances_ic]
1715 sym_ref_indices = [a.copy() for a in scorer.sym_ref_indices_ic]
1716 sym_ref_distances = [a.copy() for a in scorer.sym_ref_distances_ic]
1717
1718 for chain_a, chain_b in excluded_interfaces:
1719 ch_idx_a = scorer.chain_names.index(chain_a)
1720 ch_idx_b = scorer.chain_names.index(chain_b)
1721 a_s, a_e = _chain_range(scorer, ch_idx_a)
1722 b_s, b_e = _chain_range(scorer, ch_idx_b)
1723
1724 # atoms are stored consecutively per chain => index stretches
1725 # [a_s, a_e) and [b_s, b_e) fully cover chain_a/chain_b. Remove any
1726 # contact from one stretch pointing into the other (contacts are
1727 # stored symmetrically, i.e. i in indices[j] implies j in
1728 # indices[i], so both directions must be cleaned up)
1729 for indices, distances in [(ref_indices, ref_distances),
1730 (sym_ref_indices, sym_ref_distances)]:
1731 for at_idx in range(a_s, a_e):
1732 if indices[at_idx].shape[0] > 0:
1733 mask = np.logical_or(indices[at_idx] < b_s,
1734 indices[at_idx] >= b_e)
1735 indices[at_idx] = indices[at_idx][mask]
1736 distances[at_idx] = distances[at_idx][mask]
1737 for at_idx in range(b_s, b_e):
1738 if indices[at_idx].shape[0] > 0:
1739 mask = np.logical_or(indices[at_idx] < a_s,
1740 indices[at_idx] >= a_e)
1741 indices[at_idx] = indices[at_idx][mask]
1742 distances[at_idx] = distances[at_idx][mask]
1743
1744 return (ref_indices, ref_distances, sym_ref_indices, sym_ref_distances)
__init__(self, atom_names)
Definition lddt.py:50
AddSymmetricCompound(self, name, symmetric_atoms)
Definition lddt.py:85
DRMSD(self, model, dist_cap=5, chain_mapping=None, no_interchain=False, no_intrachain=False, residue_mapping=None, check_resnames=True, add_mdl_contacts=False, interaction_data=None)
Definition lddt.py:743
_AddMdlContacts(self, model, res_atom_indices, res_atom_hashes, ref_indices, ref_distances, no_interchain, no_intrachain)
Definition lddt.py:1243
_ResolveSymmetries(self, pos, thresholds, symmetries, sym_ref_indices, sym_ref_distances)
Definition lddt.py:1570
GetNChainContacts(self, target_chain, no_interchain=False)
Definition lddt.py:920
_ProcessModel(self, model, chain_mapping, residue_mapping=None, nirvana_dist=100, check_resnames=True)
Definition lddt.py:945
_EvalAtomSSD(self, pos, atom_idx, dist_cap, ref_indices, ref_distances)
Definition lddt.py:1615
_SetupEnv(self, compound_lib, custom_compounds, symmetry_settings, seqres_mapping, bb_only)
Definition lddt.py:1106
lDDT(self, model, thresholds=[0.5, 1.0, 2.0, 4.0], local_lddt_prop=None, local_contact_prop=None, chain_mapping=None, no_interchain=False, no_intrachain=False, penalize_extra_chains=False, residue_mapping=None, return_dist_test=False, check_resnames=True, add_mdl_contacts=False, interaction_data=None, set_atom_props=False)
Definition lddt.py:464
_EvalAtom(self, pos, atom_idx, thresholds, ref_indices, ref_distances)
Definition lddt.py:1505
_GetChainRNums(self, ch, residue_mapping, model_ch_name, target_ch_name)
Definition lddt.py:1054
__init__(self, target, compound_lib=None, custom_compounds=None, inclusion_radius=15, sequence_separation=0, symmetry_settings=None, seqres_mapping=dict(), bb_only=False)
Definition lddt.py:233
_SetupDistances(structure, n_atoms, atom_index_mapping, inclusion_radius)
Definition lddt.py:1298
_GetExtraModelChainPenalty(self, model, chain_mapping)
Definition lddt.py:1037
_SetupDistancesIC(n_atoms, chain_start_indices, ref_indices, ref_distances)
Definition lddt.py:1463
_EvalAtoms(self, pos, atom_indices, thresholds, ref_indices, ref_distances)
Definition lddt.py:1523
_GetTargetResidueNumbers(self, target, seqres_mapping)
Definition lddt.py:1155
_EvalResidues(self, pos, thresholds, res_atom_indices, ref_indices, ref_distances)
Definition lddt.py:1537
_NonSymDistances(n_atoms, symmetric_atoms, ref_indices, ref_distances)
Definition lddt.py:1486
_SetupCompound(self, r, compound_lib, custom_compounds, symmetry_settings, bb_only)
Definition lddt.py:1199
_ResolveSymmetriesSSD(self, pos, dist_cap, symmetries, sym_ref_indices, sym_ref_distances)
Definition lddt.py:1642
_GetNExp(self, atom_idx, ref_indices)
Definition lddt.py:1559
_EvalAtomsSSD(self, pos, atom_indices, dist_cap, ref_indices, ref_distances)
Definition lddt.py:1634
_SetupDistancesSC(n_atoms, chain_start_indices, ref_indices, ref_distances)
Definition lddt.py:1439
blockwise_cdist(A, B, block_size=1000)
Definition lddt.py:19
GetDefaultSymmetrySettings()
Definition lddt.py:103
DisableChainPairContacts(scorer, excluded_interfaces)
Definition lddt.py:1686