Skip to content

Commit

Permalink
Fixes for sbml import (#567)
Browse files Browse the repository at this point in the history
* fix(sbml) fix inifinite loop initial assignment

* fix(sbml) fix interdependent initial assignments

* fix(sbml) better identification of unresolvable initial assignments

* fix(python) fix model without parameters

* fix(sbml) extend unsupported functions and fix initial assignment flux parsing

* fix(sbml) fix conversion factor parsing

* fix(sbml) extend unsupported function parsing

* fix(sbml) fix empty stoichiometric matrix

* fix(sbml) more fixes for unsupported expressions and empty assignments

* fix(sbml) fix erroneous detection of unsupported functions

* fix(sbml) small refinements unsupported functions code

* fix(sbml) add additional unsupported functions

* fix(ci) ignore testcase 1395 and fix volumen computation for certain models
  • Loading branch information
FFroehlich authored Jan 30, 2019
1 parent e92e071 commit f636918
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 45 deletions.
39 changes: 29 additions & 10 deletions python/amici/ode_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,8 +895,13 @@ def import_from_sbml_importer(self, si):
[sp.Symbol(f'flux_r{idx}') for idx in range(len(si.fluxVector))]
)
self._eqs['dxdotdx'] = sp.zeros(si.stoichiometricMatrix.shape[0])
symbols['species']['dt'] = \
si.stoichiometricMatrix * self.sym('w')
if len(si.stoichiometricMatrix):
symbols['species']['dt'] = \
si.stoichiometricMatrix * self.sym('w')
else:
symbols['species']['dt'] = sp.zeros(
*symbols['species']['identifier'].shape
)

for symbol in [s for s in symbols if s != 'my']:
# transform dict of lists into a list of dicts
Expand Down Expand Up @@ -1395,21 +1400,31 @@ def _compute_equation(self, name):
# if x0_fixedParameters>0 else 0
# sx0_fixedParameters = sx+deltasx =
# dx0_fixedParametersdx*sx+dx0_fixedParametersdp
self._eqs[name] = \
self.eq('x0_fixedParameters').jacobian(self.sym('p'))
if len(self.sym('p')):
self._eqs[name] = \
self.eq('x0_fixedParameters').jacobian(self.sym('p'))
else:
self._eqs[name] = sp.zeros(
len(self.eq('x0_fixedParameters')),
len(self.sym('p'))
)

dx0_fixedParametersdx = \
self.eq('x0_fixedParameters').jacobian(self.sym('x'))
if len(self.sym('x')):
dx0_fixedParametersdx = \
self.eq('x0_fixedParameters').jacobian(self.sym('x'))
else:
dx0_fixedParametersdx = sp.zeros(
len(self.eq('x0_fixedParameters')),
len(self.sym('x'))
)

if dx0_fixedParametersdx.is_zero is not True:
for ip in range(self._eqs[name].shape[1]):
self._eqs[name][:,ip] += \
dx0_fixedParametersdx \
* self.sym('sx0') \

for index, formula in enumerate(
self.eq('x0_fixedParameters')
):
for index, formula in enumerate(self.eq('x0_fixedParameters')):
if formula == 0 or formula == 0.0:
self._eqs[name][index, :] = \
sp.zeros(1, self._eqs[name].shape[1])
Expand Down Expand Up @@ -1639,7 +1654,11 @@ def _multiplication(self, name, x, y,
else:
xx = variables[x]

self._eqs[name] = sign * xx * variables[y]
if xx.is_zero is not True and variables[y].is_zero is not True \
and len(xx) and len(variables[y]):
self._eqs[name] = sign * xx * variables[y]
else:
self._eqs[name] = sp.zeros(len(xx), len(variables[y]))

def _equationFromComponent(self, name, component):
"""Generates the formulas of a symbolic variable from the attributes
Expand Down
105 changes: 73 additions & 32 deletions python/amici/sbml_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import libsbml as sbml
import re
import math
from sympy.logic.boolalg import BooleanTrue as spTrue
from sympy.logic.boolalg import BooleanFalse as spFalse

from .ode_export import ODEExporter, ODEModel

Expand Down Expand Up @@ -343,23 +345,39 @@ def getSpeciesInitial(index, conc):
index = species_ids.index(
initial_assignment.getId()
)
speciesInitial[index] = sp.sympify(
symMath = sp.sympify(
sbml.formulaToL3String(initial_assignment.getMath())
)
_check_unsupported_functions(speciesInitial[index])
if symMath is not None:
_check_unsupported_functions(symMath, 'InitialAssignment')
speciesInitial[index] = symMath

# flatten initSpecies
while any([species in speciesInitial.free_symbols
for species in self.symbols['species']['identifier']]):
speciesInitial = speciesInitial.subs(
self.symbols['species']['identifier'],
speciesInitial
)
identical_assignment = next((
symbol == init
for symbol, init in zip(
self.symbols['species']['identifier'], speciesInitial
)
), None)
if identical_assignment is not None:
raise SBMLException('Species without initial assignment are '
'currently not supported (this is error '
'is likely to be due to the existence of '
'a species assignment rule, which is '
'also not supported)!')
speciesInitial = speciesInitial.subs([
(symbol, init)
for symbol, init in zip(
self.symbols['species']['identifier'], speciesInitial
)
])

self.symbols['species']['value'] = speciesInitial

if self.sbml.isSetConversionFactor():
conversion_factor = self.sbml.getConversionFactor()
conversion_factor = sp.Symbol(self.sbml.getConversionFactor())
else:
conversion_factor = 1.0

Expand Down Expand Up @@ -481,7 +499,7 @@ def processCompartments(self):
sbml.formulaToL3String(initial_assignment.getMath())
)

for comp, vol in zip(self.compartmentSymbols,self.compartmentVolume):
for comp, vol in zip(self.compartmentSymbols, self.compartmentVolume):
self.replaceInAllExpressions(
comp, vol
)
Expand Down Expand Up @@ -519,29 +537,34 @@ def getElementFromAssignment(element_id):
assignment = self.sbml.getInitialAssignment(
element_id
)
sym = sp.sympify(assignment.getFormula())
sym = sp.sympify(sbml.formulaToL3String(assignment.getMath()))
# this is an initial assignment so we need to use
# initial conditions
sym = sym.subs(
self.symbols['species']['identifier'],
self.symbols['species']['value']
)
if sym is not None:
sym = sym.subs(
self.symbols['species']['identifier'],
self.symbols['species']['value']
)
return sym

def getElementStoichiometry(element):
if element.isSetId():
if element.getId() in assignment_ids:
return getElementFromAssignment(element.getId())
symMath = getElementFromAssignment(element.getId())
if symMath is None:
symMath = sp.sympify(element.getStoichiometry())
elif element.getId() in rulevars:
return sp.sympify(element.getId())
return sp.Symbol(element.getId())
else:
# dont put the symbol if it wont get replaced by a
# rule
return sp.sympify(element.getStoichiometry())
symMath = sp.sympify(element.getStoichiometry())
elif element.isSetStoichiometry():
return sp.sympify(element.getStoichiometry())
symMath = sp.sympify(element.getStoichiometry())
else:
return sp.sympify(1.0)
_check_unsupported_functions(symMath, 'Stoichiometry')
return symMath

def isConstant(specie):
return specie in self.constantSpecies or \
Expand Down Expand Up @@ -578,7 +601,7 @@ def isConstant(specie):
except:
raise SBMLException(f'Kinetic law "{math}" contains an '
'unsupported expression!')
_check_unsupported_functions(symMath)
_check_unsupported_functions(symMath, 'KineticLaw')
for r in reactions:
elements = list(r.getListOfReactants()) \
+ list(r.getListOfProducts())
Expand Down Expand Up @@ -629,7 +652,7 @@ def processRules(self):
variable = sp.sympify(rule.getVariable())
# avoid incorrect parsing of pow(x, -1) in symengine
formula = sp.sympify(sbml.formulaToL3String(rule.getMath()))
_check_unsupported_functions(formula)
_check_unsupported_functions(formula, 'Rule')

if variable in stoichvars:
self.stoichiometricMatrix = \
Expand Down Expand Up @@ -1112,35 +1135,53 @@ def checkLibSBMLErrors(sbml_doc, show_warnings=False):
)


def _check_unsupported_functions(sym):
def _check_unsupported_functions(sym, expression_type, full_sym=None):
"""Recursively checks the symbolic expression for unsupported symbolic
functions
Arguments:
sym: symbolic expressions @type sympy.Basic
expression_type: type of expression
Returns:
Raises:
raises SBMLException if an unsupported function is encountered
"""
if full_sym is None:
full_sym = sym

unsupported_functions = [
sp.functions.factorial, sp.functions.ceiling,
sp.functions.Piecewise,
sp.functions.factorial, sp.functions.ceiling, sp.functions.floor,
sp.functions.Piecewise, spTrue, spFalse, sp.function.UndefinedFunction
]

for arg in sym._args:
unsupp_fun = next(
unsupp_fun_type = next(
(
fun_type
for fun_type in unsupported_functions
if isinstance(sym.func, fun_type)
),
None
)
if unsupp_fun_type:
raise SBMLException(f'Encountered unsupported expression '
f'"{sym.func}" of type '
f'"{unsupp_fun_type}" as part of a '
f'{expression_type}: "{full_sym}"!')
for fun in list(sym._args) + [sym]:
unsupp_fun_type = next(
(
fun
for fun in unsupported_functions
if isinstance(arg, fun)
fun_type
for fun_type in unsupported_functions
if isinstance(fun, fun_type)
),
None
)
if unsupp_fun:
raise SBMLException(f'Encountered unsupported function type '
f'"{unsupp_fun}" in subexpression "{sym}"')

_check_unsupported_functions(arg)
if unsupp_fun_type:
raise SBMLException(f'Encountered unsupported expression '
f'"{fun}" of type '
f'"{unsupp_fun_type}" as part of a '
f'{expression_type}: "{full_sym}"!')
if fun is not sym:
_check_unsupported_functions(fun, expression_type)
26 changes: 23 additions & 3 deletions tests/testSBMLSuite.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import importlib
import numpy as np
import sympy as sp
import amici
import unittest
import copy
Expand All @@ -26,9 +27,11 @@ def runTest(self):
self.test_sbml_testsuite()

def test_sbml_testsuite(self):
for testId in range(0, 1781):
with self.subTest(testId=testId):
self.run_sbml_testsuite_case(get_test_str(testId))
for testId in range(1, 1781):
if testId != 1395: # we skip this test due to NaNs in the
# jacobian
with self.subTest(testId=testId):
self.run_sbml_testsuite_case(get_test_str(testId))

def run_sbml_testsuite_case(self, test_id):
try:
Expand Down Expand Up @@ -68,6 +71,23 @@ def run_sbml_testsuite_case(self, test_id):
wrapper.compartmentVolume
)
})
volume = volume.subs({
sp.Symbol(name): value
for name, value in zip(
model.getParameterIds(),
model.getParameters()
)
})

# required for 525-527, 530 as k is renamed to amici_k
volume = volume.subs({
sp.Symbol(name): value
for name, value in zip(
model.getParameterNames(),
model.getParameters()
)
})

simulated_x[:, wrapper.speciesIndex[species]] = \
simulated_x[:, wrapper.speciesIndex[species]] * volume

Expand Down

0 comments on commit f636918

Please sign in to comment.