From e9f7c2434ebb35628c4638b092dabf332913c841 Mon Sep 17 00:00:00 2001 From: Jonathan Karr Date: Thu, 29 Aug 2019 11:31:09 -0400 Subject: [PATCH] supporting xlink order, stereochemistry --- bcforms/core.py | 144 ++++++++++++++++++++++++++++++++++++++++- bcforms/grammar.lark | 7 +- bcforms/web/index.html | 7 +- tests/test_core.py | 32 +++++++++ 4 files changed, 184 insertions(+), 6 deletions(-) diff --git a/bcforms/core.py b/bcforms/core.py index 0595c2d..92b61fc 100644 --- a/bcforms/core.py +++ b/bcforms/core.py @@ -7,6 +7,7 @@ :License: MIT """ +from bpforms import BondOrder, BondStereo from bpforms.util import gen_genomic_viz from ruamel import yaml from wc_utils.util.chem import EmpiricalFormula, OpenBabelUtils, draw_molecule @@ -706,6 +707,26 @@ def get_r_displaced_atoms(self): """ pass + @abc.abstractmethod + def get_order(self): + """ Get the order + + Returns: + :obj:`BondOrder`: order + + """ + pass + + @abc.abstractmethod + def get_stereo(self): + """ Get the stereochemistry + + Returns: + :obj:`BondStereo`: stereochemistry + + """ + pass + @abc.abstractmethod def __str__(self): """Generate a string representation @@ -742,6 +763,9 @@ def is_equal(self, other): if not self_atom.is_equal(other_atom): return False + if self.get_order() != other.get_order() or self.get_stereo() != other.get_stereo(): + return False + return True @@ -753,10 +777,13 @@ class InlineCrosslink(Crosslink): r_bond_atoms (:obj:`list` of :obj:`Atom`): atoms from the right subunit that bond with the left subunit l_displaced_atoms (:obj:`list` of :obj:`Atom`): atoms from the left subunit displaced by the crosslink r_displaced_atoms (:obj:`list` of :obj:`Atom`): atoms from the right subunit displaced by the crosslink + order (:obj:`BondOrder`): order + stereo (:obj:`BondStereo`): stereochemistry comments (:obj:`str`): comments """ def __init__(self, l_bond_atoms=None, r_bond_atoms=None, l_displaced_atoms=None, r_displaced_atoms=None, + order=BondOrder.single, stereo=None, comments=None): """ @@ -765,6 +792,8 @@ def __init__(self, l_bond_atoms=None, r_bond_atoms=None, l_displaced_atoms=None, r_bond_atoms (:obj:`list`): atoms from the right subunit that bond with the left subunit l_displaced_atoms (:obj:`list`): atoms from the left subunit displaced by the crosslink r_displaced_atoms (:obj:`list`): atoms from the right subunit displaced by the crosslink + order (:obj:`BondOrder`, optional): order + stereo (:obj:`BondStereo`, optional): stereochemistry comments (:obj:`str`): comments """ if l_bond_atoms is None: @@ -787,6 +816,9 @@ def __init__(self, l_bond_atoms=None, r_bond_atoms=None, l_displaced_atoms=None, else: self.r_displaced_atoms = r_bond_atoms + self.order = order + self.stereo = stereo + self.comments = comments @property @@ -889,6 +921,52 @@ def r_displaced_atoms(self, value): raise ValueError('`value` must be an instance of `list`') self._r_displaced_atoms = value + @property + def order(self): + """ Get the order + + Returns: + :obj:`BondOrder`: order + """ + return self._order + + @order.setter + def order(self, value): + """ Set the order + + Args: + value (:obj:`BondOrder`): order + + Raises: + :obj:`ValueError`: if `order` is not an instance of `BondOrder` + """ + if not isinstance(value, BondOrder): + raise ValueError('`order` must be an instance of `BondOrder`') + self._order = value + + @property + def stereo(self): + """ Get the stereochemistry + + Returns: + :obj:`BondStereo`: stereochemistry + """ + return self._stereo + + @stereo.setter + def stereo(self, value): + """ Set the stereo + + Args: + value (:obj:`BondStereo`): stereochemistry + + Raises: + :obj:`ValueError`: if `stereo` is not an instance of `BondStereo` + """ + if value is not None and not isinstance(value, BondStereo): + raise ValueError('`stereo` must be an instance of `BondStereo` or `None`') + self._stereo = value + @property def comments(self): """ Get comments @@ -925,6 +1003,11 @@ def __str__(self): for atom in getattr(self, atom_type): s += ' {}: {} |'.format(atom_type[:-1].replace('_', '-'), str(atom)) + if self.order != BondOrder.single: + s += ' order: "{}" |'.format(self.order.name) + if self.stereo is not None: + s += ' stereo: "{}" |'.format(self.stereo.name) + if self.comments: s += ' comments: "{}" |'.format(self.comments.replace('"', '\\"')) @@ -967,6 +1050,22 @@ def get_r_displaced_atoms(self): """ return self.r_displaced_atoms + def get_order(self): + """ Get the order + + Returns: + :obj:`BondOrder`: order + """ + return self.order + + def get_stereo(self): + """ Get the stereochemistry + + Returns: + :obj:`BondStereo`: stereochemistry + """ + return self.stereo + _xlink_filename = pkg_resources.resource_filename('bpforms', 'xlink/xlink.yml') @@ -1301,6 +1400,26 @@ def get_r_displaced_atoms(self): atoms.append(atom) return atoms + def get_order(self): + """ Get the order + + Returns: + :obj:`BondOrder`: order + """ + return BondOrder[self.xlink_details[1].get('order' , 'single')] + + def get_stereo(self): + """ Get the stereochemistry + + Returns: + :obj:`BondStereo`: stereochemistry + """ + val = self.xlink_details[1].get('stereo', None) + if val is None: + return None + else: + return BondStereo[val] + def __str__(self): """Generate a string representation @@ -1564,7 +1683,7 @@ def inline_crosslink(self, *args): for arg in args: if isinstance(arg, lark.tree.Tree): attr, val = arg.children[0] - if attr == 'comments': + if attr in ['order', 'stereo', 'comments']: setattr(bond, attr, val) else: attr_val_list = getattr(bond, attr + "s") @@ -1607,6 +1726,14 @@ def inline_crosslink_atom(self, *args): def inline_crosslink_atom_type(self, *args): return ('inline_crosslink_atom_type', args[0].value + '_' + args[1].value + '_atom') + @lark.v_args(inline=True) + def inline_crosslink_order(self, *args): + return ('order', BondOrder[args[-2].value]) + + @lark.v_args(inline=True) + def inline_crosslink_stereo(self, *args): + return ('stereo', BondStereo[args[-2].value]) + @lark.v_args(inline=True) def inline_crosslink_comments(self, *args): return ('comments', args[1].value[1:-1]) @@ -2022,7 +2149,7 @@ def get_structure(self): # print(OpenBabelUtils.export(mol, format='smiles', options=[])) # make the crosslink bonds - for atoms in crosslinks_atoms: + for crosslink, atoms in zip(self.crosslinks, crosslinks_atoms): for atom, i_subunit, subunit_idx, i_monomer, i_position, atom_charge in itertools.chain(atoms['l_displaced_atoms'], atoms['r_displaced_atoms']): if atom: @@ -2035,7 +2162,18 @@ def get_structure(self): bond = openbabel.OBBond() bond.SetBegin(l_atom) bond.SetEnd(r_atom) - bond.SetBondOrder(1) + bond.SetBondOrder(crosslink.get_order().value) + stereo = crosslink.get_stereo() + if stereo is None: + pass + elif stereo == BondStereo.wedge: + bond.SetWedge() + elif stereo == BondStereo.hash: + bond.SetHash() + elif stereo == BondStereo.up: + bond.SetUp() + elif stereo == BondStereo.down: + bond.SetDown() assert mol.AddBond(bond) if l_atom_charge: diff --git a/bcforms/grammar.lark b/bcforms/grammar.lark index 4156496..2faf9f5 100644 --- a/bcforms/grammar.lark +++ b/bcforms/grammar.lark @@ -13,9 +13,11 @@ onto_crosslink_monomer: onto_crosslink_monomer_type FIELD_SEP subunit subunit_id onto_crosslink_monomer_type: /(l|r)/ inline_crosslink: inline_crosslink_attr (ATTR_SEP inline_crosslink_attr)* -inline_crosslink_attr: inline_crosslink_atom | inline_crosslink_comments +inline_crosslink_attr: inline_crosslink_atom | inline_crosslink_order | inline_crosslink_stereo | inline_crosslink_comments inline_crosslink_atom: inline_crosslink_atom_type FIELD_SEP subunit subunit_idx? "-" monomer_position atom_element atom_position atom_component_type? atom_charge? inline_crosslink_atom_type: /(l|r)/ "-" /(bond|displaced)/ "-atom" +inline_crosslink_order: "order" field_sep QUOTE_DELIMITER /(single|double|triple|aromatic)/ QUOTE_DELIMITER +inline_crosslink_stereo: "stereo" field_sep QUOTE_DELIMITER /(wedge|hash|up|down)/ QUOTE_DELIMITER inline_crosslink_comments: "comments" FIELD_SEP ESCAPED_STRING monomer_position: /[0-9]+/ @@ -25,11 +27,14 @@ atom_position: /[0-9]+/ atom_charge: /[\+\-][0-9]+/ atom_component_type: /[mb]/ +?field_sep: WS? ":" WS? + NAME: /(?!(^|\b)(\d+(\.\d*)?(\b|$))|(\.\d+$)|(0[x][0-9a-f]+(\b|$))|([0-9]+e[0-9]+(\b|$)))[a-z0-9_]+/ PLUS_SEP: WS* "+" WS* STAR_SEP: WS* "*" WS* ATTR_SEP: WS* "|" WS* FIELD_SEP: WS* ":" WS* +QUOTE_DELIMITER: "\"" WS: /[ \t\f\r\n]+/ INT: /[0-9]+/ diff --git a/bcforms/web/index.html b/bcforms/web/index.html index 024f13f..170bc94 100644 --- a/bcforms/web/index.html +++ b/bcforms/web/index.html @@ -195,11 +195,14 @@

Examples

-

Inline definition of crosslinks

-

Each crosslink can be described using four types of attributes:

+

User-defined crosslinks

+

Each crosslink can be described using the following attributes:

Each crosslink can have one or more left and right bond atoms, and zero or more left and right displaced atoms. Each crosslink must have the same number of left and right bond atoms.

diff --git a/tests/test_core.py b/tests/test_core.py index 965c214..028f412 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -311,6 +311,19 @@ def test_set_r_displaced_atoms(self): with self.assertRaises(ValueError): crosslink.r_displaced_atoms = None + def test_set_order(self): + crosslink = core.InlineCrosslink() + crosslink.order = bpforms.BondOrder.double + with self.assertRaises(ValueError): + crosslink.order = None + + def test_set_stereo(self): + crosslink = core.InlineCrosslink() + crosslink.stereo = None + crosslink.stereo = bpforms.BondStereo.wedge + with self.assertRaises(ValueError): + crosslink.stereo = 1 + def test_str(self): crosslink = core.InlineCrosslink() atom_1 = core.Atom(subunit='abc', subunit_idx=1, element='H', position=1, monomer=10, charge=0) @@ -320,6 +333,12 @@ def test_str(self): self.assertEqual(str(crosslink), 'x-link: [ l-bond-atom: abc(1)-10H1 | r-bond-atom: def(1)-10H1 ]') + crosslink = core.InlineCrosslink(order=bpforms.BondOrder.double) + self.assertEqual(str(crosslink), 'x-link: [ order: "double" ]') + + crosslink = core.InlineCrosslink(stereo=bpforms.BondStereo.wedge) + self.assertEqual(str(crosslink), 'x-link: [ stereo: "wedge" ]') + def test_is_equal(self): atom_1 = core.Atom(subunit='abc', subunit_idx=1, element='H', position=1, monomer=10, charge=0) atom_2 = core.Atom(subunit='abc', subunit_idx=1, element='H', position=1, monomer=10, charge=0) @@ -644,6 +663,19 @@ def test_from_str(self): ' comments: "a comment"]') self.assertEqual(list(bc_form_9.crosslinks)[0].comments, 'a comment') + bc_form_10 = core.BcForm().from_str('unit_1 + unit_2' + '| x-link: [ l-bond-atom: unit_1-1C2 |' + ' r-bond-atom: unit_2-2N1-1 |' + ' l-displaced-atom: unit_1-1O1 |' + ' l-displaced-atom: unit_1-1H1 |' + ' r-displaced-atom: unit_2-2H1+1 |' + ' r-displaced-atom: unit_2-2H1 | ' + ' order: "triple" |' + ' stereo: "wedge"' + ']') + self.assertEqual(list(bc_form_10.crosslinks)[0].order, bpforms.BondOrder.triple) + self.assertEqual(list(bc_form_10.crosslinks)[0].stereo, bpforms.BondStereo.wedge) + def test_from_set(self):