diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 06a4337b0..8eaccab79 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -799,10 +799,14 @@ def get_operator_display(self) -> Optional[str]: class Predefined(Builtin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.symbol = Symbol(self.get_name()) + def get_functions(self, prefix="eval", is_pymodule=False) -> List[Callable]: functions = list(super().get_functions(prefix)) if prefix == "eval" and hasattr(self, "evaluate"): - functions.append((Symbol(self.get_name()), self.evaluate)) + functions.append((self.symbol, self.evaluate)) return functions diff --git a/mathics/builtin/numbers/constants.py b/mathics/builtin/numbers/constants.py index 787826869..73779ff6a 100644 --- a/mathics/builtin/numbers/constants.py +++ b/mathics/builtin/numbers/constants.py @@ -9,25 +9,19 @@ # This tells documentation how to sort this module sort_order = "mathics.builtin.mathematical-constants" - import math +from typing import Optional import mpmath import numpy import sympy from mathics.builtin.base import Builtin, Predefined, SympyObject -from mathics.core.atoms import MachineReal, PrecisionReal +from mathics.core.atoms import NUMERICAL_CONSTANTS, MachineReal, PrecisionReal from mathics.core.attributes import A_CONSTANT, A_PROTECTED, A_READ_PROTECTED +from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation -from mathics.core.number import ( - MACHINE_DIGITS, - MAX_MACHINE_NUMBER, - MIN_MACHINE_NUMBER, - PrecisionValueError, - get_precision, - prec, -) +from mathics.core.number import MACHINE_DIGITS, PrecisionValueError, get_precision, prec from mathics.core.symbols import Atom, Symbol, strip_context from mathics.core.systemsymbols import SymbolIndeterminate @@ -89,28 +83,33 @@ def eval_N(self, precision, evaluation): def is_constant(self) -> bool: return True - def get_constant(self, precision, evaluation): + def get_constant( + self, + precision: Optional[BaseElement] = None, + evaluation: Optional[Evaluation] = None, + ): # first, determine the precision d = None - if precision: - try: - d = get_precision(precision, evaluation) - except PrecisionValueError: - pass + preference = None + if evaluation: + if precision: + try: + d = get_precision(precision, evaluation) + except PrecisionValueError: + pass + + preflist = evaluation._preferred_n_method.copy() + while preflist: + pref_method = preflist.pop() + if pref_method in ("numpy", "mpmath", "sympy"): + preference = pref_method + break if d is None: d = MACHINE_DIGITS # If preference not especified, determine it # from the precision. - preference = None - preflist = evaluation._preferred_n_method.copy() - while preflist: - pref_method = preflist.pop() - if pref_method in ("numpy", "mpmath", "sympy"): - preference = pref_method - break - if preference is None: if d <= MACHINE_DIGITS: preference = "numpy" @@ -131,10 +130,16 @@ def get_constant(self, precision, evaluation): preference = "mpmath" else: preference = "" + if preference == "numpy": - value = numpy_constant(self.numpy_name) if d == MACHINE_DIGITS: - return MachineReal(value) + try: + return NUMERICAL_CONSTANTS[self.symbol] + except KeyError: + value = MachineReal(numpy_constant(self.numpy_name)) + NUMERICAL_CONSTANTS[self.symbol] = value + return value + value = numpy_constant(self.numpy_name) if preference == "sympy": value = sympy_constant(self.sympy_name, d + 2) if preference == "mpmath": @@ -177,13 +182,16 @@ class _NumpyConstant(_Constant_Common): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.numpy_name is None: - self.numpy_name = strip_context(self.get_name()).lower() + self.numpy_name = strip_context(self.symbol.name).lower() self.mathics_to_numpy[self.__class__.__name__] = self.numpy_name + try: + value_float = numpy_constant(self.numpy_name) + except AttributeError: + value_float = self.to_numpy(self.symbol) + NUMERICAL_CONSTANTS[self.symbol] = MachineReal(value_float) def to_numpy(self, args): - if self.numpy_name is None or len(args) != 0: - return None - return self.get_constant() + return NUMERICAL_CONSTANTS[self.symbol] class _SympyConstant(_Constant_Common, SympyObject): @@ -608,7 +616,7 @@ class MaxMachineNumber(Predefined): summary_text = "largest normalized positive machine number" def evaluate(self, evaluation: Evaluation) -> MachineReal: - return MachineReal(MAX_MACHINE_NUMBER) + return NUMERICAL_CONSTANTS[self.symbol] class MinMachineNumber(Predefined): @@ -635,10 +643,10 @@ class MinMachineNumber(Predefined): summary_text = "smallest normalized positive machine number" def evaluate(self, evaluation: Evaluation) -> MachineReal: - return MachineReal(MIN_MACHINE_NUMBER) + return NUMERICAL_CONSTANTS[self.symbol] -class Pi(_MPMathConstant, _SympyConstant): +class Pi(_MPMathConstant, _NumpyConstant, _SympyConstant): """ :Pi, \u03c0: https://en.wikipedia.org/wiki/Pi ( @@ -740,3 +748,10 @@ class Underflow(Builtin): "Underflow[] * x_Real": "0.", } summary_text = "underflow in numeric evaluation" + + +# Constants that are not numpy constants, +for cls in (Catalan, Degree, Glaisher, GoldenRatio, Khinchin): + instance = cls(expression=False) + val = instance.get_constant() + NUMERICAL_CONSTANTS[instance.symbol] = MachineReal(val.value) diff --git a/mathics/builtin/testing_expressions/numerical_properties.py b/mathics/builtin/testing_expressions/numerical_properties.py index 8de45e8b1..5f5822f9e 100644 --- a/mathics/builtin/testing_expressions/numerical_properties.py +++ b/mathics/builtin/testing_expressions/numerical_properties.py @@ -13,6 +13,7 @@ from mathics.core.expression import Expression from mathics.core.symbols import BooleanType, SymbolFalse, SymbolTrue from mathics.core.systemsymbols import SymbolExpandAll, SymbolSimplify +from mathics.eval.arithmetic import test_zero_arithmetic_expr from mathics.eval.nevaluator import eval_N @@ -460,6 +461,10 @@ def eval(self, expr, evaluation): "%(name)s[expr_]" from sympy.matrices.utilities import _iszero + # This handles most of the arithmetic cases + if test_zero_arithmetic_expr(expr): + return SymbolTrue + sympy_expr = expr.to_sympy() result = _iszero(sympy_expr) if result is None: diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index f2b5c91a1..eadc66ad0 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -13,6 +13,8 @@ from mathics.core.number import ( FP_MANTISA_BINARY_DIGITS, MACHINE_PRECISION_VALUE, + MAX_MACHINE_NUMBER, + MIN_MACHINE_NUMBER, dps, min_prec, prec, @@ -946,6 +948,14 @@ def is_zero(self) -> bool: MATHICS3_COMPLEX_I = Complex(Integer0, Integer1) MATHICS3_COMPLEX_I_NEG = Complex(Integer0, IntegerM1) +# Numerical constants +# These constants are populated by the `Predefined` +# classes. See `mathics.builtin.numbers.constants` +NUMERICAL_CONSTANTS = { + Symbol("System`$MaxMachineNumber"): MachineReal(MAX_MACHINE_NUMBER), + Symbol("System`$MinMachineNumber"): MachineReal(MIN_MACHINE_NUMBER), +} + class String(Atom, BoxElementMixin): value: str diff --git a/mathics/core/number.py b/mathics/core/number.py index f30de3b92..01673f0ea 100644 --- a/mathics/core/number.py +++ b/mathics/core/number.py @@ -68,7 +68,9 @@ def _get_float_inf(value, evaluation) -> Optional[float]: return value.round_to_float(evaluation) -def get_precision(value, evaluation, show_messages=True) -> Optional[float]: +def get_precision( + value: BaseElement, evaluation, show_messages: bool = True +) -> Optional[float]: """ Returns the ``float`` in the interval [``$MinPrecision``, ``$MaxPrecision``] closest to ``value``. If ``value`` does not belongs to that interval, and ``show_messages`` is True, a Message warning is shown. diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 52659e775..f463e5f91 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -24,6 +24,7 @@ # This list is sorted in alphabetic order. SymbolAborted = Symbol("System`$Aborted") +SymbolAbs = Symbol("System`Abs") SymbolAccuracy = Symbol("System`Accuracy") SymbolAll = Symbol("System`All") SymbolAlternatives = Symbol("System`Alternatives") @@ -84,6 +85,7 @@ SymbolEquivalent = Symbol("System`Equivalent") SymbolEulerGamma = Symbol("System`EulerGamma") SymbolExactNumberQ = Symbol("System`ExactNumberQ") +SymbolExp = Symbol("System`Exp") SymbolExpandAll = Symbol("System`ExpandAll") SymbolExport = Symbol("System`Export") SymbolExportString = Symbol("System`ExportString") @@ -109,6 +111,7 @@ SymbolHoldForm = Symbol("System`HoldForm") SymbolHoldPattern = Symbol("System`HoldPattern") SymbolHue = Symbol("System`Hue") +SymbolI = Symbol("System`I") SymbolIf = Symbol("System`If") SymbolIm = Symbol("System`Im") SymbolImage = Symbol("System`Image") @@ -126,6 +129,7 @@ SymbolLess = Symbol("System`Less") SymbolLessEqual = Symbol("System`LessEqual") SymbolKey = Symbol("System`Key") +SymbolKhinchin = Symbol("System`Khinchin") SymbolLetterCharacter = Symbol("System`LetterCharacter") SymbolLine = Symbol("System`Line") SymbolLog = Symbol("System`Log") @@ -179,6 +183,7 @@ SymbolPi = Symbol("System`Pi") SymbolPiecewise = Symbol("System`Piecewise") SymbolPlot = Symbol("System`Plot") +SymbolPlus = Symbol("System`Plus") SymbolPoint = Symbol("System`Point") SymbolPower = Symbol("System`Power") SymbolPolygon = Symbol("System`Polygon") @@ -242,6 +247,7 @@ SymbolTan = Symbol("System`Tan") SymbolTanh = Symbol("System`Tanh") SymbolTeXForm = Symbol("System`TeXForm") +SymbolTimes = Symbol("System`Times") SymbolThrow = Symbol("System`Throw") SymbolThreshold = Symbol("System`Threshold") SymbolToString = Symbol("System`ToString") diff --git a/mathics/eval/arithmetic.py b/mathics/eval/arithmetic.py index 0b9564067..00976ac7b 100644 --- a/mathics/eval/arithmetic.py +++ b/mathics/eval/arithmetic.py @@ -14,12 +14,14 @@ import sympy from mathics.core.atoms import ( + NUMERICAL_CONSTANTS, Complex, Integer, Integer0, Integer1, Integer2, IntegerM1, + MachineReal, Number, Rational, RationalOneHalf, @@ -29,11 +31,24 @@ from mathics.core.convert.sympy import from_sympy from mathics.core.element import BaseElement, ElementsProperties from mathics.core.expression import Expression -from mathics.core.number import FP_MANTISA_BINARY_DIGITS, SpecialValueError, min_prec -from mathics.core.symbols import Symbol, SymbolPlus, SymbolPower, SymbolTimes -from mathics.core.systemsymbols import SymbolComplexInfinity, SymbolIndeterminate +from mathics.core.number import ( + FP_MANTISA_BINARY_DIGITS, + MAX_MACHINE_NUMBER, + MIN_MACHINE_NUMBER, + SpecialValueError, + min_prec, +) +from mathics.core.rules import Rule +from mathics.core.symbols import Atom, Symbol, SymbolPlus, SymbolPower, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolComplexInfinity, + SymbolI, + SymbolIndeterminate, + SymbolLog, +) RationalMOneHalf = Rational(-1, 2) +RealOne = Real(1.0) # This cache might not be used that much. @@ -374,3 +389,222 @@ def segregate_numbers_from_sorted_list( if not isinstance(element, Number): return list(elements[:pos]), list(elements[pos:]) return list(elements), [] + + +def test_arithmetic_expr(expr: BaseElement, only_real: bool = True) -> bool: + """ + Check if an expression `expr` is an arithmetic expression + composed only by numbers and arithmetic operations. + If only_real is set to True, then `I` is not considered a number. + """ + if isinstance(expr, (Integer, Rational, Real)): + return True + if expr in NUMERICAL_CONSTANTS: + return True + if isinstance(expr, Complex) or expr is SymbolI: + return not only_real + if isinstance(expr, Symbol): + return False + + head, elements = expr.head, expr.elements + + if head in (SymbolPlus, SymbolTimes): + return all(test_arithmetic_expr(term, only_real) for term in elements) + if expr.has_form("Exp", 1): + return test_arithmetic_expr(elements[0], only_real) + if head is SymbolLog: + if len(elements) > 2: + return False + if len(elements) == 2: + base = elements[0] + if not test_positive_arithmetic_expr(base): + return False + return test_arithmetic_expr(elements[-1], only_real) + if expr.has_form("Power", 2): + base, exponent = elements + if only_real: + if isinstance(exponent, Integer): + return test_arithmetic_expr(base) + return all(test_arithmetic_expr(item, only_real) for item in elements) + return False + + +def test_negative_arithmetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a negative value. + """ + if isinstance(expr, (Integer, Rational, Real)): + return expr.value < 0 + + expr = eval_multiply_numbers(IntegerM1, expr) + return test_positive_arithmetic_expr(expr) + + +def test_nonnegative_arithmetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a nonnegative number + """ + if not test_arithmetic_expr(expr): + return False + + if test_zero_arithmetic_expr(expr) or test_positive_arithmetic_expr(expr): + return True + + +def test_nonpositive_arithetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a nonnegative number + """ + if not test_arithmetic_expr(expr): + return False + + if test_zero_arithmetic_expr(expr) or test_negative_arithmetic_expr(expr): + return True + return False + + +def test_positive_arithmetic_expr(expr: BaseElement) -> bool: + """ + Check if the expression is an arithmetic expression + representing a positive value. + """ + if isinstance(expr, (Integer, Rational, Real)): + return expr.value > 0 + if expr in NUMERICAL_CONSTANTS: + return True + if isinstance(expr, Atom): + return False + + head, elements = expr.get_head(), expr.elements + + if head is SymbolPlus: + positive_nonpositive_terms = {True: [], False: []} + for term in elements: + positive_nonpositive_terms[test_positive_arithmetic_expr(term)].append(term) + + if len(positive_nonpositive_terms[False]) == 0: + return True + if len(positive_nonpositive_terms[True]) == 0: + return False + + pos, neg = ( + eval_add_numbers(*items) for items in positive_nonpositive_terms.values() + ) + if neg.is_zero: + return True + if not test_arithmetic_expr(neg): + return False + + total = eval_add_numbers(pos, neg) + # Check positivity of the evaluated expression + if isinstance(total, (Integer, Rational, Real)): + return total.value > 0 + if isinstance(total, Complex): + return False + if total.sameQ(expr): + return False + return test_positive_arithmetic_expr(total) + + if head is SymbolTimes: + nonpositive_factors = tuple( + (item for item in elements if not test_positive_arithmetic_expr(item)) + ) + if len(nonpositive_factors) == 0: + return True + evaluated_expr = eval_multiply_numbers(*nonpositive_factors) + if evaluated_expr.sameQ(expr): + return False + return test_positive_arithmetic_expr(evaluated_expr) + if expr.has_form("Power", 2): + base, exponent = elements + if isinstance(exponent, Integer) and exponent.value % 2 == 0: + return test_arithmetic_expr(base) + return test_arithmetic_expr(exponent) and test_positive_arithmetic_expr(base) + if expr.has_form("Exp", 1): + return test_arithmetic_expr(expr.elements[0], only_real=True) + if expr.has_form("Sqrt", 1): + return test_positive_arithmetic_expr(expr.elements[0]) + if head is SymbolLog: + if len(elements) > 2: + return False + if len(elements) == 2: + if not test_positive_arithmetic_expr(elements[0]): + return False + arg = elements[-1] + return test_positive_arithmetic_expr(eval_add_numbers(arg, IntegerM1)) + if expr.has_form("Abs", 1): + arg = elements[0] + return test_arithmetic_expr( + arg, only_real=False + ) and not test_zero_arithmetic_expr(arg) + if head.has_form("DirectedInfinity", 1): + return test_positive_arithmetic_expr(elements[0]) + + return False + + +def test_zero_arithmetic_expr(expr: BaseElement, numeric: bool = False) -> bool: + """ + return True if expr evaluates to a number compatible + with 0 + """ + + def is_numeric_zero(z: Number): + if isinstance(z, Complex): + if abs(z.real.value) + abs(z.imag.value) < 2.0e-10: + return True + if isinstance(z, Number): + if abs(z.value) < 1e-10: + return True + return False + + if expr.is_zero: + return True + if numeric: + if is_numeric_zero(expr): + return True + expr = to_inexact_value(expr) + if expr.has_form("Times", None): + if any( + test_zero_arithmetic_expr(element, numeric=numeric) + for element in expr.elements + ) and not any( + element.has_form("DirectedInfinity", None) for element in expr.elements + ): + return True + if expr.has_form("Power", 2): + base, exp = expr.elements + if test_zero_arithmetic_expr(base, numeric): + return test_nonnegative_arithmetic_expr(exp) + if base.has_form("DirectedInfinity", None): + return test_positive_arithmetic_expr(exp) + if expr.has_form("Plus", None): + result = eval_add_numbers(*expr.elements) + if numeric: + if isinstance(result, complex): + if abs(result.real.value) + abs(result.imag.value) < 2.0e-10: + return True + if isinstance(result, Number): + if abs(result.value) < 1e-10: + return True + return result.is_zero + return False + + +def to_inexact_value(expr: BaseElement) -> BaseElement: + """ + Converts an expression into an inexact expression. + Replaces numerical constants by their numerical approximation, + and then multiplies the expression by Real(1.) + """ + if expr.is_inexact(): + return expr + + if isinstance(expr, Expression): + for const, value in NUMERICAL_CONSTANTS.items(): + expr, success = expr.do_apply_rule(Rule(const, value)) + + return eval_multiply_numbers(RealOne, expr) diff --git a/mathics/eval/parts.py b/mathics/eval/parts.py index 3ecf356d1..61a1adf33 100644 --- a/mathics/eval/parts.py +++ b/mathics/eval/parts.py @@ -6,7 +6,7 @@ from typing import List -from mathics.core.atoms import Integer, Integer1 +from mathics.core.atoms import Integer from mathics.core.convert.expression import make_expression from mathics.core.element import BaseElement, BoxElementMixin from mathics.core.exceptions import ( @@ -20,11 +20,7 @@ from mathics.core.list import ListExpression from mathics.core.subexpression import SubExpression from mathics.core.symbols import Atom, Symbol, SymbolList -from mathics.core.systemsymbols import ( - SymbolDirectedInfinity, - SymbolInfinity, - SymbolNothing, -) +from mathics.core.systemsymbols import SymbolInfinity, SymbolNothing from mathics.eval.patterns import Matcher diff --git a/mathics/session.py b/mathics/session.py index 410d9c2b1..043106b34 100644 --- a/mathics/session.py +++ b/mathics/session.py @@ -100,3 +100,9 @@ def format_result(self, str_expression=None, timeout=None, form=None): if form is None: form = self.form return res.do_format(self.evaluation, form) + + def parse(self, str_expression): + """ + Just parse the expression + """ + return parse(self.definitions, MathicsSingleLineFeeder(str_expression)) diff --git a/test/builtin/arithmetic/test_abs.py b/test/builtin/arithmetic/test_abs.py index c523bf1d3..11b3da92b 100644 --- a/test/builtin/arithmetic/test_abs.py +++ b/test/builtin/arithmetic/test_abs.py @@ -42,8 +42,7 @@ def test_abs(str_expr, str_expected, msg): ("Sign[2+3 I]", "(2 + 3 I)/(13^(1/2))", None), ("Sign[2.+3 I]", "0.5547 + 0.83205 I", None), ("Sign[4^(2 Pi)]", "1", None), - # FIXME: add rules to handle this kind of case - # ("Sign[I^(2 Pi)]", "I^(2 Pi)", None), + ("Sign[I^(2 Pi)]", "I^(2 Pi)", None), # ("Sign[4^(2 Pi I)]", "1", None), ], ) diff --git a/test/builtin/arithmetic/test_lowlevel_properties.py b/test/builtin/arithmetic/test_lowlevel_properties.py new file mode 100644 index 000000000..10cebfd06 --- /dev/null +++ b/test/builtin/arithmetic/test_lowlevel_properties.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.eval.arithmetic low level positivity tests +""" +from test.helper import session + +import pytest + +from mathics.eval.arithmetic import ( + test_arithmetic_expr as check_arithmetic, + test_positive_arithmetic_expr as check_positive, + test_zero_arithmetic_expr as check_zero, +) + + +@pytest.mark.parametrize( + ("str_expr", "expected", "msg"), + [ + ("I", False, None), + ("0", False, None), + ("1", True, None), + ("Pi", True, None), + ("a", False, None), + ("-Pi", False, None), + ("(-1)^2", True, None), + ("(-1)^3", False, None), + ("Sqrt[2]", True, None), + ("Sqrt[-2]", False, None), + ("(-2)^(1/2)", False, None), + ("(2)^(1/2)", True, None), + ("Exp[a]", False, None), + ("Exp[2.3]", True, None), + ("Log[1/2]", False, None), + ("Exp[I]", False, None), + ("Log[3]", True, None), + ("Log[I]", False, None), + ("Abs[a]", False, None), + ("Abs[0]", False, None), + ("Abs[1+3 I]", True, None), + ("Sin[Pi]", False, None), + ], +) +def test_positivity(str_expr, expected, msg): + expr = session.parse(str_expr) + if msg: + assert check_positive(expr) == expected, msg + else: + assert check_positive(expr) == expected + + +@pytest.mark.parametrize( + ("str_expr", "expected", "msg"), + [ + ("I", False, None), + ("0", True, None), + ("1", False, None), + ("Pi", False, None), + ("a", False, None), + ("a-a", True, None), + ("3-3.", True, None), + ("2-Sqrt[4]", True, None), + ("-Pi", False, None), + ("(-1)^2", False, None), + ("(-1)^3", False, None), + ("Sqrt[2]", False, None), + ("Sqrt[-2]", False, None), + ("(-2)^(1/2)", False, None), + ("(2)^(1/2)", False, None), + ("Exp[a]", False, None), + ("Exp[2.3]", False, None), + ("Log[1/2]", False, None), + ("Exp[I]", False, None), + ("Log[3]", False, None), + ("Log[I]", False, None), + ("Abs[a]", False, None), + ("Abs[0]", False, None), + ("Abs[1+3 I]", False, None), + # ("Sin[Pi]", False, None), + ], +) +def test_zero(str_expr, expected, msg): + expr = session.parse(str_expr) + if msg: + assert check_zero(expr) == expected, msg + else: + assert check_zero(expr) == expected