diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py
index 6fb5a8873..63752d4ab 100644
--- a/mathics/builtin/arithfns/basic.py
+++ b/mathics/builtin/arithfns/basic.py
@@ -6,6 +6,8 @@
"""
+import sympy
+
from mathics.builtin.arithmetic import _MPMathFunction, create_infix
from mathics.builtin.base import BinaryOperator, Builtin, PrefixOperator, SympyFunction
from mathics.core.atoms import (
@@ -38,7 +40,6 @@
Symbol,
SymbolDivide,
SymbolHoldForm,
- SymbolNull,
SymbolPower,
SymbolTimes,
)
@@ -49,10 +50,17 @@
SymbolInfix,
SymbolLeft,
SymbolMinus,
+ SymbolOverflow,
SymbolPattern,
- SymbolSequence,
)
-from mathics.eval.arithmetic import eval_Plus, eval_Times
+from mathics.eval.arithmetic import (
+ associate_powers,
+ eval_Exponential,
+ eval_Plus,
+ eval_Power_inexact,
+ eval_Power_number,
+ eval_Times,
+)
from mathics.eval.nevaluator import eval_N
from mathics.eval.numerify import numerify
@@ -520,6 +528,8 @@ class Power(BinaryOperator, _MPMathFunction):
rules = {
"Power[]": "1",
"Power[x_]": "x",
+ "Power[I,-1]": "-I",
+ "Power[-1, 1/2]": "I",
}
summary_text = "exponentiate"
@@ -528,15 +538,15 @@ class Power(BinaryOperator, _MPMathFunction):
# Remember to up sympy doc link when this is corrected
sympy_name = "Pow"
+ def eval_exp(self, x, evaluation):
+ "Power[E, x]"
+ return eval_Exponential(x)
+
def eval_check(self, x, y, evaluation):
"Power[x_, y_]"
-
- # Power uses _MPMathFunction but does some error checking first
- if isinstance(x, Number) and x.is_zero:
- if isinstance(y, Number):
- y_err = y
- else:
- y_err = eval_N(y, evaluation)
+ # if x is zero
+ if x.is_zero:
+ y_err = y if isinstance(y, Number) else eval_N(y, evaluation)
if isinstance(y_err, Number):
py_y = y_err.round_to_float(permit_complex=True).real
if py_y > 0:
@@ -550,17 +560,47 @@ def eval_check(self, x, y, evaluation):
evaluation.message(
"Power", "infy", Expression(SymbolPower, x, y_err)
)
- return SymbolComplexInfinity
- if isinstance(x, Complex) and x.real.is_zero:
- yhalf = Expression(SymbolTimes, y, RationalOneHalf)
- factor = self.eval(Expression(SymbolSequence, x.imag, y), evaluation)
- return Expression(
- SymbolTimes, factor, Expression(SymbolPower, IntegerM1, yhalf)
- )
-
- result = self.eval(Expression(SymbolSequence, x, y), evaluation)
- if result is None or result != SymbolNull:
- return result
+ return SymbolComplexInfinity
+
+ # If x and y are inexact numbers, use the numerical function
+
+ if x.is_inexact() and y.is_inexact():
+ try:
+ return eval_Power_inexact(x, y)
+ except OverflowError:
+ evaluation.message("General", "ovfl")
+ return Expression(SymbolOverflow)
+
+ # Tries to associate powers a^b^c-> a^(b*c)
+ assoc = associate_powers(x, y)
+ if not assoc.has_form("Power", 2):
+ return assoc
+
+ assoc = numerify(assoc, evaluation)
+ x, y = assoc.elements
+ # If x and y are numbers
+ if isinstance(x, Number) and isinstance(y, Number):
+ try:
+ return eval_Power_number(x, y)
+ except OverflowError:
+ evaluation.message("General", "ovfl")
+ return Expression(SymbolOverflow)
+
+ # if x or y are inexact, leave the expression
+ # as it is:
+ if x.is_inexact() or y.is_inexact():
+ return assoc
+
+ # Finally, try to convert to sympy
+ base_sp, exp_sp = x.to_sympy(), y.to_sympy()
+ if base_sp is None or exp_sp is None:
+ # If base or exp can not be converted to sympy,
+ # returns the result of applying the associative
+ # rule.
+ return assoc
+
+ result = from_sympy(sympy.Pow(base_sp, exp_sp))
+ return result.evaluate_elements(evaluation)
class Sqrt(SympyFunction):
diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py
index 5a85bbe7e..152e16d9f 100644
--- a/mathics/builtin/arithmetic.py
+++ b/mathics/builtin/arithmetic.py
@@ -629,10 +629,16 @@ class DirectedInfinity(SympyFunction):
rules = {
"DirectedInfinity[args___] ^ -1": "0",
# Special arguments:
- "DirectedInfinity[DirectedInfinity[args___]]": "DirectedInfinity[args]",
"DirectedInfinity[Indeterminate]": "Indeterminate",
"DirectedInfinity[Alternatives[0, 0.]]": "DirectedInfinity[]",
# Plus
+ "DirectedInfinity[DirectedInfinity[args___]]": "DirectedInfinity[args]",
+ # "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]",
"DirectedInfinity[a_] + DirectedInfinity[b_] /; b == -a": (
"Message[Infinity::indet,"
" Unevaluated[DirectedInfinity[a] + DirectedInfinity[b]]];"
@@ -644,23 +650,27 @@ class DirectedInfinity(SympyFunction):
"Indeterminate"
),
"DirectedInfinity[args___] + _?NumberQ": "DirectedInfinity[args]",
- # Times. See if can be reinstalled in eval_Times
+ "DirectedInfinity[Alternatives[0, 0.]]": "DirectedInfinity[]",
+ "DirectedInfinity[0.]": (
+ "Message[Infinity::indet,"
+ " Unevaluated[DirectedInfinity[0.]]];"
+ "Indeterminate"
+ ),
"Alternatives[0, 0.] DirectedInfinity[z___]": (
"Message[Infinity::indet,"
" Unevaluated[0 DirectedInfinity[z]]];"
"Indeterminate"
),
- "a_?NumericQ * DirectedInfinity[b_]": "DirectedInfinity[a * b]",
"a_ DirectedInfinity[]": "DirectedInfinity[]",
"DirectedInfinity[a_] * DirectedInfinity[b_]": "DirectedInfinity[a * b]",
+ "a_?NumericQ * DirectedInfinity[b_]": "DirectedInfinity[a * b]",
}
formats = {
"DirectedInfinity[1]": "HoldForm[Infinity]",
- "DirectedInfinity[-1]": "HoldForm[-Infinity]",
+ "DirectedInfinity[-1]": "PrecedenceForm[-HoldForm[Infinity], 390]",
"DirectedInfinity[]": "HoldForm[ComplexInfinity]",
- "DirectedInfinity[DirectedInfinity[z_]]": "DirectedInfinity[z]",
- "DirectedInfinity[z_?NumericQ]": "HoldForm[z Infinity]",
+ "DirectedInfinity[z_]": "PrecedenceForm[z HoldForm[Infinity], 390]",
}
def eval_complex_infinity(self, evaluation: Evaluation):
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..2e7dad161 100644
--- a/mathics/eval/arithmetic.py
+++ b/mathics/eval/arithmetic.py
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
"""
-arithmetic-related evaluation functions.
+helper functions for arithmetic evaluation, which do not
+depends on the evaluation context. Conversions to Sympy are
+used just as a last resource.
Many of these do do depend on the evaluation context. Conversions to Sympy are
used just as a last resource.
@@ -14,6 +16,7 @@
import sympy
from mathics.core.atoms import (
+ NUMERICAL_CONSTANTS,
Complex,
Integer,
Integer0,
@@ -30,10 +33,21 @@
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.rules import Rule
+from mathics.core.symbols import Atom, Symbol, SymbolPlus, SymbolPower, SymbolTimes
+from mathics.core.systemsymbols import (
+ SymbolAbs,
+ SymbolComplexInfinity,
+ SymbolExp,
+ SymbolI,
+ SymbolIndeterminate,
+ SymbolLog,
+ SymbolSign,
+)
RationalMOneHalf = Rational(-1, 2)
+RealM0p5 = Real(-0.5)
+RealOne = Real(1.0)
# This cache might not be used that much.
@@ -70,31 +84,110 @@ def eval_Abs(expr: BaseElement) -> Optional[BaseElement]:
"""
if expr is a number, return the absolute value.
"""
- if isinstance(expr, (Integer, Rational, Real)):
- if expr.value >= 0:
- return expr
- return eval_multiply_numbers(*[IntegerM1, expr])
- if isinstance(expr, Complex):
- re, im = expr.real, expr.imag
- sqabs = eval_add_numbers(
- eval_multiply_numbers(re, re), eval_multiply_numbers(im, im)
- )
- return Expression(SymbolPower, sqabs, RationalOneHalf)
+
+ if isinstance(expr, Number):
+ return eval_Abs_number(expr)
+ if expr.has_form("Power", 2):
+ base, exp = expr.elements
+ if exp.is_zero:
+ return Integer1
+ if test_arithmetic_expr(expr):
+ abs_base = eval_Abs(base)
+ if abs_base is None:
+ abs_base = Expression(SymbolAbs, base)
+ return Expression(SymbolPower, abs_base, exp)
+ if expr.get_head() is SymbolTimes:
+ return eval_multiply_numbers(*(eval_Abs(x) for x in expr.elements))
+ if test_nonnegative_arithmetic_expr(expr):
+ return expr
+ if test_negative_arithmetic_expr(expr):
+ return eval_multiply_numbers(IntegerM1, expr)
return None
+def eval_Abs_number(n: Number) -> Number:
+ """
+ Evals the absolute value of a number
+ """
+ if isinstance(n, Integer):
+ n_val = n.value
+ if n_val >= 0:
+ return n
+ return Integer(-n_val)
+ if isinstance(n, Rational):
+ n_num, n_den = n.value.as_numer_denom()
+ if n_num >= 0:
+ return n
+ return Rational(-n_num, n_den)
+ if isinstance(n, Real):
+ n_val = n.value
+ if n_val >= 0:
+ return n
+ return eval_multiply_numbers(IntegerM1, n)
+ if isinstance(n, Complex):
+ if n.real.is_zero:
+ return eval_Abs_number(n.imag)
+ sq_comp = tuple((eval_multiply_numbers(x, x) for x in (n.real, n.imag)))
+ sq_abs = eval_add_numbers(*sq_comp)
+ result = eval_Power_number(sq_abs, RationalOneHalf) or Expression(
+ SymbolPower, sq_abs, RationalOneHalf
+ )
+ return result
+
+
def eval_Sign(expr: BaseElement) -> Optional[BaseElement]:
"""
if expr is a number, return its sign.
"""
- if isinstance(expr, (Integer, Rational, Real)):
- if expr.value > 0:
+ if isinstance(expr, Atom):
+ return eval_Sign_number(expr)
+ if expr.has_form("Power", 2):
+ base, exp = expr.elements
+ if exp.is_zero:
return Integer1
- elif expr.value == 0:
- return Integer0
- else:
- return IntegerM1
-
+ if isinstance(exp, (Integer, Real, Rational)):
+ sign = eval_Sign(base) or Expression(SymbolSign, base)
+ return Expression(SymbolPower, sign, exp)
+ if isinstance(exp, Complex):
+ sign = eval_Sign(base) or Expression(SymbolSign, base)
+ return Expression(SymbolPower, sign, exp.real)
+ if test_arithmetic_expr(exp):
+ sign = eval_Sign(base) or Expression(SymbolSign, base)
+ return Expression(SymbolPower, sign, exp)
+ return None
+ if expr.has_form("Exp", 1):
+ exp = expr.elements[0]
+ if isinstance(exp, (Integer, Real, Rational)):
+ return Integer1
+ if isinstance(exp, Complex):
+ return Expression(SymbolExp, exp.imag)
+ if expr.get_head() is SymbolTimes:
+ abs_value = eval_Abs(eval_multiply_numbers(*expr.elements))
+ if abs_value is Integer1:
+ return expr
+ if abs_value is None:
+ return None
+ criteria = eval_add_numbers(abs_value, IntegerM1)
+ if test_zero_arithmetic_expr(criteria, numeric=True):
+ return expr
+ return None
+ if expr.get_head() is SymbolPlus:
+ abs_value = eval_Abs(eval_add_numbers(*expr.elements))
+ if abs_value is Integer1:
+ return expr
+ if abs_value is None:
+ return None
+ criteria = eval_add_numbers(abs_value, IntegerM1)
+ if test_zero_arithmetic_expr(criteria, numeric=True):
+ return expr
+ return None
+ if test_nonnegative_arithmetic_expr(expr):
+ return Integer1
+ if test_negative_arithmetic_expr(expr):
+ return IntegerM1
+ if test_zero_arithmetic_expr:
+ return Integer0
+ return None
if isinstance(expr, Complex):
re, im = expr.real, expr.imag
sqabs = eval_add_numbers(eval_Times(re, re), eval_Times(im, im))
@@ -106,6 +199,28 @@ def eval_Sign(expr: BaseElement) -> Optional[BaseElement]:
return None
+def eval_Sign_number(n: Number) -> Number:
+ """
+ Evals the absolute value of a number.
+ """
+ if n.is_zero:
+ return Integer0
+ if isinstance(n, (Integer, Rational, Real)):
+ return Integer1 if n.value > 0 else IntegerM1
+ if isinstance(n, Complex):
+ abs_sq = eval_add_numbers(
+ *(eval_multiply_numbers(x, x) for x in (n.real, n.imag))
+ )
+ criteria = eval_add_numbers(abs_sq, IntegerM1)
+ if test_zero_arithmetic_expr(criteria):
+ return n
+ if n.is_inexact():
+ return eval_multiply_numbers(n, eval_Power_number(abs_sq, RealM0p5))
+ if test_zero_arithmetic_expr(criteria, numeric=True):
+ return n
+ return eval_multiply_numbers(n, eval_Power_number(abs_sq, RationalMOneHalf))
+
+
def eval_mpmath_function(
mpmath_function: Callable, *args: Number, prec: Optional[int] = None
) -> Optional[Number]:
@@ -133,6 +248,31 @@ def eval_mpmath_function(
return call_mpmath(mpmath_function, tuple(mpmath_args), prec)
+def eval_Exponential(exp: BaseElement) -> BaseElement:
+ """
+ Eval E^exp
+ """
+ # If both base and exponent are exact quantities,
+ # use sympy.
+
+ if not exp.is_inexact():
+ exp_sp = exp.to_sympy()
+ if exp_sp is None:
+ return None
+ return from_sympy(sympy.Exp(exp_sp))
+
+ prec = exp.get_precision()
+ if prec is not None:
+ if exp.is_machine_precision():
+ number = mpmath.exp(exp.to_mpmath())
+ result = from_mpmath(number)
+ return result
+ else:
+ with mpmath.workprec(prec):
+ number = mpmath.exp(exp.to_mpmath())
+ return from_mpmath(number, prec)
+
+
def eval_Plus(*items: BaseElement) -> BaseElement:
"evaluate Plus for general elements"
numbers, items_tuple = segregate_numbers_from_sorted_list(*items)
@@ -200,6 +340,157 @@ def append_last():
elements_properties=ElementsProperties(False, False, True),
)
+ elements.sort()
+ return Expression(
+ SymbolPlus,
+ *elements,
+ elements_properties=ElementsProperties(False, False, True),
+ )
+
+
+def eval_Power_number(base: Number, exp: Number) -> Optional[Number]:
+ """
+ Eval base^exp for `base` and `exp` two numbers. If the expression
+ remains the same, return None.
+ """
+ # If both base and exponent are exact quantities,
+ # use sympy.
+ # If base or exp are inexact quantities, use
+ # the inexact routine.
+ if base.is_inexact() or exp.is_inexact():
+ return eval_Power_inexact(base, exp)
+
+ # Trivial special cases
+ if exp is Integer1:
+ return base
+ if exp is Integer0:
+ return Integer1
+ if base is Integer1:
+ return Integer1
+
+ def eval_Power_sympy() -> Optional[Number]:
+ """
+ Tries to compute x^p using sympy rules.
+ If the answer is again x^p, return None.
+ """
+ # This function is called just if useful native rules
+ # are available.
+ result = from_sympy(sympy.Pow(base.to_sympy(), exp.to_sympy()))
+ if result.has_form("Power", 2):
+ # If the expression didnĀ“t change, return None
+ if result.elements[0].sameQ(base):
+ return None
+ return result
+
+ # Rational exponent
+ if isinstance(exp, Rational):
+ exp_p, exp_q = exp.value.as_numer_denom()
+ if abs(exp_p) > exp_q:
+ exp_int, exp_num = divmod(exp_p, exp_q)
+ exp_rem = Rational(exp_num, exp_q)
+ factor_1 = eval_Power_number(base, Integer(exp_int))
+ factor_2 = eval_Power_number(base, exp_rem) or Expression(
+ SymbolPower, base, exp_rem
+ )
+ if factor_1 is Integer1:
+ return factor_2
+ return Expression(SymbolTimes, factor_1, factor_2)
+
+ # Integer base
+ if isinstance(base, Integer):
+ base_value = base.value
+ if base_value == -1:
+ if isinstance(exp, Rational):
+ if exp.sameQ(RationalOneHalf):
+ return SymbolI
+ return None
+ return eval_Power_sympy()
+ elif base_value < 0:
+ neg_base = eval_negate_number(base)
+ candidate = eval_Power_number(neg_base, exp)
+ if candidate is None:
+ return None
+ sign_factor = eval_Power_number(IntegerM1, exp)
+ if candidate is Integer1:
+ return sign_factor
+ return Expression(SymbolTimes, candidate, sign_factor)
+
+ # Rational base
+ if isinstance(base, Rational):
+ # If the exponent is an Integer or Rational negative value
+ # restate as a positive power
+ if (
+ isinstance(exp, Integer)
+ and exp.value < 0
+ or isinstance(exp, Rational)
+ and exp.value.p < 0
+ ):
+ base, exp = eval_inverse_number(base), eval_negate_number(exp)
+ return eval_Power_number(base, exp) or Expression(SymbolPower, base, exp)
+
+ p, q = (Integer(u) for u in base.value.as_numer_denom())
+ p_eval, q_eval = (eval_Power_number(u, exp) for u in (p, q))
+ # If neither p^exp or q^exp produced a new result,
+ # leave it alone
+ if q_eval is None and p_eval is None:
+ return None
+ # if q^exp == 1: return p_eval
+ # (should not happen)
+ if q_eval is Integer1:
+ return p_eval
+ if isinstance(q_eval, Integer):
+ if isinstance(p_eval, Integer):
+ return Rational(p_eval.value, q_eval.value)
+
+ if p_eval is None:
+ p_eval = Expression(SymbolPower, p, exp)
+
+ if q_eval is None:
+ q_eval = Expression(SymbolPower, q, exp)
+ return Expression(
+ SymbolTimes, p_eval, Expression(SymbolPower, q_eval, IntegerM1)
+ )
+ # Pure imaginary base case
+ elif isinstance(base, Complex) and base.real.is_zero:
+ base = base.imag
+ if base.value < 0:
+ base = eval_negate_number(base)
+ phase = Expression(
+ SymbolPower,
+ IntegerM1,
+ eval_multiply_numbers(IntegerM1, RationalOneHalf, exp),
+ )
+ else:
+ phase = Expression(
+ SymbolPower, IntegerM1, eval_multiply_numbers(RationalOneHalf, exp)
+ )
+ real_factor = eval_Power_number(base, exp)
+
+ if real_factor is None:
+ return None
+ return Expression(SymbolTimes, real_factor, phase)
+
+ # Generic case
+ return eval_Power_sympy()
+
+
+def eval_Power_inexact(base: Number, exp: Number) -> BaseElement:
+ """
+ Eval base^exp for `base` and `exp` inexact numbers
+ """
+ # If both base and exponent are exact quantities,
+ # use sympy.
+ prec = min_prec(base, exp)
+ if prec is not None:
+ is_machine_precision = base.is_machine_precision() or exp.is_machine_precision()
+ if is_machine_precision:
+ number = mpmath.power(base.to_mpmath(), exp.to_mpmath())
+ return from_mpmath(number)
+ else:
+ with mpmath.workprec(prec):
+ number = mpmath.power(base.to_mpmath(), exp.to_mpmath())
+ return from_mpmath(number, prec)
+
def eval_Times(*items: BaseElement) -> BaseElement:
elements = []
@@ -287,9 +578,112 @@ def eval_Times(*items: BaseElement) -> BaseElement:
)
+# Here I used the convention of calling eval_* to functions that can produce a new expression, or None
+# if the result can not be evaluated, or is trivial. For example, if we call eval_Power_number(Integer2, RationalOneHalf)
+# it returns ``None`` instead of ``Expression(SymbolPower, Integer2, RationalOneHalf)``.
+# The reason is that these functions are written to be part of replacement rules, to be applied during the evaluation process.
+# In that process, a rule is considered applied if produces an expression that is different from the original one, or
+# if the replacement function returns (Python's) ``None``.
+#
+# For example, when the expression ``Power[4, 1/2]`` is evaluated, a (Builtin) rule ``Power[base_, exp_]->eval_repl_rule(base, expr)``
+# is applied. If the rule matches, `repl_rule` is called with arguments ``(4, 1/2)`` and produces `2`. As `Integer2.sameQ(Power[4, 1/2])`
+# is False, then no new rules for `Power` are checked, and a new round of evaluation is atempted.
+#
+# On the other hand, if ``Power[3, 1/2]``, ``repl_rule`` can do two possible things: one is return ``Power[3, 1/2]``. If it does,
+# the rule is considered applied. Then, the evaluation method checks if `Power[3, 1/2].sameQ(Power[3, 1/2])`. In this case it is true,
+# and then the expression is kept as it is.
+# The other possibility is to return (Python's) `None`. In that case, the evaluator considers that the rule failed to be applied,
+# and look for another rule associated to ``Power``. To return ``None`` produces then a faster evaluation, since no ``sameQ`` call is needed,
+# and do not prevent that other rules are attempted.
+#
+# The bad part of using ``None`` as a return is that I would expect that ``eval`` produces always a valid Expression, so if at some point of
+# the code I call ``eval_Power_number(Integer3, RationalOneHalf)`` I get ``Expression(SymbolPower, Integer3, RationalOneHalf)``.
+#
+# From my point of view, it would make more sense to use the following convention:
+# * if the method has signature ``eval_method(...)->BaseElement:`` then use the prefix ``eval_``
+# * if the method has the siguature ``apply_method(...)->Optional[BaseElement]`` use the prefix ``apply_`` or maybe ``repl_``.
+#
+# In any case, let's keep the current convention.
+#
+#
+
+
+def associate_powers(expr: BaseElement, power: BaseElement = Integer1) -> BaseElement:
+ """
+ base^a^b^c^...^power -> base^(a*b*c*...power)
+ provided one of the following cases
+ * `a`, `b`, ... `power` are all integer numbers
+ * `a`, `b`,... are Rational/Real number with absolute value <=1,
+ and the other powers are not integer numbers.
+ * `a` is not a Rational/Real number, and b, c, ... power are all
+ integer numbers.
+ """
+ powers = []
+ base = expr
+ if power is not Integer1:
+ powers.append(power)
+
+ while base.has_form("Power", 2):
+ previous_base, outer_power = base, power
+ base, power = base.elements
+ if len(powers) == 0:
+ if power is not Integer1:
+ powers.append(power)
+ continue
+ if power is IntegerM1:
+ powers.append(power)
+ continue
+ if isinstance(power, (Rational, Real)):
+ if abs(power.value) < 1:
+ powers.append(power)
+ continue
+ # power is not rational/real and outer_power is integer,
+ elif isinstance(outer_power, Integer):
+ if power is not Integer1:
+ powers.append(power)
+ if isinstance(power, Integer):
+ continue
+ else:
+ break
+ # in any other case, use the previous base and
+ # exit the loop
+ base = previous_base
+ break
+
+ if len(powers) == 0:
+ return base
+ elif len(powers) == 1:
+ return Expression(SymbolPower, base, powers[0])
+ result = Expression(SymbolPower, base, Expression(SymbolTimes, *powers))
+ return result
+
+
+def distribute_factor(expr: BaseElement, factor: BaseElement) -> BaseElement:
+ """
+ q * (a + b + c) -> (q a + q b + q c)
+ """
+ if not expr.has_form("Plus", None):
+ return expr
+ terms = (Expression(SymbolTimes, factor, term) for term in expr.elements)
+ return Expression(SymbolPlus, *terms)
+
+
+def distribute_powers(expr: BaseElement) -> BaseElement:
+ """
+ (a b c)^p -> (a^p b^p c^p)
+ """
+ if not expr.has_form("Power", 2):
+ return expr
+ base, exp = expr.elements
+ if not base.has_form("Times", None):
+ return expr
+ factors = (Expression(SymbolPower, factor, exp) for factor in base.elements)
+ return Expression(SymbolTimes, *factors)
+
+
def eval_add_numbers(
- *numbers: Number,
-) -> BaseElement:
+ *numbers: List[Number],
+) -> Number:
"""
Add the elements in ``numbers``.
"""
@@ -315,7 +709,37 @@ def eval_add_numbers(
return from_sympy(sum(item.to_sympy() for item in numbers))
-def eval_multiply_numbers(*numbers: Number) -> BaseElement:
+def eval_complex_conjugate(z: Number) -> Number:
+ """
+ Evaluates the complex conjugate of z.
+ """
+ if isinstance(z, Complex):
+ re, im = z.real, z.imag
+ return Complex(re, eval_negate_number(im))
+ return z
+
+
+def eval_inverse_number(n: Number) -> Number:
+ """
+ Eval 1/n
+ """
+ if isinstance(n, Integer):
+ n_value = n.value
+ if n_value == 1 or n_value == -1:
+ return n
+ return Rational(-1, -n_value) if n_value < 0 else Rational(1, n_value)
+ if isinstance(n, Rational):
+ n_num, n_den = n.value.as_numer_denom()
+ if n_num < 0:
+ n_num, n_den = -n_num, -n_den
+ if n_num == 1:
+ return Integer(n_den)
+ return Rational(n_den, n_num)
+ # Otherwise, use power....
+ return eval_Power_number(n, IntegerM1)
+
+
+def eval_multiply_numbers(*numbers: List[Number]) -> Number:
"""
Multiply the elements in ``numbers``.
"""
@@ -340,6 +764,37 @@ def eval_multiply_numbers(*numbers: Number) -> BaseElement:
return from_sympy(sympy.Mul(*(item.to_sympy() for item in numbers)))
+def eval_negate_number(n: Number) -> Number:
+ """
+ Changes the sign of n
+ """
+ if isinstance(n, Integer):
+ return Integer(-n.value)
+ if isinstance(n, Rational):
+ n_num, n_den = n.value.as_numer_denom()
+ return Rational(-n_num, n_den)
+ # Otherwise, multiply by -1:
+ return eval_multiply_numbers(IntegerM1, n)
+
+
+def flat_arithmetic_operators(expr: Expression) -> Expression:
+ """
+ operation[a_number, b, operation[c_number, d], e]-> operation[a, c, b, c, d, e]
+ """
+ # items is a dict with two keys: True and False.
+ # In True we store numeric items, and in False the symbolic ones.
+ items = {True: [], False: []}
+ head = expr.get_head()
+ for element in expr.elements:
+ # If the element is also head[elements],
+ # take its elements, and append to the main expression.
+ if element.get_head() is head:
+ for item in flat_arithmetic_operators(element).elements:
+ item[isinstance(item, Number)].append(item)
+ item[isinstance(item, Number)].append(item)
+ return Expression(head, *items[True], *items[False])
+
+
def segregate_numbers(
*elements: BaseElement,
) -> Tuple[List[Number], List[BaseElement]]:
@@ -374,3 +829,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/eval/testing_expressions.py b/mathics/eval/testing_expressions.py
index c15471f48..cab4a7d4b 100644
--- a/mathics/eval/testing_expressions.py
+++ b/mathics/eval/testing_expressions.py
@@ -13,7 +13,6 @@ def cmp(a, b) -> int:
def do_cmp(x1, x2) -> Optional[int]:
-
# don't attempt to compare complex numbers
for x in (x1, x2):
# TODO: Send message General::nord
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_basic.py b/test/builtin/arithmetic/test_basic.py
index d99b0b9dc..9a05aac28 100644
--- a/test/builtin/arithmetic/test_basic.py
+++ b/test/builtin/arithmetic/test_basic.py
@@ -153,8 +153,8 @@ def test_multiply(str_expr, str_expected, msg):
("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)", ""),
+ ("a b DirectedInfinity[-I]", "a b (-I Infinity)", ""),
+ ("a b DirectedInfinity[-3]", "a b (-Infinity)", ""),
],
)
def test_directed_infinity_precedence(str_expr, str_expected, msg):
@@ -197,7 +197,7 @@ def test_directed_infinity_precedence(str_expr, str_expected, msg):
("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),
+ ("(-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),
@@ -208,15 +208,15 @@ def test_directed_infinity_precedence(str_expr, str_expected, msg):
# 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^"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", "Power associativity rational, real"),
- # ("(a^(.3))^3.", "a ^ 0.9", "Power associativity for real powers"),
+ ("(a^(1/2))^3.", "a ^ 1.5", "Power associativity rational, real"),
+ ("(a^(.3))^3.", "a ^ 0.9", "Power associativity for real powers"),
("(a^(1.3))^3.", "(a ^ 1.3) ^ 3.", None),
# Exponentials involving expressions
("(a^(p-2 q))^3", "a ^ (3 p - 6 q)", 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
diff --git a/test/format/test_format.py b/test/format/test_format.py
index 161ebc5df..ee81add3c 100644
--- a/test/format/test_format.py
+++ b/test/format/test_format.py
@@ -456,34 +456,53 @@
"Sqrt[1/(1+1/(1+1/a))]": {
"msg": "SqrtBox",
"text": {
- "System`StandardForm": "Sqrt[1 / (1+1 / (1+1 / a))]",
- "System`TraditionalForm": "Sqrt[1 / (1+1 / (1+1 / a))]",
- "System`InputForm": "Sqrt[1 / (1 + 1 / (1 + 1 / a))]",
- "System`OutputForm": "Sqrt[1 / (1 + 1 / (1 + 1 / a))]",
+ "System`StandardForm": "1 / Sqrt[1+1 / (1+1 / a)]",
+ "System`TraditionalForm": "1 / Sqrt[1+1 / (1+1 / a)]",
+ "System`InputForm": "1 / Sqrt[1 + 1 / (1 + 1 / a)]",
+ "System`OutputForm": "1 / Sqrt[1 + 1 / (1 + 1 / a)]",
},
"mathml": {
"System`StandardForm": (
- " 1 1 + 1 1 + 1 a ",
+ (
+ r"1 1 + 1 "
+ r"1 + 1 a "
+ r""
+ ),
"Fragile!",
),
"System`TraditionalForm": (
- " 1 1 + 1 1 + 1 a ",
+ (
+ r"1 1 + 1 "
+ r"1 + 1 a "
+ r""
+ ),
"Fragile!",
),
"System`InputForm": (
- "Sqrt [ 1 / ( 1 + 1 / ( 1 + 1 / a ) ) ]",
+ (
+ r"1 / Sqrt [ "
+ r"1 + 1 / "
+ r"( 1 + 1 "
+ r" / a ) ]"
+ ),
"Fragile!",
),
"System`OutputForm": (
- "Sqrt [ 1 / ( 1 + 1 / ( 1 + 1 / a ) ) ]",
+ (
+ r"1 / Sqrt ["
+ r" 1 + 1 "
+ r" / ( 1 "
+ r" + 1 / "
+ r"a ) ]"
+ ),
"Fragile!",
),
},
"latex": {
- "System`StandardForm": "\\sqrt{\\frac{1}{1+\\frac{1}{1+\\frac{1}{a}}}}",
- "System`TraditionalForm": "\\sqrt{\\frac{1}{1+\\frac{1}{1+\\frac{1}{a}}}}",
- "System`InputForm": "\\text{Sqrt}\\left[1\\text{ / }\\left(1\\text{ + }1\\text{ / }\\left(1\\text{ + }1\\text{ / }a\\right)\\right)\\right]",
- "System`OutputForm": "\\text{Sqrt}\\left[1\\text{ / }\\left(1\\text{ + }1\\text{ / }\\left(1\\text{ + }1\\text{ / }a\\right)\\right)\\right]",
+ "System`StandardForm": "\\frac{1}{\\sqrt{1+\\frac{1}{1+\\frac{1}{a}}}}",
+ "System`TraditionalForm": "\\frac{1}{\\sqrt{1+\\frac{1}{1+\\frac{1}{a}}}}",
+ "System`InputForm": r"1\text{ / }\text{Sqrt}\left[1\text{ + }1\text{ / }\left(1\text{ + }1\text{ / }a\right)\right]",
+ "System`OutputForm": r"1\text{ / }\text{Sqrt}\left[1\text{ + }1\text{ / }\left(1\text{ + }1\text{ / }a\right)\right]",
},
},
# Grids, arrays and matrices