Skip to content

Commit 128da31

Browse files
authored
Refactor sympification (#261)
Centralize sympification of PEtab and SBML math expressions. Simplifies incorporating #260 Also fixes likely collisions of SBML model entities with sympy entities.
1 parent 2258112 commit 128da31

File tree

6 files changed

+72
-31
lines changed

6 files changed

+72
-31
lines changed

petab/calculate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
import numpy as np
88
import pandas as pd
99
import sympy
10-
from sympy.abc import _clash
1110

1211
import petab
1312

1413
from .C import *
14+
from .math import sympify_petab
1515

1616
__all__ = [
1717
"calculate_residuals",
@@ -157,7 +157,7 @@ def get_symbolic_noise_formulas(observable_df) -> Dict[str, sympy.Expr]:
157157
if NOISE_FORMULA not in observable_df.columns:
158158
noise_formula = None
159159
else:
160-
noise_formula = sympy.sympify(row.noiseFormula, locals=_clash)
160+
noise_formula = sympify_petab(row.noiseFormula)
161161
noise_formulas[observable_id] = noise_formula
162162
return noise_formulas
163163

petab/lint.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
import numpy as np
1111
import pandas as pd
1212
import sympy as sp
13-
from sympy.abc import _clash
1413

1514
import petab
1615

1716
from . import core, measurements, parameters
1817
from .C import * # noqa: F403
18+
from .math import sympify_petab
1919
from .models import Model
2020

2121
logger = logging.getLogger(__name__)
@@ -317,7 +317,7 @@ def check_observable_df(observable_df: pd.DataFrame) -> None:
317317
for row in observable_df.itertuples():
318318
obs = getattr(row, OBSERVABLE_FORMULA)
319319
try:
320-
sp.sympify(obs, locals=_clash)
320+
sympify_petab(obs)
321321
except sp.SympifyError as e:
322322
raise AssertionError(
323323
f"Cannot parse expression '{obs}' "
@@ -326,7 +326,7 @@ def check_observable_df(observable_df: pd.DataFrame) -> None:
326326

327327
noise = getattr(row, NOISE_FORMULA)
328328
try:
329-
sympified_noise = sp.sympify(noise, locals=_clash)
329+
sympified_noise = sympify_petab(noise)
330330
if sympified_noise is None or (
331331
sympified_noise.is_Number and not sympified_noise.is_finite
332332
):

petab/math/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Functions for parsing and evaluating mathematical expressions."""
2+
from .sympify import sympify_petab # noqa: F401

petab/math/sympify.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""PEtab math to sympy conversion."""
2+
3+
import sympy as sp
4+
from sympy.abc import _clash
5+
6+
7+
def sympify_petab(expr: str) -> sp.Expr:
8+
"""
9+
Convert a PEtab math expression to a sympy expression.
10+
11+
Parameters
12+
----------
13+
expr:
14+
The PEtab math expression.
15+
16+
Returns
17+
-------
18+
The sympy expression corresponding to ``expr``.
19+
"""
20+
return sp.sympify(expr, locals=_clash)

petab/models/sbml_model.py

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import libsbml
88
import sympy as sp
9+
from sympy.abc import _clash
910

1011
from ..sbml import (
1112
get_sbml_model,
@@ -104,34 +105,17 @@ def get_free_parameter_ids_with_values(
104105
ar.getVariable() for ar in self.sbml_model.getListOfRules()
105106
}
106107

107-
parser_settings = libsbml.L3ParserSettings(
108-
self.sbml_model,
109-
libsbml.L3P_PARSE_LOG_AS_LOG10,
110-
libsbml.L3P_EXPAND_UNARY_MINUS,
111-
libsbml.L3P_NO_UNITS,
112-
libsbml.L3P_AVOGADRO_IS_CSYMBOL,
113-
libsbml.L3P_COMPARE_BUILTINS_CASE_INSENSITIVE,
114-
None,
115-
libsbml.L3P_MODULO_IS_PIECEWISE,
116-
)
117-
118108
def get_initial(p):
119109
# return the initial assignment value if there is one, and it is a
120110
# number; `None`, if there is a non-numeric initial assignment;
121111
# otherwise, the parameter value
122112
if ia := self.sbml_model.getInitialAssignmentBySymbol(p.getId()):
123-
formula_str = libsbml.formulaToL3StringWithSettings(
124-
ia.getMath(), parser_settings
113+
sym_expr = sympify_sbml(ia.getMath())
114+
return (
115+
float(sym_expr.evalf())
116+
if sym_expr.evalf().is_Number
117+
else None
125118
)
126-
try:
127-
return float(formula_str)
128-
except ValueError:
129-
sym_expr = sp.sympify(formula_str)
130-
return (
131-
float(sym_expr.evalf())
132-
if sym_expr.evalf().is_Number
133-
else None
134-
)
135119
return p.getValue()
136120

137121
return (
@@ -200,3 +184,39 @@ def is_state_variable(self, id_: str) -> bool:
200184
or self.sbml_model.getCompartment(id_) is not None
201185
or self.sbml_model.getRuleByVariable(id_) is not None
202186
)
187+
188+
189+
def sympify_sbml(sbml_obj: libsbml.ASTNode | libsbml.SBase) -> sp.Expr:
190+
"""Convert SBML math expression to sympy expression.
191+
192+
Parameters
193+
----------
194+
sbml_obj:
195+
SBML math element or an SBML object with a math element.
196+
197+
Returns
198+
-------
199+
The sympy expression corresponding to ``sbml_obj``.
200+
"""
201+
ast_node = (
202+
sbml_obj
203+
if isinstance(sbml_obj, libsbml.ASTNode)
204+
else sbml_obj.getMath()
205+
)
206+
207+
parser_settings = libsbml.L3ParserSettings(
208+
ast_node.getParentSBMLObject().getModel(),
209+
libsbml.L3P_PARSE_LOG_AS_LOG10,
210+
libsbml.L3P_EXPAND_UNARY_MINUS,
211+
libsbml.L3P_NO_UNITS,
212+
libsbml.L3P_AVOGADRO_IS_CSYMBOL,
213+
libsbml.L3P_COMPARE_BUILTINS_CASE_INSENSITIVE,
214+
None,
215+
libsbml.L3P_MODULO_IS_PIECEWISE,
216+
)
217+
218+
formula_str = libsbml.formulaToL3StringWithSettings(
219+
ast_node, parser_settings
220+
)
221+
222+
return sp.sympify(formula_str, locals=_clash)

petab/observables.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
from typing import List, Literal, Union
77

88
import pandas as pd
9-
import sympy as sp
10-
from sympy.abc import _clash
119

1210
from . import core, lint
1311
from .C import * # noqa: F403
12+
from .math import sympify_petab
1413
from .models import Model
1514

1615
__all__ = [
@@ -102,15 +101,15 @@ def get_output_parameters(
102101

103102
for formula in formulas:
104103
free_syms = sorted(
105-
sp.sympify(formula, locals=_clash).free_symbols,
104+
sympify_petab(formula).free_symbols,
106105
key=lambda symbol: symbol.name,
107106
)
108107
for free_sym in free_syms:
109108
sym = str(free_sym)
110109
if model.symbol_allowed_in_observable_formula(sym):
111110
continue
112111

113-
# does it mapping to a model entity?
112+
# does it map to a model entity?
114113
if (
115114
mapping_df is not None
116115
and sym in mapping_df.index

0 commit comments

Comments
 (0)