From ec985b7affec10d5e56defc968837956e16e06f8 Mon Sep 17 00:00:00 2001 From: Ben Webb Date: Mon, 25 Sep 2023 11:32:59 -0700 Subject: [PATCH] Add basic support for links in branched entities Add a BranchLink class which tracks bonds between saccharides, and read/write the _pdbx_entity_branch_link mmCIF table. --- docs/main.rst | 3 +++ ihm/__init__.py | 27 +++++++++++++++++++++++++++ ihm/dumper.py | 26 +++++++++++++++++++++++++- ihm/reader.py | 19 ++++++++++++++++++- test/test_dumper.py | 35 +++++++++++++++++++++++++++++++++++ test/test_main.py | 14 ++++++++++++++ test/test_reader.py | 39 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 2 deletions(-) diff --git a/docs/main.rst b/docs/main.rst index 1f9c7d6..800f7c4 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -113,3 +113,6 @@ The :mod:`ihm` Python module .. autoclass:: BranchDescriptor :members: + +.. autoclass:: BranchLink + :members: diff --git a/ihm/__init__.py b/ihm/__init__.py index e08bbfa..6006a3a 100644 --- a/ihm/__init__.py +++ b/ihm/__init__.py @@ -1304,6 +1304,10 @@ def get_chem_comp(s): #: and should be a list of :class:`BranchDescriptor` objects. self.branch_descriptors = [] + #: Any links between components in a branched entity. + #: This is a list of :class:`BranchLink` objects. + self.branch_links = [] + def __str__(self): return "" % self.description @@ -1608,3 +1612,26 @@ class BranchDescriptor(object): def __init__(self, text, type, program=None, program_version=None): self.text, self.type = text, type self.program, self.program_version = program, program_version + + +class BranchLink(object): + """A link between components in a branched entity. + These generally only make sense for oligosaccharide entities. + See :attr:`Entity.branch_links`. + + :param int num1: 1-based index of the first component. + :param str atom_id1: Name of the first atom in the linkage. + :param str leaving_atom_id1: Name of the first leaving atom. + :param int num2: 1-based index of the second component. + :param str atom_id2: Name of the second atom in the linkage. + :param str leaving_atom_id2: Name of the second leaving atom. + :param str order: Bond order (e.g. sing, doub, trip). + :param str details: More information about this link. + """ + def __init__(self, num1, atom_id1, leaving_atom_id1, num2, atom_id2, + leaving_atom_id2, order=None, details=None): + self.num1, self.atom_id1 = num1, atom_id1 + self.num2, self.atom_id2 = num2, atom_id2 + self.leaving_atom_id1 = leaving_atom_id1 + self.leaving_atom_id2 = leaving_atom_id2 + self.order, self.details = order, details diff --git a/ihm/dumper.py b/ihm/dumper.py index 609a4ed..1bc7803 100644 --- a/ihm/dumper.py +++ b/ihm/dumper.py @@ -764,6 +764,30 @@ def dump(self, system, writer): program_version=d.program_version) +class _BranchLinkDumper(Dumper): + def dump(self, system, writer): + ordinal = itertools.count(1) + with writer.loop("_pdbx_entity_branch_link", + ["link_id", "entity_id", "entity_branch_list_num_1", + "comp_id_1", "atom_id_1", "leaving_atom_id_1", + "entity_branch_list_num_2", "comp_id_2", "atom_id_2", + "leaving_atom_id_2", "value_order", + "details"]) as lp: + for entity in system.entities: + for lnk in entity.branch_links: + lp.write( + link_id=next(ordinal), entity_id=entity._id, + entity_branch_list_num_1=lnk.num1, + comp_id_1=entity.sequence[lnk.num1 - 1].id, + atom_id_1=lnk.atom_id1, + leaving_atom_id_1=lnk.leaving_atom_id1, + entity_branch_list_num_2=lnk.num2, + comp_id_2=entity.sequence[lnk.num2 - 1].id, + atom_id_2=lnk.atom_id2, + leaving_atom_id_2=lnk.leaving_atom_id2, + value_order=lnk.order, details=lnk.details) + + class _AsymIDProvider(object): """Provide unique asym IDs""" def __init__(self, seen_ids): @@ -3307,7 +3331,7 @@ class IHMVariant(Variant): _EntityPolySegmentDumper, _EntityBranchListDumper, _EntityBranchDumper, _StructAsymDumper, _PolySeqSchemeDumper, _NonPolySchemeDumper, _BranchSchemeDumper, _BranchDescriptorDumper, - _AssemblyDumper, _ExternalReferenceDumper, + _BranchLinkDumper, _AssemblyDumper, _ExternalReferenceDumper, _DatasetDumper, _ModelRepresentationDumper, _StartingModelDumper, _ProtocolDumper, _PostProcessDumper, _PseudoSiteDumper, _GeometricObjectDumper, _FeatureDumper, _CrossLinkDumper, diff --git a/ihm/reader.py b/ihm/reader.py index feccc41..7471aec 100644 --- a/ihm/reader.py +++ b/ihm/reader.py @@ -2554,6 +2554,23 @@ def __call__(self, entity_id, descriptor, type, program, program_version): e.branch_descriptors.append(d) +class _BranchLinkHandler(Handler): + category = '_pdbx_entity_branch_link' + + def __call__(self, entity_id, entity_branch_list_num_1, atom_id_1, + leaving_atom_id_1, entity_branch_list_num_2, atom_id_2, + leaving_atom_id_2, value_order, details): + e = self.sysr.entities.get_by_id(entity_id) + num1 = self.get_int(entity_branch_list_num_1) + num2 = self.get_int(entity_branch_list_num_2) + lnk = ihm.BranchLink(num1=num1, atom_id1=atom_id_1, + leaving_atom_id1=leaving_atom_id_1, + num2=num2, atom_id2=atom_id_2, + leaving_atom_id2=leaving_atom_id_2, + order=value_order, details=details) + e.branch_links.append(lnk) + + class _CrossLinkListHandler(Handler): category = '_ihm_cross_link_list' ignored_keywords = ['entity_description_1', 'entity_description_2', @@ -3373,7 +3390,7 @@ class IHMVariant(Variant): _SphereHandler, _TorusHandler, _HalfTorusHandler, _AxisHandler, _PlaneHandler, _GeometricRestraintHandler, _PolySeqSchemeHandler, _NonPolySchemeHandler, _BranchSchemeHandler, _EntityBranchListHandler, - _BranchDescriptorHandler, _CrossLinkListHandler, + _BranchDescriptorHandler, _BranchLinkHandler, _CrossLinkListHandler, _CrossLinkRestraintHandler, _CrossLinkPseudoSiteHandler, _CrossLinkResultHandler, _StartingModelSeqDifHandler, _OrderedEnsembleHandler] diff --git a/test/test_dumper.py b/test/test_dumper.py index 4a95370..b157dc0 100644 --- a/test/test_dumper.py +++ b/test/test_dumper.py @@ -4546,6 +4546,41 @@ def test_branch_descriptor_dumper(self): # """) + def test_branch_link_dumper(self): + """Test BranchLinkDumper""" + system = ihm.System() + e1 = ihm.Entity([ihm.SaccharideChemComp('NAG'), + ihm.SaccharideChemComp('BMC'), + ihm.SaccharideChemComp('FUC')]) + lnk1 = ihm.BranchLink(num1=1, atom_id1='CA', leaving_atom_id1='H1', + num2=2, atom_id2='N', leaving_atom_id2='H2', + order='sing', details='foo') + lnk2 = ihm.BranchLink(num1=2, atom_id1='CA', leaving_atom_id1='H1', + num2=3, atom_id2='N', leaving_atom_id2='H2') + e1.branch_links.extend((lnk1, lnk2)) + system.entities.append(e1) + ihm.dumper._EntityDumper().finalize(system) + dumper = ihm.dumper._BranchLinkDumper() + out = _get_dumper_output(dumper, system) + self.assertEqual(out, """# +loop_ +_pdbx_entity_branch_link.link_id +_pdbx_entity_branch_link.entity_id +_pdbx_entity_branch_link.entity_branch_list_num_1 +_pdbx_entity_branch_link.comp_id_1 +_pdbx_entity_branch_link.atom_id_1 +_pdbx_entity_branch_link.leaving_atom_id_1 +_pdbx_entity_branch_link.entity_branch_list_num_2 +_pdbx_entity_branch_link.comp_id_2 +_pdbx_entity_branch_link.atom_id_2 +_pdbx_entity_branch_link.leaving_atom_id_2 +_pdbx_entity_branch_link.value_order +_pdbx_entity_branch_link.details +1 1 1 NAG CA H1 2 BMC N H2 sing foo +2 1 2 BMC CA H1 3 FUC N H2 . . +# +""") + if __name__ == '__main__': unittest.main() diff --git a/test/test_main.py b/test/test_main.py index 644959f..e0430f5 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -1092,6 +1092,20 @@ def test_branch_descriptor(self): self.assertEqual(bd.program, 'baz') self.assertEqual(bd.program_version, '1.0') + def test_branch_link(self): + """Test the BranchLink class""" + lnk = ihm.BranchLink(num1=1, atom_id1='CA', leaving_atom_id1='H1', + num2=2, atom_id2='N', leaving_atom_id2='H2', + order='sing', details='foo') + self.assertEqual(lnk.num1, 1) + self.assertEqual(lnk.atom_id1, 'CA') + self.assertEqual(lnk.leaving_atom_id1, 'H1') + self.assertEqual(lnk.num2, 2) + self.assertEqual(lnk.atom_id2, 'N') + self.assertEqual(lnk.leaving_atom_id2, 'H2') + self.assertEqual(lnk.order, 'sing') + self.assertEqual(lnk.details, 'foo') + if __name__ == '__main__': unittest.main() diff --git a/test/test_reader.py b/test/test_reader.py index edd3d96..0369f65 100644 --- a/test/test_reader.py +++ b/test/test_reader.py @@ -4425,6 +4425,45 @@ def test_entity_branch_descriptor_handler(self): self.assertIsNone(bd2.program) self.assertIsNone(bd2.program_version) + def test_entity_branch_link_handler(self): + """Test EntityBranchLinkHandler""" + fh = StringIO(""" +loop_ +_pdbx_entity_branch_link.link_id +_pdbx_entity_branch_link.entity_id +_pdbx_entity_branch_link.entity_branch_list_num_1 +_pdbx_entity_branch_link.comp_id_1 +_pdbx_entity_branch_link.atom_id_1 +_pdbx_entity_branch_link.leaving_atom_id_1 +_pdbx_entity_branch_link.entity_branch_list_num_2 +_pdbx_entity_branch_link.comp_id_2 +_pdbx_entity_branch_link.atom_id_2 +_pdbx_entity_branch_link.leaving_atom_id_2 +_pdbx_entity_branch_link.value_order +_pdbx_entity_branch_link.details +1 1 1 NAG CA H1 2 BMC N H2 sing foo +2 1 2 BMC CA H1 3 FUC N H2 . . +""") + s, = ihm.reader.read(fh) + e1, = s.entities + lnk1, lnk2 = e1.branch_links + self.assertEqual(lnk1.num1, 1) + self.assertEqual(lnk1.atom_id1, 'CA') + self.assertEqual(lnk1.leaving_atom_id1, 'H1') + self.assertEqual(lnk1.num2, 2) + self.assertEqual(lnk1.atom_id2, 'N') + self.assertEqual(lnk1.leaving_atom_id2, 'H2') + self.assertEqual(lnk1.order, 'sing') + self.assertEqual(lnk1.details, 'foo') + self.assertEqual(lnk2.num1, 2) + self.assertEqual(lnk2.atom_id1, 'CA') + self.assertEqual(lnk2.leaving_atom_id1, 'H1') + self.assertEqual(lnk2.num2, 3) + self.assertEqual(lnk2.atom_id2, 'N') + self.assertEqual(lnk2.leaving_atom_id2, 'H2') + self.assertIsNone(lnk2.order) + self.assertIsNone(lnk2.details) + if __name__ == '__main__': unittest.main()