Skip to content

Commit

Permalink
Fix #123, better documentation of equation
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiaskoenig committed Jun 3, 2019
1 parent 4fd84f4 commit cd5ec9d
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 46 deletions.
132 changes: 92 additions & 40 deletions sbmlutils/equation.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
"""
Parse equation strings into a standard format.
Simplifies the creation of SBML models from given strings.
Module for parsing equation strings.
Various string formats are allowed which are subsequently brought into
an internal standard format.
Equations are of the form
'1.0 S1 + 2 S2 => 2.0 P1 + 2 P2 [M1, M2]'
The equation consists of
- substrates concatenated via '+' on the left side
(with optional stoichiometric coefficients)
- separation characters separating the left and right equation sides:
'<=>' or '<->' for reversible reactions,
'=>' or '->' for irreversible reactions (irreversible reactions
are written from left to right)
- products concatenated via '+' on the right side
(with optional stoichiometric coefficients)
- optional list of modifiers within brackets [] separated by ','
Examples of valid equations are:
'1.0 S1 + 2 S2 => 2.0 P1 + 2 P2 [M1, M2]',
'c__gal1p => c__gal + c__phos',
'e__h2oM <-> c__h2oM',
'3 atp + 2.0 phos + ki <-> 16.98 tet',
'c__gal1p => c__gal + c__phos [c__udp, c__utp]',
'A_ext => A []',
'=> cit',
'acoa =>',
"""

import re
Expand Down Expand Up @@ -30,19 +54,22 @@ def __init__(self, equation):
self.modifiers = []
self.reversible = None

self._parseEquation()
self._parse_equation()

def _parseEquation(self):
def _parse_equation(self):
eq_string = self.raw[:]

# get modifiers and remove from equation string
mod_list = re.findall(MOD_PATTERN, eq_string)
if len(mod_list) == 1:
self._parseModifiers(mod_list[0])
self._parse_modifiers(mod_list[0])
tokens = eq_string.split('[')
eq_string = tokens[0].strip()
elif len(mod_list) > 1:
raise self.EquationException('Invalid equation: {}'.format(self.raw))
raise self.EquationException(
f"Invalid equation: {self.raw}. "
f"Modifier list could not be parsed."
+ Equation.help())

# now parse the equation without modifiers
items = re.split(REV_PATTERN, eq_string)
Expand All @@ -53,33 +80,43 @@ def _parseEquation(self):
items = re.split(IRREV_PATTERN, eq_string)
self.reversible = False
else:
raise self.EquationException('Invalid equation: {}'.format(self.raw))
raise self.EquationException(
f"Invalid equation: {self.raw}. "
f"Equation could not be split into left "
f"and right side. " + Equation.help())

# remove whitespaces
items = [o.strip() for o in items]
if len(items) < 2:
raise self.EquationException(
f"Invalid equation: {self.raw}. "
f"Equation could not be split into left "
f"and right side. Use '<=>' or '=>' as separator. "
+ Equation.help())
left, right = items[0], items[1]
if len(left) > 0:
self.reactants = self._parseHalfEquation(left)
self.reactants = self._parse_half_equation(left)
if len(right) > 0:
self.products = self._parseHalfEquation(right)
self.products = self._parse_half_equation(right)

def _parseModifiers(self, s):
def _parse_modifiers(self, s):
s = s.replace('[', '')
s = s.replace(']', '')
s = s.strip()
tokens = re.split('[,;]', s)
modifiers = [t.strip() for t in tokens]
self.modifiers = [t for t in modifiers if len(t) > 0]

def _parseHalfEquation(self, string):
def _parse_half_equation(self, string):
""" Only '+ supported in equation !, do not use negative
stoichiometries.
"""
items = re.split('[+-]', string)
items = [item.strip() for item in items]
return [self._parseReactant(item) for item in items]
return [self._parse_reactant(item) for item in items]

def _parseReactant(self, item):
@staticmethod
def _parse_reactant(item):
""" Returns tuple of stoichiometry, sid. """
tokens = item.split()
if len(tokens) == 1:
Expand All @@ -90,21 +127,8 @@ def _parseReactant(self, item):
sid = ' '.join(tokens[1:])
return Part(stoichiometry, sid)

def toString(self, modifiers=False):
left = self._toStringSide(self.reactants)
right = self._toStringSide(self.products)
if self.reversible:
sep = REV_SEP
elif not self.reversible:
sep = IRREV_SEP

if modifiers:
mod = self.toStringModifiers()
return ' '.join([left, sep, right, mod])
else:
return ' '.join([left, sep, right])

def _toStringSide(self, items):
@staticmethod
def _to_string_side(items):
tokens = []
for item in items:
stoichiometry, sid = item[0], item[1]
Expand All @@ -114,13 +138,29 @@ def _toStringSide(self, items):
tokens.append(' '.join([str(stoichiometry), sid]))
return ' + '.join(tokens)

def toStringModifiers(self):
def _to_string_modifiers(self):
return '[{}]'.format(', '.join(self.modifiers))

def to_string(self, modifiers=False):
""" String representation of equation. """
left = self._to_string_side(self.reactants)
right = self._to_string_side(self.products)
if self.reversible:
sep = REV_SEP
else:
sep = IRREV_SEP

if modifiers:
mod = self._to_string_modifiers()
return ' '.join([left, sep, right, mod])
else:
return ' '.join([left, sep, right])

def info(self):
""" Overview of parsed equation. """
lines = [
'{:<10s} : {}'.format('raw', self.raw),
'{:<10s} : {}'.format('parsed', self.toString()),
'{:<10s} : {}'.format('parsed', self.to_string()),
'{:<10s} : {}'.format('reversible', self.reversible),
'{:<10s} : {}'.format('reactants', self.reactants),
'{:<10s} : {}'.format('products', self.products),
Expand All @@ -129,18 +169,30 @@ def info(self):
]
print('\n'.join(lines))

@staticmethod
def help():
""" Help information. """
return """
For information on the supported equation format use
from sbmlutils import equation
help(equation)
"""


##################################################################
# -----------------------------------------------------------------------------
if __name__ == '__main__':

tests = ['c__gal1p => c__gal + c__phos',
'e__h2oM <-> c__h2oM',
'3 atp + 2.0 phos + ki <-> 16.98 tet',
'c__gal1p => c__gal + c__phos [c__udp, c__utp]',
'A_ext => A []',
'=> cit',
'acoa =>',
]
tests = [
'1.0 S1 + 2 S2 => 2.0 P1 + 2 P2 [M1, M2]',
'c__gal1p => c__gal + c__phos',
'e__h2oM <-> c__h2oM',
'3 atp + 2.0 phos + ki <-> 16.98 tet',
'c__gal1p => c__gal + c__phos [c__udp, c__utp]',
'A_ext => A []',
'=> cit',
'acoa =>',
]

for test in tests:
print('-' * 40)
Expand Down
27 changes: 21 additions & 6 deletions sbmlutils/tests/test_equation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,26 @@
class TestEquation(unittest.TestCase):
""" Unit tests for modelcreator. """

def test_equation_examples(self):
equations = [
'1.0 S1 + 2 S2 => 2.0 P1 + 2 P2 [M1, M2]',
'c__gal1p => c__gal + c__phos',
'e__h2oM <-> c__h2oM',
'3 atp + 2.0 phos + ki <-> 16.98 tet',
'c__gal1p => c__gal + c__phos [c__udp, c__utp]',
'A_ext => A []',
'=> cit',
'acoa =>',
]
for eq_str in equations:
eq = Equation(eq_str)
assert eq

def test_equation_1(self):
""" Test Equation. """
eq_string = 'c__gal1p => c__gal + c__phos'
eq = Equation(eq_string)
self.assertEqual(eq.toString(), eq_string)
self.assertEqual(eq.to_string(), eq_string)

def test_equation_2(self):
""" Test Equation. """
Expand All @@ -23,7 +38,7 @@ def test_equation_2(self):
self.assertTrue(eq.reversible)

test_res = eq_string.replace('<->', REV_SEP)
self.assertEqual(eq.toString(), test_res)
self.assertEqual(eq.to_string(), test_res)

def test_equation_double_stoichiometry(self):
""" Test Equation. """
Expand All @@ -32,13 +47,13 @@ def test_equation_double_stoichiometry(self):
self.assertTrue(eq.reversible)

test_res = eq_string.replace('<->', REV_SEP)
self.assertEqual(eq.toString(), test_res)
self.assertEqual(eq.to_string(), test_res)

def test_equation_modifier(self):
""" Test Equation. """
eq_string = 'c__gal1p => c__gal + c__phos [c__udp, c__utp]'
eq = Equation(eq_string)
self.assertEqual(eq.toString(modifiers=True), eq_string)
self.assertEqual(eq.to_string(modifiers=True), eq_string)

def test_equation_empty_modifier(self):
""" Test Equation. """
Expand All @@ -51,14 +66,14 @@ def test_equation_no_reactants(self):
eq_string = ' => A'
eq = Equation(eq_string)
test_res = eq_string.replace('=>', IRREV_SEP)
self.assertEqual(eq.toString(), test_res)
self.assertEqual(eq.to_string(), test_res)

def test_equation_no_products(self):
""" Test Equation. """
eq_string = 'B => '
eq = Equation(eq_string)
test_res = eq_string.replace('=>', IRREV_SEP)
self.assertEqual(eq.toString(), test_res)
self.assertEqual(eq.to_string(), test_res)


if __name__ == "__main__":
Expand Down

0 comments on commit cd5ec9d

Please sign in to comment.