diff --git a/CHANGES.rst b/CHANGES.rst
index 774387872..249d1fa6b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -76,7 +76,9 @@ Internals
#. ``_SetOperator.assign_elementary`` was renamed as ``_SetOperator.assign``. All the special cases are not handled by the ``_SetOperator.special_cases`` dict.
#. ``isort`` run over all Python files. More type annotations and docstrings on functions added.
#. caching on immutable atoms like, ``String``, ``Integer``, ``Real``, etc. was improved; the ``__hash__()`` function was sped up. There is a small speedup overall from this at the expense of increased memory.
-#. more type annotations added to functions, especially builtin functions
+#. more type annotations added to functions, especially builtin functions.
+#. Improved implementation for ``Accuracy`` and ``Precision``.
+#. Improved implementation for ``Plus``, ``Times`` and ``Power``, ``Exp``, ``Abs`` and ``Sign`` for numerical elements. Also, the new versions improves the compatibility with WMA in the results of arithmetic symbolic evaluation.
Bugs
@@ -93,6 +95,8 @@ Bugs
#. Units and Quantities were sometimes failing. Also they were omitted from documentation.
#. Better handling of ``Infinite`` quantities.
#. Fix ``Precision`` compatibility with WMA.
+#. Prevent that `DirectedInfinity[dir]` be transformed into a `ComplexInfinity`. Rules for `Infinity`, `ComplexInfinity` and `DirectedInfinity wre improved`.
+
PyPI Package requirements
diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py
index f09dd735c..6260c68ec 100644
--- a/mathics/builtin/arithfns/basic.py
+++ b/mathics/builtin/arithfns/basic.py
@@ -6,8 +6,6 @@
"""
-
-import mpmath
import sympy
from mathics.builtin.arithmetic import _MPMathFunction, create_infix
@@ -37,22 +35,19 @@
A_READ_PROTECTED,
)
from mathics.core.convert.expression import to_expression
-from mathics.core.convert.mpmath import from_mpmath
from mathics.core.convert.sympy import from_sympy
+from mathics.core.element import ElementsProperties
from mathics.core.expression import Expression
from mathics.core.list import ListExpression
-from mathics.core.number import dps, min_prec
from mathics.core.symbols import (
Symbol,
SymbolDivide,
SymbolHoldForm,
- SymbolNull,
SymbolPlus,
SymbolPower,
SymbolTimes,
)
from mathics.core.systemsymbols import (
- SymbolAccuracy,
SymbolBlank,
SymbolComplexInfinity,
SymbolDirectedInfinity,
@@ -61,8 +56,17 @@
SymbolInfix,
SymbolLeft,
SymbolMinus,
+ SymbolOverflow,
SymbolPattern,
- SymbolSequence,
+)
+from mathics.eval.arithmetic import (
+ associate_powers,
+ eval_add_numbers,
+ eval_exponential,
+ eval_multiply_numbers,
+ eval_power_inexact,
+ eval_power_number,
+ segregate_numbers_from_sorted_list as segregate_numbers,
)
from mathics.eval.nevaluator import eval_N
from mathics.eval.numerify import numerify
@@ -396,13 +400,13 @@ def eval(self, items, evaluation):
"Plus[items___]"
items_tuple = numerify(items, evaluation).get_sequence()
+ numbers, items_tuple = segregate_numbers(*items_tuple)
elements = []
last_item = last_count = None
+ number = eval_add_numbers(numbers)
- prec = min_prec(*items_tuple)
- is_machine_precision = any(item.is_machine_precision() for item in items_tuple)
- numbers = []
-
+ # This reduces common factors
+ # TODO: Check if it possible to avoid the conversions back and forward to sympy.
def append_last():
if last_item is not None:
if last_count == 1:
@@ -420,65 +424,46 @@ def append_last():
)
for item in items_tuple:
- if isinstance(item, Number):
- numbers.append(item)
+ count = rest = None
+ if item.has_form("Times", None):
+ for element in item.elements:
+ if isinstance(element, Number):
+ count = element.to_sympy()
+ rest = item.get_mutable_elements()
+ rest.remove(element)
+ if len(rest) == 1:
+ rest = rest[0]
+ else:
+ rest.sort()
+ rest = Expression(SymbolTimes, *rest)
+ break
+ if count is None:
+ count = sympy.Integer(1)
+ rest = item
+ if last_item is not None and last_item == rest:
+ last_count = last_count + count
else:
- count = rest = None
- if item.has_form("Times", None):
- for element in item.elements:
- if isinstance(element, Number):
- count = element.to_sympy()
- rest = item.get_mutable_elements()
- rest.remove(element)
- if len(rest) == 1:
- rest = rest[0]
- else:
- rest.sort()
- rest = Expression(SymbolTimes, *rest)
- break
- if count is None:
- count = sympy.Integer(1)
- rest = item
- if last_item is not None and last_item == rest:
- last_count = last_count + count
- else:
- append_last()
- last_item = rest
- last_count = count
+ append_last()
+ last_item = rest
+ last_count = count
append_last()
- if numbers:
- if prec is not None:
- if is_machine_precision:
- numbers = [item.to_mpmath() for item in numbers]
- number = mpmath.fsum(numbers)
- number = from_mpmath(number)
- else:
- # For a sum, what is relevant is the minimum accuracy of the terms
- acc = (
- Expression(SymbolAccuracy, ListExpression(items))
- .evaluate(evaluation)
- .to_python()
- )
- with mpmath.workprec(prec):
- numbers = [item.to_mpmath() for item in numbers]
- number = mpmath.fsum(numbers)
- number = from_mpmath(number, acc=acc)
- else:
- number = from_sympy(sum(item.to_sympy() for item in numbers))
- else:
- number = Integer0
+ # now elements contains the symbolic terms which can not be simplified.
+ # by collecting common symbolic factors.
+ if not elements:
+ return number
- if not number.sameQ(Integer0):
+ if number is not Integer0:
elements.insert(0, number)
-
- if not elements:
- return Integer0
elif len(elements) == 1:
return elements[0]
- else:
- elements.sort()
- return Expression(SymbolPlus, *elements)
+
+ elements.sort()
+ return Expression(
+ SymbolPlus,
+ *elements,
+ elements_properties=ElementsProperties(False, False, True),
+ )
class Power(BinaryOperator, _MPMathFunction):
@@ -614,6 +599,8 @@ class Power(BinaryOperator, _MPMathFunction):
rules = {
"Power[]": "1",
"Power[x_]": "x",
+ "Power[I,-1]": "-I",
+ "Power[-1, 1/2]": "I",
}
summary_text = "exponentiate"
@@ -622,15 +609,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:
@@ -644,17 +631,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)
- )
+ return SymbolComplexInfinity
- result = self.eval(Expression(SymbolSequence, x, y), evaluation)
- if result is None or result != SymbolNull:
- return result
+ # 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):
@@ -932,90 +949,112 @@ def format_outputform(self, items, evaluation):
def eval(self, items, evaluation):
"Times[items___]"
- items = numerify(items, evaluation).get_sequence()
elements = []
numbers = []
infinity_factor = False
- prec = min_prec(*items)
- is_machine_precision = any(item.is_machine_precision() for item in items)
-
# find numbers and simplify Times -> Power
- for item in items:
- if isinstance(item, Number):
- numbers.append(item)
- elif elements and item == elements[-1]:
- elements[-1] = Expression(SymbolPower, elements[-1], Integer2)
- elif (
- elements
- and item.has_form("Power", 2)
- and elements[-1].has_form("Power", 2)
- and item.elements[0].sameQ(elements[-1].elements[0])
- ):
- elements[-1] = Expression(
- SymbolPower,
- elements[-1].elements[0],
- Expression(SymbolPlus, item.elements[1], elements[-1].elements[1]),
- )
- elif (
- elements
- and item.has_form("Power", 2)
- and item.elements[0].sameQ(elements[-1])
- ):
- elements[-1] = Expression(
- SymbolPower,
- elements[-1],
- Expression(SymbolPlus, item.elements[1], Integer1),
- )
- elif (
- elements
- and elements[-1].has_form("Power", 2)
- and elements[-1].elements[0].sameQ(item)
- ):
- elements[-1] = Expression(
- SymbolPower,
- item,
- Expression(SymbolPlus, Integer1, elements[-1].elements[1]),
- )
- elif item.get_head().sameQ(SymbolDirectedInfinity):
+
+ numbers, symbolic_items = segregate_numbers(
+ *(numerify(items, evaluation).get_sequence())
+ )
+
+ # This loop handles factors representing infinite quantities,
+ # and factors which are powers of the same basis.
+ for item in symbolic_items:
+ # Process Infinity factors
+ if item.get_head() is SymbolDirectedInfinity:
+ if len(item.elements) == 0:
+ return (
+ SymbolIndeterminate
+ if any(n.is_zero for n in numbers)
+ else SymbolComplexInfinity
+ )
infinity_factor = True
- if len(item.elements) > 0:
- direction = item.elements[0]
- if isinstance(direction, Number):
- numbers.append(direction)
- else:
- elements.append(direction)
- elif item.sameQ(SymbolInfinity) or item.sameQ(SymbolComplexInfinity):
+ direction = item.elements[0]
+ if isinstance(direction, Number):
+ numbers.append(direction)
+ continue
+ else:
+ item = direction
+ elif item is SymbolInfinity:
infinity_factor = True
+ continue
+ elif item is SymbolComplexInfinity:
+ return (
+ SymbolIndeterminate
+ if any(n.is_zero for n in numbers)
+ else SymbolComplexInfinity
+ )
+ elif item is SymbolIndeterminate:
+ return item
+ # Process powers
+ if elements:
+ previous_elem = elements[-1]
+ if item == previous_elem:
+ elements[-1] = Expression(SymbolPower, previous_elem, Integer2)
+ continue
+ elif item.has_form("Power", 2):
+ base, exp = item.elements
+ if previous_elem.has_form("Power", 2) and base.sameQ(
+ previous_elem.elements[0]
+ ):
+ exp = Expression(
+ SymbolPlus, exp, previous_elem.elements[1]
+ ).evaluate(evaluation)
+ elements[-1] = Expression(
+ SymbolPower,
+ base,
+ exp,
+ )
+ continue
+ if base.sameQ(previous_elem):
+ exp = Expression(SymbolPlus, Integer1, exp).evaluate(evaluation)
+ elements[-1] = Expression(
+ SymbolPower,
+ base,
+ exp,
+ )
+ continue
+ elif previous_elem.has_form("Power", 2) and previous_elem.elements[
+ 0
+ ].sameQ(item):
+ exp = Expression(
+ SymbolPlus, Integer1, previous_elem.elements[1]
+ ).evaluate(evaluation)
+ elements[-1] = Expression(
+ SymbolPower,
+ item,
+ exp,
+ )
+ continue
else:
- elements.append(item)
-
- if numbers:
- if prec is not None:
- if is_machine_precision:
- numbers = [item.to_mpmath() for item in numbers]
- number = mpmath.fprod(numbers)
- number = from_mpmath(number)
+ item = item
+ # Otherwise, just append the element...
+ elements.append(item)
+
+ number = eval_multiply_numbers(numbers)
+
+ if infinity_factor:
+ numeric = [number]
+ non_numeric = []
+ for element in elements:
+ if element.is_numeric(evaluation):
+ numeric.append(element)
else:
- with mpmath.workprec(prec):
- numbers = [item.to_mpmath() for item in numbers]
- number = mpmath.fprod(numbers)
- number = from_mpmath(number, dps(prec))
- else:
- number = sympy.Mul(*[item.to_sympy() for item in numbers])
- number = from_sympy(number)
- else:
+ non_numeric.append(element)
+ elements = non_numeric
+ direction = (
+ numeric[0]
+ if len(numeric) == 1
+ else Times(*numeric).evaluate(evaluation)
+ )
+ elements.append(Expression(SymbolDirectedInfinity, direction))
number = Integer1
-
- if number.sameQ(Integer1):
- number = None
- elif number.is_zero:
- if infinity_factor:
- return SymbolIndeterminate
+ elif len(elements) == 0 or number is Integer0:
return number
- elif (
- number.sameQ(IntegerM1) and elements and elements[0].has_form("Plus", None)
- ):
+
+ if number is IntegerM1 and elements and elements[0].has_form("Plus", None):
elements[0] = Expression(
elements[0].get_head(),
*[
@@ -1023,21 +1062,22 @@ def eval(self, items, evaluation):
for element in elements[0].elements
],
)
- number = None
+ number = Integer1
- if number is not None:
+ if number is not Integer1:
elements.insert(0, number)
- if not elements:
- if infinity_factor:
- return SymbolComplexInfinity
- return Integer1
-
if len(elements) == 1:
- ret = elements[0]
- else:
- ret = Expression(SymbolTimes, *elements)
- if infinity_factor:
- return Expression(SymbolDirectedInfinity, ret)
- else:
- return ret
+ return elements[0]
+
+ elements = sorted(elements)
+ items_elements = items.elements
+ if len(elements) == len(items_elements) and all(
+ elem.sameQ(item) for elem, item in zip(elements, items_elements)
+ ):
+ return None
+ return Expression(
+ SymbolTimes,
+ *elements,
+ elements_properties=ElementsProperties(False, False, True),
+ )
diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py
index 28c279d7d..f5f4bfd72 100644
--- a/mathics/builtin/arithmetic.py
+++ b/mathics/builtin/arithmetic.py
@@ -7,6 +7,7 @@
Basic arithmetic functions, including complex number arithmetic.
"""
+from mathics.eval.arithmetic import eval_abs, eval_sign
from mathics.eval.numerify import numerify
# This tells documentation how to sort this module
@@ -56,17 +57,14 @@
from mathics.core.symbols import (
Atom,
Symbol,
- SymbolAbs,
SymbolFalse,
SymbolList,
SymbolPlus,
- SymbolPower,
SymbolTimes,
SymbolTrue,
)
from mathics.core.systemsymbols import (
SymbolAnd,
- SymbolComplexInfinity,
SymbolDirectedInfinity,
SymbolIndeterminate,
SymbolInfix,
@@ -78,6 +76,8 @@
)
from mathics.eval.nevaluator import eval_N
+ExpressionComplexInfinity = Expression(SymbolDirectedInfinity)
+
@lru_cache(maxsize=4096)
def call_mpmath(mpmath_function, mpmath_args):
@@ -86,7 +86,7 @@ def call_mpmath(mpmath_function, mpmath_args):
except ValueError as exc:
text = str(exc)
if text == "gamma function pole":
- return SymbolComplexInfinity
+ return ExpressionComplexInfinity
else:
raise
except ZeroDivisionError:
@@ -130,6 +130,8 @@ def eval(self, z, evaluation):
result = self.prepare_mathics(result)
result = from_sympy(result)
# evaluate elements to convert e.g. Plus[2, I] -> Complex[2, 1]
+ if result is None:
+ return result
return result.evaluate_elements(evaluation)
elif mpmath_function is None:
return
@@ -150,7 +152,7 @@ def eval(self, z, evaluation):
if isinstance(result, (mpmath.mpc, mpmath.mpf)):
if mpmath.isinf(result) and isinstance(result, mpmath.mpc):
- result = SymbolComplexInfinity
+ result = ExpressionComplexInfinity
elif mpmath.isinf(result) and result > 0:
result = Expression(SymbolDirectedInfinity, Integer1)
elif mpmath.isinf(result) and result < 0:
@@ -262,6 +264,13 @@ class Abs(_MPMathFunction):
summary_text = "absolute value of a number"
sympy_name = "Abs"
+ def eval(self, x, evaluation):
+ "%(name)s[x_]"
+ result = eval_abs(x)
+ if result is not None:
+ return result
+ return super(Abs, self).eval(x, evaluation)
+
class Arg(_MPMathFunction):
"""
@@ -636,7 +645,6 @@ class DirectedInfinity(SympyFunction):
= ComplexInfinity
>> DirectedInfinity[1 + I]
= (1 / 2 + I / 2) Sqrt[2] Infinity
-
>> 1 / DirectedInfinity[1 + I]
= 0
>> DirectedInfinity[1] + DirectedInfinity[-1]
@@ -648,7 +656,7 @@ class DirectedInfinity(SympyFunction):
= Indeterminate
#> DirectedInfinity[1+I]+DirectedInfinity[2+I]
- = (2 / 5 + I / 5) Sqrt[5] Infinity + (1 / 2 + I / 2) Sqrt[2] Infinity
+ = (2 / 5 + I / 5) Sqrt[5] Infinity + (1 / 2 + I / 2) Sqrt[2] Infinity
#> DirectedInfinity[Sqrt[3]]
= Infinity
@@ -658,7 +666,8 @@ class DirectedInfinity(SympyFunction):
rules = {
"DirectedInfinity[Indeterminate]": "Indeterminate",
"DirectedInfinity[args___] ^ -1": "0",
- "0 * DirectedInfinity[args___]": "Message[Infinity::indet, Unevaluated[0 DirectedInfinity[args]]]; Indeterminate",
+ "DirectedInfinity[DirectedInfinity[args___]]": "DirectedInfinity[args]",
+ # "0 * DirectedInfinity[args___]": "Message[Infinity::indet, Unevaluated[0 DirectedInfinity[args]]]; Indeterminate",
# "DirectedInfinity[a_?NumericQ] /; N[Abs[a]] != 1": "DirectedInfinity[a / Abs[a]]",
# "DirectedInfinity[a_] * DirectedInfinity[b_]": "DirectedInfinity[a*b]",
# "DirectedInfinity[] * DirectedInfinity[args___]": "DirectedInfinity[]",
@@ -691,24 +700,27 @@ class DirectedInfinity(SympyFunction):
formats = {
"DirectedInfinity[1]": "HoldForm[Infinity]",
- "DirectedInfinity[-1]": "HoldForm[-Infinity]",
+ "DirectedInfinity[-1]": "-HoldForm[Infinity]",
"DirectedInfinity[]": "HoldForm[ComplexInfinity]",
"DirectedInfinity[DirectedInfinity[z_]]": "DirectedInfinity[z]",
"DirectedInfinity[z_?NumericQ]": "HoldForm[z Infinity]",
}
- def eval(self, z, evaluation):
- """DirectedInfinity[z_]"""
- if z in (Integer1, IntegerM1):
+ def eval_directed_infinity(self, direction, evaluation):
+ """DirectedInfinity[direction_]"""
+ if direction in (Integer1, IntegerM1):
return None
- if isinstance(z, Number) or isinstance(eval_N(z, evaluation), Number):
- direction = (z / Expression(SymbolAbs, z)).evaluate(evaluation)
- return Expression(
- SymbolDirectedInfinity,
- direction,
- elements_properties=ElementsProperties(True, True, True),
- )
- return None
+ if direction.is_zero:
+ return ExpressionComplexInfinity
+
+ normalized_direction = eval_sign(direction)
+ if normalized_direction is None:
+ return None
+ return Expression(
+ SymbolDirectedInfinity,
+ normalized_direction.evaluate(evaluation),
+ elements_properties=ElementsProperties(True, False, False),
+ )
def to_sympy(self, expr, **kwargs):
if len(expr.elements) == 1:
@@ -1203,18 +1215,15 @@ class Sign(SympyFunction):
def eval(self, x, evaluation):
"%(name)s[x_]"
- # Sympy and mpmath do not give the desired form of complex number
- if isinstance(x, Complex):
- return Expression(
- SymbolTimes,
- x,
- Expression(SymbolPower, Expression(SymbolAbs, x), IntegerM1),
- )
-
+ result = eval_sign(x)
+ if result is not None:
+ return result
+ return None
sympy_x = x.to_sympy()
if sympy_x is None:
return None
- return super().eval(x, evaluation)
+ # Unhandled cases. Use sympy
+ return super(Sign, self).eval(x, evaluation)
def eval_error(self, x, seqs, evaluation):
"Sign[x_, seqs__]"
diff --git a/mathics/builtin/atomic/numbers.py b/mathics/builtin/atomic/numbers.py
index c5c4b405f..631102e01 100644
--- a/mathics/builtin/atomic/numbers.py
+++ b/mathics/builtin/atomic/numbers.py
@@ -24,12 +24,10 @@
from mathics.builtin.base import Builtin, Predefined, Test
from mathics.core.atoms import (
- Complex,
Integer,
Integer0,
Integer10,
MachineReal,
- MachineReal0,
Number,
Rational,
Real,
@@ -38,18 +36,20 @@
from mathics.core.convert.python import from_python
from mathics.core.expression import Expression
from mathics.core.list import ListExpression
-from mathics.core.number import dps, machine_epsilon, machine_precision
+from mathics.core.number import machine_epsilon, machine_precision
from mathics.core.symbols import Symbol, SymbolDivide
from mathics.core.systemsymbols import (
SymbolIndeterminate,
SymbolInfinity,
SymbolLog,
+ SymbolMachinePrecision,
SymbolN,
SymbolPrecision,
SymbolRealDigits,
SymbolRound,
)
from mathics.eval.nevaluator import eval_N
+from mathics.eval.numbers import MACHINE_PRECISION_VALUE, eval_accuracy, eval_precision
SymbolIntegerDigits = Symbol("IntegerDigits")
SymbolIntegerExponent = Symbol("IntegerExponent")
@@ -203,32 +203,9 @@ class Accuracy(Builtin):
def eval(self, z, evaluation):
"Accuracy[z_]"
- if isinstance(z, Real):
- if z.is_zero:
- return MachineReal(dps(z.get_precision()))
- z_f = z.to_python()
- log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10)
- return MachineReal(dps(z.get_precision()) - log10_z)
-
- if isinstance(z, Complex):
- acc_real = self.eval(z.real, evaluation)
- acc_imag = self.eval(z.imag, evaluation)
- if acc_real is SymbolInfinity:
- return acc_imag
- if acc_imag is SymbolInfinity:
- return acc_real
- return Real(min(acc_real.to_python(), acc_imag.to_python()))
-
- if isinstance(z, Expression):
- result = None
- for element in z.elements:
- candidate = self.eval(element, evaluation)
- if isinstance(candidate, Real):
- candidate_f = candidate.to_python()
- if result is None or candidate_f < result:
- result = candidate_f
- if result is not None:
- return Real(result)
+ acc = eval_accuracy(z)
+ if acc is not None:
+ return Real(acc)
return SymbolInfinity
@@ -954,37 +931,16 @@ class Precision(Builtin):
/doc/reference-of-built-in-symbols/atomic-elements-of-expressions/representation-of-numbers/accuracy/.
"""
- rules = {
- "Precision[z_?MachineNumberQ]": "MachinePrecision",
- }
-
summary_text = "find the precision of a number"
def eval(self, z, evaluation):
"Precision[z_]"
- if isinstance(z, Real):
- if z.is_zero:
- return MachineReal0
- return MachineReal(dps(z.get_precision()))
-
- if isinstance(z, Complex):
- prec_real = self.eval(z.real, evaluation)
- prec_imag = self.eval(z.imag, evaluation)
- if prec_real is SymbolInfinity:
- return prec_imag
- if prec_imag is SymbolInfinity:
- return prec_real
-
- return Real(min(prec_real.to_python(), prec_imag.to_python()))
-
- if isinstance(z, Expression):
- result = None
- for element in z.elements:
- candidate = self.eval(element, evaluation)
- if isinstance(candidate, Real):
- candidate_f = candidate.to_python()
- if result is None or candidate_f < result:
- result = candidate_f
- if result is not None:
- return Real(result)
- return SymbolInfinity
+ if isinstance(z, MachineReal):
+ return SymbolMachinePrecision
+ prec = eval_precision(z)
+ if prec is None:
+ return SymbolInfinity
+ if prec == MACHINE_PRECISION_VALUE:
+ return SymbolMachinePrecision
+
+ return MachineReal(prec)
diff --git a/mathics/builtin/testing_expressions/numerical_properties.py b/mathics/builtin/testing_expressions/numerical_properties.py
index 2a7af66b4..3c95f38ac 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 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
@@ -373,6 +374,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/number.py b/mathics/core/number.py
index ba166ed61..9608a9c67 100644
--- a/mathics/core/number.py
+++ b/mathics/core/number.py
@@ -119,12 +119,10 @@ def prec(dps) -> int:
def min_prec(*args):
- result = None
- for arg in args:
- prec = arg.get_precision()
- if result is None or (prec is not None and prec < result):
- result = prec
- return result
+ args_prec = (arg.get_precision() for arg in args)
+ return min(
+ (arg_prec for arg_prec in args_prec if arg_prec is not None), default=None
+ )
def pickle_mp(value):
diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py
index 05635182e..0c435f139 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")
@@ -81,6 +82,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")
@@ -105,6 +107,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")
@@ -122,6 +125,7 @@
SymbolLess = Symbol("System`Less")
SymbolLessEqual = Symbol("System`LessEqual")
SymbolKey = Symbol("System`Key")
+SymbolKhinchin = Symbol("System`Khinchin")
SymbolLine = Symbol("System`Line")
SymbolLog = Symbol("System`Log")
SymbolLog10 = Symbol("System`Log10")
@@ -173,6 +177,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")
@@ -233,6 +238,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
new file mode 100644
index 000000000..fdc191d28
--- /dev/null
+++ b/mathics/eval/arithmetic.py
@@ -0,0 +1,815 @@
+# -*- coding: utf-8 -*-
+
+"""
+helper functions for arithmetic evaluation, which do not
+depends on the evaluation context. Conversions to Sympy are
+used just as a last resource.
+"""
+
+from typing import List, Optional, Tuple
+
+import mpmath
+import sympy
+
+from mathics.core.atoms import (
+ Complex,
+ Integer,
+ Integer0,
+ Integer1,
+ IntegerM1,
+ Number,
+ Rational,
+ RationalOneHalf,
+ Real,
+)
+from mathics.core.convert.mpmath import from_mpmath
+from mathics.core.convert.sympy import from_sympy
+from mathics.core.element import BaseElement
+from mathics.core.expression import Expression
+from mathics.core.number import dps, min_prec
+from mathics.core.rules import Rule
+from mathics.core.symbols import Atom
+from mathics.core.systemsymbols import (
+ SymbolAbs,
+ SymbolE,
+ SymbolEulerGamma,
+ SymbolExp,
+ SymbolI,
+ SymbolKhinchin,
+ SymbolLog,
+ SymbolPi,
+ SymbolPlus,
+ SymbolPower,
+ SymbolSign,
+ SymbolTimes,
+)
+from mathics.eval.numbers import eval_accuracy
+
+RationalMOneHalf = Rational(-1, 2)
+RealM0p5 = Real(-0.5)
+RealOne = Real(1.0)
+
+
+# Here we store numerical constants with their approximate
+# numerical values.
+
+NUMERICAL_CONSTANTS = {
+ SymbolE: 2.718281828,
+ SymbolEulerGamma: 0.5772156649,
+ SymbolKhinchin: 2.685452001,
+ SymbolPi: 3.141592654,
+}
+
+
+# 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_abs(expr: BaseElement) -> Optional[BaseElement]:
+ """
+ evaluates Abs[expr]
+ """
+ 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_add_numbers(
+ numbers: List[Number],
+) -> Number:
+ """
+ Add the elements in ``numbers``.
+ """
+ if len(numbers) == 0:
+ return Integer0
+
+ prec = min_prec(*numbers)
+ if prec is not None:
+ is_machine_precision = any(number.is_machine_precision() for number in numbers)
+ if is_machine_precision:
+ numbers = (item.to_mpmath() for item in numbers)
+ number = mpmath.fsum(numbers)
+ return from_mpmath(number)
+ else:
+ # For a sum, what is relevant is the minimum accuracy of the terms
+ item_accuracies = (eval_accuracy(item) for item in numbers)
+ acc = min(
+ (item_acc for item_acc in item_accuracies if item_acc is not None),
+ default=None,
+ )
+ with mpmath.workprec(prec):
+ numbers = (item.to_mpmath() for item in numbers)
+ number = mpmath.fsum(numbers)
+ return from_mpmath(number, acc=acc)
+ else:
+ return from_sympy(sum(item.to_sympy() for item in numbers))
+
+
+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_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, dps(prec))
+
+
+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``.
+ """
+ if len(numbers) == 0:
+ return Integer1
+
+ prec = min_prec(*numbers)
+ if prec is not None:
+ is_machine_precision = any(number.is_machine_precision() for number in numbers)
+ if is_machine_precision:
+ numbers = (item.to_mpmath() for item in numbers)
+ number = mpmath.fprod(numbers)
+ return from_mpmath(number)
+ else:
+ with mpmath.workprec(prec):
+ numbers = (item.to_mpmath() for item in numbers)
+ number = mpmath.fprod(numbers)
+ return from_mpmath(number, dps(prec))
+ else:
+ 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 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, dps(prec))
+
+
+def eval_sign(expr: BaseElement) -> Optional[BaseElement]:
+ """
+ evaluates Sign[expr]
+ """
+ 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
+ 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
+
+
+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)])
+
+
+# TODO: Annotate me
+def segregate_numbers(
+ *elements: BaseElement,
+) -> Tuple[List[Number], List[BaseElement]]:
+ """
+ From a list of elements, produce two lists, one with the numeric items
+ and the other with the remaining
+ """
+ items = {True: [], False: []}
+ for element in elements:
+ items[isinstance(element, Number)].append(element)
+ return items[True], items[False]
+
+
+# TODO: Annotate me
+def segregate_numbers_from_sorted_list(
+ *elements: BaseElement,
+) -> Tuple[List[Number], List[BaseElement]]:
+ """
+ From a list of elements, produce two lists, one with the numeric items
+ and the other with the remaining. Differently than `segregate_numbers`,
+ this function assumes that elements are sorted with the numbers at
+ the begining.
+ """
+ for pos, element in enumerate(elements):
+ if not isinstance(element, Number):
+ return list(elements[:pos]), list(elements[pos:])
+ return list(elements), []
+
+
+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 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):
+ return not only_real
+
+ head, elements = expr.get_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)
+ if not only_real:
+ if expr is SymbolI or isinstance(expr, Complex):
+ return True
+ 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 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 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(exponent)
+ 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 head.has_form("Abs", 1):
+ return True
+ 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
+ return test_zero_arithmetic_expr(base, numeric) and test_arithmetic_expr(
+ exp, numeric
+ )
+ 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, Real(value)))
+ return eval_multiply_numbers([RealOne, expr])
diff --git a/mathics/eval/numbers.py b/mathics/eval/numbers.py
index 4e1db1dc3..48beeff26 100644
--- a/mathics/eval/numbers.py
+++ b/mathics/eval/numbers.py
@@ -1,9 +1,102 @@
+from typing import Optional
+
+import mpmath
import sympy
+from mathics.core.atoms import Complex, MachineReal, PrecisionReal, Real
from mathics.core.convert.sympy import from_sympy
+from mathics.core.element import BaseElement
from mathics.core.expression import Expression
+from mathics.core.number import dps, machine_precision
from mathics.core.symbols import SymbolPlus
+MACHINE_PRECISION_VALUE = machine_precision * mpmath.log(2, 10.0)
+
+
+def eval_accuracy(z: BaseElement) -> Optional[float]:
+ """
+ Determine the accuracy of an expression expr.
+ If z is a Real value, returns the difference between
+ the number of significant decimal figures (Precision) and
+ log_10(z).
+
+ For example,
+ ```
+ 12.345`2
+ ```
+ which is equivalent to 12.`2 has an accuracy of
+ ```
+ 0.908509 == 2. - log(10, 12.345)
+ ```
+
+ If the expression contains Real values, returns
+ the minimal accuracy of all the numbers in the expression.
+
+ Otherwise returns None, representing infinite accuracy.
+ """
+ if isinstance(z, Real):
+ if z.is_zero:
+ # WMA returns 323.607 ~ $MachinePrecision - Log[10, 2.225073*^-308]
+ # i.e. the accuracy for the smallest positive double precision value.
+ return dps(z.get_precision())
+ z_f = z.to_python()
+ log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10)
+ return dps(z.get_precision()) - log10_z
+
+ if isinstance(z, Complex):
+ acc_real = eval_accuracy(z.real)
+ acc_imag = eval_accuracy(z.imag)
+ if acc_real is None:
+ return acc_imag
+ if acc_imag is None:
+ return acc_real
+ return min(acc_real, acc_imag)
+
+ if isinstance(z, Expression):
+ elem_accuracies = (eval_accuracy(z_elem) for z_elem in z.elements)
+ return min((acc for acc in elem_accuracies if acc is not None), default=None)
+ return None
+
+
+def eval_precision(z: BaseElement) -> Optional[float]:
+ """
+ Determine the precision of an expression expr.
+ If z is a Real value, returns the number of significant
+ decimal figures of z. For example,
+ ```
+ 12.345`2
+ ```
+ which is equivalent to 12.`2 has a precision of 2.
+
+ If the expression contains Real values, returns
+ the minimal accuracy of all the numbers in the expression.
+
+ Otherwise returns None, representing infinite precision.
+ """
+
+ if isinstance(z, MachineReal):
+ return MACHINE_PRECISION_VALUE
+
+ if isinstance(z, PrecisionReal):
+ if z.is_zero:
+ return MACHINE_PRECISION_VALUE
+ return float(dps(z.get_precision()))
+
+ if isinstance(z, Complex):
+ prec_real = eval_precision(z.real)
+ prec_imag = eval_precision(z.imag)
+ if prec_real is None:
+ return prec_imag
+ if prec_imag is None:
+ return prec_real
+ return min(prec_real, prec_imag)
+
+ if isinstance(z, Expression):
+ elem_prec = (eval_precision(z_elem) for z_elem in z.elements)
+ return min((prec for prec in elem_prec if prec is not None), default=None)
+
+ return None
+
def cancel(expr):
if expr.has_form("Plus", None):
diff --git a/test/builtin/arithmetic/test_abs.py b/test/builtin/arithmetic/test_abs.py
index f17bb24ea..890e80d6f 100644
--- a/test/builtin/arithmetic/test_abs.py
+++ b/test/builtin/arithmetic/test_abs.py
@@ -39,8 +39,8 @@ 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),
+ ("Sign[I^(2 Pi)]", "I^(2 Pi)", None),
# FixME: add rules to handle this kind of case
- # ("Sign[I^(2 Pi)]", "I^(2 Pi)", None),
# ("Sign[4^(2 Pi I)]", "1", None),
],
)
diff --git a/test/builtin/arithmetic/test_basic.py b/test/builtin/arithmetic/test_basic.py
index cc771eb00..0708b02c7 100644
--- a/test/builtin/arithmetic/test_basic.py
+++ b/test/builtin/arithmetic/test_basic.py
@@ -129,7 +129,6 @@ def test_exponential(str_expr, str_expected):
# ("a b DirectedInfinity[-3]", "a b (-Infinity)", ""),
],
)
-@pytest.mark.xfail
def test_multiply(str_expr, str_expected, msg):
check_evaluation(str_expr, str_expected, failure_message=msg, hold_expected=True)
@@ -190,6 +189,5 @@ def test_multiply(str_expr, str_expected, msg):
("(a^(p-2 q))^3.", "(a ^ (p - 2 q)) ^ 3.", None),
],
)
-@pytest.mark.xfail
def test_power(str_expr, str_expected, msg):
check_evaluation(str_expr, str_expected, failure_message=msg)
diff --git a/test/builtin/atomic/test_numbers.py b/test/builtin/atomic/test_numbers.py
index 9039abc63..0bdd83ee0 100644
--- a/test/builtin/atomic/test_numbers.py
+++ b/test/builtin/atomic/test_numbers.py
@@ -205,9 +205,9 @@ def test_precision():
for str_expr, str_expected in (
("0`4", "0"),
("Precision[0.0]", "MachinePrecision"),
- ("Precision[0.000000000000000000000000000000000000]", "0."),
+ ("Precision[0.000000000000000000000000000000000000]", "MachinePrecision"),
("Precision[-0.0]", "MachinePrecision"),
- ("Precision[-0.000000000000000000000000000000000000]", "0."),
+ ("Precision[-0.000000000000000000000000000000000000]", "MachinePrecision"),
("1.0000000000000000 // Precision", "MachinePrecision"),
("1.00000000000000000 // Precision", "17."),
(" 0.4 + 2.4 I // Precision", "MachinePrecision"),
diff --git a/test/format/test_format.py b/test/format/test_format.py
index eaca6fec2..2130d1ed6 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