From 8356d810a83359111fd0c994cbe07b8ca1c60a4c Mon Sep 17 00:00:00 2001 From: Xavier Robin <xavier.robin@unibas.ch> Date: Fri, 26 Apr 2024 13:35:28 +0200 Subject: [PATCH] ligand scoring: fix infinite loop in assignment --- modules/mol/alg/pymod/ligand_scoring.py | 21 +++++++++----- modules/mol/alg/tests/test_ligand_scoring.py | 29 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/modules/mol/alg/pymod/ligand_scoring.py b/modules/mol/alg/pymod/ligand_scoring.py index 4c7ebdf54..ce98690dd 100644 --- a/modules/mol/alg/pymod/ligand_scoring.py +++ b/modules/mol/alg/pymod/ligand_scoring.py @@ -695,11 +695,6 @@ class LigandScorer: # Disconnected graph is handled elsewhere continue - substructure_match = len(symmetries[0][0]) != len(model_ligand.atoms) - coverage = len(symmetries[0][0]) / len(model_ligand.atoms) - self._assignment_match_coverage[target_id, model_id] = coverage - self._assignment_isomorphisms[target_id, model_id] = 1. - ################################################################ # Compute best rmsd/lddt-pli by naively enumerating symmetries # ################################################################ @@ -713,13 +708,25 @@ class LigandScorer: ########################################### # Extend results by symmetry related info # ########################################### + if (rmsd_result is None) != (lddt_pli_result is None): + # Ligand assignment makes assumptions here, and is likely + # to not work properly if this differs. There is no reason + # it would ever do, so let's just check it + raise Exception("Ligand scoring bug: discrepency between " + "RMSD and lDDT-PLI definition.") if rmsd_result is not None: + # Now we assume both rmsd_result and lddt_pli_result are defined + # Add coverage + substructure_match = len(symmetries[0][0]) != len(model_ligand.atoms) + coverage = len(symmetries[0][0]) / len(model_ligand.atoms) + self._assignment_match_coverage[target_id, model_id] = coverage + self._assignment_isomorphisms[target_id, model_id] = 1. + # Add RMSD rmsd_result["substructure_match"] = substructure_match rmsd_result["coverage"] = coverage if self.unassigned: rmsd_result["unassigned"] = False - - if lddt_pli_result is not None: + # Add lDDT-PLI lddt_pli_result["substructure_match"] = substructure_match lddt_pli_result["coverage"] = coverage if self.unassigned: diff --git a/modules/mol/alg/tests/test_ligand_scoring.py b/modules/mol/alg/tests/test_ligand_scoring.py index 0eb503787..6755b0e8e 100644 --- a/modules/mol/alg/tests/test_ligand_scoring.py +++ b/modules/mol/alg/tests/test_ligand_scoring.py @@ -731,6 +731,35 @@ class TestLigandScoring(unittest.TestCase): sc.unassigned_target_ligands["F"][1] == "symmetries" + def test_no_binding_site(self): + """ + Test the behavior when there's no binding site in proximity of + the ligand. This test was introduced to identify some subtle issues + with the ligand assignment that can cause it to enter an infinite + loop when the data matrices are not filled properly. + """ + trg = _LoadMMCIF("1r8q.cif.gz").Copy() + mdl = trg.Copy() + + trg_zn = trg.FindResidue("H", 1) + trg_g3d = trg.FindResidue("F", 1) + + + # Move the zinc out of the reference binding site... + ed = trg.EditXCS() + ed.SetAtomPos(trg_zn.FindAtom("ZN"), + trg_zn.FindAtom("ZN").pos + geom.Vec3(6, 0, 0)) + # Remove some atoms from G3D to decrease coverage. This messed up + # the assignment in the past. + ed.DeleteAtom(trg_g3d.FindAtom("O6")) + ed.UpdateICS() + + sc = LigandScorer(mdl, trg, target_ligands=[trg_zn, trg_g3d], + coverage_delta=0, substructure_match=True) + self.assertTrue(np.isnan(sc.rmsd_matrix[0, 3])) + self.assertEqual(sc.unassigned_target_ligands["H"][1], "binding_site") + + def test_no_lddt_pli_contact(self): """ Test behaviour where a binding site has no lDDT-PLI contacts. -- GitLab