diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index 265e90e8d..82b380c2c 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -802,8 +802,13 @@ def to_sympy(self, expr, **kwargs): try: e_kwargs = kwargs.copy() e_kwargs["convert_all_global_functions"] = True + e_kwargs["dummies"] = e_kwargs.get("dummies", set()).union((index,)) e = expr.elements[0].to_sympy(**e_kwargs) - i = index.elements[0].to_sympy(**kwargs) + e_kwargs["convert_all_global_functions"] = kwargs.get( + "convert_all_global_functions", False + ) + + i = index.elements[0].to_sympy(**e_kwargs) start = index.elements[1].to_sympy(**kwargs) stop = index.elements[2].to_sympy(**kwargs) @@ -1045,6 +1050,7 @@ def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: index = expr.elements[1] arg_kwargs = kwargs.copy() arg_kwargs["convert_all_global_functions"] = True + arg_kwargs["dummies"] = kwargs.get("dummies", set()).union((index,)) f_sympy = expr.elements[0].to_sympy(**arg_kwargs) if f_sympy is None: return @@ -1052,16 +1058,19 @@ def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: evaluation = kwargs.get("evaluation", None) # Handle summation parameters: variable, min, max - var_min_max = index.elements[:3] - bounds = [expr.to_sympy(**kwargs) for expr in var_min_max] + arg_kwargs["convert_all_global_functions"] = kwargs.get( + "convert_all_global_functions", False + ) + var_min_max = index.elements[:3] + bounds = [expr.to_sympy(**arg_kwargs) for expr in var_min_max] if evaluation: # Min and max might be Mathics expressions. If so, evaluate them. for i in (1, 2): min_max_expr = var_min_max[i] if not isinstance(expr, Symbol): min_max_expr_eval = min_max_expr.evaluate(evaluation) - value = min_max_expr_eval.to_sympy(**kwargs) + value = min_max_expr_eval.to_sympy(**arg_kwargs) bounds[i] = value # FIXME: The below tests on SympyExpression, but really the @@ -1075,7 +1084,7 @@ def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: # If we have integer bounds, we'll use Mathics's iterator Sum # (which is Plus) - if all( + if evaluation and all( (hasattr(i, "is_integer") and i.is_integer) or (hasattr(i, "is_finite") and i.is_finite and i.is_constant()) for i in bounds[1:] diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index a734822c3..96f02123b 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -57,6 +57,7 @@ SymbolPower, SymbolTimes, SymbolTrue, + sympy_name, ) from mathics.core.systemsymbols import ( SymbolAnd, @@ -528,7 +529,7 @@ def to_sympy(self, expr, **kwargs): return func = exprs[1].elements[0] - sym_func = sympy.Function(str(SYMPY_SYMBOL_PREFIX + func.__str__()))(*sym_args) + sym_func = sympy.Function(sympy_name(func))(*sym_args) counts = [element.get_int_value() for element in exprs[2].elements] if None in counts: diff --git a/mathics/core/convert/sympy.py b/mathics/core/convert/sympy.py index 80344ccc3..7fb96555c 100644 --- a/mathics/core/convert/sympy.py +++ b/mathics/core/convert/sympy.py @@ -7,7 +7,12 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, Union, cast import sympy -from sympy import Symbol as Sympy_Symbol, false as SympyFalse, true as SympyTrue +from sympy import ( + Dummy as Sympy_Dummy, + Symbol as Sympy_Symbol, + false as SympyFalse, + true as SympyTrue, +) from sympy.core.singleton import S from mathics.core.atoms import ( @@ -108,6 +113,16 @@ } +def sympy_decode_mathics_symbol_name(name: str) -> str: + """ + Remove the Prefix for Mathics symbols + and restore the context separator character. + """ + if name.startswith(SYMPY_SYMBOL_PREFIX): + return name[len(SYMPY_SYMBOL_PREFIX) :].replace("_", "`") + return name + + def is_Cn_expr(name: str) -> bool: """Check if name is of the form {prefix}Cnnn""" if name.startswith(SYMPY_SYMBOL_PREFIX) or name.startswith(SYMPY_SLOT_PREFIX): @@ -152,7 +167,9 @@ def __new__(cls, *exprs, **kwargs): if kwargs.get("convert_functions_for_polynomialq", False): sympy_elements = [] else: - sympy_elements = [element.to_sympy() for element in expr.elements] + sympy_elements = [ + element.to_sympy(**kwargs) for element in expr.elements + ] if sympy_head is None or None in sympy_elements: return None obj = super().__new__(cls, sympy_head, *sympy_elements) @@ -229,7 +246,6 @@ def expression_to_sympy(expr: Expression, **kwargs): """ Convert `expr` to its sympy form. """ - if len(expr.elements) > 0: head_name = expr.get_head_name() if head_name.startswith("Global`"): @@ -243,6 +259,7 @@ def expression_to_sympy(expr: Expression, **kwargs): 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: @@ -261,11 +278,10 @@ def symbol_to_sympy(symbol: Symbol, **kwargs) -> Sympy_Symbol: 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 + if symbol in kwargs.get("dummies", {}): + return Sympy_Dummy(sympy_name(symbol)) return Sympy_Symbol(sympy_name(symbol)) return builtin.to_sympy(symbol, **kwargs) @@ -342,7 +358,6 @@ def old_from_sympy(expr) -> BaseElement: """ converts a SymPy object to a Mathics3 element. """ - if isinstance(expr, (tuple, list)): return to_mathics_list(*expr, elements_conversion_fn=from_sympy) if isinstance(expr, int): @@ -366,13 +381,16 @@ def old_from_sympy(expr) -> BaseElement: if expr.is_Symbol: name = str(expr) if isinstance(expr, sympy.Dummy): - name = f"sympy`dummy`Dummy${expr.dummy_index}" # type: ignore[attr-defined] + name = name[1:] + if "_" not in name: + name = f"sympy`dummy`Dummy${expr.dummy_index}" # type: ignore[attr-defined] + else: + name = sympy_decode_mathics_symbol_name(name) # Probably, this should be the value attribute - return Symbol(name, sympy_dummy=expr) + return Symbol(name) if is_Cn_expr(name): return Expression(SymbolC, Integer(int(name[1:]))) - if name.startswith(SYMPY_SYMBOL_PREFIX): - name = name[len(SYMPY_SYMBOL_PREFIX) :] + name = sympy_decode_mathics_symbol_name(name) if name.startswith(SYMPY_SLOT_PREFIX): index = int(name[len(SYMPY_SLOT_PREFIX) :]) return Expression(SymbolSlot, Integer(index)) @@ -411,7 +429,8 @@ def old_from_sympy(expr) -> BaseElement: if isinstance(expr, sympy.core.numbers.NaN): return SymbolIndeterminate if isinstance(expr, sympy.core.function.FunctionClass): - return Symbol(str(expr)) + name = str(expr).replace("_", "`") + return Symbol(name) if expr is sympy.true: return SymbolTrue if expr is sympy.false: @@ -537,8 +556,7 @@ def old_from_sympy(expr) -> BaseElement: Expression(Symbol("C"), Integer(int(name[1:]))), *[from_sympy(arg) for arg in expr.args], ) - if name.startswith(SYMPY_SYMBOL_PREFIX): - name = name[len(SYMPY_SYMBOL_PREFIX) :] + name = sympy_decode_mathics_symbol_name(name) args = [from_sympy(arg) for arg in expr.args] builtin = sympy_to_mathics.get(name) if builtin is not None: diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index db9135d87..2f85a22b1 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -126,7 +126,7 @@ def sympy_strip_context(name) -> str: produce invalid code. In a next round, we would like to use another character for split contexts in sympy variables. """ - return strip_context(name) + return name.split("_")[-1] # system_symbols_dict({'SomeSymbol': ...}) -> {Symbol('System`SomeSymbol'): ...} @@ -342,7 +342,6 @@ class Symbol(Atom, NumericOperators, EvalMixin): name: str hash: int - sympy_dummy: Any _short_name: str # Dictionary of Symbols defined so far. @@ -355,7 +354,7 @@ class Symbol(Atom, NumericOperators, EvalMixin): # __new__ instead of __init__ is used here because we want # to return the same object for a given "name" value. - def __new__(cls, name: str, sympy_dummy=None): + def __new__(cls, name: str): """ Allocate an object ensuring that for a given ``name`` and ``cls`` we get back the same object, id(object) is the same and its object.__hash__() is the same. @@ -383,20 +382,7 @@ def __new__(cls, name: str, sympy_dummy=None): # Python objects, so we include the class in the # event that different objects have the same Python value. # For example, this can happen with String constants. - self.hash = hash((cls, name)) - - # TODO: revise how we convert sympy.Dummy - # symbols. - # - # In some cases, SymPy returns a sympy.Dummy - # object. It is converted to Mathics as a - # Symbol. However, we probably should have - # a different class for this kind of symbols. - # Also, sympy_dummy should be stored as the - # value attribute. - self.sympy_dummy = sympy_dummy - self._short_name = strip_context(name) return self @@ -405,7 +391,7 @@ def __eq__(self, other) -> bool: return self is other def __getnewargs__(self): - return (self.name, self.sympy_dummy) + return (self.name,) def __hash__(self) -> int: """ @@ -703,6 +689,9 @@ def __new__(cls, name, value): self.hash = hash((cls, name)) return self + def __getnewargs__(self): + return (self.name, self._value) + @property def is_literal(self) -> bool: """ @@ -761,7 +750,7 @@ def symbol_set(*symbols: Symbol) -> FrozenSet[Symbol]: def sympy_name(mathics_symbol: Symbol): """Convert a mathics symbol name into a sympy symbol name""" - return SYMPY_SYMBOL_PREFIX + mathics_symbol.name + return SYMPY_SYMBOL_PREFIX + mathics_symbol.name.replace("`", "_") # Symbols used in this module. diff --git a/mathics/eval/drawing/plot_compile.py b/mathics/eval/drawing/plot_compile.py index 1cfaf2066..04b92bca2 100644 --- a/mathics/eval/drawing/plot_compile.py +++ b/mathics/eval/drawing/plot_compile.py @@ -18,8 +18,8 @@ import scipy import sympy -from mathics.core.convert.sympy import SympyExpression -from mathics.core.symbols import strip_context +from mathics.core.convert.sympy import SympyExpression, mathics_to_sympy +from mathics.core.symbols import sympy_strip_context from mathics.core.util import print_expression_tree, print_sympy_tree @@ -69,7 +69,8 @@ def plot_compile(evaluation, expr, names, debug=0): # Strip symbols in sympy expression of context. subs = { - sym: sympy.Symbol(strip_context(str(sym))) for sym in sympy_expr.free_symbols + sym: sympy.Symbol(sympy_strip_context(str(sym))) + for sym in sympy_expr.free_symbols } sympy_expr = sympy_expr.subs(subs) diff --git a/test/core/test_sympy_python_convert.py b/test/core/test_sympy_python_convert.py index 31f4fd2dc..e5f9fbb93 100644 --- a/test/core/test_sympy_python_convert.py +++ b/test/core/test_sympy_python_convert.py @@ -25,6 +25,7 @@ SYMPY_SYMBOL_PREFIX, Symbol, SymbolPlus, + sympy_name, ) from mathics.core.systemsymbols import ( SymbolD, @@ -36,6 +37,12 @@ SymbolSlot, ) +Symbol_f = Symbol("Global`f") +Symbol_x = Symbol("Global`x") +Symbol_y = Symbol("Global`y") +Symbol_z = Symbol("Global`z") +Symbol_Mathics_User_x = Symbol("Mathics`User`x") + class SympyConvert(unittest.TestCase): def compare_to_sympy(self, mathics_expr, sympy_expr, **kwargs): @@ -49,11 +56,17 @@ def compare(self, mathics_expr, sympy_expr, **kwargs): self.compare_to_mathics(mathics_expr, sympy_expr) def testSymbol(self): - self.compare(Symbol("Global`x"), sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x")) + self.compare(Symbol_x, sympy.Symbol(sympy_name(Symbol_x))) self.compare( - Symbol("_Mathics_User_x"), - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}System`_Mathics_User_x"), + Symbol_Mathics_User_x, + sympy.Symbol(sympy_name(Symbol_Mathics_User_x)), ) + # Sympy symbols without prefix are mapped to symbols in + # System` context: + self.compare_to_mathics(Symbol("x"), sympy.Symbol("x")) + # Notice that a sympy Symbol named "x" is converted + # to the Mathics symbol "System`x", and then, when converted + # back to sympy, goes to sympy.Symbol("_uSystem_x"). def testReal(self): self.compare(Real("1.0"), sympy.Float("1.0")) @@ -87,25 +100,25 @@ def testString(self): def testAdd(self): self.compare( - Expression(SymbolPlus, Integer1, Symbol("Global`x")), - sympy.Add(sympy.Integer(1), sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x")), + Expression(SymbolPlus, Integer1, Symbol_x), + sympy.Add(sympy.Integer(1), sympy.Symbol(sympy_name(Symbol_x))), ) def testIntegrate(self): self.compare( - Expression(SymbolIntegrate, Symbol("Global`x"), Symbol("Global`y")), + Expression(SymbolIntegrate, Symbol_x, Symbol_y), sympy.Integral( - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x"), - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`y"), + sympy.Symbol(sympy_name(Symbol_x)), + sympy.Symbol(sympy_name(Symbol_y)), ), ) def testDerivative(self): self.compare( - Expression(SymbolD, Symbol("Global`x"), Symbol("Global`y")), + Expression(SymbolD, Symbol_x, Symbol_y), sympy.Derivative( - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x"), - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`y"), + sympy.Symbol(sympy_name(Symbol_x)), + sympy.Symbol(sympy_name(Symbol_y)), ), ) @@ -114,17 +127,15 @@ def testDerivative2(self): head = Expression( Expression(SymbolDerivative, Integer1, Integer0), - Symbol("Global`f"), + Symbol_f, ) - expr = Expression(head, Symbol("Global`x"), Symbol("Global`y")) + expr = Expression(head, Symbol_x, Symbol_y) - sfxy = sympy.Function(str(f"{SYMPY_SYMBOL_PREFIX}Global`f"))( - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x"), - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`y"), - ) - sym_expr = sympy.Derivative( - sfxy, sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x") + sfxy = sympy.Function(sympy_name(Symbol_f))( + sympy.Symbol(sympy_name(Symbol_x)), + sympy.Symbol(sympy_name(Symbol_y)), ) + sym_expr = sympy.Derivative(sfxy, sympy.Symbol(sympy_name(Symbol_x))) self.compare_to_sympy(expr, sym_expr, **kwargs) # compare_to_mathics fails because Derivative becomes D (which then evaluates to Derivative) @@ -132,29 +143,27 @@ def testDerivative2(self): def testConvertedFunctions(self): kwargs = {"converted_functions": set(["Global`f"])} - marg1 = Expression(Symbol("Global`f"), Symbol("Global`x")) - sarg1 = sympy.Function(str(f"{SYMPY_SYMBOL_PREFIX}Global`f"))( - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x") - ) + marg1 = Expression(Symbol_f, Symbol_x) + sarg1 = sympy.Function(sympy_name(Symbol_f))(sympy.Symbol(sympy_name(Symbol_x))) self.compare(marg1, sarg1, **kwargs) - marg2 = Expression(Symbol("Global`f"), Symbol("Global`x"), Symbol("Global`y")) - sarg2 = sympy.Function(str(f"{SYMPY_SYMBOL_PREFIX}Global`f"))( - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x"), - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`y"), + marg2 = Expression(Symbol_f, Symbol_x, Symbol_y) + sarg2 = sympy.Function(sympy_name(Symbol_f))( + sympy.Symbol(sympy_name(Symbol_x)), + sympy.Symbol(sympy_name(Symbol_y)), ) self.compare(marg2, sarg2, **kwargs) self.compare( - Expression(SymbolD, marg2, Symbol("Global`x")), - sympy.Derivative(sarg2, sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x")), + Expression(SymbolD, marg2, Symbol_x), + sympy.Derivative(sarg2, sympy.Symbol(sympy_name(Symbol_x))), **kwargs, ) def testExpression(self): self.compare( - Expression(SymbolSin, Symbol("Global`x")), - sympy.sin(sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x")), + Expression(SymbolSin, Symbol_x), + sympy.sin(sympy.Symbol(sympy_name(Symbol_x))), ) def testConstant(self): @@ -163,14 +172,14 @@ def testConstant(self): def testGamma(self): self.compare( - Expression(SymbolGamma, Symbol("Global`z")), - sympy.gamma(sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`z")), + Expression(SymbolGamma, Symbol_z), + sympy.gamma(sympy.Symbol(sympy_name(Symbol_z))), ) self.compare( - Expression(SymbolGamma, Symbol("Global`z"), Symbol("Global`x")), + Expression(SymbolGamma, Symbol_z, Symbol_x), sympy.uppergamma( - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`z"), - sympy.Symbol(f"{SYMPY_SYMBOL_PREFIX}Global`x"), + sympy.Symbol(sympy_name(Symbol_z)), + sympy.Symbol(sympy_name(Symbol_x)), ), )