Skip to content

Commit 81af370

Browse files
authored
Add evaluate: bool argument to math parser (#365)
So far, when math expressions were parsed, they were evaluated as far as possible. This was not always desirable. Now, this optional as far as conveniently possible. Closes #363.
1 parent 017746e commit 81af370

File tree

3 files changed

+74
-15
lines changed

3 files changed

+74
-15
lines changed

petab/v1/math/SympyVisitor.py

+37-13
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,12 @@
3939
}
4040
_unary_funcs = {
4141
"exp": sp.exp,
42-
"log10": lambda x: -sp.oo if x.is_zero is True else sp.log(x, 10),
43-
"log2": lambda x: -sp.oo if x.is_zero is True else sp.log(x, 2),
42+
"log10": lambda x, evaluate=True: -sp.oo
43+
if x.is_zero is True
44+
else sp.log(x, 10, evaluate=evaluate),
45+
"log2": lambda x, evaluate=True: -sp.oo
46+
if x.is_zero is True
47+
else sp.log(x, 2, evaluate=evaluate),
4448
"ln": sp.log,
4549
"sqrt": sp.sqrt,
4650
"abs": sp.Abs,
@@ -75,8 +79,14 @@ class MathVisitorSympy(PetabMathExprParserVisitor):
7579
7680
For a general introduction to ANTLR4 visitors, see:
7781
https://github.com/antlr/antlr4/blob/7d4cea92bc3f7d709f09c3f1ac77c5bbc71a6749/doc/python-target.md
82+
83+
:param evaluate: Whether to evaluate the expression.
7884
"""
7985

86+
def __init__(self, evaluate=True):
87+
super().__init__()
88+
self.evaluate = evaluate
89+
8090
def visitPetabExpression(
8191
self, ctx: PetabMathExprParser.PetabExpressionContext
8292
) -> sp.Expr | sp.Basic:
@@ -101,9 +111,17 @@ def visitMultExpr(
101111
operand1 = bool2num(self.visit(ctx.getChild(0)))
102112
operand2 = bool2num(self.visit(ctx.getChild(2)))
103113
if ctx.ASTERISK():
104-
return operand1 * operand2
114+
return sp.Mul(operand1, operand2, evaluate=self.evaluate)
105115
if ctx.SLASH():
106-
return operand1 / operand2
116+
return (
117+
operand1 / operand2
118+
if self.evaluate
119+
else sp.Mul(
120+
operand1,
121+
sp.Pow(operand2, -1, evaluate=False),
122+
evaluate=False,
123+
)
124+
)
107125

108126
raise AssertionError(f"Unexpected expression: {ctx.getText()}")
109127

@@ -112,9 +130,9 @@ def visitAddExpr(self, ctx: PetabMathExprParser.AddExprContext) -> sp.Expr:
112130
op1 = bool2num(self.visit(ctx.getChild(0)))
113131
op2 = bool2num(self.visit(ctx.getChild(2)))
114132
if ctx.PLUS():
115-
return op1 + op2
133+
return sp.Add(op1, op2, evaluate=self.evaluate)
116134
if ctx.MINUS():
117-
return op1 - op2
135+
return sp.Add(op1, -op2, evaluate=self.evaluate)
118136

119137
raise AssertionError(
120138
f"Unexpected operator: {ctx.getChild(1).getText()} "
@@ -146,28 +164,32 @@ def visitFunctionCall(
146164
f"Unexpected number of arguments: {len(args)} "
147165
f"in {ctx.getText()}"
148166
)
149-
return _trig_funcs[func_name](*args)
167+
return _trig_funcs[func_name](*args, evaluate=self.evaluate)
150168
if func_name in _unary_funcs:
151169
if len(args) != 1:
152170
raise AssertionError(
153171
f"Unexpected number of arguments: {len(args)} "
154172
f"in {ctx.getText()}"
155173
)
156-
return _unary_funcs[func_name](*args)
174+
return _unary_funcs[func_name](*args, evaluate=self.evaluate)
157175
if func_name in _binary_funcs:
158176
if len(args) != 2:
159177
raise AssertionError(
160178
f"Unexpected number of arguments: {len(args)} "
161179
f"in {ctx.getText()}"
162180
)
163-
return _binary_funcs[func_name](*args)
181+
return _binary_funcs[func_name](*args, evaluate=self.evaluate)
164182
if func_name == "log":
165183
if len(args) not in [1, 2]:
166184
raise AssertionError(
167185
f"Unexpected number of arguments: {len(args)} "
168186
f"in {ctx.getText()}"
169187
)
170-
return -sp.oo if args[0].is_zero is True else sp.log(*args)
188+
return (
189+
-sp.oo
190+
if args[0].is_zero is True
191+
else sp.log(*args, evaluate=self.evaluate)
192+
)
171193

172194
if func_name == "piecewise":
173195
if (len(args) - 1) % 2 != 0:
@@ -184,7 +206,7 @@ def visitFunctionCall(
184206
args[::2], args[1::2], strict=True
185207
)
186208
)
187-
return sp.Piecewise(*sp_args)
209+
return sp.Piecewise(*sp_args, evaluate=self.evaluate)
188210

189211
raise ValueError(f"Unknown function: {ctx.getText()}")
190212

@@ -203,7 +225,7 @@ def visitPowerExpr(
203225
)
204226
operand1 = bool2num(self.visit(ctx.getChild(0)))
205227
operand2 = bool2num(self.visit(ctx.getChild(2)))
206-
return sp.Pow(operand1, operand2)
228+
return sp.Pow(operand1, operand2, evaluate=self.evaluate)
207229

208230
def visitUnaryExpr(
209231
self, ctx: PetabMathExprParser.UnaryExprContext
@@ -240,7 +262,7 @@ def visitComparisonExpr(
240262
if op in ops:
241263
lhs = bool2num(lhs)
242264
rhs = bool2num(rhs)
243-
return ops[op](lhs, rhs)
265+
return ops[op](lhs, rhs, evaluate=self.evaluate)
244266

245267
raise AssertionError(f"Unexpected operator: {op}")
246268

@@ -301,4 +323,6 @@ def num2bool(x: sp.Basic | sp.Expr) -> sp.Basic | sp.Expr:
301323
return sp.false
302324
if x.is_zero is False:
303325
return sp.true
326+
if isinstance(x, Boolean):
327+
return x
304328
return sp.Piecewise((True, x != 0.0), (False, True))

petab/v1/math/sympify.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
__all__ = ["sympify_petab"]
1313

1414

15-
def sympify_petab(expr: str | int | float) -> sp.Expr | sp.Basic:
15+
def sympify_petab(
16+
expr: str | int | float, evaluate: bool = True
17+
) -> sp.Expr | sp.Basic:
1618
"""Convert PEtab math expression to sympy expression.
1719
1820
.. note::
@@ -22,6 +24,7 @@ def sympify_petab(expr: str | int | float) -> sp.Expr | sp.Basic:
2224
2325
Args:
2426
expr: PEtab math expression.
27+
evaluate: Whether to evaluate the expression.
2528
2629
Raises:
2730
ValueError: Upon lexer/parser errors or if the expression is
@@ -30,6 +33,33 @@ def sympify_petab(expr: str | int | float) -> sp.Expr | sp.Basic:
3033
Returns:
3134
The sympy expression corresponding to `expr`.
3235
Boolean values are converted to numeric values.
36+
37+
38+
:example:
39+
>>> from petab.math import sympify_petab
40+
>>> sympify_petab("sin(0)")
41+
0
42+
>>> sympify_petab("sin(0)", evaluate=False)
43+
sin(0.0)
44+
>>> sympify_petab("sin(0)", evaluate=True)
45+
0
46+
>>> sympify_petab("1 + 2", evaluate=True)
47+
3.00000000000000
48+
>>> sympify_petab("1 + 2", evaluate=False)
49+
1.0 + 2.0
50+
>>> sympify_petab("piecewise(1, 1 > 2, 0)", evaluate=True)
51+
0.0
52+
>>> sympify_petab("piecewise(1, 1 > 2, 0)", evaluate=False)
53+
Piecewise((1.0, 1.0 > 2.0), (0.0, True))
54+
>>> # currently, boolean values are converted to numeric values
55+
>>> # independent of the `evaluate` flag
56+
>>> sympify_petab("true", evaluate=True)
57+
1.00000000000000
58+
>>> sympify_petab("true", evaluate=False)
59+
1.00000000000000
60+
>>> # ... and integer values are converted to floats
61+
>>> sympify_petab("2", evaluate=True)
62+
2.00000000000000
3363
"""
3464
if isinstance(expr, sp.Expr):
3565
# TODO: check if only PEtab-compatible symbols and functions are used
@@ -62,7 +92,7 @@ def sympify_petab(expr: str | int | float) -> sp.Expr | sp.Basic:
6292
raise ValueError(f"Error parsing {expr!r}: {e.args[0]}") from None
6393

6494
# Convert to sympy expression
65-
visitor = MathVisitorSympy()
95+
visitor = MathVisitorSympy(evaluate=evaluate)
6696
expr = visitor.visit(tree)
6797
expr = bool2num(expr)
6898
# check for `False`, we'll accept both `True` and `None`

tests/v1/math/test_math.py

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def test_parse_simple():
2424
assert float(sympify_petab("1 + 2 * (3 + 4) / 2")) == 8
2525

2626

27+
def test_evaluate():
28+
act = sympify_petab("piecewise(1, 1 > 2, 0)", evaluate=False)
29+
assert str(act) == "Piecewise((1.0, 1.0 > 2.0), (0.0, True))"
30+
31+
2732
def read_cases():
2833
"""Read test cases from YAML file in the petab_test_suite package."""
2934
yaml_file = importlib.resources.files("petabtests.cases").joinpath(

0 commit comments

Comments
 (0)