10 from scipy.spatial.distance
import cdist
13 x2 = np.sum(p1**2, axis=1)
14 y2 = np.sum(p2**2, axis=1)
15 xy = np.matmul(p1, p2.T)
16 x2 = x2.reshape(-1, 1)
17 return np.sqrt(x2 - 2*xy + y2)
20 """ Memory efficient cdist implementation that performs blockwise operations
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.
28 This function is adapted from chatgpt output
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)
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)
40 """ Defines atoms for custom compounds
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.
47 :param atom_names: Names of atoms of custom compound
48 :type atom_names: :class:`list` of :class:`str`
55 """ Construct custom compound from residue
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`
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")
69 """Container for symmetric compounds
71 LDDT considers symmetries and selects the one resulting in the highest
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
79 Use :func:`AddSymmetricCompound` to define a symmetry which can then
80 directly be accessed through the *symmetric_compounds* member.
86 """Adds symmetry for compound with *name*
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`
97 for pair
in symmetric_atoms:
99 raise RuntimeError(
"Expect pairs when defining symmetries")
104 """Constructs and returns :class:`SymmetrySettings` object for natural amino
110 symmetry_settings.AddSymmetricCompound(
"ASP", [(
"OD1",
"OD2")])
113 symmetry_settings.AddSymmetricCompound(
"GLU", [(
"OE1",
"OE2")])
116 symmetry_settings.AddSymmetricCompound(
"LEU", [(
"CD1",
"CD2")])
119 symmetry_settings.AddSymmetricCompound(
"VAL", [(
"CG1",
"CG2")])
122 symmetry_settings.AddSymmetricCompound(
"ARG", [(
"NH1",
"NH2")])
125 symmetry_settings.AddSymmetricCompound(
126 "PHE", [(
"CD1",
"CD2"), (
"CE1",
"CE2")]
130 symmetry_settings.AddSymmetricCompound(
131 "TYR", [(
"CD1",
"CD2"), (
"CE1",
"CE2")]
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")]
141 return symmetry_settings
145 """LDDT scorer object for a specific target
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
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
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
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
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).
227 custom_compounds=None,
229 sequence_separation=0,
230 symmetry_settings=None,
231 seqres_mapping=dict(),
238 if compound_lib
is None:
239 compound_lib = conop.GetDefaultLib()
240 if compound_lib
is None:
241 raise RuntimeError(
"No compound_lib given and conop.GetDefaultLib "
242 "returns no valid compound library")
245 if symmetry_settings
is None:
346 lDDTScorer._SetupDistances(self.
targettarget, self.
n_atomsn_atoms,
355 lDDTScorer._SetupDistances(self.
targettarget, self.
n_atomsn_atoms,
380 lDDTScorer._SetupDistancesSC(self.
n_atomsn_atoms,
390 lDDTScorer._SetupDistancesSC(self.
n_atomsn_atoms,
400 lDDTScorer._NonSymDistances(self.
n_atomsn_atoms,
410 lDDTScorer._NonSymDistances(self.
n_atomsn_atoms,
420 lDDTScorer._SetupDistancesIC(self.
n_atomsn_atoms,
430 lDDTScorer._SetupDistancesIC(self.
n_atomsn_atoms,
440 lDDTScorer._NonSymDistances(self.
n_atomsn_atoms,
450 lDDTScorer._NonSymDistances(self.
n_atomsn_atoms,
456 def lDDT(self, model, thresholds = [0.5, 1.0, 2.0, 4.0],
457 local_lddt_prop=None, local_contact_prop=None,
458 chain_mapping=None, no_interchain=False,
459 no_intrachain=False, penalize_extra_chains=False,
460 residue_mapping=None, return_dist_test=False,
461 check_resnames=True, add_mdl_contacts=False,
462 interaction_data=None, set_atom_props=False):
463 """Computes LDDT of *model* - globally and per-residue
465 :param model: Model to be scored - models are preferably scored upon
466 performing stereo-chemistry checks in order to punish for
467 non-sensical irregularities. This must be done separately
468 as a pre-processing step. Target contacts that are not
469 covered by *model* are considered not conserved, thus
470 decreasing LDDT score. This also includes missing model
471 chains or model chains for which no mapping is provided in
473 :type model: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
474 :param thresholds: Thresholds of distance differences to be considered
475 as correct - see docs in constructor for more info.
476 default: [0.5, 1.0, 2.0, 4.0]
477 :type thresholds: :class:`list` of :class:`floats`
478 :param local_lddt_prop: If set, per-residue scores will be assigned as
479 generic float property of that name
480 :type local_lddt_prop: :class:`str`
481 :param local_contact_prop: If set, number of expected contacts as well
482 as number of conserved contacts will be
483 assigned as generic int property.
484 Excected contacts will be set as
485 <local_contact_prop>_exp, conserved contacts
486 as <local_contact_prop>_cons. Values
487 are summed over all thresholds.
488 :type local_contact_prop: :class:`str`
489 :param chain_mapping: Mapping of model chains (key) onto target chains
490 (value). This is required if target or model have
492 :type chain_mapping: :class:`dict` with :class:`str` as keys/values
493 :param no_interchain: Whether to exclude interchain contacts
494 :type no_interchain: :class:`bool`
495 :param no_intrachain: Whether to exclude intrachain contacts (i.e. only
496 consider interface related contacts)
497 :type no_intrachain: :class:`bool`
498 :param penalize_extra_chains: Whether to include a fixed penalty for
499 additional chains in the model that are
500 not mapped to the target. ONLY AFFECTS
501 RETURNED GLOBAL SCORE. In detail: adds the
502 number of intra-chain contacts of each
503 extra chain to the expected contacts, thus
505 :type penalize_extra_chains: :class:`bool`
506 :param residue_mapping: By default, residue mapping is based on residue
507 numbers. That means, a model chain and the
508 respective target chain map to the same
509 underlying reference sequence (SEQRES).
510 Alternatively, you can specify one or
511 several alignment(s) between model and target
512 chains by providing a dictionary. key: Name
513 of chain in model (respective target chain is
514 extracted from *chain_mapping*),
515 value: Alignment with first sequence
516 corresponding to target chain and second
517 sequence to model chain. There is NO reference
518 sequence involved, so the two sequences MUST
519 exactly match the actual residues observed in
520 the respective target/model chains (ATOMSEQ).
521 :type residue_mapping: :class:`dict` with key: :class:`str`,
522 value: :class:`ost.seq.AlignmentHandle`
523 :param return_dist_test: Whether to additionally return the underlying
524 per-residue data for the distance difference
525 test. Adds five objects to the return tuple.
526 First: Number of total contacts summed over all
528 Second: Number of conserved contacts summed
530 Third: list with length of scored residues.
531 Contains indices referring to model.residues.
532 Fourth: numpy array of size
533 len(scored_residues) containing the number of
535 Fifth: numpy matrix of shape
536 (len(scored_residues), len(thresholds))
537 specifying how many for each threshold are
539 :param check_resnames: On by default. Enforces residue name matches
540 between mapped model and target residues.
541 :type check_resnames: :class:`bool`
542 :param add_mdl_contacts: Adds model contacts - Only using contacts that
543 are within a certain distance threshold in the
544 target does not penalize for added model
545 contacts. If set to True, this flag will also
546 consider target contacts that are within the
547 specified distance threshold in the model but
548 not necessarily in the target. No contact will
549 be added if the respective atom pair is not
550 resolved in the target.
551 :type add_mdl_contacts: :class:`bool`
552 :param interaction_data: Pro param - don't use
553 :type interaction_data: :class:`tuple`
554 :param set_atom_props: If True, sets generic properties on a per atom
555 level if *local_lddt_prop*/*local_contact_prop*
557 In other words: this is the only way you can
558 get per-atom LDDT values.
559 :type set_atom_props: :class:`bool`
561 :returns: global and per-residue LDDT scores as a tuple -
562 first element is global LDDT score (None if *target* has no
563 contacts) and second element a list of per-residue scores with
564 length len(*model*.residues). None is assigned to residues that
565 are not covered by target. If a residue is covered but has no
566 contacts in *target*, 0.0 is assigned.
568 if chain_mapping
is None:
569 if len(self.
chain_nameschain_names) > 1
or len(model.chains) > 1:
570 raise NotImplementedError(
"Must provide chain mapping if "
571 "target or model have > 1 chains.")
572 chain_mapping = {model.chains[0].GetName(): self.
chain_nameschain_names[0]}
575 for model_chain, target_chain
in chain_mapping.items():
576 if target_chain
not in self.
chain_nameschain_names:
577 raise RuntimeError(f
"Target chain specified in "
578 f
"chain_mapping ({target_chain}) does "
579 f
"not exist. Target has chains: "
580 f
"{self.chain_names}")
581 ch = model.FindChain(model_chain)
583 raise RuntimeError(f
"Model chain specified in "
584 f
"chain_mapping ({model_chain}) does "
585 f
"not exist. Model has chains: "
586 f
"{[c.GetName() for c in model.chains]}")
590 pos, res_ref_atom_indices, res_atom_indices, res_atom_hashes, \
591 res_indices, ref_res_indices, symmetries = \
593 residue_mapping = residue_mapping,
595 check_resnames = check_resnames)
597 if no_interchain
and no_intrachain:
598 raise RuntimeError(
"no_interchain and no_intrachain flags are "
599 "mutually exclusive")
601 sym_ref_indices =
None
602 sym_ref_distances =
None
606 if interaction_data
is None:
624 ref_indices, ref_distances = \
625 self.
_AddMdlContacts_AddMdlContacts(model, res_atom_indices, res_atom_hashes,
626 ref_indices, ref_distances,
627 no_interchain, no_intrachain)
629 sym_ref_indices, sym_ref_distances = \
631 ref_indices, ref_distances)
633 sym_ref_indices, sym_ref_distances, ref_indices, ref_distances = \
636 self.
_ResolveSymmetries_ResolveSymmetries(pos, thresholds, symmetries, sym_ref_indices,
639 atom_indices = list(itertools.chain.from_iterable(res_atom_indices))
641 per_atom_exp = np.asarray([self.
_GetNExp_GetNExp(i, ref_indices)
642 for i
in atom_indices], dtype=np.int32)
643 per_res_exp = np.asarray([self.
_GetNExp_GetNExp(res_ref_atom_indices[idx],
644 ref_indices)
for idx
in range(len(res_indices))], dtype=np.int32)
646 per_atom_conserved = self.
_EvalAtoms_EvalAtoms(pos, atom_indices, thresholds,
647 ref_indices, ref_distances)
648 per_res_conserved = np.zeros((len(res_atom_indices), len(thresholds)),
651 for r_idx
in range(len(res_atom_indices)):
652 end_idx = start_idx + len(res_atom_indices[r_idx])
653 per_res_conserved[r_idx] = np.sum(per_atom_conserved[start_idx:end_idx,:],
657 n_thresh = len(thresholds)
660 per_res_lDDT = [
None] * model.GetResidueCount()
661 for idx
in range(len(res_indices)):
662 n_exp = n_thresh * per_res_exp[idx]
664 score = np.sum(per_res_conserved[idx,:]) / n_exp
665 per_res_lDDT[res_indices[idx]] = score
667 per_res_lDDT[res_indices[idx]] = 0.0
670 n_distances = sum([len(x)
for x
in ref_indices])
671 if penalize_extra_chains:
674 lDDT_tot = int(n_thresh * n_distances)
675 lDDT_cons = int(np.sum(per_res_conserved))
678 lDDT = float(lDDT_cons) / lDDT_tot
682 residues = model.residues
683 for idx
in res_indices:
684 residues[idx].SetFloatProp(local_lddt_prop, per_res_lDDT[idx])
686 if local_contact_prop:
687 residues = model.residues
688 exp_prop = local_contact_prop +
"_exp"
689 conserved_prop = local_contact_prop +
"_cons"
691 for i, r_idx
in enumerate(res_indices):
692 residues[r_idx].SetIntProp(exp_prop,
693 n_thresh * int(per_res_exp[i]))
694 residues[r_idx].SetIntProp(conserved_prop,
695 int(np.sum(per_res_conserved[i,:])))
697 if set_atom_props
and (local_lddt_prop
or local_contact_prop):
699 residues = model.residues
700 for i, indices
in enumerate(res_atom_indices):
701 r = residues[res_indices[i]]
702 r_idx = ref_res_indices[i]
706 a = r.FindAtom(anames[a_i - res_start_idx])
710 summed_per_atom_conserved = per_atom_conserved.sum(axis=1)
714 for a_idx
in range(len(atom_list)):
715 if per_atom_exp[a_idx] != 0:
716 tmp = summed_per_atom_conserved[a_idx] / per_atom_exp[a_idx]
718 atom_list[a_idx].SetFloatProp(local_lddt_prop, tmp)
720 if local_contact_prop:
721 conserved_prop = local_contact_prop +
"_cons"
722 exp_prop = local_contact_prop +
"_exp"
723 for a_idx
in range(len(atom_list)):
725 tmp = summed_per_atom_conserved[a_idx]
726 atom_list[a_idx].SetIntProp(conserved_prop, tmp)
728 tmp = per_atom_exp[a_idx] * n_thresh
729 atom_list[a_idx].SetIntProp(exp_prop, tmp)
732 return lDDT, per_res_lDDT, lDDT_tot, lDDT_cons, res_indices, \
733 per_res_exp, per_res_conserved
735 return lDDT, per_res_lDDT
737 def DRMSD(self, model, dist_cap = 5,
738 chain_mapping=None, no_interchain=False,
739 no_intrachain=False, residue_mapping=None,
740 check_resnames=True, add_mdl_contacts=False,
741 interaction_data=None):
742 """ DRMSD of *model* - globally and per-residue
744 Very similar to LDDT as we operate on distance differences for all
745 interatomic distances within the same inclusion radius as in LDDT.
746 DRMSD is the distance rmsd, i.e. the RMSD of distance differences.
747 Distance differences are capped at *dist_cap* which is also the default
748 value for missing distances.
750 :param model: Model to be scored - models are preferably scored upon
751 performing stereo-chemistry checks in order to punish for
752 non-sensical irregularities. This must be done separately
753 as a pre-processing step. Target contacts that are not
754 covered by *model* are considered not conserved, thus
755 increasing DRMSD score. This also includes missing model
756 chains or model chains for which no mapping is provided in
758 :type model: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView`
759 :param dist_cap: Cap for distance differences.
760 :type dist_cap: :class:`float`
761 :param chain_mapping: Mapping of model chains (key) onto target chains
762 (value). This is required if target or model have
764 :type chain_mapping: :class:`dict` with :class:`str` as keys/values
765 :param no_interchain: Whether to exclude interchain contacts
766 :type no_interchain: :class:`bool`
767 :param no_intrachain: Whether to exclude intrachain contacts (i.e. only
768 consider interface related contacts)
769 :type no_intrachain: :class:`bool`
770 :param residue_mapping: By default, residue mapping is based on residue
771 numbers. That means, a model chain and the
772 respective target chain map to the same
773 underlying reference sequence (SEQRES).
774 Alternatively, you can specify one or
775 several alignment(s) between model and target
776 chains by providing a dictionary. key: Name
777 of chain in model (respective target chain is
778 extracted from *chain_mapping*),
779 value: Alignment with first sequence
780 corresponding to target chain and second
781 sequence to model chain. There is NO reference
782 sequence involved, so the two sequences MUST
783 exactly match the actual residues observed in
784 the respective target/model chains (ATOMSEQ).
785 :type residue_mapping: :class:`dict` with key: :class:`str`,
786 value: :class:`ost.seq.AlignmentHandle`
787 :param check_resnames: On by default. Enforces residue name matches
788 between mapped model and target residues.
789 :type check_resnames: :class:`bool`
790 :param add_mdl_contacts: Adds model contacts - Only using contacts that
791 are within a certain distance threshold in the
792 target does not penalize for added model
793 contacts. If set to True, this flag will also
794 consider target contacts that are within the
795 specified distance threshold in the model but
796 not necessarily in the target. No contact will
797 be added if the respective atom pair is not
798 resolved in the target.
799 :type add_mdl_contacts: :class:`bool`
800 :param interaction_data: Pro param - don't use
801 :type interaction_data: :class:`tuple`
803 :returns: global and per-residue DRMSD scores as a tuple -
804 first element is global DRMSD score (None if *target* has no
805 contacts) and second element a list of per-residue scores with
806 length len(*model*.residues). None is assigned to residues that
807 are not covered by target. If a residue is covered but has no
808 contacts in *target*, None is assigned.
810 if chain_mapping
is None:
811 if len(self.
chain_nameschain_names) > 1
or len(model.chains) > 1:
812 raise NotImplementedError(
"Must provide chain mapping if "
813 "target or model have > 1 chains.")
814 chain_mapping = {model.chains[0].GetName(): self.
chain_nameschain_names[0]}
817 for model_chain, target_chain
in chain_mapping.items():
818 if target_chain
not in self.
chain_nameschain_names:
819 raise RuntimeError(f
"Target chain specified in "
820 f
"chain_mapping ({target_chain}) does "
821 f
"not exist. Target has chains: "
822 f
"{self.chain_names}")
823 ch = model.FindChain(model_chain)
825 raise RuntimeError(f
"Model chain specified in "
826 f
"chain_mapping ({model_chain}) does "
827 f
"not exist. Model has chains: "
828 f
"{[c.GetName() for c in model.chains]}")
832 pos, res_ref_atom_indices, res_atom_indices, res_atom_hashes, \
833 res_indices, ref_res_indices, symmetries = \
835 residue_mapping = residue_mapping,
837 check_resnames = check_resnames)
839 if no_interchain
and no_intrachain:
840 raise RuntimeError(
"no_interchain and no_intrachain flags are "
841 "mutually exclusive")
843 sym_ref_indices =
None
844 sym_ref_distances =
None
848 if interaction_data
is None:
866 ref_indices, ref_distances = \
867 self.
_AddMdlContacts_AddMdlContacts(model, res_atom_indices, res_atom_hashes,
868 ref_indices, ref_distances,
869 no_interchain, no_intrachain)
871 sym_ref_indices, sym_ref_distances = \
873 ref_indices, ref_distances)
875 sym_ref_indices, sym_ref_distances, ref_indices, ref_distances = \
881 atom_indices = list(itertools.chain.from_iterable(res_atom_indices))
883 per_atom_exp = np.asarray([self.
_GetNExp_GetNExp(i, ref_indices)
884 for i
in atom_indices], dtype=np.int32)
885 per_res_exp = np.asarray([self.
_GetNExp_GetNExp(res_ref_atom_indices[idx],
886 ref_indices)
for idx
in range(len(res_indices))], dtype=np.int32)
887 per_atom_ssd = self.
_EvalAtomsSSD_EvalAtomsSSD(pos, atom_indices, dist_cap,
888 ref_indices, ref_distances)
892 per_res_drmsd = [
None] * model.GetResidueCount()
893 for r_idx
in range(len(res_atom_indices)):
894 end_idx = start_idx + len(res_atom_indices[r_idx])
895 n_tot = per_res_exp[r_idx]
897 ssd = np.sum(per_atom_ssd[start_idx:end_idx])
900 n_missing = n_tot - np.sum(per_atom_exp[start_idx:end_idx])
901 ssd += n_missing*dist_cap*dist_cap
902 per_res_drmsd[res_indices[r_idx]] = np.sqrt(ssd/n_tot)
907 n_tot = sum([len(x)
for x
in ref_indices])
909 ssd = np.sum(per_atom_ssd)
912 n_missing = n_tot - np.sum(per_atom_exp)
913 ssd += (dist_cap*dist_cap*n_missing)
914 drmsd = np.sqrt(ssd/n_tot)
916 return drmsd, per_res_drmsd
919 """Returns number of contacts expected for a certain chain in *target*
921 :param target_chain: Chain in *target* for which you want the number
923 :type target_chain: :class:`str`
924 :param no_interchain: Whether to exclude interchain contacts
925 :type no_interchain: :class:`bool`
926 :raises: :class:`RuntimeError` if specified chain doesnt exist
928 if target_chain
not in self.
chain_nameschain_names:
929 raise RuntimeError(f
"Specified chain name ({target_chain}) not in "
931 ch_idx = self.
chain_nameschain_names.index(target_chain)
941 def _ProcessModel(self, model, chain_mapping, residue_mapping = None,
943 check_resnames = True):
944 """ Helper that generates data structures from model
949 max_pos = model.bounds.GetMax()
950 max_coordinate = abs(max(max_pos[0], max_pos[1], max_pos[2]))
951 max_coordinate += 42 * nirvana_dist
952 pos = np.ones((self.
n_atomsn_atoms, 3), dtype=np.float32) * max_coordinate
956 res_ref_atom_indices = list()
960 res_atom_indices = list()
964 res_atom_hashes = list()
970 ref_res_indices = list()
975 current_model_res_idx = -1
976 for ch
in model.chains:
977 model_ch_name = ch.GetName()
978 if model_ch_name
not in chain_mapping:
979 current_model_res_idx += len(ch.residues)
981 target_ch_name = chain_mapping[model_ch_name]
983 rnums = self.
_GetChainRNums_GetChainRNums(ch, residue_mapping, model_ch_name,
986 for r, rnum
in zip(ch.residues, rnums):
987 current_model_res_idx += 1
988 res_mapper_key = (target_ch_name, rnum)
989 if res_mapper_key
not in self.
res_mapperres_mapper:
991 r_idx = self.
res_mapperres_mapper[res_mapper_key]
992 if check_resnames
and r.name != self.
compound_namescompound_names[r_idx]:
994 f
"Residue name mismatch for {r}, "
995 f
" expect {self.compound_names[r_idx]}"
1000 atoms = [r.FindAtom(aname)
for aname
in anames]
1001 res_ref_atom_indices.append(
1002 list(range(res_start_idx, res_start_idx + len(anames)))
1004 res_atom_indices.append(list())
1005 res_atom_hashes.append(list())
1006 res_indices.append(current_model_res_idx)
1007 ref_res_indices.append(r_idx)
1008 for a_idx, a
in enumerate(atoms):
1011 pos[res_start_idx + a_idx][0] = p[0]
1012 pos[res_start_idx + a_idx][1] = p[1]
1013 pos[res_start_idx + a_idx][2] = p[2]
1014 res_atom_indices[-1].append(res_start_idx + a_idx)
1015 res_atom_hashes[-1].append(a.handle.GetHashCode())
1017 sym_indices = list()
1019 a_one = atoms[sym_tuple[0]]
1020 a_two = atoms[sym_tuple[1]]
1021 if a_one.IsValid()
and a_two.IsValid():
1024 res_start_idx + sym_tuple[0],
1025 res_start_idx + sym_tuple[1],
1028 if len(sym_indices) > 0:
1029 symmetries.append(sym_indices)
1031 return (pos, res_ref_atom_indices, res_atom_indices, res_atom_hashes,
1032 res_indices, ref_res_indices, symmetries)
1035 def _GetExtraModelChainPenalty(self, model, chain_mapping):
1036 """Counts n distances in extra model chains to be added as penalty
1039 for chain
in model.chains:
1040 ch_name = chain.GetName()
1041 if ch_name
not in chain_mapping:
1043 mdl_sel = model.Select(f
"cname={mol.QueryQuoteName(ch_name)}")
1045 symmetry_settings = sm,
1047 bb_only = self.
bb_onlybb_only)
1048 penalty += sum([len(x)
for x
in dummy_scorer.ref_indices])
1051 def _GetChainRNums(self, ch, residue_mapping, model_ch_name,
1053 """Map residues in model chain to target residues
1055 There are two options: one is simply using residue numbers,
1056 the other is a custom mapping as given in *residue_mapping*
1058 if residue_mapping
and model_ch_name
in residue_mapping:
1060 ch_idx = self.
chain_nameschain_names.index(target_ch_name)
1066 target_rnums = self.
res_resnumsres_resnums[start_idx:end_idx]
1068 target_seq = residue_mapping[model_ch_name].GetSequence(0)
1069 model_seq = residue_mapping[model_ch_name].GetSequence(1)
1070 if len(target_seq.GetGaplessString()) != len(target_rnums):
1071 raise RuntimeError(f
"Try to perform residue mapping for "
1072 f
"model chain {model_ch_name} which "
1073 f
"maps to {target_ch_name} in target. "
1074 f
"Target sequence in alignment suggests "
1075 f
"{len(target_seq.GetGaplessString())} "
1076 f
"residues but {len(target_rnums)} are "
1078 if len(model_seq.GetGaplessString()) != len(ch.residues):
1079 raise RuntimeError(f
"Try to perform residue mapping for "
1080 f
"model chain {model_ch_name} which "
1081 f
"maps to {target_ch_name} in target. "
1082 f
"Model sequence in alignment suggests "
1083 f
"{len(model_seq.GetGaplessString())} "
1084 f
"residues but {len(ch.residues)} are "
1088 for col
in residue_mapping[model_ch_name]:
1092 if col[0] !=
'-' and col[1] !=
'-':
1093 rnums.append(target_rnums[target_idx])
1095 if col[0] ==
'-' and col[1] !=
'-':
1098 rnums = [r.GetNumber()
for r
in ch.residues]
1103 def _SetupEnv(self, compound_lib, custom_compounds, symmetry_settings,
1104 seqres_mapping, bb_only):
1105 """Sets target related lDDTScorer members defined in constructor
1107 No distance related members - see _SetupDistances
1113 for chain
in self.
targettarget.chains:
1114 ch_name = chain.GetName()
1118 for r, rnum
in zip(chain.residues, residue_numbers[ch_name]):
1122 self.
_SetupCompound_SetupCompound(r, compound_lib, custom_compounds,
1123 symmetry_settings, bb_only)
1130 atoms = [r.FindAtom(an)
for an
in self.
compound_anamescompound_anames[r.name]]
1133 self.
atom_indicesatom_indices[a.handle.GetHashCode()] = current_idx
1135 positions.append(np.asarray([p[0], p[1], p[2]],
1138 positions.append(np.zeros(3, dtype=np.float32))
1143 for a_idx
in sym_tuple:
1146 hashcode = a.handle.GetHashCode()
1150 self.
positionspositions = np.vstack(positions)
1151 self.
n_atomsn_atoms = current_idx
1153 def _GetTargetResidueNumbers(self, target, seqres_mapping):
1154 """Returns residue numbers for each chain in target as dict
1156 They're either directly extracted from the raw residue number
1157 from the structure or from user provided alignments
1159 residue_numbers = dict()
1160 for ch
in target.chains:
1161 ch_name = ch.GetName()
1163 if ch_name
in seqres_mapping:
1164 seqres = seqres_mapping[ch_name].GetSequence(0).GetString()
1165 atomseq = seqres_mapping[ch_name].GetSequence(1).GetString()
1169 "SEQRES in seqres_mapping must not " "contain gaps"
1171 atomseq_from_chain = [r.one_letter_code
for r
in ch.residues]
1172 if atomseq.replace(
"-",
"") != atomseq_from_chain:
1174 "ATOMSEQ in seqres_mapping must match "
1175 "raw sequence extracted from chain "
1179 for seqres_olc, atomseq_olc
in zip(seqres, atomseq):
1180 if seqres_olc !=
"-":
1182 if atomseq_olc !=
"-":
1183 if seqres_olc != atomseq_olc:
1185 f
"Residue with number {rnum} in "
1186 f
"chain {ch_name} has SEQRES "
1191 rnums = [r.GetNumber()
for r
in ch.residues]
1192 assert len(rnums) == len(ch.residues)
1193 residue_numbers[ch_name] = rnums
1194 return residue_numbers
1196 def _SetupCompound(self, r, compound_lib, custom_compounds,
1197 symmetry_settings, bb_only):
1198 """fill self.compound_anames/self.compound_symmetric_atoms
1202 if r.chem_class.IsPeptideLinking():
1204 elif r.chem_class.IsNucleotideLinking():
1207 raise RuntimeError(f
"Only support amino acids and nucleotides "
1208 f
"if bb_only is True, failed with {str(r)}")
1212 symmetric_atoms = list()
1213 if custom_compounds
is not None and r.GetName()
in custom_compounds:
1214 atom_names = list(custom_compounds[r.GetName()].atom_names)
1216 compound = compound_lib.FindCompound(r.name)
1217 if compound
is None:
1218 raise RuntimeError(f
"no entry for {r} in compound_lib")
1219 for atom_spec
in compound.GetAtomSpecs():
1220 if atom_spec.element
not in [
"H",
"D"]:
1221 atom_names.append(atom_spec.name)
1222 if r.name
in symmetry_settings.symmetric_compounds:
1223 for pair
in symmetry_settings.symmetric_compounds[r.name]:
1225 a = atom_names.index(pair[0])
1226 b = atom_names.index(pair[1])
1228 msg = f
"Could not find symmetric atoms "
1229 msg += f
"({pair[0]}, {pair[1]}) for {r.name} "
1230 msg += f
"as specified in SymmetrySettings in "
1231 msg += f
"compound from component dictionary. "
1232 msg += f
"Atoms in compound: {atom_names}"
1233 raise RuntimeError(msg)
1234 symmetric_atoms.append((a, b))
1236 if len(symmetric_atoms) > 0:
1239 def _AddMdlContacts(self, model, res_atom_indices, res_atom_hashes,
1240 ref_indices, ref_distances, no_interchain,
1244 in_target = np.zeros(self.
n_atomsn_atoms, dtype=bool)
1247 mdl_atom_indices = dict()
1248 for at_indices, at_hashes
in zip(res_atom_indices, res_atom_hashes):
1249 for i, h
in zip(at_indices, at_hashes):
1251 mdl_atom_indices[h] = i
1256 mdl_ref_indices, mdl_ref_distances = \
1257 lDDTScorer._SetupDistances(model, self.
n_atomsn_atoms, mdl_atom_indices,
1260 mdl_ref_indices, mdl_ref_distances = \
1261 lDDTScorer._SetupDistancesSC(self.
n_atomsn_atoms,
1267 mdl_ref_indices, mdl_ref_distances = \
1268 lDDTScorer._SetupDistancesIC(self.
n_atomsn_atoms,
1274 for i
in range(self.
n_atomsn_atoms):
1275 mask = np.isin(mdl_ref_indices[i], ref_indices[i],
1276 assume_unique=
True, invert=
True)
1277 if np.sum(mask) > 0:
1278 added_mdl_indices = mdl_ref_indices[i][mask]
1279 ref_indices[i] = np.append(ref_indices[i],
1283 tmp = self.
positionspositions.take(added_mdl_indices, axis=0)
1284 np.subtract(tmp, self.
positionspositions[i][
None, :], out=tmp)
1285 np.square(tmp, out=tmp)
1286 tmp = tmp.sum(axis=1)
1287 np.sqrt(tmp, out=tmp)
1288 ref_distances[i] = np.append(ref_distances[i], tmp)
1290 return (ref_indices, ref_distances)
1295 def _SetupDistances(structure, n_atoms, atom_index_mapping,
1298 """Compute distance related members of lDDTScorer
1300 Brute force all vs all distance computation kills LDDT for large
1301 complexes. Instead of building some KD tree data structure, we make use
1302 of expected spatial proximity of atoms in the same chain. Distances are
1303 computed as follows:
1305 - process each chain individually
1306 - perform crude collision detection
1307 - process potentially interacting chain pairs
1308 - concatenate distances from all processing steps
1310 ref_indices = [np.asarray([], dtype=np.int32)
for idx
in range(n_atoms)]
1311 ref_distances = [np.asarray([], dtype=np.float32)
for idx
in range(n_atoms)]
1313 indices = [list()
for _
in range(n_atoms)]
1314 distances = [list()
for _
in range(n_atoms)]
1315 per_chain_pos = list()
1316 per_chain_indices = list()
1319 for ch
in structure.chains:
1321 atom_indices = list()
1325 for r_idx, r
in enumerate(ch.residues):
1328 hash_code = a.handle.GetHashCode()
1329 if hash_code
in atom_index_mapping:
1331 pos_list.append(np.asarray([p[0], p[1], p[2]], dtype=np.float32))
1332 atom_indices.append(atom_index_mapping[hash_code])
1334 mask_start.extend([r_start_idx] * n_valid_atoms)
1335 mask_end.extend([r_start_idx + n_valid_atoms] * n_valid_atoms)
1336 r_start_idx += n_valid_atoms
1338 if len(pos_list) == 0:
1342 pos = np.vstack(pos_list)
1343 atom_indices = np.asarray(atom_indices, dtype=np.int32)
1345 if atom_indices.shape[0] > 20000:
1348 dists =
cdist(pos, pos)
1351 far_away = 2 * inclusion_radius
1352 for idx
in range(atom_indices.shape[0]):
1353 dists[idx, range(mask_start[idx], mask_end[idx])] = far_away
1356 within_mask = dists < inclusion_radius
1357 for idx
in range(atom_indices.shape[0]):
1358 indices_to_append = atom_indices[within_mask[idx,:]]
1359 if indices_to_append.shape[0] > 0:
1360 full_at_idx = atom_indices[idx]
1361 indices[full_at_idx].append(indices_to_append)
1362 distances[full_at_idx].append(dists[idx, within_mask[idx,:]])
1366 per_chain_pos.append(pos)
1367 per_chain_indices.append(atom_indices)
1370 min_pos = [p.min(0)
for p
in per_chain_pos]
1371 max_pos = [p.max(0)
for p
in per_chain_pos]
1372 chain_pairs = list()
1373 for idx_one
in range(len(per_chain_pos)):
1374 for idx_two
in range(idx_one + 1, len(per_chain_pos)):
1375 if np.max(min_pos[idx_one] - max_pos[idx_two]) > inclusion_radius:
1377 if np.max(min_pos[idx_two] - max_pos[idx_one]) > inclusion_radius:
1379 chain_pairs.append((idx_one, idx_two))
1382 for pair
in chain_pairs:
1383 if per_chain_pos[pair[0]].shape[0] > 20000
or per_chain_pos[pair[1]].shape[0] > 20000:
1384 dists =
blockwise_cdist(per_chain_pos[pair[0]], per_chain_pos[pair[1]])
1386 dists =
cdist(per_chain_pos[pair[0]], per_chain_pos[pair[1]])
1387 within = dists <= inclusion_radius
1390 tmp = within.sum(axis=1)
1391 for idx
in range(tmp.shape[0]):
1396 at_idx = per_chain_indices[pair[0]][idx]
1397 indices_to_insert = per_chain_indices[pair[1]][within[idx,:]]
1398 distances_to_insert = dists[idx, within[idx, :]]
1399 insertion_idx = len(indices[at_idx])
1400 for i
in range(insertion_idx):
1401 if indices_to_insert[0] > indices[at_idx][i][0]:
1404 indices[at_idx].insert(insertion_idx, indices_to_insert)
1405 distances[at_idx].insert(insertion_idx, distances_to_insert)
1408 tmp = within.sum(axis=0)
1409 for idx
in range(tmp.shape[0]):
1414 at_idx = per_chain_indices[pair[1]][idx]
1415 indices_to_insert = per_chain_indices[pair[0]][within[:, idx]]
1416 distances_to_insert = dists[within[:, idx], idx]
1417 insertion_idx = len(indices[at_idx])
1418 for i
in range(insertion_idx):
1419 if indices_to_insert[0] > indices[at_idx][i][0]:
1422 indices[at_idx].insert(insertion_idx, indices_to_insert)
1423 distances[at_idx].insert(insertion_idx, distances_to_insert)
1428 for at_idx
in range(n_atoms):
1429 if len(indices[at_idx]) > 0:
1430 ref_indices[at_idx] = np.hstack(indices[at_idx])
1431 ref_distances[at_idx] = np.hstack(distances[at_idx])
1433 return (ref_indices, ref_distances)
1436 def _SetupDistancesSC(n_atoms, chain_start_indices,
1437 ref_indices, ref_distances):
1438 """Select subset of contacts only covering intra-chain contacts
1441 ref_indices_sc = [np.asarray([], dtype=np.int32)
for idx
in range(n_atoms)]
1442 ref_distances_sc = [np.asarray([], dtype=np.float32)
for idx
in range(n_atoms)]
1444 n_chains = len(chain_start_indices)
1445 for ch_idx
in range(n_chains):
1446 chain_s = chain_start_indices[ch_idx]
1448 if ch_idx + 1 < n_chains:
1449 chain_e = chain_start_indices[ch_idx+1]
1450 for i
in range(chain_s, chain_e):
1451 if len(ref_indices[i]) > 0:
1452 intra_idx = np.where(np.logical_and(ref_indices[i]>=chain_s,
1453 ref_indices[i]<chain_e))[0]
1454 ref_indices_sc[i] = ref_indices[i][intra_idx]
1455 ref_distances_sc[i] = ref_distances[i][intra_idx]
1457 return (ref_indices_sc, ref_distances_sc)
1460 def _SetupDistancesIC(n_atoms, chain_start_indices,
1461 ref_indices, ref_distances):
1462 """Select subset of contacts only covering inter-chain contacts
1465 ref_indices_ic = [np.asarray([], dtype=np.int32)
for idx
in range(n_atoms)]
1466 ref_distances_ic = [np.asarray([], dtype=np.float32)
for idx
in range(n_atoms)]
1468 n_chains = len(chain_start_indices)
1469 for ch_idx
in range(n_chains):
1470 chain_s = chain_start_indices[ch_idx]
1472 if ch_idx + 1 < n_chains:
1473 chain_e = chain_start_indices[ch_idx+1]
1474 for i
in range(chain_s, chain_e):
1475 if len(ref_indices[i]) > 0:
1476 inter_idx = np.where(np.logical_or(ref_indices[i]<chain_s,
1477 ref_indices[i]>=chain_e))[0]
1478 ref_indices_ic[i] = ref_indices[i][inter_idx]
1479 ref_distances_ic[i] = ref_distances[i][inter_idx]
1481 return (ref_indices_ic, ref_distances_ic)
1484 def _NonSymDistances(n_atoms, symmetric_atoms, ref_indices, ref_distances):
1485 """Transfer indices/distances of non-symmetric atoms and return
1488 sym_ref_indices = [np.asarray([], dtype=np.int32)
for idx
in range(n_atoms)]
1489 sym_ref_distances = [np.asarray([], dtype=np.float32)
for idx
in range(n_atoms)]
1491 for idx
in symmetric_atoms:
1494 for i, d
in zip(ref_indices[idx], ref_distances[idx]):
1495 if i
not in symmetric_atoms:
1498 sym_ref_indices[idx] = indices
1499 sym_ref_distances[idx] = np.asarray(distances)
1501 return (sym_ref_indices, sym_ref_distances)
1503 def _EvalAtom(self, pos, atom_idx, thresholds, ref_indices, ref_distances):
1504 """Computes number of distance differences within given thresholds
1506 returns np.array with len(thresholds) elements
1508 a_p = pos[atom_idx, :]
1509 tmp = pos.take(ref_indices[atom_idx], axis=0)
1510 np.subtract(tmp, a_p[
None, :], out=tmp)
1511 np.square(tmp, out=tmp)
1512 tmp = tmp.sum(axis=1)
1513 np.sqrt(tmp, out=tmp)
1514 np.subtract(ref_distances[atom_idx], tmp, out=tmp)
1515 np.absolute(tmp, out=tmp)
1516 return np.asarray([(tmp <= thresh).sum()
for thresh
in thresholds],
1520 self, pos, atom_indices, thresholds, ref_indices, ref_distances
1522 """Calls _EvalAtom for several atoms and sums up the computed number
1523 of distance differences within given thresholds
1525 returns numpy matrix of shape (n_atoms, len(threshold))
1527 conserved = np.zeros((len(atom_indices), len(thresholds)),
1529 for a_idx, a
in enumerate(atom_indices):
1530 conserved[a_idx, :] = self.
_EvalAtom_EvalAtom(pos, a, thresholds,
1531 ref_indices, ref_distances)
1534 def _EvalResidues(self, pos, thresholds, res_atom_indices, ref_indices,
1536 """Calls _EvalAtoms for a bunch of residues
1538 residues are defined in *res_atom_indices* as lists of atom indices
1539 returns numpy matrix of shape (n_residues, len(thresholds)).
1541 conserved = np.zeros((len(res_atom_indices), len(thresholds)),
1543 for rai_idx, rai
in enumerate(res_atom_indices):
1544 conserved[rai_idx,:] = np.sum(self.
_EvalAtoms_EvalAtoms(pos, rai, thresholds,
1545 ref_indices, ref_distances), axis=0)
1548 def _ProcessSequenceSeparation(self):
1550 raise NotImplementedError(
"Congratulations! You're the first one "
1551 "requesting a non-default "
1552 "sequence_separation in the new and "
1553 "awesome LDDT implementation. A crate of "
1554 "beer for Gabriel and he'll implement "
1557 def _GetNExp(self, atom_idx, ref_indices):
1558 """Returns number of close atoms around one or several atoms
1560 if isinstance(atom_idx, int):
1561 return len(ref_indices[atom_idx])
1562 elif isinstance(atom_idx, list):
1563 return sum([len(ref_indices[idx])
for idx
in atom_idx])
1565 raise RuntimeError(
"invalid input type")
1567 def _ResolveSymmetries(self, pos, thresholds, symmetries, sym_ref_indices,
1569 """Swaps symmetric positions in-place in order to maximize LDDT scores
1570 towards non-symmetric atoms.
1572 for sym
in symmetries:
1574 atom_indices = list()
1575 for sym_tuple
in sym:
1576 atom_indices += [sym_tuple[0], sym_tuple[1]]
1577 tot = self.
_GetNExp_GetNExp(atom_indices, sym_ref_indices)
1583 sym_one_conserved = self.
_EvalAtoms_EvalAtoms(
1593 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
1595 sym_two_conserved = self.
_EvalAtoms_EvalAtoms(
1603 sym_one_score = np.sum(sym_one_conserved) / (len(thresholds) * tot)
1604 sym_two_score = np.sum(sym_two_conserved) / (len(thresholds) * tot)
1606 if sym_one_score >= sym_two_score:
1611 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
1613 def _EvalAtomSSD(self, pos, atom_idx, dist_cap, ref_indices, ref_distances):
1614 """ Computes summed squared distances
1616 distances are capped at dist_cap
1618 a_p = pos[atom_idx, :]
1619 tmp = pos.take(ref_indices[atom_idx], axis=0)
1620 np.subtract(tmp, a_p[
None, :], out=tmp)
1621 np.square(tmp, out=tmp)
1622 tmp = tmp.sum(axis=1)
1623 np.sqrt(tmp, out=tmp)
1624 np.subtract(ref_distances[atom_idx], tmp, out=tmp)
1625 np.square(tmp, out=tmp)
1626 squared_dist_cap = dist_cap*dist_cap
1627 tmp[tmp > squared_dist_cap] = squared_dist_cap
1631 self, pos, atom_indices, dist_cap, ref_indices, ref_distances
1633 """Calls _EvalAtomSSD for several atoms
1635 return np.asarray([self.
_EvalAtomSSD_EvalAtomSSD(pos, a, dist_cap, ref_indices,
1636 ref_distances)
for a
in atom_indices],
1639 def _ResolveSymmetriesSSD(self, pos, dist_cap, symmetries, sym_ref_indices,
1641 """Swaps symmetric positions in-place in order to maximize summed
1642 squared distances towards non-symmetric atoms.
1644 for sym
in symmetries:
1646 atom_indices = list()
1647 for sym_tuple
in sym:
1648 atom_indices += [sym_tuple[0], sym_tuple[1]]
1649 tot = self.
_GetNExp_GetNExp(atom_indices, sym_ref_indices)
1665 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
1675 sym_one_score = np.sum(sym_one_ssd)
1676 sym_two_score = np.sum(sym_two_ssd)
1678 if sym_one_score < sym_two_score:
1681 pos[[pair[0], pair[1]]] = pos[[pair[1], pair[0]]]
def __init__(self, atom_names)
def AddSymmetricCompound(self, name, symmetric_atoms)
def _SetupCompound(self, r, compound_lib, custom_compounds, symmetry_settings, bb_only)
def 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)
def sym_ref_indices(self)
def _ResolveSymmetriesSSD(self, pos, dist_cap, symmetries, sym_ref_indices, sym_ref_distances)
def _GetChainRNums(self, ch, residue_mapping, model_ch_name, target_ch_name)
def _ProcessSequenceSeparation(self)
def sym_ref_distances(self)
def _ResolveSymmetries(self, pos, thresholds, symmetries, sym_ref_indices, sym_ref_distances)
def ref_distances_ic(self)
def _GetTargetResidueNumbers(self, target, seqres_mapping)
def _EvalAtom(self, pos, atom_idx, thresholds, ref_indices, ref_distances)
def sym_ref_distances_ic(self)
def sym_ref_distances_sc(self)
def sym_ref_indices_ic(self)
def GetNChainContacts(self, target_chain, no_interchain=False)
def sym_ref_indices_sc(self)
def ref_distances_sc(self)
def _AddMdlContacts(self, model, res_atom_indices, res_atom_hashes, ref_indices, ref_distances, no_interchain, no_intrachain)
def 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)
def _GetNExp(self, atom_idx, ref_indices)
def __init__(self, target, compound_lib=None, custom_compounds=None, inclusion_radius=15, sequence_separation=0, symmetry_settings=None, seqres_mapping=dict(), bb_only=False)
def _EvalAtomsSSD(self, pos, atom_indices, dist_cap, ref_indices, ref_distances)
def _SetupEnv(self, compound_lib, custom_compounds, symmetry_settings, seqres_mapping, bb_only)
def _EvalAtomSSD(self, pos, atom_idx, dist_cap, ref_indices, ref_distances)
def _ProcessModel(self, model, chain_mapping, residue_mapping=None, nirvana_dist=100, check_resnames=True)
def _GetExtraModelChainPenalty(self, model, chain_mapping)
def _EvalAtoms(self, pos, atom_indices, thresholds, ref_indices, ref_distances)
def GetDefaultSymmetrySettings()
def blockwise_cdist(A, B, block_size=1000)