From 8984d76f0282c1f5cf5b4c4a60e5eed5d2d6dc69 Mon Sep 17 00:00:00 2001 From: Gabriel Studer <gabriel.studer@unibas.ch> Date: Thu, 20 Jun 2024 09:54:46 +0200 Subject: [PATCH] SCHWED-6298: QS-score for two monomers is defined as 1.0 They have the exact same quaternary structure in the end... This definition has been generalized to: if there are no contacts observed, QS-score is 1.0 if the scored structures have the same stoichiometry, 0.0 otherwise. This becomes a bit more abstract for higher order complexes but in principle they still have the exact same quaternary structure if they match in stoichiometry but have no single contact. --- modules/mol/alg/pymod/qsscore.py | 63 ++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/modules/mol/alg/pymod/qsscore.py b/modules/mol/alg/pymod/qsscore.py index 3b32746d6..2c98d9fa7 100644 --- a/modules/mol/alg/pymod/qsscore.py +++ b/modules/mol/alg/pymod/qsscore.py @@ -213,11 +213,12 @@ class QSScorerResult: from `Xu et al. 2009 <https://dx.doi.org/10.1016%2Fj.jmb.2008.06.002>`_. """ def __init__(self, weighted_scores, weight_sum, weight_extra_mapped, - weight_extra_all): + weight_extra_all, complete_mapping): self._weighted_scores = weighted_scores self._weight_sum = weight_sum self._weight_extra_mapped = weight_extra_mapped self._weight_extra_all = weight_extra_all + self._complete_mapping = complete_mapping @property def weighted_scores(self): @@ -251,11 +252,30 @@ class QSScorerResult: """ return self._weight_extra_all + @property + def complete_mapping(self): + """ Whether the underlying mapping of the scored assemblies is complete + + In other words: If they have the same stoichiometry. This is relevant + for :attr:`~QS_best` and :attr:`~QS_global` in case of no contacts in + any of the scored entities. + + :type: :class:`bool` + """ + return self._complete_mapping + @property def QS_best(self): """ QS_best - the actual score as described in formula section above - Returns None if there are no contacts in the compared structures + If there are no contacts observed in any of the scored entities this + score is 1.0 if we're comparing structures with + :attr:`~complete_mapping`, 0.0 otherwise. In the example of two + monomers, no contacts can be observed but they exactly match in terms + of quaternary structure. Thus a perfect score. In terms of higher order + structure that becomes a bit more abstract but in principle they still + have the exact same quaternary structure if they match in stoichiometry + but have no single contact. :type: :class:`float` """ @@ -263,14 +283,23 @@ class QSScorerResult: denominator = self.weight_sum + self.weight_extra_mapped if denominator != 0.0: return nominator/denominator + elif self.complete_mapping: + return 1.0 else: - return None + return 0.0 @property def QS_global(self): """ QS_global - the actual score as described in formula section above - Returns None if there are no contacts in the compared structures + If there are no contacts observed in any of the scored entities this + score is 1.0 if we're comparing structures with + :attr:`~complete_mapping`, 0.0 otherwise. In the example of two + monomers, no contacts can be observed but they exactly match in terms + of quaternary structure. Thus a perfect score. In terms of higher order + structure that becomes a bit more abstract but in principle they still + have the exact same quaternary structure if they match in stoichiometry + but have no single contact. :type: :class:`float` """ @@ -278,8 +307,10 @@ class QSScorerResult: denominator = self.weight_sum + self.weight_extra_all if denominator != 0.0: return nominator/denominator + elif self.complete_mapping: + return 1.0 else: - return None + return 0.0 class QSScorer: @@ -457,6 +488,11 @@ class QSScorer: This only works for interfaces that are computed in :func:`Score`, i.e. interfaces for which the alignments are set up correctly. + As all specified chains must be present, the mapping is considered + complete which affects + :attr:`QSScorerResult.QS_global`/:attr:`QSScorerResult.QS_best` in + edge cases of no observed contacts. + :param trg_ch1: Name of first interface chain in target :type trg_ch1: :class:`str` :param trg_ch2: Name of second interface chain in target @@ -484,7 +520,10 @@ class QSScorer: trg_int = (trg_ch1, trg_ch2) mdl_int = (mdl_ch1, mdl_ch2) a, b, c, d = self._MappedInterfaceScores(trg_int, mdl_int) - return QSScorerResult(a, b, c, d) + + # complete_mapping is True by definition, as the requested chain pairs + # are both present + return QSScorerResult(a, b, c, d, True) def FromFlatMapping(self, flat_mapping): """ Same as :func:`Score` but with flat mapping @@ -530,8 +569,16 @@ class QSScorer: else: weight_extra_all += self._InterfacePenalty2(int2) + trg_chains = sorted(self.qsent1.chain_names) # should be sorted already + mdl_chains = sorted(self.qsent2.chain_names) # should be sorted already + mapped_trg_chains = sorted(flat_mapping.keys()) + mapped_mdl_chains = sorted(flat_mapping.values()) + trg_complete = trg_chains == mapped_trg_chains + mdl_complete = mdl_chains == mapped_mdl_chains + complete_mapping = trg_complete and mdl_complete + return QSScorerResult(weighted_scores, weight_sum, weight_extra_mapped, - weight_extra_all) + weight_extra_all, complete_mapping) def _MappedInterfaceScores(self, int1, int2): key_one = (int1, int2) @@ -704,4 +751,4 @@ class QSScorer: return penalty # specify public interface -__all__ = ('QSEntity', 'QSScorer') +__all__ = ('QSEntity', 'QSScorer', 'QSScorerResult') -- GitLab