diff --git a/modules/mol/alg/pymod/chain_mapping.py b/modules/mol/alg/pymod/chain_mapping.py index 2307ee2b1651156097de51055b70591673f1dd10..dd3ba5b5c9b63a7686deff3d9e70c2ab358a3ee5 100644 --- a/modules/mol/alg/pymod/chain_mapping.py +++ b/modules/mol/alg/pymod/chain_mapping.py @@ -2467,11 +2467,10 @@ class _QSScoreGreedySearcher(qsscore.QSScorer): if max_ext is not None and len(newly_mapped_ref_chains) >= max_ext: break - # nominator and denominator to determine current QS score - nominator, denominator = self._FromFlatMapping(mapping) - old_score = 0.0 - if denominator != 0.0: - old_score = nominator/denominator + score_result = self.FromFlatMapping(mapping) + old_score = score_result.QS_global + nominator = score_result.weighted_scores + denominator = score_result.weight_sum + score_result.weight_extra_all max_diff = 0.0 max_mapping = None @@ -2487,9 +2486,10 @@ class _QSScoreGreedySearcher(qsscore.QSScorer): # are added to mapping int1 = (ref_ch, neighbor) int2 = (mdl_ch, mapping[neighbor]) - a, b = self._MappedInterfaceScores(int1, int2) - nominator_diff += a - denominator_diff += b + a, b, c, d = self._MappedInterfaceScores(int1, int2) + nominator_diff += a # weighted_scores + denominator_diff += b # weight_sum + denominator_diff += d # weight_extra_all # the respective interface penalties are subtracted # from denominator denominator_diff -= self._InterfacePenalty1(int1) @@ -2552,10 +2552,8 @@ class _QSScoreGreedySearcher(qsscore.QSScorer): # try all possible mapping swaps. Swaps that improve the score are # immediately accepted and we start all over again - nominator, denominator = self._FromFlatMapping(mapping) - current_score = 0.0 - if denominator != 0.0: - current_score = nominator / denominator + score_result = self.FromFlatMapping(mapping) + current_score = score_result.QS_global something_happened = True while something_happened: something_happened = False @@ -2566,16 +2564,12 @@ class _QSScoreGreedySearcher(qsscore.QSScorer): swapped_mapping = dict(mapping) swapped_mapping[ch1] = mapping[ch2] swapped_mapping[ch2] = mapping[ch1] - a, b = self._FromFlatMapping(swapped_mapping) - score = 0.0 - if b != 0.0: - score = a/b - if score > current_score: + score_result = self.FromFlatMapping(swapped_mapping) + if score_result.QS_global > current_score: something_happened = True mapping = swapped_mapping - current_score = score + current_score = score_result.QS_global break - return mapping @@ -2587,10 +2581,10 @@ def _QSScoreNaive(trg, mdl, chem_groups, chem_mapping, ref_mdl_alns, contact_d, # you'll just hit a wall when the number of possible mappings becomes large qs_scorer = qsscore.QSScorer(trg, chem_groups, mdl, ref_mdl_alns) for mapping in _ChainMappings(chem_groups, chem_mapping, n_max_naive): - score = qs_scorer.GetQSScore(mapping, check=False) - if score > best_score: + score_result = qs_scorer.Score(mapping, check=False) + if score_result.QS_global > best_score: best_mapping = mapping - best_score = score + best_score = score_result.QS_global return best_mapping @@ -2675,12 +2669,9 @@ def _QSScoreGreedyFull(the_greed, n_mdl_chains): tmp_mapping = dict(mapping) tmp_mapping[seed[0]] = seed[1] tmp_mapping = the_greed.ExtendMapping(tmp_mapping) - a, b = the_greed._FromFlatMapping(tmp_mapping) - tmp_score = 0.0 - if b != 0.0: - tmp_score = a/b - if tmp_score > best_score: - best_score = tmp_score + score_result = the_greed.FromFlatMapping(tmp_mapping) + if score_result.QS_global > best_score: + best_score = score_result.QS_global best_mapping = tmp_mapping if best_mapping is not None and len(best_mapping) > len(mapping): @@ -2750,12 +2741,9 @@ def _QSScoreGreedyBlock(the_greed, seed_size, blocks_per_chem_group): seed = dict(mapping) seed.update({s[0]: s[1]}) seed = the_greed.ExtendMapping(seed, max_ext = max_ext) - a, b = the_greed._FromFlatMapping(seed) - seed_score = 0.0 - if b != 0.0: - seed_score = a/b - if seed_score > best_score: - best_score = seed_score + score_result = the_greed.FromFlatMapping(seed) + if score_result.QS_global > best_score: + best_score = score_result.QS_global best_mapping = seed if best_mapping != None: starting_blocks.append(best_mapping) @@ -2765,12 +2753,9 @@ def _QSScoreGreedyBlock(the_greed, seed_size, blocks_per_chem_group): best_mapping = None for seed in starting_blocks: seed = the_greed.ExtendMapping(seed) - a, b = the_greed._FromFlatMapping(seed) - seed_score = 0.0 - if b != 0.0: - seed_score = a/b - if seed_score > best_score: - best_score = seed_score + score_result = the_greed.FromFlatMapping(seed) + if score_result.QS_global > best_score: + best_score = score_result.QS_global best_mapping = seed if best_mapping is not None and len(best_mapping) > len(mapping): diff --git a/modules/mol/alg/pymod/qsscore.py b/modules/mol/alg/pymod/qsscore.py index 391499ae514f6c3c00a4163c501746538378fdea..3d28456532e7eb94ee80a9ba5a2d88cf6d7bf485 100644 --- a/modules/mol/alg/pymod/qsscore.py +++ b/modules/mol/alg/pymod/qsscore.py @@ -134,38 +134,20 @@ class QSEntity: 'euclidean') return self._pair_dist[key] -class QSScorer: - """ Helper object to compute QS-score - - It's conceptually tightly integrated into the mechanisms from the - chain_mapping module. The prefered way to derive an object of type - :class:`QSScorer` is through the static constructor: - :func:`~FromMappingResult`. Example score computation including mapping: +class QSScorerResult: + """ + Holds data relevant for QS-score computation. Formulas for QS scores: :: - from ost.mol.alg.qsscore import QSScorer - from ost.mol.alg.chain_mapping import ChainMapper - - ent_1 = io.LoadPDB("path_to_assembly_1.pdb") - ent_2 = io.LoadPDB("path_to_assembly_2.pdb") - - chain_mapper = ChainMapper(ent_1) - mapping_result = chain_mapper.GetlDDTMapping(ent_2) - qs_scorer = QSScorer.FromMappingResult(mapping_result) - print("score:", qs_scorer.GetQSScore(mapping_result.mapping)) - - - Formulas for QS scores: - - :: - + - QS_best = weighted_scores / (weight_sum + weight_extra_mapped) - QS_global = weighted_scores / (weight_sum + weight_extra_all) -> weighted_scores = sum(w(min(d1,d2)) * (1 - abs(d1-d2)/12)) for shared -> weight_sum = sum(w(min(d1,d2))) for shared + -> weight_extra_mapped = sum(w(d)) for all mapped but non-shared -> weight_extra_all = sum(w(d)) for all non-shared -> w(d) = 1 if d <= 5, exp(-2 * ((d-5.0)/4.28)^2) else - + In the formulas above: * "d": CA/CB-CA/CB distance of an "inter-chain contact" ("d1", "d2" for @@ -178,10 +160,97 @@ class QSScorer: (fallback to CA-CA if :attr:`calpha_only` is True). * "w(d)": weighting function (prob. of 2 res. to interact given CB distance) 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): + self._weighted_scores = weighted_scores + self._weight_sum = weight_sum + self._weight_extra_mapped = weight_extra_mapped + self._weight_extra_all = weight_extra_all - The actual QS-score computation in :func:`QSScorer.GetQSScore` implements - caching. Repeated computations with alternative chain mappings thus become - faster. + @property + def weighted_scores(self): + """ weighted_scores attribute as described in formula section above + + :type: :class:`float` + """ + return self._weighted_scores + + @property + def weight_sum(self): + """ weight_sum attribute as described in formula section above + + :type: :class:`float` + """ + return self._weight_sum + + @property + def weight_extra_mapped(self): + """ weight_extra_mapped attribute as described in formula section above + + :type: :class:`float` + """ + return self._weight_extra_mapped + + @property + def weight_extra_all(self): + """ weight_extra_all attribute as described in formula section above + + :type: :class:`float` + """ + return self._weight_extra_all + + @property + def QS_best(self): + """ QS_best - the actual score as described in formula section above + + :type: :class:`float` + """ + nominator = self.weighted_scores + denominator = self.weight_sum + self.weight_extra_mapped + if denominator != 0.0: + return nominator/denominator + else: + return 0.0 + + @property + def QS_global(self): + """ QS_global - the actual score as described in formula section above + + :type: :class:`float` + """ + nominator = self.weighted_scores + denominator = self.weight_sum + self.weight_extra_all + if denominator != 0.0: + return nominator/denominator + else: + return 0.0 + + +class QSScorer: + """ Helper object to compute QS-score + + Tightly integrated into the mechanisms from the chain_mapping module. + The prefered way to derive an object of type :class:`QSScorer` is through + the static constructor: :func:`~FromMappingResult`. Example score + computation including mapping: + + :: + + from ost.mol.alg.qsscore import QSScorer + from ost.mol.alg.chain_mapping import ChainMapper + + ent_1 = io.LoadPDB("path_to_assembly_1.pdb") + ent_2 = io.LoadPDB("path_to_assembly_2.pdb") + + chain_mapper = ChainMapper(ent_1) + mapping_result = chain_mapper.GetlDDTMapping(ent_2) + qs_scorer = QSScorer.FromMappingResult(mapping_result) + score_result = qs_scorer.GetQSScore(mapping_result.mapping) + print("score:", score_result.QS_global) + + QS-score computation in :func:`QSScorer.Score` implements caching. + Repeated computations with alternative chain mappings thus become faster. :param target: Structure designated as "target". Can be fetched from :class:`ost.mol.alg.chain_mapping.MappingResult` @@ -221,17 +290,16 @@ class QSScorer: # cache for mapped interface scores # key: tuple of tuple ((qsent1_ch1, qsent1_ch2), # ((qsent2_ch1, qsent2_ch2)) - # value: tuple with two numbers referring to QS-score formalism - # (equation 6 in Bertoni et al., 2017): - # 1: contribution of that interface to nominator - # 2: contribution of that interface to denominator + # value: tuple with four numbers referring to QS-score formalism + # 1: weighted_scores + # 2: weight_sum + # 3: weight_extra_mapped + # 4: weight_extra_all self._mapped_cache = dict() # cache for non-mapped interfaces in qsent1 # key: tuple (qsent1_ch1, qsent1_ch2) - # value: contribution of that interface to second term in denominator in - # equation 6 in Bertoni et al., 2017. - # (sum of non-shared penalty of qsent1) + # value: contribution of that interface to weight_extra_all self._qsent_1_penalties = dict() # same for qsent2 @@ -291,10 +359,10 @@ class QSScorer: """ return self._alns - def GetQSScore(self, mapping, check=True): + def Score(self, mapping, check=True): """ Computes QS-score given chain mapping - Again, the preferred way is to get this chain mapping is from an object + Again, the preferred way is to get *mapping* is from an object of type :class:`ost.mol.alg.chain_mapping.MappingResult`. :param mapping: see @@ -303,7 +371,7 @@ class QSScorer: :param check: Perform input checks, can be disabled for speed purposes if you know what you're doing. :type check: :class:`bool` - :returns: The QS-Score + :returns: Result object of type :class:`QSScorerResult` """ if check: @@ -326,19 +394,21 @@ class QSScorer: for a, b in zip(self.chem_groups, mapping): flat_mapping.update({x: y for x, y in zip(a, b) if y is not None}) - # refers to equation 6 in Bertoni et al., 2017 - nominator, denominator = self._FromFlatMapping(flat_mapping) + return self.FromFlatMapping(flat_mapping) - if denominator > 0.0: - return nominator / denominator - else: - return 0.0 + def FromFlatMapping(self, flat_mapping): + """ Same as :func:`Score` but with flat mapping - def _FromFlatMapping(self, flat_mapping): + :param flat_mapping: Dictionary with target chain names as keys and + the mapped model chain names as value + :type flat_mapping: :class:`dict` with :class:`str` as key and value + :returns: Result object of type :class:`QSScorerResult` + """ - # refers to equation 6 in Bertoni et al., 2017 - nominator = 0.0 - denominator= 0.0 + weighted_scores = 0.0 + weight_sum = 0.0 + weight_extra_mapped = 0.0 + weight_extra_all = 0.0 # keep track of processed interfaces in qsent2 processed_qsent2_interfaces = set() @@ -346,20 +416,32 @@ class QSScorer: for int1 in self.qsent1.interacting_chains: if int1[0] in flat_mapping and int1[1] in flat_mapping: int2 = (flat_mapping[int1[0]], flat_mapping[int1[1]]) - a,b = self._MappedInterfaceScores(int1, int2) - nominator += a - denominator += b + a, b, c, d = self._MappedInterfaceScores(int1, int2) + weighted_scores += a + weight_sum += b + weight_extra_mapped += c + weight_extra_all += d processed_qsent2_interfaces.add((min(int2[0], int2[1]), max(int2[0], int2[1]))) else: - denominator += self._InterfacePenalty1(int1) + weight_extra_all += self._InterfacePenalty1(int1) - # add penalties for non-mapped interfaces in qsent2 + # process interfaces that only exist in qsent2 + r_flat_mapping = {v:k for k,v in flat_mapping.items()} # reverse mapping... for int2 in self.qsent2.interacting_chains: if int2 not in processed_qsent2_interfaces: - denominator += self._InterfacePenalty2(int2) - - return (nominator, denominator) + if int2[0] in r_flat_mapping and int2[1] in r_flat_mapping: + int1 = (r_flat_mapping[int2[0]], r_flat_mapping[int2[1]]) + a, b, c, d = self._MappedInterfaceScores(int1, int2) + weighted_scores += a + weight_sum += b + weight_extra_mapped += c + weight_extra_all += d + else: + weight_extra_all += self._InterfacePenalty2(int2) + + return QSScorerResult(weighted_scores, weight_sum, weight_extra_mapped, + weight_extra_all) def _MappedInterfaceScores(self, int1, int2): key_one = (int1, int2) @@ -368,9 +450,12 @@ class QSScorer: key_two = ((int1[1], int1[0]), (int2[1], int2[0])) if key_two in self._mapped_cache: return self._mapped_cache[key_two] - nominator, denominator = self._InterfaceScores(int1, int2) - self._mapped_cache[key_one] = (nominator, denominator) - return (nominator, denominator) + + weighted_scores, weight_sum, weight_extra_mapped, weight_extra_all = \ + self._InterfaceScores(int1, int2) + self._mapped_cache[key_one] = (weighted_scores, weight_sum, weight_extra_mapped, + weight_extra_all) + return (weighted_scores, weight_sum, weight_extra_mapped, weight_extra_all) def _InterfaceScores(self, int1, int2): @@ -392,23 +477,30 @@ class QSScorer: self._IndexMapping(int1[1], int2[1]) # get shared_masks - for this we first need to select the actual - # mapped positions to get a one-to-one relationshop and map it back + # mapped positions to get a one-to-one relationship and map it back # to the original mask size - mapped_idx_grid_1 = np.ix_(mapped_indices_1_1, mapped_indices_2_1) - mapped_idx_grid_2 = np.ix_(mapped_indices_1_2, mapped_indices_2_2) - mapped_d1 = d1[mapped_idx_grid_1] - mapped_d2 = d2[mapped_idx_grid_2] - assert(mapped_d1.shape == mapped_d2.shape) assert(self.qsent1.contact_d == self.qsent2.contact_d) contact_d = self.qsent1.contact_d - shared_mask = np.logical_and(mapped_d1 < contact_d, - mapped_d2 < contact_d) + mapped_idx_grid_1 = np.ix_(mapped_indices_1_1, mapped_indices_2_1) + mapped_idx_grid_2 = np.ix_(mapped_indices_1_2, mapped_indices_2_2) + mapped_d1_contacts = d1[mapped_idx_grid_1] < contact_d + mapped_d2_contacts = d2[mapped_idx_grid_2] < contact_d + assert(mapped_d1_contacts.shape == mapped_d2_contacts.shape) + shared_mask = np.logical_and(mapped_d1_contacts, mapped_d2_contacts) shared_mask_d1 = np.full(d1.shape, False, dtype=bool) shared_mask_d1[mapped_idx_grid_1] = shared_mask shared_mask_d2 = np.full(d2.shape, False, dtype=bool) shared_mask_d2[mapped_idx_grid_2] = shared_mask - # nominator and denominator contributions from shared contacts + # get mapped but nonshared masks + mapped_nonshared_mask_d1 = np.full(d1.shape, False, dtype=bool) + mapped_nonshared_mask_d1[mapped_idx_grid_1] = \ + np.logical_and(np.logical_not(shared_mask), mapped_d1_contacts) + mapped_nonshared_mask_d2 = np.full(d2.shape, False, dtype=bool) + mapped_nonshared_mask_d2[mapped_idx_grid_2] = \ + np.logical_and(np.logical_not(shared_mask), mapped_d2_contacts) + + # contributions from shared contacts shared_d1 = d1[shared_mask_d1] shared_d2 = d2[shared_mask_d2] shared_min = np.minimum(shared_d1, shared_d2) @@ -419,26 +511,40 @@ class QSScorer: weight_term[bigger_5_mask] = weights diff_term = np.subtract(np.ones(weight_term.shape[0]), shared_abs_diff_div_12) - nominator = np.sum(np.multiply(weight_term, diff_term)) - denominator = np.sum(weight_term) + weighted_scores = np.sum(np.multiply(weight_term, diff_term)) + weight_sum = np.sum(weight_term) - # denominator contribution from non shared contacts in interface one - contact_distances = d1[np.logical_and(np.logical_not(shared_mask_d1), - d1 < contact_d)] + # do weight_extra_all for interface one + nonshared_contact_mask_d1 = np.logical_and(np.logical_not(shared_mask_d1), + d1 < contact_d) + contact_distances = d1[nonshared_contact_mask_d1] bigger_5 = contact_distances[contact_distances > 5] - denominator += np.sum(np.exp(-2.0*np.square((bigger_5-5.0)/4.28))) + weight_extra_all = np.sum(np.exp(-2.0*np.square((bigger_5-5.0)/4.28))) # add 1.0 for all contact distances <= 5.0 - denominator += contact_distances.shape[0] - bigger_5.shape[0] + weight_extra_all += contact_distances.shape[0] - bigger_5.shape[0] + # same for interface two + nonshared_contact_mask_d2 = np.logical_and(np.logical_not(shared_mask_d2), + d2 < contact_d) + contact_distances = d2[nonshared_contact_mask_d2] + bigger_5 = contact_distances[contact_distances > 5] + weight_extra_all += np.sum(np.exp(-2.0*np.square((bigger_5-5.0)/4.28))) + # add 1.0 for all contact distances <= 5.0 + weight_extra_all += contact_distances.shape[0] - bigger_5.shape[0] + # do weight_extra_mapped for interface one + contact_distances = d1[mapped_nonshared_mask_d1] + bigger_5 = contact_distances[contact_distances > 5] + weight_extra_mapped = np.sum(np.exp(-2.0*np.square((bigger_5-5.0)/4.28))) + # add 1.0 for all contact distances <= 5.0 + weight_extra_mapped += contact_distances.shape[0] - bigger_5.shape[0] # same for interface two - contact_distances = d2[np.logical_and(np.logical_not(shared_mask_d2), - d2 < contact_d)] + contact_distances = d2[mapped_nonshared_mask_d2] bigger_5 = contact_distances[contact_distances > 5] - denominator += np.sum(np.exp(-2.0*np.square((bigger_5-5.0)/4.28))) + weight_extra_mapped += np.sum(np.exp(-2.0*np.square((bigger_5-5.0)/4.28))) # add 1.0 for all contact distances <= 5.0 - denominator += contact_distances.shape[0] - bigger_5.shape[0] + weight_extra_mapped += contact_distances.shape[0] - bigger_5.shape[0] - return (nominator, denominator) + return (weighted_scores, weight_sum, weight_extra_mapped, weight_extra_all) def _IndexMapping(self, ch1, ch2): """ Fetches aln and returns indices of (non-)aligned residues diff --git a/modules/mol/alg/tests/test_qsscore.py b/modules/mol/alg/tests/test_qsscore.py index bf2ac40916ea9d1b09db9ddd8144a755466ec1b3..34ba726ad41926866a292c28174bebc05c3ac82e 100644 --- a/modules/mol/alg/tests/test_qsscore.py +++ b/modules/mol/alg/tests/test_qsscore.py @@ -80,8 +80,8 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(target) res = mapper.GetRigidMapping(model, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.472, places=2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.472, places=2) def test_hetero_case_1(self): # additional chains @@ -90,8 +90,20 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.825, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.825, 2) + self.assertAlmostEqual(score_result.QS_best, 1.0, 2) + + def test_hetero_case_1_switched_order(self): + # additional chains + ent_2 = _LoadFile('4ux8.1.pdb') # A2 B2 C2, symmetry: C2 + ent_1 = _LoadFile('3fub.2.pdb') # A2 B2 , symmetry: C2 + mapper = ChainMapper(ent_1) + res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") + qs_scorer = QSScorer.FromMappingResult(res) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.825, 2) + self.assertAlmostEqual(score_result.QS_best, 1.0, 2) def test_HeteroCase1b(self): # as above but with assymetric unit of 3fub @@ -101,8 +113,14 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.356, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.356, 2) + self.assertAlmostEqual(score_result.QS_best, 0.419, 2) + + def test_HeteroCase1b_switched_order(self): + # chain mapping differs a bit when switching the order... I'm just + # too lazy... + pass def test_hetero_case_2(self): # different stoichiometry @@ -111,8 +129,20 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.3131, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.3131, 2) + self.assertAlmostEqual(score_result.QS_best, 0.941, 2) + + def test_hetero_case_2_switched_order(self): + # different stoichiometry + ent_2 = _LoadFile('1efu.1.pdb') # A2 B2, symmetry: C2 + ent_1 = _LoadFile('4pc6.1.pdb') # A B , no symmetry + mapper = ChainMapper(ent_1) + res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") + qs_scorer = QSScorer.FromMappingResult(res) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.3131, 2) + self.assertAlmostEqual(score_result.QS_best, 0.941, 2) def test_hetero_case_3(self): # more chains @@ -121,8 +151,20 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.359, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.359, 2) + self.assertAlmostEqual(score_result.QS_best, 0.958, 2) + + def test_hetero_case_3_switched_order(self): + # more chains + ent_2 = _LoadFile('2vjt.1.pdb') # A6 B6, symmetry: D3 + ent_1 = _LoadFile('3dbj.1.pdb') # A3 B3, symmetry: C3 + mapper = ChainMapper(ent_1) + res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") + qs_scorer = QSScorer.FromMappingResult(res) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.359, 2) + self.assertAlmostEqual(score_result.QS_best, 0.958, 2) def test_hetero_case_4(self): # inverted chains @@ -131,8 +173,20 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.980, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.980, 2) + self.assertAlmostEqual(score_result.QS_best, 0.980, 2) + + def test_hetero_case_4_switched_order(self): + # inverted chains + ent_2 = _LoadFile('3ia3.1.pdb') # AB, no symmetry + ent_1 = _LoadFile('3ia3.2.pdb') # BA, no symmetry + mapper = ChainMapper(ent_1) + res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") + qs_scorer = QSScorer.FromMappingResult(res) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.980, 2) + self.assertAlmostEqual(score_result.QS_best, 0.980, 2) def test_hetero_model(self): # uncomplete model missing 2 third of the contacts @@ -141,8 +195,9 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(target) res = mapper.GetRigidMapping(model, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.323, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.323, 2) + self.assertAlmostEqual(score_result.QS_best, 0.921, 2) def test_hetero_model_switched_order(self): # same as above but with switched order to test for symmetric behaviour @@ -152,8 +207,9 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(target) res = mapper.GetRigidMapping(model, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.323, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.323, 2) + self.assertAlmostEqual(score_result.QS_best, 0.921, 2) def test_homo_1(self): # different stoichiometry SOD @@ -165,8 +221,23 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1, pep_gap_open = -5, pep_gap_ext = -2) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 0.147, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.147, 2) + self.assertAlmostEqual(score_result.QS_best, 0.866, 2) + + def test_homo_1_switched_order(self): + # different stoichiometry SOD + ent_2 = _LoadFile('4dvh.1.pdb') # A2, symmetry: C2 + ent_1 = _LoadFile('4br6.1.pdb') # A4, symmetry: D2 + # original qsscoring uses other default values for gap_open and gap_extension + # penalties, let's use those to reproduce the old results as the alignments + # would differ otherwise + mapper = ChainMapper(ent_1, pep_gap_open = -5, pep_gap_ext = -2) + res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") + qs_scorer = QSScorer.FromMappingResult(res) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 0.147, 2) + self.assertAlmostEqual(score_result.QS_best, 0.866, 2) def test_homo_2(self): # broken cyclic symmetry @@ -175,8 +246,9 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 1/6, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 1/6, 2) + self.assertAlmostEqual(score_result.QS_best, 1.0, 2) def test_homo_2_switched_order(self): # same as above but with switched order to test for symmetric behaviour @@ -186,8 +258,9 @@ class TestQSScore(unittest.TestCase): mapper = ChainMapper(ent_1) res = mapper.GetRigidMapping(ent_2, strategy="greedy_iterative_rmsd") qs_scorer = QSScorer.FromMappingResult(res) - qs_score = qs_scorer.GetQSScore(res.mapping) - self.assertAlmostEqual(qs_score, 1/6, 2) + score_result = qs_scorer.Score(res.mapping) + self.assertAlmostEqual(score_result.QS_global, 1/6, 2) + self.assertAlmostEqual(score_result.QS_best, 1.0, 2) if __name__ == "__main__": from ost import testutils