Skip to content

Commit 522e06e

Browse files
committed
Add petab-compatible sympy string-printer
Add a sympy Printer to stringify sympy expressions in a petab-compatible way. For example, we need to avoid `str(sympy.sympify("x^2"))` -> `'x**2'`. Closes #362.
1 parent 5b47448 commit 522e06e

File tree

4 files changed

+115
-11
lines changed

4 files changed

+115
-11
lines changed

petab/v1/math/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
"""Functions for parsing and evaluating mathematical expressions."""
22

3+
from .printer import PetabStrPrinter, petab_math_str # noqa: F401
34
from .sympify import sympify_petab # noqa: F401

petab/v1/math/printer.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""A PEtab-compatible sympy string-printer."""
2+
3+
from itertools import chain, islice
4+
5+
import sympy as sp
6+
from sympy.printing.str import StrPrinter
7+
8+
9+
class PetabStrPrinter(StrPrinter):
10+
"""A PEtab-compatible sympy string-printer."""
11+
12+
#: Mapping of sympy functions to PEtab functions
13+
func_map = {
14+
"asin": "arcsin",
15+
"acos": "arccos",
16+
"atan": "arctan",
17+
"acot": "arccot",
18+
"asec": "arcsec",
19+
"acsc": "arccsc",
20+
"asinh": "arcsinh",
21+
"acosh": "arccosh",
22+
"atanh": "arctanh",
23+
"acoth": "arccoth",
24+
"asech": "arcsech",
25+
"acsch": "arccsch",
26+
"Abs": "abs",
27+
}
28+
29+
def _print_BooleanTrue(self, expr):
30+
return "true"
31+
32+
def _print_BooleanFalse(self, expr):
33+
return "false"
34+
35+
def _print_Pow(self, expr: sp.Pow):
36+
"""Custom printing for the power operator"""
37+
base, exp = expr.as_base_exp()
38+
return f"{self._print(base)} ^ {self._print(exp)}"
39+
40+
def _print_Infinity(self, expr):
41+
"""Custom printing for infinity"""
42+
return "inf"
43+
44+
def _print_NegativeInfinity(self, expr):
45+
"""Custom printing for negative infinity"""
46+
return "-inf"
47+
48+
def _print_Function(self, expr):
49+
"""Custom printing for specific functions"""
50+
51+
if expr.func.__name__ == "Piecewise":
52+
return self._print_Piecewise(expr)
53+
54+
if func := self.func_map.get(expr.func.__name__):
55+
return f"{func}({', '.join(map(self._print, expr.args))})"
56+
57+
return super()._print_Function(expr)
58+
59+
def _print_Piecewise(self, expr):
60+
"""Custom printing for Piecewise function"""
61+
# merge the tuples and drop the final `True` condition
62+
str_args = map(
63+
self._print,
64+
islice(chain.from_iterable(expr.args), len(expr.args) - 1),
65+
)
66+
return f"piecewise({', '.join(str_args)})"
67+
68+
def _print_Min(self, expr):
69+
"""Custom printing for Min function"""
70+
return f"min({', '.join(map(self._print, expr.args))})"
71+
72+
def _print_Max(self, expr):
73+
"""Custom printing for Max function"""
74+
return f"max({', '.join(map(self._print, expr.args))})"
75+
76+
77+
def petab_math_str(expr: sp.Expr) -> str:
78+
"""Convert a sympy expression to a PEtab-compatible math expression string.
79+
80+
:example:
81+
>>> expr = sp.sympify("x**2 + sin(y)")
82+
>>> petab_math_str(expr)
83+
'x ^ 2 + sin(y)'
84+
"""
85+
86+
return PetabStrPrinter().doprint(expr)

petab/v1/math/sympify.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,17 @@ def sympify_petab(expr: str | int | float) -> sp.Expr | sp.Basic:
5252
visitor = MathVisitorSympy()
5353
expr = visitor.visit(tree)
5454
expr = bool2num(expr)
55-
# check for `False`, we'll accept both `True` and `None`
56-
if expr.is_extended_real is False:
57-
raise ValueError(f"Expression {expr} is not real-valued.")
58-
55+
try:
56+
# check for `False`, we'll accept both `True` and `None`
57+
if expr.is_extended_real is False:
58+
raise ValueError(f"Expression {expr} is not real-valued.")
59+
except AttributeError as e:
60+
# work-around for `sp.sec(0, evaluate=False).is_extended_real` error
61+
if str(e) not in (
62+
"'One' object has no attribute '_eval_is_extended_real'",
63+
"'Float' object has no attribute '_eval_is_extended_real'",
64+
):
65+
raise
5966
return expr
6067

6168

tests/v1/math/test_math.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import sympy as sp
77
import yaml
88
from sympy.abc import _clash
9-
from sympy.logic.boolalg import Boolean
9+
from sympy.logic.boolalg import Boolean, BooleanFalse, BooleanTrue
1010

11-
from petab.math import sympify_petab
11+
from petab.math import petab_math_str, sympify_petab
1212

1313

1414
def test_sympify_numpy():
@@ -23,6 +23,9 @@ def test_parse_simple():
2323
assert float(sympify_petab("1 + 2 * (3 + 4)")) == 15
2424
assert float(sympify_petab("1 + 2 * (3 + 4) / 2")) == 8
2525

26+
def test_printer():
27+
assert petab_math_str(BooleanTrue()) == "true"
28+
assert petab_math_str(BooleanFalse()) == "false"
2629

2730
def read_cases():
2831
"""Read test cases from YAML file in the petab_test_suite package."""
@@ -55,20 +58,27 @@ def read_cases():
5558
@pytest.mark.parametrize("expr_str, expected", read_cases())
5659
def test_parse_cases(expr_str, expected):
5760
"""Test PEtab math expressions for the PEtab test suite."""
58-
result = sympify_petab(expr_str)
59-
if isinstance(result, Boolean):
60-
assert result == expected
61+
sym_expr = sympify_petab(expr_str)
62+
if isinstance(sym_expr, Boolean):
63+
assert sym_expr == expected
6164
else:
6265
try:
63-
result = float(result.evalf())
66+
result = float(sym_expr.evalf())
6467
assert np.isclose(result, expected), (
6568
f"{expr_str}: Expected {expected}, got {result}"
6669
)
6770
except TypeError:
68-
assert result == expected, (
71+
assert sym_expr == expected, (
6972
f"{expr_str}: Expected {expected}, got {result}"
7073
)
7174

75+
# test parsing, printing, and parsing again
76+
resympified = sympify_petab(petab_math_str(sym_expr))
77+
if sym_expr.is_number:
78+
assert np.isclose(float(resympified), float(sym_expr))
79+
else:
80+
assert resympified.equals(sym_expr), (sym_expr, resympified)
81+
7282

7383
def test_ids():
7484
"""Test symbols in expressions."""

0 commit comments

Comments
 (0)