From 7a1241c01988eb6f3ac07fae7c92af59a79fcba5 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 24 Jul 2023 06:05:36 -0400 Subject: [PATCH] Start to reduce/refactor arithmetic ... Remove stuff from mathics.builtin.arithmetic that does not belong there: * _MPMathFunction -> MPMathFunction and move to mathics.buitin.base * _MPMathMultiFunction -> MPMathFunction and move to mathics.builtin.base * Abs, Piecewise, RealAbs, RealSign, Sign moved to numeric to follow WMA organization better The corresponding eval routines will be gone over in another PR. This one is already large. Url's gone over to make this not exceed standard line limit. Note that the formatting has been gone over to follow the existing pattern that we have been using. cythonization in mathics.builtin class files removed. It is not clear this has benefit in modern Pythons, especially in this kind of builtin function and all of this needs to be retested if not rethought. --- mathics/builtin/arithfns/basic.py | 18 +- mathics/builtin/arithmetic.py | 403 +------------------- mathics/builtin/base.py | 441 +++++++++++++--------- mathics/builtin/intfns/combinatorial.py | 5 +- mathics/builtin/intfns/misc.py | 4 +- mathics/builtin/intfns/recurrence.py | 7 +- mathics/builtin/numbers/exp.py | 10 +- mathics/builtin/numbers/hyperbolic.py | 25 +- mathics/builtin/numbers/trig.py | 31 +- mathics/builtin/numeric.py | 329 +++++++++++++++- mathics/builtin/specialfns/bessel.py | 13 +- mathics/builtin/specialfns/erf.py | 35 +- mathics/builtin/specialfns/expintegral.py | 10 +- mathics/builtin/specialfns/gamma.py | 29 +- mathics/builtin/specialfns/orthogonal.py | 22 +- mathics/builtin/specialfns/zeta.py | 6 +- mathics/timing.py | 5 +- test/test_evaluators.py | 4 +- 18 files changed, 716 insertions(+), 681 deletions(-) diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index 6fb5a8873..558369c9f 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -2,12 +2,19 @@ """ Basic Arithmetic -The functions here are the basic arithmetic operations that you might find on a calculator. +The functions here are the basic arithmetic operations that you might find \ +on a calculator. """ -from mathics.builtin.arithmetic import _MPMathFunction, create_infix -from mathics.builtin.base import BinaryOperator, Builtin, PrefixOperator, SympyFunction +from mathics.builtin.arithmetic import create_infix +from mathics.builtin.base import ( + BinaryOperator, + Builtin, + MPMathFunction, + PrefixOperator, + SympyFunction, +) from mathics.core.atoms import ( Complex, Integer, @@ -387,7 +394,7 @@ def eval(self, items, evaluation): return eval_Plus(*items_tuple) -class Power(BinaryOperator, _MPMathFunction): +class Power(BinaryOperator, MPMathFunction): """ :Exponentiation: @@ -531,7 +538,7 @@ class Power(BinaryOperator, _MPMathFunction): def eval_check(self, x, y, evaluation): "Power[x_, y_]" - # Power uses _MPMathFunction but does some error checking first + # Power uses MPMathFunction but does some error checking first if isinstance(x, Number) and x.is_zero: if isinstance(y, Number): y_err = y @@ -788,7 +795,6 @@ def inverse(item): and isinstance(item.elements[1], (Integer, Rational, Real)) and item.elements[1].to_sympy() < 0 ): # nopep8 - negative.append(inverse(item)) elif isinstance(item, Rational): numerator = item.numerator() diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index 851316ad0..19b2c3393 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# cython: language_level=3 """ Mathematical Functions @@ -7,21 +6,21 @@ Basic arithmetic functions, including complex number arithmetic. """ -from functools import lru_cache from typing import Optional -import mpmath import sympy from mathics.builtin.base import ( Builtin, IterationFunction, + MPMathFunction, Predefined, SympyFunction, SympyObject, Test, ) -from mathics.builtin.inference import evaluate_predicate, get_assumptions_list +from mathics.builtin.inference import get_assumptions_list +from mathics.builtin.numeric import Abs from mathics.builtin.scoping import dynamic_scoping from mathics.core.atoms import ( MATHICS3_COMPLEX_I, @@ -31,20 +30,17 @@ Integer0, Integer1, IntegerM1, - Number, Rational, Real, String, ) from mathics.core.attributes import ( - A_HOLD_ALL, A_HOLD_REST, A_LISTABLE, A_NO_ATTRIBUTES, A_NUMERIC_FUNCTION, A_PROTECTED, ) -from mathics.core.convert.expression import to_expression from mathics.core.convert.sympy import SympyExpression, from_sympy, sympy_symbol_prefix from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation @@ -58,7 +54,6 @@ PredefinedExpression, ) from mathics.core.list import ListExpression -from mathics.core.number import dps, min_prec from mathics.core.symbols import ( Atom, Symbol, @@ -72,20 +67,12 @@ SymbolAnd, SymbolDirectedInfinity, SymbolInfix, - SymbolPiecewise, SymbolPossibleZeroQ, SymbolTable, SymbolUndefined, ) -from mathics.eval.arithmetic import ( - eval_Abs, - eval_mpmath_function, - eval_negate_number, - eval_RealSign, - eval_Sign, -) +from mathics.eval.arithmetic import eval_Sign from mathics.eval.nevaluator import eval_N -from mathics.eval.numerify import numerify # This tells documentation how to sort this module sort_order = "mathics.builtin.mathematical-functions" @@ -99,86 +86,6 @@ } -class _MPMathFunction(SympyFunction): - - # These below attributes are the default attributes: - # - # * functions take lists as an argument - # * functions take numeric values only - # * functions can't be changed - # - # However hey are not correct for some derived classes, like - # InverseErf or InverseErfc. - # So those classes should expclicitly set/override this. - attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - - mpmath_name = None - nargs = {1} - - @lru_cache(maxsize=1024) - def get_mpmath_function(self, args): - if self.mpmath_name is None or len(args) not in self.nargs: - return None - return getattr(mpmath, self.mpmath_name) - - def eval(self, z, evaluation: Evaluation): - "%(name)s[z__]" - - args = numerify(z, evaluation).get_sequence() - - # if no arguments are inexact attempt to use sympy - if all(not x.is_inexact() for x in args): - result = to_expression(self.get_name(), *args).to_sympy() - result = self.prepare_mathics(result) - result = from_sympy(result) - # evaluate elements to convert e.g. Plus[2, I] -> Complex[2, 1] - return result.evaluate_elements(evaluation) - - if not all(isinstance(arg, Number) for arg in args): - return - - mpmath_function = self.get_mpmath_function(tuple(args)) - if mpmath_function is None: - return - - if any(arg.is_machine_precision() for arg in args): - prec = None - else: - prec = min_prec(*args) - d = dps(prec) - args = [arg.round(d) for arg in args] - - return eval_mpmath_function(mpmath_function, *args, prec=prec) - - -class _MPMathMultiFunction(_MPMathFunction): - - sympy_names = None - mpmath_names = None - - def get_sympy_names(self): - if self.sympy_names is None: - return [self.sympy_name] - return self.sympy_names.values() - - def get_function(self, module, names, fallback_name, elements): - try: - name = fallback_name - if names is not None: - name = names[len(elements)] - if name is None: - return None - return getattr(module, name) - except KeyError: - return None - - def get_sympy_function(self, elements): - return self.get_function(sympy, self.sympy_names, self.sympy_name, elements) - - def get_mpmath_function(self, elements): - return self.get_function(mpmath, self.mpmath_names, self.mpmath_name, elements) - - def create_infix(items, operator, prec, grouping): if len(items) == 1: return items[0] @@ -192,55 +99,7 @@ def create_infix(items, operator, prec, grouping): ) -class Abs(_MPMathFunction): - """ - - :Absolute value: - https://en.wikipedia.org/wiki/Absolute_value ( - :SymPy: - https://docs.sympy.org/latest/modules/functions/ - elementary.html#sympy.functions.elementary.complexes.Abs, - :WMA: https://reference.wolfram.com/language/ref/Abs) - -
-
'Abs[$x$]' -
returns the absolute value of $x$. -
- - >> Abs[-3] - = 3 - - >> Plot[Abs[x], {x, -4, 4}] - = -Graphics- - - 'Abs' returns the magnitude of complex numbers: - >> Abs[3 + I] - = Sqrt[10] - >> Abs[3.0 + I] - = 3.16228 - - All of the below evaluate to Infinity: - - >> Abs[Infinity] == Abs[I Infinity] == Abs[ComplexInfinity] - = True - """ - - mpmath_name = "fabs" # mpmath actually uses python abs(x) / x.__abs__() - rules = { - "Abs[Undefined]": "Undefined", - } - summary_text = "absolute value of a number" - sympy_name = "Abs" - - def eval(self, x, evaluation: Evaluation): - "Abs[x_]" - result = eval_Abs(x) - if result is not None: - return result - return super(Abs, self).eval(x, evaluation) - - -class Arg(_MPMathFunction): +class Arg(MPMathFunction): """ :Argument (complex analysis): https://en.wikipedia.org/wiki/Argument_(complex_analysis) ( @@ -255,7 +114,8 @@ class Arg(_MPMathFunction):
  • 'Arg'[$z$] is left unevaluated if $z$ is not a numeric quantity.
  • 'Arg'[$z$] gives the phase angle of $z$ in radians.
  • The result from 'Arg'[$z$] is always between -Pi and +Pi. -
  • 'Arg'[$z$] has a branch cut discontinuity in the complex $z$ plane running from -Infinity to 0. +
  • 'Arg'[$z$] has a branch cut discontinuity in the complex $z$ plane running \ + from -Infinity to 0.
  • 'Arg'[0] is 0. @@ -302,7 +162,7 @@ def eval(self, z, evaluation, options={}): if preference is None or preference == "Automatic": return super(Arg, self).eval(z, evaluation) elif preference == "mpmath": - return _MPMathFunction.eval(self, z, evaluation) + return MPMathFunction.eval(self, z, evaluation) elif preference == "sympy": return SympyFunction.eval(self, z, evaluation) # TODO: add NumpyFunction @@ -559,7 +419,7 @@ def to_sympy(self, expr, **kwargs): return sympy.Piecewise(*sympy_cases) -class Conjugate(_MPMathFunction): +class Conjugate(MPMathFunction): """ :Complex Conjugate: https://en.wikipedia.org/wiki/Complex_conjugate \ @@ -890,101 +750,6 @@ class Integer_(Builtin): name = "Integer" -class Piecewise(SympyFunction): - """ - :WMA link:https://reference.wolfram.com/language/ref/Piecewise.html - -
    -
    'Piecewise[{{expr1, cond1}, ...}]' -
    represents a piecewise function. - -
    'Piecewise[{{expr1, cond1}, ...}, expr]' -
    represents a piecewise function with default 'expr'. -
    - - Heaviside function - >> Piecewise[{{0, x <= 0}}, 1] - = Piecewise[{{0, x <= 0}}, 1] - - ## D[%, x] - ## Piecewise({{0, Or[x < 0, x > 0]}}, Indeterminate). - - >> Integrate[Piecewise[{{1, x <= 0}, {-1, x > 0}}], x] - = Piecewise[{{x, x <= 0}}, -x] - - >> Integrate[Piecewise[{{1, x <= 0}, {-1, x > 0}}], {x, -1, 2}] - = -1 - - Piecewise defaults to 0 if no other case is matching. - >> Piecewise[{{1, False}}] - = 0 - - >> Plot[Piecewise[{{Log[x], x > 0}, {x*-0.5, x < 0}}], {x, -1, 1}] - = -Graphics- - - >> Piecewise[{{0 ^ 0, False}}, -1] - = -1 - """ - - summary_text = "an arbitrary piecewise function" - sympy_name = "Piecewise" - - attributes = A_HOLD_ALL | A_PROTECTED - - def eval(self, items, evaluation: Evaluation): - "%(name)s[items__]" - result = self.to_sympy( - Expression(SymbolPiecewise, *items.get_sequence()), evaluation=evaluation - ) - if result is None: - return - if not isinstance(result, sympy.Piecewise): - result = from_sympy(result) - return result - - def to_sympy(self, expr, **kwargs): - elements = expr.elements - evaluation = kwargs.get("evaluation", None) - if len(elements) not in (1, 2): - return - - sympy_cases = [] - for case in elements[0].elements: - if case.get_head_name() != "System`List": - return - if len(case.elements) != 2: - return - then, cond = case.elements - if evaluation: - cond = evaluate_predicate(cond, evaluation) - - sympy_cond = None - if isinstance(cond, Symbol): - if cond is SymbolTrue: - sympy_cond = True - elif cond is SymbolFalse: - sympy_cond = False - if sympy_cond is None: - sympy_cond = cond.to_sympy(**kwargs) - if not (sympy_cond.is_Relational or sympy_cond.is_Boolean): - return - - sympy_cases.append((then.to_sympy(**kwargs), sympy_cond)) - - if len(elements) == 2: # default case - sympy_cases.append((elements[1].to_sympy(**kwargs), True)) - else: - sympy_cases.append((Integer0.to_sympy(**kwargs), True)) - - return sympy.Piecewise(*sympy_cases) - - def from_sympy(self, sympy_name, args): - # Hack to get around weird sympy.Piecewise 'otherwise' behaviour - if str(args[-1].elements[1]).startswith("System`_True__Dummy_"): - args[-1].elements[1] = SymbolTrue - return Expression(self.get_name(), args) - - class Product(IterationFunction, SympyFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Product.html @@ -1215,49 +980,6 @@ class Real_(Builtin): name = "Real" -class RealAbs(Builtin): - """ - - :Abs (Real): - https://en.wikipedia.org/wiki/Absolute_value ( - :WMA link: - https://reference.wolfram.com/language/ref/RealAbs.html - ) - -
    -
    'RealAbs[$x$]' -
    returns the absolute value of a real number $x$. -
    - 'RealAbs' is also known as modulus. It is evaluated if $x$ can be compared \ - with $0$. - - >> RealAbs[-3.] - = 3. - 'RealAbs[$z$]' is left unevaluated for complex $z$: - >> RealAbs[2. + 3. I] - = RealAbs[2. + 3. I] - >> D[RealAbs[x ^ 2], x] - = 2 x ^ 3 / RealAbs[x ^ 2] - """ - - attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - rules = { - "D[RealAbs[x_],x_]": "x/RealAbs[x]", - "Integrate[RealAbs[x_],x_]": "1/2 x RealAbs[x]", - "Integrate[RealAbs[u_],{u_,a_,b_}]": "1/2 b RealAbs[b]-1/2 a RealAbs[a]", - } - summary_text = "real absolute value" - - def eval(self, x: BaseElement, evaluation: Evaluation): - """RealAbs[x_]""" - real_sign = eval_RealSign(x) - if real_sign is IntegerM1: - return eval_negate_number(x) - if real_sign is None: - return - return x - - class RealNumberQ(Test): """ ## Not found in WMA @@ -1288,113 +1010,6 @@ def test(self, expr) -> bool: return isinstance(expr, (Integer, Rational, Real)) -class RealSign(Builtin): - """ - - :Sign function: - https://en.wikipedia.org/wiki/Sign_function ( - :WMA link: - https://reference.wolfram.com/language/ref/RealSign.html - ) -
    -
    'RealSign[$x$]' -
    returns $-1$, $0$ or $1$ depending on whether $x$ is negative, - zero or positive. -
    - 'RealSign' is also known as $sgn$ or $signum$ function. - - >> RealSign[-3.] - = -1 - 'RealSign[$z$]' is left unevaluated for complex $z$: - >> RealSign[2. + 3. I] - = RealSign[2. + 3. I] - - >> D[RealSign[x^2],x] - = 2 x Piecewise[{{0, x ^ 2 != 0}}, Indeterminate] - >> Integrate[RealSign[u],{u,0,x}] - = RealAbs[x] - """ - - attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - rules = { - "D[RealSign[x_],x_]": "Piecewise[{{0, x!=0}}, Indeterminate]", - "Integrate[RealSign[x_],x_]": "RealAbs[x]", - "Integrate[RealSign[u_],{u_, a_, b_}]": "RealAbs[b]-RealSign[a]", - } - summary_text = "real sign" - - def eval(self, x: Number, evaluation: Evaluation) -> Optional[Integer]: - """RealSign[x_]""" - return eval_RealSign(x) - - -class Sign(SympyFunction): - """ - - :Sign: - https://en.wikipedia.org/wiki/Sign_function ( - :WMA link: - https://reference.wolfram.com/language/ref/Sign.html - ) - -
    -
    'Sign[$x$]' -
    return -1, 0, or 1 depending on whether $x$ is negative, zero, or positive. -
    - - >> Sign[19] - = 1 - >> Sign[-6] - = -1 - >> Sign[0] - = 0 - >> Sign[{-5, -10, 15, 20, 0}] - = {-1, -1, 1, 1, 0} - #> Sign[{1, 2.3, 4/5, {-6.7, 0}, {8/9, -10}}] - = {1, 1, 1, {-1, 0}, {1, -1}} - >> Sign[3 - 4*I] - = 3 / 5 - 4 I / 5 - #> Sign[1 - 4*I] == (1/17 - 4 I/17) Sqrt[17] - = True - #> Sign[4, 5, 6] - : Sign called with 3 arguments; 1 argument is expected. - = Sign[4, 5, 6] - #> Sign["20"] - = Sign[20] - """ - - summary_text = "complex sign of a number" - sympy_name = "sign" - # mpmath_name = 'sign' - - attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - - messages = { - "argx": "Sign called with `1` arguments; 1 argument is expected.", - } - - rules = { - "Sign[Power[a_, b_]]": "Power[Sign[a], b]", - } - - def eval(self, x, evaluation: Evaluation): - "%(name)s[x_]" - 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 - # Unhandled cases. Use sympy - return super(Sign, self).eval(x, evaluation) - - def eval_error(self, x, seqs, evaluation: Evaluation): - "Sign[x_, seqs__]" - evaluation.message("Sign", "argx", Integer(len(seqs.get_sequence()) + 1)) - - class Sum(IterationFunction, SympyFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Sum.html diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 4a21365a2..306acc62e 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# cython: language_level=3 import importlib import re @@ -7,6 +6,7 @@ from itertools import chain from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast +import mpmath import sympy from mathics.core.atoms import ( @@ -18,7 +18,13 @@ PrecisionReal, String, ) -from mathics.core.attributes import A_HOLD_ALL, A_NO_ATTRIBUTES, A_PROTECTED +from mathics.core.attributes import ( + A_HOLD_ALL, + A_LISTABLE, + A_NO_ATTRIBUTES, + A_NUMERIC_FUNCTION, + A_PROTECTED, +) from mathics.core.convert.expression import to_expression from mathics.core.convert.op import ascii_operator_to_symbol from mathics.core.convert.python import from_bool @@ -29,7 +35,7 @@ from mathics.core.expression import Expression, SymbolDefault from mathics.core.interrupt import BreakInterrupt, ContinueInterrupt, ReturnInterrupt from mathics.core.list import ListExpression -from mathics.core.number import PrecisionValueError, get_precision +from mathics.core.number import PrecisionValueError, dps, get_precision, min_prec from mathics.core.parser.util import PyMathicsDefinitions, SystemDefinitions from mathics.core.rules import BuiltinRule, Pattern, Rule from mathics.core.symbols import ( @@ -50,6 +56,7 @@ SymbolRule, SymbolSequence, ) +from mathics.eval.arithmetic import eval_mpmath_function from mathics.eval.numbers import cancel from mathics.eval.numerify import numerify from mathics.eval.scoping import dynamic_scoping @@ -58,103 +65,6 @@ no_doc = True -class DefaultOptionChecker: - """ - Callable class that is used in checking that options are valid. - - If initialized with ``strict`` set to True, - then a instantance calls will return True only if all - options listed in ``options_to_check`` are in the constructor's - list of options. In either case, when an option is not in the - constructor list, give an "optx" message. - """ - - def __init__(self, builtin, options, strict: bool): - self.name = builtin.get_name() - self.strict = strict - self.options = options - - def __call__(self, options_to_check, evaluation): - option_name = self.name - options = self.options - strict = self.strict - - for key, value in options_to_check.items(): - short_key = strip_context(key) - if not has_option(options, short_key, evaluation): - evaluation.message( - option_name, - "optx", - Expression(SymbolRule, String(short_key), value), - strip_context(option_name), - ) - if strict: - return False - return True - - -class UnavailableFunction: - """ - Callable class used when the evaluation function is not available. - """ - - def __init__(self, builtin): - self.name = builtin.get_name() - - def __call__(self, **kwargs): - kwargs["evaluation"].message( - "General", - "pyimport", # see messages.py for error message definition - strip_context(self.name), - ) - - -def check_requires_list(requires: list) -> bool: - """ - Check if module names in ``requires`` can be imported and return - True if they can, or False if not. - - """ - for package in requires: - lib_is_installed = True - try: - lib_is_installed = importlib.util.find_spec(package) is not None - except ImportError: - # print("XXX requires import error", requires) - lib_is_installed = False - if not lib_is_installed: - # print("XXX requires not found error", requires) - return False - return True - - -def get_option(options, name, evaluation, pop=False, evaluate=True): - # we do not care whether an option X is given as System`X, - # Global`X, or with any prefix from $ContextPath for that - # matter. Also, the quoted string form "X" is ok. all these - # variants name the same option. this matches Wolfram Language - # behaviour. - name = strip_context(name) - contexts = (s + "%s" for s in evaluation.definitions.get_context_path()) - - for variant in chain(contexts, ('"%s"',)): - resolved_name = variant % name - if pop: - value = options.pop(resolved_name, None) - else: - value = options.get(resolved_name) - if value is not None: - return value.evaluate(evaluation) if evaluate else value - return None - - -def has_option(options, name, evaluation): - return get_option(options, name, evaluation, evaluate=False) is not None - - -mathics_to_python = {} # here we have: name -> string - - class Builtin: """ A base class for a Built-in function symbols, like List, or @@ -565,6 +475,260 @@ def __hash__(self): return hash((self.get_name(), id(self))) +# This has to come before SympyFunction +class SympyObject(Builtin): + sympy_name: Optional[str] = None + + mathics_to_sympy = {} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.sympy_name is None: + self.sympy_name = strip_context(self.get_name()).lower() + self.mathics_to_sympy[self.__class__.__name__] = self.sympy_name + + def is_constant(self) -> bool: + return False + + def get_sympy_names(self) -> List[str]: + if self.sympy_name: + return [self.sympy_name] + return [] + + +# This has to come before MPMathFunction +class SympyFunction(SympyObject): + def eval(self, z, evaluation): + # Note: we omit a docstring here, so as not to confuse + # function signature collector ``contribute``. + + # Generic eval method that uses the class sympy_name. + # to call the corresponding sympy function. Arguments are + # converted to python and the result is converted from sympy + # + # "%(name)s[z__]" + sympy_args = to_numeric_sympy_args(z, evaluation) + sympy_fn = getattr(sympy, self.sympy_name) + try: + return from_sympy(run_sympy(sympy_fn, *sympy_args)) + except Exception: + return + + def get_constant(self, precision, evaluation, have_mpmath=False): + try: + d = get_precision(precision, evaluation) + except PrecisionValueError: + return + + sympy_fn = self.to_sympy() + if d is None: + result = self.get_mpmath_function() if have_mpmath else sympy_fn() + return MachineReal(result) + else: + return PrecisionReal(sympy_fn.n(d)) + + def get_sympy_function(self, elements=None): + if self.sympy_name: + return getattr(sympy, self.sympy_name) + return None + + def prepare_sympy(self, elements: Iterable) -> Iterable: + return elements + + def to_sympy(self, expr, **kwargs): + try: + if self.sympy_name: + elements = self.prepare_sympy(expr.elements) + sympy_args = [element.to_sympy(**kwargs) for element in elements] + if None in sympy_args: + return None + sympy_function = self.get_sympy_function(elements) + return sympy_function(*sympy_args) + except TypeError: + pass + + def from_sympy(self, sympy_name, elements): + return to_expression(self.get_name(), *elements) + + def prepare_mathics(self, sympy_expr): + return sympy_expr + + +class MPMathFunction(SympyFunction): + # These below attributes are the default attributes: + # + # * functions take lists as an argument + # * functions take numeric values only + # * functions can't be changed + # + # However hey are not correct for some derived classes, like + # InverseErf or InverseErfc. + # So those classes should expclicitly set/override this. + attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED + + mpmath_name = None + nargs = {1} + + @lru_cache(maxsize=1024) + def get_mpmath_function(self, args): + if self.mpmath_name is None or len(args) not in self.nargs: + return None + return getattr(mpmath, self.mpmath_name) + + def eval(self, z, evaluation: Evaluation): + "%(name)s[z__]" + + args = numerify(z, evaluation).get_sequence() + + # if no arguments are inexact attempt to use sympy + if all(not x.is_inexact() for x in args): + result = to_expression(self.get_name(), *args).to_sympy() + result = self.prepare_mathics(result) + result = from_sympy(result) + # evaluate elements to convert e.g. Plus[2, I] -> Complex[2, 1] + return result.evaluate_elements(evaluation) + + if not all(isinstance(arg, Number) for arg in args): + return + + mpmath_function = self.get_mpmath_function(tuple(args)) + if mpmath_function is None: + return + + if any(arg.is_machine_precision() for arg in args): + prec = None + else: + prec = min_prec(*args) + d = dps(prec) + args = [arg.round(d) for arg in args] + + return eval_mpmath_function(mpmath_function, *args, prec=prec) + + +class MPMathMultiFunction(MPMathFunction): + sympy_names = None + mpmath_names = None + + def get_sympy_names(self): + if self.sympy_names is None: + return [self.sympy_name] + return self.sympy_names.values() + + def get_function(self, module, names, fallback_name, elements): + try: + name = fallback_name + if names is not None: + name = names[len(elements)] + if name is None: + return None + return getattr(module, name) + except KeyError: + return None + + def get_sympy_function(self, elements): + return self.get_function(sympy, self.sympy_names, self.sympy_name, elements) + + def get_mpmath_function(self, elements): + return self.get_function(mpmath, self.mpmath_names, self.mpmath_name, elements) + + +class DefaultOptionChecker: + """ + Callable class that is used in checking that options are valid. + + If initialized with ``strict`` set to True, + then a instantance calls will return True only if all + options listed in ``options_to_check`` are in the constructor's + list of options. In either case, when an option is not in the + constructor list, give an "optx" message. + """ + + def __init__(self, builtin, options, strict: bool): + self.name = builtin.get_name() + self.strict = strict + self.options = options + + def __call__(self, options_to_check, evaluation): + option_name = self.name + options = self.options + strict = self.strict + + for key, value in options_to_check.items(): + short_key = strip_context(key) + if not has_option(options, short_key, evaluation): + evaluation.message( + option_name, + "optx", + Expression(SymbolRule, String(short_key), value), + strip_context(option_name), + ) + if strict: + return False + return True + + +class UnavailableFunction: + """ + Callable class used when the evaluation function is not available. + """ + + def __init__(self, builtin): + self.name = builtin.get_name() + + def __call__(self, **kwargs): + kwargs["evaluation"].message( + "General", + "pyimport", # see messages.py for error message definition + strip_context(self.name), + ) + + +def check_requires_list(requires: list) -> bool: + """ + Check if module names in ``requires`` can be imported and return + True if they can, or False if not. + + """ + for package in requires: + lib_is_installed = True + try: + lib_is_installed = importlib.util.find_spec(package) is not None + except ImportError: + # print("XXX requires import error", requires) + lib_is_installed = False + if not lib_is_installed: + # print("XXX requires not found error", requires) + return False + return True + + +def get_option(options, name, evaluation, pop=False, evaluate=True): + # we do not care whether an option X is given as System`X, + # Global`X, or with any prefix from $ContextPath for that + # matter. Also, the quoted string form "X" is ok. all these + # variants name the same option. this matches Wolfram Language + # behaviour. + name = strip_context(name) + contexts = (s + "%s" for s in evaluation.definitions.get_context_path()) + + for variant in chain(contexts, ('"%s"',)): + resolved_name = variant % name + if pop: + value = options.pop(resolved_name, None) + else: + value = options.get(resolved_name) + if value is not None: + return value.evaluate(evaluation) if evaluate else value + return None + + +def has_option(options, name, evaluation): + return get_option(options, name, evaluation, evaluate=False) is not None + + +mathics_to_python = {} # here we have: name -> string + + class AtomBuiltin(Builtin): """ This class is used to define Atoms other than those ones in core, but also @@ -809,26 +973,6 @@ def get_functions(self, prefix="eval", is_pymodule=False) -> List[Callable]: return functions -class SympyObject(Builtin): - sympy_name: Optional[str] = None - - mathics_to_sympy = {} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.sympy_name is None: - self.sympy_name = strip_context(self.get_name()).lower() - self.mathics_to_sympy[self.__class__.__name__] = self.sympy_name - - def is_constant(self) -> bool: - return False - - def get_sympy_names(self) -> List[str]: - if self.sympy_name: - return [self.sympy_name] - return [] - - class UnaryOperator(Operator): def __init__(self, format_function, *args, **kwargs): super().__init__(*args, **kwargs) @@ -922,63 +1066,6 @@ def run_sympy(sympy_fn: Callable, *sympy_args) -> Any: return sympy_fn(*sympy_args) -class SympyFunction(SympyObject): - def eval(self, z, evaluation): - # Note: we omit a docstring here, so as not to confuse - # function signature collector ``contribute``. - - # Generic eval method that uses the class sympy_name. - # to call the corresponding sympy function. Arguments are - # converted to python and the result is converted from sympy - # - # "%(name)s[z__]" - sympy_args = to_numeric_sympy_args(z, evaluation) - sympy_fn = getattr(sympy, self.sympy_name) - try: - return from_sympy(run_sympy(sympy_fn, *sympy_args)) - except Exception: - return - - def get_constant(self, precision, evaluation, have_mpmath=False): - try: - d = get_precision(precision, evaluation) - except PrecisionValueError: - return - - sympy_fn = self.to_sympy() - if d is None: - result = self.get_mpmath_function() if have_mpmath else sympy_fn() - return MachineReal(result) - else: - return PrecisionReal(sympy_fn.n(d)) - - def get_sympy_function(self, elements=None): - if self.sympy_name: - return getattr(sympy, self.sympy_name) - return None - - def prepare_sympy(self, elements: Iterable) -> Iterable: - return elements - - def to_sympy(self, expr, **kwargs): - try: - if self.sympy_name: - elements = self.prepare_sympy(expr.elements) - sympy_args = [element.to_sympy(**kwargs) for element in elements] - if None in sympy_args: - return None - sympy_function = self.get_sympy_function(elements) - return sympy_function(*sympy_args) - except TypeError: - pass - - def from_sympy(self, sympy_name, elements): - return to_expression(self.get_name(), *elements) - - def prepare_mathics(self, sympy_expr): - return sympy_expr - - class PatternError(Exception): def __init__(self, name, tag, *args): super().__init__() diff --git a/mathics/builtin/intfns/combinatorial.py b/mathics/builtin/intfns/combinatorial.py index d46ef1f78..6bb62aa88 100644 --- a/mathics/builtin/intfns/combinatorial.py +++ b/mathics/builtin/intfns/combinatorial.py @@ -14,8 +14,7 @@ from itertools import combinations -from mathics.builtin.arithmetic import _MPMathFunction -from mathics.builtin.base import Builtin, SympyFunction +from mathics.builtin.base import Builtin, MPMathFunction, SympyFunction from mathics.core.atoms import Integer from mathics.core.attributes import ( A_LISTABLE, @@ -86,7 +85,7 @@ class _NoBoolVector(Exception): pass -class Binomial(_MPMathFunction): +class Binomial(MPMathFunction): """ diff --git a/mathics/builtin/intfns/misc.py b/mathics/builtin/intfns/misc.py index 5746a5ac4..2370c0817 100644 --- a/mathics/builtin/intfns/misc.py +++ b/mathics/builtin/intfns/misc.py @@ -1,8 +1,8 @@ -from mathics.builtin.arithmetic import _MPMathFunction +from mathics.builtin.base import MPMathFunction from mathics.core.attributes import A_LISTABLE, A_PROTECTED -class BernoulliB(_MPMathFunction): +class BernoulliB(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/BernoulliB.html diff --git a/mathics/builtin/intfns/recurrence.py b/mathics/builtin/intfns/recurrence.py index 23434581c..cbe3fdc5b 100644 --- a/mathics/builtin/intfns/recurrence.py +++ b/mathics/builtin/intfns/recurrence.py @@ -11,8 +11,7 @@ from sympy.functions.combinatorial.numbers import stirling -from mathics.builtin.arithmetic import _MPMathFunction -from mathics.builtin.base import Builtin +from mathics.builtin.base import Builtin, MPMathFunction from mathics.core.atoms import Integer from mathics.core.attributes import ( A_LISTABLE, @@ -23,7 +22,7 @@ from mathics.core.evaluation import Evaluation -class Fibonacci(_MPMathFunction): +class Fibonacci(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Fibonacci.html @@ -49,7 +48,7 @@ class Fibonacci(_MPMathFunction): summary_text = "Fibonacci's numbers" -class HarmonicNumber(_MPMathFunction): +class HarmonicNumber(MPMathFunction): """ :Harmonic Number:https://en.wikipedia.org/wiki/Harmonic_number \( :WMA link:https://reference.wolfram.com/language/ref/HarmonicNumber.html) diff --git a/mathics/builtin/numbers/exp.py b/mathics/builtin/numbers/exp.py index 9ad654462..301da0a83 100644 --- a/mathics/builtin/numbers/exp.py +++ b/mathics/builtin/numbers/exp.py @@ -3,7 +3,8 @@ """ Exponential Functions -Numerical values and derivatives can be computed; however, most special exact values and simplification rules are not implemented yet. +Numerical values and derivatives can be computed; however, most special exact values \ +and simplification rules are not implemented yet. """ import math @@ -13,8 +14,7 @@ import mpmath -from mathics.builtin.arithmetic import _MPMathFunction -from mathics.builtin.base import Builtin +from mathics.builtin.base import Builtin, MPMathFunction from mathics.core.atoms import Real from mathics.core.attributes import A_LISTABLE, A_NUMERIC_FUNCTION, A_PROTECTED from mathics.core.convert.python import from_python @@ -157,7 +157,7 @@ def converted_operands(): init = y -class Exp(_MPMathFunction): +class Exp(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Exp.html @@ -191,7 +191,7 @@ def from_sympy(self, sympy_name, elements): return Expression(SymbolPower, SymbolE, elements[0]) -class Log(_MPMathFunction): +class Log(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Log.html diff --git a/mathics/builtin/numbers/hyperbolic.py b/mathics/builtin/numbers/hyperbolic.py index 44670b15c..f4ffe5727 100644 --- a/mathics/builtin/numbers/hyperbolic.py +++ b/mathics/builtin/numbers/hyperbolic.py @@ -14,8 +14,7 @@ from typing import Optional -from mathics.builtin.arithmetic import _MPMathFunction -from mathics.builtin.base import Builtin, SympyFunction +from mathics.builtin.base import Builtin, MPMathFunction, SympyFunction from mathics.core.atoms import IntegerM1 from mathics.core.convert.sympy import SympyExpression from mathics.core.evaluation import Evaluation @@ -30,7 +29,7 @@ SymbolSinh = Symbol("Sinh") -class ArcCosh(_MPMathFunction): +class ArcCosh(MPMathFunction): """ :Inverse hyperbolic cosine: @@ -70,7 +69,7 @@ class ArcCosh(_MPMathFunction): sympy_name = "acosh" -class ArcCoth(_MPMathFunction): +class ArcCoth(MPMathFunction): """ :Inverse hyperbolic cotangent: @@ -111,7 +110,7 @@ class ArcCoth(_MPMathFunction): } -class ArcCsch(_MPMathFunction): +class ArcCsch(MPMathFunction): """ :Inverse hyperbolic cosecant: @@ -153,7 +152,7 @@ def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: ).to_sympy() -class ArcSech(_MPMathFunction): +class ArcSech(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/ArcSech.html @@ -189,7 +188,7 @@ def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: ).to_sympy() -class ArcSinh(_MPMathFunction): +class ArcSinh(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/ArcSinh.html @@ -216,7 +215,7 @@ class ArcSinh(_MPMathFunction): } -class ArcTanh(_MPMathFunction): +class ArcTanh(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/ArcTanh.html @@ -309,7 +308,7 @@ def eval_with_complex_vars(self, expr, vars, evaluation: Evaluation): return eval_ComplexExpand(expr, vars) -class Cosh(_MPMathFunction): +class Cosh(MPMathFunction): """ :WMA link: @@ -334,7 +333,7 @@ class Cosh(_MPMathFunction): summary_text = "hyperbolic cosine function" -class Coth(_MPMathFunction): +class Coth(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Coth.html @@ -438,7 +437,7 @@ class InverseGudermannian(Builtin): summary_text = "inverse Gudermannian function gd^-1(z)" -class Sech(_MPMathFunction): +class Sech(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Sech.html @@ -467,7 +466,7 @@ def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: ).to_sympy() -class Sinh(_MPMathFunction): +class Sinh(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Sinh.html @@ -489,7 +488,7 @@ class Sinh(_MPMathFunction): } -class Tanh(_MPMathFunction): +class Tanh(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Tanh.html diff --git a/mathics/builtin/numbers/trig.py b/mathics/builtin/numbers/trig.py index 079976d72..565a7a4d7 100644 --- a/mathics/builtin/numbers/trig.py +++ b/mathics/builtin/numbers/trig.py @@ -14,8 +14,7 @@ import mpmath -from mathics.builtin.arithmetic import _MPMathFunction -from mathics.builtin.base import Builtin +from mathics.builtin.base import Builtin, MPMathFunction from mathics.core.atoms import Integer, Integer0, IntegerM1, Real from mathics.core.convert.python import from_python from mathics.core.exceptions import IllegalStepSpecification @@ -334,7 +333,7 @@ def _fold(self, state, steps, math): yield x, y, phi -class ArcCos(_MPMathFunction): +class ArcCos(MPMathFunction): """ Inverse cosine, :arccosine: @@ -371,7 +370,7 @@ class ArcCos(_MPMathFunction): sympy_name = "acos" -class ArcCot(_MPMathFunction): +class ArcCot(MPMathFunction): """ Inverse cotangent, :arccotangent: @@ -406,7 +405,7 @@ class ArcCot(_MPMathFunction): sympy_name = "acot" -class ArcCsc(_MPMathFunction): +class ArcCsc(MPMathFunction): """ Inverse cosecant, :arccosecant: @@ -447,7 +446,7 @@ def to_sympy(self, expr, **kwargs): ).to_sympy() -class ArcSec(_MPMathFunction): +class ArcSec(MPMathFunction): """ Inverse secant, :arcsecant: @@ -489,7 +488,7 @@ def to_sympy(self, expr, **kwargs): ).to_sympy() -class ArcSin(_MPMathFunction): +class ArcSin(MPMathFunction): """ Inverse sine, :arcsine: @@ -525,7 +524,7 @@ class ArcSin(_MPMathFunction): sympy_name = "asin" -class ArcTan(_MPMathFunction): +class ArcTan(MPMathFunction): """ Inverse tangent, :arctangent: @@ -585,7 +584,7 @@ class ArcTan(_MPMathFunction): sympy_name = "atan" -class Cos(_MPMathFunction): +class Cos(MPMathFunction): """ :Cosine: @@ -624,7 +623,7 @@ class Cos(_MPMathFunction): sympy_name = "cos" -class Cot(_MPMathFunction): +class Cot(MPMathFunction): """ :Cotangent: @@ -659,7 +658,7 @@ class Cot(_MPMathFunction): sympy_name = "cot" -class Csc(_MPMathFunction): +class Csc(MPMathFunction): """ :Cosecant: @@ -702,7 +701,7 @@ def to_sympy(self, expr, **kwargs): ).to_sympy() -class Haversine(_MPMathFunction): +class Haversine(MPMathFunction): """ :WMA link: @@ -724,7 +723,7 @@ class Haversine(_MPMathFunction): summary_text = "Haversine function" -class InverseHaversine(_MPMathFunction): +class InverseHaversine(MPMathFunction): """ :WMA link: @@ -746,7 +745,7 @@ class InverseHaversine(_MPMathFunction): summary_text = "inverse Haversine function" -class Sec(_MPMathFunction): +class Sec(MPMathFunction): """ :Secant: @@ -788,7 +787,7 @@ def to_sympy(self, expr, **kwargs): ).to_sympy() -class Sin(_MPMathFunction): +class Sin(MPMathFunction): """ :Sine: @@ -835,7 +834,7 @@ class Sin(_MPMathFunction): sympy_name = "sin" -class Tan(_MPMathFunction): +class Tan(MPMathFunction): """ :Tangent: diff --git a/mathics/builtin/numeric.py b/mathics/builtin/numeric.py index d0bb14222..455d9a940 100644 --- a/mathics/builtin/numeric.py +++ b/mathics/builtin/numeric.py @@ -1,4 +1,3 @@ -# cython: language_level=3 # -*- coding: utf-8 -*- # Note: docstring is flowed in documentation. Line breaks in the @@ -12,16 +11,47 @@ in algebraic or symbolic form. """ +from typing import Optional + import sympy -from mathics.builtin.base import Builtin -from mathics.core.atoms import Complex, Integer, Integer0, Rational, Real -from mathics.core.attributes import A_LISTABLE, A_NUMERIC_FUNCTION, A_PROTECTED +from mathics.builtin.base import Builtin, MPMathFunction, SympyFunction +from mathics.builtin.inference import evaluate_predicate +from mathics.core.atoms import ( + Complex, + Integer, + Integer0, + IntegerM1, + Number, + Rational, + Real, +) +from mathics.core.attributes import ( + A_HOLD_ALL, + A_LISTABLE, + A_NUMERIC_FUNCTION, + A_PROTECTED, +) from mathics.core.convert.sympy import from_sympy +from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.number import MACHINE_EPSILON -from mathics.core.symbols import SymbolDivide, SymbolMachinePrecision, SymbolTimes +from mathics.core.symbols import ( + Symbol, + SymbolDivide, + SymbolFalse, + SymbolMachinePrecision, + SymbolTimes, + SymbolTrue, +) +from mathics.core.systemsymbols import SymbolPiecewise +from mathics.eval.arithmetic import ( + eval_Abs, + eval_negate_number, + eval_RealSign, + eval_Sign, +) from mathics.eval.nevaluator import eval_NValues @@ -45,6 +75,54 @@ def chop(expr, delta=10.0 ** (-10.0)): return expr +class Abs(MPMathFunction): + """ + + :Absolute value: + https://en.wikipedia.org/wiki/Absolute_value ( + :SymPy: + https://docs.sympy.org/latest/modules/functions + /elementary.html#sympy.functions.elementary.complexes.Abs, + :WMA: https://reference.wolfram.com/language/ref/Abs) + +
    +
    'Abs[$x$]' +
    returns the absolute value of $x$. +
    + + >> Abs[-3] + = 3 + + >> Plot[Abs[x], {x, -4, 4}] + = -Graphics- + + 'Abs' returns the magnitude of complex numbers: + >> Abs[3 + I] + = Sqrt[10] + >> Abs[3.0 + I] + = 3.16228 + + All of the below evaluate to Infinity: + + >> Abs[Infinity] == Abs[I Infinity] == Abs[ComplexInfinity] + = True + """ + + mpmath_name = "fabs" # mpmath actually uses python abs(x) / x.__abs__() + rules = { + "Abs[Undefined]": "Undefined", + } + summary_text = "absolute value of a number" + sympy_name = "Abs" + + def eval(self, x, evaluation: Evaluation): + "Abs[x_]" + result = eval_Abs(x) + if result is not None: + return result + return super(Abs, self).eval(x, evaluation) + + class Chop(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Chop.html @@ -252,6 +330,101 @@ def eval_N(self, expr, evaluation: Evaluation): return eval_NValues(expr, SymbolMachinePrecision, evaluation) +class Piecewise(SympyFunction): + """ + :WMA link:https://reference.wolfram.com/language/ref/Piecewise.html + +
    +
    'Piecewise[{{expr1, cond1}, ...}]' +
    represents a piecewise function. + +
    'Piecewise[{{expr1, cond1}, ...}, expr]' +
    represents a piecewise function with default 'expr'. +
    + + Heaviside function + >> Piecewise[{{0, x <= 0}}, 1] + = Piecewise[{{0, x <= 0}}, 1] + + ## D[%, x] + ## Piecewise({{0, Or[x < 0, x > 0]}}, Indeterminate). + + >> Integrate[Piecewise[{{1, x <= 0}, {-1, x > 0}}], x] + = Piecewise[{{x, x <= 0}}, -x] + + >> Integrate[Piecewise[{{1, x <= 0}, {-1, x > 0}}], {x, -1, 2}] + = -1 + + Piecewise defaults to 0 if no other case is matching. + >> Piecewise[{{1, False}}] + = 0 + + >> Plot[Piecewise[{{Log[x], x > 0}, {x*-0.5, x < 0}}], {x, -1, 1}] + = -Graphics- + + >> Piecewise[{{0 ^ 0, False}}, -1] + = -1 + """ + + summary_text = "an arbitrary piecewise function" + sympy_name = "Piecewise" + + attributes = A_HOLD_ALL | A_PROTECTED + + def eval(self, items, evaluation: Evaluation): + "%(name)s[items__]" + result = self.to_sympy( + Expression(SymbolPiecewise, *items.get_sequence()), evaluation=evaluation + ) + if result is None: + return + if not isinstance(result, sympy.Piecewise): + result = from_sympy(result) + return result + + def to_sympy(self, expr, **kwargs): + elements = expr.elements + evaluation = kwargs.get("evaluation", None) + if len(elements) not in (1, 2): + return + + sympy_cases = [] + for case in elements[0].elements: + if case.get_head_name() != "System`List": + return + if len(case.elements) != 2: + return + then, cond = case.elements + if evaluation: + cond = evaluate_predicate(cond, evaluation) + + sympy_cond = None + if isinstance(cond, Symbol): + if cond is SymbolTrue: + sympy_cond = True + elif cond is SymbolFalse: + sympy_cond = False + if sympy_cond is None: + sympy_cond = cond.to_sympy(**kwargs) + if not (sympy_cond.is_Relational or sympy_cond.is_Boolean): + return + + sympy_cases.append((then.to_sympy(**kwargs), sympy_cond)) + + if len(elements) == 2: # default case + sympy_cases.append((elements[1].to_sympy(**kwargs), True)) + else: + sympy_cases.append((Integer0.to_sympy(**kwargs), True)) + + return sympy.Piecewise(*sympy_cases) + + def from_sympy(self, sympy_name, args): + # Hack to get around weird sympy.Piecewise 'otherwise' behaviour + if str(args[-1].elements[1]).startswith("System`_True__Dummy_"): + args[-1].elements[1] = SymbolTrue + return Expression(self.get_name(), args) + + class Rationalize(Builtin): """ :WMA link: @@ -395,6 +568,86 @@ def approx_interval_continued_fraction(xmin, xmax): return result +class RealAbs(Builtin): + """ + :Abs (Real): + https://en.wikipedia.org/wiki/Absolute_value ( + :WMA link: + https://reference.wolfram.com/language/ref/RealAbs.html) + +
    +
    'RealAbs[$x$]' +
    returns the absolute value of a real number $x$. +
    + 'RealAbs' is also known as modulus. It is evaluated if $x$ can be compared \ + with $0$. + + >> RealAbs[-3.] + = 3. + 'RealAbs[$z$]' is left unevaluated for complex $z$: + >> RealAbs[2. + 3. I] + = RealAbs[2. + 3. I] + >> D[RealAbs[x ^ 2], x] + = 2 x ^ 3 / RealAbs[x ^ 2] + """ + + attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED + rules = { + "D[RealAbs[x_],x_]": "x/RealAbs[x]", + "Integrate[RealAbs[x_],x_]": "1/2 x RealAbs[x]", + "Integrate[RealAbs[u_],{u_,a_,b_}]": "1/2 b RealAbs[b]-1/2 a RealAbs[a]", + } + summary_text = "real absolute value" + + def eval(self, x: BaseElement, evaluation: Evaluation): + """RealAbs[x_]""" + real_sign = eval_RealSign(x) + if real_sign is IntegerM1: + return eval_negate_number(x) + if real_sign is None: + return + return x + + +class RealSign(Builtin): + """ + :Sign function: + https://en.wikipedia.org/wiki/Sign_function ( + :WMA link: + https://reference.wolfram.com/language/ref/RealSign.html) + +
    +
    'RealSign[$x$]' +
    returns -1, 0 or 1 depending on whether $x$ is negative, + zero or positive. +
    + 'RealSign' is also known as $sgn$ or $signum$ function. + + >> RealSign[-3.] + = -1 + 'RealSign[$z$]' is left unevaluated for complex $z$: + >> RealSign[2. + 3. I] + = RealSign[2. + 3. I] + + >> D[RealSign[x^2],x] + = 2 x Piecewise[{{0, x ^ 2 != 0}}, Indeterminate] + >> Integrate[RealSign[u],{u,0,x}] + = RealAbs[x] + """ + + attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED + rules = { + "D[RealSign[x_],x_]": "Piecewise[{{0, x!=0}}, Indeterminate]", + "Integrate[RealSign[x_],x_]": "RealAbs[x]", + "Integrate[RealSign[u_],{u_, a_, b_}]": "RealAbs[b]-RealSign[a]", + } + summary_text = "real sign" + + def eval(self, x: Number, evaluation: Evaluation) -> Optional[Integer]: + """RealSign[x_]""" + return eval_RealSign(x) + + class RealValuedNumberQ(Builtin): # No docstring since this is internal and it will mess up documentation. # FIXME: Perhaps in future we will have a more explicite way to indicate not @@ -493,3 +746,69 @@ def eval(self, expr, k, evaluation: Evaluation): n = round(n) n = int(n) return Expression(SymbolTimes, Integer(n), k) + + +class Sign(SympyFunction): + """ + + :Sign: + https://en.wikipedia.org/wiki/Sign_function ( + :WMA link: + https://reference.wolfram.com/language/ref/Sign.html) + +
    +
    'Sign[$x$]' +
    return -1, 0, or 1 depending on whether $x$ is negative, zero, or positive. +
    + + >> Sign[19] + = 1 + >> Sign[-6] + = -1 + >> Sign[0] + = 0 + >> Sign[{-5, -10, 15, 20, 0}] + = {-1, -1, 1, 1, 0} + #> Sign[{1, 2.3, 4/5, {-6.7, 0}, {8/9, -10}}] + = {1, 1, 1, {-1, 0}, {1, -1}} + >> Sign[3 - 4*I] + = 3 / 5 - 4 I / 5 + #> Sign[1 - 4*I] == (1/17 - 4 I/17) Sqrt[17] + = True + #> Sign[4, 5, 6] + : Sign called with 3 arguments; 1 argument is expected. + = Sign[4, 5, 6] + #> Sign["20"] + = Sign[20] + """ + + summary_text = "complex sign of a number" + sympy_name = "sign" + # mpmath_name = 'sign' + + attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED + + messages = { + "argx": "Sign called with `1` arguments; 1 argument is expected.", + } + + rules = { + "Sign[Power[a_, b_]]": "Power[Sign[a], b]", + } + + def eval(self, x, evaluation: Evaluation): + "Sign[x_]" + 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 + # Unhandled cases. Use sympy + return super(Sign, self).eval(x, evaluation) + + def eval_error(self, x, seqs, evaluation: Evaluation): + "Sign[x_, seqs__]" + evaluation.message("Sign", "argx", Integer(len(seqs.get_sequence()) + 1)) diff --git a/mathics/builtin/specialfns/bessel.py b/mathics/builtin/specialfns/bessel.py index efd9de159..f5c80ce4f 100644 --- a/mathics/builtin/specialfns/bessel.py +++ b/mathics/builtin/specialfns/bessel.py @@ -4,8 +4,7 @@ import mpmath -from mathics.builtin.arithmetic import _MPMathFunction -from mathics.builtin.base import Builtin +from mathics.builtin.base import Builtin, MPMathFunction from mathics.core.atoms import Integer from mathics.core.attributes import ( A_LISTABLE, @@ -24,14 +23,14 @@ ) -class _Bessel(_MPMathFunction): +class _Bessel(MPMathFunction): attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED | A_READ_PROTECTED nargs = {2} -class AiryAi(_MPMathFunction): +class AiryAi(MPMathFunction): """ :Airy function of the first kind: https://en.wikipedia.org/wiki/Airy_function ( @@ -65,7 +64,7 @@ class AiryAi(_MPMathFunction): sympy_name = "airyai" -class AiryAiPrime(_MPMathFunction): +class AiryAiPrime(MPMathFunction): """ Derivative of Airy function ( :Sympy: @@ -160,7 +159,7 @@ def eval_N(self, k, precision, evaluation: Evaluation): return from_mpmath(result, precision=p) -class AiryBi(_MPMathFunction): +class AiryBi(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/AiryBi.html @@ -193,7 +192,7 @@ class AiryBi(_MPMathFunction): sympy_name = "airybi" -class AiryBiPrime(_MPMathFunction): +class AiryBiPrime(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/AiryBiPrime.html diff --git a/mathics/builtin/specialfns/erf.py b/mathics/builtin/specialfns/erf.py index a79aac241..2e8a4f938 100644 --- a/mathics/builtin/specialfns/erf.py +++ b/mathics/builtin/specialfns/erf.py @@ -5,17 +5,18 @@ """ -from mathics.builtin.arithmetic import _MPMathFunction, _MPMathMultiFunction +from mathics.builtin.base import MPMathFunction, MPMathMultiFunction from mathics.core.attributes import A_LISTABLE, A_NUMERIC_FUNCTION, A_PROTECTED -class Erf(_MPMathMultiFunction): +class Erf(MPMathMultiFunction): """ :Error function: https://en.wikipedia.org/wiki/Error_function ( :SymPy: - https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.error_functions.erf, + https://docs.sympy.org/latest/modules/functions + /special.html#sympy.functions.special.error_functions.erf, :WMA: https://reference.wolfram.com/language/ref/Erf.html)
    @@ -55,12 +56,13 @@ class Erf(_MPMathMultiFunction): } -class Erfc(_MPMathFunction): +class Erfc(MPMathFunction): """ :Complementary Error function: https://en.wikipedia.org/wiki/Error_function ( - :SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.error_functions.erfc, + :SymPy: https://docs.sympy.org/latest/modules/functions + /special.html#sympy.functions.special.error_functions.erfc, :WMA: https://reference.wolfram.com/language/ref/Erfc.html) @@ -86,13 +88,14 @@ class Erfc(_MPMathFunction): } -class FresnelC(_MPMathFunction): +class FresnelC(MPMathFunction): """ :Fresnel integral: https://en.wikipedia.org/wiki/Fresnel_integral ( :mpmath: - https://mpmath.org/doc/current/functions/expintegrals.html?highlight=fresnelc#mpmath.fresnelc, + https://mpmath.org/doc/current/functions/expintegrals.html?mpmath.fresnelc,\ + :WMA: https://reference.wolfram.com/language/ref/FresnelC.html)
    @@ -115,13 +118,14 @@ class FresnelC(_MPMathFunction): mpmath_name = "fresnelc" -class FresnelS(_MPMathFunction): +class FresnelS(MPMathFunction): """ :Fresnel integral: https://en.wikipedia.org/wiki/Fresnel_integral ( :mpmath: - https://mpmath.org/doc/current/functions/expintegrals.html#mpmath.fresnels, + https://mpmath.org/doc/current/functions/expintegrals.html#mpmath.fresnels,\ + :WMA: https://reference.wolfram.com/language/ref/FresnelS.html) @@ -145,13 +149,13 @@ class FresnelS(_MPMathFunction): mpmath_name = "fresnels" -class InverseErf(_MPMathFunction): +class InverseErf(MPMathFunction): """ :Inverse error function: https://en.wikipedia.org/wiki/Error_function#Inverse_functions ( :SymPy: - https://docs.sympy.org/latest/modules/functions/special.html?highlight=erfinv#sympy.functions.special.error_functions.erfinv, + https://docs.sympy.org/latest/modules/functions/special.html?sympy.functions.special.error_functions.erfinv, :WMA: https://reference.wolfram.com/language/ref/InverseErf.html) @@ -192,13 +196,16 @@ def eval(self, z, evaluation): raise -class InverseErfc(_MPMathFunction): +class InverseErfc(MPMathFunction): """ :Complementary error function: - https://en.wikipedia.org/wiki/Error_function#Complementary_error_function ( + https://en.wikipedia.org/wiki/Error_function#Complementary_error_function\ + ( :SymPy: - https://docs.sympy.org/latest/modules/functions/special.html?highlight=erfinv#sympy.functions.special.error_functions.erfcinv, + https://docs.sympy.org/latest/modules/functions + /special.html?sympy.functions.special.error_functions.erfcinv,\ + :WMA: https://reference.wolfram.com/language/ref/InverseErfc.html)
    diff --git a/mathics/builtin/specialfns/expintegral.py b/mathics/builtin/specialfns/expintegral.py index 63cc79849..fd7468c46 100644 --- a/mathics/builtin/specialfns/expintegral.py +++ b/mathics/builtin/specialfns/expintegral.py @@ -5,10 +5,10 @@ """ -from mathics.builtin.arithmetic import _MPMathFunction +from mathics.builtin.base import MPMathFunction -class ExpIntegralE(_MPMathFunction): +class ExpIntegralE(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/ExpIntegralE.html @@ -27,7 +27,7 @@ class ExpIntegralE(_MPMathFunction): mpmath_name = "expint" -class ExpIntegralEi(_MPMathFunction): +class ExpIntegralEi(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/ExpIntegralEi.html @@ -45,7 +45,7 @@ class ExpIntegralEi(_MPMathFunction): mpmath_name = "ei" -class ProductLog(_MPMathFunction): +class ProductLog(MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/ProductLog.html @@ -83,7 +83,7 @@ class ProductLog(_MPMathFunction): # TODO: Zernike polynomials not yet implemented in mpmath nor sympy # -# class ZernikeR(_MPMathFunction): +# class ZernikeR(MPMathFunction): # """ #
    #
    'ZernikeR[$n$, $m$, $r$]' diff --git a/mathics/builtin/specialfns/gamma.py b/mathics/builtin/specialfns/gamma.py index e441b8a57..9c935d1ae 100644 --- a/mathics/builtin/specialfns/gamma.py +++ b/mathics/builtin/specialfns/gamma.py @@ -6,8 +6,12 @@ import mpmath import sympy -from mathics.builtin.arithmetic import _MPMathFunction, _MPMathMultiFunction -from mathics.builtin.base import PostfixOperator, SympyFunction +from mathics.builtin.base import ( + MPMathFunction, + MPMathMultiFunction, + PostfixOperator, + SympyFunction, +) from mathics.core.atoms import Integer, Integer0, Number from mathics.core.attributes import A_LISTABLE, A_NUMERIC_FUNCTION, A_PROTECTED from mathics.core.convert.mpmath import from_mpmath @@ -22,7 +26,7 @@ from mathics.eval.numerify import numerify -class Beta(_MPMathMultiFunction): +class Beta(MPMathMultiFunction): """ :Euler beta function: @@ -118,7 +122,7 @@ def eval_with_z(self, z, a, b, evaluation): return result -class Factorial(PostfixOperator, _MPMathFunction): +class Factorial(PostfixOperator, MPMathFunction): """ :Factorial: https://en.wikipedia.org/wiki/Factorial ( @@ -163,7 +167,7 @@ class Factorial(PostfixOperator, _MPMathFunction): summary_text = "factorial" -class Factorial2(PostfixOperator, _MPMathFunction): +class Factorial2(PostfixOperator, MPMathFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Factorial2.html @@ -172,7 +176,8 @@ class Factorial2(PostfixOperator, _MPMathFunction):
    '$n$!!'
    computes the double factorial of $n$.
    - The double factorial or semifactorial of a number $n$, is the product of all the integers from 1 up to n that have the same parity (odd or even) as $n$. + The double factorial or semifactorial of a number $n$, is the product of all the \ + integers from 1 up to n that have the same parity (odd or even) as $n$. >> 5!! = 15. @@ -248,11 +253,12 @@ def fact2_generic(x): return convert_from_fn(result) -class Gamma(_MPMathMultiFunction): +class Gamma(MPMathMultiFunction): """ :Gamma function: https://en.wikipedia.org/wiki/Gamma_function ( - :SymPy:https://docs.sympy.org/latest/modules/functions/special.html#module-sympy.functions.special.gamma_functions, + :SymPy:https://docs.sympy.org/latest/modules/functions + /special.html#module-sympy.functions.special.gamma_functions, :mpmath: https://mpmath.org/doc/current/functions/gamma.html#gamma, :WMA:https://reference.wolfram.com/language/ref/Gamma.html) @@ -345,7 +351,7 @@ def from_sympy(self, sympy_name, elements): return Expression(Symbol(self.get_name()), *elements) -class LogGamma(_MPMathMultiFunction): +class LogGamma(MPMathMultiFunction): """ :log-gamma function: https://en.wikipedia.org/wiki/Gamma_function#The_log-gamma_function ( @@ -448,7 +454,7 @@ class Pochhammer(SympyFunction): sympy_name = "RisingFactorial" -class PolyGamma(_MPMathMultiFunction): +class PolyGamma(MPMathMultiFunction): r""" :Polygamma function: https://en.wikipedia.org/wiki/Polygamma_function ( @@ -495,7 +501,8 @@ class StieltjesGamma(SympyFunction): :Stieltjes constants: https://en.wikipedia.org/wiki/Stieltjes_constants ( :SymPy: - https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.zeta_functions.stieltjes, + https://docs.sympy.org/latest/modules/functions + /special.html#sympy.functions.special.zeta_functions.stieltjes, :WMA: https://reference.wolfram.com/language/ref/StieltjesGamma.html) diff --git a/mathics/builtin/specialfns/orthogonal.py b/mathics/builtin/specialfns/orthogonal.py index 82cbee21a..9815087f7 100644 --- a/mathics/builtin/specialfns/orthogonal.py +++ b/mathics/builtin/specialfns/orthogonal.py @@ -3,11 +3,11 @@ """ -from mathics.builtin.arithmetic import _MPMathFunction +from mathics.builtin.base import MPMathFunction from mathics.core.atoms import Integer0 -class ChebyshevT(_MPMathFunction): +class ChebyshevT(MPMathFunction): """ :Chebyshev polynomial of the first kind: https://en.wikipedia.org/wiki/Chebyshev_polynomials (:Sympy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.polynomials.chebyshevt, :WMA: https://reference.wolfram.com/language/ref/ChebyshevT.html) @@ -29,7 +29,7 @@ class ChebyshevT(_MPMathFunction): sympy_name = "chebyshevt" -class ChebyshevU(_MPMathFunction): +class ChebyshevU(MPMathFunction): """ :Chebyshev polynomial of the second kind: https://en.wikipedia.org/wiki/Chebyshev_polynomials (:Sympy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.polynomials.chebyshevu, :WMA: https://reference.wolfram.com/language/ref/ChebyshevU.html) @@ -52,7 +52,7 @@ class ChebyshevU(_MPMathFunction): sympy_name = "chebyshevu" -class GegenbauerC(_MPMathFunction): +class GegenbauerC(MPMathFunction): """ :Gegenbauer polynomials: https://en.wikipedia.org/wiki/Gegenbauer_polynomials (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.polynomials.gegenbauer, :WMA: https://reference.wolfram.com/language/ref/GegenbauerC.html) @@ -76,7 +76,7 @@ class GegenbauerC(_MPMathFunction): sympy_name = "gegenbauer" -class HermiteH(_MPMathFunction): +class HermiteH(MPMathFunction): """ :Hermite polynomial: https://en.wikipedia.org/wiki/Hermite_polynomials (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.polynomials.hermite, :WMA: https://reference.wolfram.com/language/ref/HermiteH.html)
    @@ -100,7 +100,7 @@ class HermiteH(_MPMathFunction): summary_text = "Hermite's polynomials" -class JacobiP(_MPMathFunction): +class JacobiP(MPMathFunction): """ :Jacobi polynomials: https://en.wikipedia.org/wiki/Jacobi_polynomials (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.polynomials.jacobi, :WMA: https://reference.wolfram.com/language/ref/JacobiP.html) @@ -122,7 +122,7 @@ class JacobiP(_MPMathFunction): summary_text = "Jacobi's polynomials" -class LaguerreL(_MPMathFunction): +class LaguerreL(MPMathFunction): """ :Laguerre polynomials: https://en.wikipedia.org/wiki/Laguerre_polynomials (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.polynomials.leguarre_poly, :WMA: https://reference.wolfram.com/language/ref/LeguerreL.html) @@ -159,7 +159,7 @@ def prepare_sympy(self, leaves): return leaves -class LegendreP(_MPMathFunction): +class LegendreP(MPMathFunction): """ :Lengendre polynomials: https://en.wikipedia.org/wiki/Legendre_polynomials (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.polynomials.legendre, :WMA: https://reference.wolfram.com/language/ref/LegendreP)
    @@ -210,7 +210,7 @@ def prepare_sympy(self, elements): return elements -class LegendreQ(_MPMathFunction): +class LegendreQ(MPMathFunction): """ :Legendre functions of the second kind: https://mathworld.wolfram.com/LegendreFunctionoftheSecondKind.html (:mpmath: https://mpmath.org/doc/current/functions/orthogonal.html#mpmath.legenq, :WMA: https://reference.wolfram.com/language/ref/LegendreQ)
    @@ -255,7 +255,7 @@ def prepare_sympy(self, elements): return elements -class SphericalHarmonicY(_MPMathFunction): +class SphericalHarmonicY(MPMathFunction): """ :Spherical Harmonic https://mathworld.wolfram.com/SphericalHarmonic.html (:mpmath: https://mpmath.org/doc/current/functions/orthogonal.html#mpmath.sperharm, :WMA: https://reference.wolfram.com/language/ref/SphericalHarmonicY.html)
    @@ -290,7 +290,7 @@ def prepare_mathics(self, sympy_expr): # TODO: Zernike polynomials not yet implemented in mpmath nor sympy # -# class ZernikeR(_MPMathFunction): +# class ZernikeR(MPMathFunction): # """ # :Zermike polynomials: https://en.wikipedia.org/wiki/Zernike_polynomials. diff --git a/mathics/builtin/specialfns/zeta.py b/mathics/builtin/specialfns/zeta.py index 85059404c..84ce43bdb 100644 --- a/mathics/builtin/specialfns/zeta.py +++ b/mathics/builtin/specialfns/zeta.py @@ -6,11 +6,11 @@ import mpmath -from mathics.builtin.arithmetic import _MPMathFunction +from mathics.builtin.base import MPMathFunction from mathics.core.convert.mpmath import from_mpmath -class LerchPhi(_MPMathFunction): +class LerchPhi(MPMathFunction): """ :WMA link: @@ -45,7 +45,7 @@ def eval(self, z, s, a, evaluation): # return sympy.expand_func(sympy.lerchphi(py_z, py_s, py_a)) -class Zeta(_MPMathFunction): +class Zeta(MPMathFunction): """ :WMA link: diff --git a/mathics/timing.py b/mathics/timing.py index f8ab01ac7..72ac28737 100644 --- a/mathics/timing.py +++ b/mathics/timing.py @@ -65,9 +65,8 @@ def show_lru_cache_statistics(): """ Print statistics from LRU caches (@lru_cache of functools) """ - from mathics.builtin.arithmetic import _MPMathFunction from mathics.builtin.atomic.numbers import log_n_b - from mathics.builtin.base import run_sympy + from mathics.builtin.base import MPMathFunction, run_sympy from mathics.core.atoms import Integer, Rational from mathics.core.convert.mpmath import from_mpmath from mathics.eval.arithmetic import call_mpmath @@ -77,6 +76,6 @@ def show_lru_cache_statistics(): print(f"call_mpmath {call_mpmath.cache_info()}") print(f"log_n_b {log_n_b.cache_info()}") print(f"from_mpmath {from_mpmath.cache_info()}") - print(f"get_mpmath_function {_MPMathFunction.get_mpmath_function.cache_info()}") + print(f"get_mpmath_function {MPMathFunction.get_mpmath_function.cache_info()}") print(f"run_sympy {run_sympy.cache_info()}") diff --git a/test/test_evaluators.py b/test/test_evaluators.py index 6257b2544..2d53767f1 100644 --- a/test/test_evaluators.py +++ b/test/test_evaluators.py @@ -4,9 +4,9 @@ from mathics.eval.nevaluator import eval_N, eval_NValues from mathics.eval.numerify import numerify as eval_numerify -from mathics.session import MathicsSession -session = MathicsSession() +from .helper import session + evaluation = session.evaluation