diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index 1b366531dd..1ef7bafa55 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -58,6 +58,7 @@ coeff_power, coefficient, eval_Apart, + eval_PolynomialMod, expand_polynomial, find_all_vars, get_exponents_sorted, @@ -1387,6 +1388,41 @@ def eval(self, expr, evaluation: Evaluation, options: dict): return eval_Numerator(expr, evaluation, algebraic_options) +class PolynomialMod(Builtin): + """ + :WMA link: + https://reference.wolfram.com/language/ref/PolynomialMod.html + +
+
'PolynomialMod'[$poly$, $m$] +
for a positive integer $m$, gives $poly$ expanded, with all its \ + coefficients reduced modulo $m$. +
+ + >> PolynomialMod[3 x ^ 2 + 2 x + 1, 2] + = 1 + x ^ 2 + >> PolynomialMod[3 x ^ 3 + 21 x ^ 2 - 7 x + 55, 9] + = 1 + 2 x + 3 x ^ 2 + 3 x ^ 3 + >> PolynomialMod[1234 (x + y) ^ 7 - (x - y) ^ 5, 7] + = 6 x ^ 5 + 2 x ^ 7 + 5 x ^ 4 y + 4 x ^ 3 y ^ 2 + 3 x ^ 2 y ^ 3 + 2 x y ^ 4 + y ^ 5 + 2 y ^ 7 + + Parts that are not polynomials are treated as variables: + >> PolynomialMod[3 f[1] + 2 f[2] - f[3], 2] + = f[1] + f[3] + + Reduction modulo a polynomial is not supported yet. + """ + + summary_text = "reduce the coefficients of a polynomial modulo an integer" + + def eval(self, poly, m: Integer, evaluation: Evaluation): + "PolynomialMod[poly_, m_Integer]" + modulus = m.value + if modulus <= 0: + return None + return eval_PolynomialMod(poly, modulus) + + class PolynomialQ(Builtin): """ :Polynomial: diff --git a/mathics/eval/numbers/algebra/polynomial.py b/mathics/eval/numbers/algebra/polynomial.py index 126dc9ff2a..5b9f436efc 100644 --- a/mathics/eval/numbers/algebra/polynomial.py +++ b/mathics/eval/numbers/algebra/polynomial.py @@ -705,3 +705,50 @@ def get_exponents_sorted(expr, var) -> list: expos = [term.as_coeff_exponent(x)[1] for mul in muls for term in mul] result.add(from_sympy(tracing.run_sympy(sympy.Max, *[e for e in expos]))) return sorted(result) + + +def eval_PolynomialMod(poly: BaseElement, modulus: int) -> Optional[BaseElement]: + """ + Evaluation routine for PolynomialMod[poly, m] with a positive integer m: + expand poly and reduce each integer coefficient to the range 0..m-1. + + As in WMA, parts that are not polynomials, like f[1] or Sin[x], are + treated as variables. If a coefficient is not an integer (a Rational, + Real or Complex), None is returned and the expression is left + unevaluated. + """ + expanded = expand_polynomial(poly, True, False, modulus=None, trig=False) + if expanded.has_form(SymbolList, None): + return None + terms = expanded.elements if expanded.has_form(SymbolPlus, None) else (expanded,) + reduced = [] + for term in terms: + rest: Optional[BaseElement] + if isinstance(term, Number): + coefficient, rest = term, None + elif term.has_form(SymbolTimes, 2, None) and isinstance( + term.elements[0], Number + ): + coefficient = term.elements[0] + factors = term.elements[1:] + rest = ( + factors[0] if len(factors) == 1 else Expression(SymbolTimes, *factors) + ) + else: + coefficient, rest = Integer1, term + if not isinstance(coefficient, Integer): + return None + value = coefficient.value % modulus + if value == 0: + continue + if rest is None: + reduced.append(Integer(value)) + elif value == 1: + reduced.append(rest) + else: + reduced.append(Expression(SymbolTimes, Integer(value), rest)) + if not reduced: + return Integer0 + if len(reduced) == 1: + return reduced[0] + return Expression(SymbolPlus, *reduced) diff --git a/test/builtin/numbers/test_algebra.py b/test/builtin/numbers/test_algebra.py index aebbb8e5dd..c40f654888 100644 --- a/test/builtin/numbers/test_algebra.py +++ b/test/builtin/numbers/test_algebra.py @@ -3,6 +3,7 @@ Unit tests for mathics.builtins.numbers.algebra and mathics.builtins.numbers.integer """ + from test.helper import check_arg_counts, check_evaluation import pytest @@ -388,6 +389,27 @@ def test_factor_terms_list(): check_evaluation(str_expr, str_expected) +def test_polynomialmod(): + for str_expr, str_expected in [ + ("PolynomialMod[3 x ^ 2 + 2 x + 1, 2]", "1 + x ^ 2"), + ("PolynomialMod[3 (x + 1) ^ 2, 2]", "1 + x ^ 2"), + ("PolynomialMod[-x, 2]", "x"), + ("PolynomialMod[-7, 3]", "2"), + ("PolynomialMod[2 x, 2]", "0"), + # Parts that are not polynomials are variables. + ("PolynomialMod[3 f[1] + 2 f[2], 2]", "f[1]"), + ("PolynomialMod[g[a, b], 2]", "g[a, b]"), + ("PolynomialMod[3 Sin[x] + 4 Sqrt[2] y, 3]", "Sqrt[2] y"), + # Not supported: left unevaluated rather than guessed. + ("PolynomialMod[x / 2, 3]", "PolynomialMod[x / 2, 3]"), + ("PolynomialMod[2.5 x, 2]", "PolynomialMod[2.5 x, 2]"), + ("PolynomialMod[x, -2]", "PolynomialMod[x, -2]"), + ("PolynomialMod[x ^ 2 + 1, x + 1]", "PolynomialMod[1 + x ^ 2, 1 + x]"), + ("PolynomialMod[{3 x}, 2]", "PolynomialMod[{3 x}, 2]"), + ]: + check_evaluation(str_expr, str_expected) + + def test_polynomialq(): for str_expr, str_expected in [ ("PolynomialQ[1/x[1]^2]", "False"),