diff --git a/modules/mol/alg/pymod/qsscoring.py b/modules/mol/alg/pymod/qsscoring.py index bc1a62c2efac77e7d660326d39233ba96005f3bd..0ace94a439ffc82c840ec20914eb9501d1721d88 100644 --- a/modules/mol/alg/pymod/qsscoring.py +++ b/modules/mol/alg/pymod/qsscoring.py @@ -180,6 +180,10 @@ class QSscorer: :type: :class:`dict` with key = :class:`tuple` of chain names in :attr:`qs_ent_1` and value = :class:`tuple` of chain names in :attr:`qs_ent_2`. + + :raises: :class:`QSscoreError` if we end up having less than 2 chains for + either entity in the mapping (can happen if chains do not have CA + atoms). """ if self._chem_mapping is None: self._chem_mapping = _GetChemGroupsMapping(self.qs_ent_1, self.qs_ent_2) @@ -709,7 +713,8 @@ class QSscoreEntity(object): def chem_groups(self): """ Intra-complex group of chemically identical (seq. id. > 95%) polypeptide - chains. First chain in group is the one with the longest sequence. + chains as extracted from :attr:`ca_chains`. First chain in group is the one + with the longest sequence. :getter: Computed on first use (cached) :type: :class:`list` of :class:`list` of :class:`str` (chain names) @@ -994,8 +999,8 @@ def _GetChemGroups(qs_ent, seqid_thr=95.): :type seqid_thr: :class:`float` """ # get data from qs_ent - chain_names = [ch.name for ch in qs_ent.ent.chains] ca_chains = qs_ent.ca_chains + chain_names = sorted(ca_chains.keys()) # get pairs of identical chains # NOTE: this scales quadratically with number of chains and may be optimized # -> one could merge it with "merge transitive pairs" below... @@ -1119,7 +1124,10 @@ def _GetChemGroupsMapping(qs_ent_1, qs_ent_2): LogWarning('Unmapped Chains in %s: %s' % (qs_ent_2.GetName(), ','.join(list(chains_2 - mapped_2)))) + # check if we have any chains left LogInfo('Chemical chain-groups mapping: ' + str(chem_mapping)) + if len(mapped_1) < 2 or len(mapped_2) < 2: + raise QSscoreError('Less than 2 chains left in chem_mapping.') return chem_mapping def _SelectFew(l, max_elements): diff --git a/modules/mol/alg/tests/test_qsscoring.py b/modules/mol/alg/tests/test_qsscoring.py index a860a05287ee60935a962b90ee961204a699cf28..ddc3a465a231c32219bf33e1b4eea375cdc0b4ca 100644 --- a/modules/mol/alg/tests/test_qsscoring.py +++ b/modules/mol/alg/tests/test_qsscoring.py @@ -71,8 +71,8 @@ class TestQSscore(unittest.TestCase): # use QSscoreEntity to go faster qs_scorer_symm = QSscorer(qs_scorer.qs_ent_1, qs_scorer.qs_ent_2) qs_scorer_symm.SetSymmetries(symm_1, symm_2) - self.assertAlmostEqual(qs_scorer_symm.global_score, 0.359, 2) - self.assertAlmostEqual(qs_scorer_symm.best_score, 0.958, 2) + self.assertAlmostEqual(qs_scorer_symm.global_score, qs_scorer.global_score) + self.assertAlmostEqual(qs_scorer_symm.best_score, qs_scorer.best_score) def test_HeteroCase4(self): # inverted chains @@ -82,6 +82,22 @@ class TestQSscore(unittest.TestCase): self.assertAlmostEqual(qs_scorer.global_score, 0.980, 2) self.assertAlmostEqual(qs_scorer.best_score, 0.980, 2) + # check if CA-only scoring is close to this + ent_2_ca = ent_2.Select('aname=CA') + # use QSscoreEntity to go faster + qs_scorer_ca = QSscorer(qs_scorer.qs_ent_1, ent_2_ca) + self.assertAlmostEqual(qs_scorer_ca.global_score, qs_scorer.global_score, 2) + self.assertAlmostEqual(qs_scorer_ca.best_score, qs_scorer.best_score, 2) + # throw exception for messed up chains without CA atoms + ent_2_no_ca = ent_2.Select('aname!=CA') + with self.assertRaises(QSscoreError): + qs_scorer_tst = QSscorer(qs_scorer.qs_ent_1, ent_2_no_ca) + qs_scorer_tst.global_score + ent_2_almost_no_ca = ent_2.Select('aname!=CA or cname=A') + with self.assertRaises(QSscoreError): + qs_scorer_tst = QSscorer(qs_scorer.qs_ent_1, ent_2_almost_no_ca) + qs_scorer_tst.global_score + def test_HeteroModel(self): # uncomplete model missing 2 third of the contacts ent_1 = _LoadFile('1eud_ref.pdb') # AB, no symmetry @@ -101,20 +117,6 @@ class TestQSscore(unittest.TestCase): self.assertAlmostEqual(qs_scorer.global_score, 0.147, 2) self.assertAlmostEqual(qs_scorer.best_score, 0.866, 2) - def test_lDDT(self): - # lDDT is not symmetrical and does not account for overprediction! - ref = _LoadFile('4br6.1.pdb').Select('cname=A,B') - mdl = _LoadFile('4br6.1.pdb') - qs_scorer = QSscorer(ref, mdl) - self.assertAlmostEqual(qs_scorer.global_score, 0.171, 2) - self.assertAlmostEqual(qs_scorer.best_score, 1.00, 2) - self.assertAlmostEqual(qs_scorer.lddt_score, 1.00, 2) - # flip them (use QSscoreEntity to go faster) - qs_scorer2 = QSscorer(qs_scorer.qs_ent_2, qs_scorer.qs_ent_1) - self.assertAlmostEqual(qs_scorer2.global_score, 0.171, 2) - self.assertAlmostEqual(qs_scorer2.best_score, 1.00, 2) - self.assertAlmostEqual(qs_scorer2.lddt_score, 0.483, 2) - def test_HomoCase2(self): # broken cyclic symmetry ent_1 = _LoadFile('4r7y.1.pdb') # A6, symmetry: C6 @@ -134,6 +136,23 @@ class TestQSscore(unittest.TestCase): self.assertEqual(qs_scorer_symm.best_score, qs_scorer.best_score) + # TEST EXTRA SCORES + + def test_lDDT(self): + # lDDT is not symmetrical and does not account for overprediction! + ref = _LoadFile('4br6.1.pdb').Select('cname=A,B') + mdl = _LoadFile('4br6.1.pdb') + qs_scorer = QSscorer(ref, mdl) + self.assertAlmostEqual(qs_scorer.global_score, 0.171, 2) + self.assertAlmostEqual(qs_scorer.best_score, 1.00, 2) + self.assertAlmostEqual(qs_scorer.lddt_score, 1.00, 2) + # flip them (use QSscoreEntity to go faster) + qs_scorer2 = QSscorer(qs_scorer.qs_ent_2, qs_scorer.qs_ent_1) + self.assertAlmostEqual(qs_scorer2.global_score, 0.171, 2) + self.assertAlmostEqual(qs_scorer2.best_score, 1.00, 2) + self.assertAlmostEqual(qs_scorer2.lddt_score, 0.483, 2) + + # TEST BIG STUFF and FANCY SYMMETRIES def test_HeteroBig(self):