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