diff --git a/petab/v1/math/printer.py b/petab/v1/math/printer.py index a421989c..f2146233 100644 --- a/petab/v1/math/printer.py +++ b/petab/v1/math/printer.py @@ -41,7 +41,10 @@ def _print_Pow(self, expr: sp.Pow): str_exp = self._print(exp) if not base.is_Atom: str_base = f"({str_base})" - if not exp.is_Atom: + # A non-integer Rational exponent (e.g. sqrt -> 1/2) is an Atom but + # prints as the multi-token "1/2", so without parentheses "x ^ 1/2" + # re-parses as (x^1)/2. Parenthesize it explicitly. + if not exp.is_Atom or (exp.is_Rational and not exp.is_Integer): str_exp = f"({str_exp})" return f"{str_base} ^ {str_exp}" diff --git a/tests/v1/math/test_math.py b/tests/v1/math/test_math.py index 60bb04b5..03bd0cbf 100644 --- a/tests/v1/math/test_math.py +++ b/tests/v1/math/test_math.py @@ -43,6 +43,10 @@ def test_printer(): assert petab_math_str(BooleanTrue()) == "true" assert petab_math_str(BooleanFalse()) == "false" assert petab_math_str((a + b) ** (c + d)) == "(a + b) ^ (c + d)" + # A non-integer rational exponent must be parenthesized, else "a ^ 1/2" + # re-parses as (a^1)/2 (i.e. sqrt(a) would round-trip to a/2). + assert petab_math_str(sp.sqrt(a)) == "a ^ (1/2)" + assert petab_math_str(a ** sp.Rational(2, 3)) == "a ^ (2/3)" def read_cases():