Skip to content

Commit

Permalink
Add basic support for links in branched entities
Browse files Browse the repository at this point in the history
Add a BranchLink class which tracks bonds between
saccharides, and read/write the _pdbx_entity_branch_link
mmCIF table.
  • Loading branch information
benmwebb committed Sep 25, 2023
1 parent 9c0281b commit ec985b7
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/main.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,6 @@ The :mod:`ihm` Python module

.. autoclass:: BranchDescriptor
:members:

.. autoclass:: BranchLink
:members:
27 changes: 27 additions & 0 deletions ihm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<ihm.Entity(%s)>" % self.description

Expand Down Expand Up @@ -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
26 changes: 25 additions & 1 deletion ihm/dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion ihm/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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]
Expand Down
35 changes: 35 additions & 0 deletions test/test_dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
14 changes: 14 additions & 0 deletions test/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
39 changes: 39 additions & 0 deletions test/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit ec985b7

Please sign in to comment.