From 983e59a9a1d0d05b91338fa3c2feffd81532f420 Mon Sep 17 00:00:00 2001 From: Stanislav Date: Fri, 16 Oct 2026 15:17:16 +0400 Subject: [PATCH] Sum, Product with integer bounds: add the terms, do not go through SymPy With step 1, IterationFunction.eval_iter always converted Sum/Product to SymPy: the summand was converted, the terms were evaluated by Mathics3, the result converted to SymPy again, then Together, back to Mathics3 and Cancel. For integer bounds all that is needed is to evaluate the terms and add them, as Table does, and the round trip costs far more than the sum itself when the terms are large symbolic expressions. Now SymPy is used only when the bounds are not integers. A sum of rational functions with integer bounds is no longer put over a common denominator: Sum[1/(x + i), {i, 1, 2}] gives 1/(1 + x) + 1/(2 + x). Co-Authored-By: Claude Opus 5.5 --- mathics/core/builtin.py | 11 ++++++++--- test/builtin/test_arithmetic.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index cd48318b2a..87ba6df2e9 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -1209,7 +1209,14 @@ def eval_iter_nostep(self, expr, i, imin, imax, evaluation): def eval_iter(self, expr, i, imin, imax, di, evaluation): "%(name)s[expr_, {i_Symbol, imin_, imax_, di_}]" - if isinstance(self, SympyFunction) and di.int_value == 1: + imin = imin.evaluate(evaluation) + imax = imax.evaluate(evaluation) + # With integer bounds the terms are just evaluated and combined below, as + # in Table; SymPy is needed only for symbolic bounds. Going through SymPy + # (summand and result converted there and back, then Together) costs far + # more than the sum itself when the terms are large symbolic expressions. + integer_bounds = isinstance(imin, Integer) and isinstance(imax, Integer) + if isinstance(self, SympyFunction) and di.int_value == 1 and not integer_bounds: whole_expr = to_expression( self.get_name(), expr, ListExpression(i, imin, imax) ) @@ -1227,8 +1234,6 @@ def eval_iter(self, expr, i, imin, imax, di, evaluation): return index = Integer0 - imin = imin.evaluate(evaluation) - imax = imax.evaluate(evaluation) di = di.evaluate(evaluation) # (imax - imin) / di diff --git a/test/builtin/test_arithmetic.py b/test/builtin/test_arithmetic.py index aa4a0bd199..3b5a6f77d0 100644 --- a/test/builtin/test_arithmetic.py +++ b/test/builtin/test_arithmetic.py @@ -3,7 +3,7 @@ Unit tests from mathics.builtin.arithmetic """ -from test.helper import check_arg_counts +from test.helper import check_arg_counts, check_evaluation import pytest @@ -60,3 +60,30 @@ def test_arithmetic_arg_errors(function_name, msg_fragment): """ """ check_arg_counts(function_name, msg_fragment) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "fail_msg"), + [ + ("Sum[i, {i, 1, 10}]", "55", None), + ("Sum[f[i], {i, 0, 3}]", "f[0] + f[1] + f[2] + f[3]", None), + ("Sum[x^i, {i, 0, 3}]", "1 + x + x ^ 2 + x ^ 3", None), + ( + "Sum[1/(x + i), {i, 1, 2}]", + "1 / (1 + x) + 1 / (2 + x)", + "With integer bounds the terms are added, not put over a common denominator", + ), + ("Sum[i, {i, 1, 0}]", "0", "Empty range"), + ("Product[x + i, {i, 1, 3}]", "(1 + x) (2 + x) (3 + x)", None), + ("Product[i, {i, 1, 0}]", "1", "Empty range"), + ( + "Sum[k, {k, 1, n}]", + "n (1 + n) / 2", + "Symbolic bounds still go through SymPy", + ), + ("Sum[1/k^2, {k, 1, Infinity}]", "Pi ^ 2 / 6", None), + ], +) +def test_sum_product_bounds(str_expr, str_expected, fail_msg): + """Sum and Product with integer and with symbolic bounds""" + check_evaluation(str_expr, str_expected, failure_message=fail_msg)