From 09b0fe5ad06acb2e59ffefc8c4a4d4aa121acd18 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 27 May 2023 08:59:03 -0300 Subject: [PATCH 1/2] `session.parse` (#858) Adding this method which is handy for low-level tests. Also, * Removing unused imports. * Adding symbols to mathics.core.systemsymbols --------- Co-authored-by: R. Bernstein --- mathics/builtin/base.py | 6 +- mathics/builtin/numbers/constants.py | 81 +++--- .../numerical_properties.py | 5 + mathics/core/atoms.py | 10 + mathics/core/number.py | 4 +- mathics/core/systemsymbols.py | 6 + mathics/eval/arithmetic.py | 240 +++++++++++++++++- mathics/eval/parts.py | 8 +- mathics/session.py | 6 + test/builtin/arithmetic/test_abs.py | 3 +- .../arithmetic/test_lowlevel_properties.py | 86 +++++++ 11 files changed, 409 insertions(+), 46 deletions(-) create mode 100644 test/builtin/arithmetic/test_lowlevel_properties.py diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 06a4337b0..8eaccab79 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -799,10 +799,14 @@ def get_operator_display(self) -> Optional[str]: class Predefined(Builtin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.symbol = Symbol(self.get_name()) + def get_functions(self, prefix="eval", is_pymodule=False) -> List[Callable]: functions = list(super().get_functions(prefix)) if prefix == "eval" and hasattr(self, "evaluate"): - functions.append((Symbol(self.get_name()), self.evaluate)) + functions.append((self.symbol, self.evaluate)) return functions diff --git a/mathics/builtin/numbers/constants.py b/mathics/builtin/numbers/constants.py index 787826869..73779ff6a 100644 --- a/mathics/builtin/numbers/constants.py +++ b/mathics/builtin/numbers/constants.py @@ -9,25 +9,19 @@ # This tells documentation how to sort this module sort_order = "mathics.builtin.mathematical-constants" - import math +from typing import Optional import mpmath import numpy import sympy from mathics.builtin.base import Builtin, Predefined, SympyObject -from mathics.core.atoms import MachineReal, PrecisionReal +from mathics.core.atoms import NUMERICAL_CONSTANTS, MachineReal, PrecisionReal from mathics.core.attributes import A_CONSTANT, A_PROTECTED, A_READ_PROTECTED +from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation -from mathics.core.number import ( - MACHINE_DIGITS, - MAX_MACHINE_NUMBER, - MIN_MACHINE_NUMBER, - PrecisionValueError, - get_precision, - prec, -) +from mathics.core.number import MACHINE_DIGITS, PrecisionValueError, get_precision, prec from mathics.core.symbols import Atom, Symbol, strip_context from mathics.core.systemsymbols import SymbolIndeterminate @@ -89,28 +83,33 @@ def eval_N(self, precision, evaluation): def is_constant(self) -> bool: return True - def get_constant(self, precision, evaluation): + def get_constant( + self, + precision: Optional[BaseElement] = None, + evaluation: Optional[Evaluation] = None, + ): # first, determine the precision d = None - if precision: - try: - d = get_precision(precision, evaluation) - except PrecisionValueError: - pass + preference = None + if evaluation: + if precision: + try: + d = get_precision(precision, evaluation) + except PrecisionValueError: + pass + + preflist = evaluation._preferred_n_method.copy() + while preflist: + pref_method = preflist.pop() + if pref_method in ("numpy", "mpmath", "sympy"): + preference = pref_method + break if d is None: d = MACHINE_DIGITS # If preference not especified, determine it # from the precision. - preference = None - preflist = evaluation._preferred_n_method.copy() - while preflist: - pref_method = preflist.pop() - if pref_method in ("numpy", "mpmath", "sympy"): - preference = pref_method - break - if preference is None: if d <= MACHINE_DIGITS: preference = "numpy" @@ -131,10 +130,16 @@ def get_constant(self, precision, evaluation): preference = "mpmath" else: preference = "" + if preference == "numpy": - value = numpy_constant(self.numpy_name) if d == MACHINE_DIGITS: - return MachineReal(value) + try: + return NUMERICAL_CONSTANTS[self.symbol] + except KeyError: + value = MachineReal(numpy_constant(self.numpy_name)) + NUMERICAL_CONSTANTS[self.symbol] = value + return value + value = numpy_constant(self.numpy_name) if preference == "sympy": value = sympy_constant(self.sympy_name, d + 2) if preference == "mpmath": @@ -177,13 +182,16 @@ class _NumpyConstant(_Constant_Common): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.numpy_name is None: - self.numpy_name = strip_context(self.get_name()).lower() + self.numpy_name = strip_context(self.symbol.name).lower() self.mathics_to_numpy[self.__class__.__name__] = self.numpy_name + try: + value_float = numpy_constant(self.numpy_name) + except AttributeError: + value_float = self.to_numpy(self.symbol) + NUMERICAL_CONSTANTS[self.symbol] = MachineReal(value_float) def to_numpy(self, args): - if self.numpy_name is None or len(args) != 0: - return None - return self.get_constant() + return NUMERICAL_CONSTANTS[self.symbol] class _SympyConstant(_Constant_Common, SympyObject): @@ -608,7 +616,7 @@ class MaxMachineNumber(Predefined): summary_text = "largest normalized positive machine number" def evaluate(self, evaluation: Evaluation) -> MachineReal: - return MachineReal(MAX_MACHINE_NUMBER) + return NUMERICAL_CONSTANTS[self.symbol] class MinMachineNumber(Predefined): @@ -635,10 +643,10 @@ class MinMachineNumber(Predefined): summary_text = "smallest normalized positive machine number" def evaluate(self, evaluation: Evaluation) -> MachineReal: - return MachineReal(MIN_MACHINE_NUMBER) + return NUMERICAL_CONSTANTS[self.symbol] -class Pi(_MPMathConstant, _SympyConstant): +class Pi(_MPMathConstant, _NumpyConstant, _SympyConstant): """ :Pi, \u03c0: https://en.wikipedia.org/wiki/Pi ( @@ -740,3 +748,10 @@ class Underflow(Builtin): "Underflow[] * x_Real": "0.", } summary_text = "underflow in numeric evaluation" + + +# Constants that are not numpy constants, +for cls in (Catalan, Degree, Glaisher, GoldenRatio, Khinchin): + instance = cls(expression=False) + val = instance.get_constant() + NUMERICAL_CONSTANTS[instance.symbol] = MachineReal(val.value) diff --git a/mathics/builtin/testing_expressions/numerical_properties.py b/mathics/builtin/testing_expressions/numerical_properties.py index 8de45e8b1..5f5822f9e 100644 --- a/mathics/builtin/testing_expressions/numerical_properties.py +++ b/mathics/builtin/testing_expressions/numerical_properties.py @@ -13,6 +13,7 @@ from mathics.core.expression import Expression from mathics.core.symbols import BooleanType, SymbolFalse, SymbolTrue from mathics.core.systemsymbols import SymbolExpandAll, SymbolSimplify +from mathics.eval.arithmetic import test_zero_arithmetic_expr from mathics.eval.nevaluator import eval_N @@ -460,6 +461,10 @@ def eval(self, expr, evaluation): "%(name)s[expr_]" from sympy.matrices.utilities import _iszero + # This handles most of the arithmetic cases + if test_zero_arithmetic_expr(expr): + return SymbolTrue + sympy_expr = expr.to_sympy() result = _iszero(sympy_expr) if result is None: diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index f2b5c91a1..eadc66ad0 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -13,6 +13,8 @@ from mathics.core.number import ( FP_MANTISA_BINARY_DIGITS, MACHINE_PRECISION_VALUE, + MAX_MACHINE_NUMBER, + MIN_MACHINE_NUMBER, dps, min_prec, prec, @@ -946,6 +948,14 @@ def is_zero(self) -> bool: MATHICS3_COMPLEX_I = Complex(Integer0, Integer1) MATHICS3_COMPLEX_I_NEG = Complex(Integer0, IntegerM1) +# Numerical constants +# These constants are populated by the `Predefined` +# classes. See `mathics.builtin.numbers.constants` +NUMERICAL_CONSTANTS = { + Symbol("System`$MaxMachineNumber"): MachineReal(MAX_MACHINE_NUMBER), + Symbol("System`$MinMachineNumber"): MachineReal(MIN_MACHINE_NUMBER), +} + class String(Atom, BoxElementMixin): value: str diff --git a/mathics/core/number.py b/mathics/core/number.py index f30de3b92..01673f0ea 100644 --- a/mathics/core/number.py +++ b/mathics/core/number.py @@ -68,7 +68,9 @@ def _get_float_inf(value, evaluation) -> Optional[float]: return value.round_to_float(evaluation) -def get_precision(value, evaluation, show_messages=True) -> Optional[float]: +def get_precision( + value: BaseElement, evaluation, show_messages: bool = True +) -> Optional[float]: """ Returns the ``float`` in the interval [``$MinPrecision``, ``$MaxPrecision``] closest to ``value``. If ``value`` does not belongs to that interval, and ``show_messages`` is True, a Message warning is shown. diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 52659e775..f463e5f91 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -24,6 +24,7 @@ # This list is sorted in alphabetic order. SymbolAborted = Symbol("System`$Aborted") +SymbolAbs = Symbol("System`Abs") SymbolAccuracy = Symbol("System`Accuracy") SymbolAll = Symbol("System`All") SymbolAlternatives = Symbol("System`Alternatives") @@ -84,6 +85,7 @@ SymbolEquivalent = Symbol("System`Equivalent") SymbolEulerGamma = Symbol("System`EulerGamma") SymbolExactNumberQ = Symbol("System`ExactNumberQ") +SymbolExp = Symbol("System`Exp") SymbolExpandAll = Symbol("System`ExpandAll") SymbolExport = Symbol("System`Export") SymbolExportString = Symbol("System`ExportString") @@ -109,6 +111,7 @@ SymbolHoldForm = Symbol("System`HoldForm") SymbolHoldPattern = Symbol("System`HoldPattern") SymbolHue = Symbol("System`Hue") +SymbolI = Symbol("System`I") SymbolIf = Symbol("System`If") SymbolIm = Symbol("System`Im") SymbolImage = Symbol("System`Image") @@ -126,6 +129,7 @@ SymbolLess = Symbol("System`Less") SymbolLessEqual = Symbol("System`LessEqual") SymbolKey = Symbol("System`Key") +SymbolKhinchin = Symbol("System`Khinchin") SymbolLetterCharacter = Symbol("System`LetterCharacter") SymbolLine = Symbol("System`Line") SymbolLog = Symbol("System`Log") @@ -179,6 +183,7 @@ SymbolPi = Symbol("System`Pi") SymbolPiecewise = Symbol("System`Piecewise") SymbolPlot = Symbol("System`Plot") +SymbolPlus = Symbol("System`Plus") SymbolPoint = Symbol("System`Point") SymbolPower = Symbol("System`Power") SymbolPolygon = Symbol("System`Polygon") @@ -242,6 +247,7 @@ SymbolTan = Symbol("System`Tan") SymbolTanh = Symbol("System`Tanh") SymbolTeXForm = Symbol("System`TeXForm") +SymbolTimes = Symbol("System`Times") SymbolThrow = Symbol("System`Throw") SymbolThreshold = Symbol("System`Threshold") SymbolToString = Symbol("System`ToString") diff --git a/mathics/eval/arithmetic.py b/mathics/eval/arithmetic.py index 0b9564067..00976ac7b 100644 --- a/mathics/eval/arithmetic.py +++ b/mathics/eval/arithmetic.py @@ -14,12 +14,14 @@ import sympy from mathics.core.atoms import ( + NUMERICAL_CONSTANTS, Complex, Integer, Integer0, Integer1, Integer2, IntegerM1, + MachineReal, Number, Rational, RationalOneHalf, @@ -29,11 +31,24 @@ from mathics.core.convert.sympy import from_sympy from mathics.core.element import BaseElement, ElementsProperties from mathics.core.expression import Expression -from mathics.core.number import FP_MANTISA_BINARY_DIGITS, SpecialValueError, min_prec -from mathics.core.symbols import Symbol, SymbolPlus, SymbolPower, SymbolTimes -from mathics.core.systemsymbols import SymbolComplexInfinity, SymbolIndeterminate +from mathics.core.number import ( + FP_MANTISA_BINARY_DIGITS, + MAX_MACHINE_NUMBER, + MIN_MACHINE_NUMBER, + SpecialValueError, + min_prec, +) +from mathics.core.rules import Rule +from mathics.core.symbols import Atom, Symbol, SymbolPlus, SymbolPower, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolComplexInfinity, + SymbolI, + SymbolIndeterminate, + SymbolLog, +) RationalMOneHalf = Rational(-1, 2) +RealOne = Real(1.0) # This cache might not be used that much. @@ -374,3 +389,222 @@ def segregate_numbers_from_sorted_list( if not isinstance(element, Number): return list(elements[:pos]), list(elements[pos:]) return list(elements), [] + + +def test_arithmetic_expr(expr: BaseElement, only_real: bool = True) -> bool: + """ + Check if an expression `expr` is an arithmetic expression + composed only by numbers and arithmetic operations. + If only_real is set to True, then `I` is not considered a number. + """ + if isinstance(expr, (Integer, Rational, Real)): + return True + if expr in NUMERICAL_CONSTANTS: + return True + if isinstance(expr, Complex) or expr is SymbolI: + return not only_real + if isinstance(expr, Symbol): + return False + + head, elements = expr.head, expr.elements + + if head in (SymbolPlus, SymbolTimes): + return all(test_arithmetic_expr(term, only_real) for term in elements) + if expr.has_form("Exp", 1): + return test_arithmetic_expr(elements[0], only_real) + if head is SymbolLog: + if len(elements) > 2: + return False + if len(elements) == 2: + base = elements[0] + if not test_positive_arithmetic_expr(base): + return False + return test_arithmetic_expr(elements[-1], only_real) + if expr.has_form("Power", 2): + base, exponent = elements + if only_real: + if isinstance(exponent, Integer): + return test_arithmetic_expr(base) + return all(test_arithmetic_expr(item, only_real) for item in elements) + return False + + +def test_negative_arithmetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a negative value. + """ + if isinstance(expr, (Integer, Rational, Real)): + return expr.value < 0 + + expr = eval_multiply_numbers(IntegerM1, expr) + return test_positive_arithmetic_expr(expr) + + +def test_nonnegative_arithmetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a nonnegative number + """ + if not test_arithmetic_expr(expr): + return False + + if test_zero_arithmetic_expr(expr) or test_positive_arithmetic_expr(expr): + return True + + +def test_nonpositive_arithetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a nonnegative number + """ + if not test_arithmetic_expr(expr): + return False + + if test_zero_arithmetic_expr(expr) or test_negative_arithmetic_expr(expr): + return True + return False + + +def test_positive_arithmetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a positive value. + """ + if isinstance(expr, (Integer, Rational, Real)): + return expr.value > 0 + if expr in NUMERICAL_CONSTANTS: + return True + if isinstance(expr, Atom): + return False + + head, elements = expr.get_head(), expr.elements + + if head is SymbolPlus: + positive_nonpositive_terms = {True: [], False: []} + for term in elements: + positive_nonpositive_terms[test_positive_arithmetic_expr(term)].append(term) + + if len(positive_nonpositive_terms[False]) == 0: + return True + if len(positive_nonpositive_terms[True]) == 0: + return False + + pos, neg = ( + eval_add_numbers(*items) for items in positive_nonpositive_terms.values() + ) + if neg.is_zero: + return True + if not test_arithmetic_expr(neg): + return False + + total = eval_add_numbers(pos, neg) + # Check positivity of the evaluated expression + if isinstance(total, (Integer, Rational, Real)): + return total.value > 0 + if isinstance(total, Complex): + return False + if total.sameQ(expr): + return False + return test_positive_arithmetic_expr(total) + + if head is SymbolTimes: + nonpositive_factors = tuple( + (item for item in elements if not test_positive_arithmetic_expr(item)) + ) + if len(nonpositive_factors) == 0: + return True + evaluated_expr = eval_multiply_numbers(*nonpositive_factors) + if evaluated_expr.sameQ(expr): + return False + return test_positive_arithmetic_expr(evaluated_expr) + if expr.has_form("Power", 2): + base, exponent = elements + if isinstance(exponent, Integer) and exponent.value % 2 == 0: + return test_arithmetic_expr(base) + return test_arithmetic_expr(exponent) and test_positive_arithmetic_expr(base) + if expr.has_form("Exp", 1): + return test_arithmetic_expr(expr.elements[0], only_real=True) + if expr.has_form("Sqrt", 1): + return test_positive_arithmetic_expr(expr.elements[0]) + if head is SymbolLog: + if len(elements) > 2: + return False + if len(elements) == 2: + if not test_positive_arithmetic_expr(elements[0]): + return False + arg = elements[-1] + return test_positive_arithmetic_expr(eval_add_numbers(arg, IntegerM1)) + if expr.has_form("Abs", 1): + arg = elements[0] + return test_arithmetic_expr( + arg, only_real=False + ) and not test_zero_arithmetic_expr(arg) + if head.has_form("DirectedInfinity", 1): + return test_positive_arithmetic_expr(elements[0]) + + return False + + +def test_zero_arithmetic_expr(expr: BaseElement, numeric: bool = False) -> bool: + """ + return True if expr evaluates to a number compatible + with 0 + """ + + def is_numeric_zero(z: Number): + if isinstance(z, Complex): + if abs(z.real.value) + abs(z.imag.value) < 2.0e-10: + return True + if isinstance(z, Number): + if abs(z.value) < 1e-10: + return True + return False + + if expr.is_zero: + return True + if numeric: + if is_numeric_zero(expr): + return True + expr = to_inexact_value(expr) + if expr.has_form("Times", None): + if any( + test_zero_arithmetic_expr(element, numeric=numeric) + for element in expr.elements + ) and not any( + element.has_form("DirectedInfinity", None) for element in expr.elements + ): + return True + if expr.has_form("Power", 2): + base, exp = expr.elements + if test_zero_arithmetic_expr(base, numeric): + return test_nonnegative_arithmetic_expr(exp) + if base.has_form("DirectedInfinity", None): + return test_positive_arithmetic_expr(exp) + if expr.has_form("Plus", None): + result = eval_add_numbers(*expr.elements) + if numeric: + if isinstance(result, complex): + if abs(result.real.value) + abs(result.imag.value) < 2.0e-10: + return True + if isinstance(result, Number): + if abs(result.value) < 1e-10: + return True + return result.is_zero + return False + + +def to_inexact_value(expr: BaseElement) -> BaseElement: + """ + Converts an expression into an inexact expression. + Replaces numerical constants by their numerical approximation, + and then multiplies the expression by Real(1.) + """ + if expr.is_inexact(): + return expr + + if isinstance(expr, Expression): + for const, value in NUMERICAL_CONSTANTS.items(): + expr, success = expr.do_apply_rule(Rule(const, value)) + + return eval_multiply_numbers(RealOne, expr) diff --git a/mathics/eval/parts.py b/mathics/eval/parts.py index 3ecf356d1..61a1adf33 100644 --- a/mathics/eval/parts.py +++ b/mathics/eval/parts.py @@ -6,7 +6,7 @@ from typing import List -from mathics.core.atoms import Integer, Integer1 +from mathics.core.atoms import Integer from mathics.core.convert.expression import make_expression from mathics.core.element import BaseElement, BoxElementMixin from mathics.core.exceptions import ( @@ -20,11 +20,7 @@ from mathics.core.list import ListExpression from mathics.core.subexpression import SubExpression from mathics.core.symbols import Atom, Symbol, SymbolList -from mathics.core.systemsymbols import ( - SymbolDirectedInfinity, - SymbolInfinity, - SymbolNothing, -) +from mathics.core.systemsymbols import SymbolInfinity, SymbolNothing from mathics.eval.patterns import Matcher diff --git a/mathics/session.py b/mathics/session.py index 410d9c2b1..043106b34 100644 --- a/mathics/session.py +++ b/mathics/session.py @@ -100,3 +100,9 @@ def format_result(self, str_expression=None, timeout=None, form=None): if form is None: form = self.form return res.do_format(self.evaluation, form) + + def parse(self, str_expression): + """ + Just parse the expression + """ + return parse(self.definitions, MathicsSingleLineFeeder(str_expression)) diff --git a/test/builtin/arithmetic/test_abs.py b/test/builtin/arithmetic/test_abs.py index c523bf1d3..11b3da92b 100644 --- a/test/builtin/arithmetic/test_abs.py +++ b/test/builtin/arithmetic/test_abs.py @@ -42,8 +42,7 @@ def test_abs(str_expr, str_expected, msg): ("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[I^(2 Pi)]", "I^(2 Pi)", None), # ("Sign[4^(2 Pi I)]", "1", None), ], ) diff --git a/test/builtin/arithmetic/test_lowlevel_properties.py b/test/builtin/arithmetic/test_lowlevel_properties.py new file mode 100644 index 000000000..10cebfd06 --- /dev/null +++ b/test/builtin/arithmetic/test_lowlevel_properties.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.eval.arithmetic low level positivity tests +""" +from test.helper import session + +import pytest + +from mathics.eval.arithmetic import ( + test_arithmetic_expr as check_arithmetic, + test_positive_arithmetic_expr as check_positive, + test_zero_arithmetic_expr as check_zero, +) + + +@pytest.mark.parametrize( + ("str_expr", "expected", "msg"), + [ + ("I", False, None), + ("0", False, None), + ("1", True, None), + ("Pi", True, None), + ("a", False, None), + ("-Pi", False, None), + ("(-1)^2", True, None), + ("(-1)^3", False, None), + ("Sqrt[2]", True, None), + ("Sqrt[-2]", False, None), + ("(-2)^(1/2)", False, None), + ("(2)^(1/2)", True, None), + ("Exp[a]", False, None), + ("Exp[2.3]", True, None), + ("Log[1/2]", False, None), + ("Exp[I]", False, None), + ("Log[3]", True, None), + ("Log[I]", False, None), + ("Abs[a]", False, None), + ("Abs[0]", False, None), + ("Abs[1+3 I]", True, None), + ("Sin[Pi]", False, None), + ], +) +def test_positivity(str_expr, expected, msg): + expr = session.parse(str_expr) + if msg: + assert check_positive(expr) == expected, msg + else: + assert check_positive(expr) == expected + + +@pytest.mark.parametrize( + ("str_expr", "expected", "msg"), + [ + ("I", False, None), + ("0", True, None), + ("1", False, None), + ("Pi", False, None), + ("a", False, None), + ("a-a", True, None), + ("3-3.", True, None), + ("2-Sqrt[4]", True, None), + ("-Pi", False, None), + ("(-1)^2", False, None), + ("(-1)^3", False, None), + ("Sqrt[2]", False, None), + ("Sqrt[-2]", False, None), + ("(-2)^(1/2)", False, None), + ("(2)^(1/2)", False, None), + ("Exp[a]", False, None), + ("Exp[2.3]", False, None), + ("Log[1/2]", False, None), + ("Exp[I]", False, None), + ("Log[3]", False, None), + ("Log[I]", False, None), + ("Abs[a]", False, None), + ("Abs[0]", False, None), + ("Abs[1+3 I]", False, None), + # ("Sin[Pi]", False, None), + ], +) +def test_zero(str_expr, expected, msg): + expr = session.parse(str_expr) + if msg: + assert check_zero(expr) == expected, msg + else: + assert check_zero(expr) == expected From 4d0bcb9a860930e4bae9cc44b606de4643067537 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 27 May 2023 09:22:31 -0300 Subject: [PATCH 2/2] Adding pytests for from_sympy / to_sympy. (#828) This PR adds some basic tests to check the consistency in the behavior of `from_sympy`. When I was writing the tests, I found some issues in the behavior of these functions, and also some failures in the modularity. So, this PR * moves `mathics_to_sympy` and `sympy_to_mathics` dicts from `mathics.builtin` to `mathics.core.convert.sympy`. This allows for avoiding some local imports. * `to_numeric_sympy_args` was moved from `mathics.core.convert.expression` to `mathics.core.convert.sympy`, which again helps to reduce local imports. * moves the implementation of `Expression.to_sympy` and `Symbol.to_sympy` to `mathics.core.convert.sympy`. * Constant expressions like `MATHICS_*INFINITY` and numbers `MATHICS_COMPLEX_I` were included in `mathics.core.expression` to avoid local duplication. * Improves the implementation of `Symbol.to_sympy` by using a dictionary to translate some predefined symbols in SymPy, like `pi`, `E`, `GoldenRatio` or `I`. * adds pytests for `from_sympy` and `to_sympy`. In the code, it is also a proposal for a rewriting of `from_sympy` using dictionaries, which could help to get a clearer code. --- mathics/builtin/__init__.py | 4 +- mathics/builtin/arithmetic.py | 11 +- mathics/builtin/base.py | 4 +- mathics/builtin/specialfns/elliptic.py | 3 +- mathics/core/convert/expression.py | 17 -- mathics/core/convert/sympy.py | 231 +++++++++++++++--- mathics/core/expression.py | 30 +-- mathics/core/symbols.py | 18 +- mathics/doc/latex/Makefile | 2 +- mathics/docpipeline.py | 6 + mathics/eval/nevaluator.py | 3 +- .../convert/{mpmath.py => test_mpmath.py} | 0 test/core/convert/test_sympy.py | 144 +++++++++++ 13 files changed, 370 insertions(+), 103 deletions(-) rename test/core/convert/{mpmath.py => test_mpmath.py} (100%) create mode 100644 test/core/convert/test_sympy.py diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index d8cbc3ed4..f81819c23 100755 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -50,6 +50,8 @@ def add_builtins(new_builtins): + from mathics.core.convert.sympy import mathics_to_sympy, sympy_to_mathics + for var_name, builtin in new_builtins: name = builtin.get_name() if hasattr(builtin, "python_equivalent"): @@ -255,8 +257,6 @@ def name_is_builtin_symbol(module, name: str) -> Optional[type]: _builtins_list.append((instance.get_name(), instance)) builtins_by_module[module.__name__].append(instance) -mathics_to_sympy = {} # here we have: name -> sympy object -sympy_to_mathics = {} new_builtins = _builtins_list diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index 5a85bbe7e..d54f8f7bd 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -18,6 +18,7 @@ IterationFunction, Predefined, SympyFunction, + SympyObject, Test, ) from mathics.builtin.inference import evaluate_predicate, get_assumptions_list @@ -788,7 +789,7 @@ def eval_Element_alternatives( return Element(Expression(elems.head, *unknown), domain) -class I_(Predefined): +class I_(Predefined, SympyObject): """ :Imaginary unit:https://en.wikipedia.org/wiki/Imaginary_unit \ (:WMA:https://reference.wolfram.com/language/ref/I.html) @@ -805,9 +806,17 @@ class I_(Predefined): """ name = "I" + sympy_name = "I" + sympy_obj = sympy.I summary_text = "imaginary unit" python_equivalent = 1j + def is_constant(self) -> bool: + return True + + def to_sympy(self, symb, **kwargs): + return self.sympy_obj + def evaluate(self, evaluation: Evaluation): return Complex(Integer0, Integer1) diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 8eaccab79..ac3f1e1b7 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -19,10 +19,10 @@ String, ) from mathics.core.attributes import A_HOLD_ALL, A_NO_ATTRIBUTES, A_PROTECTED -from mathics.core.convert.expression import to_expression, to_numeric_sympy_args +from mathics.core.convert.expression import to_expression from mathics.core.convert.op import ascii_operator_to_symbol from mathics.core.convert.python import from_bool -from mathics.core.convert.sympy import from_sympy +from mathics.core.convert.sympy import from_sympy, to_numeric_sympy_args from mathics.core.definitions import Definition from mathics.core.evaluation import Evaluation from mathics.core.exceptions import MessageException diff --git a/mathics/builtin/specialfns/elliptic.py b/mathics/builtin/specialfns/elliptic.py index 0b9009462..928b4bd26 100644 --- a/mathics/builtin/specialfns/elliptic.py +++ b/mathics/builtin/specialfns/elliptic.py @@ -15,8 +15,7 @@ from mathics.builtin.base import SympyFunction from mathics.core.atoms import Integer from mathics.core.attributes import A_LISTABLE, A_NUMERIC_FUNCTION, A_PROTECTED -from mathics.core.convert.expression import to_numeric_sympy_args -from mathics.core.convert.sympy import from_sympy +from mathics.core.convert.sympy import from_sympy, to_numeric_sympy_args from mathics.eval.numerify import numerify diff --git a/mathics/core/convert/expression.py b/mathics/core/convert/expression.py index fc96fd537..d4360a3f2 100644 --- a/mathics/core/convert/expression.py +++ b/mathics/core/convert/expression.py @@ -99,23 +99,6 @@ def to_numeric_args(mathics_args: Type[BaseElement], evaluation) -> list: ) -def to_numeric_sympy_args(mathics_args: Type[BaseElement], evaluation) -> list: - """ - Convert Mathics arguments, such as the arguments in an evaluation - method a Python list that is sutiable for feeding as arguments - into SymPy. - - We make use of fast conversions for literals. - """ - if mathics_args.is_literal: - sympy_args = [mathics_args.value] - else: - args = numerify(mathics_args, evaluation).get_sequence() - sympy_args = [a.to_sympy() for a in args] - - return sympy_args - - expression_constructor_map = { SymbolList: lambda head, *args, **kwargs: ListExpression(*args, **kwargs) } diff --git a/mathics/core/convert/sympy.py b/mathics/core/convert/sympy.py index 980da1d69..843d679f3 100644 --- a/mathics/core/convert/sympy.py +++ b/mathics/core/convert/sympy.py @@ -5,17 +5,45 @@ Conversion to SymPy is handled directly in BaseElement descendants. """ -from typing import Optional +from typing import Optional, Type, Union import sympy +from sympy import Symbol as Sympy_Symbol, false as SympyFalse, true as SympyTrue + +# Import the singleton class +from sympy.core.numbers import S BasicSympy = sympy.Expr +from mathics.core.atoms import ( + MATHICS3_COMPLEX_I, + Complex, + Integer, + Integer0, + Integer1, + IntegerM1, + MachineReal, + Rational, + RationalOneHalf, + Real, + String, +) +from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.convert.matrix import matrix_data +from mathics.core.element import BaseElement +from mathics.core.expression import Expression +from mathics.core.expression_predefined import ( + MATHICS3_COMPLEX_INFINITY, + MATHICS3_INFINITY, + MATHICS3_NEG_INFINITY, +) +from mathics.core.list import ListExpression +from mathics.core.number import FP_MANTISA_BINARY_DIGITS from mathics.core.symbols import ( Symbol, SymbolFalse, + SymbolNull, SymbolPlus, SymbolPower, SymbolTimes, @@ -25,16 +53,20 @@ ) from mathics.core.systemsymbols import ( SymbolC, + SymbolCatalan, + SymbolE, SymbolEqual, + SymbolEulerGamma, SymbolFunction, + SymbolGoldenRatio, SymbolGreater, SymbolGreaterEqual, SymbolIndeterminate, - SymbolInfinity, SymbolLess, SymbolLessEqual, SymbolMatrixPower, SymbolO, + SymbolPi, SymbolPiecewise, SymbolSlot, SymbolUnequal, @@ -45,6 +77,36 @@ SymbolRootSum = Symbol("RootSum") +mathics_to_sympy = {} # here we have: name -> sympy object +sympy_to_mathics = {} + + +sympy_singleton_to_mathics = { + None: SymbolNull, + S.Catalan: SymbolCatalan, + S.ComplexInfinity: MATHICS3_COMPLEX_INFINITY, + S.EulerGamma: SymbolEulerGamma, + S.Exp1: SymbolE, + S.GoldenRatio: SymbolGoldenRatio, + S.Half: RationalOneHalf, + S.ImaginaryUnit: MATHICS3_COMPLEX_I, + S.Infinity: MATHICS3_INFINITY, + S.NaN: SymbolIndeterminate, + S.NegativeInfinity: MATHICS3_NEG_INFINITY, + S.NegativeOne: IntegerM1, + S.One: Integer1, + S.Pi: SymbolPi, + S.Zero: Integer0, + SympyFalse: SymbolFalse, + SympyTrue: SymbolTrue, +} + + +mathics_to_sympy_singleton = { + key: val for val, key in sympy_singleton_to_mathics.items() +} + + def is_Cn_expr(name) -> bool: if name.startswith(sympy_symbol_prefix) or name.startswith(sympy_slot_prefix): return False @@ -75,7 +137,6 @@ class SympyExpression(BasicSympy): def __new__(cls, *exprs): # sympy simplify may also recreate the object if simplification occurred # in the elements - from mathics.core.expression import Expression if all(isinstance(expr, BasicSympy) for expr in exprs): # called with SymPy arguments @@ -153,23 +214,125 @@ def eval(cls, n): pass -def from_sympy(expr): - from mathics.builtin import sympy_to_mathics - from mathics.core.atoms import ( - Complex, - Integer, - Integer0, - Integer1, - MachineReal, - Rational, - Real, - String, - ) - from mathics.core.convert.expression import to_expression, to_mathics_list - from mathics.core.expression import Expression - from mathics.core.list import ListExpression - from mathics.core.number import FP_MANTISA_BINARY_DIGITS - from mathics.core.symbols import Symbol, SymbolNull +def expression_to_sympy(expr: Expression, **kwargs): + """ + Convert `expr` to its sympy form. + """ + + if "convert_all_global_functions" in kwargs: + if len(expr.elements) > 0 and kwargs["convert_all_global_functions"]: + if expr.get_head_name().startswith("Global`"): + return expr._as_sympy_function(**kwargs) + + if "converted_functions" in kwargs: + functions = kwargs["converted_functions"] + if len(expr._elements) > 0 and expr.get_head_name() in functions: + sym_args = [element.to_sympy() for element in expr._elements] + if None in sym_args: + return None + func = sympy.Function(str(sympy_symbol_prefix + expr.get_head_name()))( + *sym_args + ) + return func + + lookup_name = expr.get_lookup_name() + builtin = mathics_to_sympy.get(lookup_name) + if builtin is not None: + sympy_expr = builtin.to_sympy(expr, **kwargs) + if sympy_expr is not None: + return sympy_expr + return SympyExpression(expr) + + +def symbol_to_sympy(symbol: Symbol, **kwargs) -> Sympy_Symbol: + """ + Convert `symbol` to its sympy form. + """ + + result = mathics_to_sympy_singleton.get(symbol, None) + if result is not None: + return result + + if symbol.sympy_dummy is not None: + return symbol.sympy_dummy + + builtin = mathics_to_sympy.get(symbol.name) + if builtin is None or not builtin.sympy_name or not builtin.is_constant(): # nopep8 + return Sympy_Symbol(sympy_symbol_prefix + symbol.name) + return builtin.to_sympy(symbol, **kwargs) + + +def to_numeric_sympy_args(mathics_args: Type[BaseElement], evaluation) -> list: + """ + Convert Mathics arguments, such as the arguments in an evaluation + method a Python list that is sutiable for feeding as arguments + into SymPy. + + We make use of fast conversions for literals. + """ + from mathics.eval.numerify import numerify + + if mathics_args.is_literal: + sympy_args = [mathics_args.value] + else: + args = numerify(mathics_args, evaluation).get_sequence() + sympy_args = [a.to_sympy() for a in args] + + return sympy_args + + +def from_sympy_matrix( + expr: Union[sympy.Matrix, sympy.ImmutableMatrix] +) -> ListExpression: + """ + Convert `expr` of the type sympy.Matrix or sympy.ImmutableMatrix to + a Mathics list. + """ + if len(expr.shape) == 2 and (expr.shape[1] == 1): + # This is a vector (only one column) + # Transpose and select first row to get result equivalent to Mathematica + return to_mathics_list(*expr.T.tolist()[0], elements_conversion_fn=from_sympy) + else: + return to_mathics_list(*expr.tolist(), elements_conversion_fn=from_sympy) + + +""" +sympy_conversion_by_type = { + complex: lambda expr: Complex(Real(expr.real), Real(expr.imag)), + int: lambda x: Integer(x), + float: lambda x: Real(x), + tuple: lambda expr: to_mathics_list(*expr, elements_conversion_fn=from_sympy), + list: lambda expr: to_mathics_list(*expr, elements_conversion_fn=from_sympy), + str: lambda x: String(x), + sympy.Matrix :from_sympy_matrix, + sympy.ImmutableMatrix :from_sympy_matrix, + sympy.MatPow: lambda expr: Expression( + SymbolMatrixPower, from_sympy(expr.base), from_sympy(expr.exp) + ), + SympyExpression: lambda expr: expr.expr, + SympyPrime: lambda expr: Expression(SymbolPrime, from_sympy(expr.args[0])), + sympy.RootSum: lambda expr: Expression(SymbolRootSum, from_sympy(expr.poly), from_sympy(expr.fun)), + sympy.Tuple: lambda expr: to_mathics_list(*expr, elements_conversion_fn=from_sympy), +} + +""" + +# def new_from_sympy(expr)->BaseElement: +# """ +# converts a SymPy object to a Mathics element. +# """ +# try: +# return sympy_singleton_to_mathics[expr] +# except (KeyError, TypeError): +# pass +# +# return sympy_conversion_by_type.get(type(expr), old_from_sympy)(expr) + + +def old_from_sympy(expr) -> BaseElement: + """ + converts a SymPy object to a Mathics element. + """ if isinstance(expr, (tuple, list)): return to_mathics_list(*expr, elements_conversion_fn=from_sympy) @@ -184,14 +347,7 @@ def from_sympy(expr): if expr is None: return SymbolNull if isinstance(expr, sympy.Matrix) or isinstance(expr, sympy.ImmutableMatrix): - if len(expr.shape) == 2 and (expr.shape[1] == 1): - # This is a vector (only one column) - # Transpose and select first row to get result equivalent to Mathematica - return to_mathics_list( - *expr.T.tolist()[0], elements_conversion_fn=from_sympy - ) - else: - return to_mathics_list(*expr.tolist(), elements_conversion_fn=from_sympy) + return from_sympy_matrix(expr) if isinstance(expr, sympy.MatPow): return Expression( SymbolMatrixPower, from_sympy(expr.base), from_sympy(expr.exp) @@ -218,23 +374,23 @@ def from_sympy(expr): if builtin is not None: name = builtin.get_name() return Symbol(name) - elif isinstance( - expr, (sympy.core.numbers.Infinity, sympy.core.numbers.ComplexInfinity) - ): - return Symbol(expr.__class__.__name__) + elif isinstance(expr, sympy.core.numbers.Infinity): + return MATHICS3_INFINITY + elif isinstance(expr, sympy.core.numbers.ComplexInfinity): + return MATHICS3_COMPLEX_INFINITY elif isinstance(expr, sympy.core.numbers.NegativeInfinity): - return Expression(SymbolTimes, Integer(-1), SymbolInfinity) + return MATHICS3_NEG_INFINITY elif isinstance(expr, sympy.core.numbers.ImaginaryUnit): - return Complex(Integer0, Integer1) + return MATHICS3_COMPLEX_I elif isinstance(expr, sympy.Integer): return Integer(int(expr)) elif isinstance(expr, sympy.Rational): numerator, denominator = map(int, expr.as_numer_denom()) if denominator == 0: if numerator > 0: - return SymbolInfinity + return MATHICS3_INFINITY elif numerator < 0: - return Expression(SymbolTimes, Integer(-1), SymbolInfinity) + return MATHICS3_NEG_INFINITY else: assert numerator == 0 return SymbolIndeterminate @@ -408,3 +564,6 @@ def from_sympy(expr): expr, str(expr.__class__) ) ) + + +from_sympy = old_from_sympy diff --git a/mathics/core/expression.py b/mathics/core/expression.py index fec9820b9..74b4c6b9e 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -26,7 +26,6 @@ attribute_string_to_number, ) from mathics.core.convert.python import from_python -from mathics.core.convert.sympy import SympyExpression, sympy_symbol_prefix from mathics.core.element import ElementsProperties, EvalMixin, ensure_context from mathics.core.evaluation import Evaluation from mathics.core.interrupt import ReturnInterrupt @@ -258,6 +257,8 @@ def __str__(self) -> str: ) def _as_sympy_function(self, **kwargs) -> sympy.Function: + from mathics.core.convert.sympy import sympy_symbol_prefix + sym_args = [element.to_sympy(**kwargs) for element in self._elements] if None in sym_args: @@ -1461,32 +1462,9 @@ def to_python(self, *args, **kwargs): return self def to_sympy(self, **kwargs): - from mathics.builtin import mathics_to_sympy - - if "convert_all_global_functions" in kwargs: - if len(self.elements) > 0 and kwargs["convert_all_global_functions"]: - if self.get_head_name().startswith("Global`"): - return self._as_sympy_function(**kwargs) - - if "converted_functions" in kwargs: - functions = kwargs["converted_functions"] - if len(self._elements) > 0 and self.get_head_name() in functions: - sym_args = [element.to_sympy() for element in self._elements] - if None in sym_args: - return None - func = sympy.Function(str(sympy_symbol_prefix + self.get_head_name()))( - *sym_args - ) - return func - - lookup_name = self.get_lookup_name() - builtin = mathics_to_sympy.get(lookup_name) - if builtin is not None: - sympy_expr = builtin.to_sympy(self, **kwargs) - if sympy_expr is not None: - return sympy_expr + from mathics.core.convert.sympy import expression_to_sympy - return SympyExpression(self) + return expression_to_sympy(self, **kwargs) def process_style_box(self, options): if self.has_form("StyleBox", 1, None): diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index cdfd6bf9d..d49a4e031 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -4,8 +4,6 @@ import time from typing import Any, FrozenSet, List, Optional, Tuple -import sympy - from mathics.core.element import ( BaseElement, EvalMixin, @@ -624,19 +622,9 @@ def to_python(self, *args, python_form: bool = False, **kwargs): return self.name def to_sympy(self, **kwargs): - from mathics.builtin import mathics_to_sympy - - if self.sympy_dummy is not None: - return self.sympy_dummy - - builtin = mathics_to_sympy.get(self.name) - if ( - builtin is None - or not builtin.sympy_name - or not builtin.is_constant() # nopep8 - ): - return sympy.Symbol(sympy_symbol_prefix + self.name) - return builtin.to_sympy(self, **kwargs) + from mathics.core.convert.sympy import symbol_to_sympy + + return symbol_to_sympy(self, **kwargs) class SymbolConstant(Symbol): diff --git a/mathics/doc/latex/Makefile b/mathics/doc/latex/Makefile index a66dc37bf..9f4369ad3 100644 --- a/mathics/doc/latex/Makefile +++ b/mathics/doc/latex/Makefile @@ -10,7 +10,7 @@ BASH ?= /bin/bash DOCTEST_LATEX_DATA_PCL ?= $(HOME)/.local/var/mathics/doctest_latex_data.pcl # Variable indicating Mathics3 Modules you have available on your system, in latex2doc option format -MATHICS3_MODULE_OPTION ?= --load-module pymathics.graph,pymathics.natlang +MATHICS3_MODULE_OPTION ?= # --load-module pymathics.graph,pymathics.natlang #: Default target: Make everything all doc texdoc: mathics.pdf diff --git a/mathics/docpipeline.py b/mathics/docpipeline.py index 81c32f69d..0696f4e59 100644 --- a/mathics/docpipeline.py +++ b/mathics/docpipeline.py @@ -449,6 +449,12 @@ def save_doctest_data(output_data: Dict[tuple, dict]): should_be_readable=False, create_parent=True ) print(f"Writing internal document data to {doctest_latex_data_path}") + i = 0 + for key in output_data: + i = i + 1 + print(key, output_data[key]) + if i > 9: + break with open(doctest_latex_data_path, "wb") as output_file: pickle.dump(output_data, output_file, 4) diff --git a/mathics/eval/nevaluator.py b/mathics/eval/nevaluator.py index 17e4f8d39..79d872507 100644 --- a/mathics/eval/nevaluator.py +++ b/mathics/eval/nevaluator.py @@ -15,7 +15,6 @@ from mathics.core.atoms import Number from mathics.core.attributes import A_N_HOLD_ALL, A_N_HOLD_FIRST, A_N_HOLD_REST -from mathics.core.convert.sympy import from_sympy from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression @@ -50,6 +49,8 @@ def eval_NValues( stored in ``evaluation.definitions``. If ``prec`` can not be evaluated as a number, returns None, otherwise, returns an expression. """ + from mathics.core.convert.sympy import from_sympy + # The first step is to determine the precision goal try: # Here ``get_precision`` is called with ``show_messages`` diff --git a/test/core/convert/mpmath.py b/test/core/convert/test_mpmath.py similarity index 100% rename from test/core/convert/mpmath.py rename to test/core/convert/test_mpmath.py diff --git a/test/core/convert/test_sympy.py b/test/core/convert/test_sympy.py new file mode 100644 index 000000000..319274366 --- /dev/null +++ b/test/core/convert/test_sympy.py @@ -0,0 +1,144 @@ +import test.helper + +import pytest +from sympy import Float as SympyFloat + +from mathics.core.atoms import ( + MATHICS3_COMPLEX_I, + Complex, + Integer, + Integer0, + Integer1, + Integer2, + Integer3, + IntegerM1, + MachineReal, + PrecisionReal, + Rational, + RationalOneHalf, + Real, + String, +) +from mathics.core.convert.sympy import from_sympy, sympy_singleton_to_mathics +from mathics.core.expression import Expression +from mathics.core.expression_predefined import ( + MATHICS3_COMPLEX_INFINITY, + MATHICS3_INFINITY, +) +from mathics.core.symbols import ( + Symbol, + SymbolFalse, + SymbolNull, + SymbolPlus, + SymbolPower, + SymbolTimes, + SymbolTrue, +) +from mathics.core.systemsymbols import ( + SymbolAnd, + SymbolComplexInfinity, + SymbolDirectedInfinity, + SymbolE, + SymbolExp, + SymbolI, + SymbolIndeterminate, + SymbolInfinity, + SymbolPi, + SymbolSin, +) + +Symbol_a = Symbol("Global`a") +Symbol_b = Symbol("Global`b") +Symbol_x = Symbol("Global`x") +Symbol_y = Symbol("Global`y") +Symbol_F = Symbol("Global`F") +Symbol_G = Symbol("Global`G") + + +@pytest.mark.parametrize( + ("expr",), + [ + (Symbol_x,), + (Expression(Symbol_F, Symbol_x),), + (SymbolPi,), + (SymbolTrue,), + (SymbolFalse,), + (Integer1,), + (Integer(37),), + (Rational(1, 5),), + (Real(1.2),), + (Real(SympyFloat(1.2, 10)),), + (Complex(Real(2.0), Real(3.0)),), + (Expression(Symbol_F, Symbol_x, SymbolPi),), + (Expression(Symbol_G, Expression(Symbol_F, Symbol_x, SymbolPi)),), + (Expression(SymbolPlus, Integer3, Symbol_x, Symbol_y),), + ], +) +def test_from_to_sympy_invariant(expr): + """ + Check if the conversion back and forward is consistent. + """ + result_sympy = expr.to_sympy() + back_to_mathics = from_sympy(result_sympy) + print([expr, result_sympy, back_to_mathics]) + assert expr.sameQ(back_to_mathics) + + +@pytest.mark.parametrize( + ("expr", "result", "msg"), + [ + ( + Expression(SymbolExp, Expression(SymbolTimes, SymbolI, SymbolPi)), + IntegerM1, + None, + ), + ( + Expression( + SymbolPower, SymbolE, Expression(SymbolTimes, SymbolI, SymbolPi) + ), + IntegerM1, + None, + ), + (Expression(SymbolSin, SymbolPi), Integer0, None), + (Expression(SymbolPlus, Integer1, Integer2), Integer3, None), + (String("Hola"), SymbolNull, None), + (Rational(1, 0), MATHICS3_COMPLEX_INFINITY, None), + (MATHICS3_COMPLEX_I, MATHICS3_COMPLEX_I, None), + ( + SymbolI, + MATHICS3_COMPLEX_I, + ( + "System`I evaluates to Complex[0,1] in the back and forward conversion. " + "This prevents an infinite recursion in evaluation" + ), + ), + # (Integer3**Rational(-1, 2), Rational(Integer1, Integer3)* (Integer3 ** (RationalOneHalf)), None ), + ], +) +def test_from_to_sympy_change(expr, result, msg): + """ + Check if the conversion back and forward produces + the expected evaluation. + """ + print([expr, result]) + if msg: + assert result.sameQ(from_sympy(expr.to_sympy())), msg + else: + assert result.sameQ(from_sympy(expr.to_sympy())) + + +def test_convert_sympy_singletons(): + """ + Check conversions between singleton symbols in + SymPy and Mathics Symbols. + """ + for key, val in sympy_singleton_to_mathics.items(): + print("equivalence", key, "<->", val) + if key is not None: + res = from_sympy(key) + print(" -> ", res) + assert from_sympy(key).sameQ(val) + + res = val.to_sympy() + print(res, " <- ") + assert res is key