Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ubuntu-minimal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Mathics (ubuntu-minimal)

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:
env:
NO_CYTHON: 1
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: [3.9]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev
python -m pip install --upgrade pip
# Can remove after next Mathics-Scanner release
# python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full]
- name: Install Mathics with full dependencies
run: |
make develop
- name: Test Mathics
run: |
make -j3 check
5 changes: 4 additions & 1 deletion mathics/algorithm/optimizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,16 +350,19 @@ def sub(evaluation):
return x0, True


native_optimizer_messages = {}

native_local_optimizer_methods = {
"Automatic": find_minimum_newton1d,
"newton": find_minimum_newton1d,
"Newton": find_minimum_newton1d,
}

native_findroot_methods = {
"Automatic": find_root_newton,
"Newton": find_root_newton,
"Secant": find_root_secant,
}
native_findroot_messages = {}


def is_zero(
Expand Down
1 change: 1 addition & 0 deletions mathics/builtin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
SympyObject,
Operator,
PatternObject,
check_requires_list,
)


Expand Down
58 changes: 40 additions & 18 deletions mathics/builtin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,31 @@
from mathics.core.attributes import protected, read_protected


# This global dict stores which libraries was required to
# be available, and the corresponding result.
requires_lib_cache = {}


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.

This state value is also recorded in dictionary `requires_lib_cache` keyed by module name and is used to determine whether to skip trying to get information from the module."""
for package in requires:
lib_is_installed = requires_lib_cache.get(package, None)
if lib_is_installed is None:
lib_is_installed = True
try:
importlib.import_module(package)
except ImportError:
lib_is_installed = False
requires_lib_cache[package] = lib_is_installed

if not lib_is_installed:
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
Expand Down Expand Up @@ -407,7 +432,6 @@ def get_functions(self, prefix="apply", is_pymodule=False):
unavailable_function = self._get_unavailable_function()
for name in dir(self):
if name.startswith(prefix):

function = getattr(self, name)
pattern = function.__doc__
if pattern is None: # Fixes PyPy bug
Expand Down Expand Up @@ -439,25 +463,23 @@ def get_functions(self, prefix="apply", is_pymodule=False):
def get_option(options, name, evaluation, pop=False):
return get_option(options, name, evaluation, pop)

def _get_unavailable_function(self):
requires = getattr(self, "requires", [])

for package in requires:
try:
importlib.import_module(package)
except ImportError:

def apply(**kwargs): # will override apply method
kwargs["evaluation"].message(
"General",
"pyimport", # see inout.py
strip_context(self.get_name()),
package,
)
def _get_unavailable_function(self) -> "Optional[function]":
"""
If some of the required libraries for a symbol are not available,
returns a default function that override the ``apply_`` methods
of the class. Otherwise, returns ``None``.
"""

return apply
def apply_unavailable(**kwargs): # will override apply method
kwargs["evaluation"].message(
"General",
"pyimport", # see inout.py
strip_context(self.get_name()),
package,
)

return None
requires = getattr(self, "requires", [])
return None if check_requires_list(requires) else apply_unavailable

def get_option_string(self, *params):
s = self.get_option(*params)
Expand Down
60 changes: 33 additions & 27 deletions mathics/builtin/numbers/calculus.py
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,6 @@ class _BaseFinder(Builtin):
"""

attributes = hold_all | protected
requires = ["scipy"]
methods = {}
messages = {
"snum": "Value `1` is not a number.",
Expand Down Expand Up @@ -1482,9 +1481,13 @@ class FindRoot(_BaseFinder):
)

try:
from mathics.algorithm.optimizers import native_findroot_methods
from mathics.algorithm.optimizers import (
native_findroot_methods,
native_findroot_messages,
)

methods.update(native_findroot_methods)
messages.update(native_findroot_messages)
except Exception:
pass
try:
Expand Down Expand Up @@ -1518,8 +1521,8 @@ class FindMinimum(_BaseFinder):
= {2., {x -> 3.}}
>> FindMinimum[Sin[x], {x, 1}]
= {-1., {x -> -1.5708}}
>> phi[x_?NumberQ]:=NIntegrate[u,{u,0,x}];
>> FindMinimum[phi[x]-x,{x,1.2}]
>> phi[x_?NumberQ]:=NIntegrate[u,{u,0,x}, Method->"Internal"];
>> Quiet[FindMinimum[phi[x]-x,{x, 1.2}, Method->"Newton"]]
= {-0.5, {x -> 1.00001}}
>> Clear[phi];
For a not so well behaving function, the result can be less accurate:
Expand All @@ -1531,9 +1534,13 @@ class FindMinimum(_BaseFinder):
methods = {}
summary_text = "local minimum optimization"
try:
from mathics.algorithm.optimizers import native_local_optimizer_methods
from mathics.algorithm.optimizers import (
native_local_optimizer_methods,
native_optimizer_messages,
)

methods.update(native_local_optimizer_methods)
messages.update(native_optimizer_messages)
except Exception:
pass
try:
Expand Down Expand Up @@ -1561,8 +1568,8 @@ class FindMaximum(_BaseFinder):
= {2., {x -> 3.}}
>> FindMaximum[Sin[x], {x, 1}]
= {1., {x -> 1.5708}}
>> phi[x_?NumberQ]:=NIntegrate[u,{u,0,x}];
>> FindMaximum[-phi[x]+x,{x,1.2}]
>> phi[x_?NumberQ]:=NIntegrate[u, {u, 0., x}, Method->"Internal"];
>> Quiet[FindMaximum[-phi[x] + x, {x, 1.2}, Method->"Newton"]]
= {0.5, {x -> 1.00001}}
>> Clear[phi];
For a not so well behaving function, the result can be less accurate:
Expand Down Expand Up @@ -2021,33 +2028,31 @@ class NIntegrate(Builtin):
<dd>returns a numeric approximation to the multiple integral of $expr$ with limits $interval1$, $interval2$ and with a precision of $prec$ digits.
</dl>

>> NIntegrate[Exp[-x],{x,0,Infinity},Tolerance->1*^-6]
>> NIntegrate[Exp[-x],{x,0,Infinity},Tolerance->1*^-6, Method->"Internal"]
= 1.
>> NIntegrate[Exp[x],{x,-Infinity, 0},Tolerance->1*^-6]
>> NIntegrate[Exp[x],{x,-Infinity, 0},Tolerance->1*^-6, Method->"Internal"]
= 1.
>> NIntegrate[Exp[-x^2/2.],{x,-Infinity, Infinity},Tolerance->1*^-6]
= 2.50663
>> NIntegrate[Exp[-x^2/2.],{x,-Infinity, Infinity},Tolerance->1*^-6, Method->"Internal"]
= 2.50664

>> Table[1./NIntegrate[x^k,{x,0,1},Tolerance->1*^-6], {k,0,6}]
: The specified method failed to return a number. Falling back into the internal evaluator.
= {1., 2., 3., 4., 5., 6., 7.}
"""

>> NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance->1.*^-4]
: Integration over a complex domain is not implemented yet
= NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance -> 0.0001]
## = 6.2832 I
# ## The Following tests fails if sympy is not installed.
# >> Table[1./NIntegrate[x^k,{x,0,1},Tolerance->1*^-6], {k,0,6}]
# : The specified method failed to return a number. Falling back into the internal evaluator.
# = {1., 2., 3., 4., 5., 6., 7.}

Integrate singularities with weak divergences:
>> Table[ NIntegrate[x^(1./k-1.), {x,0,1.}, Tolerance->1*^-6], {k,1,7.} ]
= {1., 2., 3., 4., 5., 6., 7.}
# >> NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance->1.*^-4]
# ## = 6.2832 I

Mutiple Integrals :
>> NIntegrate[x * y,{x, 0, 1}, {y, 0, 1}]
= 0.25
# Integrate singularities with weak divergences:
# >> Table[ NIntegrate[x^(1./k-1.), {x,0,1.}, Tolerance->1*^-6], {k,1,7.}]
# = {1., 2., 3., 4., 5., 6., 7.}

"""
# Mutiple Integrals :
# >> NIntegrate[x * y,{x, 0, 1}, {y, 0, 1}]
# = 0.25

requires = ["scipy"]
summary_text = "numerical integration in one or several variables"
messages = {
"bdmtd": "The Method option should be a built-in method name.",
Expand Down Expand Up @@ -2122,6 +2127,8 @@ def apply_with_func_domain(self, func, domain, evaluation, options):
method = method.value
elif isinstance(method, Symbol):
method = method.get_name()
# strip context
method = method[method.rindex("`") + 1 :]
else:
evaluation.message("NIntegrate", "bdmtd", method)
return
Expand Down Expand Up @@ -2155,7 +2162,6 @@ def apply_with_func_domain(self, func, domain, evaluation, options):

intvars = ListExpression(*coords)
integrand = Expression(SymbolCompile, intvars, func).evaluate(evaluation)

if len(integrand.elements) >= 3:
integrand = integrand.elements[2].cfunc
else:
Expand Down
3 changes: 2 additions & 1 deletion mathics/builtin/scipy_utils/integrators.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
import sys
from mathics.builtin import check_requires_list

IS_PYPY = "__pypy__" in sys.builtin_module_names
if IS_PYPY:
if IS_PYPY or not check_requires_list(["scipy", "numpy"]):
raise ImportError

import numpy as np
Expand Down
5 changes: 4 additions & 1 deletion mathics/builtin/scipy_utils/optimizers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import sys

from mathics.builtin import check_requires_list

from mathics.core.expression import Expression
from mathics.core.evaluation import Evaluation
from mathics.core.atoms import Number, Real
Expand All @@ -12,9 +14,10 @@
SymbolCompile = Symbol("Compile")

IS_PYPY = "__pypy__" in sys.builtin_module_names
if IS_PYPY:
if IS_PYPY or not check_requires_list(["scipy", "numpy"]):
raise ImportError


from scipy.optimize import (
minimize_scalar,
# minimize,
Expand Down
36 changes: 14 additions & 22 deletions mathics/doc/common_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

from mathics import builtin
from mathics import settings
from mathics.builtin import get_module_doc
from mathics.builtin import get_module_doc, check_requires_list
from mathics.core.evaluation import Message, Print
from mathics.doc.utils import slugify

Expand Down Expand Up @@ -665,6 +665,8 @@ def get_tests(self):
# iterated below. Probably some other code is faulty and
# when fixed the below loop and collection into doctest_list[]
# will be removed.
if not docsubsection.installed:
continue
doctest_list = []
index = 1
for doctests in docsubsection.items:
Expand Down Expand Up @@ -906,13 +908,8 @@ def add_section(
object to the chapter, a DocChapter object.
"section_object" is either a Python module or a Class object instance.
"""
installed = True
for package in getattr(section_object, "requires", []):
try:
importlib.import_module(package)
except ImportError:
installed = False
break
installed = check_requires_list(getattr(section_object, "requires", []))

# FIXME add an additional mechanism in the module
# to allow a docstring and indicate it is not to go in the
# user manual
Expand Down Expand Up @@ -949,17 +946,12 @@ def add_subsection(
operator=None,
in_guide=False,
):
installed = True
for package in getattr(instance, "requires", []):
try:
importlib.import_module(package)
except ImportError:
installed = False
break
installed = check_requires_list(getattr(instance, "requires", []))

# FIXME add an additional mechanism in the module
# to allow a docstring and indicate it is not to go in the
# user manual

if not instance.__doc__:
return
subsection = DocSubsection(
Expand Down Expand Up @@ -1058,6 +1050,8 @@ def __init__(self, module=None):
and var.__module__[: len(self.pymathicsmodule.__name__)]
== self.pymathicsmodule.__name__
): # nopep8
if not check_requires_list(var):
continue
instance = var(expression=False)
if isinstance(instance, Builtin):
self.symbols[instance.get_name()] = instance
Expand Down Expand Up @@ -1105,13 +1099,7 @@ def __init__(self, module=None):
chapter = DocChapter(builtin_part, title, XMLDoc(text))
for name in self.symbols:
instance = self.symbols[name]
installed = True
for package in getattr(instance, "requires", []):
try:
importlib.import_module(package)
except ImportError:
installed = False
break
installed = check_requires_list(getattr(instance, "requires", []))
section = DocSection(
chapter,
strip_system_prefix(name),
Expand Down Expand Up @@ -1329,8 +1317,12 @@ def get_tests(self):
# A guide section's subsection are Sections without the Guide.
# it is *their* subsections where we generally find tests.
for section in self.subsections:
if not section.installed:
continue
for subsection in section.subsections:
# FIXME we are omitting the section title here...
if not subsection.installed:
continue
for doctests in subsection.items:
yield doctests.get_tests()

Expand Down
Loading