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