From 1b3641d0dda559c8478ae3e22f1041327d2b715a Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 28 Jan 2023 19:35:50 -0300 Subject: [PATCH] Improve infinity and Precision (#767) This would be the simplest part of #766, including tests and changes in `Infinity` and `Precision`. Some of the new tests are commented / marked as xfail because require the other changes. --- CHANGES.rst | 3 + mathics/builtin/arithfns/basic.py | 2 +- mathics/builtin/arithmetic.py | 24 ++- mathics/builtin/numbers/hyperbolic.py | 17 +-- mathics/core/convert/mpmath.py | 7 + mathics/core/convert/sympy.py | 10 +- mathics/core/expression.py | 8 +- test/builtin/arithmetic/test_abs.py | 47 +++++- test/builtin/arithmetic/test_basic.py | 195 ++++++++++++++++++++++++ test/builtin/numbers/test_hyperbolic.py | 4 +- 10 files changed, 286 insertions(+), 31 deletions(-) create mode 100644 test/builtin/arithmetic/test_basic.py diff --git a/CHANGES.rst b/CHANGES.rst index 7ee76eb81..774387872 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -91,7 +91,10 @@ Bugs #. Some scikit image routines line ``EdgeDetect`` were getting omitted due to overly stringent PYPI requirements #. Units and Quantities were sometimes failing. Also they were omitted from documentation. +#. Better handling of ``Infinite`` quantities. +#. Fix ``Precision`` compatibility with WMA. + PyPI Package requirements +++++++++++++++++++++++++ diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index 08f291380..f09dd735c 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -979,7 +979,7 @@ def eval(self, items, evaluation): ) elif item.get_head().sameQ(SymbolDirectedInfinity): infinity_factor = True - if len(item.elements) > 1: + if len(item.elements) > 0: direction = item.elements[0] if isinstance(direction, Number): numbers.append(direction) diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index bdbca4f5e..28c279d7d 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -49,6 +49,7 @@ from mathics.core.convert.expression import to_expression from mathics.core.convert.mpmath import from_mpmath from mathics.core.convert.sympy import SympyExpression, from_sympy, sympy_symbol_prefix +from mathics.core.element import ElementsProperties from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.number import SpecialValueError, dps, min_prec @@ -658,9 +659,9 @@ class DirectedInfinity(SympyFunction): "DirectedInfinity[Indeterminate]": "Indeterminate", "DirectedInfinity[args___] ^ -1": "0", "0 * DirectedInfinity[args___]": "Message[Infinity::indet, Unevaluated[0 DirectedInfinity[args]]]; Indeterminate", - "DirectedInfinity[a_?NumericQ] /; N[Abs[a]] != 1": "DirectedInfinity[a / Abs[a]]", - "DirectedInfinity[a_] * DirectedInfinity[b_]": "DirectedInfinity[a*b]", - "DirectedInfinity[] * DirectedInfinity[args___]": "DirectedInfinity[]", + # "DirectedInfinity[a_?NumericQ] /; N[Abs[a]] != 1": "DirectedInfinity[a / Abs[a]]", + # "DirectedInfinity[a_] * DirectedInfinity[b_]": "DirectedInfinity[a*b]", + # "DirectedInfinity[] * DirectedInfinity[args___]": "DirectedInfinity[]", # Rules already implemented in Times.eval # "z_?NumberQ * DirectedInfinity[]": "DirectedInfinity[]", # "z_?NumberQ * DirectedInfinity[a_]": "DirectedInfinity[z * a]", @@ -685,9 +686,7 @@ class DirectedInfinity(SympyFunction): " Unevaluated[DirectedInfinity[0.]]];" "Indeterminate" ), - "DirectedInfinity[ComplexInfinity]": "ComplexInfinity", - "DirectedInfinity[Infinity]": "Infinity", - "DirectedInfinity[-Infinity]": "-Infinity", + "DirectedInfinity[DirectedInfinity[x___]]": "DirectedInfinity[x]", } formats = { @@ -698,6 +697,19 @@ class DirectedInfinity(SympyFunction): "DirectedInfinity[z_?NumericQ]": "HoldForm[z Infinity]", } + def eval(self, z, evaluation): + """DirectedInfinity[z_]""" + if z in (Integer1, IntegerM1): + return None + if isinstance(z, Number) or isinstance(eval_N(z, evaluation), Number): + direction = (z / Expression(SymbolAbs, z)).evaluate(evaluation) + return Expression( + SymbolDirectedInfinity, + direction, + elements_properties=ElementsProperties(True, True, True), + ) + return None + def to_sympy(self, expr, **kwargs): if len(expr.elements) == 1: dir = expr.elements[0].get_int_value() diff --git a/mathics/builtin/numbers/hyperbolic.py b/mathics/builtin/numbers/hyperbolic.py index 32af6876c..20f9f041f 100644 --- a/mathics/builtin/numbers/hyperbolic.py +++ b/mathics/builtin/numbers/hyperbolic.py @@ -58,9 +58,9 @@ class ArcCosh(_MPMathFunction): rules = { "ArcCosh[Undefined]": "Undefined", - "ArcCosh[I Infinity]": "Infinity", - "ArcCosh[-I Infinity]": "Infinity", - "ArcCosh[ComplexInfinity]": "Infinity", + "ArcCosh[DirectedInfinity[I]]": "Infinity", + "ArcCosh[DirectedInfinity[-I]]": "Infinity", + "ArcCosh[DirectedInfinity[]]": "Infinity", "Derivative[1][ArcCosh]": "1/(Sqrt[#-1]*Sqrt[#+1])&", } summary_text = "inverse hyperbolic cosine function" @@ -324,15 +324,8 @@ class Gudermannian(Builtin): "Gudermannian[Undefined]": "Undefined", "Gudermannian[0]": "0", "Gudermannian[2*Pi*I]": "0", - # FIXME: handling DirectedInfinity[-I] leads to problems - # "Gudermannian[6/4 Pi I]": "DirectedInfinity[-I]", - # Handled already - # "Gudermannian[Infinity]": "Pi/2", - # FIXME: Pi/2 from Sympy seems to take precedence - "Gudermannian[-Infinity]": "-Pi/2", - # Below, we don't use instead of ComplexInfinity that gets - # substituted out for DirectedInfinity[] before we match on - # Gudermannian[...] + "Gudermannian[3 I / 2 Pi]": "DirectedInfinity[-I]", + "Gudermannian[DirectedInfinity[-1]]": "-Pi/2", "Gudermannian[DirectedInfinity[]]": "Indeterminate", "Gudermannian[z_]": "2 ArcTan[Tanh[z / 2]]", # Commented out because := might not work properly diff --git a/mathics/core/convert/mpmath.py b/mathics/core/convert/mpmath.py index 655189cf4..181554c85 100644 --- a/mathics/core/convert/mpmath.py +++ b/mathics/core/convert/mpmath.py @@ -11,7 +11,12 @@ @lru_cache(maxsize=1024) def from_mpmath(value, prec=None, acc=None): "Converts mpf or mpc to Number." + if mpmath.isnan(value): + return SymbolIndeterminate if isinstance(value, mpmath.mpf): + if mpmath.isinf(value): + direction = Integer1 if value > 0 else IntegerM1 + return Expression(SymbolDirectedInfinity, direction) # if accuracy is given, override # prec: if acc is not None: @@ -28,6 +33,8 @@ def from_mpmath(value, prec=None, acc=None): # HACK: use str here to prevent loss of precision return PrecisionReal(sympy.Float(str(value), prec)) elif isinstance(value, mpmath.mpc): + if mpmath.isinf(value): + return SymbolComplexInfinity if value.imag == 0.0: return from_mpmath(value.real, prec, acc) real = from_mpmath(value.real, prec, acc) diff --git a/mathics/core/convert/sympy.py b/mathics/core/convert/sympy.py index 528612219..29932ff9d 100644 --- a/mathics/core/convert/sympy.py +++ b/mathics/core/convert/sympy.py @@ -252,10 +252,14 @@ def from_sympy(expr): elif expr is sympy.false: return SymbolFalse - elif expr.is_number and all([x.is_Number for x in expr.as_real_imag()]): + if expr.is_number and all([x.is_Number for x in expr.as_real_imag()]): # Hack to convert * I to Complex[0, ] - return Complex(*[from_sympy(arg) for arg in expr.as_real_imag()]) - elif expr.is_Add: + try: + return Complex(*[from_sympy(arg) for arg in expr.as_real_imag()]) + except ValueError: + # The exception happens if one of the components is infinity + pass + if expr.is_Add: return to_expression( SymbolPlus, *sorted([from_sympy(arg) for arg in expr.args]) ) diff --git a/mathics/core/expression.py b/mathics/core/expression.py index ea26c7197..e67359131 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -862,7 +862,13 @@ def get_sort_key(self, pattern_sort=False) -> tuple: exps[name] = exps.get(name, 0) + 1 elif self.has_form("Power", 2): var = self._elements[0].get_name() - exp = self._elements[1].round_to_float() + # TODO: Check if this is the expected behaviour. + # round_to_float is an attribute of Expression, + # but not for Atoms. + try: + exp = self._elements[1].round_to_float() + except AttributeError: + exp = None if var and exp is not None: exps[var] = exps.get(var, 0) + exp if exps: diff --git a/test/builtin/arithmetic/test_abs.py b/test/builtin/arithmetic/test_abs.py index b22f47049..f17bb24ea 100644 --- a/test/builtin/arithmetic/test_abs.py +++ b/test/builtin/arithmetic/test_abs.py @@ -4,10 +4,45 @@ """ from test.helper import check_evaluation +import pytest -def test_abs(): - for str_expr, str_expected in [ - ("Abs[a - b]", "Abs[a - b]"), - ("Abs[Sqrt[3]]", "Sqrt[3]"), - ]: - check_evaluation(str_expr, str_expected) + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("Abs[a - b]", "Abs[a - b]", None), + ("Abs[Sqrt[3]]", "Sqrt[3]", None), + ("Abs[Sqrt[3]/5]", "Sqrt[3]/5", None), + ("Abs[-2/3]", "2/3", None), + ("Abs[2+3 I]", "Sqrt[13]", None), + ("Abs[2.+3 I]", "3.60555", None), + # TODO: Implement rules for these cases. + # ("Abs[4^(2 Pi)]", "4^(2 Pi)", None), + ], +) +def test_abs(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("Sign[a - b]", "Sign[a - b]", None), + ("Sign[Sqrt[3]]", "1", None), + ("Sign[0]", "0", None), + ("Sign[0.]", "0", None), + ("Sign[(1 + I)]", "(1/2 + I/2)Sqrt[2]", None), + ("Sign[(1. + I)]", "(0.707107 + 0.707107 I)", None), + ("Sign[(1 + I)/Sqrt[2]]", "(1 + I)/Sqrt[2]", None), + ("Sign[(1 + I)/Sqrt[2.]]", "(0.707107 + 0.707107 I)", None), + ("Sign[-2/3]", "-1", None), + ("Sign[2+3 I]", "(2 + 3 I)/(13^(1/2))", None), + ("Sign[2.+3 I]", "0.5547 + 0.83205 I", None), + ("Sign[4^(2 Pi)]", "1", None), + # FixME: add rules to handle this kind of case + # ("Sign[I^(2 Pi)]", "I^(2 Pi)", None), + # ("Sign[4^(2 Pi I)]", "1", None), + ], +) +def test_sign(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) diff --git a/test/builtin/arithmetic/test_basic.py b/test/builtin/arithmetic/test_basic.py new file mode 100644 index 000000000..cc771eb00 --- /dev/null +++ b/test/builtin/arithmetic/test_basic.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.arithmetic.basic +""" +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("1. + 2. + 3.", "6.", None), + ("1 + 2/3 + 3/5", "34 / 15", None), + ("1 - 2/3 + 3/5", "14 / 15", None), + ("1. - 2/3 + 3/5", "0.933333", None), + ("1 - 2/3 + 2 I", "1 / 3 + 2 I", None), + ("1. - 2/3 + 2 I", "0.333333 + 2. I", None), + ( + "a + 2 a + 3 a q", + "3 a + 3 a q", + "WMA do not collect the common factor `a` in the last expression neither", + ), + ("a - 2 a + 3 a q", "-a + 3 a q", None), + ("a - (5+ a+ 2 b) + 3 a q", "-5 + 3 a q - 2 b", "WMA distribute the sign (-)"), + ( + "a - 2 (5+ a+ 2 b) + 3 a q", + "a + 3 a q - 2 (5 + a + 2 b)", + "WMA do not distribute neither in the general case", + ), + ], +) +def test_add(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg, hold_expected=True) + + +@pytest.mark.parametrize( + ( + "str_expr", + "str_expected", + ), + [ + ("E^(3+I Pi)", "-E ^ 3"), + ("E^(I Pi/2)", "I"), + ("E^1", "E"), + ("log2=Log[2.]; E^log2", "2."), + ("log2=Log[2.]; Chop[E^(log2+I Pi)]", "-2."), + ("log2=.; E^(I Pi/4)", "E ^ (I / 4 Pi)"), + ("E^(.25 I Pi)", "0.707107 + 0.707107 I"), + ], +) +def test_exponential(str_expr, str_expected): + check_evaluation(str_expr, str_expected, hold_expected=True) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("1. 2. 3.", "6.", None), + ("1 * 2/3 * 3/5", "2 / 5", None), + ("1 (- 2/3) ( 3/5)", "-2 / 5", None), + ("1. (- 2/3) ( 3 / 5)", "-0.4", None), + ("1 (- 2/3) (2 I)", "-4 I / 3", None), + ("1. (- 2/3) (2 I)", "0. - 1.33333 I", None), + ("a ( 2 a) ( 3 a q)", "6 a ^ 3 q", None), + ("a (- 2 a) ( 3 Sqrt[a] q)", "-6 a ^ (5 / 2) q", None), + ( + "a (5+ a+ 2 b) (3 a q)", + "3 a ^ 2 q (5 + a + 2 b)", + "WMA distribute the sign (-)", + ), + ( + "a (- 2 (5+ a+ 2 b)) * (3 a q)", + "-6 a ^ 2 q (5 + a + 2 b)", + "WMA do not distribute neither in the general case", + ), + ( + "a b a^2 / (2 a)^(3/2)", + "Sqrt[2] a ^ (3 / 2) b / 4", + "WMA do not distribute neither in the general case", + ), + ( + "a b a^2 / (a)^(3/2)", + "a ^ (3 / 2) b", + "WMA do not distribute neither in the general case", + ), + ( + "a b a^2 / (a b)^(3/2)", + "a ^ 3 b / (a b) ^ (3 / 2)", + "WMA do not distribute neither in the general case", + ), + ( + "a b a ^ 2 (a b)^(-3 / 2)", + "a ^ 3 b / (a b) ^ (3 / 2)", + "Goes to the previous case because of the rule in Power", + ), + ( + "a b Infinity", + "a b Infinity", + "Goes to the previous case because of the rule in Power", + ), + ( + "a b 0 * Infinity", + "Indeterminate", + "Goes to the previous case because of the rule in Power", + ), + ( + "a b ComplexInfinity", + "ComplexInfinity", + "Goes to the previous case because of the rule in Power", + ), + ( + "a b DirectedInfinity[1. + 2. I]", + "a b (0.447214 + 0.894427 I) Infinity", + "", + ), + ("a b DirectedInfinity[I]", "a b I Infinity", ""), + ("a b (-1 + 2 I) Infinity", "a b (-1 / 5 + 2 I / 5) Sqrt[5] Infinity", ""), + ("a b (-1 + 2 Pi I) Infinity", "a b (-1 + 2 I Pi) Infinity", ""), + ( + "a b DirectedInfinity[(1 + 2 I)/ Sqrt[5]]", + "a b (1 / 5 + 2 I / 5) Sqrt[5] Infinity", + "", + ), + ("a b DirectedInfinity[q]", "a b q Infinity", ""), + # Failing tests + # Problem with formatting. Parenthezise are missing... + # ("a b DirectedInfinity[-I]", "a b (-I Infinity)", ""), + # ("a b DirectedInfinity[-3]", "a b (-Infinity)", ""), + ], +) +@pytest.mark.xfail +def test_multiply(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg, hold_expected=True) + + +@pytest.mark.parametrize( + ( + "str_expr", + "str_expected", + "msg", + ), + [ + ("2^0", "1", None), + ("(2/3)^0", "1", None), + ("2.^0", "1.", None), + ("2^1", "2", None), + ("(2/3)^1", "2 / 3", None), + ("2.^1", "2.", None), + ("2^(3)", "8", None), + ("(1/2)^3", "1 / 8", None), + ("2^(-3)", "1 / 8", None), + ("(1/2)^(-3)", "8", None), + ("(-7)^(5/3)", "-7 (-7) ^ (2 / 3)", None), + ("3^(1/2)", "Sqrt[3]", None), + # WMA do not rationalize numbers + ("(1/5)^(1/2)", "Sqrt[5] / 5", None), + # WMA do not rationalize numbers + ("(3)^(-1/2)", "Sqrt[3] / 3", None), + ("(1/3)^(-1/2)", "Sqrt[3]", None), + ("(5/3)^(1/2)", "Sqrt[5 / 3]", None), + ("(5/3)^(-1/2)", "Sqrt[3 / 5]", None), + ("1/Sqrt[Pi]", "1 / Sqrt[Pi]", None), + ("I^(2/3)", "(-1) ^ (1 / 3)", None), + # In WMA, the next test would return ``-(-I)^(2/3)`` + # which is less compact and elegant... + ("(-I)^(2/3)", "(-1) ^ (-1 / 3)", None), + ("(2+3I)^3", "-46 + 9 I", None), + ("(1.+3. I)^.6", "1.46069 + 1.35921 I", None), + ("3^(1+2 I)", "3 ^ (1 + 2 I)", None), + ("3.^(1+2 I)", "-1.75876 + 2.43038 I", None), + ("3^(1.+2 I)", "-1.75876 + 2.43038 I", None), + # In WMA, the following expression returns + # ``(Pi/3)^I``. By now, this is handled by + # sympy, which produces the result + ("(3/Pi)^(-I)", "(3 / Pi) ^ (-I)", None), + # Association rules + ('(a^"w")^2', 'a^(2 "w")', "Integer power of a power with string exponent"), + ('(a^2)^"w"', '(a ^ 2) ^ "w"', None), + ('(a^2)^"w"', '(a ^ 2) ^ "w"', None), + ("(a^2)^(1/2)", "Sqrt[a ^ 2]", None), + ("(a^(1/2))^2", "a", None), + ("(a^(1/2))^2", "a", None), + ("(a^(3/2))^3.", "(a ^ (3 / 2)) ^ 3.", None), + ("(a^(1/2))^3.", "a ^ 1.5", None), + ("(a^(.3))^3.", "a ^ 0.9", None), + ("(a^(1.3))^3.", "(a ^ 1.3) ^ 3.", None), + # Exponentials involving expressions + ("(a^(p-2 q))^3", "a ^ (3 p - 6 q)", None), + ("(a^(p-2 q))^3.", "(a ^ (p - 2 q)) ^ 3.", None), + ], +) +@pytest.mark.xfail +def test_power(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) diff --git a/test/builtin/numbers/test_hyperbolic.py b/test/builtin/numbers/test_hyperbolic.py index 983fbe349..78e4288a4 100644 --- a/test/builtin/numbers/test_hyperbolic.py +++ b/test/builtin/numbers/test_hyperbolic.py @@ -14,10 +14,10 @@ def test_gudermannian(): ("Gudermannian[0]", "0"), ("Gudermannian[2 Pi I]", "0"), # FIXME: Mathics can't handle Rule substitution - # ("Gudermannian[6/4 Pi I]", "DirectedInfinity[-I]"), + ("Gudermannian[6/4 Pi I]", "DirectedInfinity[-I]"), ("Gudermannian[Infinity]", "Pi/2"), # FIXME: rule does not work - # ("Gudermannian[-Infinity]", "-Pi/2"), + ("Gudermannian[-Infinity]", "-Pi/2"), ("Gudermannian[ComplexInfinity]", "Indeterminate"), # FIXME Tanh[1 / 2] doesn't eval but Tanh[0.5] does ("Gudermannian[z]", "2 ArcTan[Tanh[z / 2]]"),