diff --git a/.github/workflows/consistency-checks.yml b/.github/workflows/consistency-checks.yml index 10d27b12d..9719ae42b 100644 --- a/.github/workflows/consistency-checks.yml +++ b/.github/workflows/consistency-checks.yml @@ -1,4 +1,4 @@ -name: Mathics (Consistency Checks) +name: Mathics3 (Consistency Checks) on: push: diff --git a/.github/workflows/isort-and-black-checks.yml b/.github/workflows/isort-and-black-checks.yml index 3c6c41b38..fbfce12c4 100644 --- a/.github/workflows/isort-and-black-checks.yml +++ b/.github/workflows/isort-and-black-checks.yml @@ -18,15 +18,15 @@ jobs: run: pip install 'click==8.0.4' 'black==22.3.0' 'isort==5.10.1' - name: Run isort --check . run: isort --check . - - name: Run isort --check . + - name: Run black --check . run: black --check . - - name: If needed, commit black changes to the pull request - if: failure() - run: | - black . - git config --global user.name 'autoblack' - git config --global user.email 'rocky@users.noreply.github.com' - git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY - git checkout $GITHUB_HEAD_REF - git commit -am "fixup: Format Python code with Black" - git push + # - name: If needed, commit black changes to the pull request + # if: failure() + # run: | + # black . + # git config --global user.name 'autoblack' + # git config --global user.email 'rocky@users.noreply.github.com' + # git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY + # git checkout $GITHUB_HEAD_REF + # git commit -am "fixup: Format Python code with Black" + # git push diff --git a/.github/workflows/osx.yml b/.github/workflows/osx.yml index adf475d12..81bc64aa2 100644 --- a/.github/workflows/osx.yml +++ b/.github/workflows/osx.yml @@ -1,10 +1,10 @@ -name: Mathics (OSX) +name: Mathics3 (OSX) on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: '**' jobs: build: @@ -15,23 +15,24 @@ jobs: strategy: matrix: os: [macOS] - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '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 + - name: Install OS dependencies run: | - brew install llvm@11 + brew install llvm@11 tesseract python -m pip install --upgrade pip LLVM_CONFIG=/usr/local/Cellar/llvm@11/11.1.0/bin/llvm-config pip install llvmlite - # We can comment out 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 + - name: Install Mathics3 with full Python dependencies run: | + # We can comment out after next Mathics-Scanner release + # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install Mathics-Scanner make develop-full - - name: Test Mathics + - name: Test Mathics3 run: | make -j3 check diff --git a/.github/workflows/ubuntu-cython.yml b/.github/workflows/ubuntu-cython.yml index 8c6c33654..c98d9d57d 100644 --- a/.github/workflows/ubuntu-cython.yml +++ b/.github/workflows/ubuntu-cython.yml @@ -1,4 +1,4 @@ -name: Mathics (ubuntu full with Cython) +name: Mathics3 (ubuntu full with Cython) on: push: @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.9] + python-version: ['3.10'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} @@ -22,10 +22,11 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev + sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install Mathics-Scanner - name: Install Mathics with full dependencies run: | make develop-full-cython diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index dc14666a6..894f97c79 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -1,31 +1,32 @@ -name: Mathics (ubuntu) +name: Mathics3 (ubuntu) on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: '**' jobs: build: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install OS dependencies + run: | + sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr + - name: Install Mathics3 with full dependencies run: | - sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev python -m pip install --upgrade pip # We can comment out 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: | + # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install Mathics-Scanner make develop-full - name: Test Mathics run: | diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 786cdc8b9..8bfe1b9bd 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,4 +1,4 @@ -name: Mathics (Windows) +name: Mathics3 (Windows) on: push: @@ -12,26 +12,30 @@ jobs: strategy: matrix: os: [windows] - python-version: [3.7, 3.8] + python-version: ['3.8', '3.9'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install OS dependencies run: | python -m pip install --upgrade pip python -m pip install wheel - choco install llvm + # use --force because llvm may already exist, but it also may not exist. + # so we will be safe here. Another possibility would be check and install + # conditionally. + choco install --force llvm + choco install tesseract set LLVM_DIR="C:\Program Files\LLVM" + - name: Install Mathics3 with Python dependencies + run: | # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + # python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install Mathics-Scanner make develop-full - - name: Install Mathics - run: | - python setup.py install - - name: Test Mathics + - name: Test Mathics3 # Limit pip install to a basic install *without* full dependencies. # Here is why: # * Windows is the slowest CI build, this speeds up testing by about @@ -40,6 +44,10 @@ jobs: # we needs some CI that tests running when packages aren't available # So "dev" only below, not "dev,full". run: | + pip install pyocr # from full pip install -e .[dev] set PYTEST_WORKERS="-n3" - make check + # Until we can't figure out what's up with TextRecognize: + make pytest gstest + make doctest o="--exclude TextRecognize" + # make check diff --git a/CHANGES.rst b/CHANGES.rst index 934fd0eb8..3d259dbf0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,8 +3,74 @@ CHANGES ======= -5.0.3dev0 ---------- + +New Builtins +++++++++++++ + + +* `Elements` + + +Compatibility +------------- + +* ``*Plot`` does not show messages during the evaluation. + + + +Internals +--- + +* ``eval_abs`` and ``eval_sign`` extracted from ``Abs`` and ``Sign`` and added to ``mathics.eval.arithmetic``. + + +Bugs +---- + +* Improved support for ``DirectedInfinity`` and ``Indeterminate``. + + +6.0.1 +----- + +Release to get Pillow 9.2 dependency added for Python 3.7+ + +Some Pattern-matching code gone over to add type annotations and to start +documenting its behavior and characteristics. Function +attributes are now examined and stored at the time of Pattern-object creation +rather than at evaluation time. This better matches WMA behavior which pulls +out attribute this even earlier than this. These changes speed up +doctest running time by about 7% under Pyston. + +Combinatorica version upgraded from 0.9 (circa 1992) to 0.91 (circa 1995) which closer matches the published book. + +Random builtin documentation gone over to conform to current documentation style. + +6.0.0 +----- + +A fair bit of code refactoring has gone on so that we might be able to +scale the code, get it to be more performant, and more in line with +other interpreters. There is Greater use of Symbols as opposed to strings. + +The builtin Functions have been organized into grouping akin to what is found in WMA. +This is not just for documentation purposes, but it better modularizes the code and keep +the modules smaller while suggesting where functions below as we scale. + +Image Routines have been gone over and fixed. Basically we use Pillow +imaging routines and as opposed to home-grown image code. + +A number of Built-in functions that were implemented were not accessible for various reasons. + +Mathics3 Modules are better integrated into the documentation. +Existing Mathics3 modules ``pymathics.graph`` and ``pymathics.natlang`` have +had a major overhaul, although more is needed. And will continue after the 6.0.0 release + +We have gradually been rolling in more Python type annotations and +current Python practices such as using ``isort``, ``black`` and ``flake8``. + +Evaluation methods of built-in functions start ``eval_`` not ``apply_``. + API +++ @@ -34,6 +100,9 @@ New Builtins #. ``Kurtosis`` #. ``ListLogPlot`` #. ``LogPlot`` +#. ``$MaxMachineNumber`` +#. ``$MinMachineNumber`` +#. ``NumberLinePlot`` #. ``PauliMatrix`` #. ``Remove`` #. ``SetOptions`` @@ -45,36 +114,64 @@ New Builtins Documentation +++++++++++++ -#. "Functional Programming" section split out. -#. "Exponential Functional" split out from "Trigonometry Functions" -#. A new section on "Accuracy and Precision" was included in the manual. -#. "Forms of Input and Output" is its own section #. All Builtins have links to WMA pages. -#. More url links to Wiki pages added; more internal cross links added. -#. Image has been split off from Graphics and Drawing. There are now subsections for Image +#. "Accuracy and Precision" section added to the Tutorial portion. +#. "Attribute Definitions" section reinstated. +#. "Expression Structure" split out as a guide section (was "Structure of Expressions"). +#. "Exponential Functional" split out from "Trigonometry Functions" +#. "Functional Programming" section split out. +#. "Image Manipulation" has been split off from Graphics and Drawing and turned into a guide section. +#. Image examples now appear in the LaTeX and therfore the PDF doc +#. "Logic and Boolean Algebra" section reinstated. +#. "Forms of Input and Output" is its own guide section. +#. More URL links to Wiki pages added; more internal cross links added. +#. "Units and Quantities" section reinstated. +#. The Mathics3 Modules are now included in LaTeX and therefore the PDF doc. Internals +++++++++ #. ``boxes_to_`` methods are now optional for ``BoxElement`` subclasses. Most of the code is now moved to the ``mathics.format`` submodule, and implemented in a more scalable way. -#. ``mathics.builtin.inout`` was splitted in several modules (``inout``, ``messages``, ``layout``, ``makeboxes``) in order to improve the documentation. #. ``from_mpmath`` conversion supports a new parameter ``acc`` to set the accuracy of the number. -#. Operator name to unicode or ASCII comes from Mathics scanner character tables. -#. Builtin instance methods that start ``apply`` are considered rule matching and function application; the use of the name ``apply``is deprecated, when ``eval`` is intended. +#. ``mathics.builtin.inout`` was split in several modules (``inout``, ``messages``, ``layout``, ``makeboxes``) in order to improve the documentation. +#. ``mathics.eval`` was create to have code that might be put in an instruction interpreter. The opcodes-like functions start ``eval_``, other functions are helper functions for those. +#. Operator name to Unicode or ASCII comes from Mathics scanner character tables. +#. Builtin instance methods that start ``eval`` are considered rule matching and function application; the use of the name ``apply``is deprecated, when ``eval`` is intended. #. Modularize and improve the way in which ``Builtin`` classes are selected to have an associated ``Definition``. #. ``_SetOperator.assign_elementary`` was renamed as ``_SetOperator.assign``. All the special cases are not handled by the ``_SetOperator.special_cases`` dict. #. ``isort`` run over all Python files. More type annotations and docstrings on functions added. #. caching on immutable atoms like, ``String``, ``Integer``, ``Real``, etc. was improved; the ``__hash__()`` function was sped up. There is a small speedup overall from this at the expense of increased memory. - +#. more type annotations added to functions, especially builtin functions +#. Numerical constants used along the code was renamed using caps, according to the Python's convention. Bugs ++++ # ``0`` with a given precision (like in ```0`3```) is now parsed as ``0``, an integer number. +# Reading certain GIFs now work again +#. ``Random[]`` works now. #. ``RandomSample`` with one list argument now returns a random ordering of the list items. Previously it would return just one item. #. Origin placement corrected on ``ListPlot`` and ``LinePlot``. #. Fix long-standing bugs in Image handling +#. Some scikit image routines line ``EdgeDetect`` were getting omitted due to overly stringent PyPI requirements +#. Units and Quantities were sometimes failing. Also they were omitted from documentation. +#. Better handling of ``Infinite`` quantities. +#. Improved ``Precision`` and ``Accuracy``compatibility with WMA. In particular, ``Precision[0.]`` and ``Accuracy[0.]`` +#. Accuracy in numbers using the notation ``` n.nnn``acc ``` now is properly handled. +#. numeric precision in mpmath was not reset after operations that changed these. This cause huges slowdowns after an operation that set the mpmath precison high. This was the source of several-minute slowdowns in testing. +#. GIF87a (```MadTeaParty.gif`` or ExampleData) image loading fixed +#. Replace non-free Leena image with a a freely distributable image. Issue #728 + + +PyPI Package requirements ++++++++++++++++++++++++++ +Mathics3 aims at a more richer set of functionality. + +Therefore NumPy and Pillow (9.10 or later) are required Python +packages where they had been optional before. In truth, probably +running Mathics without one or both probably did not work well if it +worked at all; we had not been testing setups that did not have NumPy. Enhancements ++++++++++++ @@ -83,12 +180,22 @@ Enhancements #. Better handling of comparisons with finite precision numbers. #. Improved implementation for ``Precision``. #. Infix operators, like ``->`` render with their Unicode symbol when ``$CharacterEncoding`` is not "ASCII". +#. ``Grid`` compatibility with WMA was improved. Now it supports non-uniform list of lists and lists with general elements. +#. Support for BigEndian Big TIFF + + 5.0.2 ----- Get in `requirements-cython.txt`` into tarball. Issue #483 +New Symbols ++++++++++++ + +#. ``Undefined`` + + 5.0.1 ----- @@ -110,7 +217,7 @@ New Builtin Documentation +++++++++++++ -Hyperbolic functions were split off form trigonometry and exponential functions. More url links were added. +Hyperbolic functions were split off form trigonometry and exponential functions. More URL links were added. Bugs ++++ @@ -1006,8 +1113,8 @@ Backward incompatibilities ----- -1.0 --- +1.0 (October 2016) +------------------ New features ++++++++++++ @@ -1162,15 +1269,15 @@ Performance improvements ----- -0.9 ---- +0.9 (March 2016) +---------------- New features ++++++++++++ #. Improve syntax error messages #329 #. ``SVD``, ``LeastSquares``, ``PseudoInverse`` #258, #321 -#. Python 3 support #317 +#. Python 2.7, 3.2-3.5 via six support #317 #. Improvements to ``Riffle`` #313 #. Tweaks to ``PolarPlot`` #305 #. ``StringTake`` #285 @@ -1206,8 +1313,8 @@ Bug fixes ----------- -0.8 ---- +0.8 (late May 2015) +------------------- New features +++++++++++++ @@ -1230,8 +1337,8 @@ Bug fixes ----------- -0.7 ---- +0.7 (Dec 2014) +-------------- New features ++++++++++++ @@ -1263,8 +1370,8 @@ Bugs fixed -------------- -0.6 ---- +0.6 (late October 2013) +------------------------ New features ++++++++++++ @@ -1299,8 +1406,8 @@ Bugs fixed ------- -0.5 ---- +0.5 (August 2012) +----------------- #. Compatibility with Sage 5, SymPy 0.7, Cython 0.15, Django 1.2 #. 3D graphics and plots using WebGL in the browser and Asymptote in TeX output diff --git a/FUTURE.rst b/FUTURE.rst index 0baef1fc9..e37758f22 100644 --- a/FUTURE.rst +++ b/FUTURE.rst @@ -2,6 +2,120 @@ .. contents:: +The following 2023 road map that appears the 6.0.0 hasn't gone through enough discussion. This provisional. +Check the github repository for updates. + + +2023 Roadmap +============ + + +When the release settles, "Forms, Boxing, And "Formatting" is the next +large refactor slated. Having this will allow us to supporting Jupyter or other front +ends. And it is something that is most visibly wrong in Mathics3 output. + +See ``PAST.rst`` for how the 2023 Roadmap compares to the 2022 Roadmap. + +Forms, Boxing and Formatting +---------------------------- + +This remains the biggest holdover item from 2022, and seems easily doable. +It hinders interaction with Jupyter or other front ends. + +Right now "Form" (a high-level specification of how to format) and +"format" (a low level specification of how output is encoded) are sometimes muddied. + +For example, TeXForm may be a "Form", but output encoded for AMS-LaTeX is done by a *formatter*. +So AMS-LaTeX rendering and other kinds of rendering should be split into its own rendering for formatter module. +Currently we have asymptote, and svg "format" modules. + +Back to high-level again, Boxing is something that can be written in Mathics3, and doing this at +least initially ensures that we have design that fits more naturally +into the Wolfram Language philosophy. + + +Performance +----------- + +While this is probably more of an overall concern, for now, big refactoring needed here, such as +going over pattern matching, will get done after Forms, Boxing and Formatting . + +Forms, Boxing and Formatting will however contain one improvement that +should speed up our performance: separating M-Expression evaluation from +Box "evaluations). + +We expect there will be other little opportunities here and there as we have seen in the past. + + +More Custom kinds of (compound) Expressions ++++++++++++++++++++++++++++++++++++++++++++ + +We scratched the surface here with ListExpression. Associations and Python/Sympy/numpy literals can be customized with an aim towards reducing conversions from and to M-expressions. +A number of compound expressions, especially those which involve literals are more efficiently represented in some other way. For example, +representing a Mathics3 Association as a Python ordered dictionary, a Mathics3 List as a Python list or tuple, or as a numpy array. + + +Further Code Reorganization in Core and Eval +-------------------------------------------- + +Core object like ``BaseElement`` and possibly ``Symbol``, (and +probably others) are too "fat": they have too many custom methods that +are not applicable for most of the subclasses support. It is likely +another pass will be made over this. + +We have started moving "eval" code out of the "eval" methods and into its own module. + +Mathics3 Module Enhancement +--------------------------- + +While we have put in quite a bit of effort to get these to be 6.0.0 compliant. There is still more work to do, and there are numerous bugs there. +Refactoring code to generate Graphs in ``pymathics.graph`` might happen. Porting the ``pymathics.graph`` code to use NetworkX 3.0 would be nice; +``pymathics.natlang`` could also use a look over in terms of the libraries we are using. + +Python upgrades +--------------- + +After Mathics3 Version 6.0.0, Python 3.6 will be dropped and possibly 3.7. Changes are needed to support 3.11 so we will be focusing on 3.8 to 3.11. + +We have gradually been using a more modern Python programming style +and idioms: more type annotation, use of ``isort`` (order Python +imports), ``black`` (code formatting), and ``flake8`` (Python lint +checking). + + +Deferred +-------- + +As mentioned before, pattern-matching revision is for later. `This +discussion +`_ is a +placeholder for this discussion. + +Overhauling the documentation to use something better supported and +more mainstream like sphinx is deferred. This would really be nice to +have, but it will require a bit of effort and detracts from all of the other work that is needed. + +We will probably try this out in a limited basis in one of the Mathics3 modules. + +Speaking of Mathics3 Modules, there are probably various scoping/context issues that Mathics3 modules make more apparent. +This will is deferred for now. + +Way down the line, is converting to a more sequence-based interpreter which is needed for JIT'ing and better Compilation support. + +Likewise, speeding up startup time via saving and loading an image is something that is more of a long-term goal. + +Things in this section can change, depending on the help we can get. + + +Miscellaneous +------------- + +No doubt there will be numerous bug fixes, and builtin-function additions especially now that we have a better framework to support this kind of growth. +Some of the smaller deferred issues refactorings may get addressed. + +As always, where and how fast things grow here depends on help available. + + 2022 Roadmap ============= @@ -57,11 +171,11 @@ The current home-grown documentation should be replaced with Sphynx and autodoc. Compilation ----------- -Complation is a rather unsophisticated process by trying to speed up Python code using llvmlite. The gains here will always be small compared the kinds of gains a compiler can get. However in order to even be able to contemplate writing a compiler (let alone say a JIT compiler), the code base needs to be made to work more like a traditional interpreter. Some work will be needed just to be able or create a sequence of instructions to run. +Compilation is a rather unsophisticated process by trying to speed up Python code using llvmlite. The gains here will always be small compared the kinds of gains a compiler can get. However in order to even be able to contemplate writing a compiler (let alone say a JIT compiler), the code base needs to be made to work more like a traditional interpreter. Some work will be needed just to be able or create a sequence of instructions to run. -Right now the interpreter is strictly a tree interperter. +Right now the interpreter is strictly a tree interpreter. -Simpiler Things +Simpler Things --------------- There have been a number of things that have been deferred: diff --git a/Makefile b/Makefile index 9b2e85a0b..3b3c04b76 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ PIP ?= pip3 BASH ?= bash RM ?= rm +# Variable indicating Mathics3 Modules you have available on your system, in latex2doc option format +# MATHICS3_MODULE_OPTION ?= +MATHICS3_MODULE_OPTION ?= --load-module pymathics.graph,pymathics.natlang + .PHONY: \ all \ build \ @@ -26,7 +30,7 @@ RM ?= rm dist \ doc \ doctest \ - doc-data \ + doctest-data \ djangotest \ gstest \ latexdoc \ @@ -120,9 +124,10 @@ gstest: (cd examples/symbolic_logic/gries_schneider && $(PYTHON) test_gs.py) -#: Create data that is used to in Django docs and to build LaTeX PDF -doc-data: mathics/builtin/*.py mathics/doc/documentation/*.mdoc mathics/doc/documentation/images/* - MATHICS_CHARACTER_ENCODING="ASCII" $(PYTHON) mathics/docpipeline.py --output --keep-going +#: Create doctest test data and test results that is used to build LaTeX PDF +# For LaTeX docs we assume Unicode +doctest-data: mathics/builtin/*.py mathics/doc/documentation/*.mdoc mathics/doc/documentation/images/* + MATHICS_CHARACTER_ENCODING="UTF-8" $(PYTHON) mathics/docpipeline.py --output --keep-going $(MATHICS3_MODULE_OPTION) #: Run tests that appear in docstring in the code. doctest: diff --git a/PAST.rst b/PAST.rst new file mode 100644 index 000000000..20d41b9c7 --- /dev/null +++ b/PAST.rst @@ -0,0 +1,55 @@ +While ``FUTURE.rst`` gives our current roadmap, this file ``PAST.rst`` +looks the other way for what we have accomplished when compared to what _was_ planned in ``FUTURE.rst`` + +While this is also listed in ``CHANGES.rst``, here we extract that to +make it easier to see the bigger picture without the details that are +in ``CHANGES.rst``. + +Progress from 2022 +================== + +A fair bit of code refactoring has gone on so that we might be able to +scale the code, get it to be more performant, and more in line with +other interpreters. There is Greater use of Symbols as opposed to strings. + +The buitin Functions have been organized into grouping akind to what is found in WMA. +This is not just for documentation purposes, but it better modularizes the code and keep +the modules smaller while suggesting where functions below as we scale. + +Image Routines have been gone over. + +A number of Built-in functions that were implemented were not accessible for various reasons. + +Mathics3 Modules are better integrated into the documentation. +Existing Mathics3 modules ``pymathics.graph`` and ``pymathics.natlang`` have +had a major overhaul, although more is needed. And will continue after th 6.0.0 release + +We have gradually been rolling in more Python type annotations and +current Python practices such as using ``isort``, ``black`` and ``flake8``. + + +Boxing and Formatting +--------------------- + +While some work on formatting is done has been made and the change in API reflects a little of this. +However a lot more work needs to be done. + +Excecution Performance +---------------------- + +This has improved a slight bit, but not because it has been a focus, but +rather because in going over the code organization, we are doing this +less dumb, e.g. using Symbols more where symbols are intended. Or +fixing bugs like resetting mpmath numeric precision on operations that +need to chnage it temporarily. + +Simpler Things +-------------- + +A number of items here remain, but should not be thought as independent items, but instead part of +"Forms, Boxing and Formatting". + +"Making StandardOutput of polynomials match WMA" is really are Forms, Boxing and Formatting issue; +"Working on Jupyter integrations" is also very dependant this. + +So the next major refactor will be on Forms, Boxing and Formatting. diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index ef4f558e4..291d744a7 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -40,7 +40,9 @@ System`$Machine System`$MachineEpsilon System`$MachineName System`$MachinePrecision +System`$MaxMachineNumber System`$MaxPrecision +System`$MinMachineNumber System`$MinPrecision System`$ModuleNumber System`$OperatingSystem @@ -211,7 +213,6 @@ System`Coefficient System`CoefficientArrays System`CoefficientList System`Collect -System`ColorCombine System`ColorConvert System`ColorData System`ColorDataFunction @@ -245,8 +246,8 @@ System`Continue System`ContinuedFraction System`Convert`B64Dump`B64Decode System`Convert`B64Dump`B64Encode -System`ConvertersDump`$extensionMappings -System`ConvertersDump`$formatMappings +System`ConvertersDump`$ExtensionMappings +System`ConvertersDump`$FormatMappings System`CoprimeQ System`CopyDirectory System`CopyFile @@ -335,6 +336,7 @@ System`EditDistance System`Eigensystem System`Eigenvalues System`Eigenvectors +System`Element System`ElementData System`EllipticE System`EllipticF @@ -613,7 +615,6 @@ System`MachinePrecision System`Magenta System`MakeBoxes System`ManhattanDistance -System`Manipulate System`MantissaExponent System`Map System`MapAt @@ -687,6 +688,7 @@ System`Null System`NullSpace System`Number System`NumberForm +System`NumberLinePlot System`NumberQ System`NumberString System`Numerator @@ -738,7 +740,6 @@ System`Pi System`Pick System`PieChart System`Piecewise -System`PillowImageFilter System`Pink System`PixelValue System`PixelValuePositions @@ -768,6 +769,7 @@ System`PowerMod System`PreDecrement System`PreIncrement System`Precedence +System`PrecedenceForm System`Precision System`Prefix System`Prepend @@ -780,7 +782,6 @@ System`Print System`PrintTrace System`Private`$ContextPathStack System`Private`$ContextStack -System`Private`ManipulateParameter System`Product System`ProductLog System`Projection @@ -829,6 +830,7 @@ System`Reap System`Record System`Rectangle System`RectangleBox +System`Rectangular System`Red System`RegularExpression System`RegularPolygon diff --git a/admin-tools/make-dist.sh b/admin-tools/make-dist.sh index 5d438b7c7..015477da8 100755 --- a/admin-tools/make-dist.sh +++ b/admin-tools/make-dist.sh @@ -16,7 +16,7 @@ fi cd .. source mathics/version.py -cp -v ${HOME}/.local/var/mathics/doc_tex_data.pcl mathics/data/ +cp -v ${HOME}/.local/var/mathics/doctest_latex_data.pcl mathics/data/ echo $__version__ diff --git a/admin-tools/pyenv-versions b/admin-tools/pyenv-versions index f0328ac4b..9b7641862 100644 --- a/admin-tools/pyenv-versions +++ b/admin-tools/pyenv-versions @@ -5,4 +5,4 @@ if [[ $0 == ${BASH_SOURCE[0]} ]] ; then echo "This script should be *sourced* rather than run directly through bash" exit 1 fi -export PYVERSIONS='3.6.15 3.7.13 pyston-2.3.4 pypy3.9-7.3.9 3.8.13 3.9.13 3.10.5' +export PYVERSIONS='3.6.15 3.7.16 pyston-2.3.5 pypy3.9-7.3.11 3.8.16 3.9.16 3.10.10' diff --git a/mathics/__init__.py b/mathics/__init__.py index 8c45fe456..ff26e6b2e 100644 --- a/mathics/__init__.py +++ b/mathics/__init__.py @@ -32,7 +32,7 @@ "networkx", "nltk", "psutil", - "scikit-image", + "skimage", "scipy", "wordcloud", ) diff --git a/mathics/algorithm/__init__.py b/mathics/algorithm/__init__.py index 56fafa58b..fccabe456 100644 --- a/mathics/algorithm/__init__.py +++ b/mathics/algorithm/__init__.py @@ -1,2 +1,5 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +Note: functions here will eventually get moved to mathics.eval +""" diff --git a/mathics/algorithm/integrators.py b/mathics/algorithm/integrators.py index cd0c777a9..e10ef8999 100644 --- a/mathics/algorithm/integrators.py +++ b/mathics/algorithm/integrators.py @@ -5,7 +5,7 @@ from mathics.core.atoms import Integer, Integer0, Number from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.number import machine_epsilon +from mathics.core.number import MACHINE_EPSILON from mathics.core.symbols import Symbol, SymbolPlus, SymbolSequence, SymbolTimes from mathics.core.systemsymbols import ( SymbolBlank, @@ -118,12 +118,12 @@ def ensure_evaluation(f, x): fa, fb = ensure_evaluation(f, a), ensure_evaluation(f, b) if fa is None: - x = 10.0 * machine_epsilon if a == 0 else a * (1.0 + 10.0 * machine_epsilon) + x = 10.0 * MACHINE_EPSILON if a == 0 else a * (1.0 + 10.0 * MACHINE_EPSILON) fa = ensure_evaluation(f, x) if fa is None: raise Exception(f"Function undefined around {a}. Cannot integrate") if fb is None: - x = -10.0 * machine_epsilon if b == 0 else b * (1.0 - 10.0 * machine_epsilon) + x = -10.0 * MACHINE_EPSILON if b == 0 else b * (1.0 - 10.0 * MACHINE_EPSILON) fb = ensure_evaluation(f, x) if fb is None: raise Exception(f"Function undefined around {b}. Cannot integrate") @@ -162,7 +162,7 @@ def ff(*z): return val -def apply_D_to_Integral(func, domain, var, evaluation, options, head): +def eval_D_to_Integral(func, domain, var, evaluation, options, head): """Implements D[%(name)s[func_, domain__, OptionsPattern[%(name)s]], var_Symbol]""" if head is SymbolNIntegrate: options = tuple( diff --git a/mathics/autoload/rules/Bessel.m b/mathics/autoload/rules/Bessel.m new file mode 100644 index 000000000..00c65469d --- /dev/null +++ b/mathics/autoload/rules/Bessel.m @@ -0,0 +1,80 @@ +(*Extended rules for handling expressions with Bessel functions*) + +Begin["internals`bessel`"] + +Unprotect[HankelH1]; +(*HankelH1[x_Integer?NegativeQ, z_]:=-HankelH1[-x, z];*) +(*Limit cases*) +HankelH1[nu_, 0] := DirectedInfinity[]; +Protect[HankelH1]; + + +Unprotect[HankelH2]; +(*HankelH2[x_Integer?NegativeQ, z_]:=-HankelH2[-x, z];*) +(*Limit cases*) +HankelH2[nu_,0] := DirectedInfinity[]; +Protect[HankelH2]; + + +Unprotect[BesselI] +(*Rayleigh's formulas for half-integer indices*) +BesselI[nu_/;(nu>0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k= nu-1/2},f=Sinh[u]/u;While[k>0, k=k-1;f = (-D[f, u]/u)]; (Sqrt[2/Pi z] * ((-u)^(nu-1/2)*f))/.u->z]; +BesselI[nu_/;(nu<0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k=-nu-1/2},f=Cosh[u]/u;While[k>0, k=k-1;f = (-D[f, u]/u)]; (Sqrt[2/Pi z] * ((-u)^(-nu-1/2)*f))/.u->z]; +(*Limit cases*) +BesselI[0, 0] := 1; +BesselI[nu_Integer,0]:=0; +BesselI[nu_Rational, 0] := If[nu>0, 0, DirectedInfinity[]]; +BesselI[nu_Real, 0] := If[nu>0, 0, DirectedInfinity[]]; +BesselI[nu_, DirectedInfinity[z___]] := 0; +Protect[BesselI] + +Unprotect[BesselK] +(*Rayleigh's formulas for half-integer indices*) +BesselK[nu_/;(nu>0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k= nu-1/2},f=Exp[-u]/u;While[k>0, k=k-1;f = (D[f, u]/u)]; (Sqrt[Pi/2 z] * ((-u)^(nu-1/2)*f))/.u->z]; +BesselK[nu_/;(nu<0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k=-nu-1/2},f=Exp[-u]/u;While[k>0, k=k-1;f = (D[f, u]/u)]; (Sqrt[Pi/2 z] * ((-u)^(-nu-1/2)*f))/.u->z]; +(*Limit cases*) +BesselK[0, 0] = DirectedInfinity[-1]; +BesselK[nu_?NumericQ, 0] = DirectedInfinity[]; +Protect[BesselK] + + +Unprotect[BesselJ] +(*Rayleigh's formulas for half-integer indices*) +BesselJ[nu_/;(nu>0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k= nu-1/2},f=Sin[u]/u;While[k>0, k=k-1;f = (D[f, u]/u)]; (Sqrt[2/Pi z] * ((-u)^(nu-1/2)*f))/.u->z]; +BesselJ[nu_/;(nu<0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k=-nu-1/2},f=Cos[u]/u;While[k>0, k=k-1;f = (-D[f, u]/u)]; (Sqrt[2/Pi z] * ((-u)^(-nu-1/2)*f))/.u->z]; +(*Limit cases*) +BesselJ[0, 0] := 1; +BesselJ[nu_Integer,0]:=0; +BesselJ[nu_Rational, 0] := If[nu>0, 0, DirectedInfinity[]]; +BesselJ[nu_Real, 0] := If[nu>0, 0, DirectedInfinity[]]; +BesselJ[nu_, DirectedInfinity[z___]] := 0; +Protect[BesselJ] + + +Unprotect[BesselY] +(*Rayleigh's formulas for half-integer indices*) +BesselY[nu_/;(nu>0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k= nu-1/2},f=Cos[u]/u;While[k>0, k=k-1;f = (D[f, u]/u)]; (-Sqrt[2/Pi z] * ((-u)^(nu-1/2)*f))/.u->z]; +BesselY[nu_/;(nu<0 && IntegerQ[2*nu]), z_]:=Module[{u,f,k=-nu-1/2},f=Sin[u]/u;While[k>0, k=k-1;f = (D[f, u]/u)]; (Sqrt[2/Pi z] * ((u)^(-nu-1/2)*f))/.u->z]; +(*Limit cases*) +BesselY[0, 0] = DirectedInfinity[-1]; +BesselY[nu_, 0] = DirectedInfinity[]; +Protect[BesselY] + + + + + +Unprotect[Integrate]; +(* See https://dlmf.nist.gov/10.9 *) +Integrate[Cos[z_Integer Sin[Theta_]], {Theta_, 0, Pi}]:= Pi BesselJ[0, Abs[z]]; +Integrate[Cos[z_Rational Sin[Theta_]], {Theta_, 0, Pi}]:= Pi BesselJ[0, Abs[z]]; +Integrate[Cos[z_Real Sin[Theta_]], {Theta_, 0, Pi}]:= Pi BesselJ[0, Abs[z]]; + + +(* This rule needs to implement Elements*) +Integrate[Cos[z_ Sin[Theta_]], {Theta_, 0, Pi}]:= ConditionalExpression[Pi BesselJ[0, Abs[z]], Element[z, Reals]]; + +Protect[Integrate]; + +(*TODO: extend me with series expansions, integrals, etc*) +End[] diff --git a/mathics/autoload/rules/Element.m b/mathics/autoload/rules/Element.m new file mode 100644 index 000000000..f71d2dcd0 --- /dev/null +++ b/mathics/autoload/rules/Element.m @@ -0,0 +1,114 @@ +(*Rules for Elements*) + + +System`Integers::usage="Represents the set of the Integers numbers"; +System`Primes::usage="Represents the set of the prime numbers"; +System`Rationals::usage="Represents the set of the Rational numbers"; +System`Reals::usage="Represents the field of the Real numbers"; +System`Complexes::usage="Represents the field of the Complex numbers"; +System`Algebraics::usage="Represents the set of the algebraic numbers"; +System`Booleans::usage="Represents the set of boolean values"; + +Begin["internals`elements`"] +Unprotect[Element] + + +Element[_Integer, Reals]:=True; + +(*Booleans*) +Element[True|False, Booleans]:=True; +Element[E|I|EulerGamma|Khinchin|MachinePrecision|Pi, Booleans]:=False; +Element[_Integer|_Rational|_Real|_Complex, Booleans]:=False; + + + +(*Integers*) +Element[True|False|E|I|EulerGamma|Khinchin|MachinePrecision|Pi, Integers]:=False; +Element[_Integer, Integers]:=True; +Element[_Rational|_Complex, Integers]:=False; +Element[x_Real/;(FractionalPart[x]!=0.), Integers]:=False; + + + + +(*Rationals*) +Element[True|False|E|I|EulerGamma|Khinchin|MachinePrecision|Pi, Rationals]:=False; +Element[_Integer|_Rational, Rationals]:=True; +Element[_Complex, Rationals]:=False; + + + + + + +(*Reals*) +Element[True|False|I, Reals]:=False; +Element[E|EulerGamma|Khinchin|MachinePrecision|Pi, Reals]:=True; +Element[_Rational, Reals]:=True; +Element[_Real, Reals]:=True; +Element[_Complex, Reals]:=False; + + + +(*Complex*) +Element[True|False, Complexes]:=False; +Element[E|EulerGamma|I|Khinchin|MachinePrecision|Pi, Complexes]:=True; +Element[_Integer|_Rational|_Real|_Complex, Complexes]:=True; + + + +(*Elementary inexact functions*) +Element[f:(Sin[_]|Cos[_]|Tan[_]|Cot[_]|Sec[_]|Cosec[_]|Sinh[_]|Cosh[_]|Tanh[_]|Coth[_]|Sech[_]|Cosech[_]| + Log[_]|Exp[_]| ArcSin[_]|ArcCos[_]|ArcTan[_]|ArcCot[_]|ArcSec[_]|ArcCosec[_]| + ArcSinh[_]|ArcCosh[_]|ArcTanh[_]|ArcCoth[_]|ArcSech[_]|ArcCosech[_]), domain:Reals|Complexes]:=Element[f[[1]], domain]; + + + + + +(*Primes*) +Element[True|False|E|I|EulerGamma|Khinchin|MachinePrecision|Pi, Primes]:=False; +Element[z_Integer, Primes]:=PrimeQ[z]; +Element[_Rational|_Complex, Primes]:=False; +Element[x_Real/;(FractionalPart[x]!=0.), Primes]:=False; +(*TODO: Check this condition. Probably this need to be implemented in Python...*) +Element[x_, Primes]:=If[Element[x, Algebraics]===True, False, HoldForm[Element[x, Primes]]]; + + +(*General Algebraic*) + +Element[z:(_Plus|_Times), domain:(Integers|Rationals|Reals|Complexes|Algebraics)]:=Element[Alternatives@@z, domain]; +Element[z:(_Plus|_Times), Booleans]:=False; +Element[_Times, Primes]:=False; + + + +Element[z_Power, Algebraics]:=Element[Alternatives@@z, Algebraics]; +Element[z:(_Integer|_Rational|_Complex), Algebraics]:=True; +Element[I, Algebraics]:=True; +Element[True|False|E|EulerGamma|Khinchin|MachinePrecision|Pi, Algebraics]:=False; +Element[z_DirectedInfinity, domain:(Booleans|Integers|Rationals|Reals|Complexes)]:=False; +Element[z_Power, Integers]:= (Element[Alternatives@@z, Integers] && z[[2]]>=0); +Element[z_Power/;Element[Alternatives@@z, Integers], Integers]:= (z[[2]]>=0); + +Element[z_Power, Complexes]:= Element[z, Algebraics]; +Element[Power[b_,p_], Rationals]:=Element[b, Rationals] && Element[p, Integers] ; + + + + +Element[Sin[_]|Cos[_]|Tan[_]|Cot[_]|Sec[_]|Cosec[_]| + Sinh[_]|Cosh[_]|Tanh[_]|Coth[_]|Sech[_]|Cosech[_]| + Log[_]|Exp[_]| + ArcSin[_]|ArcCos[_]|ArcTan[_]|ArcCot[_]|ArcSec[_]|ArcCosec[_]| + ArcSinh[_]|ArcCosh[_]|ArcTanh[_]|ArcCoth[_]|ArcSech[_]|ArcCosech[_] + , Integers|Primes|Rationals|Algebraics|Booleans]:=False; + + + +Element[Power[b_Real|b_Rational|b_Integer, _Real|_Rational], Reals]:= (b>=0); +Element[Power[_Real|_Rational|_Integer, p_Integer], Reals]= True; +Element[Power[b_/;(Element[b, Reals]), p_/;Element[p, Reals]], Reals]:=(Element[p, Integers] || b>=0); + +Protect[Element] +End[] diff --git a/mathics/autoload/rules/GudermannianRules.m b/mathics/autoload/rules/GudermannianRules.m deleted file mode 100644 index 72677c0b3..000000000 --- a/mathics/autoload/rules/GudermannianRules.m +++ /dev/null @@ -1,24 +0,0 @@ -(* Adapted from symja_android_library/symja_android_library/rules/QuantileRules.m *) -(* This has been added as a Mathics Builtin in mathics.builtin.numbers.hyperbolic -Begin["System`"] - -Gudermannian::usage = "gives the Gudermannian function"; -Gudermannian[Undefined]=Undefined; -Gudermannian[0]=0; -Gudermannian[2*Pi*I]=0; -Gudermannian[6/4*Pi*I]=DirectedInfinity[-I]; -Gudermannian[Infinity]=Pi/2; -Gudermannian[-Infinity]=-Pi/2; -Gudermannian[ComplexInfinity]=Indeterminate; -Gudermannian[z_]=2 ArcTan[Tanh[z / 2]]; - *) - -(* Commented out because ":=" might not work properly... - -Gudermannian[z_] := Piecewise[{{1/2*[Pi - 4*ArcCot[E^z]], Re[z]>0||(Re[z]==0&&Im[z]>=0 )}}, 1/2 (-Pi + 4 ArcTan[E^z])]; -D[Gudermannian[f_],x_?NotListQ] := Sech[f] D[f,x]; -Derivative[1][InverseGudermannian] := Sec[#] &; -Derivative[1][Gudermannian] := Sech[#] &; - -End[] -*) diff --git a/mathics/autoload/rules/trig.m b/mathics/autoload/rules/trig.m new file mode 100644 index 000000000..1be817482 --- /dev/null +++ b/mathics/autoload/rules/trig.m @@ -0,0 +1,8 @@ +(* Additions to mathics.builtin.numbers.trig that are either wrong + or not covered by SymPy + *) + +Unprotect[ArcCos]; +ArcCos[I Infinity] := -I Infinity; +ArcCos[-I Infinity] := I Infinity; +Protect[ArcCos]; diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index 0f88784ef..76ecd6551 100755 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -36,6 +36,8 @@ mathics_to_python, ) from mathics.core.pattern import pattern_objects +from mathics.core.symbols import Symbol +from mathics.eval.makeboxes import builtins_precedence from mathics.settings import ENABLE_FILES_MODULE from mathics.version import __version__ # noqa used in loading to check consistency. @@ -60,7 +62,7 @@ def add_builtins(new_builtins): # print("XXX1", sympy_name) sympy_to_mathics[sympy_name] = builtin if isinstance(builtin, Operator): - builtins_precedence[name] = builtin.precedence + builtins_precedence[Symbol(name)] = builtin.precedence if isinstance(builtin, PatternObject): pattern_objects[name] = builtin.__class__ _builtins.update(dict(new_builtins)) @@ -141,12 +143,8 @@ def name_is_builtin_symbol(module, name: str) -> Optional[type]: if not inspect.isclass(module_object): return None - # FIXME: tests involving module_object.__module__ are fragile and - # Python implementation specific. Figure out how to do this - # via the inspect module which is not implementation specific. - # Skip those builtins defined in or imported from another module. - if module_object.__module__ != module.__name__: + if inspect.getmodule(module_object) is not module: return None # Skip objects in module mathics.builtin.base. @@ -154,7 +152,10 @@ def name_is_builtin_symbol(module, name: str) -> Optional[type]: return None # Skip those builtins that are not submodules of mathics.builtin. - if not module_object.__module__.startswith("mathics.builtin."): + if not ( + module_object.__module__.startswith("mathics.builtin.") + or module_object.__module__.startswith("pymathics.") + ): return None # If it is not a subclass of Builtin, skip it. @@ -190,6 +191,7 @@ def name_is_builtin_symbol(module, name: str) -> Optional[type]: "colors", "distance", "drawing", + "exp_structure", "fileformats", "files_io", "forms", @@ -203,6 +205,7 @@ def name_is_builtin_symbol(module, name: str) -> Optional[type]: "specialfns", "statistics", "string", + "testing_expressions", "vectors", ): import_name = f"{__name__}.{subdir}" @@ -237,8 +240,6 @@ def name_is_builtin_symbol(module, name: str) -> Optional[type]: mathics_to_sympy = {} # here we have: name -> sympy object sympy_to_mathics = {} -builtins_precedence = {} - new_builtins = _builtins_list # FIXME: some magic is going on here.. diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index b4875bae9..6fb5a8873 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -6,18 +6,12 @@ """ - -import mpmath -import sympy - from mathics.builtin.arithmetic import _MPMathFunction, create_infix from mathics.builtin.base import BinaryOperator, Builtin, PrefixOperator, SympyFunction from mathics.core.atoms import ( Complex, Integer, - Integer0, Integer1, - Integer2, Integer3, Integer310, IntegerM1, @@ -37,41 +31,38 @@ A_READ_PROTECTED, ) from mathics.core.convert.expression import to_expression -from mathics.core.convert.mpmath import from_mpmath from mathics.core.convert.sympy import from_sympy -from mathics.core.expression import ElementsProperties, Expression +from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.number import dps, min_prec from mathics.core.symbols import ( Symbol, SymbolDivide, SymbolHoldForm, SymbolNull, - SymbolPlus, SymbolPower, SymbolTimes, ) from mathics.core.systemsymbols import ( - SymbolAccuracy, SymbolBlank, SymbolComplexInfinity, - SymbolDirectedInfinity, SymbolIndeterminate, - SymbolInfinity, SymbolInfix, SymbolLeft, SymbolMinus, SymbolPattern, SymbolSequence, ) +from mathics.eval.arithmetic import eval_Plus, eval_Times from mathics.eval.nevaluator import eval_N from mathics.eval.numerify import numerify class CubeRoot(Builtin): """ - :WMA link: - https://reference.wolfram.com/language/ref/CubeRoot.html + + :Cube root: + https://en.wikipedia.org/wiki/Cube_root ( :WMA: + https://reference.wolfram.com/language/ref/CubeRoot.html)
'CubeRoot[$n$]' @@ -115,7 +106,7 @@ class CubeRoot(Builtin): ), } - summary_text = "cubed root" + summary_text = "cube root" def eval(self, n, evaluation): "CubeRoot[n_Complex]" @@ -124,18 +115,17 @@ def eval(self, n, evaluation): return Expression( SymbolPower, n, - Expression( - SymbolDivide, - Integer1, - Integer3, - elements_properties=ElementsProperties(True, True, True), - ), + Integer1 / Integer3, ) class Divide(BinaryOperator): """ - :WMA link:https://reference.wolfram.com/language/ref/Divide.html + + :Division: + https://en.wikipedia.org/wiki/Division_(mathematics) ( + :WMA link: + https://reference.wolfram.com/language/ref/Divide.html)
'Divide[$a$, $b$]' @@ -176,13 +166,20 @@ class Divide(BinaryOperator): """ - operator = "/" - precedence = 470 attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - grouping = "Left" default_formats = False + formats = { + (("InputForm", "OutputForm"), "Divide[x_, y_]"): ( + 'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]' + ), + } + + grouping = "Left" + operator = "/" + precedence = 470 + rules = { "Divide[x_, y_]": "Times[x, Power[y, -1]]", "MakeBoxes[Divide[x_, y_], f:StandardForm|TraditionalForm]": ( @@ -190,18 +187,16 @@ class Divide(BinaryOperator): ), } - formats = { - (("InputForm", "OutputForm"), "Divide[x_, y_]"): ( - 'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]' - ), - } - - summary_text = r"division" + summary_text = "divide" class Minus(PrefixOperator): """ - :WMA link:https://reference.wolfram.com/language/ref/Minus.html + + :Additive inverse: + https://en.wikipedia.org/wiki/Additive_inverse ( + :WMA: + https://reference.wolfram.com/language/ref/Minus.html)
'Minus[$expr$]' @@ -220,14 +215,8 @@ class Minus(PrefixOperator): = {-1, -2, -3, -4, -5, -6, -7, -8, -9, -10} """ - operator = "-" - precedence = 480 attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - rules = { - "Minus[x_]": "Times[-1, x]", - } - formats = { "Minus[x_]": 'Prefix[{HoldForm[x]}, "-", 480]', # don't put e.g. -2/3 in parentheses @@ -237,7 +226,14 @@ class Minus(PrefixOperator): ), } - summary_text = "arithmetic negation" + operator = "-" + precedence = 480 + + rules = { + "Minus[x_]": "Times[-1, x]", + } + + summary_text = "arithmetic negate" def eval_int(self, x: Integer, evaluation): "Minus[x_Integer]" @@ -246,7 +242,13 @@ def eval_int(self, x: Integer, evaluation): class Plus(BinaryOperator, SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Plus.html + + :Addition: + https://en.wikipedia.org/wiki/Addition ( + :SymPy: + https://docs.sympy.org/latest/modules/core.html#id48, + :WMA: + https://reference.wolfram.com/language/ref/Plus.html)
'Plus[$a$, $b$, ...]' @@ -304,8 +306,6 @@ class Plus(BinaryOperator, SympyFunction): = 30. """ - operator = "+" - precedence = 310 attributes = ( A_FLAT | A_LISTABLE @@ -321,7 +321,13 @@ class Plus(BinaryOperator, SympyFunction): None: "0", } - summary_text = "addition of numbers, lists, arrays, or symbolic expressions" + operator = "+" + precedence = 310 + + summary_text = "add" + + # FIXME Note this is deprecated in 1.11 + # Remember to up sympy doc link when this is corrected sympy_name = "Add" def format_plus(self, items, evaluation): @@ -377,96 +383,19 @@ def is_negative(value) -> bool: def eval(self, items, evaluation): "Plus[items___]" - items_tuple = numerify(items, evaluation).get_sequence() - elements = [] - last_item = last_count = None - - prec = min_prec(*items_tuple) - is_machine_precision = any(item.is_machine_precision() for item in items_tuple) - numbers = [] - - def append_last(): - if last_item is not None: - if last_count == 1: - elements.append(last_item) - else: - if last_item.has_form("Times", None): - elements.append( - Expression( - SymbolTimes, from_sympy(last_count), *last_item.elements - ) - ) - else: - elements.append( - Expression(SymbolTimes, from_sympy(last_count), last_item) - ) - - for item in items_tuple: - if isinstance(item, Number): - numbers.append(item) - else: - count = rest = None - if item.has_form("Times", None): - for element in item.elements: - if isinstance(element, Number): - count = element.to_sympy() - rest = item.get_mutable_elements() - rest.remove(element) - if len(rest) == 1: - rest = rest[0] - else: - rest.sort() - rest = Expression(SymbolTimes, *rest) - break - if count is None: - count = sympy.Integer(1) - rest = item - if last_item is not None and last_item == rest: - last_count = last_count + count - else: - append_last() - last_item = rest - last_count = count - append_last() - - if numbers: - if prec is not None: - if is_machine_precision: - numbers = [item.to_mpmath() for item in numbers] - number = mpmath.fsum(numbers) - number = from_mpmath(number) - else: - # For a sum, what is relevant is the minimum accuracy of the terms - acc = ( - Expression(SymbolAccuracy, ListExpression(items)) - .evaluate(evaluation) - .to_python() - ) - with mpmath.workprec(prec): - numbers = [item.to_mpmath() for item in numbers] - number = mpmath.fsum(numbers) - number = from_mpmath(number, acc=acc) - else: - number = from_sympy(sum(item.to_sympy() for item in numbers)) - else: - number = Integer0 - - if not number.sameQ(Integer0): - elements.insert(0, number) - - if not elements: - return Integer0 - elif len(elements) == 1: - return elements[0] - else: - elements.sort() - return Expression(SymbolPlus, *elements) + return eval_Plus(*items_tuple) class Power(BinaryOperator, _MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Power.html + + :Exponentiation: + https://en.wikipedia.org/wiki/Exponentiation ( + :SymPy: + https://docs.sympy.org/latest/modules/core.html#sympy.core.power.Pow, + :WMA: + https://reference.wolfram.com/language/ref/Power.html)
'Power[$a$, $b$]' @@ -547,22 +476,9 @@ class Power(BinaryOperator, _MPMathFunction): = a ^ b """ - operator = "^" - precedence = 590 attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_ONE_IDENTITY | A_PROTECTED - grouping = "Right" - default_formats = False - sympy_name = "Pow" - mpmath_name = "power" - nargs = {2} - - messages = { - "infy": "Infinite expression `1` encountered.", - "indet": "Indeterminate expression `1` encountered.", - } - defaults = { 2: "1", } @@ -588,12 +504,29 @@ class Power(BinaryOperator, _MPMathFunction): ), } + grouping = "Right" + + mpmath_name = "power" + + messages = { + "infy": "Infinite expression `1` encountered.", + "indet": "Indeterminate expression `1` encountered.", + } + + nargs = {2} + operator = "^" + precedence = 590 + rules = { "Power[]": "1", "Power[x_]": "x", } - summary_text = "exponentiation" + summary_text = "exponentiate" + + # FIXME Note this is deprecated in 1.11 + # Remember to up sympy doc link when this is corrected + sympy_name = "Pow" def eval_check(self, x, y, evaluation): "Power[x_, y_]" @@ -632,7 +565,13 @@ def eval_check(self, x, y, evaluation): class Sqrt(SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Sqrt.html + + :Square root: + https://en.wikipedia.org/wiki/Square_root ( + :SymPy: + https://docs.sympy.org/latest/modules/codegen.html#sympy.codegen.cfunctions.Sqrt, + :WMA: + https://reference.wolfram.com/language/ref/Sqrt.html)
'Sqrt[$expr$]' @@ -675,8 +614,10 @@ class Sqrt(SympyFunction): class Subtract(BinaryOperator): """ - :WMA link: - https://reference.wolfram.com/language/ref/Subtract.html + + :Subtraction: + https://en.wikipedia.org/wiki/Subtraction, (:WMA: + https://reference.wolfram.com/language/ref/Subtract.html)
'Subtract[$a$, $b$]' @@ -694,22 +635,27 @@ class Subtract(BinaryOperator): = a - b + c """ - operator = "-" - precedence_parse = 311 - precedence = 310 attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED grouping = "Left" + operator = "-" + precedence = 310 + precedence_parse = 311 rules = { "Subtract[x_, y_]": "Plus[x, Times[-1, y]]", } - summary_text = "subtraction" + summary_text = "subtract" class Times(BinaryOperator, SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Times.html + + :Multiplication: + https://en.wikipedia.org/wiki/Multiplication ( + :SymPy: + https://docs.sympy.org/latest/modules/core.html#sympy.core.mul.Mul, + :WMA:https://reference.wolfram.com/language/ref/Times.html)
'Times[$a$, $b$, ...]' @@ -789,9 +735,6 @@ class Times(BinaryOperator, SympyFunction): = 30. """ - operator = "*" - operator_display = " " - precedence = 400 attributes = ( A_FLAT | A_LISTABLE @@ -809,11 +752,17 @@ class Times(BinaryOperator, SympyFunction): formats = {} + operator = "*" + operator_display = " " + + precedence = 400 rules = {} + # FIXME Note this is deprecated in 1.11 + # Remember to up sympy doc link when this is corrected sympy_name = "Mul" - summary_text = "mutiplication" + summary_text = "mutiply" def format_times(self, items, evaluation, op="\u2062"): "Times[items__]" @@ -890,111 +839,4 @@ def format_outputform(self, items, evaluation): def eval(self, items, evaluation): "Times[items___]" items = numerify(items, evaluation).get_sequence() - elements = [] - numbers = [] - infinity_factor = False - - prec = min_prec(*items) - is_machine_precision = any(item.is_machine_precision() for item in items) - - # find numbers and simplify Times -> Power - for item in items: - if isinstance(item, Number): - numbers.append(item) - elif elements and item == elements[-1]: - elements[-1] = Expression(SymbolPower, elements[-1], Integer2) - elif ( - elements - and item.has_form("Power", 2) - and elements[-1].has_form("Power", 2) - and item.elements[0].sameQ(elements[-1].elements[0]) - ): - elements[-1] = Expression( - SymbolPower, - elements[-1].elements[0], - Expression(SymbolPlus, item.elements[1], elements[-1].elements[1]), - ) - elif ( - elements - and item.has_form("Power", 2) - and item.elements[0].sameQ(elements[-1]) - ): - elements[-1] = Expression( - SymbolPower, - elements[-1], - Expression(SymbolPlus, item.elements[1], Integer1), - ) - elif ( - elements - and elements[-1].has_form("Power", 2) - and elements[-1].elements[0].sameQ(item) - ): - elements[-1] = Expression( - SymbolPower, - item, - Expression(SymbolPlus, Integer1, elements[-1].elements[1]), - ) - elif item.get_head().sameQ(SymbolDirectedInfinity): - infinity_factor = True - if len(item.elements) > 1: - direction = item.elements[0] - if isinstance(direction, Number): - numbers.append(direction) - else: - elements.append(direction) - elif item.sameQ(SymbolInfinity) or item.sameQ(SymbolComplexInfinity): - infinity_factor = True - else: - elements.append(item) - - if numbers: - if prec is not None: - if is_machine_precision: - numbers = [item.to_mpmath() for item in numbers] - number = mpmath.fprod(numbers) - number = from_mpmath(number) - else: - with mpmath.workprec(prec): - numbers = [item.to_mpmath() for item in numbers] - number = mpmath.fprod(numbers) - number = from_mpmath(number, dps(prec)) - else: - number = sympy.Mul(*[item.to_sympy() for item in numbers]) - number = from_sympy(number) - else: - number = Integer1 - - if number.sameQ(Integer1): - number = None - elif number.is_zero: - if infinity_factor: - return SymbolIndeterminate - return number - elif ( - number.sameQ(IntegerM1) and elements and elements[0].has_form("Plus", None) - ): - elements[0] = Expression( - elements[0].get_head(), - *[ - Expression(SymbolTimes, IntegerM1, element) - for element in elements[0].elements - ], - ) - number = None - - if number is not None: - elements.insert(0, number) - - if not elements: - if infinity_factor: - return SymbolComplexInfinity - return Integer1 - - if len(elements) == 1: - ret = elements[0] - else: - ret = Expression(SymbolTimes, *elements) - if infinity_factor: - return Expression(SymbolDirectedInfinity, ret) - else: - return ret + return eval_Times(*items) diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index 049955ad6..5a85bbe7e 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -7,22 +7,24 @@ Basic arithmetic functions, including complex number arithmetic. """ -from mathics.eval.numerify import numerify - -# This tells documentation how to sort this module -sort_order = "mathics.builtin.mathematical-functions" - - from functools import lru_cache +from typing import Optional import mpmath import sympy -from mathics.builtin.base import Builtin, Predefined, SympyFunction, Test +from mathics.builtin.base import ( + Builtin, + IterationFunction, + Predefined, + SympyFunction, + Test, +) from mathics.builtin.inference import evaluate_predicate, get_assumptions_list -from mathics.builtin.lists import _IterationFunction from mathics.builtin.scoping import dynamic_scoping from mathics.core.atoms import ( + MATHICS3_COMPLEX_I, + MATHICS3_COMPLEX_I_NEG, Complex, Integer, Integer0, @@ -42,54 +44,52 @@ A_PROTECTED, ) from mathics.core.convert.expression import to_expression -from mathics.core.convert.mpmath import from_mpmath -from mathics.core.convert.python import from_python from mathics.core.convert.sympy import SympyExpression, from_sympy, sympy_symbol_prefix +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression +from mathics.core.expression_predefined import ( + MATHICS3_COMPLEX_INFINITY, + MATHICS3_I_INFINITY, + MATHICS3_I_NEG_INFINITY, + MATHICS3_INFINITY, + MATHICS3_NEG_INFINITY, + PredefinedExpression, +) from mathics.core.list import ListExpression -from mathics.core.number import SpecialValueError, dps, min_prec +from mathics.core.number import dps, min_prec from mathics.core.symbols import ( Atom, Symbol, - SymbolAbs, SymbolFalse, SymbolList, SymbolPlus, - SymbolPower, SymbolTimes, SymbolTrue, ) from mathics.core.systemsymbols import ( SymbolAnd, - SymbolComplexInfinity, SymbolDirectedInfinity, - SymbolExpandAll, - SymbolIndeterminate, SymbolInfix, - SymbolOverflow, SymbolPiecewise, SymbolPossibleZeroQ, - SymbolSimplify, SymbolTable, SymbolUndefined, ) +from mathics.eval.arithmetic import eval_Abs, eval_mpmath_function, 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" -@lru_cache(maxsize=4096) -def call_mpmath(mpmath_function, mpmath_args): - try: - return mpmath_function(*mpmath_args) - except ValueError as exc: - text = str(exc) - if text == "gamma function pole": - return SymbolComplexInfinity - else: - raise - except ZeroDivisionError: - return - except SpecialValueError as exc: - return Symbol(exc.name) + +map_direction_infinity = { + Integer1: MATHICS3_INFINITY, + IntegerM1: MATHICS3_NEG_INFINITY, + MATHICS3_COMPLEX_I: MATHICS3_I_INFINITY, + MATHICS3_COMPLEX_I_NEG: MATHICS3_I_NEG_INFINITY, +} class _MPMathFunction(SympyFunction): @@ -114,12 +114,10 @@ def get_mpmath_function(self, args): return None return getattr(mpmath, self.mpmath_name) - def eval(self, z, evaluation): + def eval(self, z, evaluation: Evaluation): "%(name)s[z__]" args = numerify(z, evaluation).get_sequence() - mpmath_function = self.get_mpmath_function(tuple(args)) - result = None # if no arguments are inexact attempt to use sympy if all(not x.is_inexact() for x in args): @@ -128,55 +126,22 @@ def eval(self, z, evaluation): result = from_sympy(result) # evaluate elements to convert e.g. Plus[2, I] -> Complex[2, 1] return result.evaluate_elements(evaluation) - elif mpmath_function is None: - return if not all(isinstance(arg, Number) for arg in args): return - if any(arg.is_machine_precision() for arg in args): - # if any argument has machine precision then the entire calculation - # is done with machine precision. - float_args = [ - arg.round().get_float_value(permit_complex=True) for arg in args - ] - if None in float_args: - return + mpmath_function = self.get_mpmath_function(tuple(args)) + if mpmath_function is None: + return - result = call_mpmath(mpmath_function, tuple(float_args)) - - if isinstance(result, (mpmath.mpc, mpmath.mpf)): - if mpmath.isinf(result) and isinstance(result, mpmath.mpc): - result = SymbolComplexInfinity - elif mpmath.isinf(result) and result > 0: - result = Expression(SymbolDirectedInfinity, Integer1) - elif mpmath.isinf(result) and result < 0: - result = Expression(SymbolDirectedInfinity, IntegerM1) - elif mpmath.isnan(result): - result = SymbolIndeterminate - else: - # FIXME: replace try/except as a context manager - # like "with evaluation.from_mpmath()... - # which can be instrumented for - # or mpmath tracing and benchmarking on demand. - # Then use it on other places where mpmath appears. - try: - result = from_mpmath(result) - except OverflowError: - evaluation.message("General", "ovfl") - result = Expression(SymbolOverflow) + if any(arg.is_machine_precision() for arg in args): + prec = None else: prec = min_prec(*args) d = dps(prec) - args = [eval_N(arg, evaluation, Integer(d)) for arg in args] - with mpmath.workprec(prec): - mpmath_args = [x.to_mpmath() for x in args] - if None in mpmath_args: - return - result = call_mpmath(mpmath_function, tuple(mpmath_args)) - if isinstance(result, (mpmath.mpc, mpmath.mpf)): - result = from_mpmath(result, d) - return result + args = [arg.round(d) for arg in args] + + return eval_mpmath_function(mpmath_function, *args, prec=prec) class _MPMathMultiFunction(_MPMathFunction): @@ -222,34 +187,35 @@ 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 + + :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__() @@ -259,6 +225,13 @@ class Abs(_MPMathFunction): 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): """ @@ -315,7 +288,7 @@ class Arg(_MPMathFunction): sympy_name = "arg" def eval(self, z, evaluation, options={}): - "%(name)s[z_, OptionsPattern[%(name)s]]" + "Arg[z_, OptionsPattern[Arg]]" if Expression(SymbolPossibleZeroQ, z).evaluate(evaluation) is SymbolTrue: return Integer0 preference = self.get_option(options, "Method", evaluation).get_string_value() @@ -353,7 +326,7 @@ class Assuming(Builtin): summary_text = "set assumptions during the evaluation" attributes = A_HOLD_REST | A_PROTECTED - def eval_assuming(self, assumptions, expr, evaluation): + def eval_assuming(self, assumptions, expr, evaluation: Evaluation): "Assuming[assumptions_, expr_]" assumptions = assumptions.evaluate(evaluation) if assumptions is SymbolTrue: @@ -409,11 +382,11 @@ class Boole(Builtin): = Boole[a == 7] """ - summary_text = "translate 'True' to 1, and 'False' to 0" attributes = A_LISTABLE | A_PROTECTED + summary_text = "translate 'True' to 1, and 'False' to 0" - def eval(self, expr, evaluation): - "%(name)s[expr_]" + def eval(self, expr, evaluation: Evaluation): + "Boole[expr_]" if expr is SymbolTrue: return Integer1 elif expr is SymbolFalse: @@ -483,8 +456,8 @@ class Complex_(Builtin): summary_text = "head for complex numbers" name = "Complex" - def eval(self, r, i, evaluation): - "%(name)s[r_?NumberQ, i_?NumberQ]" + def eval(self, r, i, evaluation: Evaluation): + "Complex[r_?NumberQ, i_?NumberQ]" if isinstance(r, Complex) or isinstance(i, Complex): sym_form = r.to_sympy() + sympy.I * i.to_sympy() @@ -495,11 +468,13 @@ def eval(self, r, i, evaluation): class ConditionalExpression(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/ConditionalExpression.html + :WMA link:https://reference.wolfram.com/ +language/ref/ConditionalExpression.html
'ConditionalExpression[$expr$, $cond$]' -
returns $expr$ if $cond$ evaluates to $True$, $Undefined$ if $cond$ evaluates to $False$. +
returns $expr$ if $cond$ evaluates to $True$, $Undefined$ if $cond$ \ + evaluates to $False$.
>> ConditionalExpression[x^2, True] @@ -536,7 +511,7 @@ class ConditionalExpression(Builtin): "expr1_ ^ ConditionalExpression[expr2_, cond_]": "ConditionalExpression[expr1^expr2, cond]", } - def eval_generic(self, expr, cond, evaluation): + def eval_generic(self, expr, cond, evaluation: Evaluation): "ConditionalExpression[expr_, cond_]" # What we need here is a way to evaluate # cond as a predicate, using assumptions. @@ -641,8 +616,7 @@ class DirectedInfinity(SympyFunction): = Indeterminate >> DirectedInfinity[0] - : Indeterminate expression 0 Infinity encountered. - = Indeterminate + = ComplexInfinity #> DirectedInfinity[1+I]+DirectedInfinity[2+I] = (2 / 5 + I / 5) Sqrt[5] Infinity + (1 / 2 + I / 2) Sqrt[2] Infinity @@ -653,15 +627,12 @@ class DirectedInfinity(SympyFunction): summary_text = "infinite quantity with a defined direction in the complex plane" rules = { - "DirectedInfinity[Indeterminate]": "Indeterminate", "DirectedInfinity[args___] ^ -1": "0", - "0 * DirectedInfinity[args___]": "Message[Infinity::indet, Unevaluated[0 DirectedInfinity[args]]]; Indeterminate", - "DirectedInfinity[a_?NumericQ] /; N[Abs[a]] != 1": "DirectedInfinity[a / Abs[a]]", - "DirectedInfinity[a_] * DirectedInfinity[b_]": "DirectedInfinity[a*b]", - "DirectedInfinity[] * DirectedInfinity[args___]": "DirectedInfinity[]", - # Rules already implemented in Times.eval - # "z_?NumberQ * DirectedInfinity[]": "DirectedInfinity[]", - # "z_?NumberQ * DirectedInfinity[a_]": "DirectedInfinity[z * a]", + # Special arguments: + "DirectedInfinity[DirectedInfinity[args___]]": "DirectedInfinity[args]", + "DirectedInfinity[Indeterminate]": "Indeterminate", + "DirectedInfinity[Alternatives[0, 0.]]": "DirectedInfinity[]", + # Plus "DirectedInfinity[a_] + DirectedInfinity[b_] /; b == -a": ( "Message[Infinity::indet," " Unevaluated[DirectedInfinity[a] + DirectedInfinity[b]]];" @@ -673,19 +644,15 @@ class DirectedInfinity(SympyFunction): "Indeterminate" ), "DirectedInfinity[args___] + _?NumberQ": "DirectedInfinity[args]", - "DirectedInfinity[0]": ( + # Times. See if can be reinstalled in eval_Times + "Alternatives[0, 0.] DirectedInfinity[z___]": ( "Message[Infinity::indet," - " Unevaluated[DirectedInfinity[0]]];" + " Unevaluated[0 DirectedInfinity[z]]];" "Indeterminate" ), - "DirectedInfinity[0.]": ( - "Message[Infinity::indet," - " Unevaluated[DirectedInfinity[0.]]];" - "Indeterminate" - ), - "DirectedInfinity[ComplexInfinity]": "ComplexInfinity", - "DirectedInfinity[Infinity]": "Infinity", - "DirectedInfinity[-Infinity]": "-Infinity", + "a_?NumericQ * DirectedInfinity[b_]": "DirectedInfinity[a * b]", + "a_ DirectedInfinity[]": "DirectedInfinity[]", + "DirectedInfinity[a_] * DirectedInfinity[b_]": "DirectedInfinity[a * b]", } formats = { @@ -696,6 +663,47 @@ class DirectedInfinity(SympyFunction): "DirectedInfinity[z_?NumericQ]": "HoldForm[z Infinity]", } + def eval_complex_infinity(self, evaluation: Evaluation): + """DirectedInfinity[]""" + return MATHICS3_COMPLEX_INFINITY + + def eval_directed_infinity(self, direction, evaluation: Evaluation): + """DirectedInfinity[direction_]""" + result = map_direction_infinity.get(direction, None) + if result: + return result + + if direction.is_zero: + return MATHICS3_COMPLEX_INFINITY + + normalized_direction = eval_Sign(direction) + # TODO: improve eval_Sign, to avoid the need of the + # following block: + # ############################################ + if normalized_direction is None: + ndir = eval_N(direction, evaluation) + if isinstance(ndir, (Integer, Rational, Real)): + if abs(ndir.value) == 1.0: + normalized_direction = direction + else: + normalized_direction = direction / Abs(direction) + elif isinstance(ndir, Complex): + re, im = ndir.value + if abs(re.value**2 + im.value**2 - 1.0) < 1.0e-9: + normalized_direction = direction + else: + normalized_direction = direction / Abs(direction) + else: + return None + # ############################################## + + if normalized_direction is None: + return None + return PredefinedExpression( + SymbolDirectedInfinity, + normalized_direction.evaluate(evaluation), + ) + def to_sympy(self, expr, **kwargs): if len(expr.elements) == 1: dir = expr.elements[0].get_int_value() @@ -709,7 +717,78 @@ def to_sympy(self, expr, **kwargs): return sympy.zoo -class I(Predefined): +class Element(Builtin): + """ + :Element of:https://en.wikipedia.org/wiki/Element_(mathematics) \ + (:WMA:https://reference.wolfram.com/language/ref/Element.html) + +
+
'Element[$expr$, $domain$]' +
returns $True$ if $expr$ is an element of $domain$ +
'Element[$expr_1$|$expr_2$|..., $domain$]' +
returns $True$ if all the $expr_i$ belongs to $domain$, and \ + $False$ if one of the items doesn't. +
+ + + Check if $3$ and $a$ are both integers. If $a$ is not defined, then \ +'Element' reduces the condition: + >> Element[3 | a, Integers] + = Element[a, Integers] + + Notice that standard domain names ('Primes', 'Integers', 'Rationals', \ +'Algebraics', 'Reals', 'Complexes', and 'Booleans')\ + are in plural form. If a singular form is used, a warning is shown: + + >> Element[a, Real] + : The second argument Real of Element should be one of: Primes, Integers, \ +Rationals, Algebraics, Reals, Complexes, or Booleans. + = Element[a, Real] + + """ + + messages = { + "bset": ( + "The second argument `1` of Element should be one of: " + "Primes, Integers, Rationals, Algebraics, " + "Reals, Complexes, or Booleans." + ), + } + + summary_text = "check whether belongs the domain" + + def eval_wrong_domain( + self, elem: BaseElement, domain: BaseElement, evaluation: Evaluation + ): + ( + "Element[elem_, domain:(Alternatives[" + "Algebraic, Bool, Integer, Prime, Rational, Real, Complex])]" + ) + evaluation.message("Element", "bset", domain) + return None + + def eval_Element_alternatives( + self, elems: BaseElement, domain: BaseElement, evaluation: Evaluation + ) -> Optional[Expression]: + """Element[elems_Alternatives, domain_]""" + items = elems.elements + unknown = [] + for item in items: + item_belongs = Element(item, domain).evaluate(evaluation) + if item_belongs is SymbolTrue: + continue + if item_belongs is SymbolFalse: + return SymbolFalse + unknown.append(item) + if len(unknown) == len(items): + return None + if len(unknown) == 0: + return SymbolTrue + # If some of the items remain unkown, return a reduced expression + return Element(Expression(elems.head, *unknown), domain) + + +class I_(Predefined): """ :Imaginary unit:https://en.wikipedia.org/wiki/Imaginary_unit \ (:WMA:https://reference.wolfram.com/language/ref/I.html) @@ -725,10 +804,11 @@ class I(Predefined): = 10 """ + name = "I" summary_text = "imaginary unit" python_equivalent = 1j - def evaluate(self, evaluation): + def evaluate(self, evaluation: Evaluation): return Complex(Integer0, Integer1) @@ -756,17 +836,17 @@ class Im(SympyFunction): summary_text = "imaginary part" attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - def eval_complex(self, number, evaluation): + def eval_complex(self, number, evaluation: Evaluation): "Im[number_Complex]" + if isinstance(number, Complex): + return number.imag - return number.imag - - def eval_number(self, number, evaluation): + def eval_number(self, number, evaluation: Evaluation): "Im[number_?NumberQ]" return Integer0 - def eval(self, number, evaluation): + def eval(self, number, evaluation: Evaluation): "Im[number_]" return from_sympy(sympy.im(number.to_sympy().expand(complex=True))) @@ -793,29 +873,6 @@ class Integer_(Builtin): name = "Integer" -class NumberQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/NumberQ.html - -
-
'NumberQ[$expr$]' -
returns 'True' if $expr$ is an explicit number, and 'False' otherwise. -
- - >> NumberQ[3+I] - = True - >> NumberQ[5!] - = True - >> NumberQ[Pi] - = False - """ - - summary_text = "test whether an expression is a number" - - def test(self, expr): - return isinstance(expr, Number) - - class Piecewise(SympyFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Piecewise.html @@ -857,7 +914,7 @@ class Piecewise(SympyFunction): attributes = A_HOLD_ALL | A_PROTECTED - def eval(self, items, evaluation): + def eval(self, items, evaluation: Evaluation): "%(name)s[items__]" result = self.to_sympy( Expression(SymbolPiecewise, *items.get_sequence()), evaluation=evaluation @@ -911,76 +968,7 @@ def from_sympy(self, sympy_name, args): return Expression(self.get_name(), args) -class PossibleZeroQ(SympyFunction): - """ - :WMA link:https://reference.wolfram.com/language/ref/PossibleZeroQ.html - -
-
'PossibleZeroQ[$expr$]' -
returns 'True' if basic symbolic and numerical methods suggest that expr has value zero, and 'False' otherwise. -
- - Test whether a numeric expression is zero: - >> PossibleZeroQ[E^(I Pi/4) - (-1)^(1/4)] - = True - - The determination is approximate. - - Test whether a symbolic expression is likely to be identically zero: - >> PossibleZeroQ[(x + 1) (x - 1) - x^2 + 1] - = True - - - >> PossibleZeroQ[(E + Pi)^2 - E^2 - Pi^2 - 2 E Pi] - = True - - Show that a numeric expression is nonzero: - >> PossibleZeroQ[E^Pi - Pi^E] - = False - - >> PossibleZeroQ[1/x + 1/y - (x + y)/(x y)] - = True - - Decide that a numeric expression is zero, based on approximate computations: - >> PossibleZeroQ[2^(2 I) - 2^(-2 I) - 2 I Sin[Log[4]]] - = True - - >> PossibleZeroQ[Sqrt[x^2] - x] - = False - """ - - summary_text = "test whether an expression is estimated to be zero" - attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - - sympy_name = "_iszero" - - def eval(self, expr, evaluation): - "%(name)s[expr_]" - from sympy.matrices.utilities import _iszero - - sympy_expr = expr.to_sympy() - result = _iszero(sympy_expr) - if result is None: - # try expanding the expression - exprexp = Expression(SymbolExpandAll, expr).evaluate(evaluation) - exprexp = exprexp.to_sympy() - result = _iszero(exprexp) - if result is None: - # Can't get exact answer, so try approximate equal - numeric_val = eval_N(expr, evaluation) - if numeric_val and hasattr(numeric_val, "is_approx_zero"): - result = numeric_val.is_approx_zero - elif not numeric_val.is_numeric(evaluation): - return ( - SymbolTrue - if Expression(SymbolSimplify, expr).evaluate(evaluation) == Integer0 - else SymbolFalse - ) - - return from_python(result) - - -class Product(_IterationFunction, SympyFunction): +class Product(IterationFunction, SympyFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Product.html @@ -1030,7 +1018,7 @@ class Product(_IterationFunction, SympyFunction): sympy_name = "Product" - rules = _IterationFunction.rules.copy() + rules = IterationFunction.rules.copy() rules.update( { "MakeBoxes[Product[f_, {i_, a_, b_, 1}]," @@ -1085,8 +1073,8 @@ class Rational_(Builtin): summary_text = "head for rational numbers" name = "Rational" - def eval(self, n: Integer, m: Integer, evaluation): - "%(name)s[n_Integer, m_Integer]" + def eval(self, n: Integer, m: Integer, evaluation: Evaluation): + "Rational[n_Integer, m_Integer]" if m.value == 1: return n @@ -1119,19 +1107,18 @@ class Re(SympyFunction): attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED sympy_name = "re" - def eval_complex(self, number, evaluation): + def eval_complex(self, number, evaluation: Evaluation): "Re[number_Complex]" + if isinstance(number, Complex): + return number.real - return number.real - - def eval_number(self, number, evaluation): + def eval_number(self, number, evaluation: Evaluation): "Re[number_?NumberQ]" return number - def eval(self, number, evaluation): + def eval(self, number, evaluation: Evaluation): "Re[number_]" - return from_sympy(sympy.re(number.to_sympy().expand(complex=True))) @@ -1233,9 +1220,11 @@ class RealNumberQ(Test): = True """ + attributes = A_NO_ATTRIBUTES + summary_text = "test whether an expression is a real number" - def test(self, expr): + def test(self, expr) -> bool: return isinstance(expr, (Integer, Rational, Real)) @@ -1279,27 +1268,29 @@ class Sign(SympyFunction): "argx": "Sign called with `1` arguments; 1 argument is expected.", } - def eval(self, x, evaluation): + rules = { + "Sign[Power[a_, b_]]": "Power[Sign[a], b]", + } + + def eval(self, x, evaluation: Evaluation): "%(name)s[x_]" - # Sympy and mpmath do not give the desired form of complex number - if isinstance(x, Complex): - return Expression( - SymbolTimes, - x, - Expression(SymbolPower, Expression(SymbolAbs, x), IntegerM1), - ) + result = eval_Sign(x) + if result is not None: + return result + # return None sympy_x = x.to_sympy() if sympy_x is None: return None - return super().eval(x, evaluation) + # Unhandled cases. Use sympy + return super(Sign, self).eval(x, evaluation) - def eval_error(self, x, seqs, evaluation): + def eval_error(self, x, seqs, evaluation: Evaluation): "Sign[x_, seqs__]" - return evaluation.message("Sign", "argx", Integer(len(seqs.get_sequence()) + 1)) + evaluation.message("Sign", "argx", Integer(len(seqs.get_sequence()) + 1)) -class Sum(_IterationFunction, SympyFunction): +class Sum(IterationFunction, SympyFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Sum.html @@ -1314,9 +1305,11 @@ class Sum(_IterationFunction, SympyFunction):
$i$ ranges from $imin$ to $imax$ in steps of $di$.
'Sum[$expr$, {$i$, $imin$, $imax$}, {$j$, $jmin$, $jmax$}, ...]' -
evaluates $expr$ as a multiple sum, with {$i$, ...}, {$j$, ...}, ... being in outermost-to-innermost order. +
evaluates $expr$ as a multiple sum, with {$i$, ...}, {$j$, ...}, ... being \ + in outermost-to-innermost order.
+ A sum that Gauss in elementary school was asked to do to kill time: >> Sum[k, {k, 1, 10}] = 55 @@ -1333,7 +1326,7 @@ class Sum(_IterationFunction, SympyFunction): >> Sum[1 / 2 ^ i, {i, 1, Infinity}] = 1 - Leibniz forumla used in computing Pi: + Leibniz formula used in computing Pi: >> Sum[1 / ((-1)^k (2k + 1)), {k, 0, Infinity}] = Pi / 4 @@ -1353,6 +1346,9 @@ class Sum(_IterationFunction, SympyFunction): >> Sum[k, {k, I, I + 1}] = 1 + 2 I + >> Sum[k, {k, Range[5]}] + = 15 + >> Sum[f[i], {i, 1, 7}] = f[1] + f[2] + f[3] + f[4] + f[5] + f[6] + f[7] @@ -1381,7 +1377,7 @@ class Sum(_IterationFunction, SympyFunction): sympy_name = "Sum" - rules = _IterationFunction.rules.copy() + rules = IterationFunction.rules.copy() rules.update( { "MakeBoxes[Sum[f_, {i_, a_, b_, 1}]," @@ -1396,7 +1392,7 @@ class Sum(_IterationFunction, SympyFunction): def get_result(self, items): return Expression(SymbolPlus, *items) - def to_sympy(self, expr, **kwargs) -> SympyExpression: + def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: """ Perform summation via sympy.summation """ diff --git a/mathics/builtin/assignments/__init__.py b/mathics/builtin/assignments/__init__.py index 6fe4412c5..d3bb57ecf 100644 --- a/mathics/builtin/assignments/__init__.py +++ b/mathics/builtin/assignments/__init__.py @@ -1,7 +1,8 @@ """ Assignments -Assigments allow you to set or clear variables, indexed variables, structure elements, functions, and general transformations. +Assignments allow you to set or clear variables, indexed variables, \ +structure elements, functions, and general transformations. You can also get assignment and documentation information about symbols. """ diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index 384d96c59..f960691c5 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -11,15 +11,16 @@ assign_store_rules_by_tag, normalize_lhs, ) +from mathics.core.atoms import String from mathics.core.attributes import ( A_HOLD_ALL, A_HOLD_FIRST, A_PROTECTED, A_SEQUENCE_HOLD, ) -from mathics.core.pymathics import PyMathicsLoadException, eval_load_module from mathics.core.symbols import SymbolNull from mathics.core.systemsymbols import SymbolFailed +from mathics.eval.pymathics import PyMathicsLoadException, eval_LoadModule class _SetOperator: @@ -66,29 +67,29 @@ class LoadModule(Builtin):
'Load Mathics definitions from the python module $module$
>> LoadModule["nomodule"] - : Python module nomodule does not exist. + : Python import errors with: No module named 'nomodule'. = $Failed >> LoadModule["sys"] - : Python module sys is not a pymathics module. + : Python module "sys" is not a Mathics3 module. = $Failed """ name = "LoadModule" messages = { - "notfound": "Python module `1` does not exist.", - "notmathicslib": "Python module `1` is not a pymathics module.", + "loaderror": """Python import errors with: `1`.""", + "notmathicslib": """Python module "`1`" is not a Mathics3 module.""", } summary_text = "load a pymathics module" def eval(self, module, evaluation): "LoadModule[module_String]" try: - eval_load_module(module.value, evaluation) + eval_LoadModule(module.value, evaluation.definitions) except PyMathicsLoadException: evaluation.message(self.name, "notmathicslib", module) return SymbolFailed - except ImportError: - evaluation.message(self.get_name(), "notfound", module) + except Exception as e: + evaluation.message(self.get_name(), "loaderror", String(str(e))) return SymbolFailed return module diff --git a/mathics/builtin/assignments/upvalues.py b/mathics/builtin/assignments/upvalues.py index ebbed3903..ac05393ac 100644 --- a/mathics/builtin/assignments/upvalues.py +++ b/mathics/builtin/assignments/upvalues.py @@ -9,17 +9,9 @@ https://reference.wolfram.com/language/tutorial/TransformationRulesAndDefinitions.html#6972. """ -from mathics.builtin.assignments.assignment import _SetOperator -from mathics.builtin.base import BinaryOperator, Builtin +from mathics.builtin.base import Builtin from mathics.core.assignment import get_symbol_values -from mathics.core.attributes import ( - A_HOLD_ALL, - A_HOLD_FIRST, - A_PROTECTED, - A_SEQUENCE_HOLD, -) -from mathics.core.symbols import SymbolNull -from mathics.core.systemsymbols import SymbolFailed +from mathics.core.attributes import A_HOLD_ALL, A_PROTECTED # In Mathematica 5, this appears under "Types of Values". diff --git a/mathics/builtin/atomic/atomic.py b/mathics/builtin/atomic/atomic.py index d493da1a0..d86d156fc 100644 --- a/mathics/builtin/atomic/atomic.py +++ b/mathics/builtin/atomic/atomic.py @@ -56,7 +56,7 @@ class AtomQ(Test): summary_text = "test whether an expression is an atom" - def test(self, expr): + def test(self, expr) -> bool: return isinstance(expr, Atom) diff --git a/mathics/builtin/atomic/numbers.py b/mathics/builtin/atomic/numbers.py index 108bbc9da..be40f7a58 100644 --- a/mathics/builtin/atomic/numbers.py +++ b/mathics/builtin/atomic/numbers.py @@ -9,7 +9,7 @@ Representation of Numbers Integers and Real numbers with any number of digits, automatically tagging \ -numerical preceision when appropriate. +numerical precision when appropriate. Precision is not "guarded" through the evaluation process. Only integer \ precision is supported. @@ -24,32 +24,35 @@ from mathics.builtin.base import Builtin, Predefined, Test from mathics.core.atoms import ( - Complex, Integer, Integer0, Integer10, MachineReal, - MachineReal0, Number, Rational, - Real, ) from mathics.core.attributes import A_LISTABLE, A_PROTECTED -from mathics.core.convert.python import from_bool, from_python +from mathics.core.convert.python import from_python from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.number import dps, machine_epsilon, machine_precision +from mathics.core.number import ( + FP_MANTISA_BINARY_DIGITS, + MACHINE_EPSILON, + MACHINE_PRECISION_VALUE, +) from mathics.core.symbols import Symbol, SymbolDivide from mathics.core.systemsymbols import ( SymbolIndeterminate, SymbolInfinity, SymbolLog, + SymbolMachinePrecision, SymbolN, SymbolPrecision, SymbolRealDigits, SymbolRound, ) from mathics.eval.nevaluator import eval_N +from mathics.eval.numbers import eval_Accuracy, eval_Precision SymbolIntegerDigits = Symbol("IntegerDigits") SymbolIntegerExponent = Symbol("IntegerExponent") @@ -157,7 +160,8 @@ class Accuracy(Builtin):
examines the number of significant digits of $expr$ after the \ decimal point in the number x.
- This is rather a proof-of-concept than a full implementation. + Notice that the result could be slightly different than the obtained \ + in WMA, due to differencs in the internal representation of the real numbers. Accuracy of a real number is estimated from its value and its precision: @@ -173,21 +177,28 @@ class Accuracy(Builtin): >> Accuracy[A] = Infinity - For Complex numbers, the accuracy is the smaller of the accuracies of its \ - real and imaginary parts: - >> Accuracy[1.00`2 + 2.00`2 I] - = 1. + For Complex numbers, the accuracy is estimated as (minus) the base-10 log + of the square root of the squares of the errors on the real and complex parts: + >> z=Complex[3.00``2, 4..00``2]; + >> Accuracy[z] == -Log[10, Sqrt[10^(-2 Accuracy[Re[z]]) + 10^(-2 Accuracy[Im[z]])]] + = True Accuracy of expressions is given by the minimum accuracy of its elements: >> Accuracy[F[1, Pi, A]] = Infinity >> Accuracy[F[1.3, Pi, A]] - = 14.8861 + = ... 'Accuracy' for the value 0 is a fixed-precision Real number: >> 0``2 = 0.00 + >> Accuracy[0.``2] + = 2. + + For 0.`, the accuracy satisfies: + >> Accuracy[0.`] == $MachinePrecision - Log[10, $MinMachineNumber] + = True In compound expressions, the 'Accuracy' is fixed by the number with the lowest 'Accuracy': @@ -196,71 +207,18 @@ class Accuracy(Builtin): See also :'Precision': - /doc/reference-of-built-in-symbols/atomic-elements-of-expressions/representation-of-numbers/precision/. + /doc/reference-of-built-in-symbols/atomic-elements-of-expressions +/representation-of-numbers/precision/. """ summary_text = "find the accuracy of a number" def eval(self, z, evaluation): "Accuracy[z_]" - if isinstance(z, Real): - if z.is_zero: - return MachineReal(dps(z.get_precision())) - z_f = z.to_python() - log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10) - return MachineReal(dps(z.get_precision()) - log10_z) - - if isinstance(z, Complex): - acc_real = self.eval(z.real, evaluation) - acc_imag = self.eval(z.imag, evaluation) - if acc_real is SymbolInfinity: - return acc_imag - if acc_imag is SymbolInfinity: - return acc_real - return Real(min(acc_real.to_python(), acc_imag.to_python())) - - if isinstance(z, Expression): - result = None - for element in z.elements: - candidate = self.eval(element, evaluation) - if isinstance(candidate, Real): - candidate_f = candidate.to_python() - if result is None or candidate_f < result: - result = candidate_f - if result is not None: - return Real(result) - return SymbolInfinity - - -class ExactNumberQ(Test): - """ - - :WMA link: - https://reference.wolfram.com/language/ref/ExactNumberQ.html - -
-
'ExactNumberQ[$expr$]' -
returns 'True' if $expr$ is an exact number, and 'False' otherwise. -
- - >> ExactNumberQ[10] - = True - >> ExactNumberQ[4.0] - = False - >> ExactNumberQ[n] - = False - - 'ExactNumberQ' can be applied to complex numbers: - >> ExactNumberQ[1 + I] - = True - >> ExactNumberQ[1 + 1. I] - = False - """ - - summary_text = "test if an expression is an exact real or complex number" - - def test(self, expr): - return isinstance(expr, Number) and not expr.is_inexact() + acc = eval_Accuracy(z) + if acc is None: + return SymbolInfinity + return MachineReal(acc) class IntegerExponent(Builtin): @@ -422,83 +380,6 @@ def eval(self, n, b, evaluation): return Integer(j) -class InexactNumberQ(Test): - """ - :WMA link: - https://reference.wolfram.com/language/ref/InexactNumberQ.html - -
-
'InexactNumberQ[$expr$]' -
returns 'True' if $expr$ is not an exact number, and 'False' otherwise. -
- - >> InexactNumberQ[a] - = False - >> InexactNumberQ[3.0] - = True - >> InexactNumberQ[2/3] - = False - - 'InexactNumberQ' can be applied to complex numbers: - >> InexactNumberQ[4.0+I] - = True - """ - - summary_text = "the negation of ExactNumberQ" - - def test(self, expr): - return isinstance(expr, Number) and expr.is_inexact() - - -class IntegerQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/IntegerQ.html - -
-
'IntegerQ[$expr$]' -
returns 'True' if $expr$ is an integer, and 'False' otherwise. -
- - >> IntegerQ[3] - = True - >> IntegerQ[Pi] - = False - """ - - summary_text = "test whether an expression is an integer" - - def test(self, expr): - return isinstance(expr, Integer) - - -class MachineNumberQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/MachineNumberQ.html - -
-
'MachineNumberQ[$expr$]' -
returns 'True' if $expr$ is a machine-precision real or complex number. -
- - = True - >> MachineNumberQ[3.14159265358979324] - = False - >> MachineNumberQ[1.5 + 2.3 I] - = True - >> MachineNumberQ[2.71828182845904524 + 3.14159265358979324 I] - = False - #> MachineNumberQ[1.5 + 3.14159265358979324 I] - = True - #> MachineNumberQ[1.5 + 5 I] - = True - """ - - summary_text = "test if expression is a machine‐precision real or complex number" - - def test(self, expr): - return expr.is_machine_precision() - - class RealDigits(Builtin): """ :WMA link: @@ -589,7 +470,7 @@ class RealDigits(Builtin): def eval_complex(self, n, var, evaluation): "%(name)s[n_Complex, var___]" - return evaluation.message("RealDigits", "realx", n) + evaluation.message("RealDigits", "realx", n) def eval_rational_with_base(self, n, b, evaluation): "%(name)s[n_Rational, b_Integer]" @@ -623,7 +504,8 @@ def eval(self, n, evaluation): # Handling the testcases that throw the error message and return the # output that doesn't include `base` argument if isinstance(n, Symbol) and n.name.startswith("System`"): - return evaluation.message("RealDigits", "ndig", n) + evaluation.message("RealDigits", "ndig", n) + return if n.is_numeric(evaluation): return self.eval_with_base(n, from_python(10), evaluation) @@ -639,7 +521,7 @@ def eval_with_base(self, n, b, evaluation, nr_elements=None, pos=None): if isinstance(n, (Expression, Symbol, Rational)): pos_len = abs(pos) + 1 if pos is not None and pos < 0 else 1 if nr_elements is not None: - # we can't use apply_n here because we have the two-arguemnt form + # we can't use eval_n here because we have the two-arguemnt form n = Expression( SymbolN, n, @@ -649,14 +531,17 @@ def eval_with_base(self, n, b, evaluation, nr_elements=None, pos=None): if rational_no: n = eval_N(n, evaluation) else: - return evaluation.message("RealDigits", "ndig", expr) + evaluation.message("RealDigits", "ndig", expr) + return py_n = abs(n.value) if not py_b > 1: - return evaluation.message("RealDigits", "rbase", py_b) + evaluation.message("RealDigits", "rbase", py_b) + return if isinstance(py_n, complex): - return evaluation.message("RealDigits", "realx", expr) + evaluation.message("RealDigits", "realx", expr) + return if isinstance(n, Integer): display_len = ( @@ -746,7 +631,8 @@ def eval_with_base_and_length(self, n, b, length, evaluation, pos=None): elements.append(from_python(pos)) expr = Expression(SymbolRealDigits, n, b, length, *elements) if not (isinstance(length, Integer) and length.get_int_value() >= 0): - return evaluation.message("RealDigits", "intnm", expr) + evaluation.message("RealDigits", "intnm", expr) + return return self.eval_with_base( n, b, evaluation, nr_elements=length.get_int_value(), pos=pos @@ -755,9 +641,10 @@ def eval_with_base_and_length(self, n, b, length, evaluation, pos=None): def eval_with_base_length_and_precision(self, n, b, length, p, evaluation): "%(name)s[n_?NumericQ, b_Integer, length_, p_]" if not isinstance(p, Integer): - return evaluation.message( + evaluation.message( "RealDigits", "intm", Expression(SymbolRealDigits, n, b, length, p) ) + return return self.eval_with_base_and_length( n, b, length, evaluation, pos=p.get_int_value() @@ -846,7 +733,7 @@ class MachineEpsilon_(Predefined): summary_text = "the difference between 1.0 and the next-nearest number representable as a machine-precision number" def evaluate(self, evaluation): - return MachineReal(machine_epsilon) + return MachineReal(MACHINE_EPSILON) class MachinePrecision_(Predefined): @@ -895,7 +782,9 @@ class MachinePrecision(Predefined): is_numeric = True rules = { - "N[MachinePrecision, prec_]": ("N[Log[10, 2] * %i, prec]" % machine_precision), + "N[MachinePrecision, prec_]": ( + "N[Log[10, 2] * %i, prec]" % FP_MANTISA_BINARY_DIGITS + ), } summary_text = "symbol used to indicate machine‐number precision" @@ -958,57 +847,6 @@ class MinPrecision(Builtin): summary_text = "settable global minimum precision bound" -class NumericQ(Builtin): - """ - :WMA link: - https://reference.wolfram.com/language/ref/NumericQ.html - -
-
'NumericQ[$expr$]' -
tests whether $expr$ represents a numeric quantity. -
- - >> NumericQ[2] - = True - >> NumericQ[Sqrt[Pi]] - = True - >> NumberQ[Sqrt[Pi]] - = False - - It is possible to set that a symbol is numeric or not by assign a boolean value - to ``NumericQ`` - >> NumericQ[a]=True - = True - >> NumericQ[a] - = True - >> NumericQ[Sin[a]] - = True - - Clear and ClearAll do not restore the default value. - - >> Clear[a]; NumericQ[a] - = True - >> ClearAll[a]; NumericQ[a] - = True - >> NumericQ[a]=False; NumericQ[a] - = False - NumericQ can only set to True or False - >> NumericQ[a] = 37 - : Cannot set NumericQ[a] to 37; the lhs argument must be a symbol and the rhs must be True or False. - = 37 - """ - - messages = { - "argx": "NumericQ called with `1` arguments; 1 argument is expected.", - "set": "Cannot set `1` to `2`; the lhs argument must be a symbol and the rhs must be True or False.", - } - summary_text = "test whether an expression is a number" - - def eval(self, expr, evaluation): - "NumericQ[expr_]" - return from_bool(expr.is_numeric(evaluation)) - - class Precision(Builtin): """ @@ -1021,8 +859,8 @@ class Precision(Builtin):
'Precision[$expr$]'
examines the number of significant digits of $expr$.
- - This is rather a proof-of-concept than a full implementation. + Note that the result could be slightly different than the obtained \ + in WMA, due to differencs in the internal representation of the real numbers. The precision of an exact number, e.g. an Integer, is 'Infinity': @@ -1048,43 +886,39 @@ class Precision(Builtin): >> Precision[{{1, 1.`},{1.`5, 1.`10}}] = 5. + For non-zero Real values, it holds in general: + + 'Accuracy'[$z$] == 'Precision'[$z$] + 'Log'[$z$] + + >> (Accuracy[z] == Precision[z] + Log[z])/.z-> 37.` + = True + + The case of `0.` values is special. Following WMA, in a Machine Real\ + representation, the precision is set to 'MachinePrecision': + >> Precision[0.] + = MachinePrecision + + On the other hand, for a Precision Real with fixed accuracy,\ + the precision is evaluated to 0.: + >> Precision[0.``3] + = 0. + See also :'Accuracy': /doc/reference-of-built-in-symbols/atomic-elements-of-expressions/representation-of-numbers/accuracy/. """ - rules = { - "Precision[z_?MachineNumberQ]": "MachinePrecision", - } - summary_text = "find the precision of a number" def eval(self, z, evaluation): "Precision[z_]" - if isinstance(z, Real): - if z.is_zero: - return MachineReal0 - return MachineReal(dps(z.get_precision())) - - if isinstance(z, Complex): - prec_real = self.eval(z.real, evaluation) - prec_imag = self.eval(z.imag, evaluation) - if prec_real is SymbolInfinity: - return prec_imag - if prec_imag is SymbolInfinity: - return prec_real - - return Real(min(prec_real.to_python(), prec_imag.to_python())) - - if isinstance(z, Expression): - result = None - for element in z.elements: - candidate = self.eval(element, evaluation) - if isinstance(candidate, Real): - candidate_f = candidate.to_python() - if result is None or candidate_f < result: - result = candidate_f - if result is not None: - return Real(result) - return SymbolInfinity + if isinstance(z, MachineReal): + return SymbolMachinePrecision + + prec = eval_Precision(z) + if prec is None: + return SymbolInfinity + if prec == MACHINE_PRECISION_VALUE: + return SymbolMachinePrecision + return MachineReal(prec) diff --git a/mathics/builtin/atomic/strings.py b/mathics/builtin/atomic/strings.py index de4aa411e..2283308e0 100644 --- a/mathics/builtin/atomic/strings.py +++ b/mathics/builtin/atomic/strings.py @@ -19,12 +19,12 @@ from mathics.core.convert.python import from_bool from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression +from mathics.core.expression_predefined import MATHICS3_INFINITY from mathics.core.list import ListExpression from mathics.core.parser import MathicsFileLineFeeder, parse from mathics.core.symbols import Symbol, SymbolTrue from mathics.core.systemsymbols import ( SymbolBlank, - SymbolDirectedInfinity, SymbolFailed, SymbolInputForm, SymbolOutputForm, @@ -155,7 +155,8 @@ def _pattern_search(name, string, patt, evaluation, options, matched): for p in patts: py_p = to_regex(p, evaluation) if py_p is None: - return evaluation.message("StringExpression", "invld", p, patt) + evaluation.message("StringExpression", "invld", p, patt) + return re_patts.append(py_p) flags = re.MULTILINE @@ -171,16 +172,18 @@ def _search(patts, str, flags, matched): if string.has_form("List", None): py_s = [s.get_string_value() for s in string.elements] if any(s is None for s in py_s): - return evaluation.message( + evaluation.message( name, "strse", Integer1, Expression(Symbol(name), string, patt) ) + return return to_mathics_list(*[_search(re_patts, s, flags, matched) for s in py_s]) else: py_s = string.get_string_value() if py_s is None: - return evaluation.message( + evaluation.message( name, "strse", Integer1, Expression(Symbol(name), string, patt) ) + return return _search(re_patts, py_s, flags, matched) @@ -548,14 +551,14 @@ class InterpretedBox(PrefixOperator): precedence = 670 summary_text = "interpret boxes as an expression" - def eval_dummy(self, boxes, evaluation: Evaluation): + def eval(self, boxes, evaluation: Evaluation): """InterpretedBox[boxes_]""" # TODO: the following is a very raw and dummy way to # handle these expressions. # In the first place, this should handle different kind # of boxes in different ways. reinput = boxes.boxes_to_text() - return Expression(SymbolToExpression, reinput).evaluate(evaluation) + return Expression(SymbolToExpression, String(reinput)).evaluate(evaluation) class LetterNumber(Builtin): @@ -646,10 +649,11 @@ def eval_alpha_str(self, chars: List[Any], alpha: String, evaluation): elif chars.has_form("List", 1, None): result = [] for element in chars.elements: - result.append(self.apply_alpha_str(element, alpha, evaluation)) + result.append(self.eval_alpha_str(element, alpha, evaluation)) return ListExpression(*result) else: - return evaluation.message(self.__class__.__name__, "nas", chars) + evaluation.message(self.__class__.__name__, "nas", chars) + return return None def eval(self, chars: List[Any], evaluation): @@ -673,7 +677,7 @@ def eval(self, chars: List[Any], evaluation): result.append(self.eval(element, evaluation)) return ListExpression(*result) else: - return evaluation.message(self.__class__.__name__, "nas", chars) + evaluation.message(self.__class__.__name__, "nas", chars) return None @@ -757,29 +761,34 @@ def _apply(self, string, rule, n, evaluation, options, cases): if string.has_form("List", None): py_strings = [stri.get_string_value() for stri in string.elements] if None in py_strings: - return evaluation.message(self.get_name(), "strse", Integer1, expr) + evaluation.message(self.get_name(), "strse", Integer1, expr) + return else: py_strings = string.get_string_value() if py_strings is None: - return evaluation.message(self.get_name(), "strse", Integer1, expr) + evaluation.message(self.get_name(), "strse", Integer1, expr) + return # convert rule def convert_rule(r): if r.has_form("Rule", None) and len(r.elements) == 2: py_s = to_regex(r.elements[0], evaluation) if py_s is None: - return evaluation.message( + evaluation.message( "StringExpression", "invld", r.elements[0], r.elements[0] ) + return py_sp = r.elements[1] return py_s, py_sp elif cases: py_s = to_regex(r, evaluation) if py_s is None: - return evaluation.message("StringExpression", "invld", r, r) + evaluation.message("StringExpression", "invld", r, r) + return return py_s, None - return evaluation.message(self.get_name(), "srep", r) + evaluation.message(self.get_name(), "srep", r) + return if rule.has_form("List", None): py_rules = [convert_rule(r) for r in rule.elements] @@ -791,12 +800,13 @@ def convert_rule(r): # convert n if n is None: py_n = 0 - elif n == Expression(SymbolDirectedInfinity, Integer1): + elif n.sameQ(MATHICS3_INFINITY): py_n = 0 else: py_n = n.get_int_value() if py_n is None or py_n < 0: - return evaluation.message(self.get_name(), "innf", Integer(3), expr) + evaluation.message(self.get_name(), "innf", Integer(3), expr) + return # flags flags = re.MULTILINE @@ -937,7 +947,7 @@ class StringContainsQ(Builtin): summary_text = "test whether a pattern matches with a substring" - def eval(self, string, patt, evaluation, options): + def eval(self, string, patt, evaluation: Evaluation, options: dict): "StringContainsQ[string_, patt_, OptionsPattern[%(name)s]]" return _pattern_search( self.__class__.__name__, string, patt, evaluation, options, True @@ -964,7 +974,7 @@ class StringQ(Test): summary_text = "test whether an expression is a string" - def test(self, expr): + def test(self, expr) -> bool: return isinstance(expr, String) @@ -1033,7 +1043,9 @@ class SystemCharacterEncoding(Predefined):
$SystemCharacterEncoding
gives the default character encoding of the system. - On startup, the value of environment variable 'MATHICS_CHARACTER_ENCODING' sets this value. However if that evironment varaible is not set, set the value is set in Python using 'sys.getdefaultencoding()'. + On startup, the value of environment variable 'MATHICS_CHARACTER_ENCODING' \ + sets this value. However if that environment variable is not set, set the value \ + is set in Python using 'sys.getdefaultencoding()'.
>> $SystemCharacterEncoding @@ -1056,7 +1068,7 @@ class ToExpression(Builtin): https://reference.wolfram.com/language/ref/ToExpression.html
'ToExpression[$input$]' -
inteprets a given string as Mathics input. +
interprets a given string as Mathics input.
'ToExpression[$input$, $form$]'
reads the given input in the specified $form$. @@ -1221,11 +1233,11 @@ class ToString(Builtin): summary_text = "format an expression and produce a string" - def eval_default(self, value, evaluation, options): + def eval_default(self, value, evaluation: Evaluation, options: dict): "ToString[value_, OptionsPattern[ToString]]" return self.eval_form(value, SymbolOutputForm, evaluation, options) - def eval_form(self, expr, form, evaluation, options): + def eval_form(self, expr, form, evaluation: Evaluation, options: dict): "ToString[expr_, form_, OptionsPattern[ToString]]" encoding = options["System`CharacterEncoding"] return eval_ToString(expr, form, encoding.value, evaluation) diff --git a/mathics/builtin/atomic/symbols.py b/mathics/builtin/atomic/symbols.py index 01f1ac0ce..bae882941 100644 --- a/mathics/builtin/atomic/symbols.py +++ b/mathics/builtin/atomic/symbols.py @@ -22,6 +22,7 @@ attributes_bitset_to_list, ) from mathics.core.convert.expression import to_mathics_list +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.rules import Rule @@ -41,6 +42,7 @@ SymbolGrid, SymbolInfix, SymbolInputForm, + SymbolLeft, SymbolOptions, SymbolRule, SymbolSet, @@ -342,19 +344,18 @@ def rhs(expr): ), ) ) - if grid: - if lines: + if lines: + if grid: return Expression( SymbolGrid, ListExpression(*(ListExpression(line) for line in lines)), - Expression(SymbolRule, Symbol("ColumnAlignments"), Symbol("Left")), + Expression(SymbolRule, Symbol("ColumnAlignments"), SymbolLeft), ) else: - return SymbolNull - else: - for line in lines: - evaluation.print_out(Expression(SymbolInputForm, line)) - return SymbolNull + for line in lines: + evaluation.print_out(Expression(SymbolInputForm, line)) + + return SymbolNull def format_definition_input(self, symbol, evaluation): "InputForm: Definition[symbol_]" @@ -482,7 +483,7 @@ def format_definition(self, symbol, evaluation, options, grid=True): infoshow = Expression( SymbolGrid, ListExpression(*(to_mathics_list(line) for line in lines)), - Expression(SymbolRule, Symbol("ColumnAlignments"), Symbol("Left")), + Expression(SymbolRule, Symbol("ColumnAlignments"), SymbolLeft), ) evaluation.print_out(infoshow) else: @@ -584,7 +585,7 @@ def rhs(expr): ) return - def format_definition_input(self, symbol, evaluation, options): + def format_definition_input(self, symbol, evaluation: Evaluation, options: dict): "InputForm: Information[symbol_, OptionsPattern[Information]]" self.format_definition(symbol, evaluation, options, grid=False) ret = SymbolNull @@ -767,7 +768,7 @@ class SymbolQ(Test): summary_text = "test whether is a symbol" - def test(self, expr): + def test(self, expr) -> bool: return isinstance(expr, Symbol) diff --git a/mathics/builtin/attributes.py b/mathics/builtin/attributes.py index e1b6ec8fc..4163cd372 100644 --- a/mathics/builtin/attributes.py +++ b/mathics/builtin/attributes.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Attributes of Definitions +Definition Attributes While a definition like 'cube[$x_$] = $x$^3' gives a way to specify \ values of a function, attributes allow a way to \ @@ -13,6 +13,9 @@ However in contrast to \Mathematica, you can set any symbol as an attribute. """ +# This tells documentation how to sort this module +sort_order = "mathics.builtin.definition-attributes" + from mathics.builtin.base import Builtin, Predefined from mathics.core.assignment import get_symbol_list diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 7475c8e1e..6b0e3fd7c 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -9,20 +9,50 @@ import sympy -from mathics.core.atoms import Integer, MachineReal, PrecisionReal, String -from mathics.core.attributes import A_NO_ATTRIBUTES, A_PROTECTED +from mathics.core.atoms import ( + Integer, + Integer0, + Integer1, + MachineReal, + Number, + PrecisionReal, + String, +) +from mathics.core.attributes import A_HOLD_ALL, A_NO_ATTRIBUTES, A_PROTECTED from mathics.core.convert.expression import to_expression, to_numeric_sympy_args from mathics.core.convert.op import ascii_operator_to_symbol from mathics.core.convert.python import from_bool from mathics.core.convert.sympy import from_sympy from mathics.core.definitions import Definition +from mathics.core.evaluation import Evaluation from mathics.core.exceptions import MessageException 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.parser.util import PyMathicsDefinitions, SystemDefinitions from mathics.core.rules import BuiltinRule, Pattern, Rule -from mathics.core.symbols import BaseElement, Symbol, ensure_context, strip_context -from mathics.core.systemsymbols import SymbolMessageName, SymbolRule +from mathics.core.symbols import ( + BaseElement, + BooleanType, + Symbol, + SymbolFalse, + SymbolPlus, + SymbolTrue, + ensure_context, + strip_context, +) +from mathics.core.systemsymbols import ( + SymbolGreaterEqual, + SymbolLess, + SymbolLessEqual, + SymbolMessageName, + SymbolRule, + SymbolSequence, +) +from mathics.eval.numbers import cancel +from mathics.eval.numerify import numerify +from mathics.eval.scoping import dynamic_scoping # Signals to Mathics doc processing not to include this module in its documentation. no_doc = True @@ -37,8 +67,10 @@ def check_requires_list(requires: list) -> bool: 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 @@ -106,7 +138,7 @@ def eval(x, evaluation): For rules including ``OptionsPattern`` ``` - def eval_with_options(x, evaluation, options): + def eval_with_options(x, evaluation: Evaluation, options: dict): '''F[x_Real, OptionsPattern[]]''' ... ``` @@ -377,7 +409,7 @@ def get_operator(self) -> Optional[str]: def get_operator_display(self) -> Optional[str]: return None - def get_functions(self, prefix="apply", is_pymodule=False): + def get_functions(self, prefix="eval", is_pymodule=False): from mathics.core.parser import parse_builtin_rule unavailable_function = self._get_unavailable_function() @@ -421,7 +453,7 @@ def _get_unavailable_function(self) -> Optional[Callable]: of the class. Otherwise, returns ``None``. """ - def apply_unavailable(**kwargs): # will override apply method + def eval_unavailable(**kwargs): # will override apply method kwargs["evaluation"].message( "General", "pyimport", # see inout.py @@ -429,7 +461,7 @@ def apply_unavailable(**kwargs): # will override apply method ) requires = getattr(self, "requires", []) - return None if check_requires_list(requires) else apply_unavailable + return None if check_requires_list(requires) else eval_unavailable def get_option_string(self, *params): s = self.get_option(*params) @@ -496,6 +528,205 @@ def get_name(self, short=False) -> str: return re.sub(r"Atom$", "", name) +class IterationFunction(Builtin): + attributes = A_HOLD_ALL | A_PROTECTED + allow_loopcontrol = False + throw_iterb = True + + def get_result(self, items): + pass + + def eval_symbol(self, expr, iterator, evaluation): + "%(name)s[expr_, iterator_Symbol]" + iterator = iterator.evaluate(evaluation) + if iterator.has_form(["List", "Range", "Sequence"], None): + elements = iterator.elements + if len(elements) == 1: + return self.eval_max(expr, *elements, evaluation) + elif len(elements) == 2: + if elements[1].has_form(["List", "Sequence"], None): + seq = Expression(SymbolSequence, *(elements[1].elements)) + return self.eval_list(expr, elements[0], seq, evaluation) + else: + return self.eval_range(expr, *elements, evaluation) + elif len(elements) == 3: + return self.eval_iter_nostep(expr, *elements, evaluation) + elif len(elements) == 4: + return self.eval_iter(expr, *elements, evaluation) + + if self.throw_iterb: + evaluation.message(self.get_name(), "iterb") + return + + def eval_range(self, expr, i, imax, evaluation): + "%(name)s[expr_, {i_Symbol, imax_}]" + imax = imax.evaluate(evaluation) + if imax.has_form("Range", None): + # FIXME: this should work as an iterator in Python3, not + # building the sequence explicitly... + seq = Expression(SymbolSequence, *(imax.evaluate(evaluation).elements)) + return self.eval_list(expr, i, seq, evaluation) + elif imax.has_form("List", None): + seq = Expression(SymbolSequence, *(imax.elements)) + return self.eval_list(expr, i, seq, evaluation) + else: + return self.eval_iter(expr, i, Integer1, imax, Integer1, evaluation) + + def eval_max(self, expr, imax, evaluation): + "%(name)s[expr_, {imax_}]" + + # Even though `imax` should be an integeral value, its type does not + # have to be an Integer. + + result = [] + + def do_iteration(): + evaluation.check_stopped() + try: + result.append(expr.evaluate(evaluation)) + except ContinueInterrupt: + if self.allow_loopcontrol: + pass + else: + raise + except BreakInterrupt: + if self.allow_loopcontrol: + raise StopIteration + else: + raise + except ReturnInterrupt as e: + if self.allow_loopcontrol: + return e.expr + else: + raise + + if isinstance(imax, Integer): + try: + for _ in range(imax.value): + do_iteration() + except StopIteration: + pass + + else: + imax = imax.evaluate(evaluation) + imax = numerify(imax, evaluation) + if isinstance(imax, Number): + imax = imax.round() + py_max = imax.get_float_value() + if py_max is None: + if self.throw_iterb: + evaluation.message(self.get_name(), "iterb") + return + + index = 0 + try: + while index < py_max: + do_iteration() + index += 1 + except StopIteration: + pass + + return self.get_result(result) + + def eval_iter_nostep(self, expr, i, imin, imax, evaluation): + "%(name)s[expr_, {i_Symbol, imin_, imax_}]" + return self.eval_iter(expr, i, imin, imax, Integer1, evaluation) + + def eval_iter(self, expr, i, imin, imax, di, evaluation): + "%(name)s[expr_, {i_Symbol, imin_, imax_, di_}]" + + if isinstance(self, SympyFunction) and di.get_int_value() == 1: + whole_expr = to_expression( + self.get_name(), expr, ListExpression(i, imin, imax) + ) + sympy_expr = whole_expr.to_sympy(evaluation=evaluation) + if sympy_expr is None: + return None + + # apply Together to produce results similar to Mathematica + result = sympy.together(sympy_expr) + result = from_sympy(result) + result = cancel(result) + + if not result.sameQ(whole_expr): + return result + return + + index = imin.evaluate(evaluation) + imax = imax.evaluate(evaluation) + di = di.evaluate(evaluation) + + result = [] + compare_type = ( + SymbolGreaterEqual + if Expression(SymbolLess, di, Integer0).evaluate(evaluation).to_python() + else SymbolLessEqual + ) + while True: + cont = Expression(compare_type, index, imax).evaluate(evaluation) + if cont is SymbolFalse: + break + if cont is not SymbolTrue: + if self.throw_iterb: + evaluation.message(self.get_name(), "iterb") + return + + evaluation.check_stopped() + try: + item = dynamic_scoping(expr.evaluate, {i.name: index}, evaluation) + result.append(item) + except ContinueInterrupt: + if self.allow_loopcontrol: + pass + else: + raise + except BreakInterrupt: + if self.allow_loopcontrol: + break + else: + raise + except ReturnInterrupt as e: + if self.allow_loopcontrol: + return e.expr + else: + raise + index = Expression(SymbolPlus, index, di).evaluate(evaluation) + return self.get_result(result) + + def eval_list(self, expr, i, items, evaluation): + "%(name)s[expr_, {i_Symbol, {items___}}]" + items = items.evaluate(evaluation).get_sequence() + result = [] + for item in items: + evaluation.check_stopped() + try: + item = dynamic_scoping(expr.evaluate, {i.name: item}, evaluation) + result.append(item) + except ContinueInterrupt: + if self.allow_loopcontrol: + pass + else: + raise + except BreakInterrupt: + if self.allow_loopcontrol: + break + else: + raise + except ReturnInterrupt as e: + if self.allow_loopcontrol: + return e.expr + else: + raise + return self.get_result(result) + + def eval_multi(self, expr, first, sequ, evaluation): + "%(name)s[expr_, first_, sequ__]" + + sequ = sequ.get_sequence() + name = self.get_name() + return to_expression(name, to_expression(name, expr, *sequ), first) + + class Operator(Builtin): operator: Optional[str] = None precedence: Optional[int] = None @@ -515,9 +746,9 @@ def get_operator_display(self) -> Optional[str]: class Predefined(Builtin): - def get_functions(self, prefix="apply", is_pymodule=False) -> List[Callable]: + def get_functions(self, prefix="eval", is_pymodule=False) -> List[Callable]: functions = list(super().get_functions(prefix)) - if prefix in ("apply", "eval") and hasattr(self, "evaluate"): + if prefix == "eval" and hasattr(self, "evaluate"): functions.append((Symbol(self.get_name()), self.evaluate)) return functions @@ -613,11 +844,18 @@ def __init__(self, *args, **kwargs): class Test(Builtin): - def eval(self, expr, evaluation) -> Optional[Symbol]: - "%(name)s[expr_]" + def eval(self, expr, evaluation) -> Optional[BooleanType]: + # Note: in the docstring below, we need to use %(name)s for + # subclasses like ExactNumberQ to work with function-application + # pattern matching. + """%(name)s[expr_]""" test_expr = self.test(expr) return None if test_expr is None else from_bool(bool(test_expr)) + def test(self, expr) -> bool: + """Subclasses of test must implement a boolean test function""" + raise NotImplementedError + @lru_cache() def run_sympy(sympy_fn: Callable, *sympy_args) -> Any: @@ -642,7 +880,7 @@ def eval(self, z, evaluation): sympy_fn = getattr(sympy, self.sympy_name) try: return from_sympy(run_sympy(sympy_fn, *sympy_args)) - except: + except Exception: return def get_constant(self, precision, evaluation, have_mpmath=False): @@ -703,14 +941,16 @@ class PatternObject(BuiltinElement, Pattern): arg_counts: List[int] = [] - def init(self, expr): - super().init(expr) + def init(self, expr, evaluation: Optional[Evaluation] = None): + super().init(expr, evaluation=evaluation) if self.arg_counts is not None: if len(expr.elements) not in self.arg_counts: self.error_args(len(expr.elements), *self.arg_counts) self.expr = expr - self.head = Pattern.create(expr.head) - self.elements = [Pattern.create(element) for element in expr.elements] + self.head = Pattern.create(expr.head, evaluation=evaluation) + self.elements = [ + Pattern.create(element, evaluation=evaluation) for element in expr.elements + ] def error(self, tag, *args): raise PatternError(self.get_name(), tag, *args) diff --git a/mathics/builtin/binary/bytearray.py b/mathics/builtin/binary/bytearray.py index cce80a221..940962f94 100644 --- a/mathics/builtin/binary/bytearray.py +++ b/mathics/builtin/binary/bytearray.py @@ -35,14 +35,14 @@ class ByteArray(Builtin): >> ByteArray["ARkD"] = ByteArray[<3>] >> B=ByteArray["asy"] - : The first argument in Bytearray[asy] should be a B64 enconded string or a vector of integers. + : The first argument in Bytearray[asy] should be a B64 encoded string or a vector of integers. = $Failed """ messages = { "aotd": "Elements in `1` are inconsistent with type Byte", "lend": "The first argument in Bytearray[`1`] should " - + "be a B64 enconded string or a vector of integers.", + + "be a B64 encoded string or a vector of integers.", } summary_text = "array of bytes" diff --git a/mathics/builtin/binary/io.py b/mathics/builtin/binary/io.py index 9dcd5fb41..d4fe8c7a2 100644 --- a/mathics/builtin/binary/io.py +++ b/mathics/builtin/binary/io.py @@ -15,15 +15,16 @@ from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.convert.mpmath import from_mpmath from mathics.core.expression import Expression -from mathics.core.number import dps +from mathics.core.expression_predefined import ( + MATHICS3_I_INFINITY, + MATHICS3_I_NEG_INFINITY, + MATHICS3_INFINITY, + MATHICS3_NEG_INFINITY, +) from mathics.core.read import SymbolEndOfFile from mathics.core.streams import stream_manager from mathics.core.symbols import Symbol -from mathics.core.systemsymbols import ( - SymbolComplex, - SymbolDirectedInfinity, - SymbolIndeterminate, -) +from mathics.core.systemsymbols import SymbolIndeterminate from mathics.eval.nevaluator import eval_N SymbolBinaryWrite = Symbol("BinaryWrite") @@ -39,7 +40,7 @@ def _IEEE_real(real): if math.isnan(real): return SymbolIndeterminate elif math.isinf(real): - return Expression(SymbolDirectedInfinity, Integer((-1) ** (real < 0))) + return MATHICS3_NEG_INFINITY if real < 0 else MATHICS3_INFINITY else: return Real(real) @@ -47,19 +48,13 @@ def _IEEE_real(real): def _IEEE_cmplx(real, imag): if math.isnan(real) or math.isnan(imag): return SymbolIndeterminate - elif math.isinf(real) or math.isinf(imag): - if math.isinf(real) and math.isinf(imag): + if math.isinf(real): + if math.isinf(imag): return SymbolIndeterminate - return Expression( - SymbolDirectedInfinity, - to_expression( - SymbolComplex, - (-1) ** (real < 0) if math.isinf(real) else 0, - (-1) ** (imag < 0) if math.isinf(imag) else 0, - ), - ) - else: - return Complex(MachineReal(real), MachineReal(imag)) + return MATHICS3_NEG_INFINITY if real < 0 else MATHICS3_INFINITY + if math.isinf(imag): + return MATHICS3_I_NEG_INFINITY if imag < 0 else MATHICS3_I_INFINITY + return Complex(MachineReal(real), MachineReal(imag)) @classmethod def get_readers(cls): @@ -173,7 +168,7 @@ def _Real128_reader(s): return Real(sympy.Float(0, 4965)) elif expbits == 0x7FFF: if fracbits == 0: - return Expression(SymbolDirectedInfinity, Integer((-1) ** signbit)) + return MATHICS3_NEG_INFINITY if signbit else MATHICS3_INFINITY else: return SymbolIndeterminate @@ -193,7 +188,7 @@ def _Real128_reader(s): else: result = mpmath.fdiv(core, 2**-exp) - return from_mpmath(result, dps(112)) + return from_mpmath(result, precision=112) @staticmethod def _TerminatedString_reader(s): @@ -1016,12 +1011,14 @@ def eval(self, name, n, b, typ, evaluation): x_py = x.get_int_value() if x_py is None: - return evaluation.message(SymbolBinaryWrite, "nocoerce", b) + evaluation.message(SymbolBinaryWrite, "nocoerce", b) + return try: self.writers[t](stream.io, x_py) except struct.error: - return evaluation.message(SymbolBinaryWrite, "nocoerce", b) + evaluation.message(SymbolBinaryWrite, "nocoerce", b) + return i += 1 try: diff --git a/mathics/builtin/box/__init__.py b/mathics/builtin/box/__init__.py index 70be6070f..fcff563f7 100644 --- a/mathics/builtin/box/__init__.py +++ b/mathics/builtin/box/__init__.py @@ -1,8 +1,10 @@ """ Boxing modules. -Boxes are added in formatting Mathics Expressions. +Boxes are added in formatting \Mathics Expressions. -Boxing information like width and size makes it easier for formatters to do +Boxing information like bounding-box width and size makes it easier for formatters to do layout without having to know the intricacies of what is inside the box. """ +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True diff --git a/mathics/builtin/box/compilation.py b/mathics/builtin/box/compilation.py index 5fb7f515c..8fc6a82de 100644 --- a/mathics/builtin/box/compilation.py +++ b/mathics/builtin/box/compilation.py @@ -1,4 +1,9 @@ # -*- coding: utf-8 -*- +""" +Boxing Symbols for compiled code +""" +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True from mathics.builtin.box.expression import BoxExpression @@ -6,16 +11,12 @@ class CompiledCodeBox(BoxExpression): """
-
'CompiledCodeBox[...]' -
holds the compiled code generated by 'Compile'. +
'CompiledCodeBox' +
is the symbol used in boxing 'CompiledCode' expression.
- - Routines which get called when Boxing (adding formatting and bounding-box information) - to CompiledCode. - """ - # summary_text = "box representation of a compiled code" + summary_text = "symbol used in boxing 'CompiledCode' expressions" def boxes_to_text(self, elements=None, **options): if elements is None: diff --git a/mathics/builtin/box/expression.py b/mathics/builtin/box/expression.py index 6b73a00a5..51fa69d25 100644 --- a/mathics/builtin/box/expression.py +++ b/mathics/builtin/box/expression.py @@ -1,3 +1,6 @@ +# This is never intended to go in Mathics3 docs +no_doc = True + from mathics.builtin.base import BuiltinElement from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED from mathics.core.element import BoxElementMixin diff --git a/mathics/builtin/box/graphics.py b/mathics/builtin/box/graphics.py index f36ea2620..b52d03d15 100644 --- a/mathics/builtin/box/graphics.py +++ b/mathics/builtin/box/graphics.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- """ -Boxing Routines for 2D Graphics +Boxing Symbols for 2D Graphics """ +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True from math import atan2, ceil, cos, degrees, floor, log10, pi, sin @@ -138,11 +140,13 @@ def _arc_params(self): class ArrowBox(_Polyline): """
-
'ArrowBox[...]' -
is a box structure for 'Arrow' elements. +
'ArrowBox' +
is the symbol used in boxing 'Arrow' expressions.
""" + summary_text = "symbol used in boxing 'Arrow' expressions" + def init(self, graphics, style, item=None): if not item: raise BoxExpressionError @@ -338,11 +342,13 @@ def default_arrow(px, py, vx, vy, t1, s): class BezierCurveBox(_Polyline): """
-
'BezierCurveBox[...]' -
is a box structure for a 'BezierCurve' element. +
'BezierCurveBox' +
is the symbol used in boxing 'BezierCurve' expressions.
""" + summary_text = "symbol used in boxing 'BezierCurve' expressions" + def init(self, graphics, style, item, options): super(BezierCurveBox, self).init(graphics, item, style) if len(item.elements) != 1 or item.elements[0].get_head_name() != "System`List": @@ -360,40 +366,38 @@ def init(self, graphics, style, item, options): class CircleBox(_ArcBox): """
-
'CircleBox[...]' -
box structure for a 'Circle' element. +
'CircleBox' +
is the symbol used in boxing 'Circle' expressions.
""" face_element = False - summary_text = "internal box representation for 'Circle' elements" + summary_text = "is the symbol used in boxing 'Circle' expressions" class DiskBox(_ArcBox): """
-
'DiskBox[...]' -
box structure for a 'Disk' element. +
'DiskBox' +
is the symbol used in boxing 'Disk' expressions.
""" face_element = True - summary_text = "internal box representation for 'Disk' elements" + summary_text = "symbol used in boxing 'Disk' expressions" class GraphicsBox(BoxExpression): """
-
'GraphicsBox[...]' -
box structure holding a 'Graphics' object. +
'GraphicsBox' +
is the symbol used in boxing 'Graphics'.
- - Boxing method which get called when Boxing (adding formatting and bounding-box information) - Graphics. """ attributes = A_HOLD_ALL | A_PROTECTED | A_READ_PROTECTED options = Graphics.options + summary_text = "symbol used in boxing 'Graphics'" def __new__(cls, *elements, **kwargs): instance = super().__new__(cls, *elements, **kwargs) @@ -692,26 +696,40 @@ def boxes_to_svg(self, elements=None, **options) -> str: svg_body = format_fn(self, elements, data=data, **options) return svg_body - def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax): + def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax) -> tuple: + + # Note that Asymptote has special commands for drawing axes, like "xaxis" + # "yaxis", "xtick" "labelx", "labely". Entend our language + # here and use those in render-like routines. + use_log_for_y_axis = graphics_options.get("System`LogPlot", False) - axes = graphics_options.get("System`Axes") - if axes is SymbolTrue: + axes_option = graphics_options.get("System`Axes") + + if axes_option is SymbolTrue: axes = (True, True) - elif axes.has_form("List", 2): - axes = (axes.elements[0] is SymbolTrue, axes.elements[1] is SymbolTrue) + elif axes_option.has_form("List", 2): + axes = ( + axes_option.elements[0] is SymbolTrue, + axes_option.elements[1] is SymbolTrue, + ) else: axes = (False, False) - ticks_style = graphics_options.get("System`TicksStyle") - axes_style = graphics_options.get("System`AxesStyle") + + # The Style option pushes its setting down into graphics components + # like ticks, axes, and labels. + ticks_style_option = graphics_options.get("System`TicksStyle") + axes_style_option = graphics_options.get("System`AxesStyle") label_style = graphics_options.get("System`LabelStyle") - if ticks_style.has_form("List", 2): - ticks_style = ticks_style.elements + + if ticks_style_option.has_form("List", 2): + ticks_style = ticks_style_option.elements else: - ticks_style = [ticks_style] * 2 - if axes_style.has_form("List", 2): - axes_style = axes_style.elements + ticks_style = [ticks_style_option] * 2 + + if axes_style_option.has_form("List", 2): + axes_style = axes_style_option.elements else: - axes_style = [axes_style] * 2 + axes_style = [axes_style_option] * 2 ticks_style = [elements.create_style(s) for s in ticks_style] axes_style = [elements.create_style(s) for s in axes_style] @@ -723,12 +741,16 @@ def add_element(element): element.is_completely_visible = True elements.elements.append(element) + # Units seem to be in point size units + ticks_x, ticks_x_small, origin_x = self.axis_ticks(xmin, xmax) ticks_y, ticks_y_small, origin_y = self.axis_ticks(ymin, ymax) axes_extra = 6 + tick_small_size = 3 tick_large_size = 5 + tick_label_d = 2 ticks_x_int = all(floor(x) == x for x in ticks_x) @@ -791,8 +813,10 @@ def add_element(element): ) ) ticks_lines = [] + tick_label_style = ticks_style[index].clone() tick_label_style.extend(label_style) + for x in ticks: ticks_lines.append( [ @@ -816,6 +840,7 @@ def add_element(element): content = String( "%g" % tick_value ) # fix e.g. 0.6000000000000001 + add_element( InsetBox( elements, @@ -839,38 +864,39 @@ def add_element(element): add_element(LineBox(elements, axes_style[0], lines=ticks_lines)) return axes - """if axes[1]: - add_element(LineBox(elements, axes_style[1], lines=[[Coords(elements, pos=(origin_x,ymin), d=(0,-axes_extra)), - Coords(elements, pos=(origin_x,ymax), d=(0,axes_extra))]])) - ticks = [] - tick_label_style = ticks_style[1].clone() - tick_label_style.extend(label_style) - for k in range(start_k_y, start_k_y+steps_y+1): - if k != origin_k_y: - y = k * step_y - if y > ymax: - break - pos = (origin_x,y) - ticks.append([Coords(elements, pos=pos), - Coords(elements, pos=pos, d=(tick_large_size,0))]) - add_element(InsetBox(elements, tick_label_style, content=Real(y), pos=Coords(elements, pos=pos, - d=(-tick_label_d,0)), opos=(1,0))) - for k in range(start_k_y_small, start_k_y_small+steps_y_small+1): - if k % sub_y != 0: - y = k * step_y_small - if y > ymax: - break - pos = (origin_x,y) - ticks.append([Coords(elements, pos=pos), - Coords(elements, pos=pos, d=(tick_small_size,0))]) - add_element(LineBox(elements, axes_style[1], lines=ticks))""" + # Old code? + # if axes[1]: + # add_element(LineBox(elements, axes_style[1], lines=[[Coords(elements, pos=(origin_x,ymin), d=(0,-axes_extra)), + # Coords(elements, pos=(origin_x,ymax), d=(0,axes_extra))]])) + # ticks = [] + # tick_label_style = ticks_style[1].clone() + # tick_label_style.extend(label_style) + # for k in range(start_k_y, start_k_y+steps_y+1): + # if k != origin_k_y: + # y = k * step_y + # if y > ymax: + # break + # pos = (origin_x,y) + # ticks.append([Coords(elements, pos=pos), + # Coords(elements, pos=pos, d=(tick_large_size,0))]) + # add_element(InsetBox(elements, tick_label_style, content=Real(y), pos=Coords(elements, pos=pos, + # d=(-tick_label_d,0)), opos=(1,0))) + # for k in range(start_k_y_small, start_k_y_small+steps_y_small+1): + # if k % sub_y != 0: + # y = k * step_y_small + # if y > ymax: + # break + # pos = (origin_x,y) + # ticks.append([Coords(elements, pos=pos), + # Coords(elements, pos=pos, d=(tick_small_size,0))]) + # add_element(LineBox(elements, axes_style[1], lines=ticks)) class FilledCurveBox(_GraphicsElementBox): """
-
'FilledCurveBox[...]' -
is a box structure for 'FilledCurve' elements. +
'FilledCurveBox' +
is the symbol used in boxing 'FilledCurve' expressions.
""" @@ -947,6 +973,9 @@ def extent(self): class InsetBox(_GraphicsElementBox): + # We have no documentation for this (yet). + no_doc = True + def init( self, graphics, @@ -1012,7 +1041,14 @@ def extent(self): class LineBox(_Polyline): - # Boxing methods for a list of Line. + """ +
+
'LineBox' +
is the symbol used in boxing 'Line' expressions. +
+ """ + + summary_text = "symbol used in boxing 'Line' expressions" def init(self, graphics, style, item=None, lines=None): super(LineBox, self).init(graphics, item, style) @@ -1032,20 +1068,15 @@ def init(self, graphics, style, item=None, lines=None): class PointBox(_Polyline): """
-
'PointBox'[{$x$, $y$}] -
a box construction representing a point in a Graphic. -
'PointBox'[{$x$, $y$, $z$}] -
represents a point in a Graphic3D. -
'PointBox'[{$p_1$, $p_2$,...}] -
represents a set of points. +
'PointBox'] +
is the symbol used in boxing 'Point' expessions.
- ## Boxing methods for a list of Point. - ## - ## object attributes: - ## edge_color: _ColorObject - ## point_radius: radius of each point + + Options include the edge color and the point radius for each of the points. """ + summary_text = "symbol used in boxing 'Point' expressions" + def init(self, graphics, style, item=None): super(PointBox, self).init(graphics, item, style) self.edge_color, self.face_color = style.get_style( @@ -1097,6 +1128,9 @@ def extent(self): class PolygonBox(_Polyline): + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item=None): super(PolygonBox, self).init(graphics, item, style) self.edge_color, self.face_color = style.get_style( @@ -1148,6 +1182,9 @@ def process_option(self, name, value): class RectangleBox(_GraphicsElementBox): + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item): super(RectangleBox, self).init(graphics, item, style) if len(item.elements) not in (1, 2): @@ -1181,6 +1218,9 @@ def extent(self): class RegularPolygonBox(PolygonBox): + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item): if len(item.elements) in (1, 2, 3) and isinstance(item.elements[-1], Integer): r = 1.0 diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index 3b6ffeee4..df8b53c67 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- """ -Boxing Routines for 3D Graphics +Boxing Symbols for 3D Graphics """ +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True import json import numbers @@ -29,18 +31,18 @@ class Graphics3DBox(GraphicsBox): """
-
'Graphics3DBox[{...}]' -
a box structure for Graphics3D elements. +
'Graphics3DBox' +
is the symbol used in boxing 'Graphics3D' expressions.
- Routines which get called when Boxing (adding formatting and bounding-box information) - a Graphics3D object. """ - def _prepare_elements(self, leaves, options, max_width=None): - if not leaves: + summary_text = "symbol used boxing Graphics3D expresssions" + + def _prepare_elements(self, elements, options, max_width=None): + if not elements: raise BoxExpressionError - self.graphics_options = self.get_option_values(leaves[1:], **options) + self.graphics_options = self.get_option_values(elements[1:], **options) background = self.graphics_options["System`Background"] if ( @@ -224,7 +226,7 @@ def _prepare_elements(self, leaves, options, max_width=None): if not isinstance(plot_range, list) or len(plot_range) != 3: raise BoxExpressionError - elements = Graphics3DElements(leaves[0], evaluation) + elements = Graphics3DElements(elements[0], evaluation) def calc_dimensions(final_pass=True): if "System`Automatic" in plot_range: @@ -475,6 +477,9 @@ def get_boundbox_lines(self, xmin, xmax, ymin, ymax, zmin, zmax): class Arrow3DBox(ArrowBox): + # We have no documentation for this (yet). + no_doc = True + def init(self, *args, **kwargs): super(Arrow3DBox, self).init(*args, **kwargs) @@ -496,6 +501,9 @@ class Cone3DBox(_GraphicsElementBox): # Internal Python class used when Boxing a 'Cone' object. # """ + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item): self.edge_color, self.face_color = style.get_style( _ColorObject, face_element=True @@ -544,6 +552,9 @@ class Cuboid3DBox(_GraphicsElementBox): # Internal Python class used when Boxing a 'Cuboid' object. # """ + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item): self.edge_color, self.face_color = style.get_style( _ColorObject, face_element=True @@ -576,6 +587,9 @@ class Cylinder3DBox(_GraphicsElementBox): # Internal Python class used when Boxing a 'Cylinder' object. # """ + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item): self.edge_color, self.face_color = style.get_style( _ColorObject, face_element=True @@ -622,6 +636,9 @@ def _apply_boxscaling(self, boxscale): class Line3DBox(LineBox): # summary_text = "box representation for a 3D line" + # We have no documentation for this (yet). + no_doc = True + def init(self, *args, **kwargs): super(Line3DBox, self).init(*args, **kwargs) @@ -641,6 +658,9 @@ def _apply_boxscaling(self, boxscale): class Point3DBox(PointBox): # summary_text = "box representation for a 3D point" + # We have no documentation for this (yet). + no_doc = True + def get_default_face_color(self): return RGBColor(components=(0, 0, 0, 1)) @@ -669,6 +689,9 @@ def _apply_boxscaling(self, boxscale): class Polygon3DBox(PolygonBox): # summary_text = "box representation for a 3D polygon" + # We have no documentation for this (yet). + no_doc = True + def init(self, *args, **kwargs): self.vertex_normals = None super(Polygon3DBox, self).init(*args, **kwargs) @@ -693,6 +716,9 @@ def _apply_boxscaling(self, boxscale): class Sphere3DBox(_GraphicsElementBox): # summary_text = "box representation for a sphere" + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item): self.edge_color, self.face_color = style.get_style( _ColorObject, face_element=True @@ -739,6 +765,9 @@ def _apply_boxscaling(self, boxscale): class Tube3DBox(_GraphicsElementBox): # summary_text = "box representation for a tube" + # We have no documentation for this (yet). + no_doc = True + def init(self, graphics, style, item): self.graphics = graphics self.edge_color, self.face_color = style.get_style( diff --git a/mathics/builtin/box/image.py b/mathics/builtin/box/image.py index 186a2716b..e2c5c00c3 100644 --- a/mathics/builtin/box/image.py +++ b/mathics/builtin/box/image.py @@ -1,30 +1,121 @@ # -*- coding: utf-8 -*- +""" +Boxing Symbol for Raster Images +""" +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True + +import base64 +import tempfile +import warnings +from copy import deepcopy +from io import BytesIO +from typing import Tuple + +import PIL.Image from mathics.builtin.box.expression import BoxExpression +from mathics.core.element import BaseElement +from mathics.eval.image import pixels_as_ubyte class ImageBox(BoxExpression): """
-
'ImageBox[...]' -
is a box structure for an image element. +
'ImageBox' +
is the symbol used in boxing 'Image' expressions.
- Routines which get called when Boxing (adding formatting and bounding-box information) - an Image object. + """ - def boxes_to_text(self, elements=None, **options): + summary_text = "symbol used boxing Image expresssions" + + def boxes_to_b64text( + self, elements: Tuple[BaseElement] = None, **options + ) -> Tuple[bytes, Tuple[int, int]]: + """ + Produces a base64 png representation and a tuple with the size of the pillow image + associated to the object. + """ + contents, size = self.boxes_to_png(elements, **options) + encoded = base64.b64encode(contents) + encoded = b"data:image/png;base64," + encoded + return (encoded, size) + + def boxes_to_png(self, elements=None, **options) -> Tuple[bytes, Tuple[int, int]]: + """ + returns a tuple with the set of bytes with a png representation of the image + and the scaled size. + """ + image = self.elements[0] if elements is None else elements[0] + + pixels = pixels_as_ubyte(image.color_convert("RGB", True).pixels) + shape = pixels.shape + + width = shape[1] + height = shape[0] + scaled_width = width + scaled_height = height + + # If the image was created from PIL, use that rather than + # reconstruct it from pixels which we can get wrong. + # In particular getting color-mapping info right can be + # tricky. + if hasattr(image, "pillow"): + pillow = deepcopy(image.pillow) + else: + pixels_format = "RGBA" if len(shape) >= 3 and shape[2] == 4 else "RGB" + pillow = PIL.Image.fromarray(pixels, pixels_format) + + # if the image is very small, scale it up using nearest neighbour. + min_size = 128 + if width < min_size and height < min_size: + scale = min_size / max(width, height) + scaled_width = int(scale * width) + scaled_height = int(scale * height) + pillow = pillow.resize( + (scaled_height, scaled_width), resample=PIL.Image.NEAREST + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + stream = BytesIO() + pillow.save(stream, format="png") + stream.seek(0) + contents = stream.read() + stream.close() + + return (contents, (scaled_width, scaled_height)) + + def boxes_to_text(self, elements=None, **options) -> str: return "-Image-" - def boxes_to_mathml(self, elements=None, **options): - if elements is None: - elements = self._elements + def boxes_to_mathml(self, elements=None, **options) -> str: + encoded, size = self.boxes_to_b64text(elements, **options) + decoded = encoded.decode("utf8") # see https://tools.ietf.org/html/rfc2397 - return '' % ( - elements[0].get_string_value(), - elements[1].get_int_value(), - elements[2].get_int_value(), - ) + return f'' - def boxes_to_tex(self, elements=None, **options): - return "-Image-" + def boxes_to_tex(self, elements=None, **options) -> str: + """ + Store the associated image as a png file and return + a LaTeX command for including it. + """ + + data, size = self.boxes_to_png(elements, **options) + res = 100 # pixels/cm + width_str, height_str = (str(n / res).strip() for n in size) + head = rf"\includegraphics[width={width_str}cm,height={height_str}cm]" + + # This produces a random name, where the png file is going to be stored. + # LaTeX does not have a native way to store an figure embeded in + # the source. + fp = tempfile.NamedTemporaryFile(delete=True, suffix=".png") + path = fp.name + fp.close() + + with open(path, "wb") as imgfile: + imgfile.write(data) + + return head + "{" + format(path) + "}" diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 74b83a2e6..829b929e3 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- - """ -Formatting constructs are represented as a hierarchy of low-level symbolic "boxes". +Formatting constructs are represented as a hierarchy of low-level \ +symbolic "boxes". -The routines here assist in boxing at the bottom of the hierarchy. At the other end, the top level, we have a Notebook which is just a collection of Expressions usually contained in boxes. +The routines here assist in boxing at the bottom of the hierarchy. \ +At the other end, the top level, we have a Notebook which is just a \ +collection of Expressions usually contained in boxes. """ +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True from mathics.builtin.base import Builtin from mathics.builtin.box.expression import BoxExpression @@ -70,7 +74,8 @@ class ButtonBox(BoxExpression): """
'ButtonBox[$boxes$]' -
is a low-level box construct that represents a button in a notebook expression. +
is a low-level box construct that represents a button \ + in a notebook expression.
""" @@ -102,7 +107,7 @@ class FractionBox(BoxExpression): "FractionLine": "Automatic", } - def apply(self, num, den, evaluation, options): + def eval(self, num, den, evaluation: Evaluation, options: dict): """FractionBox[num_, den_, OptionsPattern[]]""" num_box, den_box = ( to_boxes(num, evaluation, options), @@ -147,16 +152,29 @@ class GridBox(BoxExpression): # elements in its evaluated form. def get_array(self, elements, evaluation): - options = self.get_option_values(elements=elements[1:], evaluation=evaluation) if not elements: raise BoxConstructError + + options = self.get_option_values(elements=elements[1:], evaluation=evaluation) expr = elements[0] if not expr.has_form("List", None): if not all(element.has_form("List", None) for element in expr.elements): raise BoxConstructError - items = [element.elements for element in expr.elements] - if not is_constant_list([len(row) for row in items]): - raise BoxConstructError + items = [ + element.elements if element.has_form("List", None) else element + for element in expr.elements + ] + if not is_constant_list([len(row) for row in items if isinstance(row, tuple)]): + max_len = max(len(items) for item in items) + empty_string = String("") + + def complete_rows(row): + if isinstance(row, tuple): + return row + (max_len - len(row)) * (empty_string,) + return row + + items = [complete_rows(row) for row in items] + return items, options @@ -183,11 +201,11 @@ class InterpretationBox(BoxExpression): attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED | A_READ_PROTECTED summary_text = "box associated to an input expression" - def apply_to_expression(boxexpr, form, evaluation): + def eval_to_expression(boxexpr, form, evaluation): """ToExpression[boxexpr_IntepretationBox, form___]""" return boxexpr.elements[1] - def apply_display(boxexpr, evaluation): + def eval_display(boxexpr, evaluation): """DisplayForm[boxexpr_IntepretationBox]""" return boxexpr.elements[0] @@ -209,7 +227,7 @@ class RowBox(BoxExpression): def __repr__(self): return "RowBox[List[" + self.items.__repr__() + "]]" - def apply_list(self, boxes, evaluation): + def eval_list(self, boxes, evaluation): """RowBox[boxes_List]""" boxes = boxes.evaluate(evaluation) items = tuple(to_boxes(b, evaluation) for b in boxes.elements) @@ -305,7 +323,7 @@ class SqrtBox(BoxExpression): "MinSize": "Automatic", } - def apply_index(self, radicand, index, evaluation, options): + def eval_index(self, radicand, index, evaluation: Evaluation, options: dict): """SqrtBox[radicand_, index_, OptionsPattern[]]""" radicand_box, index_box = ( to_boxes(radicand, evaluation, options), @@ -313,7 +331,7 @@ def apply_index(self, radicand, index, evaluation, options): ) return SqrtBox(radicand_box, index_box, **options) - def apply(self, radicand, evaluation, options): + def eval(self, radicand, evaluation: Evaluation, options: dict): """SqrtBox[radicand_, OptionsPattern[]]""" radicand_box = to_boxes(radicand, evaluation, options) return SqrtBox(radicand_box, None, **options) @@ -346,11 +364,11 @@ class StyleBox(BoxExpression): attributes = A_PROTECTED | A_READ_PROTECTED summary_text = "associate boxes with styles" - def apply_options(self, boxes, evaluation, options): + def eval_options(self, boxes, evaluation: Evaluation, options: dict): """StyleBox[boxes_, OptionsPattern[]]""" return StyleBox(boxes, style="", **options) - def apply_style(self, boxes, style, evaluation, options): + def eval_style(self, boxes, style, evaluation: Evaluation, options: dict): """StyleBox[boxes_, style_String, OptionsPattern[]]""" return StyleBox(boxes, style=style, **options) @@ -401,7 +419,7 @@ class SubscriptBox(BoxExpression): "MultilineFunction": "Automatic", } - def apply(self, a, b, evaluation, options): + def eval(self, a, b, evaluation: Evaluation, options: dict): """SubscriptBox[a_, b__, OptionsPattern[]]""" a_box, b_box = ( to_boxes(a, evaluation, options), @@ -439,7 +457,7 @@ class SubsuperscriptBox(BoxExpression): "MultilineFunction": "Automatic", } - def apply(self, a, b, c, evaluation, options): + def eval(self, a, b, c, evaluation: Evaluation, options: dict): """SubsuperscriptBox[a_, b__, c__, OptionsPattern[]]""" a_box, b_box, c_box = ( to_boxes(a, evaluation, options), @@ -481,7 +499,7 @@ class SuperscriptBox(BoxExpression): "MultilineFunction": "Automatic", } - def apply(self, a, b, evaluation, options): + def eval(self, a, b, evaluation: Evaluation, options: dict): """SuperscriptBox[a_, b__, OptionsPattern[]]""" a_box, b_box = ( to_boxes(a, evaluation, options), @@ -505,7 +523,9 @@ def to_expression(self): class TagBox(BoxExpression): """ - :WMA link:https://reference.wolfram.com/language/ref/TagBox.html + + :WMA link: + https://reference.wolfram.com/language/ref/TagBox.html
'TagBox[boxes, tag]' diff --git a/mathics/builtin/box/uniform_polyhedra.py b/mathics/builtin/box/uniform_polyhedra.py index bf5552ca5..31ceb9e39 100644 --- a/mathics/builtin/box/uniform_polyhedra.py +++ b/mathics/builtin/box/uniform_polyhedra.py @@ -1,3 +1,6 @@ +# Docs are not yet ready for prime time. Maybe after release 6.0.0. +no_doc = True + import numbers from mathics.builtin.box.graphics3d import Coords3D diff --git a/mathics/builtin/colors/color_directives.py b/mathics/builtin/colors/color_directives.py index b9fe2c174..516908116 100644 --- a/mathics/builtin/colors/color_directives.py +++ b/mathics/builtin/colors/color_directives.py @@ -1,7 +1,9 @@ """ Color Directives -There are many different way to specify color; we support all of the color formats below and will convert between the different color formats. +There are many different way to specify color, and we support many of these. + +We can convert between the different color formats. """ from math import atan2, cos, exp, pi, radians, sin, sqrt @@ -14,9 +16,9 @@ from mathics.core.convert.python import from_python from mathics.core.element import ImmutableValueMixin from mathics.core.exceptions import BoxExpressionError -from mathics.core.expression import Expression +from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression -from mathics.core.number import machine_epsilon +from mathics.core.number import MACHINE_EPSILON from mathics.core.symbols import Symbol from mathics.core.systemsymbols import SymbolApply @@ -25,7 +27,7 @@ def _cie2000_distance(lab1, lab2): # reference: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 - e = machine_epsilon + e = MACHINE_EPSILON kL = kC = kH = 1 # common values L1, L2 = lab1[0], lab2[0] @@ -83,14 +85,14 @@ def _cie2000_distance(lab1, lab2): ) -def _CMC_distance(lab1, lab2, l, c): +def _CMC_distance(lab1, lab2, ll, c): # reference https://en.wikipedia.org/wiki/Color_difference#CMC_l:c_.281984.29 L1, L2 = lab1[0], lab2[0] a1, a2 = lab1[1], lab2[1] b1, b2 = lab1[2], lab2[2] dL, da, db = L2 - L1, a2 - a1, b2 - b1 - e = machine_epsilon + e = MACHINE_EPSILON C1 = sqrt(a1**2 + b1**2) C2 = sqrt(a2**2 + b2**2) @@ -108,7 +110,7 @@ def _CMC_distance(lab1, lab2, l, c): SL = 0.511 if L1 < 16 else (0.040975 * L1) / (1 + 0.01765 * L1) SC = (0.0638 * C1) / (1 + 0.0131 * C1) + 0.638 SH = SC * (F * T + 1 - F) - return sqrt((dL / (l * SL)) ** 2 + (dC / (c * SC)) ** 2 + dH2 / SH**2) + return sqrt((dL / (ll * SL)) ** 2 + (dC / (c * SC)) ** 2 + dH2 / SH**2) def _component_distance(a, b, i): @@ -119,6 +121,24 @@ def _euclidean_distance(a, b): return sqrt(sum((x1 - x2) * (x1 - x2) for x1, x2 in zip(a, b))) +def color_to_expression(components, colorspace): + if colorspace == "Grayscale": + converted_color_name = "GrayLevel" + elif colorspace == "HSB": + converted_color_name = "Hue" + else: + converted_color_name = colorspace + "Color" + + return to_expression(converted_color_name, *components) + + +def expression_to_color(color): + try: + return _ColorObject.create(color) + except ColorError: + return None + + class _ColorObject(_GraphicsDirective, ImmutableValueMixin): formats = { # we are adding ImageSizeMultipliers in the rule below, because we do _not_ want color boxes to @@ -203,7 +223,9 @@ def to_color_space(self, color_space): class CMYKColor(_ColorObject): """ - :WMA link:https://reference.wolfram.com/language/ref/CMYKColor.html + + :WMA link: + https://reference.wolfram.com/language/ref/CMYKColor.html
'CMYKColor[$c$, $m$, $y$, $k$]' @@ -218,6 +240,7 @@ class CMYKColor(_ColorObject): color_space = "CMYK" components_sizes = [3, 4, 5] default_components = [0, 0, 0, 0, 1] + summary_text = "specify a CMYK color" class ColorDistance(Builtin): @@ -258,7 +281,6 @@ class ColorDistance(Builtin): """ - summary_text = "distance between two colors" options = {"DistanceFunction": "Automatic"} requires = ("numpy",) @@ -269,6 +291,8 @@ class ColorDistance(Builtin): + "two lists of colors of the same length.", } + summary_text = "get distance between two colors" + # If numpy is not installed, 100 * c1.to_color_space returns # a list of 100 x 3 elements, instead of doing elementwise multiplication requires = ("numpy",) @@ -306,7 +330,7 @@ class ColorDistance(Builtin): / 100, } - def apply(self, c1, c2, evaluation, options): + def eval(self, c1, c2, evaluation: Evaluation, options: dict): "ColorDistance[c1_, c2_, OptionsPattern[ColorDistance]]" distance_function = options.get("System`DistanceFunction") @@ -431,7 +455,9 @@ class ColorError(BoxExpressionError): class GrayLevel(_ColorObject): """ - :WMA link:https://reference.wolfram.com/language/ref/GrayLevel.html + + :WMA link: + https://reference.wolfram.com/language/ref/GrayLevel.html
'GrayLevel[$g$]' @@ -446,10 +472,14 @@ class GrayLevel(_ColorObject): components_sizes = [1, 2] default_components = [0, 1] + summary_text = "specify a Grayscale color" + class Hue(_ColorObject): """ - :WMA link:https://reference.wolfram.com/language/ref/Hue.html + + :WMA link: + https://reference.wolfram.com/language/ref/Hue.html
'Hue[$h$, $s$, $l$, $a$]' @@ -476,13 +506,15 @@ class Hue(_ColorObject): components_sizes = [1, 2, 3, 4] default_components = [0, 1, 1, 1] + summary_text = "specify a color with hue, saturation lightness, and opacity" + def hsl_to_rgba(self) -> tuple: - h, s, l = self.components[:3] - if l < 0.5: - q = l * (1 + s) + h, s, li = self.components[:3] + if li < 0.5: + q = li * (1 + s) else: - q = l + s - l * s - p = 2 * l - q + q = li + s - li * s + p = 2 * li - q rgb = (h + 1 / 3, h, h - 1 / 3) @@ -509,7 +541,9 @@ def trans(t): class LABColor(_ColorObject): """ - :WMA link:https://reference.wolfram.com/language/ref/LABColor.html + + :WMA link: + https://reference.wolfram.com/language/ref/LABColor.html
'LABColor[$l$, $a$, $b$]' @@ -522,10 +556,14 @@ class LABColor(_ColorObject): components_sizes = [3, 4] default_components = [0, 0, 0, 1] + summary_text = "specify a LAB color" + class LCHColor(_ColorObject): """ - :WMA link:https://reference.wolfram.com/language/ref/LCHColor.html + + :WMA link: + https://reference.wolfram.com/language/ref/LCHColor.html
'LCHColor[$l$, $c$, $h$]' @@ -538,6 +576,8 @@ class LCHColor(_ColorObject): components_sizes = [3, 4] default_components = [0, 0, 0, 1] + summary_text = "specify a LHC color" + class LUVColor(_ColorObject): """ @@ -553,10 +593,14 @@ class LUVColor(_ColorObject): components_sizes = [3, 4] default_components = [0, 0, 0, 1] + summary_text = "specify a LUV color" + class Opacity(_GraphicsDirective): """ - :WMA link:https://reference.wolfram.com/language/ref/Opacity.html + + :WMA link: + https://reference.wolfram.com/language/ref/Opacity.html
'Opacity[$level$]' @@ -581,7 +625,7 @@ def to_css(self): try: if 0.0 <= self.opacity <= 1.0: return self.opacity - except: + except Exception: pass return None @@ -589,10 +633,14 @@ def to_css(self): def create_as_style(klass, graphics, item): return klass(item) + summary_text = "specify a Opacity level" + class RGBColor(_ColorObject): """ - :WMA link:https://reference.wolfram.com/language/ref/RGBColor.html + + :WMA link: + https://reference.wolfram.com/language/ref/RGBColor.html
'RGBColor[$r$, $g$, $b$]' @@ -617,10 +665,14 @@ class RGBColor(_ColorObject): def to_rgba(self): return self.components + summary_text = "specify an RGB color" + class XYZColor(_ColorObject): """ - :WMA link:https://reference.wolfram.com/language/ref/XYZColor.html + + :WMA link: + https://reference.wolfram.com/language/ref/XYZColor.html
'XYZColor[$x$, $y$, $z$]' @@ -632,20 +684,4 @@ class XYZColor(_ColorObject): components_sizes = [3, 4] default_components = [0, 0, 0, 1] - -def expression_to_color(color): - try: - return _ColorObject.create(color) - except ColorError: - return None - - -def color_to_expression(components, colorspace): - if colorspace == "Grayscale": - converted_color_name = "GrayLevel" - elif colorspace == "HSB": - converted_color_name = "Hue" - else: - converted_color_name = colorspace + "Color" - - return to_expression(converted_color_name, *components) + summary_text = "specify an XYZ color" diff --git a/mathics/builtin/colors/color_internals.py b/mathics/builtin/colors/color_internals.py index 13ac617dd..c5e334d81 100644 --- a/mathics/builtin/colors/color_internals.py +++ b/mathics/builtin/colors/color_internals.py @@ -283,9 +283,9 @@ def luv_to_xyz(cie_l, cie_u, cie_v, *rest): return (x, y, z) + rest -def lch_to_lab(l, c, h, *rest): +def lch_to_lab(ll, c, h, *rest): h *= 2 * pi # MMA specific - return (l, c * cos(h), c * sin(h)) + rest + return (ll, c * cos(h), c * sin(h)) + rest @conditional @@ -296,15 +296,15 @@ def _wrap_lch_h(h, pi2): return h -def lab_to_lch(l, a, b, *rest): +def lab_to_lch(ll, a, b, *rest): h = _wrap_lch_h(arctan2(b, a), 2.0 * pi) h /= 2.0 * pi # MMA specific - return (l, sqrt(a * a + b * b), h) + rest + return (ll, sqrt(a * a + b * b), h) + rest -def lab_to_xyz(l, a, b, *rest): +def lab_to_xyz(ll, a, b, *rest): # see http://www.easyrgb.com/index.php?X=MATH&H=08#text8 - f_y = (l * 100.0 + 16.0) / 116.0 + f_y = (ll * 100.0 + 16.0) / 116.0 x, y, z = a / 5.0 + f_y, f_y, f_y - b / 2.0 x, y, z = map(_scale_lab_to_xyz, (x, y, z)) @@ -326,7 +326,7 @@ def lab_to_xyz(l, a, b, *rest): # s = FindShortestPath[g, All, All]; {#, s @@ #} & /@ Permutations[{ # "Grayscale", "RGB", "CMYK", "HSB", "XYZ", "LAB", "LUV", "LCH"}, {2}] // CForm -_paths = dict( +_PATHS = dict( ( (("Grayscale", "RGB"), ("Grayscale", "RGB")), (("Grayscale", "CMYK"), ("Grayscale", "RGB", "CMYK")), @@ -387,7 +387,7 @@ def lab_to_xyz(l, a, b, *rest): ) ) -conversions = { +CONVERSIONS = { "Grayscale>RGB": grayscale_to_rgb, "RGB>Grayscale": rgb_to_grayscale, "CMYK>RGB": cmyk_to_rgb, @@ -435,12 +435,12 @@ def omit_alpha(*c): if src == dst: return components - path = _paths.get((src, dst), None) + path = _PATHS.get((src, dst), None) if path is None: return None for s, d in zip(path[:-1], path[1:]): - func = conversions.get("%s>%s" % (s, d)) + func = CONVERSIONS.get("%s>%s" % (s, d)) if not func: return None components = stacked(func, components) diff --git a/mathics/builtin/colors/color_operations.py b/mathics/builtin/colors/color_operations.py index 7861fff28..3fced32b8 100644 --- a/mathics/builtin/colors/color_operations.py +++ b/mathics/builtin/colors/color_operations.py @@ -13,8 +13,9 @@ from mathics.builtin.colors.color_directives import ColorError, RGBColor, _ColorObject from mathics.builtin.colors.color_internals import convert_color from mathics.builtin.image.base import Image -from mathics.core.atoms import Integer, MachineReal, Rational, Real +from mathics.core.atoms import Integer, MachineReal, Rational, Real, String from mathics.core.convert.expression import to_expression, to_mathics_list +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol @@ -28,7 +29,8 @@ class Blend(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Blend.html + :WMA link: + https://reference.wolfram.com/language/ref/Blend.html
'Blend[{$c1$, $c2$}]' @@ -99,7 +101,7 @@ def do_blend(self, colors, values): result = [r + p for r, p in zip(result, part)] return type(components=result) - def eval(self, colors, u, evaluation): + def eval(self, colors, u, evaluation: Evaluation): "Blend[{colors___}, u_]" colors_orig = colors @@ -128,7 +130,8 @@ def eval(self, colors, u, evaluation): values = 0.0 use_list = False if values is None: - return evaluation.message("Blend", "argl", u, ListExpression(colors_orig)) + evaluation.message("Blend", "argl", u, ListExpression(colors_orig)) + return if use_list: return self.do_blend(colors, values).to_expr() @@ -171,7 +174,7 @@ class ColorConvert(Builtin): } summary_text = "convert between color models" - def eval(self, input, colorspace, evaluation): + def eval(self, input, colorspace, evaluation: Evaluation): "ColorConvert[input_, colorspace_String]" if isinstance(input, Image): @@ -201,26 +204,32 @@ def eval(self, input, colorspace, evaluation): class ColorNegate(Builtin): """ - - :WMA link: - https://reference.wolfram.com/language/ref/ColorNegate.html + Color Inversion ( + :WMA: + https://reference.wolfram.com/language/ref/ColorNegate.html)
-
'ColorNegate[$image$]' -
returns the negative of $image$ in which colors have been negated. -
'ColorNegate[$color$]' -
returns the negative of a color. +
returns the negative of a color, that is, the RGB color \ + subtracted from white. - Yellow is RGBColor[1.0, 1.0, 0.0] - >> ColorNegate[Yellow] - = RGBColor[0., 0., 1.] +
'ColorNegate[$image$]' +
returns an image where each pixel has its color negated.
+ + Yellow is 'RGBColor[1.0, 1.0, 0.0]' So when inverted or subtracted \ + from 'White', we get blue: + + >> ColorNegate[Yellow] == Blue + = True + + >> ColorNegate[Import["ExampleData/sunflowers.jpg"]] + = -Image- """ - summary_text = "the negative color of a given color" + summary_text = "perform color inversion on a color or image" - def eval_for_color(self, color, evaluation): + def eval_for_color(self, color, evaluation: Evaluation): "ColorNegate[color_RGBColor]" # Get components r, g, b = [element.to_python() for element in color.elements] @@ -229,7 +238,7 @@ def eval_for_color(self, color, evaluation): # Reconstitute return Expression(SymbolRGBColor, Real(r), Real(g), Real(b)) - def eval_for_image(self, image, evaluation): + def eval_for_image(self, image, evaluation: Evaluation): "ColorNegate[image_Image]" return image.filter(lambda im: PIL.ImageOps.invert(im)) @@ -293,32 +302,32 @@ class DominantColors(Builtin): The option "MinColorDistance" specifies the distance (in LAB color space) up \ to which colors are merged and thus regarded as belonging to the same dominant color. - >> img = Import["ExampleData/lena.tif"] + >> img = Import["ExampleData/hedy.tif"] = -Image- >> DominantColors[img] - = {RGBColor[0.827451, 0.537255, 0.486275], RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902], RGBColor[0.690196, 0.266667, 0.309804], RGBColor[0.533333, 0.192157, 0.298039], RGBColor[0.878431, 0.760784, 0.721569]} + = {RGBColor[0.00784314, 0.00784314, 0.0156863], RGBColor[0.996078, 0.803922, 0.721569], RGBColor[0.227451, 0.329412, 0.360784]} >> DominantColors[img, 3] - = {RGBColor[0.827451, 0.537255, 0.486275], RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902]} + = {RGBColor[0.00784314, 0.00784314, 0.0156863], RGBColor[0.996078, 0.803922, 0.721569], RGBColor[0.227451, 0.329412, 0.360784]} >> DominantColors[img, 3, "Coverage"] - = {28579 / 131072, 751 / 4096, 23841 / 131072} + = {68817 / 103360, 62249 / 516800, 37953 / 516800} >> DominantColors[img, 3, "CoverageImage"] = {-Image-, -Image-, -Image-} >> DominantColors[img, 3, "Count"] - = {57158, 48064, 47682} + = {344085, 62249, 37953} >> DominantColors[img, 2, "LABColor"] - = {LABColor[0.646831, 0.279785, 0.193184], LABColor[0.608465, 0.443559, 0.195911]} + = {LABColor[0.00581591, 0.00207458, -0.00760911], LABColor[0.863667, 0.156864, 0.173956]} >> DominantColors[img, MinColorDistance -> 0.5] - = {RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902]} + = {RGBColor[0.00784314, 0.00784314, 0.0156863], RGBColor[0.996078, 0.803922, 0.721569]} >> DominantColors[img, ColorCoverage -> 0.15] - = {RGBColor[0.827451, 0.537255, 0.486275], RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902]} + = {RGBColor[0.00784314, 0.00784314, 0.0156863]} """ rules = { @@ -329,10 +338,17 @@ class DominantColors(Builtin): options = {"ColorCoverage": "Automatic", "MinColorDistance": "Automatic"} summary_text = "find a list of dominant colors" - def eval(self, image, n, prop, evaluation, options): + def eval( + self, + image: Image, + n: Integer, + prop: String, + evaluation: Evaluation, + options: dict, + ): "DominantColors[image_Image, n_Integer, prop_String, OptionsPattern[%(name)s]]" - py_prop = prop.get_string_value() + py_prop = prop.value if py_prop not in ("Color", "LABColor", "Count", "Coverage", "CoverageImage"): return diff --git a/mathics/builtin/compilation.py b/mathics/builtin/compilation.py index 0a3570121..353d1b29a 100644 --- a/mathics/builtin/compilation.py +++ b/mathics/builtin/compilation.py @@ -3,7 +3,8 @@ Code compilation allows Mathics functions to be run faster. -When LLVM and Python libraries are available, compilation produces LLVM code. +When LLVM and Python libraries are available, compilation \ +produces LLVM code. """ # This tells documentation how to sort this module @@ -25,6 +26,7 @@ ) from mathics.core.convert.python import from_python from mathics.core.element import ImmutableValueMixin +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression, SymbolCompiledFunction from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolTrue @@ -98,11 +100,12 @@ class Compile(Builtin): requires = ("llvmlite",) summary_text = "compile an expression" - def apply(self, vars, expr, evaluation): + def eval(self, vars, expr, evaluation: Evaluation): "Compile[vars_, expr_]" if not vars.has_form("List", None): - return evaluation.message("Compile", "invars") + evaluation.message("Compile", "invars") + return try: cfunc, args = expression_to_callable_and_args( @@ -174,7 +177,7 @@ def to_sympy(self, *args, **kwargs): def __hash__(self): return hash(("CompiledCode", ctypes.addressof(self.cfunc))) # XXX hack - def atom_to_boxes(self, f, evaluation): + def atom_to_boxes(self, f, evaluation: Evaluation): return CompiledCodeBox(String(self.__str__()), evaluation=evaluation) @@ -199,7 +202,7 @@ class CompiledFunction(Builtin): messages = {"argerr": "Invalid argument `1` should be Integer, Real or boolean."} summary_text = "A CompiledFunction object." - def apply(self, argnames, expr, code, args, evaluation): + def eval(self, argnames, expr, code, args, evaluation: Evaluation): "CompiledFunction[argnames_, expr_, code_CompiledCode][args__]" argseq = args.get_sequence() @@ -220,5 +223,6 @@ def apply(self, argnames, expr, code, args, evaluation): try: result = code.cfunc(*py_args) except (TypeError, ctypes.ArgumentError): - return evaluation.message("CompiledFunction", "argerr", args) + evaluation.message("CompiledFunction", "argerr", args) + return return from_python(result) diff --git a/mathics/builtin/compress.py b/mathics/builtin/compress.py index 86be1caea..340c2d3ae 100644 --- a/mathics/builtin/compress.py +++ b/mathics/builtin/compress.py @@ -6,6 +6,7 @@ from mathics.builtin.base import Builtin from mathics.core.atoms import String +from mathics.core.evaluation import Evaluation class Compress(Builtin): @@ -29,7 +30,7 @@ class Compress(Builtin): } summary_text = "compress an expression" - def eval(self, expr, evaluation, options): + def eval(self, expr, evaluation: Evaluation, options: dict): "Compress[expr_, OptionsPattern[Compress]]" if isinstance(expr, String): string = '"' + expr.value + '"' diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index 8f550d4a2..9221c3b4f 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -26,7 +26,11 @@ from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.convert.python import from_python from mathics.core.element import ImmutableValueMixin -from mathics.core.evaluation import TimeoutInterrupt, run_with_timeout_and_stack +from mathics.core.evaluation import ( + Evaluation, + TimeoutInterrupt, + run_with_timeout_and_stack, +) from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolNull @@ -625,7 +629,7 @@ class DateObject(_DateFormat, ImmutableValueMixin): " an object representing a date of any granularity (year, hour, instant, ...)" ) - def eval_any(self, args, evaluation, options): + def eval_any(self, args, evaluation: Evaluation, options: dict): "DateObject[args_, OptionsPattern[]]" datelist = None tz = None @@ -1038,10 +1042,10 @@ def eval(self, year, evaluation): h = (19 * a + b - d - g + 15) % 30 i = c // 4 k = c % 4 - l = (32 + 2 * e + 2 * i - h - k) % 7 - m = (a + 11 * h + 22 * l) // 451 - month = (h + l - 7 * m + 114) // 31 - day = ((h + l - 7 * m + 114) % 31) + 1 + le = (32 + 2 * e + 2 * i - h - k) % 7 + m = (a + 11 * h + 22 * le) // 451 + month = (h + le - 7 * m + 114) // 31 + day = ((h + le - 7 * m + 114) % 31) + 1 return ListExpression(year, Integer(month), Integer(day)) @@ -1185,7 +1189,7 @@ def eval_3(self, expr, t, failexpr, evaluation): except TimeoutInterrupt: evaluation.timeout_queue.pop() return failexpr.evaluate(evaluation) - except: + except Exception: evaluation.timeout_queue.pop() raise evaluation.timeout_queue.pop() diff --git a/mathics/builtin/distance/clusters.py b/mathics/builtin/distance/clusters.py new file mode 100644 index 000000000..37771f7f5 --- /dev/null +++ b/mathics/builtin/distance/clusters.py @@ -0,0 +1,514 @@ +""" +Cluster Analysis +""" + +import heapq + +from mathics.algorithm.clusters import ( + AutomaticMergeCriterion, + AutomaticSplitCriterion, + LazyDistances, + PrecomputedDistances, + agglomerate, + kmeans, + optimize, +) +from mathics.builtin.base import Builtin +from mathics.builtin.options import options_to_rules +from mathics.core.atoms import FP_MANTISA_BINARY_DIGITS, Integer, Real, String, min_prec +from mathics.core.convert.expression import to_mathics_list +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol, strip_context +from mathics.core.systemsymbols import ( + SymbolClusteringComponents, + SymbolFailed, + SymbolFindClusters, + SymbolRule, +) +from mathics.eval.distance import ( + IllegalDataPoint, + IllegalDistance, + dist_repr, + to_real_distance, +) +from mathics.eval.nevaluator import eval_N +from mathics.eval.parts import walk_levels + + +class _LazyDistances(LazyDistances): + # computes single distances only as needed, caches already computed distances. + + def __init__(self, df, p, evaluation): + super(_LazyDistances, self).__init__() + self._df = df + self._p = p + self._evaluation = evaluation + + def _compute_distance(self, i, j): + p = self._p + d = eval_N(self._df(p[i], p[j]), self._evaluation) + return to_real_distance(d) + + +class _PrecomputedDistances(PrecomputedDistances): + # computes all n^2 distances for n points with one big evaluation in the beginning. + + def __init__(self, df, p, evaluation): + distances_form = [df(p[i], p[j]) for i in range(len(p)) for j in range(i)] + distances = eval_N(ListExpression(*distances_form), evaluation) + mpmath_distances = [to_real_distance(d) for d in distances.elements] + super(_PrecomputedDistances, self).__init__(mpmath_distances) + + +class _Cluster(Builtin): + options = { + "Method": "Optimize", + "DistanceFunction": "Automatic", + "RandomSeed": "Automatic", + } + + messages = { + "amtd": "`1` failed to pick a suitable distance function for `2`.", + "bdmtd": 'Method in `` must be either "Optimize", "Agglomerate" or "KMeans".', + "intpm": "Positive integer expected at position 2 in ``.", + "list": "Expected a list or a rule with equally sized lists at position 1 in ``.", + "nclst": "Cannot find more clusters than there are elements: `1` is larger than `2`.", + "xnum": "The distance function returned ``, which is not a non-negative real value.", + "rseed": "The random seed specified through `` must be an integer or Automatic.", + "kmsud": "KMeans only supports SquaredEuclideanDistance as distance measure.", + } + + _criteria = { + "Optimize": AutomaticSplitCriterion, + "Agglomerate": AutomaticMergeCriterion, + "KMeans": None, + } + + def _cluster(self, p, k, mode, evaluation, options, expr): + method_string, method = self.get_option_string(options, "Method", evaluation) + if method_string not in ("Optimize", "Agglomerate", "KMeans"): + evaluation.message( + self.get_name(), "bdmtd", Expression(SymbolRule, "Method", method) + ) + return + + dist_p, repr_p = dist_repr(p) + + if dist_p is None or len(dist_p) != len(repr_p): + evaluation.message(self.get_name(), "list", expr) + return + + if not dist_p: + return ListExpression() + + if k is not None: # the number of clusters k is specified as an integer. + if not isinstance(k, Integer): + evaluation.message(self.get_name(), "intpm", expr) + return + py_k = k.get_int_value() + if py_k < 1: + evaluation.message(self.get_name(), "intpm", expr) + return + if py_k > len(dist_p): + evaluation.message(self.get_name(), "nclst", py_k, len(dist_p)) + return + elif py_k == 1: + return ListExpression(*repr_p) + elif py_k == len(dist_p): + return ListExpression(*[ListExpression(q) for q in repr_p]) + else: # automatic detection of k. choose a suitable method here. + if len(dist_p) <= 2: + return ListExpression(*repr_p) + constructor = self._criteria.get(method_string) + py_k = (constructor, {}) if constructor else None + + seed_string, seed = self.get_option_string(options, "RandomSeed", evaluation) + if seed_string == "Automatic": + py_seed = 12345 + elif isinstance(seed, Integer): + py_seed = seed.get_int_value() + else: + evaluation.message( + self.get_name(), "rseed", Expression(SymbolRule, "RandomSeed", seed) + ) + return + + distance_function_string, distance_function = self.get_option_string( + options, "DistanceFunction", evaluation + ) + if distance_function_string == "Automatic": + from mathics.builtin.tensors import get_default_distance + + distance_function = get_default_distance(dist_p) + if distance_function is None: + name_of_builtin = strip_context(self.get_name()) + evaluation.message( + self.get_name(), + "amtd", + name_of_builtin, + ListExpression(*dist_p), + ) + return + if method_string == "KMeans" and distance_function is not Symbol( + "SquaredEuclideanDistance" + ): + evaluation.message(self.get_name(), "kmsud") + return + + def df(i, j) -> Expression: + return Expression(distance_function, i, j) + + try: + if method_string == "Agglomerate": + clusters = self._agglomerate(mode, repr_p, dist_p, py_k, df, evaluation) + elif method_string == "Optimize": + clusters = optimize( + repr_p, py_k, _LazyDistances(df, dist_p, evaluation), mode, py_seed + ) + elif method_string == "KMeans": + clusters = self._kmeans(mode, repr_p, dist_p, py_k, py_seed, evaluation) + except IllegalDistance as e: + evaluation.message(self.get_name(), "xnum", e.distance) + return + except IllegalDataPoint: + name_of_builtin = strip_context(self.get_name()) + evaluation.message( + self.get_name(), + "amtd", + name_of_builtin, + ListExpression(*dist_p), + ) + return + + if mode == "clusters": + return ListExpression(*[ListExpression(*c) for c in clusters]) + elif mode == "components": + return to_mathics_list(*clusters) + else: + raise ValueError("illegal mode %s" % mode) + + def _agglomerate(self, mode, repr_p, dist_p, py_k, df, evaluation): + if mode == "clusters": + clusters = agglomerate( + repr_p, py_k, _PrecomputedDistances(df, dist_p, evaluation), mode + ) + elif mode == "components": + clusters = agglomerate( + repr_p, py_k, _PrecomputedDistances(df, dist_p, evaluation), mode + ) + + return clusters + + def _kmeans(self, mode, repr_p, dist_p, py_k, py_seed, evaluation): + items = [] + + def convert_scalars(p): + for q in p: + if not isinstance(q, (Real, Integer)): + raise IllegalDataPoint + mpq = q.to_mpmath() + if mpq is None: + raise IllegalDataPoint + items.append(q) + yield mpq + + def convert_vectors(p): + d = None + for q in p: + if q.get_head_name() != "System`List": + raise IllegalDataPoint + v = list(convert_scalars(q.elements)) + if d is None: + d = len(v) + elif len(v) != d: + raise IllegalDataPoint + yield v + + if dist_p[0].is_numeric(evaluation): + numeric_p = [[x] for x in convert_scalars(dist_p)] + else: + numeric_p = list(convert_vectors(dist_p)) + + # compute epsilon similar to Real.__eq__, such that "numbers that differ in their last seven binary digits + # are considered equal" + + prec = min_prec(*items) or FP_MANTISA_BINARY_DIGITS + eps = 0.5 ** (prec - 7) + + return kmeans(numeric_p, repr_p, py_k, mode, py_seed, eps) + + +class ClusteringComponents(_Cluster): + """ + :WMA link:https://reference.wolfram.com/language/ref/ClusteringComponents.html + +
+
'ClusteringComponents[$list$]' +
forms clusters from $list$ and returns a list of cluster indices, in which each + element shows the index of the cluster in which the corresponding element in $list$ + ended up. +
'ClusteringComponents[$list$, $k$]' +
forms $k$ clusters from $list$ and returns a list of cluster indices, in which + each element shows the index of the cluster in which the corresponding element in + $list$ ended up. +
+ + For more detailed documentation regarding options and behavior, see FindClusters[]. + + >> ClusteringComponents[{1, 2, 3, 1, 2, 10, 100}] + = {1, 1, 1, 1, 1, 1, 2} + + >> ClusteringComponents[{10, 100, 20}, Method -> "KMeans"] + = {1, 0, 1} + """ + + summary_text = "label data with the index of the cluster it is in" + + def eval(self, p, evaluation: Evaluation, options: dict): + "ClusteringComponents[p_, OptionsPattern[%(name)s]]" + return self._cluster( + p, + None, + "components", + evaluation, + options, + Expression(SymbolClusteringComponents, p, *options_to_rules(options)), + ) + + def eval_manual_k(self, p, k: Integer, evaluation: Evaluation, options: dict): + "ClusteringComponents[p_, k_Integer, OptionsPattern[%(name)s]]" + return self._cluster( + p, + k, + "components", + evaluation, + options, + Expression(SymbolClusteringComponents, p, k, *options_to_rules(options)), + ) + + +class FindClusters(_Cluster): + """ + :WMA link:https://reference.wolfram.com/language/ref/FindClusters.html + +
+
'FindClusters[$list$]' +
returns a list of clusters formed from the elements of $list$. The number of cluster is determined + automatically. +
'FindClusters[$list$, $k$]' +
returns a list of $k$ clusters formed from the elements of $list$. +
+ + >> FindClusters[{1, 2, 20, 10, 11, 40, 19, 42}] + = {{1, 2, 20, 10, 11, 19}, {40, 42}} + + >> FindClusters[{25, 100, 17, 20}] + = {{25, 17, 20}, {100}} + + >> FindClusters[{3, 6, 1, 100, 20, 5, 25, 17, -10, 2}] + = {{3, 6, 1, 5, -10, 2}, {100}, {20, 25, 17}} + + >> FindClusters[{1, 2, 10, 11, 20, 21}] + = {{1, 2}, {10, 11}, {20, 21}} + + >> FindClusters[{1, 2, 10, 11, 20, 21}, 2] + = {{1, 2, 10, 11}, {20, 21}} + + >> FindClusters[{1 -> a, 2 -> b, 10 -> c}] + = {{a, b}, {c}} + + >> FindClusters[{1, 2, 5} -> {a, b, c}] + = {{a, b}, {c}} + + >> FindClusters[{1, 2, 3, 1, 2, 10, 100}, Method -> "Agglomerate"] + = {{1, 2, 3, 1, 2, 10}, {100}} + + >> FindClusters[{1, 2, 3, 10, 17, 18}, Method -> "Agglomerate"] + = {{1, 2, 3}, {10}, {17, 18}} + + >> FindClusters[{{1}, {5, 6}, {7}, {2, 4}}, DistanceFunction -> (Abs[Length[#1] - Length[#2]]&)] + = {{{1}, {7}}, {{5, 6}, {2, 4}}} + + >> FindClusters[{"meep", "heap", "deep", "weep", "sheep", "leap", "keep"}, 3] + = {{meep, deep, weep, keep}, {heap, leap}, {sheep}} + + FindClusters' automatic distance function detection supports scalars, numeric tensors, boolean vectors and + strings. + + The Method option must be either "Agglomerate" or "Optimize". If not specified, it defaults to "Optimize". + Note that the Agglomerate and Optimize methods usually produce different clusterings. + + The runtime of the Agglomerate method is quadratic in the number of clustered points n, builds the clustering + from the bottom up, and is exact (no element of randomness). The Optimize method's runtime is linear in n, + Optimize builds the clustering from top down, and uses random sampling. + """ + + summary_text = "divide data into lists of similar elements" + + def eval(self, p, evaluation: Evaluation, options: dict): + "FindClusters[p_, OptionsPattern[%(name)s]]" + return self._cluster( + p, + None, + "clusters", + evaluation, + options, + Expression(SymbolFindClusters, p, *options_to_rules(options)), + ) + + def eval_manual_k(self, p, k: Integer, evaluation: Evaluation, options: dict): + "FindClusters[p_, k_Integer, OptionsPattern[%(name)s]]" + return self._cluster( + p, + k, + "clusters", + evaluation, + options, + Expression(SymbolFindClusters, p, k, *options_to_rules(options)), + ) + + +class Nearest(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Nearest.html + +
+
'Nearest[$list$, $x$]' +
returns the one item in $list$ that is nearest to $x$. + +
'Nearest[$list$, $x$, $n$]' +
returns the $n$ nearest items. + +
'Nearest[$list$, $x$, {$n$, $r$}]' +
returns up to $n$ nearest items that are not farther from $x$ than $r$. + +
'Nearest[{$p1$ -> $q1$, $p2$ -> $q2$, ...}, $x$]' +
returns $q1$, $q2$, ... but measures the distances using $p1$, $p2$, ... + +
'Nearest[{$p1$, $p2$, ...} -> {$q1$, $q2$, ...}, $x$]' +
returns $q1$, $q2$, ... but measures the distances using $p1$, $p2$, ... +
+ + >> Nearest[{5, 2.5, 10, 11, 15, 8.5, 14}, 12] + = {11} + + Return all items within a distance of 5: + + >> Nearest[{5, 2.5, 10, 11, 15, 8.5, 14}, 12, {All, 5}] + = {11, 10, 14} + + >> Nearest[{Blue -> "blue", White -> "white", Red -> "red", Green -> "green"}, {Orange, Gray}] + = {{red}, {white}} + + >> Nearest[{{0, 1}, {1, 2}, {2, 3}} -> {a, b, c}, {1.1, 2}] + = {b} + """ + + messages = { + "amtd": "`1` failed to pick a suitable distance function for `2`.", + "list": "Expected a list or a rule with equally sized lists at position 1 in ``.", + "nimp": "Method `1` is not implemented yet.", + } + + options = { + "DistanceFunction": "Automatic", + "Method": '"Scan"', + } + + rules = { + "Nearest[list_, pattern_]": "Nearest[list, pattern, 1]", + "Nearest[pattern_][list_]": "Nearest[list, pattern]", + } + summary_text = "the nearest element from a list" + + def eval( + self, items, pivot, limit, expression, evaluation: Evaluation, options: dict + ): + "Nearest[items_, pivot_, limit_, OptionsPattern[%(name)s]]" + + method = self.get_option(options, "Method", evaluation) + if not isinstance(method, String) or method.get_string_value() != "Scan": + evaluation("Nearest", "nimp", method) + return + + dist_p, repr_p = dist_repr(items) + + if dist_p is None or len(dist_p) != len(repr_p): + evaluation.message(self.get_name(), "list", expression) + return + + if limit.has_form("List", 2): + up_to = limit.elements[0] + py_r = limit.elements[1].to_mpmath() + else: + up_to = limit + py_r = None + + if isinstance(up_to, Integer): + py_n = up_to.get_int_value() + elif up_to.get_name() == "System`All": + py_n = None + else: + return + + if not dist_p or (py_n is not None and py_n < 1): + return ListExpression() + + multiple_x = False + + distance_function_string, distance_function = self.get_option_string( + options, "DistanceFunction", evaluation + ) + if distance_function_string == "Automatic": + from mathics.builtin.tensors import get_default_distance + + distance_function = get_default_distance(dist_p) + if distance_function is None: + evaluation.message( + self.get_name(), "amtd", "Nearest", ListExpression(*dist_p) + ) + return + + if pivot.get_head_name() == "System`List": + _, depth_x = walk_levels(pivot) + _, depth_items = walk_levels(dist_p[0]) + + if depth_x > depth_items: + multiple_x = True + + def nearest(x) -> ListExpression: + calls = [Expression(distance_function, x, y) for y in dist_p] + distances = ListExpression(*calls).evaluate(evaluation) + + if not distances.has_form("List", len(dist_p)): + raise ValueError() + + py_distances = [ + (to_real_distance(d), i) for i, d in enumerate(distances.elements) + ] + + if py_r is not None: + py_distances = [(d, i) for d, i in py_distances if d <= py_r] + + def pick(): + if py_n is None: + candidates = sorted(py_distances) + else: + candidates = heapq.nsmallest(py_n, py_distances) + + for d, i in candidates: + yield repr_p[i] + + return ListExpression(*list(pick())) + + try: + if not multiple_x: + return nearest(pivot) + else: + return ListExpression(*[nearest(t) for t in pivot.elements]) + except IllegalDistance: + return SymbolFailed + except ValueError: + return SymbolFailed diff --git a/mathics/builtin/distance/numeric.py b/mathics/builtin/distance/numeric.py index 491aaf4db..c5d78d968 100644 --- a/mathics/builtin/distance/numeric.py +++ b/mathics/builtin/distance/numeric.py @@ -1,10 +1,10 @@ """ -Numerial Data +Numerical Data """ from mathics.builtin.base import Builtin from mathics.core.atoms import Integer1, Integer2 -from mathics.core.expression import Expression +from mathics.core.expression import Evaluation, Expression from mathics.core.symbols import ( SymbolAbs, SymbolDivide, @@ -21,7 +21,7 @@ ) -def _norm_calc(head, u, v, evaluation): +def _norm_calc(head, u, v, evaluation: Evaluation): expr = Expression(head, u, v) old_quiet_all = evaluation.quiet_all try: @@ -38,8 +38,11 @@ def _norm_calc(head, u, v, evaluation): class BrayCurtisDistance(Builtin): """ - :Bray-Curtis Dissimilarity:https://en.wikipedia.org/wiki/Bray%E2%80%93Curtis_dissimilarity \ - (:WMA link:https://reference.wolfram.com/language/ref/BrayCurtisDistance.html) + + :Bray-Curtis Dissimilarity: + https://en.wikipedia.org/wiki/Bray%E2%80%93Curtis_dissimilarity \ + (:WMA: + https://reference.wolfram.com/language/ref/BrayCurtisDistance.html)
'BrayCurtisDistance[$u$, $v$]' @@ -56,7 +59,7 @@ class BrayCurtisDistance(Builtin): summary_text = "Bray-Curtis distance" - def apply(self, u, v, evaluation): + def eval(self, u, v, evaluation: Evaluation): "BrayCurtisDistance[u_, v_]" t = _norm_calc(SymbolSubtract, u, v, evaluation) if t is not None: @@ -71,8 +74,12 @@ def apply(self, u, v, evaluation): class CanberraDistance(Builtin): """ - :Canberra distance:https://en.wikipedia.org/wiki/Canberra_distance \ - (:WMA link:https://reference.wolfram.com/language/ref/CanberraDistance.html) + + :Canberra distance: + https://en.wikipedia.org/wiki/Canberra_distance \ + ( + :WMA: + https://reference.wolfram.com/language/ref/CanberraDistance.html)
'CanberraDistance[$u$, $v$]' @@ -88,7 +95,7 @@ class CanberraDistance(Builtin): summary_text = "Canberra distance" - def apply(self, u, v, evaluation): + def eval(self, u, v, evaluation: Evaluation): "CanberraDistance[u_, v_]" t = _norm_calc(SymbolSubtract, u, v, evaluation) if t is not None: @@ -107,7 +114,9 @@ def apply(self, u, v, evaluation): class ChessboardDistance(Builtin): """ :Chebyshev distance:https://en.wikipedia.org/wiki/Chebyshev_distance \ - (:WMA link:https://reference.wolfram.com/language/ref/ChessboardDistance.html) + ( + :WMA: + https://reference.wolfram.com/language/ref/ChessboardDistance.html)
'ChessboardDistance[$u$, $v$]' @@ -123,7 +132,7 @@ class ChessboardDistance(Builtin): summary_text = "chessboard distance" - def apply(self, u, v, evaluation): + def eval(self, u, v, evaluation: Evaluation): "ChessboardDistance[u_, v_]" t = _norm_calc(SymbolSubtract, u, v, evaluation) if t is not None: @@ -132,8 +141,11 @@ def apply(self, u, v, evaluation): class CosineDistance(Builtin): r""" - :Cosine similarity:https://en.wikipedia.org/wiki/Cosine_similarity \ - (:WMA link:https://reference.wolfram.com/language/ref/CosineDistance.html) + + :Cosine similarity: + https://en.wikipedia.org/wiki/Cosine_similarity \ + (:WMA: + https://reference.wolfram.com/language/ref/CosineDistance.html)
'CosineDistance[$u$, $v$]' @@ -152,7 +164,7 @@ class CosineDistance(Builtin): summary_text = "cosine distance" - def apply(self, u, v, evaluation): + def eval(self, u, v, evaluation: Evaluation): "CosineDistance[u_, v_]" dot = _norm_calc(SymbolDot, u, v, evaluation) if dot is not None: @@ -173,8 +185,12 @@ def apply(self, u, v, evaluation): class EuclideanDistance(Builtin): """ - :Euclidean similarity:https://en.wikipedia.org/wiki/Euclidean_distance \ - (:WMA link:https://reference.wolfram.com/language/ref/EuclideanDistance.html) + + :Euclidean similarity: + https://en.wikipedia.org/wiki/Euclidean_distance \ + ( + :WMA: + https://reference.wolfram.com/language/ref/EuclideanDistance.html)
'EuclideanDistance[$u$, $v$]' @@ -193,7 +209,7 @@ class EuclideanDistance(Builtin): summary_text = "euclidean distance" - def apply(self, u, v, evaluation): + def eval(self, u, v, evaluation: Evaluation): "EuclideanDistance[u_, v_]" t = _norm_calc(SymbolSubtract, u, v, evaluation) if t is not None: @@ -202,8 +218,12 @@ def apply(self, u, v, evaluation): class ManhattanDistance(Builtin): """ - :Manhattan distance:https://en.wikipedia.org/wiki/Taxicab_geometry \ - (:WMA link:https://reference.wolfram.com/language/ref/ManhattanDistance.html) + + :Manhattan distance: + https://en.wikipedia.org/wiki/Taxicab_geometry \ + ( + :WMA: + https://reference.wolfram.com/language/ref/ManhattanDistance.html)
'ManhattanDistance[$u$, $v$]' @@ -219,7 +239,7 @@ class ManhattanDistance(Builtin): summary_text = "Manhattan distance" - def apply(self, u, v, evaluation): + def eval(self, u, v, evaluation: Evaluation): "ManhattanDistance[u_, v_]" t = _norm_calc(SymbolSubtract, u, v, evaluation) if t is not None: @@ -228,7 +248,9 @@ def apply(self, u, v, evaluation): class SquaredEuclideanDistance(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/SquaredEuclideanDistance.html + + :WMA link: + https://reference.wolfram.com/language/ref/SquaredEuclideanDistance.html
'SquaredEuclideanDistance[$u$, $v$]' @@ -244,7 +266,7 @@ class SquaredEuclideanDistance(Builtin): summary_text = "square of the euclidean distance" - def apply(self, u, v, evaluation): + def eval(self, u, v, evaluation: Evaluation): "SquaredEuclideanDistance[u_, v_]" t = _norm_calc(SymbolSubtract, u, v, evaluation) if t is not None: diff --git a/mathics/builtin/distance/stringdata.py b/mathics/builtin/distance/stringdata.py index 7b44523d0..cb9562f06 100644 --- a/mathics/builtin/distance/stringdata.py +++ b/mathics/builtin/distance/stringdata.py @@ -8,6 +8,7 @@ from mathics.builtin.base import Builtin from mathics.core.atoms import Integer, String, Symbol +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import SymbolTrue @@ -33,13 +34,13 @@ # note: double brackets indicate 1-based indices below, e.g. s1[[1]] -def _one_based(l): # makes an enumerated generator 1-based - return ((i + 1, x) for i, x in l) +def _one_based(le): # makes an enumerated generator 1-based + return ((i + 1, x) for i, x in le) -def _prev_curr(l): # yields pairs of (x[i - 1], x[i]) for i in 1, 2, ... +def _prev_curr(le): # yields pairs of (x[i - 1], x[i]) for i in 1, 2, ... prev = None - for curr in l: + for curr in le: yield prev, curr prev = curr @@ -117,7 +118,7 @@ def _levenshtein_like_or_border_cases(s1, s2, sameQ: Callable[..., bool], comput class _StringDistance(Builtin): options = {"IgnoreCase": "False"} - def apply(self, a, b, evaluation, options): + def eval(self, a, b, evaluation, options): "%(name)s[a_, b_, OptionsPattern[%(name)s]]" if isinstance(a, String) and isinstance(b, String): py_a = a.get_string_value() @@ -255,20 +256,20 @@ class HammingDistance(Builtin): summary_text = "Hamming distance" @staticmethod - def _compute(u, v, sameQ, evaluation): + def _compute(u, v, sameQ, evaluation: Evaluation): if len(u) != len(v): evaluation.message("HammingDistance", "idim", u, v) return None else: return Integer(sum(0 if sameQ(x, y) else 1 for x, y in zip(u, v))) - def apply_list(self, u, v, evaluation): + def eval_list(self, u, v, evaluation: Evaluation): "HammingDistance[u_List, v_List]" return HammingDistance._compute( u.elements, v.elements, lambda x, y: x.sameQ(y), evaluation ) - def apply_string(self, u, v, evaluation, options): + def eval_string(self, u, v, evaluation, options): "HammingDistance[u_String, v_String, OptionsPattern[HammingDistance]]" ignore_case = self.get_option(options, "IgnoreCase", evaluation) py_u = u.get_string_value() diff --git a/mathics/builtin/drawing/__init__.py b/mathics/builtin/drawing/__init__.py index a4f640974..eec632ffb 100644 --- a/mathics/builtin/drawing/__init__.py +++ b/mathics/builtin/drawing/__init__.py @@ -4,17 +4,19 @@ Showing something visually can be done in a number of ways:
    -
  • Starting with complete images and modifiying them using the 'Image' Built-in function. +
  • Starting with complete images and modifying them using the 'Image' Built-in function.
  • Use pre-defined 2D or 3D objects like :'Circle': /doc/reference-of-built-in-symbols/drawing-graphics/circle and - :'Cuboid': /doc/reference-of-built-in-symbols/graphics-drawing-and-images/three-dimensional-graphics/cuboid/ \ - and place them in a coordiate space. + :'Cuboid': + /doc/reference-of-built-in-symbols/graphics-drawing-and-images/three-dimensional-graphics/cuboid/ \ + and place them in a coordinate space.
  • Compute the points of the space using a function. This is done using functions like :'Plot': /doc/reference-of-built-in-symbols/graphics-drawing-and-images/plotting-data/plot \ and - :'ListPlot': /doc/reference-of-built-in-symbols/graphics-drawing-and-images/plotting-data/listplot. + :'ListPlot': + /doc/reference-of-built-in-symbols/graphics-drawing-and-images/plotting-data/listplot.
""" diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index 55b5b876c..3cb3e7bbf 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -18,7 +18,7 @@ _GraphicsElements, ) from mathics.core.atoms import Integer, Rational, Real -from mathics.core.expression import Expression +from mathics.core.expression import Evaluation, Expression from mathics.core.symbols import SymbolN from mathics.eval.nevaluator import eval_N @@ -242,7 +242,7 @@ class Cone(Builtin): "Cone[positions_List]": "Cone[positions, 1]", } - def apply_check(self, positions, radius, evaluation): + def eval_check(self, positions, radius, evaluation: Evaluation): "Cone[positions_List, radius_]" if len(positions.elements) % 2 == 1: @@ -300,7 +300,7 @@ class Cuboid(Builtin): summary_text = "unit cube" - def apply_check(self, positions, evaluation): + def eval_check(self, positions, evaluation: Evaluation): "Cuboid[positions_List]" if len(positions.elements) % 2 == 1: @@ -343,7 +343,7 @@ class Cylinder(Builtin): "Cylinder[positions_List]": "Cylinder[positions, 1]", } - def apply_check(self, positions, radius, evaluation): + def eval_check(self, positions, radius, evaluation: Evaluation): "Cylinder[positions_List, radius_]" if len(positions.elements) % 2 == 1: diff --git a/mathics/builtin/drawing/graphics_internals.py b/mathics/builtin/drawing/graphics_internals.py index 07d84be2d..4b5bfd524 100644 --- a/mathics/builtin/drawing/graphics_internals.py +++ b/mathics/builtin/drawing/graphics_internals.py @@ -5,7 +5,7 @@ from mathics.builtin.base import BuiltinElement -from mathics.builtin.box.expression import BoxExpression, split_name +from mathics.builtin.box.expression import BoxExpression # Signals to Mathics doc processing not to include this module in its documentation. no_doc = True @@ -15,30 +15,6 @@ class _GraphicsDirective(BuiltinElement): - def __new__(cls, *args, **kwargs): - # This ensures that all the graphics directive have a well formatted docstring - # and a summary_text - instance = super().__new__(cls, *args, **kwargs) - if not hasattr(instance, "summary_text"): - article = ( - "an " - if instance.get_name()[0].lower() in ("a", "e", "i", "o", "u") - else "a " - ) - instance.summary_text = ( - "graphics directive setting " - + article - + split_name(cls.get_name(short=True)[:-3]) - ) - if not instance.__doc__: - instance.__doc__ = f""" -
-
'{cls.get_name()}[...]' -
is a graphics directive that sets {cls.get_name().lower()[:3]} -
- """ - return instance - def init(self, graphics, item=None): if item is not None and not item.has_form(self.get_name(), None): raise BoxExpressionError diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index e4e556c09..7c5421da4 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -62,6 +62,9 @@ SymbolRectangle = Symbol("Rectangle") SymbolText = Symbol("Text") +TwoTenths = Real(0.2) +MTwoTenths = -TwoTenths + # PlotRange Option def check_plot_range(range, range_type) -> bool: @@ -453,7 +456,8 @@ def eval(self, functions, x, start, stop, evaluation: Evaluation, options: dict) if plotpoints == "System`None": plotpoints = 57 if not (isinstance(plotpoints, int) and plotpoints >= 2): - return evaluation.message(self.get_name(), "ppts", plotpoints) + evaluation.message(self.get_name(), "ppts", plotpoints) + return # MaxRecursion Option max_recursion_limit = 15 @@ -589,9 +593,11 @@ def process_function_and_options( py_start = start.round_to_float(evaluation) py_stop = stop.round_to_float(evaluation) if py_start is None or py_stop is None: - return evaluation.message(self.get_name(), "plln", stop, expr) + evaluation.message(self.get_name(), "plln", stop, expr) + return if py_start >= py_stop: - return evaluation.message(self.get_name(), "plld", expr_limits) + evaluation.message(self.get_name(), "plld", expr_limits) + return plotrange_option = self.get_option(options, "PlotRange", evaluation) plot_range = eval_N(plotrange_option, evaluation).to_python() @@ -1184,7 +1190,7 @@ def axes(): yield Expression(SymbolFaceForm, Symbol("Black")) def points(x): - return ListExpression(vector2(x, 0), vector2(x, Real(-0.2))) + return ListExpression(vector2(x, 0), vector2(x, MTwoTenths)) for (k, n), x0, x1, y in boxes(): if k == 1: @@ -1199,7 +1205,7 @@ def labels(names): if k <= len(names): name = names[k - 1] yield Expression( - SymbolText, name, vector2((x0 + x1) / 2, Real(-0.2)) + SymbolText, name, vector2((x0 + x1) / 2, MTwoTenths) ) x_coords = list(itertools.chain(*[[x0, x1] for (k, n), x0, x1, y in boxes()])) @@ -1593,9 +1599,11 @@ def eval( py_nmax = nmax.value py_step = step.value if py_start is None or py_nmax is None: - return evaluation.message(self.get_name(), "plln", nmax, expr) + evaluation.message(self.get_name(), "plln", nmax, expr) + return if py_start >= py_nmax: - return evaluation.message(self.get_name(), "plld", expr_limits) + evaluation.message(self.get_name(), "plld", expr_limits) + return plotrange_option = self.get_option(options, "PlotRange", evaluation) plot_range = eval_N(plotrange_option, evaluation).to_python() @@ -1987,7 +1995,9 @@ class ListPlot(_ListPlot): class ListLinePlot(_ListPlot): """ - :WMA link: https://reference.wolfram.com/language/ref/ListLinePlot.html + + :WMA link: + https://reference.wolfram.com/language/ref/ListLinePlot.html
'ListLinePlot[{$y_1$, $y_2$, ...}]'
plots a line through a list of $y$-values, assuming integer $x$-values 1, 2, 3, ... @@ -2092,11 +2102,63 @@ class LogPlot(_Plot): """ - summary_text = "plots on a log scale curves of one or more functions" + summary_text = "plot on a log scale curves of one or more functions" use_log_scale = True +class NumberLinePlot(_ListPlot): + """ + :WMA link: + https://reference.wolfram.com/language/ref/NumberLinePlot.html +
+
'NumberLinePlot[{$v_1$, $v_2$, ...}]' +
plots a list of values along a line. +
+ + >> NumberLinePlot[Prime[Range[10]]] + = -Graphics- + + Compare with: + >> NumberLinePlot[Table[x^2, {x, 10}]] + + = -Graphics- + """ + + options = Graphics.options.copy() + + # This is ListPlot with some tweaks: + # * remove the Y axis in display, + # * set the Y value to a constant, and + # * set the aspect ratio to reduce the distance above the + # x-axis + options.update( + { + "Axes": "{True, False}", + "AspectRatio": "1 / 10", + "Mesh": "None", + "PlotRange": "Automatic", + "PlotPoints": "None", + "Filling": "None", + "Joined": "False", + } + ) + summary_text = "plot along a number line" + + use_log_scale = False + + def eval(self, values, evaluation: Evaluation, options: dict): + "%(name)s[values_, OptionsPattern[%(name)s]]" + + # Fill in a Y value, and use the generic _ListPlot.eval(). + # Some graphics options have been adjusted above. + points_list = [ + ListExpression(eval_N(value, evaluation), Integer1) + for value in values.elements + ] + return _ListPlot.eval(self, ListExpression(*points_list), evaluation, options) + + class PieChart(_Chart): """ :Pie Chart: https://en.wikipedia.org/wiki/Pie_chart \ @@ -2124,7 +2186,7 @@ class PieChart(_Chart):
  • SectorSpacing" (default Automatic) - A hypothetical comparsion between types of pets owned: + A hypothetical comparison between types of pets owned: >> PieChart[{30, 20, 10}, ChartLabels -> {Dogs, Cats, Fish}] = -Graphics- @@ -2132,7 +2194,7 @@ class PieChart(_Chart): >> PieChart[{8, 16, 2}, SectorOrigin -> {Automatic, 1.5}] = -Graphics- - A Pie chart with multple datasets: + A Pie chart with multiple datasets: >> PieChart[{{10, 20, 30}, {15, 22, 30}}] = -Graphics- @@ -2194,7 +2256,7 @@ def _draw(self, data, color, evaluation, options: dict): sector_spacing = self.get_option(options, "SectorSpacing", evaluation) if isinstance(sector_spacing, Symbol): if sector_spacing.get_name() == "System`Automatic": - sector_spacing = ListExpression(Integer0, Real(0.2)) + sector_spacing = ListExpression(Integer0, TwoTenths) elif sector_spacing.get_name() == "System`None": sector_spacing = ListExpression(Integer0, Integer0) else: @@ -2348,7 +2410,9 @@ def _apply_fn(self, f: Callable, x_value): class ParametricPlot(_Plot): """ - :WMA link: https://reference.wolfram.com/language/ref/ParametricPlot.html + + :WMA link + : https://reference.wolfram.com/language/ref/ParametricPlot.html
    'ParametricPlot[{$f_x$, $f_y$}, {$u$, $umin$, $umax$}]'
    plots a parametric function $f$ with the parameter $u$ ranging from $umin$ to $umax$. diff --git a/mathics/builtin/drawing/uniform_polyhedra.py b/mathics/builtin/drawing/uniform_polyhedra.py index 2c69fb93d..03f6d10e9 100644 --- a/mathics/builtin/drawing/uniform_polyhedra.py +++ b/mathics/builtin/drawing/uniform_polyhedra.py @@ -3,7 +3,8 @@ """ Uniform Polyhedra -Uniform polyhedra is the grouping of platonic solids, Archimedean solids, and regular star polyhedra. +Uniform polyhedra is the grouping of platonic solids, Archimedean solids,\ +and regular star polyhedra. """ # This tells documentation how to sort this module @@ -11,52 +12,16 @@ sort_order = "mathics.builtin.uniform-polyhedra" from mathics.builtin.base import Builtin +from mathics.core.evaluation import Evaluation uniform_polyhedra_names = "tetrahedron, octahedron, dodecahedron, icosahedron" uniform_polyhedra_set = frozenset(uniform_polyhedra_names.split(", ")) -class UniformPolyhedron(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/UniformPolyhedron.html - -
    -
    'UniformPolyhedron["name"]' -
    return a uniform polyhedron with the given name. -
    Names are "tetrahedron", "octahedron", "dodecahedron", or "icosahedron". -
    - - >> Graphics3D[UniformPolyhedron["octahedron"]] - = -Graphics3D- - - >> Graphics3D[UniformPolyhedron["dodecahedron"]] - = -Graphics3D- - - >> Graphics3D[{"Brown", UniformPolyhedron["tetrahedron"]}] - = -Graphics3D- - """ - - summary_text = "platonic polyhedra by name" - messages = { - "argtype": f"Argument `1` is not one of: {uniform_polyhedra_names}", - } - - rules = { - "UniformPolyhedron[name_String]": "UniformPolyhedron[name, {{0, 0, 0}}, 1]", - } - - def apply(self, name, positions, edgelength, evaluation): - "UniformPolyhedron[name_String, positions_List, edgelength_?NumberQ]" - - if name.to_python(string_quotes=False) not in uniform_polyhedra_set: - evaluation.error("UniformPolyhedron", "argtype", name) - - return - - class Dodecahedron(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Dodecahedron.html + :WMA link: + https://reference.wolfram.com/language/ref/Dodecahedron.html
    'Dodecahedron[]' @@ -77,7 +42,8 @@ class Dodecahedron(Builtin): class Icosahedron(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Icosahedron.html + :WMA link: + https://reference.wolfram.com/language/ref/Icosahedron.html
    'Icosahedron[]' @@ -88,17 +54,18 @@ class Icosahedron(Builtin): = -Graphics3D- """ - summary_text = "an icosahedron" rules = { "Icosahedron[]": """UniformPolyhedron["icosahedron"]""", "Icosahedron[l_?NumberQ]": """UniformPolyhedron["icosahedron", {{0, 0, 0}}, l]""", "Icosahedron[positions_List, l_?NumberQ]": """UniformPolyhedron["icosahedron", positions, l]""", } + summary_text = "an icosahedron" class Octahedron(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Octahedron.html + :WMA link + :https://reference.wolfram.com/language/ref/Octahedron.html
    'Octahedron[]' @@ -109,17 +76,18 @@ class Octahedron(Builtin): = -Graphics3D- """ - summary_text = "an octahedron" rules = { "Octahedron[]": """UniformPolyhedron["octahedron"]""", "Octahedron[l_?NumberQ]": """UniformPolyhedron["octahedron", {{0, 0, 0}}, l]""", "Octahedron[positions_List, l_?NumberQ]": """UniformPolyhedron["octahedron", positions, l]""", } + summary_text = "an octahedron" class Tetrahedron(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Tetrahedron.html + :WMA link + :https://reference.wolfram.com/language/ref/Tetrahedron.html
    'Tetrahedron[]' @@ -130,12 +98,51 @@ class Tetrahedron(Builtin): = -Graphics3D- """ - summary_text = "a tetrahedron" rules = { "Tetrahedron[]": """UniformPolyhedron["tetrahedron"]""", "Tetrahedron[l_?NumberQ]": """UniformPolyhedron["tetrahedron", {{0, 0, 0}}, l]""", "Tetrahedron[positions_List, l_?NumberQ]": """UniformPolyhedron["tetrahedron", positions, l]""", } + summary_text = "a tetrahedron" - def apply_with_length(self, length, evaluation): + def eval_with_length(self, length, evaluation: Evaluation): "Tetrahedron[l_?Numeric]" + + +class UniformPolyhedron(Builtin): + """ + :WMA link: + https://reference.wolfram.com/language/ref/UniformPolyhedron.html + +
    +
    'UniformPolyhedron["name"]' +
    return a uniform polyhedron with the given name. +
    Names are "tetrahedron", "octahedron", "dodecahedron", or "icosahedron". +
    + + >> Graphics3D[UniformPolyhedron["octahedron"]] + = -Graphics3D- + + >> Graphics3D[UniformPolyhedron["dodecahedron"]] + = -Graphics3D- + + >> Graphics3D[{"Brown", UniformPolyhedron["tetrahedron"]}] + = -Graphics3D- + """ + + messages = { + "argtype": f"Argument `1` is not one of: {uniform_polyhedra_names}", + } + + rules = { + "UniformPolyhedron[name_String]": "UniformPolyhedron[name, {{0, 0, 0}}, 1]", + } + summary_text = "platonic polyhedra by name" + + def eval(self, name, positions, edgelength, evaluation: Evaluation): + "UniformPolyhedron[name_String, positions_List, edgelength_?NumberQ]" + + if name.value not in uniform_polyhedra_set: + evaluation.error("UniformPolyhedron", "argtype", name) + + return diff --git a/mathics/builtin/evaluation.py b/mathics/builtin/evaluation.py index d4ca1c06c..0e743586b 100644 --- a/mathics/builtin/evaluation.py +++ b/mathics/builtin/evaluation.py @@ -4,7 +4,11 @@ from mathics.builtin.base import Builtin, Predefined from mathics.core.atoms import Integer from mathics.core.attributes import A_HOLD_ALL, A_HOLD_ALL_COMPLETE, A_PROTECTED -from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit +from mathics.core.evaluation import ( + MAX_RECURSION_DEPTH, + Evaluation, + set_python_recursion_limit, +) class RecursionLimit(Predefined): @@ -376,7 +380,7 @@ class Quit(Builtin): } summary_text = "terminate the session" - def apply(self, evaluation, n): + def eval(self, evaluation: Evaluation, n): "%(name)s[n___]" exitcode = 0 if isinstance(n, Integer): diff --git a/mathics/builtin/exp_structure/__init__.py b/mathics/builtin/exp_structure/__init__.py new file mode 100644 index 000000000..67f7dd86d --- /dev/null +++ b/mathics/builtin/exp_structure/__init__.py @@ -0,0 +1,3 @@ +""" +Expression Structure +""" diff --git a/mathics/builtin/structure.py b/mathics/builtin/exp_structure/general.py similarity index 50% rename from mathics/builtin/structure.py rename to mathics/builtin/exp_structure/general.py index 66c71f701..019a56bce 100644 --- a/mathics/builtin/structure.py +++ b/mathics/builtin/exp_structure/general.py @@ -1,26 +1,17 @@ # -*- coding: utf-8 -*- """ -Structural Operations on Expressions - -Structural transformations on lists, and general symbolic expressions. +General Structural Expression Functions """ -import platform - from mathics.builtin.base import BinaryOperator, Builtin, Predefined -from mathics.builtin.lists import walk_levels from mathics.core.atoms import Integer, Integer0, Integer1, Rational -from mathics.core.expression import Expression +from mathics.core.exceptions import InvalidLevelspecError +from mathics.core.expression import Evaluation, Expression +from mathics.core.list import ListExpression from mathics.core.rules import Pattern from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolTrue -from mathics.core.systemsymbols import SymbolDirectedInfinity, SymbolMap - -if platform.python_implementation() == "PyPy": - bytecount_support = False -else: - from .pympler.asizeof import asizeof as count_bytes - - bytecount_support = True +from mathics.core.systemsymbols import SymbolMap +from mathics.eval.parts import python_levelspec, walk_levels SymbolOperate = Symbol("Operate") SymbolSortBy = Symbol("SortBy") @@ -28,7 +19,9 @@ class ApplyLevel(BinaryOperator): """ - :WMA link:https://reference.wolfram.com/language/ref/ApplyLevel.html + + :WMA link: + https://reference.wolfram.com/language/ref/ApplyLevel.html
    'ApplyLevel[$f$, $expr$]' @@ -54,32 +47,51 @@ class ApplyLevel(BinaryOperator): class BinarySearch(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/BinarySearch.html + + :Binary search algorithm: + https://en.wikipedia.org/wiki/Binary_search_algorithm ( + :WMA: + https://reference.wolfram.com/language/ref/BinarySearch.html)
    'CombinatoricaOld`BinarySearch[$l$, $k$]' -
    searches the list $l$, which has to be sorted, for key $k$ and returns its index in $l$. If $k$ does not - exist in $l$, 'BinarySearch' returns (a + b) / 2, where a and b are the indices between which $k$ would have - to be inserted in order to maintain the sorting order in $l$. Please note that $k$ and the elements in $l$ - need to be comparable under a strict total order (see https://en.wikipedia.org/wiki/Total_order). +
    searches the list $l$, which has to be sorted, for key $k$ and \ + returns its index in $l$. + + If $k$ does not exist in $l$, 'BinarySearch' returns ($a$ + $b$) / 2, \ + where $a$ and $b$ are the indices between which $k$ would have \ + to be inserted in order to maintain the sorting order in $l$. + + Please note that $k$ and the elements in $l$ need to be comparable \ + under a + :strict total order: + https://en.wikipedia.org/wiki/Total_order.
    'CombinatoricaOld`BinarySearch[$l$, $k$, $f$]' -
    the index of $k in the elements of $l$ if $f$ is applied to the latter prior to comparison. Note that $f$ - needs to yield a sorted sequence if applied to the elements of $l. +
    gives the index of $k$ in the elements of $l$ if $f$ is applied to the \ + latter prior to comparison. Note that $f$ \ + needs to yield a sorted sequence if applied to the elements of $l$.
    + Number 100 is found at exactly in the fourth place of the given list: + >> CombinatoricaOld`BinarySearch[{3, 4, 10, 100, 123}, 100] = 4 + Number 7 is found in between the second and third place (3, and 9)\ + of the given list. The numerical difference between 3 and 9 does \ + not figure into the .5 part of 2.5: + >> CombinatoricaOld`BinarySearch[{2, 3, 9}, 7] // N = 2.5 - >> CombinatoricaOld`BinarySearch[{2, 7, 9, 10}, 3] // N - = 1.5 + 0.5 is what you get when the item comes before the given list: >> CombinatoricaOld`BinarySearch[{-10, 5, 8, 10}, -100] // N = 0.5 + And here is what you see when the item comes at the end of the list: + >> CombinatoricaOld`BinarySearch[{-10, 5, 8, 10}, 20] // N = 4.5 @@ -90,22 +102,22 @@ class BinarySearch(Builtin): context = "CombinatoricaOld`" rules = { - "CombinatoricaOld`BinarySearch[l_List, k_] /; Length[l] > 0": "CombinatoricaOld`BinarySearch[l, k, Identity]" + "CombinatoricaOld`BinarySearch[li_List, k_] /; Length[li] > 0": "CombinatoricaOld`BinarySearch[li, k, Identity]" } summary_text = "search a sorted list for a key" - def apply(self, l, k, f, evaluation): - "CombinatoricaOld`BinarySearch[l_List, k_, f_] /; Length[l] > 0" + def eval(self, li, k, f, evaluation: Evaluation): + "CombinatoricaOld`BinarySearch[li_List, k_, f_] /; Length[li] > 0" - elements = l.elements + elements = li.elements lower_index = 1 upper_index = len(elements) if ( lower_index > upper_index - ): # empty list l? Length[l] > 0 condition should guard us, but check anyway + ): # empty list li? Length[l] > 0 condition should guard us, but check anyway return Symbol("$Aborted") # "transform" is a handy wrapper for applying "f" or nothing @@ -143,28 +155,6 @@ def transform(x): lower_index = pivot_index + 1 -class ByteCount(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/ByteCount.html - -
    -
    'ByteCount[$expr$]' -
    gives the internal memory space used by $expr$, in bytes. -
    - - The results may heavily depend on the Python implementation in use. - """ - - summary_text = "amount of memory used by expr, in bytes" - - def apply(self, expression, evaluation): - "ByteCount[expression_]" - if not bytecount_support: - return evaluation.message("ByteCount", "pypy") - else: - return Integer(count_bytes(expression)) - - class Depth(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Depth.html @@ -196,210 +186,17 @@ class Depth(Builtin): summary_text = "the maximum number of indices to specify any part" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation): "Depth[expr_]" expr, depth = walk_levels(expr) return Integer(depth + 1) -class Flatten(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Flatten.html - -
    -
    'Flatten[$expr$]' -
    flattens out nested lists in $expr$. - -
    'Flatten[$expr$, $n$]' -
    stops flattening at level $n$. - -
    'Flatten[$expr$, $n$, $h$]' -
    flattens expressions with head $h$ instead of 'List'. -
    - - >> Flatten[{{a, b}, {c, {d}, e}, {f, {g, h}}}] - = {a, b, c, d, e, f, g, h} - >> Flatten[{{a, b}, {c, {e}, e}, {f, {g, h}}}, 1] - = {a, b, c, {e}, e, f, {g, h}} - >> Flatten[f[a, f[b, f[c, d]], e], Infinity, f] - = f[a, b, c, d, e] - - >> Flatten[{{a, b}, {c, d}}, {{2}, {1}}] - = {{a, c}, {b, d}} - - >> Flatten[{{a, b}, {c, d}}, {{1, 2}}] - = {a, b, c, d} - - Flatten also works in irregularly shaped arrays - >> Flatten[{{1, 2, 3}, {4}, {6, 7}, {8, 9, 10}}, {{2}, {1}}] - = {{1, 4, 6, 8}, {2, 7, 9}, {3, 10}} - - #> Flatten[{{1, 2}, {3, 4}}, {{-1, 2}}] - : Levels to be flattened together in {{-1, 2}} should be lists of positive integers. - = Flatten[{{1, 2}, {3, 4}}, {{-1, 2}}, List] - - #> Flatten[{a, b}, {{1}, {2}}] - : Level 2 specified in {{1}, {2}} exceeds the levels, 1, which can be flattened together in {a, b}. - = Flatten[{a, b}, {{1}, {2}}, List] - - ## Check `n` completion - #> m = {{{1, 2}, {3}}, {{4}, {5, 6}}}; - #> Flatten[m, {{2}, {1}, {3}, {4}}] - : Level 4 specified in {{2}, {1}, {3}, {4}} exceeds the levels, 3, which can be flattened together in {{{1, 2}, {3}}, {{4}, {5, 6}}}. - = Flatten[{{{1, 2}, {3}}, {{4}, {5, 6}}}, {{2}, {1}, {3}, {4}}, List] - - ## Test from issue #251 - #> m = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; - #> Flatten[m, {3}] - : Level 3 specified in {3} exceeds the levels, 2, which can be flattened together in {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}. - = Flatten[{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}, {3}, List] - - ## Reproduce strange head behaviour - #> Flatten[{{1}, 2}, {1, 2}] - : Level 2 specified in {1, 2} exceeds the levels, 1, which can be flattened together in {{1}, 2}. - = Flatten[{{1}, 2}, {1, 2}, List] - #> Flatten[a[b[1, 2], b[3]], {1, 2}, b] (* MMA BUG: {{1, 2}} not {1, 2} *) - : Level 1 specified in {1, 2} exceeds the levels, 0, which can be flattened together in a[b[1, 2], b[3]]. - = Flatten[a[b[1, 2], b[3]], {1, 2}, b] - - #> Flatten[{{1, 2}, {3, {4}}}, {{1, 2, 3}}] - : Level 3 specified in {{1, 2, 3}} exceeds the levels, 2, which can be flattened together in {{1, 2}, {3, {4}}}. - = Flatten[{{1, 2}, {3, {4}}}, {{1, 2, 3}}, List] - """ - - messages = { - "flpi": ( - "Levels to be flattened together in `1` " - "should be lists of positive integers." - ), - "flrep": ("Level `1` specified in `2` should not be repeated."), - "fldep": ( - "Level `1` specified in `2` exceeds the levels, `3`, " - "which can be flattened together in `4`." - ), - } - - rules = { - "Flatten[expr_]": "Flatten[expr, Infinity, Head[expr]]", - "Flatten[expr_, n_]": "Flatten[expr, n, Head[expr]]", - } - - summary_text = "flatten out any sequence of levels in a nested list" - - def apply_list(self, expr, n, h, evaluation): - "Flatten[expr_, n_List, h_]" - - # prepare levels - # find max depth which matches `h` - expr, max_depth = walk_levels(expr) - max_depth = {"max_depth": max_depth} # hack to modify max_depth from callback - - def callback(expr, pos): - if len(pos) < max_depth["max_depth"] and ( - isinstance(expr, Atom) or expr.head != h - ): - max_depth["max_depth"] = len(pos) - return expr - - expr, depth = walk_levels(expr, callback=callback, include_pos=True, start=0) - max_depth = max_depth["max_depth"] - - levels = n.to_python() - - # mappings - if isinstance(levels, list) and all(isinstance(level, int) for level in levels): - levels = [levels] - - # verify levels is list of lists of positive ints - if not (isinstance(levels, list) and len(levels) > 0): - evaluation.message("Flatten", "flpi", n) - return - seen_levels = [] - for level in levels: - if not (isinstance(level, list) and len(level) > 0): - evaluation.message("Flatten", "flpi", n) - return - for r in level: - if not (isinstance(r, int) and r > 0): - evaluation.message("Flatten", "flpi", n) - return - if r in seen_levels: - # level repeated - evaluation.message("Flatten", "flrep", r) - return - seen_levels.append(r) - - # complete the level spec e.g. {{2}} -> {{2}, {1}, {3}} - for s in range(1, max_depth + 1): - if s not in seen_levels: - levels.append([s]) - - # verify specified levels are smaller max depth - for level in levels: - for s in level: - if s > max_depth: - evaluation.message("Flatten", "fldep", s, n, max_depth, expr) - return - - # assign new indices to each element - new_indices = {} - - def callback(expr, pos): - if len(pos) == max_depth: - new_depth = tuple(tuple(pos[i - 1] for i in level) for level in levels) - new_indices[new_depth] = expr - return expr - - expr, depth = walk_levels(expr, callback=callback, include_pos=True) - - # build new tree inserting nodes as needed - elements = sorted(new_indices.items()) - - def insert_element(elements): - # gather elements into groups with the same leading index - # e.g. [((0, 0), a), ((0, 1), b), ((1, 0), c), ((1, 1), d)] - # -> [[(0, a), (1, b)], [(0, c), (1, d)]] - leading_index = None - grouped_elements = [] - for index, element in elements: - if index[0] == leading_index: - grouped_elements[-1].append((index[1:], element)) - else: - leading_index = index[0] - grouped_elements.append([(index[1:], element)]) - # for each group of elements we either insert them into the current level - # or make a new level and recurse - new_elements = [] - for group in grouped_elements: - if len(group[0][0]) == 0: # bottom level element or leaf - assert len(group) == 1 - new_elements.append(group[0][1]) - else: - new_elements.append(Expression(h, *insert_element(group))) - - return new_elements - - return Expression(h, *insert_element(elements)) - - def apply(self, expr, n, h, evaluation): - "Flatten[expr_, n_, h_]" - - if n == Expression(SymbolDirectedInfinity, Integer1): - n = -1 # a negative number indicates an unbounded level - else: - n_int = n.get_int_value() - # Here we test for negative since in Mathics Flatten[] as opposed to flatten_with_respect_to_head() - # negative numbers (and None) are not allowed. - if n_int is None or n_int < 0: - return evaluation.message("Flatten", "flpi", n) - n = n_int - - return expr.flatten_with_respect_to_head(h, level=n) - - class FreeQ(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/FreeQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/FreeQ.html
    'FreeQ[$expr$, $x$]' @@ -428,7 +225,7 @@ class FreeQ(Builtin): "test whether an expression is free of subexpressions matching a pattern" ) - def apply(self, expr, form, evaluation): + def eval(self, expr, form, evaluation: Evaluation): "FreeQ[expr_, form_]" form = Pattern.create(form) @@ -438,9 +235,92 @@ def apply(self, expr, form, evaluation): return SymbolFalse +class Level(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/Level.html + +
    +
    'Level[$expr$, $levelspec$]' +
    gives a list of all subexpressions of $expr$ at the + level(s) specified by $levelspec$. +
    + + Level uses standard level specifications: + +
    +
    $n$ +
    levels 1 through $n$ +
    'Infinity' +
    all levels from level 1 +
    '{$n$}' +
    level $n$ only +
    '{$m$, $n$}' +
    levels $m$ through $n$ +
    + + Level 0 corresponds to the whole expression. + + A negative level '-$n$' consists of parts with depth $n$. + + Level -1 is the set of atoms in an expression: + >> Level[a + b ^ 3 * f[2 x ^ 2], {-1}] + = {a, b, 3, 2, x, 2} + + >> Level[{{{{a}}}}, 3] + = {{a}, {{a}}, {{{a}}}} + >> Level[{{{{a}}}}, -4] + = {{{{a}}}} + >> Level[{{{{a}}}}, -5] + = {} + + >> Level[h0[h1[h2[h3[a]]]], {0, -1}] + = {a, h3[a], h2[h3[a]], h1[h2[h3[a]]], h0[h1[h2[h3[a]]]]} + + Use the option 'Heads -> True' to include heads: + >> Level[{{{{a}}}}, 3, Heads -> True] + = {List, List, List, {a}, {{a}}, {{{a}}}} + >> Level[x^2 + y^3, 3, Heads -> True] + = {Plus, Power, x, 2, x ^ 2, Power, y, 3, y ^ 3} + + >> Level[a ^ 2 + 2 * b, {-1}, Heads -> True] + = {Plus, Power, a, 2, Times, 2, b} + >> Level[f[g[h]][x], {-1}, Heads -> True] + = {f, g, h, x} + >> Level[f[g[h]][x], {-2, -1}, Heads -> True] + = {f, g, h, g[h], x, f[g[h]][x]} + """ + + options = { + "Heads": "False", + } + summary_text = "parts specified by a given number of indices" + + def eval(self, expr, ls, evaluation, options={}): + "Level[expr_, ls_, OptionsPattern[Level]]" + + try: + start, stop = python_levelspec(ls) + except InvalidLevelspecError: + evaluation.message("Level", "level", ls) + return + result = [] + + def callback(level): + result.append(level) + return level + + heads = self.get_option(options, "Heads", evaluation) is SymbolTrue + walk_levels(expr, start, stop, heads=heads, callback=callback) + return ListExpression(*result) + + class Null(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/Null.html + + :WMA link: + https://reference.wolfram.com/language/ref/Null.html
    'Null' @@ -462,7 +342,9 @@ class Null(Predefined): class Operate(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Operate.html + + :WMA link: + https://reference.wolfram.com/language/ref/Operate.html
    'Operate[$p$, $expr$]' @@ -493,14 +375,15 @@ class Operate(Builtin): "intnn": "Non-negative integer expected at position `2` in `1`.", } - def apply(self, p, expr, n, evaluation): + def eval(self, p, expr, n, evaluation: Evaluation): "Operate[p_, expr_, Optional[n_, 1]]" head_depth = n.get_int_value() if head_depth is None or head_depth < 0: - return evaluation.message( + evaluation.message( "Operate", "intnn", Expression(SymbolOperate, p, expr, n), 3 ) + return if head_depth == 0: # Act like Apply @@ -533,8 +416,10 @@ class Order(Builtin):
    'Order[$x$, $y$]' -
    returns a number indicating the canonical ordering of $x$ and $y$. 1 indicates that $x$ is before $y$, - -1 that $y$ is before $x$. 0 indicates that there is no specific ordering. Uses the same order as 'Sort'. +
    returns a number indicating the canonical ordering of $x$ and $y$. \ + 1 indicates that $x$ is before $y$, \-1 that $y$ is before $x$. \ + 0 indicates that there is no specific ordering. Uses the same order \ + as 'Sort'.
    >> Order[7, 11] @@ -552,7 +437,7 @@ class Order(Builtin): summary_text = "canonical ordering of expressions" - def apply(self, x, y, evaluation): + def eval(self, x, y, evaluation: Evaluation): "Order[x_, y_]" if x < y: return Integer1 @@ -564,7 +449,9 @@ def apply(self, x, y, evaluation): class OrderedQ(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/OrderedQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/OrderedQ.html
    'OrderedQ[{$a$, $b$}]' @@ -580,7 +467,7 @@ class OrderedQ(Builtin): summary_text = "test whether elements are canonically sorted" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation): "OrderedQ[expr_]" for index, value in enumerate(expr.elements[:-1]): @@ -593,7 +480,9 @@ def apply(self, expr, evaluation): class PatternsOrderedQ(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/PatternsOrderedQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/PatternsOrderedQ.html
    'PatternsOrderedQ[$patt1$, $patt2$]' @@ -611,7 +500,7 @@ class PatternsOrderedQ(Builtin): summary_text = "test whether patterns are canonically sorted" - def apply(self, p1, p2, evaluation): + def eval(self, p1, p2, evaluation: Evaluation): "PatternsOrderedQ[p1_, p2_]" if p1.get_sort_key(True) <= p2.get_sort_key(True): @@ -622,13 +511,17 @@ def apply(self, p1, p2, evaluation): class SortBy(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/SortBy.html + + :WMA link: + https://reference.wolfram.com/language/ref/SortBy.html
    'SortBy[$list$, $f$]' -
    sorts $list$ (or the elements of any other expression) according to canonical ordering of the keys that are - extracted from the $list$'s elements using $f. Chunks of elements that appear the same under $f are sorted - according to their natural order (without applying $f). +
    sorts $list$ (or the elements of any other expression) according to \ + canonical ordering of the keys that are extracted from the $list$'s \ + elements using $f. Chunks of elements that appear the same under $f \ + are sorted according to their natural order (without applying $f). +
    'SortBy[$f$]'
    creates an operator function that, when applied, sorts by $f.
    @@ -651,14 +544,16 @@ class SortBy(Builtin): summary_text = "sort by the values of a function applied to elements" - def apply(self, li, f, evaluation): + def eval(self, li, f, evaluation: Evaluation): "SortBy[li_, f_]" if isinstance(li, Atom): - return evaluation.message("Sort", "normal") + evaluation.message("Sort", "normal") + return elif li.get_head_name() != "System`List": expr = Expression(SymbolSortBy, li, f) - return evaluation.message(self.get_name(), "list", expr, 1) + evaluation.message(self.get_name(), "list", expr, 1) + return else: keys_expr = Expression(SymbolMap, f, li).evaluate(evaluation) # precompute: # even though our sort function has only (n log n) comparisons, we should @@ -670,7 +565,8 @@ def apply(self, li, f, evaluation): or len(keys_expr.elements) != len(li.elements) ): expr = Expression(SymbolSortBy, li, f) - return evaluation.message("SortBy", "func", expr, 2) + evaluation.message("SortBy", "func", expr, 2) + return keys = keys_expr.elements raw_keys = li.elements @@ -696,7 +592,9 @@ def __gt__(self, other): class Through(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Through.html + + :WMA link: + https://reference.wolfram.com/language/ref/Through.html
    'Through[$p$[$f$][$x$]]' @@ -711,7 +609,7 @@ class Through(Builtin): summary_text = "distribute operators that appears inside the head of expressions" - def apply(self, p, args, x, evaluation): + def eval(self, p, args, x, evaluation: Evaluation): "Through[p_[args___][x___]]" elements = [] diff --git a/mathics/builtin/exp_structure/size_and_sig.py b/mathics/builtin/exp_structure/size_and_sig.py new file mode 100644 index 000000000..68b497dba --- /dev/null +++ b/mathics/builtin/exp_structure/size_and_sig.py @@ -0,0 +1,209 @@ +""" +Expression Sizes and Signatures +""" +import hashlib +import platform +import zlib + +from mathics.builtin.base import Builtin +from mathics.core.atoms import ByteArrayAtom, Integer, String +from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.systemsymbols import SymbolByteArray +from mathics.eval.parts import walk_levels + +if platform.python_implementation() == "PyPy": + bytecount_support = False +else: + from mathics.builtin.pympler.asizeof import asizeof as count_bytes + + bytecount_support = True + +# This tells documentation how to sort this module +sort_order = "mathics.builtin.exp_structure.exp_sizes_and" + + +class _ZLibHash: # make zlib hashes behave as if they were from hashlib + def __init__(self, fn): + self._bytes = b"" + self._fn = fn + + def update(self, bytes): + self._bytes += bytes + + def hexdigest(self): + return format(self._fn(self._bytes), "x") + + +class ByteCount(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/ByteCount.html + +
    +
    'ByteCount[$expr$]' +
    gives the internal memory space used by $expr$, in bytes. +
    + + The results may heavily depend on the Python implementation in use. + """ + + summary_text = "amount of memory used by expr, in bytes" + + def eval(self, expression, evaluation: Evaluation): + "ByteCount[expression_]" + if not bytecount_support: + evaluation.message("ByteCount", "pypy") + else: + return Integer(count_bytes(expression)) + + +class Hash(Builtin): + """ + :Hash function:https://en.wikipedia.org/wiki/Hash_function \ + (:WMA link:https://reference.wolfram.com/language/ref/Hash.html) + +
    +
    'Hash[$expr$]' +
    returns an integer hash for the given $expr$. + +
    'Hash[$expr$, $type$]' +
    returns an integer hash of the specified $type$ for the given $expr$. +
    The types supported are "MD5", "Adler32", "CRC32", "SHA", "SHA224", "SHA256", "SHA384", and "SHA512". + +
    'Hash[$expr$, $type$, $format$]' +
    Returns the hash in the specified format. +
    + + > Hash["The Adventures of Huckleberry Finn"] + = 213425047836523694663619736686226550816 + + > Hash["The Adventures of Huckleberry Finn", "SHA256"] + = 95092649594590384288057183408609254918934351811669818342876362244564858646638 + + > Hash[1/3] + = 56073172797010645108327809727054836008 + + > Hash[{a, b, {c, {d, e, f}}}] + = 135682164776235407777080772547528225284 + + > Hash[SomeHead[3.1415]] + = 58042316473471877315442015469706095084 + + >> Hash[{a, b, c}, "xyzstr"] + = Hash[{a, b, c}, xyzstr, Integer] + """ + + attributes = A_PROTECTED | A_READ_PROTECTED + + rules = { + "Hash[expr_]": 'Hash[expr, "MD5", "Integer"]', + "Hash[expr_, type_String]": 'Hash[expr, type, "Integer"]', + } + + summary_text = "compute hash codes for a string" + + # FIXME md2 + _supported_hashes = { + "Adler32": lambda: _ZLibHash(zlib.adler32), + "CRC32": lambda: _ZLibHash(zlib.crc32), + "MD5": hashlib.md5, + "SHA": hashlib.sha1, + "SHA224": hashlib.sha224, + "SHA256": hashlib.sha256, + "SHA384": hashlib.sha384, + "SHA512": hashlib.sha512, + } + + @staticmethod + def compute(user_hash, py_hashtype, py_format): + hash_func = Hash._supported_hashes.get(py_hashtype) + if hash_func is None: # unknown hash function? + return # in order to return original Expression + h = hash_func() + user_hash(h.update) + res = h.hexdigest() + if py_format in ("HexString", "HexStringLittleEndian"): + return String(res) + res = int(res, 16) + if py_format == "DecimalString": + return String(str(res)) + elif py_format == "ByteArray": + return Expression(SymbolByteArray, ByteArrayAtom(res)) + return Integer(res) + + def eval(self, expr, hashtype: String, outformat: String, evaluation: Evaluation): + "Hash[expr_, hashtype_String, outformat_String]" + return Hash.compute(expr.user_hash, hashtype.value, outformat.value) + + +class LeafCount(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/LeafCount.html + +
    +
    'LeafCount[$expr$]' +
    returns the total number of indivisible subexpressions in $expr$. +
    + + >> LeafCount[1 + x + y^a] + = 6 + + >> LeafCount[f[x, y]] + = 3 + + >> LeafCount[{1 / 3, 1 + I}] + = 7 + + >> LeafCount[Sqrt[2]] + = 5 + + >> LeafCount[100!] + = 1 + + #> LeafCount[f[a, b][x, y]] + = 5 + + #> NestList[# /. s[x_][y_][z_] -> x[z][y[z]] &, s[s][s][s[s]][s][s], 4]; + #> LeafCount /@ % + = {7, 8, 8, 11, 11} + + #> LeafCount[1 / 3, 1 + I] + : LeafCount called with 2 arguments; 1 argument is expected. + = LeafCount[1 / 3, 1 + I] + """ + + messages = { + "argx": "LeafCount called with `1` arguments; 1 argument is expected.", + } + summary_text = "the total number of atomic subexpressions" + + def eval(self, expr, evaluation: Evaluation): + "LeafCount[expr___]" + + from mathics.core.atoms import Complex, Rational + + elements = [] + + def callback(level): + if isinstance(level, Rational): + elements.extend( + [level.get_head(), level.numerator(), level.denominator()] + ) + elif isinstance(level, Complex): + elements.extend([level.get_head(), level.real, level.imag]) + else: + elements.append(level) + return level + + expr = expr.get_sequence() + if len(expr) != 1: + evaluation.message("LeafCount", "argx", Integer(len(expr))) + return + + walk_levels(expr[0], start=-1, stop=-1, heads=True, callback=callback) + return Integer(len(elements)) diff --git a/mathics/builtin/fileformats/xmlformat.py b/mathics/builtin/fileformats/xmlformat.py index 16012d370..181b2153c 100644 --- a/mathics/builtin/fileformats/xmlformat.py +++ b/mathics/builtin/fileformats/xmlformat.py @@ -15,7 +15,7 @@ from mathics.core.atoms import String from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.convert.python import from_python -from mathics.core.expression import Expression +from mathics.core.expression import Evaluation, Expression from mathics.core.symbols import Symbol from mathics.core.systemsymbols import SymbolFailed @@ -211,7 +211,7 @@ def parse_xml_file(filename): return root -def parse_xml(parse, text, evaluation): +def parse_xml(parse, text, evaluation: Evaluation): try: return parse(text.get_string_value()) except ParseError as e: @@ -261,7 +261,7 @@ class _Get(Builtin): "prserr": "``.", } - def apply(self, text, evaluation): + def eval(self, text, evaluation: Evaluation): """%(name)s[text_String]""" root = parse_xml(self._parse, text, evaluation) if isinstance(root, Symbol): # $Failed? @@ -329,7 +329,7 @@ class PlaintextImport(Builtin): summary_text = "import plain text from xml" context = "XML`" - def apply(self, text, evaluation): + def eval(self, text, evaluation: Evaluation): """%(name)s[text_String]""" root = parse_xml(parse_xml_file, text, evaluation) if isinstance(root, Symbol): # $Failed? @@ -373,7 +373,7 @@ def gather(node): gather(root) return to_mathics_list(*[String(tag) for tag in sorted(list(tags))]) - def apply(self, text, evaluation): + def eval(self, text, evaluation: Evaluation): """%(name)s[text_String]""" root = parse_xml(parse_xml_file, text, evaluation) if isinstance(root, Symbol): # $Failed? @@ -400,7 +400,7 @@ class XMLObjectImport(Builtin): summary_text = "import elements from xml" context = "XML`" - def apply(self, text, evaluation): + def eval(self, text, evaluation: Evaluation): """%(name)s[text_String]""" xml = to_expression("XML`Parser`XMLGet", text).evaluate(evaluation) return to_mathics_list(to_expression("Rule", "XMLObject", xml)) diff --git a/mathics/builtin/files_io/files.py b/mathics/builtin/files_io/files.py index 80577da0c..294a7ce34 100644 --- a/mathics/builtin/files_io/files.py +++ b/mathics/builtin/files_io/files.py @@ -26,6 +26,7 @@ from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.convert.python import from_python +from mathics.core.evaluation import Evaluation from mathics.core.expression import BoxError, Expression from mathics.core.parser import MathicsFileLineFeeder, parse from mathics.core.read import ( @@ -39,10 +40,11 @@ read_name_and_stream_from_channel, ) from mathics.core.streams import path_search, stream_manager -from mathics.core.symbols import Symbol, SymbolNull, SymbolTrue +from mathics.core.symbols import Symbol, SymbolFullForm, SymbolNull, SymbolTrue from mathics.core.systemsymbols import ( SymbolFailed, SymbolHold, + SymbolInputForm, SymbolOutputForm, SymbolReal, ) @@ -110,7 +112,7 @@ class _OpenAction(Builtin): ), } - def eval_empty(self, evaluation, options): + def eval_empty(self, evaluation: Evaluation, options: dict): "%(name)s[OptionsPattern[]]" if isinstance(self, (OpenWrite, OpenAppend)): @@ -128,7 +130,7 @@ def eval_empty(self, evaluation, options): evaluation.message("OpenRead", "argx") return - def eval_path(self, path, evaluation, options): + def eval_path(self, path, evaluation: Evaluation, options: dict): "%(name)s[path_?NotOptionQ, OptionsPattern[]]" # Options @@ -267,8 +269,8 @@ class Expression_(Builtin): https://mathics-development-guide.readthedocs.io/en/latest/extending/code-overview/ast.html. """ - summary_text = "WL expression" name = "Expression" + summary_text = "WL expression" class FilePrint(Builtin): @@ -285,16 +287,15 @@ class FilePrint(Builtin): : File specification Sin[1] is not a string of one or more characters. = FilePrint[Sin[1]] - #> FilePrint["somenonexistantpath_h47sdmk^&h4"] - : Cannot open somenonexistantpath_h47sdmk^&h4. - = FilePrint[somenonexistantpath_h47sdmk^&h4] + #> FilePrint["somenonexistentpath_h47sdmk^&h4"] + : Cannot open somenonexistentpath_h47sdmk^&h4. + = FilePrint[somenonexistentpath_h47sdmk^&h4] #> FilePrint[""] : File specification is not a string of one or more characters. = FilePrint[] """ - summary_text = "display the contents of a file" messages = { "fstr": ( "File specification `1` is not a string of " "one or more characters." @@ -306,8 +307,9 @@ class FilePrint(Builtin): "RecordSeparators": '{"\r\n", "\n", "\r"}', "WordSeparators": '{" ", "\t"}', } + summary_text = "display the contents of a file" - def eval(self, path, evaluation, options): + def eval(self, path, evaluation: Evaluation, options: dict): "FilePrint[path_, OptionsPattern[FilePrint]]" pypath = path.to_python() if not ( @@ -367,8 +369,8 @@ class Number_(Builtin):
    """ - summary_text = "exact or approximate number in Fortran‐like notation" name = "Number" + summary_text = "exact or approximate number in Fortran‐like notation" class Get(PrefixOperator): @@ -408,14 +410,14 @@ class Get(PrefixOperator): #> Hold[<<`/.\-_:$*~?] // FullForm = Hold[Get["`/.\\\\-_:$*~?"]] """ - summary_text = "read in a file and evaluate commands in it" operator = "<<" - precedence = 720 options = { "Trace": "False", } + precedence = 720 + summary_text = "read in a file and evaluate commands in it" - def eval(self, path, evaluation, options): + def eval(self, path, evaluation: Evaluation, options: dict): "Get[path_String, OptionsPattern[Get]]" def check_options(options): @@ -607,11 +609,11 @@ class OpenAppend(_OpenAction): #> DeleteFile["MathicsNonExampleFile"] """ + mode = "a" + stream_type = "OutputStream" summary_text = ( "open an output stream to a file, appending to what was already in the file" ) - mode = "a" - stream_type = "OutputStream" class Put(BinaryOperator): @@ -667,9 +669,9 @@ class Put(BinaryOperator): S> DeleteFile[filename] """ - summary_text = "write an expression to a file" operator = ">>" precedence = 30 + summary_text = "write an expression to a file" def eval(self, exprs, filename, evaluation): "Put[exprs___, filename_String]" @@ -693,10 +695,18 @@ def eval_input(self, exprs, name, n, evaluation): evaluation.message("Put", "openx", to_expression("OutputSteam", name, n)) return - text = [ - evaluation.format_output(to_expression("InputForm", expr)) - for expr in exprs.get_sequence() - ] + # In Mathics-server, evaluation.format_output is modified. + # Let's avoid to use it if we want a front-end independent result. + # Eventually, we are going to replace this by a `MakeBoxes` call. + def do_format_output(expr, evaluation): + try: + boxed_expr = format_element(expr, evaluation, SymbolInputForm) + except BoxError: + boxed_expr = format_element(expr, evaluation, SymbolFullForm) + + return boxed_expr.boxes_to_text() + + text = [do_format_output(expr, evaluation) for expr in exprs.get_sequence()] text = "\n".join(text) + "\n" text.encode("utf-8") @@ -765,9 +775,9 @@ class PutAppend(BinaryOperator): = x >>> /proc/uptime """ - summary_text = "append an expression to a file" operator = ">>>" precedence = 30 + summary_text = "append an expression to a file" def eval(self, exprs, filename, evaluation): "PutAppend[exprs___, filename_String]" @@ -927,7 +937,7 @@ class Read(Builtin): = 5 >> Close[stream]; - Reading a comment however will return the empy list: + Reading a comment however will return the empty list: >> stream = StringToStream["(* ::Package:: *)"]; >> Read[stream, Hold[Expression]] @@ -962,7 +972,6 @@ class Read(Builtin): """ - summary_text = "read an object of the specified type from a stream" messages = { "openx": "`1` is not open.", "readf": "`1` is not a valid format specification.", @@ -984,6 +993,7 @@ class Read(Builtin): "TokenWords": "{}", "WordSeparators": '{" ", "\t"}', } + summary_text = "read an object of the specified type from a stream" def check_options(self, options): # Options @@ -1050,7 +1060,7 @@ def check_options(self, options): return result - def eval(self, channel, types, evaluation, options): + def eval(self, channel, types, evaluation: Evaluation, options: dict): "Read[channel_, types_, OptionsPattern[Read]]" name, n, stream = read_name_and_stream_from_channel(channel, evaluation) @@ -1258,7 +1268,6 @@ class ReadList(Read): >> InputForm[%] = {123, abc} """ - summary_text = "read a sequence of elements from a file, and put them in a WL list" rules = { "ReadList[stream_]": "ReadList[stream, Expression]", } @@ -1270,8 +1279,9 @@ class ReadList(Read): "TokenWords": "{}", "WordSeparators": '{" ", "\t"}', } + summary_text = "read a sequence of elements from a file, and put them in a WL list" - def eval(self, channel, types, evaluation, options): + def eval(self, channel, types, evaluation: Evaluation, options: dict): "ReadList[channel_, types_, OptionsPattern[ReadList]]" # Options @@ -1298,7 +1308,7 @@ def eval(self, channel, types, evaluation, options): result.append(tmp) return from_python(result) - def eval_m(self, channel, types, m, evaluation, options): + def eval_m(self, channel, types, m, evaluation: Evaluation, options: dict): "ReadList[channel_, types_, m_, OptionsPattern[ReadList]]" # Options @@ -1507,7 +1517,7 @@ class Skip(Read): } summary_text = "skip over an object of the specified type in an input stream" - def eval(self, name, n, types, m, evaluation, options): + def eval(self, name, n, types, m, evaluation: Evaluation, options: dict): "Skip[InputStream[name_, n_], types_, m_, OptionsPattern[Skip]]" channel = to_expression("InputStream", name, n) @@ -1571,7 +1581,7 @@ class Find(Read): } summary_text = "find the next occurrence of a string" - def eval(self, name, n, text, evaluation, options): + def eval(self, name, n, text, evaluation: Evaluation, options: dict): "Find[InputStream[name_, n_], text_, OptionsPattern[Find]]" # Options @@ -1686,7 +1696,7 @@ class Streams(Builtin): #> Streams[%[[1]]] = {OutputStream[...]} - #> Streams["some_nonexistant_name"] + #> Streams["some_nonexistent_name"] = {} """ @@ -1879,11 +1889,12 @@ def eval(self, channel, expr, evaluation): try: result = result.boxes_to_text(evaluation=evaluation) except BoxError: - return evaluation.message( + evaluation.message( "General", "notboxes", to_expression("FullForm", result).evaluate(evaluation), ) + return exprs.append(result) line = "".join(exprs) if type(stream) is BytesIO: diff --git a/mathics/builtin/files_io/filesystem.py b/mathics/builtin/files_io/filesystem.py index 43b5fefc0..72323cd07 100644 --- a/mathics/builtin/files_io/filesystem.py +++ b/mathics/builtin/files_io/filesystem.py @@ -14,9 +14,9 @@ from mathics.builtin.atomic.strings import to_regex from mathics.builtin.base import Builtin, MessageException, Predefined +from mathics.builtin.exp_structure.size_and_sig import Hash from mathics.builtin.files_io.files import INITIAL_DIR # noqa is used via global from mathics.builtin.files_io.files import DIRECTORY_STACK, MathicsOpen -from mathics.builtin.string.operations import Hash from mathics.core.atoms import Integer, Real, String from mathics.core.attributes import ( A_LISTABLE, @@ -27,6 +27,7 @@ ) from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.convert.python import from_python +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.streams import ( HOME_DIR, @@ -270,7 +271,7 @@ class CreateDirectory(Builtin): } summary_text = "create a directory" - def eval(self, dirname, evaluation, options): + def eval(self, dirname, evaluation: Evaluation, options: dict): "CreateDirectory[dirname_, OptionsPattern[CreateDirectory]]" expr = to_expression("CreateDirectory", dirname) @@ -294,7 +295,7 @@ def eval(self, dirname, evaluation, options): return String(osp.abspath(py_dirname)) - def eval_empty(self, evaluation, options): + def eval_empty(self, evaluation: Evaluation, options: dict): "CreateDirectory[OptionsPattern[CreateDirectory]]" dirname = tempfile.mkdtemp(prefix="m", dir=TMP_DIR) return String(dirname) @@ -334,7 +335,7 @@ def eval(self, filename, evaluation, **options): return String(res) else: return filename - except: + except Exception: return SymbolFailed @@ -354,7 +355,7 @@ def eval_0(self, evaluation): "CreateTemporary[]" try: res = create_temporary_file() - except: + except Exception: return SymbolFailed return String(res) @@ -391,7 +392,7 @@ class DeleteDirectory(Builtin): } summary_text = "delete a directory" - def eval(self, dirname, evaluation, options): + def eval(self, dirname, evaluation: Evaluation, options: dict): "DeleteDirectory[dirname_, OptionsPattern[DeleteDirectory]]" expr = to_expression("DeleteDirectory", dirname) @@ -554,7 +555,7 @@ class DirectoryName(Builtin): } summary_text = "directory part of a filename" - def eval_with_n(self, name, n, evaluation, options): + def eval_with_n(self, name, n, evaluation: Evaluation, options: dict): "DirectoryName[name_, n_, OptionsPattern[DirectoryName]]" if n is None: @@ -580,7 +581,7 @@ def eval_with_n(self, name, n, evaluation, options): return String(result) - def eval(self, name, evaluation, options): + def eval(self, name, evaluation: Evaluation, options: dict): "DirectoryName[name_, OptionsPattern[DirectoryName]]" return self.eval_with_n(name, None, evaluation, options) @@ -723,7 +724,7 @@ class FileBaseName(Builtin): } summary_text = "base name of the file" - def eval(self, filename, evaluation, options): + def eval(self, filename, evaluation: Evaluation, options: dict): "FileBaseName[filename_String, OptionsPattern[FileBaseName]]" path = filename.to_python()[1:-1] @@ -944,7 +945,7 @@ class FileExtension(Builtin): } summary_text = "file extension" - def eval(self, filename, evaluation, options): + def eval(self, filename, evaluation: Evaluation, options: dict): "FileExtension[filename_String, OptionsPattern[FileExtension]]" path = filename.to_python()[1:-1] filename_base, filename_ext = osp.splitext(path) @@ -1116,7 +1117,7 @@ class FileNameJoin(Builtin): } summary_text = "join parts into a path" - def eval(self, pathlist, evaluation, options): + def eval(self, pathlist, evaluation: Evaluation, options: dict): "FileNameJoin[pathlist_List, OptionsPattern[FileNameJoin]]" py_pathlist = pathlist.to_python() @@ -1157,7 +1158,9 @@ def eval(self, pathlist, evaluation, options): class FileType(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/FileType.html + + :WMA link: + https://reference.wolfram.com/language/ref/FileType.html
    'FileType["$file$"]' @@ -1168,7 +1171,7 @@ class FileType(Builtin): = File >> FileType["ExampleData"] = Directory - >> FileType["ExampleData/nonexistant"] + >> FileType["ExampleData/nonexistent"] = None #> FileType[x] @@ -1418,7 +1421,7 @@ class FileNameSplit(Builtin): summary_text = "split the file name in a list of parts" - def eval(self, filename, evaluation, options): + def eval(self, filename, evaluation: Evaluation, options: dict): "FileNameSplit[filename_String, OptionsPattern[FileNameSplit]]" path = filename.to_python()[1:-1] @@ -1481,12 +1484,12 @@ class FileNameTake(Builtin): } summary_text = "take a part of the filename" - def eval(self, filename, evaluation, options): + def eval(self, filename, evaluation: Evaluation, options: dict): "FileNameTake[filename_String, OptionsPattern[FileBaseName]]" path = pathlib.Path(filename.to_python()[1:-1]) return String(path.name) - def eval_n(self, filename, n, evaluation, options): + def eval_n(self, filename, n, evaluation: Evaluation, options: dict): "FileNameTake[filename_String, n_Integer, OptionsPattern[FileBaseName]]" n_int = n.get_int_value() parts = pathlib.Path(filename.to_python()[1:-1]).parts @@ -1543,11 +1546,11 @@ class FindList(Builtin): # TODO: Extra options AnchoredSearch, IgnoreCase RecordSeparators, # WordSearch, WordSeparators this is probably best done with a regex - def eval_without_n(self, filename, text, evaluation, options): + def eval_without_n(self, filename, text, evaluation: Evaluation, options: dict): "FindList[filename_, text_, OptionsPattern[FindList]]" return self.eval(filename, text, None, evaluation, options) - def eval(self, filename, text, n, evaluation, options): + def eval(self, filename, text, n, evaluation: Evaluation, options: dict): "FindList[filename_, text_, n_, OptionsPattern[FindList]]" py_text = text.to_python() py_name = filename.to_python() @@ -1901,11 +1904,13 @@ def evaluate(self, evaluation): class PathnameSeparator(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/$PathnameSeparator.html + + :WMA link: + https://reference.wolfram.com/language/ref/$PathnameSeparator.html
    '$PathnameSeparator' -
    returns a string for the seperator in paths. +
    returns a string for the separator in paths.
    >> $PathnameSeparator @@ -2124,7 +2129,7 @@ def eval(self, path, evaluation): try: os.chdir(py_path) - except: + except Exception: return SymbolFailed DIRECTORY_STACK.append(os.getcwd()) @@ -2261,7 +2266,7 @@ def eval(self, filename, datelist, attribute, evaluation): os.utime(py_filename, (osp.getatime(py_filename), stattime)) if py_attr == "All": os.utime(py_filename, (stattime, stattime)) - except OSError: # as e: + except OSError: # evaluation.message(...) return SymbolFailed @@ -2334,7 +2339,7 @@ class UserBaseDirectory(Predefined):
    returns the folder where user configurations are stored.
    - >> $RootDirectory + >> $UserBaseDirectory = ... """ diff --git a/mathics/builtin/files_io/importexport.py b/mathics/builtin/files_io/importexport.py index 2b0b3852d..ad8353e06 100644 --- a/mathics/builtin/files_io/importexport.py +++ b/mathics/builtin/files_io/importexport.py @@ -1043,13 +1043,18 @@ class RegisterImport(Builtin):
    'RegisterImport["$format$", $defaultFunction$]' -
    register '$defaultFunction$' as the default function used when importing from a file of type '"$format$"'. - -
    'RegisterImport["$format$", {"$elem1$" :> $conditionalFunction1$, "$elem2$" :> $conditionalFunction2$, ..., $defaultFunction$}]' -
    registers multiple elements ($elem1$, ...) and their corresponding converter functions ($conditionalFunction1$, ...) in addition to the $defaultFunction$. - -
    'RegisterImport["$format$", {"$conditionalFunctions$, $defaultFunction$, "$elem3$" :> $postFunction3$, "$elem4$" :> $postFunction4$, ...}]' -
    also registers additional elements ($elem3$, ...) whose converters ($postFunction3$, ...) act on output from the low-level funcions. +
    register '$defaultFunction$' as the default function used when \ + importing from a file of type '"$format$"'. + +
    'RegisterImport["$format$", {"$elem1$" :> $conditionalFunction1$, \ + "$elem2$" :> $conditionalFunction2$, ..., $defaultFunction$}]' +
    registers multiple elements ($elem1$, ...) and their corresponding \ + converter functions ($conditionalFunction1$, ...) in addition to the $defaultFunction$. + +
    'RegisterImport["$format$", {"$conditionalFunctions$, $defaultFunction$, \ + "$elem3$" :> $postFunction3$, "$elem4$" :> $postFunction4$, ...}]' +
    also registers additional elements ($elem3$, ...) whose converters \ + ($postFunction3$, ...) act on output from the low-level functions.
    First, define the default function used to import the data. @@ -1210,17 +1215,17 @@ class RegisterExport(Builtin): context = "ImportExport`" options = { - "Path": "Automatic", - "FunctionChannels": '{"FileNames"}', - "Sources": "None", - "DefaultElement": "None", + "AlphaChannel": "False", "AvailableElements": "None", - "Options": "{}", - "OriginalChannel": "False", "BinaryFormat": "False", + "DefaultElement": "None", "Encoding": "False", "Extensions": "{}", - "AlphaChannel": "False", + "FunctionChannels": '{"FileNames"}', + "Options": "{}", + "OriginalChannel": "False", + "Path": "Automatic", + "Sources": "None", } def eval(self, formatname: String, function, evaluation: Evaluation, options): @@ -1495,6 +1500,8 @@ def get_results(tmp_function, findfile): stream = None import_expression = Expression(tmp_function, findfile, *joined_options) tmp = import_expression.evaluate(evaluation) + if tmp is SymbolFailed: + return SymbolFailed if tmpfile: Expression(SymbolDeleteFile, findfile).evaluate(evaluation) elif function_channels == ListExpression(String("Streams")): @@ -1536,6 +1543,8 @@ def get_results(tmp_function, findfile): if defaults is None: evaluation.predetermined_out = current_predetermined_out return SymbolFailed + elif defaults is SymbolFailed: + return SymbolFailed if default_element is Symbol("Automatic"): evaluation.predetermined_out = current_predetermined_out return ListExpression( @@ -2134,7 +2143,7 @@ class FileFormat(Builtin): >> FileFormat["ExampleData/EinsteinSzilLetter.txt"] = Text - >> FileFormat["ExampleData/lena.tif"] + >> FileFormat["ExampleData/hedy.tif"] = TIFF ## ASCII text diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 06061e9a4..76f05d1e3 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -1,21 +1,23 @@ # FIXME: split these forms up further. -# MathML and TeXForm feel more closely related since they go with specific kinds of interpreters: -# LaTeX and MathML +# MathML and TeXForm feel more closely related since they go with +# specific kinds of interpreters: LaTeX and MathML -# SympyForm and PythonForm feel related since are our own hacky thing (and mostly broken for now) +# SympyForm and PythonForm feel related since are our own hacky thing +# (and mostly broken for now) -# NumberForm, TableForm, and MatrixForm seem closely related since they seem to be relevant -# for particular kinds of structures rather than applicable to all kinds of expressions. +# NumberForm, TableForm, and MatrixForm seem closely related since +# they seem to be relevant for particular kinds of structures rather +# than applicable to all kinds of expressions. """ Forms which appear in '$OutputForms'. """ import re +from math import ceil from typing import Optional from mathics.builtin.base import Builtin from mathics.builtin.box.layout import GridBox, RowBox, to_boxes -from mathics.builtin.comparison import expr_min from mathics.builtin.forms.base import FormBaseClass from mathics.builtin.makeboxes import MakeBoxes, number_form from mathics.builtin.tensors import get_dimensions @@ -27,9 +29,15 @@ String, StringFromPython, ) +from mathics.core.evaluation import Evaluation from mathics.core.expression import BoxError, Expression from mathics.core.list import ListExpression -from mathics.core.number import convert_base, dps, machine_precision, reconstruct_digits +from mathics.core.number import ( + LOG2_10, + RECONSTRUCT_MACHINE_PRECISION_DIGITS, + convert_base, + dps, +) from mathics.core.symbols import ( Symbol, SymbolFalse, @@ -49,7 +57,8 @@ SymbolSubscriptBox, SymbolSuperscriptBox, ) -from mathics.eval.makeboxes import format_element +from mathics.eval.makeboxes import StringLParen, StringRParen, format_element +from mathics.eval.testing_expressions import expr_min MULTI_NEWLINE_RE = re.compile(r"\n{2,}") @@ -105,7 +114,7 @@ class BaseForm(Builtin): "basf": "Requested base `1` must be between 2 and 36.", } - def apply_makeboxes(self, expr, n, f, evaluation): + def eval_makeboxes(self, expr, n, f, evaluation: Evaluation): """MakeBoxes[BaseForm[expr_, n_], f:StandardForm|TraditionalForm|OutputForm]""" @@ -116,10 +125,10 @@ def apply_makeboxes(self, expr, n, f, evaluation): if isinstance(expr, PrecisionReal): x = expr.to_sympy() - p = reconstruct_digits(expr.get_precision()) + p = int(ceil(expr.get_precision() / LOG2_10) + 1) elif isinstance(expr, MachineReal): x = expr.value - p = reconstruct_digits(machine_precision) + p = RECONSTRUCT_MACHINE_PRECISION_DIGITS elif isinstance(expr, Integer): x = expr.value p = 0 @@ -129,7 +138,8 @@ def apply_makeboxes(self, expr, n, f, evaluation): try: val = convert_base(x, base, p) except ValueError: - return evaluation.message("BaseForm", "basf", n) + evaluation.message("BaseForm", "basf", n) + return if f is SymbolOutputForm: return to_boxes(String("%s_%d" % (val, base)), evaluation) @@ -272,7 +282,7 @@ class _NumberForm(Builtin): "sigz": "In addition to the number of digits requested, one or more zeros will appear as placeholders.", } - def check_options(self, options, evaluation): + def check_options(self, options: dict, evaluation: Evaluation): """ Checks options are valid and converts them to python. """ @@ -286,7 +296,7 @@ def check_options(self, options, evaluation): result[option_name] = value return result - def check_DigitBlock(self, value, evaluation): + def check_DigitBlock(self, value, evaluation: Evaluation): py_value = value.get_int_value() if value.sameQ(SymbolInfinity): return [0, 0] @@ -310,9 +320,9 @@ def check_DigitBlock(self, value, evaluation): result = [nleft, nright] if None not in result: return result - return evaluation.message(self.get_name(), "dblk", value) + evaluation.message(self.get_name(), "dblk", value) - def check_ExponentFunction(self, value, evaluation): + def check_ExponentFunction(self, value, evaluation: Evaluation): if value.sameQ(SymbolAutomatic): return self.default_ExponentFunction @@ -321,7 +331,7 @@ def exp_function(x): return exp_function - def check_NumberFormat(self, value, evaluation): + def check_NumberFormat(self, value, evaluation: Evaluation): if value.sameQ(SymbolAutomatic): return self.default_NumberFormat @@ -330,45 +340,46 @@ def num_function(man, base, exp, options): return num_function - def check_NumberMultiplier(self, value, evaluation): + def check_NumberMultiplier(self, value, evaluation: Evaluation): result = value.get_string_value() if result is None: evaluation.message(self.get_name(), "npt", "NumberMultiplier", value) return result - def check_NumberPoint(self, value, evaluation): + def check_NumberPoint(self, value, evaluation: Evaluation): result = value.get_string_value() if result is None: evaluation.message(self.get_name(), "npt", "NumberPoint", value) return result - def check_ExponentStep(self, value, evaluation): + def check_ExponentStep(self, value, evaluation: Evaluation): result = value.get_int_value() if result is None or result <= 0: - return evaluation.message(self.get_name(), "estep", "ExponentStep", value) + evaluation.message(self.get_name(), "estep", "ExponentStep", value) + return return result - def check_SignPadding(self, value, evaluation): + def check_SignPadding(self, value, evaluation: Evaluation): if value.sameQ(SymbolTrue): return True elif value.sameQ(SymbolFalse): return False - return evaluation.message(self.get_name(), "opttf", value) + evaluation.message(self.get_name(), "opttf", value) - def _check_List2str(self, value, msg, evaluation): + def _check_List2str(self, value, msg, evaluation: Evaluation): if value.has_form("List", 2): result = [element.get_string_value() for element in value.elements] if None not in result: return result - return evaluation.message(self.get_name(), msg, value) + evaluation.message(self.get_name(), msg, value) - def check_NumberSigns(self, value, evaluation): + def check_NumberSigns(self, value, evaluation: Evaluation): return self._check_List2str(value, "nsgn", evaluation) - def check_NumberPadding(self, value, evaluation): + def check_NumberPadding(self, value, evaluation: Evaluation): return self._check_List2str(value, "npad", evaluation) - def check_NumberSeparator(self, value, evaluation): + def check_NumberSeparator(self, value, evaluation: Evaluation): py_str = value.get_string_value() if py_str is not None: return [py_str, py_str] @@ -634,7 +645,7 @@ def default_NumberFormat(man, base, exp, options): else: return man - def apply_list_n(self, expr, n, evaluation, options) -> Expression: + def eval_list_n(self, expr, n, evaluation, options) -> Expression: "NumberForm[expr_List, n_, OptionsPattern[NumberForm]]" options = [ Expression(SymbolRuleDelayed, Symbol(key), value) @@ -647,7 +658,7 @@ def apply_list_n(self, expr, n, evaluation, options) -> Expression: ] ) - def apply_list_nf(self, expr, n, f, evaluation, options) -> Expression: + def eval_list_nf(self, expr, n, f, evaluation, options) -> Expression: "NumberForm[expr_List, {n_, f_}, OptionsPattern[NumberForm]]" options = [ Expression(SymbolRuleDelayed, Symbol(key), value) @@ -660,7 +671,7 @@ def apply_list_nf(self, expr, n, f, evaluation, options) -> Expression: ], ) - def apply_makeboxes(self, expr, form, evaluation, options={}): + def eval_makeboxes(self, expr, form, evaluation, options={}): """MakeBoxes[NumberForm[expr_, OptionsPattern[NumberForm]], form:StandardForm|TraditionalForm|OutputForm]""" @@ -685,7 +696,7 @@ def apply_makeboxes(self, expr, form, evaluation, options={}): return number_form(expr, py_n, None, evaluation, py_options) return Expression(SymbolMakeBoxes, expr, form) - def apply_makeboxes_n(self, expr, n, form, evaluation, options={}): + def eval_makeboxes_n(self, expr, n, form, evaluation, options={}): """MakeBoxes[NumberForm[expr_, n_?NotOptionQ, OptionsPattern[NumberForm]], form:StandardForm|TraditionalForm|OutputForm]""" @@ -705,7 +716,7 @@ def apply_makeboxes_n(self, expr, n, form, evaluation, options={}): return number_form(expr, py_n, None, evaluation, py_options) return Expression(SymbolMakeBoxes, expr, form) - def apply_makeboxes_nf(self, expr, n, f, form, evaluation, options={}): + def eval_makeboxes_nf(self, expr, n, f, form, evaluation, options={}): """MakeBoxes[NumberForm[expr_, {n_, f_}, OptionsPattern[NumberForm]], form:StandardForm|TraditionalForm|OutputForm]""" @@ -743,8 +754,12 @@ class OutputForm(FormBaseClass): = f'[x] >> OutputForm[Derivative[1, 0][f][x]] = Derivative[1, 0][f][x] - >> OutputForm["A string"] - = A string + + 'OutputForm' is used by default: + >> OutputForm[{"A string", a + b}] + = {A string, a + b} + >> {"A string", a + b} + = {A string, a + b} >> OutputForm[Graphics[Rectangle[]]] = -Graphics- """ @@ -839,12 +854,9 @@ class StandardForm(FormBaseClass):
    >> StandardForm[a + b * c] - = a + b c + = a+b c >> StandardForm["A string"] = A string - 'StandardForm' is used by default: - >> "A string" - = A string >> f'[x] = f'[x] """ @@ -1057,12 +1069,12 @@ class MatrixForm(TableForm): in_printforms = False summary_text = "format as a matrix" - def eval_makeboxes_matrix(self, table, f, evaluation, options): + def eval_makeboxes_matrix(self, table, form, evaluation, options): """MakeBoxes[%(name)s[table_, OptionsPattern[%(name)s]], - f:StandardForm|TraditionalForm]""" + form:StandardForm|TraditionalForm]""" - result = super(MatrixForm, self).eval_makeboxes(table, f, evaluation, options) + result = super(MatrixForm, self).eval_makeboxes( + table, form, evaluation, options + ) if result.get_head_name() == "System`GridBox": - return RowBox(String("("), result, String(")")) - - return result + return RowBox(StringLParen, result, StringRParen) diff --git a/mathics/builtin/functional/application.py b/mathics/builtin/functional/application.py index c39c29ac6..34656b184 100644 --- a/mathics/builtin/functional/application.py +++ b/mathics/builtin/functional/application.py @@ -13,6 +13,7 @@ from mathics.builtin.base import Builtin, PostfixOperator from mathics.core.attributes import A_HOLD_ALL, A_N_HOLD_ALL, A_PROTECTED from mathics.core.convert.sympy import SymbolFunction +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol @@ -66,11 +67,11 @@ class Function(PostfixOperator): In the evaluation process, the attributes associated with an Expression are \ determined by its Head. If the Head is also a non-atomic Expression, in general,\ - no Attribute is assumed. In particular, it is what happens when the head of the expression \ - has the form + no Attribute is assumed. In particular, it is what happens when the head \ + of the expression has the form: ``Function[$body$]`` - or + or: ``Function[$vars$, $body$]`` >> h := Function[{x}, Hold[1+x]] @@ -81,11 +82,11 @@ class Function(PostfixOperator): the evaluation of $1+1$. To avoid that evaluation, of its arguments, the Head \ should have the attribute 'HoldAll'. This behavior can be obtained by using the \ three arguments form version of this expression: - + >> h:= Function[{x}, Hold[1+x], HoldAll] >> h[1+1] = Hold[1 + (1 + 1)] - + In this case, the attribute 'HoldAll' is assumed, \ preventing the evaluation of the argument $1+1$ before passing it \ to the function body. @@ -103,13 +104,13 @@ class Function(PostfixOperator): } summary_text = "define an anonymous (pure) function" - def apply_slots(self, body, args, evaluation): + def eval_slots(self, body, args, evaluation: Evaluation): "Function[body_][args___]" args = list(chain([Expression(SymbolFunction, body)], args.get_sequence())) return body.replace_slots(args, evaluation) - def apply_named(self, vars, body, args, evaluation): + def eval_named(self, vars, body, args, evaluation: Evaluation): "Function[vars_, body_][args___]" if vars.has_form("List", None): @@ -134,11 +135,11 @@ def apply_named(self, vars, body, args, evaluation): vars = dict(list(zip(var_names, args[: len(vars)]))) try: return body.replace_vars(vars) - except: + except Exception: return # Not sure if DRY is possible here... - def apply_named_attr(self, vars, body, attr, args, evaluation): + def eval_named_attr(self, vars, body, attr, args, evaluation: Evaluation): "Function[vars_, body_, attr_][args___]" if vars.has_form("List", None): vars = vars.elements @@ -152,19 +153,21 @@ def apply_named_attr(self, vars, body, attr, args, evaluation): vars = dict(list(zip((var.get_name() for var in vars), args[: len(vars)]))) try: return body.replace_vars(vars) - except: + except Exception: return class Slot(Builtin): """
    -
    '#$n$' -
    represents the $n$th argument to a pure function. -
    '#' -
    is short-hand for '#1'. -
    '#0' -
    represents the pure function itself. +
    '#$n$' +
    represents the $n$th argument to a pure function. + +
    '#' +
    is short-hand for '#1'. + +
    '#0' +
    represents the pure function itself.
    X> # @@ -199,10 +202,11 @@ class Slot(Builtin): class SlotSequence(Builtin): """
    -
    '##' -
    is the sequence of arguments supplied to a pure function. -
    '##$n$' -
    starts with the $n$th argument. +
    '##' +
    is the sequence of arguments supplied to a pure function. + +
    '##$n$' +
    starts with the $n$th argument.
    >> Plus[##]& [1, 2, 3] diff --git a/mathics/builtin/functional/apply_fns_to_lists.py b/mathics/builtin/functional/apply_fns_to_lists.py index 87ccb6380..fdfa31da4 100644 --- a/mathics/builtin/functional/apply_fns_to_lists.py +++ b/mathics/builtin/functional/apply_fns_to_lists.py @@ -2,9 +2,11 @@ """ Applying Functions to Lists -Many computations can be conveniently specified in terms of applying functions in parallel to many elements in a list. +Many computations can be conveniently specified in terms of applying functions \ +in parallel to many elements in a list. -Many mathematical functions are automatically taken to be "listable", so that they are always applied to every element in a list. +Many mathematical functions are automatically taken to be "listable", so that \ +they are always applied to every element in a list. """ # This tells documentation how to sort this module @@ -13,9 +15,10 @@ from typing import Iterable from mathics.builtin.base import BinaryOperator, Builtin -from mathics.builtin.lists import List, python_levelspec, walk_levels +from mathics.builtin.list.constructing import List from mathics.core.atoms import Integer from mathics.core.convert.expression import to_mathics_list +from mathics.core.evaluation import Evaluation from mathics.core.exceptions import ( InvalidLevelspecError, MessageException, @@ -24,9 +27,8 @@ from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol, SymbolNull, SymbolTrue -from mathics.core.systemsymbols import SymbolRule - -SymbolMapThread = Symbol("MapThread") +from mathics.core.systemsymbols import SymbolMapThread, SymbolRule +from mathics.eval.parts import python_levelspec, walk_levels class Apply(BinaryOperator): @@ -79,12 +81,12 @@ class Apply(BinaryOperator): "Heads": "False", } - def apply_invalidlevel(self, f, expr, ls, evaluation, options={}): + def eval_invalidlevel(self, f, expr, ls, evaluation, options={}): "Apply[f_, expr_, ls_, OptionsPattern[Apply]]" evaluation.message("Apply", "level", ls) - def apply(self, f, expr, ls, evaluation, options={}): + def eval(self, f, expr, ls, evaluation, options={}): """Apply[f_, expr_, Optional[Pattern[ls, _?LevelQ], {0}], OptionsPattern[Apply]]""" @@ -143,12 +145,12 @@ class Map(BinaryOperator): "Heads": "False", } - def apply_invalidlevel(self, f, expr, ls, evaluation, options={}): + def eval_invalidlevel(self, f, expr, ls, evaluation, options={}): "Map[f_, expr_, ls_, OptionsPattern[Map]]" evaluation.message("Map", "level", ls) - def apply_level(self, f, expr, ls, evaluation, options={}): + def eval_level(self, f, expr, ls, evaluation, options={}): """Map[f_, expr_, Optional[Pattern[ls, _?LevelQ], {1}], OptionsPattern[Map]]""" @@ -210,7 +212,7 @@ class MapAt(Builtin): "MapAt[f_, pos_][expr_]": "MapAt[f, expr, pos]", } - def apply(self, f, expr, args, evaluation, options={}): + def eval(self, f, expr, args, evaluation, options={}): "MapAt[f_, expr_, args_]" m = len(expr.elements) @@ -293,12 +295,12 @@ class MapIndexed(Builtin): "Heads": "False", } - def apply_invalidlevel(self, f, expr, ls, evaluation, options={}): + def eval_invalidlevel(self, f, expr, ls, evaluation, options={}): "MapIndexed[f_, expr_, ls_, OptionsPattern[MapIndexed]]" evaluation.message("MapIndexed", "level", ls) - def apply_level(self, f, expr, ls, evaluation, options={}): + def eval_level(self, f, expr, ls, evaluation, options={}): """MapIndexed[f_, expr_, Optional[Pattern[ls, _?LevelQ], {1}], OptionsPattern[MapIndexed]]""" @@ -371,12 +373,12 @@ class MapThread(Builtin): "list": "List expected at position `2` in `1`.", } - def apply(self, f, expr, evaluation): + def eval(self, f, expr, evaluation): "MapThread[f_, expr_]" - return self.apply_n(f, expr, None, evaluation) + return self.eval_n(f, expr, None, evaluation) - def apply_n(self, f, expr, n, evaluation): + def eval_n(self, f, expr, n, evaluation): "MapThread[f_, expr_, n_]" if n is None: @@ -387,12 +389,14 @@ def apply_n(self, f, expr, n, evaluation): n = n.get_int_value() if n is None or n < 0: - return evaluation.message("MapThread", "intnm", full_expr, 3) + evaluation.message("MapThread", "intnm", full_expr, 3) + return if expr.has_form("List", 0): return ListExpression() if not expr.has_form("List", None): - return evaluation.message("MapThread", "list", 2, full_expr) + evaluation.message("MapThread", "list", 2, full_expr) + return heads = expr.elements @@ -468,12 +472,12 @@ class Scan(Builtin): "Scan[f_][expr_]": "Scan[f, expr]", } - def apply_invalidlevel(self, f, expr, ls, evaluation, options={}): + def eval_invalidlevel(self, f, expr, ls, evaluation, options={}): "Scan[f_, expr_, ls_, OptionsPattern[Map]]" - return evaluation.message("Map", "level", ls) + evaluation.message("Map", "level", ls) - def apply_level(self, f, expr, ls, evaluation, options={}): + def eval_level(self, f, expr, ls, evaluation, options={}): """Scan[f_, expr_, Optional[Pattern[ls, _?LevelQ], {1}], OptionsPattern[Map]]""" @@ -525,7 +529,7 @@ class Thread(Builtin): summary_text = '"thread" a function across lists that appear in its arguments' - def apply(self, f, args, h, evaluation): + def eval(self, f, args, h, evaluation: Evaluation): "Thread[f_[args___], h_]" args = args.get_sequence() diff --git a/mathics/builtin/functional/composition.py b/mathics/builtin/functional/composition.py index cea920bf0..04b1408cc 100644 --- a/mathics/builtin/functional/composition.py +++ b/mathics/builtin/functional/composition.py @@ -3,12 +3,19 @@ """ Functional Composition and Operator Forms -:Functional Composition: https://en.wikipedia.org/wiki/Function_composition_(computer_science) is a way to combine simple functions to build more complicated ones. -Like the usual composition of functions in mathematics, the result of each function is passed as the argument of the next, and the result of the last one is the result of the whole. - -The symbolic structure of Mathics makes it easy to create "operators" that can be composed and manipulated symbolically—forming "pipelines" of operations—and then applied to arguments. - -Some built-in functions also directly support a "curried" form, in which they can immediately be given as symbolic operators. +:Functional Composition: +https://en.wikipedia.org/wiki/Function_composition_(computer_science) is \ +a way to combine simple functions to build more complicated ones. +Like the usual composition of functions in mathematics, the result of each \ +function is passed as the argument of the next, and the result of the last \ +one is the result of the whole. + +The symbolic structure of Mathics3 makes it easy to create "operators" that \ +can be composed and manipulated symbolically—forming "pipelines" of \ +operations—and then applied to arguments. + +Some built-in functions also directly support a "curried" form, in which they \ +can immediately be given as symbolic operators. """ # This tells documentation how to sort this module @@ -16,11 +23,16 @@ from mathics.builtin.base import Builtin from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_PROTECTED +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression class Composition(Builtin): """ + + :WMA link: + https://reference.wolfram.com/language/ref/Composition.html +
    'Composition[$f$, $g$]'
    returns the composition of two functions $f$ and $g$. @@ -47,7 +59,7 @@ class Composition(Builtin): } summary_text = "the composition of two or more functions" - def apply(self, functions, args, evaluation): + def eval(self, functions, args, evaluation: Evaluation): "Composition[functions__][args___]" functions = functions.get_sequence() @@ -60,6 +72,9 @@ def apply(self, functions, args, evaluation): class Identity(Builtin): """ + + :WMA link: + https://reference.wolfram.com/language/ref/Identity.html
    'Identity[$x$]'
    is the identity function, which returns $x$ unchanged. diff --git a/mathics/builtin/functional/functional_iteration.py b/mathics/builtin/functional/functional_iteration.py index ea3893676..e82fbf8a0 100644 --- a/mathics/builtin/functional/functional_iteration.py +++ b/mathics/builtin/functional/functional_iteration.py @@ -8,9 +8,10 @@ from mathics.builtin.base import Builtin from mathics.core.atoms import Integer1 from mathics.core.convert.python import from_python +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression +from mathics.core.expression_predefined import MATHICS3_INFINITY from mathics.core.symbols import Symbol, SymbolTrue -from mathics.core.systemsymbols import SymbolDirectedInfinity # This tells documentation how to sort this module sort_order = "mathics.builtin.iteratively-applying-functions" @@ -48,9 +49,9 @@ class FixedPoint(Builtin): summary_text = "nest until a fixed point is reached returning the last expression" - def apply(self, f, expr, n, evaluation, options): + def eval(self, f, expr, n, evaluation: Evaluation, options: dict): "FixedPoint[f_, expr_, n_:DirectedInfinity[1], OptionsPattern[FixedPoint]]" - if n == Expression(SymbolDirectedInfinity, Integer1): + if n.sameQ(MATHICS3_INFINITY): count = None else: count = n.get_int_value() @@ -127,10 +128,10 @@ class FixedPointList(Builtin): summary_text = "nest until a fixed point is reached return a list " - def apply(self, f, expr, n, evaluation): + def eval(self, f, expr, n, evaluation: Evaluation): "FixedPointList[f_, expr_, n_:DirectedInfinity[1]]" - if n == Expression(SymbolDirectedInfinity, Integer1): + if n.sameQ(MATHICS3_INFINITY): count = None else: count = n.get_int_value() @@ -218,7 +219,7 @@ class Nest(Builtin): summary_text = "give the result of nesting a function" - def apply(self, f, expr, n, evaluation): + def eval(self, f, expr, n, evaluation): "Nest[f_, expr_, n_Integer]" n = n.get_int_value() @@ -234,7 +235,8 @@ class NestList(Builtin): """
    'NestList[$f$, $expr$, $n$]' -
    starting with $expr$, iteratively applies $f$ $n$ times and returns a list of all intermediate results. +
    starting with $expr$, iteratively applies $f$ $n$ times and \ + returns a list of all intermediate results.
    >> NestList[f, x, 3] @@ -252,7 +254,7 @@ class NestList(Builtin): summary_text = "successively nest a function" - def apply(self, f, expr, n, evaluation): + def eval(self, f, expr, n, evaluation): "NestList[f_, expr_, n_Integer]" n = n.get_int_value() @@ -273,7 +275,8 @@ class NestWhile(Builtin): """
    'NestWhile[$f$, $expr$, $test$]' -
    applies a function $f$ repeatedly on an expression $expr$, until applying $test$ on the result no longer yields 'True'. +
    applies a function $f$ repeatedly on an expression $expr$, until \ + applying $test$ on the result no longer yields 'True'.
    'NestWhile[$f$, $expr$, $test$, $m$]'
    supplies the last $m$ results to $test$ (default value: 1). @@ -310,7 +313,7 @@ class NestWhile(Builtin): "NestWhile[f_, expr_, test_]": "NestWhile[f, expr, test, 1]", } - def apply(self, f, expr, test, m, evaluation): + def eval(self, f, expr, test, m, evaluation: Evaluation): "NestWhile[f_, expr_, test_, Pattern[m,_Integer|All]]" results = [expr] diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index a0f01cc64..ea8588b32 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -343,6 +343,14 @@ def convert(content): class _Polyline(_GraphicsElementBox): + """ + A structure containing a list of line segments + stored in ``self.lines`` created from + a list of points. + + Lines are formed by pairs of consecutive point. + """ + def do_init(self, graphics, points): if not points.has_form("List", None): raise BoxExpressionError @@ -356,6 +364,10 @@ def do_init(self, graphics, points): ): elements = points.elements self.multi_parts = True + elif len(points.elements) == 0: + # Ensure there are no line segments if there are no points. + self.lines = [] + return else: elements = [ListExpression(*points.elements)] self.multi_parts = False @@ -370,13 +382,18 @@ def do_init(self, graphics, points): ] def extent(self) -> list: - l = self.style.get_line_width(face_element=False) + lw = self.style.get_line_width(face_element=False) result = [] for line in self.lines: for c in line: x, y = c.pos() result.extend( - [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)] + [ + (x - lw, y - lw), + (x - lw, y + lw), + (x + lw, y - lw), + (x + lw, y + lw), + ] ) return result @@ -640,7 +657,7 @@ class Arrow(Builtin): >> Graphics[{Circle[], Arrow[{{2, 1}, {0, 0}}, 1]}] = -Graphics- - Arrows can also be drawn in 3D by giving poing in three dimensions: + Arrows can also be drawn in 3D by giving point in three dimensions: >> Graphics3D[Arrow[{{1, 1, -1}, {2, 2, 0}, {3, 3, -1}, {4, 4, 0}}]] = -Graphics3D- @@ -1144,8 +1161,8 @@ def translate_absolute(self, d): if self.pixel_width is None: return (0, 0) else: - l = 96.0 / 72 - return (d[0] * l, (-1 if self.neg_y else 1) * d[1] * l) + lw = 96.0 / 72 + return (d[0] * lw, (-1 if self.neg_y else 1) * d[1] * lw) def translate_relative(self, x): if self.pixel_width is None: @@ -1389,7 +1406,7 @@ class Rectangle(Builtin):
    represents a unit square with bottom-left corner at {$xmin$, $ymin$}.
    'Rectangle[{$xmin$, $ymin$}, {$xmax$, $ymax$}] -
    is a rectange extending from {$xmin$, $ymin$} to {$xmax$, $ymax$}. +
    is a rectangle extending from {$xmin$, $ymin$} to {$xmax$, $ymax$}.
    >> Graphics[Rectangle[]] diff --git a/mathics/builtin/image/base.py b/mathics/builtin/image/base.py index 3e4d68dc4..d87f758b7 100644 --- a/mathics/builtin/image/base.py +++ b/mathics/builtin/image/base.py @@ -1,53 +1,29 @@ -import base64 -from copy import deepcopy -from io import BytesIO +""" +Base classes for Image Manipulation +""" from typing import Tuple +import numpy +import PIL.Image + from mathics.builtin.base import AtomBuiltin, String from mathics.builtin.box.image import ImageBox from mathics.builtin.colors.color_internals import convert_color -from mathics.core.atoms import Atom, Integer +from mathics.core.atoms import Atom from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.systemsymbols import SymbolImage, SymbolRule -from mathics.eval.image import pixels_as_float, pixels_as_ubyte - -_skimage_requires = ("skimage", "scipy", "matplotlib", "networkx") - - -try: - import warnings - - import numpy - import PIL - import PIL.Image - import PIL.ImageEnhance - import PIL.ImageFilter - import PIL.ImageOps +from mathics.eval.image import image_pixels, pixels_as_float, pixels_as_ubyte -except ImportError: - pass +skimage_requires = ("skimage",) +# No user docs here. +no_doc = True -def _image_pixels(matrix): - try: - pixels = numpy.array(matrix, dtype="float64") - except ValueError: # irregular array, e.g. {{0, 1}, {0, 1, 1}} - return None - shape = pixels.shape - if len(shape) == 2 or (len(shape) == 3 and shape[2] in (1, 3, 4)): - return pixels - else: - return None - - -class _SkimageBuiltin: - """ - Image Builtins that require scikit-image. - """ - - requires = _skimage_requires +image_common_messages = { + "imginv": "Expecting an image instead of `1`.", +} class Image(Atom): @@ -91,51 +67,7 @@ def atom_to_boxes(self, form, evaluation: Evaluation) -> ImageBox: """ Converts our internal Image object into a PNG base64-encoded. """ - pixels = pixels_as_ubyte(self.color_convert("RGB", True).pixels) - shape = pixels.shape - - width = shape[1] - height = shape[0] - scaled_width = width - scaled_height = height - - # If the image was created from PIL, use that rather than - # reconstruct it from pixels which we can get wrong. - # In particular getting color-mapping info right can be - # tricky. - if hasattr(self, "pillow"): - pillow = deepcopy(self.pillow) - else: - pixels_format = "RGBA" if len(shape) >= 3 and shape[2] == 4 else "RGB" - pillow = PIL.Image.fromarray(pixels, pixels_format) - - # if the image is very small, scale it up using nearest neighbour. - min_size = 128 - if width < min_size and height < min_size: - scale = min_size / max(width, height) - scaled_width = int(scale * width) - scaled_height = int(scale * height) - pillow = pillow.resize( - (scaled_height, scaled_width), resample=PIL.Image.NEAREST - ) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - stream = BytesIO() - pillow.save(stream, format="png") - stream.seek(0) - contents = stream.read() - stream.close() - - encoded = base64.b64encode(contents) - encoded = b"data:image/png;base64," + encoded - - return ImageBox( - String(encoded.decode("utf-8")), - Integer(scaled_width), - Integer(scaled_height), - ) + return ImageBox(self) # __hash__ is defined so that we can store Number-derived objects # in a set or dictionary. @@ -303,7 +235,7 @@ class ImageAtom(AtomBuiltin): def eval_create(self, array, evaluation: Evaluation): "Image[array_]" - pixels = _image_pixels(array.to_python()) + pixels = image_pixels(array.to_python()) if pixels is not None: shape = pixels.shape is_rgb = len(shape) == 3 and shape[2] in (3, 4) diff --git a/mathics/builtin/image/basic.py b/mathics/builtin/image/basic.py index 1549c5c33..7d916bb48 100644 --- a/mathics/builtin/image/basic.py +++ b/mathics/builtin/image/basic.py @@ -6,8 +6,14 @@ import PIL from mathics.builtin.base import Builtin, String -from mathics.builtin.image.base import Image -from mathics.core.atoms import Integer, MachineReal +from mathics.builtin.image.base import Image, image_common_messages +from mathics.core.atoms import ( + Integer, + Integer0, + Integer1, + MachineReal, + is_integer_rational_or_real, +) from mathics.core.convert.python import from_python from mathics.core.evaluation import Evaluation from mathics.core.list import ListExpression @@ -33,19 +39,22 @@ class Blur(Builtin):
    blurs $image$ with a kernel of size $r$.
    - >> lena = Import["ExampleData/lena.tif"]; - >> Blur[lena] + >> hedy = Import["ExampleData/hedy.tif"]; + >> Blur[hedy] = -Image- - >> Blur[lena, 5] + >> Blur[hedy, 5] = -Image- """ - summary_text = "blur an image" rules = { "Blur[image_Image]": "Blur[image, 2]", - "Blur[image_Image, r_?RealNumberQ]": "ImageConvolve[image, BoxMatrix[r] / Total[Flatten[BoxMatrix[r]]]]", + "Blur[image_Image, r:(_Integer|_Real|_Rational)]": ( + "ImageConvolve[image, BoxMatrix[r] / Total[Flatten[BoxMatrix[r]]]]" + ), } + summary_text = "blur an image" + class ImageAdjust(Builtin): """ @@ -67,19 +76,36 @@ class ImageAdjust(Builtin):
    adjusts the contrast $c$, brightness $b$, and gamma $g$ in $image$.
    - >> lena = Import["ExampleData/lena.tif"]; - >> ImageAdjust[lena] + >> hedy = Import["ExampleData/hedy.tif"]; + >> ImageAdjust[hedy] = -Image- """ - summary_text = "adjust levels, brightness, contrast, gamma, etc" + messages = { + "arg2": "Invalid correction parameters `1`.", + "brght": ( + "The brightness specficiation in {`1`, `2`}\n" "should be a real number." + ), + "gamma": ( + "The gamma correction specficiation in {`1`, `2`, `3`}\n" + "should be a positive number." + ), + "imginv": image_common_messages["imginv"], + } + rules = { - "ImageAdjust[image_Image, c_?RealNumberQ]": "ImageAdjust[image, {c, 0, 1}]", - "ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]": "ImageAdjust[image, {c, b, 1}]", + "ImageAdjust[image, {c_, b_}]": "ImageAdjust[image, {c, b, 1}]", } + summary_text = "adjust levels, brightness, contrast, gamma, etc" + def eval_auto(self, image, evaluation: Evaluation): - "ImageAdjust[image_Image]" + "ImageAdjust[image_]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + pixels = pixels_as_float(image.pixels) # channel limits @@ -95,8 +121,37 @@ def eval_auto(self, image, evaluation: Evaluation): pixels /= scales return Image(pixels, image.color_space) - def eval_contrast_brightness_gamma(self, image, c, b, g, evaluation: Evaluation): - "ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ, g_?RealNumberQ}]" + def eval_with_correction(self, image, corr, evaluation: Evaluation): + "ImageAdjust[image_, corr_]" + + if not is_integer_rational_or_real(corr): + evaluation.message(self.get_name(), "arg2", corr) + return + + return self.eval_with_contrast_brightness_gamma( + image, corr, Integer0, Integer1, evaluation + ) + + def eval_with_contrast_brightness_gamma( + self, image, c, b, g, evaluation: Evaluation + ): + "ImageAdjust[image, {c_, b_, g_}]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + + if not is_integer_rational_or_real(c): + evaluation.message(self.get_name(), "arg2", c) + return + + if not is_integer_rational_or_real(b): + evaluation.message(self.get_name(), "brght", c, b) + return + + if not is_integer_rational_or_real(g): + evaluation.message(self.get_name(), "gamma", c, b, g) + return im = image.pil() @@ -130,40 +185,40 @@ class ImagePartition(Builtin):
    Partitions an image into an array of $w$ x $h$ pixel subimages.
    - >> lena = Import["ExampleData/lena.tif"]; - >> ImageDimensions[lena] - = {512, 512} - >> ImagePartition[lena, 256] - = {{-Image-, -Image-}, {-Image-, -Image-}} + >> hedy = Import["ExampleData/hedy.tif"]; + >> ImageDimensions[hedy] + = {646, 800} + >> ImagePartition[hedy, 256] + = {{-Image-, -Image-}, {-Image-, -Image-}, {-Image-, -Image-}} - >> ImagePartition[lena, {512, 128}] - = {{-Image-}, {-Image-}, {-Image-}, {-Image-}} + >> ImagePartition[hedy, {512, 128}] + = {{-Image-}, {-Image-}, {-Image-}, {-Image-}, {-Image-}, {-Image-}} - #> ImagePartition[lena, 257] - = {{-Image-}} - #> ImagePartition[lena, 512] + #> ImagePartition[hedy, 257] + = {{-Image-, -Image-}, {-Image-, -Image-}, {-Image-, -Image-}} + #> ImagePartition[hedy, 646] = {{-Image-}} - #> ImagePartition[lena, 513] + #> ImagePartition[hedy, 647] = {} - #> ImagePartition[lena, {256, 300}] - = {{-Image-, -Image-}} + #> ImagePartition[hedy, {256, 300}] + = {{-Image-, -Image-}, {-Image-, -Image-}} - #> ImagePartition[lena, {0, 300}] + #> ImagePartition[hedy, {0, 300}] : {0, 300} is not a valid size specification for image partitions. = ImagePartition[-Image-, {0, 300}] """ - summary_text = "divide an image in an array of sub-images" - rules = {"ImagePartition[i_Image, s_Integer]": "ImagePartition[i, {s, s}]"} - messages = {"arg2": "`1` is not a valid size specification for image partitions."} + rules = {"ImagePartition[i_Image, s_Integer]": "ImagePartition[i, {s, s}]"} + summary_text = "divide an image in an array of sub-images" def eval(self, image, w: Integer, h: Integer, evaluation: Evaluation): "ImagePartition[image_Image, {w_Integer, h_Integer}]" py_w = w.value py_h = h.value if py_w <= 0 or py_h <= 0: - return evaluation.message("ImagePartition", "arg2", ListExpression(w, h)) + evaluation.message("ImagePartition", "arg2", ListExpression(w, h)) + return pixels = image.pixels shape = pixels.shape @@ -192,18 +247,32 @@ class Sharpen(Builtin):
    sharpens $image$ with a kernel of size $r$.
    - >> lena = Import["ExampleData/lena.tif"]; - >> Sharpen[lena] + >> hedy = Import["ExampleData/hedy.tif"]; + >> Sharpen[hedy] = -Image- - >> Sharpen[lena, 5] + >> Sharpen[hedy, 5] = -Image- """ - summary_text = "sharpen version of an image" + messages = { + "bdrad": "The specified radius should be either a non-negative number", + "imginv": image_common_messages["imginv"], + } + rules = {"Sharpen[i_Image]": "Sharpen[i, 2]"} + summary_text = "sharpen version of an image" def eval(self, image, r, evaluation: Evaluation): - "Sharpen[image_Image, r_?RealNumberQ]" + "Sharpen[image_, r_]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + + if not is_integer_rational_or_real(r): + evaluation.message(self.get_name(), "bdrad", r) + return + f = PIL.ImageFilter.UnsharpMask(r.round_to_float()) return image.filter(lambda im: im.filter(f)) @@ -222,15 +291,15 @@ class Threshold(Builtin): The option "Method" may be "Cluster" (use Otsu's threshold), "Median", or "Mean". - >> img = Import["ExampleData/lena.tif"]; + >> img = Import["ExampleData/hedy.tif"]; >> Threshold[img] - = 0.456739 + = 0.408203 X> Binarize[img, %] = -Image- X> Threshold[img, Method -> "Mean"] - = 0.486458 + = 0.22086 X> Threshold[img, Method -> "Median"] - = 0.504726 + = 0.0593961 """ summary_text = "estimate a threshold value for binarize an image" @@ -244,7 +313,7 @@ class Threshold(Builtin): "skimage": "Please install scikit-image to use Method -> Cluster.", } - def eval(self, image, evaluation: Evaluation, options): + def eval(self, image, evaluation: Evaluation, options: dict): "Threshold[image_Image, OptionsPattern[Threshold]]" pixels = image.grayscale().pixels @@ -264,9 +333,11 @@ def eval(self, image, evaluation: Evaluation, options): elif method_name == "Mean": threshold = numpy.mean(pixels) else: - return evaluation.message("Threshold", "illegalmethod", method) + evaluation.message("Threshold", "illegalmethod", method) + return return MachineReal(float(threshold)) -# Todo Darker, ImageClip, ImageEffect, ImageRestyle, Lighter +# TODO Darker, ImageClip, ImageEffect, ImageRestyle, Lighter +# Some existing functions allow for other forms. diff --git a/mathics/builtin/image/colors.py b/mathics/builtin/image/colors.py index ffbfa9449..53119df13 100644 --- a/mathics/builtin/image/colors.py +++ b/mathics/builtin/image/colors.py @@ -2,61 +2,27 @@ Image Colors """ -import functools -from typing import Tuple - import numpy import PIL from mathics.builtin.base import Builtin, String from mathics.builtin.colors.color_internals import colorspaces as known_colorspaces -from mathics.builtin.image.base import Image -from mathics.core.atoms import Integer +from mathics.builtin.image.base import Image, image_common_messages +from mathics.core.atoms import Integer, is_integer_rational_or_real from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolTrue -from mathics.core.systemsymbols import SymbolMatrixQ, SymbolThreshold -from mathics.eval.image import matrix_to_numpy, pixels_as_ubyte +from mathics.core.systemsymbols import ( + SymbolColorQuantize, + SymbolMatrixQ, + SymbolThreshold, +) +from mathics.eval.image import linearize_numpy_array, matrix_to_numpy, pixels_as_ubyte # This tells documentation how to sort this module sort_order = "mathics.builtin.image.image-colors" -SymbolColorQuantize = Symbol("ColorQuantize") - - -def _linearize_numpy_array(a: numpy.array) -> Tuple[numpy.array, int]: - """ - Transforms a numpy array numpy array and return the array and the number - of dimensions in the array - - A binary search is used. - """ - - orig_shape = a.shape - a = a.reshape((functools.reduce(lambda x, y: x * y, a.shape),)) # 1 dimension - - u = numpy.unique(a) - n = len(u) - - lower = numpy.ndarray(a.shape, dtype=int) - lower.fill(0) - upper = numpy.ndarray(a.shape, dtype=int) - upper.fill(n - 1) - - h = numpy.sort(u) - q = n # worst case partition size - - while q > 2: - m = numpy.right_shift(lower + upper, 1) - f = a <= h[m] - # (lower, m) vs (m + 1, upper) - lower = numpy.where(f, lower, m + 1) - upper = numpy.where(f, m, upper) - q = (q + 1) // 2 - - return numpy.where(a == h[lower], lower, upper).reshape(orig_shape), n - class Binarize(Builtin): """ @@ -75,19 +41,31 @@ class Binarize(Builtin):
    map $t1$ < $x$ < $t2$ to 1, and all other values to 0.
    - S> img = Import["ExampleData/lena.tif"]; - S> Binarize[img] + >> hedy = Import["ExampleData/hedy.tif"]; + >> Binarize[hedy] = -Image- - S> Binarize[img, 0.7] + + >> Binarize[hedy, 0.7] = -Image- - S> Binarize[img, {0.2, 0.6}] + + >> Binarize[hedy, {0.2, 0.6}] = -Image- """ summary_text = "create a binarized image" + messages = { + "imginv": image_common_messages["imginv"], + "arg2": ("The argument `1` should be a real number or a pair of real numbers."), + } + def eval(self, image, evaluation: Evaluation): - "Binarize[image_Image]" + "Binarize[image_]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + image = image.grayscale() thresh = ( Expression(SymbolThreshold, image).evaluate(evaluation).round_to_float() @@ -95,57 +73,80 @@ def eval(self, image, evaluation: Evaluation): if thresh is not None: return Image(image.pixels > thresh, "Grayscale") - def eval_t(self, image, t, evaluation: Evaluation): - "Binarize[image_Image, t_?RealNumberQ]" + def eval_with_t(self, image, t, evaluation: Evaluation): + "Binarize[image_, t_]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + + if isinstance(t, ListExpression) and len(t.elements) == 2: + return self.eval_with_t1_t2(image, *t.elements, evaluation) + + if not is_integer_rational_or_real(t): + evaluation.message(self.get_name(), "arg2", t) + return + pixels = image.grayscale().pixels return Image(pixels > t.round_to_float(), "Grayscale") - def eval_t1_t2(self, image, t1, t2, evaluation: Evaluation): - "Binarize[image_Image, {t1_?RealNumberQ, t2_?RealNumberQ}]" + def eval_with_t1_t2(self, image, t1, t2, evaluation: Evaluation): + "Binarize[image_, {t1_, t2_}]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + + if not is_integer_rational_or_real(t1): + evaluation.message(self.get_name(), "arg2", [t1, t2]) + return + + if not is_integer_rational_or_real(t2): + evaluation.message(self.get_name(), "arg2", [t1, t2]) + return + pixels = image.grayscale().pixels mask1 = pixels > t1.round_to_float() mask2 = pixels < t2.round_to_float() return Image(mask1 * mask2, "Grayscale") -class ColorCombine(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/ColorCombine.html +# FIXME: ColorCombine works on images, not lists +# class ColorCombine(Builtin): +# """ +# :WMA link:https://reference.wolfram.com/language/ref/ColorCombine.html -
    -
    'ColorCombine[$channels$, $colorspace$]' -
    Gives an image with $colorspace$ and the respective components described by the given channels. -
    +#
    +#
    'ColorCombine[$channels$, $colorspace$]' +#
    Gives an image with $colorspace$ and the respective components described by the given channels. +#
    - >> ColorCombine[{{{1, 0}, {0, 0.75}}, {{0, 1}, {0, 0.25}}, {{0, 0}, {1, 0.5}}}, "RGB"] - = -Image- - """ +# >> ColorCombine[{{{1, 0}, {0, 0.75}}, {{0, 1}, {0, 0.25}}, {{0, 0}, {1, 0.5}}}, "RGB"] +# = -Image- +# """ - summary_text = "combine color channels" +# summary_text = "combine color channels" - def eval(self, channels, colorspace, evaluation: Evaluation): - "ColorCombine[channels_List, colorspace_String]" +# def eval(self, channels, colorspace, evaluation: Evaluation): +# "ColorCombine[channels_List, colorspace_String]" - py_colorspace = colorspace.get_string_value() - if py_colorspace not in known_colorspaces: - return +# py_colorspace = colorspace.get_string_value() +# if py_colorspace not in known_colorspaces: +# return - numpy_channels = [] - for channel in channels.elements: - if ( - not Expression(SymbolMatrixQ, channel).evaluate(evaluation) - is SymbolTrue - ): - return - numpy_channels.append(matrix_to_numpy(channel)) +# numpy_channels = [] +# for channel in channels.elements: +# if Expression(SymbolMatrixQ, channel).evaluate(evaluation) is not True: +# return +# numpy_channels.append(matrix_to_numpy(channel)) - if not numpy_channels: - return +# if not numpy_channels: +# return - if not all(x.shape == numpy_channels[0].shape for x in numpy_channels[1:]): - return +# if not all(x.shape == numpy_channels[0].shape for x in numpy_channels[1:]): +# return - return Image(numpy.dstack(numpy_channels), py_colorspace) +# return Image(numpy.dstack(numpy_channels), py_colorspace) class ColorQuantize(Builtin): @@ -159,28 +160,30 @@ class ColorQuantize(Builtin):
    gives a version of $image$ using only $n$ colors.
    - >> img = Import["ExampleData/lena.tif"]; + >> img = Import["ExampleData/hedy.tif"]; >> ColorQuantize[img, 6] = -Image- - - #> ColorQuantize[img, 0] - : Positive integer expected at position 2 in ColorQuantize[-Image-, 0]. - = ColorQuantize[-Image-, 0] - #> ColorQuantize[img, -1] - : Positive integer expected at position 2 in ColorQuantize[-Image-, -1]. - = ColorQuantize[-Image-, -1] """ summary_text = "give an approximation to image that uses only n distinct colors" - messages = {"intp": "Positive integer expected at position `2` in `1`."} + messages = { + "imginv": image_common_messages["imginv"], + "intp": "Positive integer expected at position `2` in `1`.", + } def eval(self, image, n: Integer, evaluation: Evaluation): - "ColorQuantize[image_Image, n_Integer]" + "ColorQuantize[image_, n_Integer]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + py_value = n.value if py_value <= 0: - return evaluation.message( + evaluation.message( "ColorQuantize", "intp", Expression(SymbolColorQuantize, image, n), 2 ) + return converted = image.color_convert("RGB") if converted is None: return @@ -201,16 +204,22 @@ class ColorSeparate(Builtin):
    Gives each channel of $image$ as a separate grayscale image.
    - >> img = Import["ExampleData/lena.tif"]; + >> img = Import["ExampleData/hedy.tif"]; >> ColorSeparate[img] = ... """ + messages = {"imginv": image_common_messages["imginv"]} summary_text = "separate color channels" - def eval(self, image: Image, evaluation: Evaluation): + def eval(self, image, evaluation: Evaluation): "ColorSeparate[image_]" + + if not isinstance(image, Image): + evaluation.message(self.get_name(), "imginv", image) + return + images = [] pixels = image.pixels if len(pixels.shape) < 3: @@ -228,7 +237,7 @@ class Colorize(Builtin):
    'Colorize[$values$]'
    returns an image where each number in the rectangular matrix \ - $values$ is a pixel and each occurence of the same number is \ + $values$ is a pixel and each occurrence of the same number is \ displayed in the same unique color, which is different from the \ colors of all non-identical numbers. @@ -247,7 +256,10 @@ class Colorize(Builtin): options = {"ColorFunction": "Automatic"} messages = { - "cfun": "`1` is neither a gradient ColorData nor a pure function suitable as ColorFunction." + "cfun": ( + "`1` is neither a gradient ColorData nor a pure function suitable as " + "ColorFunction." + ) } def eval(self, values, evaluation, options): @@ -257,12 +269,14 @@ def eval(self, values, evaluation, options): pixels = values.grayscale().pixels matrix = pixels_as_ubyte(pixels.reshape(pixels.shape[:2])) else: - if not Expression(SymbolMatrixQ, values).evaluate(evaluation) is SymbolTrue: + if Expression(SymbolMatrixQ, values).evaluate(evaluation) is not SymbolTrue: return matrix = matrix_to_numpy(values) - a, n = _linearize_numpy_array(matrix) - # the maximum value for n is the number of pixels in a, which is acceptable and never too large. + a, n = linearize_numpy_array(matrix) + + # the maximum value for n is the number of pixels in a, which is acceptable and + # never too large. color_function = self.get_option(options, "ColorFunction", evaluation) if ( diff --git a/mathics/builtin/image/composition.py b/mathics/builtin/image/composition.py index cc6e347c9..0135f2896 100644 --- a/mathics/builtin/image/composition.py +++ b/mathics/builtin/image/composition.py @@ -53,7 +53,8 @@ def eval(self, image, args, evaluation: Evaluation): "%(name)s[image_Image, args__]" images, arg = self.convert_args(image, *args.get_sequence()) if images is None: - return evaluation.message(self.get_name(), "bddarg", arg) + evaluation.message(self.get_name(), "bddarg", arg) + return ufunc = getattr(numpy, self.get_name(True)[5:].lower()) result = self._reduce(images, ufunc).clip(0, 1) return Image(result, image.color_space) @@ -90,9 +91,9 @@ class ImageAdd(_ImageArithmetic): >> ImageAdd[noise, ein] = -Image- - >> lena = Import["ExampleData/lena.tif"]; - >> noise = RandomImage[{-0.2, 0.2}, ImageDimensions[lena], ColorSpace -> "RGB"]; - >> ImageAdd[noise, lena] + >> hedy = Import["ExampleData/hedy.tif"]; + >> noise = RandomImage[{-0.2, 0.2}, ImageDimensions[hedy], ColorSpace -> "RGB"]; + >> ImageAdd[noise, hedy] = -Image- """ diff --git a/mathics/builtin/image/filters.py b/mathics/builtin/image/filters.py index 6a28d2e5e..222603d09 100644 --- a/mathics/builtin/image/filters.py +++ b/mathics/builtin/image/filters.py @@ -35,8 +35,8 @@ class GaussianFilter(Builtin):
    blurs $image$ using a Gaussian blur filter of radius $r$.
    - >> lena = Import["ExampleData/lena.tif"]; - >> GaussianFilter[lena, 2.5] + >> hedy = Import["ExampleData/hedy.tif"]; + >> GaussianFilter[hedy, 2.5] = -Image- """ @@ -46,7 +46,8 @@ class GaussianFilter(Builtin): def eval_radius(self, image, radius, evaluation: Evaluation): "GaussianFilter[image_Image, radius_?RealNumberQ]" if len(image.pixels.shape) > 2 and image.pixels.shape[2] > 3: - return evaluation.message("GaussianFilter", "only3") + evaluation.message("GaussianFilter", "only3") + return else: f = PIL.ImageFilter.GaussianBlur(radius.round_to_float()) return image.filter(lambda im: im.filter(f)) @@ -63,12 +64,12 @@ class ImageConvolve(Builtin):
    Computes the convolution of $image$ using $kernel$.
    - >> img = Import["ExampleData/lena.tif"]; - >> ImageConvolve[img, DiamondMatrix[5] / 61] + >> hedy = Import["ExampleData/hedy.tif"]; + >> ImageConvolve[hedy, DiamondMatrix[5] / 61] = -Image- - >> ImageConvolve[img, DiskMatrix[5] / 97] + >> ImageConvolve[hedy, DiskMatrix[5] / 97] = -Image- - >> ImageConvolve[img, BoxMatrix[5] / 121] + >> ImageConvolve[hedy, BoxMatrix[5] / 121] = -Image- """ @@ -98,8 +99,8 @@ class MaxFilter(_PillowImageFilter): picks the largest value in the filter's area.
    - >> lena = Import["ExampleData/lena.tif"]; - >> MaxFilter[lena, 5] + >> hedy = Import["ExampleData/hedy.tif"]; + >> MaxFilter[hedy, 5] = -Image- """ @@ -122,8 +123,8 @@ class MedianFilter(_PillowImageFilter): picks the median value in the filter's area.
    - >> lena = Import["ExampleData/lena.tif"]; - >> MedianFilter[lena, 5] + >> hedy = Import["ExampleData/hedy.tif"]; + >> MedianFilter[hedy, 5] = -Image- """ @@ -146,8 +147,8 @@ class MinFilter(_PillowImageFilter): picks the smallest value in the filter's area.
    - >> lena = Import["ExampleData/lena.tif"]; - >> MinFilter[lena, 5] + >> hedy = Import["ExampleData/hedy.tif"]; + >> MinFilter[hedy, 5] = -Image- """ diff --git a/mathics/builtin/image/geometric.py b/mathics/builtin/image/geometric.py index 6f0041ef3..8b4d7bdf3 100644 --- a/mathics/builtin/image/geometric.py +++ b/mathics/builtin/image/geometric.py @@ -86,7 +86,8 @@ def eval_resize_width(self, image, s, evaluation, options): width = s w = get_image_size_spec(old_w, width) if w is None: - return evaluation.message("ImageResize", "imgrssz", s) + evaluation.message("ImageResize", "imgrssz", s) + return if s.has_form("List", 1): height = width else: @@ -110,9 +111,8 @@ def eval_resize_width_height(self, image, width, height, evaluation, options): w = get_image_size_spec(old_w, width) h = get_image_size_spec(old_h, height) if h is None or w is None: - return evaluation.message( - "ImageResize", "imgrssz", to_mathics_list(width, height) - ) + evaluation.message("ImageResize", "imgrssz", to_mathics_list(width, height)) + return # handle Automatic old_aspect_ratio = old_w / old_h @@ -208,9 +208,10 @@ def no_op(i): }.get(tuple(specs), None) if method is None: - return evaluation.message( + evaluation.message( "ImageReflect", "bdrfl2", Expression(SymbolRule, orig, dest) ) + return return Image(method(image.pixels), image.color_space) @@ -265,7 +266,8 @@ def eval(self, image, angle, evaluation: Evaluation): ) if py_angle is None: - return evaluation.message("ImageRotate", "imgang", angle) + evaluation.message("ImageRotate", "imgang", angle) + return def rotate(im): return im.rotate( diff --git a/mathics/builtin/image/misc.py b/mathics/builtin/image/misc.py index f60a26ecc..1d1d1ee9e 100644 --- a/mathics/builtin/image/misc.py +++ b/mathics/builtin/image/misc.py @@ -9,17 +9,15 @@ import PIL from mathics.builtin.base import Builtin, String -from mathics.builtin.image.base import Image, _SkimageBuiltin +from mathics.builtin.image.base import Image, skimage_requires from mathics.core.convert.python import from_python from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolNull -from mathics.core.systemsymbols import SymbolRule +from mathics.core.systemsymbols import SymbolFailed, SymbolRule from mathics.eval.image import extract_exif -_skimage_requires = ("skimage", "scipy", "matplotlib", "networkx") - # The following classes are used to allow inclusion of # Builtin Functions only when certain Python packages # are available. They do this by setting the `requires` class variable. @@ -46,7 +44,8 @@ def eval(self, path: String, expr, opts, evaluation: Evaluation): expr.pil().save(path.value) return SymbolNull else: - return evaluation.message("ImageExport", "noimage") + evaluation.message("ImageExport", "noimage") + return class ImageImport(Builtin): @@ -65,15 +64,28 @@ class ImageImport(Builtin): = -Image- >> Import["ExampleData/moon.tif"] = -Image- - >> Import["ExampleData/lena.tif"] + >> Import["ExampleData/hedy.tif"] = -Image- """ + messages = { + "infer": "Cannot infer format of file `1`.", + "imgmisc": "PIL error: `1`.", + } + no_doc = True def eval(self, path: String, evaluation: Evaluation): """ImageImport[path_String]""" - pillow = PIL.Image.open(path.value) + try: + pillow = PIL.Image.open(path.value) + except PIL.UnidentifiedImageError: + evaluation.message("ImageImport", "infer", path) + return SymbolFailed + except Exception as e: + evaluation.message("ImageImport", "imgmisc", str(e)) + return SymbolFailed + pixels = numpy.asarray(pillow) is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3 options_from_exif = extract_exif(pillow, evaluation) @@ -95,11 +107,13 @@ class RandomImage(Builtin): :WMA link:https://reference.wolfram.com/language/ref/RandomImage.html
    -
    'RandomImage[$max$]' +
    'RandomImage[$max$]'
    creates an image of random pixels with values 0 to $max$. -
    'RandomImage[{$min$, $max$}]' + +
    'RandomImage[{$min$, $max$}]'
    creates an image of random pixels with values $min$ to $max$. -
    'RandomImage[..., $size$]' + +
    'RandomImage[..., $size$]'
    creates an image of the given $size$.
    @@ -145,7 +159,8 @@ def eval(self, minval, maxval, w, h, evaluation, options): cs = color_space.get_string_value() size = [w.value, h.value] if size[0] <= 0 or size[1] <= 0: - return evaluation.message("RandomImage", "bddim", from_python(size)) + evaluation.message("RandomImage", "bddim", from_python(size)) + return minrange, maxrange = minval.round_to_float(), maxval.round_to_float() if cs == "Grayscale": @@ -158,13 +173,13 @@ def eval(self, minval, maxval, w, h, evaluation, options): + minrange ) else: - return evaluation.message("RandomImage", "imgcstype", color_space) + evaluation.message("RandomImage", "imgcstype", color_space) + return return Image(data, cs) -class EdgeDetect(_SkimageBuiltin): +class EdgeDetect(Builtin): """ - :WMA link: https://reference.wolfram.com/language/ref/EdgeDetect.html @@ -174,16 +189,18 @@ class EdgeDetect(_SkimageBuiltin):
    returns an image showing the edges in $image$.
    - >> lena = Import["ExampleData/lena.tif"]; - >> EdgeDetect[lena] + >> hedy = Import["ExampleData/hedy.tif"]; + >> EdgeDetect[hedy] = -Image- - >> EdgeDetect[lena, 5] + >> EdgeDetect[hedy, 5] = -Image- - >> EdgeDetect[lena, 4, 0.5] + >> EdgeDetect[hedy, 4, 0.5] = -Image- """ summary_text = "detect edges in an image using Canny and other methods" + requires = skimage_requires + rules = { "EdgeDetect[i_Image]": "EdgeDetect[i, 2, 0.2]", "EdgeDetect[i_Image, r_?RealNumberQ]": "EdgeDetect[i, r, 0.2]", @@ -213,9 +230,17 @@ class TextRecognize(Builtin): https://reference.wolfram.com/language/ref/TextRecognize.html
    -
    'TextRecognize[{$image$}]' -
    Recognizes text in $image$ and returns it as string. +
    'TextRecognize[$image$]' +
    Recognizes text in $image$ and returns it as a 'String'.
    + + >> textimage = Import["ExampleData/TextRecognize.png"] + = -Image- + + >> TextRecognize[textimage] + = TextRecognize[ image] + . + . Recognizes text in image and returns it as a String. """ messages = { @@ -226,7 +251,7 @@ class TextRecognize(Builtin): options = {"Language": '"English"'} - requires = "pyocr" + requires = ("pyocr",) summary_text = "recognize text in an image" diff --git a/mathics/builtin/image/morph.py b/mathics/builtin/image/morph.py index 67f12af5b..5e68f9197 100644 --- a/mathics/builtin/image/morph.py +++ b/mathics/builtin/image/morph.py @@ -3,13 +3,13 @@ """ from mathics.builtin.base import Builtin -from mathics.builtin.image.base import Image, _SkimageBuiltin +from mathics.builtin.image.base import Image, skimage_requires from mathics.core.convert.python import from_python from mathics.core.evaluation import Evaluation from mathics.eval.image import matrix_to_numpy, pixels_as_float, pixels_as_ubyte -class _MorphologyFilter(_SkimageBuiltin, Builtin): +class _MorphologyFilter(Builtin): """ Base class for many Morphological Image Processing filters. This requires scikit-mage to be installed. @@ -19,6 +19,7 @@ class _MorphologyFilter(_SkimageBuiltin, Builtin): "grayscale": "Your image has been converted to grayscale as color images are not supported yet." } + requires = skimage_requires rules = {"%(name)s[i_Image, r_?RealNumberQ]": "%(name)s[i, BoxMatrix[r]]"} def eval(self, image, k, evaluation: Evaluation): @@ -88,10 +89,10 @@ class Erosion(_MorphologyFilter): = -Image- """ - summary_text = "give the erotion with respect to a range-r square" + summary_text = "give erosion with respect to a range-r square" -class MorphologicalComponents(_SkimageBuiltin): +class MorphologicalComponents(Builtin): """ :WMA link: diff --git a/mathics/builtin/image/pixel.py b/mathics/builtin/image/pixel.py index 68d1aa517..fea1c5b52 100644 --- a/mathics/builtin/image/pixel.py +++ b/mathics/builtin/image/pixel.py @@ -4,6 +4,7 @@ import numpy from mathics.builtin.base import Builtin +from mathics.builtin.image.base import Image from mathics.core.atoms import Integer, MachineReal from mathics.core.convert.expression import to_mathics_list from mathics.core.evaluation import Evaluation @@ -22,23 +23,23 @@ class PixelValue(Builtin):
    gives the value of the pixel at position {$x$, $y$} in $image$.
  • - >> lena = Import["ExampleData/lena.tif"]; - >> PixelValue[lena, {1, 1}] - = {0.321569, 0.0862745, 0.223529} + >> hedy = Import["ExampleData/hedy.tif"]; + >> PixelValue[hedy, {1, 1}] + = {0.439216, 0.356863, 0.337255} #> {82 / 255, 22 / 255, 57 / 255} // N (* pixel byte values from bottom left corner *) = {0.321569, 0.0862745, 0.223529} - #> PixelValue[lena, {0, 1}]; + #> PixelValue[hedy, {0, 1}]; : Padding not implemented for PixelValue. - #> PixelValue[lena, {512, 1}] - = {0.72549, 0.290196, 0.317647} - #> PixelValue[lena, {513, 1}]; + #> PixelValue[hedy, {512, 1}] + = {0.0509804, 0.0509804, 0.0588235} + #> PixelValue[hedy, {647, 1}]; : Padding not implemented for PixelValue. - #> PixelValue[lena, {1, 0}]; + #> PixelValue[hedy, {1, 0}]; : Padding not implemented for PixelValue. - #> PixelValue[lena, {1, 512}] - = {0.886275, 0.537255, 0.490196} - #> PixelValue[lena, {1, 513}]; + #> PixelValue[hedy, {1, 512}] + = {0.286275, 0.4, 0.423529} + #> PixelValue[hedy, {1, 801}]; : Padding not implemented for PixelValue. """ @@ -46,14 +47,15 @@ class PixelValue(Builtin): summary_text = "get pixel value of image at a given position" - def eval(self, image, x, y, evaluation: Evaluation): + def eval(self, image: Image, x, y, evaluation: Evaluation): "PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]" x = int(x.round_to_float()) y = int(y.round_to_float()) height = image.pixels.shape[0] width = image.pixels.shape[1] if not (1 <= x <= width and 1 <= y <= height): - return evaluation.message("PixelValue", "nopad") + evaluation.message("PixelValue", "nopad") + return pixel = pixels_as_float(image.pixels)[height - y, x - 1] if isinstance(pixel, (numpy.ndarray, numpy.generic, list)): return ListExpression(*[MachineReal(float(x)) for x in list(pixel)]) @@ -76,11 +78,11 @@ class PixelValuePositions(Builtin): >> PixelValuePositions[Image[{{0.2, 0.4}, {0.9, 0.6}, {0.3, 0.8}}], 0.5, 0.15] = {{2, 2}, {2, 3}} - >> img = Import["ExampleData/lena.tif"]; - >> PixelValuePositions[img, 3 / 255, 0.5 / 255] - = {{180, 192, 2}, {181, 192, 2}, {181, 193, 2}, {188, 204, 2}, {265, 314, 2}, {364, 77, 2}, {365, 72, 2}, {365, 73, 2}, {365, 77, 2}, {366, 70, 2}, {367, 65, 2}} - >> PixelValue[img, {180, 192}] - = {0.25098, 0.0117647, 0.215686} + >> hedy = Import["ExampleData/hedy.tif"]; + >> PixelValuePositions[hedy, 1, 0][[1]] + = {101, 491, 1} + >> PixelValue[hedy, {180, 192}] + = {0.00784314, 0.00784314, 0.0156863} """ rules = { @@ -89,7 +91,7 @@ class PixelValuePositions(Builtin): summary_text = "list the position of pixels with a given value" - def eval(self, image, val, d, evaluation: Evaluation): + def eval(self, image: Image, val, d, evaluation: Evaluation): "PixelValuePositions[image_Image, val_?RealNumberQ, d_?RealNumberQ]" val = val.round_to_float() d = d.round_to_float() diff --git a/mathics/builtin/image/properties.py b/mathics/builtin/image/properties.py index e14c0a27f..908e03975 100644 --- a/mathics/builtin/image/properties.py +++ b/mathics/builtin/image/properties.py @@ -29,9 +29,9 @@ class ImageAspectRatio(Builtin):
    gives the aspect ratio of $image$.
    - >> img = Import["ExampleData/lena.tif"]; + >> img = Import["ExampleData/hedy.tif"]; >> ImageAspectRatio[img] - = 1 + = 400 / 323 >> ImageAspectRatio[Image[{{0, 1}, {1, 0}, {1, 1}}]] = 3 / 2 @@ -58,7 +58,7 @@ class ImageChannels(Builtin): >> ImageChannels[Image[{{0, 1}, {1, 0}}]] = 1 - >> img = Import["ExampleData/lena.tif"]; + >> img = Import["ExampleData/hedy.tif"]; >> ImageChannels[img] = 3 """ @@ -117,7 +117,8 @@ def eval(self, image, stype: String, evaluation: Evaluation): elif stype == "Bit": pixels = pixels.astype(int) else: - return evaluation.message("ImageData", "pixelfmt", stype) + evaluation.message("ImageData", "pixelfmt", stype) + return return from_python(numpy_to_matrix(pixels)) @@ -132,9 +133,9 @@ class ImageDimensions(Builtin):
    Returns the dimensions {$width$, $height$} of $image$ in pixels.
    - >> lena = Import["ExampleData/lena.tif"]; - >> ImageDimensions[lena] - = {512, 512} + >> hedy = Import["ExampleData/hedy.tif"]; + >> ImageDimensions[hedy] + = {646, 800} >> ImageDimensions[RandomImage[1, {50, 70}]] = {50, 70} @@ -157,7 +158,7 @@ class ImageType(Builtin):
    gives the interval storage type of $image$, e.g. "Real", "Bit32", or "Bit".
    - >> img = Import["ExampleData/lena.tif"]; + >> img = Import["ExampleData/hedy.tif"]; >> ImageType[img] = Byte @@ -166,7 +167,6 @@ class ImageType(Builtin): X> ImageType[Binarize[img]] = Bit - """ summary_text = "type of values used for each pixel element in an image" diff --git a/mathics/builtin/image/structure.py b/mathics/builtin/image/structure.py index 145d6b5ea..3e45689db 100644 --- a/mathics/builtin/image/structure.py +++ b/mathics/builtin/image/structure.py @@ -1,5 +1,5 @@ """ -Structural Image Operations +Operations on Image Structure """ import numpy @@ -9,6 +9,9 @@ from mathics.core.evaluation import Evaluation from mathics.eval.image import numpy_flip +# This tells documentation how to sort this module +sort_order = "mathics.builtin.image.operations" + def clip_to(i: int, upper) -> int: return min(i, upper) if i > 0 else max(0, upper + i) diff --git a/mathics/builtin/image/test.py b/mathics/builtin/image/test.py index 5d45cb9f0..b8fdcb472 100644 --- a/mathics/builtin/image/test.py +++ b/mathics/builtin/image/test.py @@ -2,21 +2,13 @@ Image testing """ from mathics.builtin.base import Test -from mathics.builtin.image.base import Image, _skimage_requires +from mathics.builtin.image.base import Image, skimage_requires # This tells documentation how to sort this module sort_order = "mathics.builtin.image.image-filters" -class _ImageTest(Test): - """ - Testing Image Builtins -- those function names ending with "Q" -- that require scikit-image. - """ - - requires = _skimage_requires - - -class BinaryImageQ(_ImageTest): +class BinaryImageQ(Test): """ :WMA link: https://reference.wolfram.com/language/ref/BinaryImageQ.html @@ -26,7 +18,7 @@ class BinaryImageQ(_ImageTest):
    returns True if the pixels of $image are binary bit values, and False otherwise.
    - S> img = Import["ExampleData/lena.tif"]; + S> img = Import["ExampleData/hedy.tif"]; S> BinaryImageQ[img] = False @@ -35,13 +27,15 @@ class BinaryImageQ(_ImageTest): : ... """ + requires = skimage_requires + summary_text = "test whether pixels in an image are binary bit values" - def test(self, expr): + def test(self, expr) -> bool: return isinstance(expr, Image) and expr.storage_type() == "Bit" -class ImageQ(_ImageTest): +class ImageQ(Test): """ :WMA link:https://reference.wolfram.com/language/ref/ImageQ.html @@ -66,7 +60,9 @@ class ImageQ(_ImageTest): = False """ + requires = skimage_requires + summary_text = "test whether is a valid image" - def test(self, expr): + def test(self, expr) -> bool: return isinstance(expr, Image) diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index df6e684c6..581b09612 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -6,6 +6,7 @@ from mathics.builtin.base import Builtin, Predefined from mathics.core.attributes import A_NO_ATTRIBUTES +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolNull @@ -37,7 +38,9 @@ class Echo_(Predefined): class Print(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Print.html + + :WMA link: + https://reference.wolfram.com/language/ref/Print.html
    'Print[$expr$, ...]' @@ -56,7 +59,7 @@ class Print(Builtin): summary_text = "print strings and formatted text" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation): "Print[expr__]" expr = expr.get_sequence() diff --git a/mathics/builtin/intfns/combinatorial.py b/mathics/builtin/intfns/combinatorial.py index 6701b0b08..d46ef1f78 100644 --- a/mathics/builtin/intfns/combinatorial.py +++ b/mathics/builtin/intfns/combinatorial.py @@ -360,7 +360,9 @@ def _compute(self, n, c_ff, c_ft, c_tf, c_tt): class Subsets(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Subsets.html + + :WMA link: + https://reference.wolfram.com/language/ref/Subsets.html
    'Subsets[$list$]' @@ -506,18 +508,18 @@ class Subsets(Builtin): def eval_list(self, list, evaluation): "Subsets[list_]" - return ( + if isinstance(list, Atom): evaluation.message("Subsets", "normal", Expression(SymbolSubsets, list)) - if isinstance(list, Atom) - else self.eval_list_n(list, Integer(len(list.elements)), evaluation) - ) + else: + return self.eval_list_n(list, Integer(len(list.elements)), evaluation) def eval_list_n(self, list, n, evaluation): "Subsets[list_, n_]" expr = Expression(SymbolSubsets, list, n) if isinstance(list, Atom): - return evaluation.message("Subsets", "normal", expr) + evaluation.message("Subsets", "normal", expr) + return else: head_t = list.head # Note: "n" does not have to be an Integer. @@ -525,7 +527,8 @@ def eval_list_n(self, list, n, evaluation): if n_value == 0: return ListExpression(ListExpression()) if n_value is None or n_value < 0: - return evaluation.message("Subsets", "nninfseq", expr) + evaluation.message("Subsets", "nninfseq", expr) + return nested_list = [ Expression(head_t, *c) @@ -541,7 +544,8 @@ def eval_list_pattern(self, list, n, evaluation): expr = Expression(SymbolSubsets, list, n) if isinstance(list, Atom): - return evaluation.message("Subsets", "normal", expr) + evaluation.message("Subsets", "normal", expr) + return else: head_t = list.head if n.get_name() == "System`All" or n.has_form("DirectedInfinity", 1): @@ -550,12 +554,14 @@ def eval_list_pattern(self, list, n, evaluation): n_len = len(n.elements) if n_len == 0: - return evaluation.message("Subsets", "nninfseq", expr) + evaluation.message("Subsets", "nninfseq", expr) + return elif n_len == 1: elem1 = n.elements[0].get_int_value() if elem1 is None or elem1 < 0: - return evaluation.message("Subsets", "nninfseq", expr) + evaluation.message("Subsets", "nninfseq", expr) + return min_n = elem1 max_n = min_n + 1 step_n = 1 @@ -568,7 +574,8 @@ def eval_list_pattern(self, list, n, evaluation): else len(list.elements) + 1 ) if elem1 is None or elem2 is None or elem1 < 0 or elem2 < 0: - return evaluation.message("Subsets", "nninfseq", expr) + evaluation.message("Subsets", "nninfseq", expr) + return min_n = elem1 max_n = elem2 + 1 step_n = 1 @@ -588,7 +595,8 @@ def eval_list_pattern(self, list, n, evaluation): or elem1 < 0 or elem2 < 0 ): - return evaluation.message("Subsets", "nninfseq", expr) + evaluation.message("Subsets", "nninfseq", expr) + return step_n = elem3 if step_n > 0: min_n = elem1 @@ -597,9 +605,11 @@ def eval_list_pattern(self, list, n, evaluation): min_n = elem1 max_n = elem2 - 1 else: - return evaluation.message("Subsets", "nninfseq", expr) + evaluation.message("Subsets", "nninfseq", expr) + return else: - return evaluation.message("Subsets", "nninfseq", expr) + evaluation.message("Subsets", "nninfseq", expr) + return nested_list = [ Expression(head_t, *c) @@ -612,7 +622,7 @@ def eval_list_pattern(self, list, n, evaluation): def eval_atom_pattern(self, list, n, spec, evaluation): "Subsets[list_?AtomQ, Pattern[n,_List|All|DirectedInfinity[1]], spec_]" - return evaluation.message( + evaluation.message( "Subsets", "normal", Expression(SymbolSubsets, list, n, spec) ) diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 356263ae9..c0e7dfef8 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -4,13 +4,12 @@ Division-Related Functions """ -from itertools import combinations from typing import List import sympy from sympy import Q, ask -from mathics.builtin.base import Builtin, SympyFunction, Test +from mathics.builtin.base import Builtin, SympyFunction from mathics.core.atoms import Integer from mathics.core.attributes import ( A_FLAT, @@ -23,26 +22,31 @@ ) from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_bool +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue -from mathics.core.systemsymbols import SymbolComplexInfinity - -SymbolQuotient = Symbol("Quotient") -SymbolQuotientRemainder = Symbol("QuotientRemainder") +from mathics.core.systemsymbols import ( + SymbolComplexInfinity, + SymbolQuotient, + SymbolQuotientRemainder, +) class CompositeQ(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/CompositeQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/CompositeQ.html
    'CompositeQ[$n$]' -
    returns True if $n$ is a composite number +
    returns 'True' if $n$ is a composite number
      -
    • A composite number is a positive number that is the product of two integers other than 1. -
    • For negative integer $n$, 'CompositeQ[$n$]' is effectively equivalent to 'CompositeQ[-$n$]'. +
    • A composite number is a positive number that is the product of two \ + integers other than 1. +
    • For negative integer $n$, 'CompositeQ[$n$]' is effectively equivalent \ + to 'CompositeQ[-$n$]'.
    >> Table[CompositeQ[n], {n, 0, 10}] @@ -52,59 +56,11 @@ class CompositeQ(Builtin): attributes = A_LISTABLE | A_PROTECTED summary_text = "test whether a number is composite" - def apply(self, n, evaluation): + def eval(self, n: Integer, evaluation: Evaluation): "CompositeQ[n_Integer]" return from_bool(ask(Q.composite(n.value))) -class CoprimeQ(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/CoprimeQ.html - -
    -
    'CoprimeQ[$x$, $y$]' -
    tests whether $x$ and $y$ are coprime by computing their greatest common divisor. -
    - - >> CoprimeQ[7, 9] - = True - - >> CoprimeQ[-4, 9] - = True - - >> CoprimeQ[12, 15] - = False - - CoprimeQ also works for complex numbers - >> CoprimeQ[1+2I, 1-I] - = True - - >> CoprimeQ[4+2I, 6+3I] - = True - - >> CoprimeQ[2, 3, 5] - = True - - >> CoprimeQ[2, 4, 5] - = False - """ - - attributes = A_LISTABLE | A_PROTECTED - summary_text = "test whether elements are coprime" - - def apply(self, args, evaluation): - "CoprimeQ[args__]" - - py_args = [arg.to_python() for arg in args.get_sequence()] - if not all(isinstance(i, int) or isinstance(i, complex) for i in py_args): - return SymbolFalse - - if all(sympy.gcd(n, m) == 1 for (n, m) in combinations(py_args, 2)): - return SymbolTrue - else: - return SymbolFalse - - class Divisible(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Divisible.html @@ -134,34 +90,11 @@ class Divisible(Builtin): summary_text = "test whether one number is divisible by the other" -class EvenQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/EvenQ.html - -
    -
    'EvenQ[$x$]' -
    returns 'True' if $x$ is even, and 'False' otherwise. -
    - - >> EvenQ[4] - = True - >> EvenQ[-3] - = False - >> EvenQ[n] - = False - """ - - attributes = A_LISTABLE | A_PROTECTED - summary_text = "test whether one number is divisible by the other" - - def test(self, n): - value = n.get_int_value() - return value is not None and value % 2 == 0 - - class GCD(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/GCD.html + + :WMA link: + https://reference.wolfram.com/language/ref/GCD.html
    'GCD[$n1$, $n2$, ...]' @@ -183,7 +116,7 @@ class GCD(Builtin): attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED summary_text = "greatest common divisor" - def apply(self, ns, evaluation): + def eval(self, ns, evaluation: Evaluation): "GCD[ns___Integer]" ns = ns.get_sequence() @@ -214,7 +147,7 @@ class LCM(Builtin): attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED summary_text = "least common multiple" - def apply(self, ns: List[Integer], evaluation): + def eval(self, ns: List[Integer], evaluation: Evaluation): "LCM[ns___Integer]" ns = ns.get_sequence() @@ -250,7 +183,7 @@ class Mod(Builtin): attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED summary_text = "the remainder in an integer division" - def apply(self, n: Integer, m: Integer, evaluation): + def eval(self, n: Integer, m: Integer, evaluation: Evaluation): "Mod[n_Integer, m_Integer]" n, m = n.value, m.value @@ -262,14 +195,20 @@ def apply(self, n: Integer, m: Integer, evaluation): class ModularInverse(SympyFunction): """ - :Modular multiplicative inverse: https://en.wikipedia.org/wiki/Modular_multiplicative_inverse (:SymPy: https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, :WMA: https://reference.wolfram.com/language/ref/ModularInverse.html) + + :Modular multiplicative inverse: + https://en.wikipedia.org/wiki/Modular_multiplicative_inverse ( + :SymPy: https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, + :WMA: https://reference.wolfram.com/language/ref/ModularInverse.html + )
    'ModularInverse[$k$, $n$]'
    returns the modular inverse $k$^(-1) mod $n$.
    - 'ModularInverse[$k$,$n$]' gives the smallest positive integer $r$ where the remainder of the division of $r$ x $k$ by $n$ is equal to 1. + 'ModularInverse[$k$,$n$]' gives the smallest positive integer $r$ where the remainder \ + of the division of $r$ x $k$ by $n$ is equal to 1. >> ModularInverse[2, 3] = 2 @@ -278,7 +217,7 @@ class ModularInverse(SympyFunction): >> k = 2; n = 3; Mod[ModularInverse[k, n] * k, n] == 1 = True - Some modular inverses just do not exists. For example when $k$ is a multple of $n$: + Some modular inverses just do not exists. For example when $k$ is a multiple of $n$: >> ModularInverse[k, k] = ModularInverse[2, 2] @@ -289,7 +228,7 @@ class ModularInverse(SympyFunction): summary_text = "returns the modular inverse $k^(-1)$ mod $n$" sympy_name = "mod_inverse" - def apply_k_n(self, k: Integer, n: Integer, evaluation): + def eval_k_n(self, k: Integer, n: Integer, evaluation: Evaluation): "ModularInverse[k_Integer, n_Integer]" try: r = sympy.mod_inverse(k.value, n.value) @@ -298,29 +237,6 @@ def apply_k_n(self, k: Integer, n: Integer, evaluation): return Integer(r) -class OddQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/OddQ.html - -
    -
    'OddQ[$x$]' -
    returns 'True' if $x$ is odd, and 'False' otherwise. -
    - - >> OddQ[-3] - = True - >> OddQ[0] - = False - """ - - attributes = A_LISTABLE | A_PROTECTED - summary_text = "test whether elements are odd numbers" - - def test(self, n): - value = n.get_int_value() - return value is not None and value % 2 != 0 - - class PowerMod(Builtin): """ Modular exponentiaion. @@ -352,7 +268,7 @@ class PowerMod(Builtin): } summary_text = "modular exponentiation" - def apply(self, a: Integer, b: Integer, m: Integer, evaluation): + def eval(self, a: Integer, b: Integer, m: Integer, evaluation: Evaluation): "PowerMod[a_Integer, b_Integer, m_Integer]" a_int = a @@ -371,60 +287,6 @@ def apply(self, a: Integer, b: Integer, m: Integer, evaluation): return Integer(pow(a, b, m)) -class PrimeQ(SympyFunction): - """ - :WMA link:https://reference.wolfram.com/language/ref/PrimeQ.html - -
    -
    'PrimeQ[$n$]' -
    returns 'True' if $n$ is a prime number. -
    - - For very large numbers, 'PrimeQ' uses probabilistic prime testing, so it might be wrong sometimes - (a number might be composite even though 'PrimeQ' says it is prime). - The algorithm might be changed in the future. - - >> PrimeQ[2] - = True - >> PrimeQ[-3] - = True - >> PrimeQ[137] - = True - >> PrimeQ[2 ^ 127 - 1] - = True - - #> PrimeQ[1] - = False - #> PrimeQ[2 ^ 255 - 1] - = False - - All prime numbers between 1 and 100: - >> Select[Range[100], PrimeQ] - = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97} - - 'PrimeQ' has attribute 'Listable': - >> PrimeQ[Range[20]] - = {False, True, True, False, True, False, True, False, False, False, True, False, True, False, False, False, True, False, True, False} - """ - - attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED - sympy_name = "isprime" - summary_text = "test whether elements are prime numbers" - - def apply(self, n, evaluation): - "PrimeQ[n_]" - - n = n.get_int_value() - if n is None: - return SymbolFalse - - n = abs(n) - if sympy.isprime(n): - return SymbolTrue - else: - return SymbolFalse - - class Quotient(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Quotient.html @@ -455,7 +317,7 @@ class Quotient(Builtin): } summary_text = "integer quotient" - def apply(self, m: Integer, n: Integer, evaluation): + def eval(self, m: Integer, n: Integer, evaluation: Evaluation): "Quotient[m_Integer, n_Integer]" py_m = m.value py_n = n.value @@ -503,17 +365,18 @@ class QuotientRemainder(Builtin): } summary_text = "integer quotient and remainder" - def apply(self, m, n, evaluation): + def eval(self, m, n, evaluation: Evaluation): "QuotientRemainder[m_, n_]" if m.is_numeric(evaluation) and n.is_numeric(): py_m = m.to_python() py_n = n.to_python() if py_n == 0: - return evaluation.message( + evaluation.message( "QuotientRemainder", "divz", Expression(SymbolQuotientRemainder, m, n), ) + return # Note: py_m % py_n can be a float or an int. # Also note that we *want* the first arguemnt to be an Integer. return to_mathics_list(Integer(py_m // py_n), py_m % py_n) diff --git a/mathics/builtin/intfns/misc.py b/mathics/builtin/intfns/misc.py index c8e058826..5746a5ac4 100644 --- a/mathics/builtin/intfns/misc.py +++ b/mathics/builtin/intfns/misc.py @@ -1,10 +1,5 @@ from mathics.builtin.arithmetic import _MPMathFunction -from mathics.core.attributes import ( - A_LISTABLE, - A_NUMERIC_FUNCTION, - A_PROTECTED, - A_READ_PROTECTED, -) +from mathics.core.attributes import A_LISTABLE, A_PROTECTED class BernoulliB(_MPMathFunction): diff --git a/mathics/builtin/intfns/recurrence.py b/mathics/builtin/intfns/recurrence.py index 45aa017bf..23434581c 100644 --- a/mathics/builtin/intfns/recurrence.py +++ b/mathics/builtin/intfns/recurrence.py @@ -2,7 +2,10 @@ """ Recurrence and Sum Functions -A recurrence relation is an equation that recursively defines a sequence or multidimensional array of values, once one or more initial terms are given; each further term of the sequence or array is defined as a function of the preceding terms. +A recurrence relation is an equation that recursively defines a \ +sequence or multidimensional array of values, once one or more initial \ +terms are given; each further term of the sequence or array is defined \ +as a function of the preceding terms. """ @@ -17,6 +20,7 @@ A_PROTECTED, A_READ_PROTECTED, ) +from mathics.core.evaluation import Evaluation class Fibonacci(_MPMathFunction): @@ -51,7 +55,7 @@ class HarmonicNumber(_MPMathFunction): :WMA link:https://reference.wolfram.com/language/ref/HarmonicNumber.html)
    -
    'HarmonicNumber[n]' +
    'HarmonicNumber[n]'
    returns the $n$th harmonic number.
    @@ -76,8 +80,12 @@ class HarmonicNumber(_MPMathFunction): # Note: WL allows StirlingS1[{2, 4, 6}, 2], but we don't (yet). class StirlingS1(Builtin): """ - :Stirling numbers of first kind:https://en.wikipedia.org/wiki/Stirling_numbers_of_the_first_kind \ - (:WMA link:https://reference.wolfram.com/language/ref/StirlingS1.html) + + :Stirling numbers of first kind: + https://en.wikipedia.org/wiki/Stirling_numbers_of_the_first_kind \ + ( + :WMA link: + https://reference.wolfram.com/language/ref/StirlingS1.html)
    'StirlingS1[$n$, $m$]' @@ -98,24 +106,29 @@ class StirlingS1(Builtin): sympy_name = "functions.combinatorial.stirling" mpmath_name = "stirling1" - def apply(self, n, m, evaluation): + def eval(self, n: Integer, m: Integer, evaluation: Evaluation): "%(name)s[n_Integer, m_Integer]" - n_value = n.get_int_value() - m_value = m.get_int_value() + n_value = n.value + m_value = m.value return Integer(stirling(n_value, m_value, kind=1, signed=True)) class StirlingS2(Builtin): """ - :Stirling numbers of first kind:https://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind \ - (:WMA link:https://reference.wolfram.com/language/ref/StirlingS2.html) + + :Stirling numbers of second kind: + https://en.wikipedia.org/wiki/Stirling_numbers_of_the_second_kind \ + ( + :WMA link + :https://reference.wolfram.com/language/ref/StirlingS2.html) -
    +
    'StirlingS2[$n$, $m$]'
    gives the Stirling number of the second kind _n^m.
    - returns the number of ways of partitioning a set of $n$ elements into $m$ non empty subsets. + returns the number of ways of partitioning a set of $n$ elements into $m$ \ + non empty subsets. >> Table[StirlingS2[10, m], {m, 10}] = {1, 511, 9330, 34105, 42525, 22827, 5880, 750, 45, 1} @@ -127,8 +140,8 @@ class StirlingS2(Builtin): mpmath_name = "stirling2" summary_text = "Stirling numbers of the second kind" - def apply(self, m, n, evaluation): + def eval(self, m: Integer, n: Integer, evaluation: Evaluation): "%(name)s[n_Integer, m_Integer]" - n_value = n.get_int_value() - m_value = m.get_int_value() + n_value = n.value + m_value = m.value return Integer(stirling(n_value, m_value, kind=2)) diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index 9b68b9ce9..7d01543f9 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -1,25 +1,27 @@ # -*- coding: utf-8 -*- """ +Layout + This module contains symbols used to define the high level layout for expression formatting. For instance, to represent a set of consecutive expressions in a row, -we can use ``Row`` +we can use 'Row'. """ from mathics.builtin.base import BinaryOperator, Builtin, Operator from mathics.builtin.box.layout import GridBox, RowBox, to_boxes -from mathics.builtin.lists import list_boxes from mathics.builtin.makeboxes import MakeBoxes from mathics.builtin.options import options_to_rules from mathics.core.atoms import Real, String -from mathics.core.expression import Expression +from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol from mathics.core.systemsymbols import SymbolMakeBoxes +from mathics.eval.lists import list_boxes from mathics.eval.makeboxes import format_element SymbolSubscriptBox = Symbol("System`SubscriptBox") @@ -88,23 +90,62 @@ class Grid(Builtin): = a b . . c d + + For shallow lists, elements are shown as a column: + >> Grid[{a, b, c}] + = a + . + . b + . + . c + + If the sublists have different sizes, the grid has the number of columns of the \ + largest one. Incomplete rows are completed with empty strings: + + >> Grid[{{"first", "second", "third"},{a},{1, 2, 3}}] + = first second third + . + . a + . + . 1 2 3 + + If the list is a mixture of lists and other expressions, the non-list expressions are + shown as rows: + + >> Grid[{"This is a long title", {"first", "second", "third"},{a},{1, 2, 3}}] + = This is a long title + . + . first second third + . + . a + . + . 1 2 3 + """ options = GridBox.options summary_text = " 2D layout containing arbitrary objects" - def apply_makeboxes(self, array, f, evaluation, options) -> Expression: - """MakeBoxes[Grid[array_?MatrixQ, OptionsPattern[Grid]], + def eval_makeboxes(self, array, f, evaluation: Evaluation, options) -> Expression: + """MakeBoxes[Grid[array_List, OptionsPattern[Grid]], f:StandardForm|TraditionalForm|OutputForm]""" + + elements = array.elements + + rows = ( + element.elements if element.has_form("List", None) else element + for element in elements + ) + + def format_row(row): + if isinstance(row, tuple): + return ListExpression( + *(format_element(item, evaluation, f) for item in row), + ) + return format_element(row, evaluation, f) + return GridBox( - ListExpression( - *( - ListExpression( - *(format_element(item, evaluation, f) for item in row.elements), - ) - for row in array.elements - ), - ), + ListExpression(*(format_row(row) for row in rows)), *options_to_rules(options), ) @@ -156,7 +197,8 @@ class Left(Builtin):
    'Left' -
    is used with operator formatting constructs to specify a left-associative operator. +
    is used with operator formatting constructs to specify a \ + left-associative operator.
    """ @@ -230,7 +272,7 @@ class Precedence(Builtin): summary_text = "an object to be parenthesized with a given precedence level" - def apply(self, expr, evaluation) -> Real: + def eval(self, expr, evaluation) -> Real: "Precedence[expr_]" name = expr.get_name() @@ -246,6 +288,19 @@ def apply(self, expr, evaluation) -> Real: return Real(precedence) +class PrecedenceForm(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/PrecedenceForm.html + +
    +
    'PrecedenceForm'[$expr$, $prec$] +
    format $expr$ parenthesized as it would be if it contained an operator of precedence $prec$. +
    + """ + + summary_text = "parenthesize with a precedence" + + class Prefix(BinaryOperator): """ :WMA link:https://reference.wolfram.com/language/ref/Prefix.html @@ -307,21 +362,21 @@ class Row(Builtin): summary_text = "1D layouts containing arbitrary objects in a row" - def apply_makeboxes(self, items, sep, f, evaluation): + def eval_makeboxes(self, items, sep, form, evaluation: Evaluation): """MakeBoxes[Row[{items___}, sep_:""], - f:StandardForm|TraditionalForm|OutputForm]""" + form:StandardForm|TraditionalForm|OutputForm]""" items = items.get_sequence() if not isinstance(sep, String): - sep = MakeBoxes(sep, f) + sep = MakeBoxes(sep, form) if len(items) == 1: - return MakeBoxes(items[0], f) + return MakeBoxes(items[0], form) else: result = [] for index, item in enumerate(items): if index > 0 and not sep.sameQ(String("")): result.append(to_boxes(sep, evaluation)) - item = MakeBoxes(item, f).evaluate(evaluation) + item = MakeBoxes(item, form).evaluate(evaluation) item = to_boxes(item, evaluation) result.append(item) return RowBox(*result) @@ -334,22 +389,31 @@ class Style(Builtin):
    'Style[$expr$, options]'
    displays $expr$ formatted using the specified option settings. +
    'Style[$expr$, "style"]'
    uses the option settings for the specified style in the current notebook. +
    'Style[$expr$, $color$]'
    displays using the specified color. +
    'Style[$expr$, $Bold$]'
    displays with fonts made bold. +
    'Style[$expr$, $Italic$]'
    displays with fonts made italic. +
    'Style[$expr$, $Underlined$]'
    displays with fonts underlined. +
    'Style[$expr$, $Larger$]
    displays with fonts made larger. +
    'Style[$expr$, $Smaller$]'
    displays with fonts made smaller. +
    'Style[$expr$, $n$]'
    displays with font size n. +
    'Style[$expr$, $Tiny$]'
    'Style[$expr$, $Small$]', etc.
    display with fonts that are tiny, small, etc. @@ -369,7 +433,9 @@ class Style(Builtin): class Subscript(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Subscript.html + + :WMA link: + https://reference.wolfram.com/language/ref/Subscript.html
    'Subscript[$a$, $i$]' @@ -382,7 +448,7 @@ class Subscript(Builtin): summary_text = "format an expression with a subscript" - def apply_makeboxes(self, x, y, f, evaluation) -> Expression: + def eval_makeboxes(self, x, y, f, evaluation) -> Expression: "MakeBoxes[Subscript[x_, y__], f:StandardForm|TraditionalForm]" y = y.get_sequence() @@ -395,7 +461,9 @@ def apply_makeboxes(self, x, y, f, evaluation) -> Expression: class Subsuperscript(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Subsuperscript.html + + :WMA link: + https://reference.wolfram.com/language/ref/Subsuperscript.html
    'Subsuperscript[$a$, $b$, $c$]' @@ -417,7 +485,9 @@ class Subsuperscript(Builtin): class Superscript(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Superscript.html + + :WMA link: + https://reference.wolfram.com/language/ref/Superscript.html
    'Superscript[$x$, $y$]' diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index 4426376b7..9c32c9386 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -3,24 +3,29 @@ """ Associations -An Association maps keys to values and is similar to a dictionary in Python; it is often sparse in that their key space is much larger than the number of actual keys found in the collection. +An Association maps keys to values and is similar to a dictionary in Python; \ +it is often sparse in that their key space is much larger than the number of \ +actual keys found in the collection. """ from mathics.builtin.base import Builtin, Test from mathics.builtin.box.layout import RowBox -from mathics.builtin.lists import list_boxes from mathics.core.atoms import Integer from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED from mathics.core.convert.expression import to_mathics_list +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolTrue from mathics.core.systemsymbols import SymbolAssociation, SymbolMakeBoxes, SymbolMissing +from mathics.eval.lists import list_boxes class Association(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Association.html + + :WMA link: + https://reference.wolfram.com/language/ref/Association.html
    'Association[$key1$ -> $val1$, $key2$ -> $val2$, ...]' @@ -81,7 +86,7 @@ class Association(Builtin): summary_text = "an association between keys and values" - def apply_makeboxes(self, rules, f, evaluation): + def eval_makeboxes(self, rules, f, evaluation: Evaluation): """MakeBoxes[<|rules___|>, f:StandardForm|TraditionalForm|OutputForm|InputForm]""" @@ -110,7 +115,7 @@ def validate(exprs): self.error_idx -= 1 return expr - def apply(self, rules, evaluation): + def eval(self, rules, evaluation: Evaluation): "Association[rules__]" def make_flatten(exprs, rules_dictionary: dict = {}): @@ -131,7 +136,7 @@ def make_flatten(exprs, rules_dictionary: dict = {}): except TypeError: return None - def apply_key(self, rules, key, evaluation): + def eval_key(self, rules, key, evaluation: Evaluation): "Association[rules__][key_]" def find_key(exprs, rules_dictionary: dict = {}): @@ -174,7 +179,7 @@ class AssociationQ(Test): summary_text = "test if an expression is a valid association" - def test(self, expr): + def test(self, expr) -> bool: def validate(elements): for element in elements: if element.has_form(("Rule", "RuleDelayed"), 2): @@ -189,6 +194,24 @@ def validate(elements): return expr.get_head_name() == "System`Association" and validate(expr.elements) +class Key(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Key.html + +
    +
    Key[$key$] +
    represents a key used to access a value in an association. +
    Key[$key$][$assoc$] +
    +
    + """ + + rules = { + "Key[key_][assoc_Association]": "assoc[key]", + } + summary_text = "indicate a key within a part specification" + + class Keys(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Keys.html @@ -260,7 +283,7 @@ class Keys(Builtin): summary_text = "list association keys" - def apply(self, rules, evaluation): + def eval(self, rules, evaluation: Evaluation): "Keys[rules___]" def get_keys(expr): @@ -277,7 +300,8 @@ def get_keys(expr): rules = rules.get_sequence() if len(rules) != 1: - return evaluation.message("Keys", "argx", Integer(len(rules))) + evaluation.message("Keys", "argx", Integer(len(rules))) + return try: return get_keys(rules[0]) @@ -291,7 +315,8 @@ class Lookup(Builtin):
    Lookup[$assoc$, $key$] -
    looks up the value associated with $key$ in the association $assoc$, or Missing[$KeyAbsent$]. +
    looks up the value associated with $key$ in the association $assoc$, \ + or Missing[$KeyAbsent$].
    """ @@ -306,11 +331,13 @@ class Lookup(Builtin): class Missing(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Missing.html + + :WMA link: + https://reference.wolfram.com/language/ref/Missing.html
    'Missing[]' -
    represents a data that is misssing. +
    represents a data that is missing.
    >> ElementData["Meitnerium","MeltingPoint"] = Missing[NotAvailable] @@ -390,7 +417,7 @@ class Values(Builtin): summary_text = "list association values" - def apply(self, rules, evaluation): + def eval(self, rules, evaluation: Evaluation): "Values[rules___]" def get_values(expr): @@ -408,9 +435,10 @@ def get_values(expr): rules = rules.get_sequence() if len(rules) != 1: - return evaluation.message("Values", "argx", Integer(len(rules))) + evaluation.message("Values", "argx", Integer(len(rules))) + return try: return get_values(rules[0]) except TypeError: - return evaluation.message("Values", "invrl", rules[0]) + evaluation.message("Values", "invrl", rules[0]) diff --git a/mathics/builtin/list/constructing.py b/mathics/builtin/list/constructing.py index 499bd5156..93e20f267 100644 --- a/mathics/builtin/list/constructing.py +++ b/mathics/builtin/list/constructing.py @@ -10,23 +10,26 @@ from itertools import permutations -from mathics.builtin.base import Builtin, Pattern -from mathics.builtin.lists import _IterationFunction, get_tuples -from mathics.core.atoms import Integer, Symbol -from mathics.core.attributes import A_HOLD_FIRST, A_LISTABLE, A_PROTECTED +from mathics.builtin.base import Builtin, IterationFunction, Pattern +from mathics.builtin.box.layout import RowBox +from mathics.core.atoms import Integer, is_integer_rational_or_real +from mathics.core.attributes import A_HOLD_FIRST, A_LISTABLE, A_LOCKED, A_PROTECTED from mathics.core.convert.expression import to_expression from mathics.core.convert.sympy import from_sympy from mathics.core.element import ElementsProperties +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression, structure from mathics.core.list import ListExpression from mathics.core.symbols import Atom - -SymbolNormal = Symbol("Normal") +from mathics.core.systemsymbols import SymbolNormal +from mathics.eval.lists import get_tuples, list_boxes class Array(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Array.html + + :WMA link: + https://reference.wolfram.com/language/ref/Array.html
    'Array[$f$, $n$]' @@ -36,10 +39,12 @@ class Array(Builtin):
    returns the $n$-element list '{$f$[$a$], ..., $f$[$a$ + $n$]}'.
    'Array[$f$, {$n$, $m$}, {$a$, $b$}]' -
    returns an $n$-by-$m$ matrix created by applying $f$ to indices ranging from '($a$, $b$)' to '($a$ + $n$, $b$ + $m$)'. +
    returns an $n$-by-$m$ matrix created by applying $f$ to indices \ + ranging from '($a$, $b$)' to '($a$ + $n$, $b$ + $m$)'.
    'Array[$f$, $dims$, $origins$, $h$]' -
    returns an expression with the specified dimensions and index origins, with head $h$ (instead of 'List'). +
    returns an expression with the specified dimensions and index origins, \ + with head $h$ (instead of 'List').
    >> Array[f, 4] @@ -52,16 +57,6 @@ class Array(Builtin): = {{f[4, 6], f[4, 7], f[4, 8]}, {f[5, 6], f[5, 7], f[5, 8]}} >> Array[f, {2, 3}, 1, Plus] = f[1, 1] + f[1, 2] + f[1, 3] + f[2, 1] + f[2, 2] + f[2, 3] - - #> Array[f, {2, 3}, {1, 2, 3}] - : {2, 3} and {1, 2, 3} should have the same length. - = Array[f, {2, 3}, {1, 2, 3}] - #> Array[f, a] - : Single or list of non-negative integers expected at position 2. - = Array[f, a] - #> Array[f, 2, b] - : Single or list of non-negative integers expected at position 3. - = Array[f, 2, b] """ messages = { @@ -70,7 +65,7 @@ class Array(Builtin): summary_text = "form an array by applying a function to successive indices" - def apply(self, f, dimsexpr, origins, head, evaluation): + def eval(self, f, dimsexpr, origins, head, evaluation: Evaluation): "Array[f_, dimsexpr_, origins_:1, head_:List]" if dimsexpr.has_form("List", None): @@ -115,7 +110,9 @@ def rec(rest_dims, current): class ConstantArray(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/ConstantArray.html + + :WMA link: + https://reference.wolfram.com/language/ref/ConstantArray.html
    'ConstantArray[$expr$, $n$]' @@ -135,19 +132,61 @@ class ConstantArray(Builtin): } +class List(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/List.html + +
    +
    'List[$e1$, $e2$, ..., $ei$]' +
    '{$e1$, $e2$, ..., $ei$}' +
    represents a list containing the elements $e1$...$ei$. +
    + + 'List' is the head of lists: + >> Head[{1, 2, 3}] + = List + + Lists can be nested: + >> {{a, b, {c, d}}} + = {{a, b, {c, d}}} + """ + + attributes = A_LOCKED | A_PROTECTED + summary_text = "form a list" + + def eval(self, elements, evaluation): + """List[elements___]""" + # Pick out the elements part of the parameter elements; + # we we will call that `elements_part_of_elements__`. + # Note that the parameter elements may be wrapped in a Sequence[] + # so remove that if when it is present. + elements_part_of_elements__ = elements.get_sequence() + return ListExpression(*elements_part_of_elements__) + + def eval_makeboxes(self, items, f, evaluation): + """MakeBoxes[{items___}, + f:StandardForm|TraditionalForm|OutputForm|InputForm|FullForm]""" + + items = items.get_sequence() + return RowBox(*list_boxes(items, f, evaluation, "{", "}")) + + class Normal(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Normal.html + + :WMA link: + https://reference.wolfram.com/language/ref/Normal.html
    'Normal[expr_]' -
    Brings especial expressions to a normal expression from different especial forms. +
    Brings special expressions to a normal expression from different special \ + forms.
    """ summary_text = "convert objects to normal expressions" - def apply_general(self, expr, evaluation): + def eval_general(self, expr, evaluation: Evaluation): "Normal[expr_]" if isinstance(expr, Atom): return @@ -157,9 +196,16 @@ def apply_general(self, expr, evaluation): ) +range_list_elements_properties = ElementsProperties( + elements_fully_evaluated=True, is_flat=True, is_ordered=True +) + + class Range(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Range.html + + :WMA link: + https://reference.wolfram.com/language/ref/Range.html
    'Range[$n$]' @@ -171,23 +217,41 @@ class Range(Builtin): >> Range[5] = {1, 2, 3, 4, 5} + >> Range[-3, 2] = {-3, -2, -1, 0, 1, 2} + + >> Range[1.0, 2.3] + = {1., 2.} + >> Range[0, 2, 1/3] = {0, 1 / 3, 2 / 3, 1, 4 / 3, 5 / 3, 2} + + >> Range[1.0, 2.3, .5] + = {1., 1.5, 2.} + """ attributes = A_LISTABLE | A_PROTECTED + messages = { + "range": "Range specification does not have appropriate bounds.", + } + rules = { - "Range[imax_?RealNumberQ]": "Range[1, imax, 1]", - "Range[imin_?RealNumberQ, imax_?RealNumberQ]": "Range[imin, imax, 1]", + "Range[imax_]": "Range[1, imax, 1]", + "Range[imin_, imax_]": "Range[imin, imax, 1]", } summary_text = "form a list from a range of numbers or other objects" - def apply(self, imin, imax, di, evaluation): - "Range[imin_?RealNumberQ, imax_?RealNumberQ, di_?RealNumberQ]" + def eval(self, imin, imax, di, evaluation: Evaluation): + "Range[imin_, imax_, di_]" + + for arg in imin, imax, di: + if not is_integer_rational_or_real(arg): + evaluation.message(self.get_name(), "range") + return if ( isinstance(imin, Integer) @@ -195,9 +259,9 @@ def apply(self, imin, imax, di, evaluation): and isinstance(di, Integer) ): result = [Integer(i) for i in range(imin.value, imax.value + 1, di.value)] - # TODO: add ElementProperties in Expression interface refactor branch: - # fully_evaluated, flat, are True and is_ordered = di.value >= 0 - return ListExpression(*result) + return ListExpression( + *result, elements_properties=range_list_elements_properties + ) imin = imin.to_sympy() imax = imax.to_sympy() @@ -208,12 +272,16 @@ def apply(self, imin, imax, di, evaluation): evaluation.check_stopped() result.append(from_sympy(index)) index += di - return ListExpression(*result) + return ListExpression( + *result, elements_properties=range_list_elements_properties + ) class Permutations(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Permutations.html + + :WMA link: + https://reference.wolfram.com/language/ref/Permutations.html
    'Permutations[$list$]' @@ -243,22 +311,23 @@ class Permutations(Builtin): messages = { "argt": "Permutation expects at least one argument.", - "nninfseq": "The number specified at position 2 of `` must be a non-negative integer, All, or Infinity.", + "nninfseq": "The number specified at position 2 of `` must be a non-negative " + "integer, All, or Infinity.", } summary_text = "form permutations of a list" - def apply_argt(self, evaluation): + def eval_argt(self, evaluation: Evaluation): "Permutations[]" evaluation.message(self.get_name(), "argt") - def apply(self, li, evaluation): + def eval(self, li, evaluation: Evaluation): "Permutations[li_List]" return ListExpression( *[ListExpression(*p) for p in permutations(li.elements, len(li.elements))], ) - def apply_n(self, li, n, evaluation): + def eval_n(self, li, n, evaluation: Evaluation): "Permutations[li_List, n_]" rs = None @@ -291,11 +360,15 @@ def apply_n(self, li, n, evaluation): class Reap(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Reap.html + + :WMA link: + https://reference.wolfram.com/language/ref/Reap.html
    'Reap[$expr$]' -
    gives the result of evaluating $expr$, together with all values sown during this evaluation. Values sown with different tags are given in different lists. +
    gives the result of evaluating $expr$, together with all values \ + sown during this evaluation. Values sown with different tags \ + are given in different lists.
    'Reap[$expr$, $pattern$]'
    only yields values sown with a tag matching $pattern$. @@ -305,7 +378,8 @@ class Reap(Builtin):
    uses multiple patterns.
    'Reap[$expr$, $pattern$, $f$]' -
    applies $f$ on each tag and the corresponding values sown in the form '$f$[tag, {e1, e2, ...}]'. +
    applies $f$ on each tag and the corresponding values sown \ + in the form '$f$[tag, {e1, e2, ...}]'.
    >> Reap[Sow[3]; Sow[1]] @@ -339,7 +413,7 @@ class Reap(Builtin): "Reap[expr_]": "Reap[expr, _]", } - def apply(self, expr, patterns, f, evaluation): + def eval(self, expr, patterns, f, evaluation: Evaluation): "Reap[expr_, {patterns___}, f_]" patterns = patterns.get_sequence() @@ -396,7 +470,7 @@ class Sow(Builtin): "Sow[e_, tag_]": "Sow[e, {tag}]", } - def apply(self, e, tags, evaluation): + def eval(self, e, tags, evaluation: Evaluation): "Sow[e_, {tags___}]" tags = tags.get_sequence() @@ -405,9 +479,11 @@ def apply(self, e, tags, evaluation): return e -class Table(_IterationFunction): +class Table(IterationFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Table.html + + :WMA link: + https://reference.wolfram.com/language/ref/Table.html
    'Table[$expr$, $n$]' @@ -424,6 +500,7 @@ class Table(_IterationFunction):
    evaluates $expr$ with $i$ taking on the values $e1$, $e2$, ..., $ei$.
    + >> Table[x, 3] = {x, x, x} >> n = 0; Table[n = n + 1, {5}] @@ -442,11 +519,6 @@ class Table(_IterationFunction): 'Table' supports multi-dimensional tables: >> Table[{i, j}, {i, {a, b}}, {j, 1, 2}] = {{{a, 1}, {a, 2}}, {{b, 1}, {b, 2}}} - - #> Table[x, {x,0,1/3}] - = {0} - #> Table[x, {x, -0.2, 3.9}] - = {-0.2, 0.8, 1.8, 2.8, 3.8} """ rules = { @@ -494,14 +566,14 @@ class Tuples(Builtin): summary_text = "form n-tuples from a list" - def apply_n(self, expr, n, evaluation): + def eval_n(self, expr, n: Integer, evaluation: Evaluation): "Tuples[expr_, n_Integer]" if isinstance(expr, Atom): evaluation.message("Tuples", "normal") return - n = n.get_int_value() - if n is None or n < 0: + py_n = n.value + if py_n is None or py_n < 0: evaluation.message("Tuples", "intnn") return items = expr.elements @@ -516,10 +588,10 @@ def iterate(n_rest): yield [item] + rest return ListExpression( - *(Expression(expr.head, *elements) for elements in iterate(n)) + *(Expression(expr.head, *elements) for elements in iterate(py_n)) ) - def apply_lists(self, exprs, evaluation): + def eval_lists(self, exprs, evaluation: Evaluation): "Tuples[{exprs___}]" exprs = exprs.get_sequence() diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index 77f7fc635..79c9c0176 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -9,19 +9,8 @@ from itertools import chain -from mathics.algorithm.parts import ( - _drop_span_selector, - _parts, - _take_span_selector, - deletecases_with_levelspec, - python_levelspec, - set_part, - walk_levels, - walk_parts, -) from mathics.builtin.base import BinaryOperator, Builtin from mathics.builtin.box.layout import RowBox -from mathics.builtin.lists import list_boxes from mathics.core.atoms import Integer, Integer0, Integer1, String from mathics.core.attributes import ( A_HOLD_FIRST, @@ -32,7 +21,13 @@ ) from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python -from mathics.core.exceptions import InvalidLevelspecError, MessageException, PartError +from mathics.core.exceptions import ( + InvalidLevelspecError, + MessageException, + PartDepthError, + PartError, + PartRangeError, +) from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.rules import Rule @@ -42,11 +37,24 @@ SymbolByteArray, SymbolFailed, SymbolInfinity, + SymbolKey, SymbolMakeBoxes, SymbolMissing, SymbolSequence, SymbolSet, ) +from mathics.eval.lists import delete_one, delete_rec, list_boxes +from mathics.eval.parts import ( + _drop_span_selector, + _take_span_selector, + deletecases_with_levelspec, + parts, + python_levelspec, + set_part, + walk_levels, + walk_parts, +) +from mathics.eval.patterns import Matcher SymbolAppendTo = Symbol("System`AppendTo") SymbolDeleteCases = Symbol("System`DeleteCases") @@ -87,7 +95,8 @@ def eval(self, expr, item, evaluation): "Append[expr_, item_]" if isinstance(expr, Atom): - return evaluation.message("Append", "normal") + evaluation.message("Append", "normal") + return return expr.restructure( expr.head, @@ -140,7 +149,8 @@ def eval(self, s, element, evaluation): "AppendTo[s_, element_]" resolved_s = s.evaluate(evaluation) if s == resolved_s: - return evaluation.message("AppendTo", "rvalue", s) + evaluation.message("AppendTo", "rvalue", s) + return if not isinstance(resolved_s, Atom): result = Expression( @@ -148,9 +158,7 @@ def eval(self, s, element, evaluation): ) return result.evaluate(evaluation) - return evaluation.message( - "AppendTo", "normal", Expression(SymbolAppendTo, s, element) - ) + evaluation.message("AppendTo", "normal", Expression(SymbolAppendTo, s, element)) class Cases(Builtin): @@ -219,21 +227,21 @@ def eval(self, items, pattern, ls, evaluation, options): if isinstance(items, Atom): return ListExpression() - from mathics.builtin.patterns import Matcher - if ls.has_form("Rule", 2): if ls.elements[0].get_name() == "System`Heads": heads = ls.elements[1] is SymbolTrue ls = ListExpression(Integer1) else: - return evaluation.message("Position", "level", ls) + evaluation.message("Position", "level", ls) + return else: heads = self.get_option(options, "Heads", evaluation) is SymbolTrue try: start, stop = python_levelspec(ls) except InvalidLevelspecError: - return evaluation.message("Position", "level", ls) + evaluation.message("Position", "level", ls) + return results = [] @@ -288,6 +296,165 @@ class Count(Builtin): summary_text = "count the number of occurrences of a pattern" +class Delete(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/Delete.html + +
    +
    'Delete[$expr$, $i$]' +
    deletes the element at position $i$ in $expr$. The position is counted from the end if $i$ is negative. +
    'Delete[$expr$, {$m$, $n$, ...}]' +
    deletes the element at position {$m$, $n$, ...}. +
    'Delete[$expr$, {{$m1$, $n1$, ...}, {$m2$, $n2$, ...}, ...}]' +
    deletes the elements at several positions. +
    + + Delete the element at position 3: + >> Delete[{a, b, c, d}, 3] + = {a, b, d} + + Delete at position 2 from the end: + >> Delete[{a, b, c, d}, -2] + = {a, b, d} + + Delete at positions 1 and 3: + >> Delete[{a, b, c, d}, {{1}, {3}}] + = {b, d} + + Delete in a 2D array: + >> Delete[{{a, b}, {c, d}}, {2, 1}] + = {{a, b}, {d}} + + Deleting the head of a whole expression gives a Sequence object: + >> Delete[{a, b, c}, 0] + = Sequence[a, b, c] + + Delete in an expression with any head: + >> Delete[f[a, b, c, d], 3] + = f[a, b, d] + + Delete a head to splice in its arguments: + >> Delete[f[a, b, u + v, c], {3, 0}] + = f[a, b, u, v, c] + + >> Delete[{a, b, c}, 0] + = Sequence[a, b, c] + + #> Delete[1 + x ^ (a + b + c), {2, 2, 3}] + = 1 + x ^ (a + b) + + #> Delete[f[a, g[b, c], d], {{2}, {2, 1}}] + = f[a, d] + + #> Delete[f[a, g[b, c], d], m + n] + : The expression m + n cannot be used as a part specification. Use Key[m + n] instead. + = Delete[f[a, g[b, c], d], m + n] + + Delete without the position: + >> Delete[{a, b, c, d}] + : Delete called with 1 argument; 2 arguments are expected. + = Delete[{a, b, c, d}] + + Delete with many arguments: + >> Delete[{a, b, c, d}, 1, 2] + : Delete called with 3 arguments; 2 arguments are expected. + = Delete[{a, b, c, d}, 1, 2] + + Delete the element out of range: + >> Delete[{a, b, c, d}, 5] + : Part {5} of {a, b, c, d} does not exist. + = Delete[{a, b, c, d}, 5] + + #> Delete[{a, b, c, d}, {1, 2}] + : Part 2 of {a, b, c, d} does not exist. + = Delete[{a, b, c, d}, {1, 2}] + + Delete the position not integer: + >> Delete[{a, b, c, d}, {1, n}] + : Position specification n in {a, b, c, d} is not a machine-sized integer or a list of machine-sized integers. + = Delete[{a, b, c, d}, {1, n}] + + #> Delete[{a, b, c, d}, {{1}, n}] + : Position specification {n, {1}} in {a, b, c, d} is not a machine-sized integer or a list of machine-sized integers. + = Delete[{a, b, c, d}, {{1}, n}] + + #> Delete[{a, b, c, d}, {{1}, {n}}] + : Position specification n in {a, b, c, d} is not a machine-sized integer or a list of machine-sized integers. + = Delete[{a, b, c, d}, {{1}, {n}}] + """ + + messages = { + # FIXME: This message doesn't exist in more modern WMA, and + # Delete *can* take more than 2 arguments. + "argr": "Delete called with 1 argument; 2 arguments are expected.", + "argt": "Delete called with `1` arguments; 2 arguments are expected.", + "psl": "Position specification `1` in `2` is not a machine-sized integer or a list of machine-sized integers.", + "pkspec": "The expression `1` cannot be used as a part specification. Use `2` instead.", + } + summary_text = "delete elements from a list at given positions" + + def eval_one(self, expr, position: Integer, evaluation): + "Delete[expr_, position_Integer]" + pos = position.value + try: + return delete_one(expr, pos) + except PartRangeError: + evaluation.message("Part", "partw", ListExpression(position), expr) + + def eval(self, expr, positions, evaluation): + "Delete[expr_, positions___]" + positions = positions.get_sequence() + if len(positions) > 1: + evaluation.message("Delete", "argt", Integer(len(positions) + 1)) + return + elif len(positions) == 0: + evaluation.message("Delete", "argr") + return + + positions = positions[0] + if not positions.has_form("List", None): + evaluation.message( + "Delete", "pkspec", positions, Expression(SymbolKey, positions) + ) + return + + # Create new python list of the positions and sort it + positions = ( + [t for t in positions.elements] + if positions.elements[0].has_form("List", None) + else [positions] + ) + positions.sort(key=lambda e: e.get_sort_key(pattern_sort=True)) + newexpr = expr + for position in positions: + pos = [p.get_int_value() for p in position.get_elements()] + if None in pos: + evaluation.message( + "Delete", "psl", position.elements[pos.index(None)], expr + ) + return + if len(pos) == 0: + evaluation.message("Delete", "psl", ListExpression(*positions), expr) + return + try: + newexpr = delete_rec(newexpr, pos) + except PartDepthError as exc: + evaluation.message("Part", "partw", Integer(exc.index), expr) + return + except PartError: + evaluation.message("Part", "partw", ListExpression(*pos), expr) + return + return newexpr + + +# TODO: seems to want to produces a fancy box for failure. +# rules = {'Failure /: MakeBoxes[Failure[tag_, assoc_Association], StandardForm]' : +# 'With[{msg = assoc["MessageTemplate"], msgParam = assoc["MessageParameters"], type = assoc["Type"]}, ToBoxes @ Interpretation["Failure" @ Panel @ Grid[{{Style["\[WarningSign]", "Message", FontSize -> 35], Style["Message:", FontColor->GrayLevel[0.5]], ToString[StringForm[msg, Sequence @@ msgParam], StandardForm]}, {SpanFromAbove, Style["Tag:", FontColor->GrayLevel[0.5]], ToString[tag, StandardForm]},{SpanFromAbove,Style["Type:", FontColor->GrayLevel[0.5]],ToString[type, StandardForm]}},Alignment -> {Left, Top}], Failure[tag, assoc]] /; msg =!= Missing["KeyAbsent", "MessageTemplate"] && msgParam =!= Missing["KeyAbsent", "MessageParameters"] && msgParam =!= Missing["KeyAbsent", "Type"]]', +# } + + class DeleteCases(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/DeleteCases.html @@ -329,7 +496,7 @@ def eval_ls_n(self, items, pattern, levelspec, n, evaluation): # If levelspec is specified to a non-trivial value, # we need to proceed with this complicate procedure # involving 1) decode what is the levelspec means - # 2) find all the occurences + # 2) find all the occurrences # 3) Set all the occurences to ```System`Nothing``` levelspec = python_levelspec(levelspec) @@ -355,7 +522,6 @@ def eval_ls_n(self, items, pattern, levelspec, n, evaluation): if levelspec[0] != 1 or levelspec[1] != 1: return deletecases_with_levelspec(items, pattern, evaluation, levelspec, n) # A more efficient way to proceed if levelspec == 1 - from mathics.builtin.patterns import Matcher match = Matcher(pattern).match if n == -1: @@ -432,12 +598,13 @@ def eval(self, items, seqs, evaluation): seqs = seqs.get_sequence() if isinstance(items, Atom): - return evaluation.message( + evaluation.message( "Drop", "normal", 1, Expression(SymbolDrop, items, *seqs) ) + return try: - return _parts(items, [_drop_span_selector(seq) for seq in seqs], evaluation) + return parts(items, [_drop_span_selector(seq) for seq in seqs], evaluation) except MessageException as e: e.message(evaluation) @@ -685,7 +852,8 @@ def is_interger_list(expr_list): if level.has_form("List", None): len_list = len(level.elements) if len_list > 2 or not is_interger_list(level): - return evaluation.message("FirstPosition", "level", level) + evaluation.message("FirstPosition", "level", level) + return elif len_list == 0: min_Level = max_Level = None elif len_list == 1: @@ -697,7 +865,8 @@ def is_interger_list(expr_list): min_Level = 0 max_Level = level.get_int_value() else: - return evaluation.message("FirstPosition", "level", level) + evaluation.message("FirstPosition", "level", level) + return return self.eval( expr, @@ -709,6 +878,43 @@ def is_interger_list(expr_list): ) +# From backports in CellsToTeX. This functions provides compatibility to WMA 10. +# TODO: +# * Add doctests +# * Translate to python the more complex rules +# * Complete the support. + + +class Insert(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Insert.html + +
    +
    'Insert[$list$, $elem$, $n$]' +
    inserts $elem$ at position $n$ in $list$. When $n$ is negative, \ + the position is counted from the end. +
    + + >> Insert[{a,b,c,d,e}, x, 3] + = {a, b, x, c, d, e} + + >> Insert[{a,b,c,d,e}, x, -2] + = {a, b, c, d, x, e} + """ + + summary_text = "insert an element at a given position" + + def eval(self, expr, elem, n: Integer, evaluation): + "Insert[expr_List, elem_, n_Integer]" + + py_n = n.value + new_list = list(expr.get_elements()) + + position = py_n - 1 if py_n > 0 else py_n + 1 + new_list.insert(position, elem) + return expr.restructure(expr.head, new_list, evaluation, deps=(expr, elem)) + + class Last(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Last.html @@ -791,35 +997,11 @@ def eval(self, expr, evaluation): return Integer(len(expr.elements)) -class MemberQ(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/MemberQ.html - -
    -
    'MemberQ[$list$, $pattern$]' -
    returns 'True' if $pattern$ matches any element of $list$, or 'False' otherwise. -
    - - >> MemberQ[{a, b, c}, b] - = True - >> MemberQ[{a, b, c}, d] - = False - >> MemberQ[{"a", b, f[x]}, _?NumericQ] - = False - >> MemberQ[_List][{{}}] - = True - """ - - rules = { - "MemberQ[list_, pattern_]": ("Length[Select[list, MatchQ[#, pattern]&]] > 0"), - "MemberQ[pattern_][expr_]": "MemberQ[expr, pattern]", - } - summary_text = "test whether an element is a member of a list" - - class Most(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Most.html + + :WMA link: + https://reference.wolfram.com/language/ref/Most.html
    'Most[$expr$]' @@ -1073,7 +1255,6 @@ def eval(self, items, sel, evaluation): def eval_pattern(self, items, sel, pattern, evaluation): "Pick[items_, sel_, pattern_]" - from mathics.builtin.patterns import Matcher match = Matcher(pattern).match return self._do(items, sel, lambda s: match(s, evaluation), evaluation) @@ -1094,15 +1275,18 @@ class Position(Builtin): >> Position[{1, 2, 2, 1, 2, 3, 2}, 2] = {{2}, {3}, {5}, {7}} - Find positions upto 3 levels deep + Find positions upto 3 levels deep: + >> Position[{1 + Sin[x], x, (Tan[x] - y)^2}, x, 3] = {{1, 2, 1}, {2}} - Find all powers of x + Find all powers of x: + >> Position[{1 + x^2, x y ^ 2, 4 y, x ^ z}, x^_] = {{1, 2}, {4}} - Use Position as an operator + Use Position as an operator: + >> Position[_Integer][{1.5, 2, 2.5}] = {{2}} """ @@ -1117,7 +1301,8 @@ class Position(Builtin): def eval_invalidlevel(self, patt, expr, ls, evaluation, options={}): "Position[expr_, patt_, ls_, OptionsPattern[Position]]" - return evaluation.message("Position", "level", ls) + evaluation.message("Position", "level", ls) + return def eval_level(self, expr, patt, ls, evaluation, options={}): """Position[expr_, patt_, Optional[Pattern[ls, _?LevelQ], {0, DirectedInfinity[1]}], @@ -1126,9 +1311,8 @@ def eval_level(self, expr, patt, ls, evaluation, options={}): try: start, stop = python_levelspec(ls) except InvalidLevelspecError: - return evaluation.message("Position", "level", ls) - - from mathics.builtin.patterns import Matcher + evaluation.message("Position", "level", ls) + return match = Matcher(patt).match result = [] @@ -1145,7 +1329,9 @@ def callback(level, pos): class Prepend(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Prepend.html + + :WMA link: + https://reference.wolfram.com/language/ref/Prepend.html
    'Prepend[$expr$, $item$]' @@ -1179,7 +1365,8 @@ def eval(self, expr, item, evaluation): "Prepend[expr_, item_]" if isinstance(expr, Atom): - return evaluation.message("Prepend", "normal") + evaluation.message("Prepend", "normal") + return return expr.restructure( expr.head, @@ -1243,7 +1430,8 @@ def eval(self, s, item, evaluation): "PrependTo[s_, item_]" resolved_s = s.evaluate(evaluation) if s == resolved_s: - return evaluation.message("PrependTo", "rvalue", s) + evaluation.message("PrependTo", "rvalue", s) + return if not isinstance(resolved_s, Atom): result = Expression( @@ -1251,9 +1439,7 @@ def eval(self, s, item, evaluation): ) return result.evaluate(evaluation) - return evaluation.message( - "PrependTo", "normal", Expression(SymbolPrependTo, s, item) - ) + evaluation.message("PrependTo", "normal", Expression(SymbolPrependTo, s, item)) class ReplacePart(Builtin): @@ -1548,12 +1734,13 @@ def eval(self, items, seqs, evaluation): seqs = seqs.get_sequence() if isinstance(items, Atom): - return evaluation.message( + evaluation.message( "Take", "normal", 1, Expression(SymbolTake, items, *seqs) ) + return try: - return _parts(items, [_take_span_selector(seq) for seq in seqs], evaluation) + return parts(items, [_take_span_selector(seq) for seq in seqs], evaluation) except MessageException as e: e.message(evaluation) diff --git a/mathics/builtin/list/math.py b/mathics/builtin/list/math.py new file mode 100644 index 000000000..ad2a5f9b2 --- /dev/null +++ b/mathics/builtin/list/math.py @@ -0,0 +1,176 @@ +""" +Math & Counting Operations on Lists +""" +import heapq + +from mathics.builtin.base import Builtin, CountableInteger, NegativeIntegerException +from mathics.core.exceptions import MessageException +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol, SymbolTrue +from mathics.core.systemsymbols import SymbolAlternatives, SymbolMatchQ + + +class _RankedTake(Builtin): + messages = { + "intpm": "Expected non-negative integer at position `1` in `2`.", + "rank": "The specified rank `1` is not between 1 and `2`.", + } + + options = { + "ExcludedForms": "Automatic", + } + + def _compute(self, t, n, evaluation, options, f=None): + try: + limit = CountableInteger.from_expression(n) + except MessageException as e: + e.message(evaluation) + return + except NegativeIntegerException: + if f: + args = (3, Expression(self.get_name(), t, f, n)) + else: + args = (2, Expression(self.get_name(), t, n)) + evaluation.message(self.get_name(), "intpm", *args) + return + + if limit is None: + return + + if limit == 0: + return ListExpression() + else: + excluded = self.get_option(options, "ExcludedForms", evaluation) + if excluded: + if ( + isinstance(excluded, Symbol) + and excluded.get_name() == "System`Automatic" + ): + + def exclude(item): + if isinstance(item, Symbol) and item.get_name() in ( + "System`None", + "System`Null", + "System`Indeterminate", + ): + return True + elif item.get_head_name() == "System`Missing": + return True + else: + return False + + else: + excluded = Expression(SymbolAlternatives, *excluded.elements) + + def exclude(item): + return ( + Expression(SymbolMatchQ, item, excluded).evaluate( + evaluation + ) + is SymbolTrue + ) + + filtered = [element for element in t.elements if not exclude(element)] + else: + filtered = t.elements + + if limit > len(filtered): + if not limit.is_upper_limit(): + evaluation.message( + self.get_name(), "rank", limit.get_int_value(), len(filtered) + ) + return + else: + py_n = len(filtered) + else: + py_n = limit.get_int_value() + + if py_n < 1: + return ListExpression() + + if f: + heap = [ + (Expression(f, element).evaluate(evaluation), element, i) + for i, element in enumerate(filtered) + ] + element_pos = 1 # in tuple above + else: + heap = [(element, i) for i, element in enumerate(filtered)] + element_pos = 0 # in tuple above + + if py_n == 1: + result = [self._get_1(heap)] + else: + result = self._get_n(py_n, heap) + + return t.restructure("List", [x[element_pos] for x in result], evaluation) + + +class _RankedTakeSmallest(_RankedTake): + def _get_1(self, a): + return min(a) + + def _get_n(self, n, heap): + return heapq.nsmallest(n, heap) + + +class _RankedTakeLargest(_RankedTake): + def _get_1(self, a): + return max(a) + + def _get_n(self, n, heap): + return heapq.nlargest(n, heap) + + +class TakeLargestBy(_RankedTakeLargest): + """ + :WMA link:https://reference.wolfram.com/language/ref/TakeLargestBy.html + +
    +
    'TakeLargestBy[$list$, $f$, $n$]' +
    returns the a sorted list of the $n$ largest items in $list$ + using $f$ to retrieve the items' keys to compare them. +
    + + For details on how to use the ExcludedForms option, see TakeLargest[]. + + >> TakeLargestBy[{{1, -1}, {10, 100}, {23, 7, 8}, {5, 1}}, Total, 2] + = {{10, 100}, {23, 7, 8}} + + >> TakeLargestBy[{"abc", "ab", "x"}, StringLength, 1] + = {abc} + """ + + summary_text = "sublist of n largest elements according to a given criteria" + + def eval(self, element, f, n, evaluation, options): + "TakeLargestBy[element_List, f_, n_, OptionsPattern[TakeLargestBy]]" + return self._compute(element, n, evaluation, options, f=f) + + +class TakeSmallestBy(_RankedTakeSmallest): + """ + :WMA link: + https://reference.wolfram.com/language/ref/TakeSmallestBy.html + +
    +
    'TakeSmallestBy[$list$, $f$, $n$]' +
    returns the a sorted list of the $n$ smallest items in $list$ + using $f$ to retrieve the items' keys to compare them. +
    + + For details on how to use the ExcludedForms option, see TakeLargest[]. + + >> TakeSmallestBy[{{1, -1}, {10, 100}, {23, 7, 8}, {5, 1}}, Total, 2] + = {{1, -1}, {5, 1}} + + >> TakeSmallestBy[{"abc", "ab", "x"}, StringLength, 1] + = {x} + """ + + summary_text = "sublist of n largest elements according to a criteria" + + def eval(self, element, f, n, evaluation, options): + "TakeSmallestBy[element_List, f_, n_, OptionsPattern[TakeSmallestBy]]" + return self._compute(element, n, evaluation, options, f=f) diff --git a/mathics/builtin/list/predicates.py b/mathics/builtin/list/predicates.py new file mode 100644 index 000000000..80b9053fa --- /dev/null +++ b/mathics/builtin/list/predicates.py @@ -0,0 +1,114 @@ +""" +Predicates on Lists +""" + +from mathics.builtin.base import Builtin +from mathics.builtin.options import options_to_rules +from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue +from mathics.core.systemsymbols import SymbolContainsOnly + + +class ContainsOnly(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/ContainsOnly.html + +
    +
    'ContainsOnly[$list1$, $list2$]' +
    yields True if $list1$ contains only elements that appear in $list2$. +
    + + >> ContainsOnly[{b, a, a}, {a, b, c}] + = True + + The first list contains elements not present in the second list: + >> ContainsOnly[{b, a, d}, {a, b, c}] + = False + + >> ContainsOnly[{}, {a, b, c}] + = True + + #> ContainsOnly[1, {1, 2, 3}] + : List or association expected instead of 1. + = ContainsOnly[1, {1, 2, 3}] + + #> ContainsOnly[{1, 2, 3}, 4] + : List or association expected instead of 4. + = ContainsOnly[{1, 2, 3}, 4] + + Use Equal as the comparison function to have numerical tolerance: + >> ContainsOnly[{a, 1.0}, {1, a, b}, {SameTest -> Equal}] + = True + + #> ContainsOnly[{c, a}, {a, b, c}, IgnoreCase -> True] + : Unknown option IgnoreCase -> True in ContainsOnly. + : Unknown option IgnoreCase in . + = True + """ + + attributes = A_PROTECTED | A_READ_PROTECTED + + messages = { + "lsa": "List or association expected instead of `1`.", + "nodef": "Unknown option `1` for ContainsOnly.", + "optx": "Unknown option `1` in `2`.", + } + + options = { + "SameTest": "SameQ", + } + + summary_text = "test if all the elements of a list appears into another list" + + def check_options(self, expr, evaluation, options): + for key in options: + if key != "System`SameTest": + if expr is None: + evaluation.message("ContainsOnly", "optx", Symbol(key)) + else: + evaluation.message("ContainsOnly", "optx", Symbol(key), expr) + + return None + + def eval(self, list1, list2, evaluation, options={}): + "ContainsOnly[list1_List, list2_List, OptionsPattern[ContainsOnly]]" + + same_test = self.get_option(options, "SameTest", evaluation) + + def sameQ(a, b) -> bool: + """Mathics SameQ""" + result = Expression(same_test, a, b).evaluate(evaluation) + return result is SymbolTrue + + self.check_options(None, evaluation, options) + for a in list1.elements: + if not any(sameQ(a, b) for b in list2.elements): + return SymbolFalse + return SymbolTrue + + def eval_msg(self, e1, e2, evaluation, options={}): + "ContainsOnly[e1_, e2_, OptionsPattern[ContainsOnly]]" + + opts = ( + options_to_rules(options) + if len(options) <= 1 + else [ListExpression(*options_to_rules(options))] + ) + expr = Expression(SymbolContainsOnly, e1, e2, *opts) + + if not isinstance(e1, Symbol) and not e1.has_form("List", None): + evaluation.message("ContainsOnly", "lsa", e1) + return self.check_options(expr, evaluation, options) + + if not isinstance(e2, Symbol) and not e2.has_form("List", None): + evaluation.message("ContainsOnly", "lsa", e2) + return self.check_options(expr, evaluation, options) + + return self.check_options(expr, evaluation, options) + + +# TODO: ContainsAll, ContainsNone ContainsAny ContainsExactly diff --git a/mathics/builtin/list/rearrange.py b/mathics/builtin/list/rearrange.py index 6650ab103..952859860 100644 --- a/mathics/builtin/list/rearrange.py +++ b/mathics/builtin/list/rearrange.py @@ -11,14 +11,15 @@ from typing import Callable from mathics.builtin.base import Builtin, MessageException -from mathics.core.atoms import Integer +from mathics.core.atoms import Integer, Integer0 from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_PROTECTED +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression, structure +from mathics.core.expression_predefined import MATHICS3_INFINITY from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol, SymbolTrue -from mathics.core.systemsymbols import SymbolMap - -SymbolReverse = Symbol("Reverse") +from mathics.core.systemsymbols import SymbolMap, SymbolReverse, SymbolSplit +from mathics.eval.parts import walk_levels def _test_pair(test, a, b, evaluation, name): @@ -39,22 +40,23 @@ def _is_sameq(same_test): class _FastEquivalence: - # models an equivalence relation through SameQ. for n distinct elements (each - # in its own bin), we expect to make O(n) comparisons (if the hash function - # does not fail us by distributing items very unevenly). - - # IMPORTANT NOTE ON ATOM'S HASH FUNCTIONS / this code relies on this assumption: - # - # if SameQ[a, b] == true then hash(a) == hash(b) - # - # more specifically, this code bins items based on their hash code, and only if - # the hash code matches, is SameQ evoked. - # - # this assumption has been checked for these types: Integer, Real, Complex, - # String, Rational (*), Expression, Image; new atoms need proper hash functions - # - # (*) Rational values are sympy Rationals which are always held in reduced form - # and thus are hashed correctly (see sympy/core/number.py:Rational.__eq__()). + """ + Models an equivalence relation using SameQ. for n distinct elements (each + in its own bin), we expect to make O(n) comparisons (if the hash function + does not fail us by distributing items very unevenly). + + IMPORTANT NOTE ON ATOM'S HASH FUNCTIONS / this code relies on this assumption: + if SameQ[a, b] == true then hash(a) == hash(b) + + Specifically, this code bins items based on their hash code, and only if + the hash code matches, is SameQ evoked. + + This assumption has been checked for these types: Integer, Real, Complex, + String, Rational (*), Expression, Image; new atoms need proper hash functions + + (*) Rational values are sympy Rationals which are always held in reduced form + and thus are hashed correctly (see sympy/core/number.py:Rational.__eq__()). + """ def __init__(self): self._hashes = defaultdict(list) @@ -67,6 +69,195 @@ def sameQ(self, a, b) -> bool: return a.sameQ(b) +class _IllegalPaddingDepth(Exception): + def __init__(self, level): + self.level = level + + +class _Pad(Builtin): + messages = { + "normal": "Expression at position 1 in `` must not be an atom.", + "level": "Cannot pad list `3` which has `4` using padding `1` which specifies `2`.", + "ilsm": "Expected an integer or a list of integers at position `1` in `2`.", + } + + rules = {"%(name)s[l_]": "%(name)s[l, Automatic]"} + + @staticmethod + def _find_dims(expr): + def dive(expr, level): + if isinstance(expr, Expression): + if expr.elements: + return max(dive(x, level + 1) for x in expr.elements) + else: + return level + 1 + else: + return level + + def calc(expr, dims, level): + if isinstance(expr, Expression): + for x in expr.elements: + calc(x, dims, level + 1) + dims[level] = max(dims[level], len(expr.elements)) + + dims = [0] * dive(expr, 0) + calc(expr, dims, 0) + return dims + + @staticmethod + def _build( + element, n, x, m, level, mode + ): # mode < 0 for left pad, > 0 for right pad + if not n: + return element + if not isinstance(element, Expression): + raise _IllegalPaddingDepth(level) + + if isinstance(m, (list, tuple)): + current_m = m[0] if m else 0 + next_m = m[1:] + else: + current_m = m + next_m = m + + def clip(a, d, s): + assert d != 0 + if s < 0: + return a[-d:] # end with a[-1] + else: + return a[:d] # start with a[0] + + def padding(amount, sign): + if amount == 0: + return [] + elif len(n) > 1: + return [ + _Pad._build(ListExpression(), n[1:], x, next_m, level + 1, mode) + ] * amount + else: + return clip(x * (1 + amount // len(x)), amount, sign) + + elements = element.elements + d = n[0] - len(elements) + if d < 0: + new_elements = clip(elements, d, mode) + padding_main = [] + elif d >= 0: + new_elements = elements + padding_main = padding(d, mode) + + if current_m > 0: + padding_margin = padding( + min(current_m, len(new_elements) + len(padding_main)), -mode + ) + + if len(padding_margin) > len(padding_main): + padding_main = [] + new_elements = clip( + new_elements, -(len(padding_margin) - len(padding_main)), mode + ) + elif len(padding_margin) > 0: + padding_main = clip(padding_main, -len(padding_margin), mode) + else: + padding_margin = [] + + if len(n) > 1: + new_elements = ( + _Pad._build(e, n[1:], x, next_m, level + 1, mode) for e in new_elements + ) + + if mode < 0: + parts = (padding_main, new_elements, padding_margin) + else: + parts = (padding_margin, new_elements, padding_main) + + return Expression(element.get_head(), *list(chain(*parts))) + + def _pad(self, in_l, in_n, in_x, in_m, evaluation, expr): + if not isinstance(in_l, Expression): + evaluation.message(self.get_name(), "normal", expr()) + return + + py_n = None + if isinstance(in_n, Symbol) and in_n.get_name() == "System`Automatic": + py_n = _Pad._find_dims(in_l) + elif in_n.get_head_name() == "System`List": + if all(isinstance(element, Integer) for element in in_n.elements): + py_n = [element.get_int_value() for element in in_n.elements] + elif isinstance(in_n, Integer): + py_n = [in_n.get_int_value()] + + if py_n is None: + evaluation.message(self.get_name(), "ilsm", 2, expr()) + return + + if in_x.get_head_name() == "System`List": + py_x = in_x.elements + else: + py_x = [in_x] + + if isinstance(in_m, Integer): + py_m = in_m.get_int_value() + else: + if not all(isinstance(x, Integer) for x in in_m.elements): + evaluation.message(self.get_name(), "ilsm", 4, expr()) + return + py_m = [x.get_int_value() for x in in_m.elements] + + try: + return _Pad._build(in_l, py_n, py_x, py_m, 1, self._mode) + except _IllegalPaddingDepth as e: + + def levels(k): + if k == 1: + return "1 level" + else: + return "%d levels" % k + + evaluation.message( + self.get_name(), + "level", + in_n, + levels(len(py_n)), + in_l, + levels(e.level - 1), + ) + return None + + def eval_zero(self, element, n, evaluation: Evaluation): + "%(name)s[element_, n_]" + return self._pad( + element, + n, + Integer0, + Integer0, + evaluation, + lambda: Expression(self.get_name(), element, n), + ) + + def eval(self, element, n, x, evaluation: Evaluation): + "%(name)s[element_, n_, x_]" + return self._pad( + element, + n, + x, + Integer0, + evaluation, + lambda: Expression(self.get_name(), element, n, x), + ) + + def eval_margin(self, element, n, x, m, evaluation: Evaluation): + "%(name)s[element_, n_, x_, m_]" + return self._pad( + element, + n, + x, + m, + evaluation, + lambda: Expression(self.get_name(), element, n, x, m), + ) + + class _SlowEquivalence: # models an equivalence relation through a user defined test function. for n # distinct elements (each in its own bin), we need sum(1, .., n - 1) = O(n^2) @@ -117,7 +308,7 @@ class _GatherOperation(Builtin): ), } - def apply(self, values, test, evaluation): + def eval(self, values, test, evaluation: Evaluation): "%(name)s[values_, test_]" if not self._check_list(values, test, evaluation): return @@ -129,7 +320,7 @@ def apply(self, values, test, evaluation): values, values, _SlowEquivalence(test, evaluation, self.get_name()) ) - def _check_list(self, values, arg2, evaluation): + def _check_list(self, values, arg2, evaluation: Evaluation): if isinstance(values, Atom): expr = Expression(Symbol(self.get_name()), values, arg2) evaluation.message(self.get_name(), "normal", 1, expr) @@ -163,7 +354,7 @@ def _gather(self, keys, values, equivalence): class _Rotate(Builtin): messages = {"rspec": "`` should be an integer or a list of integers."} - def _rotate(self, expr, n, evaluation): + def _rotate(self, expr, n, evaluation: Evaluation): if not isinstance(expr, Expression): return expr @@ -181,11 +372,11 @@ def _rotate(self, expr, n, evaluation): return expr.restructure(expr.head, new_elements, evaluation) - def apply_one(self, expr, evaluation): + def eval_one(self, expr, evaluation: Evaluation): "%(name)s[expr_]" return self._rotate(expr, [1], evaluation) - def apply(self, expr, n, evaluation): + def eval(self, expr, n, evaluation: Evaluation): "%(name)s[expr_, n_]" if isinstance(n, Integer): py_cycles = [n.get_int_value()] @@ -228,26 +419,28 @@ def _remove_duplicates(arg, same_test): result.append(a) return result - def apply(self, lists, evaluation, options={}): + def eval(self, lists, evaluation, options={}): "%(name)s[lists__, OptionsPattern[%(name)s]]" seq = lists.get_sequence() for pos, e in enumerate(seq): if isinstance(e, Atom): - return evaluation.message( + evaluation.message( self.get_name(), "normal", pos + 1, Expression(Symbol(self.get_name()), *seq), ) + return for pos, e in enumerate(zip(seq, seq[1:])): e1, e2 = e if e1.head != e2.head: - return evaluation.message( + evaluation.message( self.get_name(), "heads", e1.head, e2.head, pos + 1, pos + 2 ) + return same_test = self.get_option(options, "SameTest", evaluation) operands = [li.elements for li in seq] @@ -296,7 +489,7 @@ class Catenate(Builtin): summary_text = "catenate elements from a list of lists" messages = {"invrp": "`1` is not a list."} - def apply(self, lists, evaluation): + def eval(self, lists, evaluation: Evaluation): "Catenate[lists_List]" def parts(): @@ -321,19 +514,24 @@ def parts(): class Complement(_SetOperation): """ - :WMA link:https://reference.wolfram.com/language/ref/Complement.html + + :WMA link: + https://reference.wolfram.com/language/ref/Complement.html
    'Complement[$all$, $e1$, $e2$, ...]' -
    returns an expression containing the elements in the set $all$ that are not in any of $e1$, $e2$, etc. +
    returns an expression containing the elements in the set $all$ \ + that are not in any of $e1$, $e2$, etc.
    'Complement[$all$, $e1$, $e2$, ..., SameTest->$test$]' -
    applies $test$ to the elements in $all$ and each of the $ei$ to determine equality. +
    applies $test$ to the elements in $all$ and each of the $ei$ to \ + determine equality.
    The sets $all$, $e1$, etc can have any head, which must all match. - The returned expression has the same head as the input - expressions. The expression will be sorted and each element will + + The returned expression has the same head as the input \ + expressions. The expression will be sorted and each element will \ only occur once. >> Complement[{a, b, c}, {a, c}] @@ -368,16 +566,19 @@ def _elementwise(self, a, b, sameQ: Callable[..., bool]): class DeleteDuplicates(_GatherOperation): """ - :WMA link:https://reference.wolfram.com/language/ref/DeleteDuplicates.html + + :WMA link: + https://reference.wolfram.com/language/ref/DeleteDuplicates.html
    'DeleteDuplicates[$list$]'
    deletes duplicates from $list$.
    'DeleteDuplicates[$list$, $test$]' -
    deletes elements from $list$ based on whether the function $test$ yields 'True' on pairs of elements. +
    deletes elements from $list$ based on whether the function $test$ yields \ + 'True' on pairs of elements. - DeleteDuplicates does not change the order of the remaining elements. + 'DeleteDuplicates' does not change the order of the remaining elements.
    >> DeleteDuplicates[{1, 7, 8, 4, 3, 4, 1, 9, 9, 2, 1}] @@ -399,7 +600,9 @@ class DeleteDuplicates(_GatherOperation): class Gather(_GatherOperation): """ - :WMA link:https://reference.wolfram.com/language/ref/Gather.html + + :WMA link: + https://reference.wolfram.com/language/ref/Gather.html
    'Gather[$list$, $test$]' @@ -422,16 +625,219 @@ class Gather(_GatherOperation): _bin = _GatherBin +class Flatten(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/Flatten.html + +
    +
    'Flatten[$expr$]' +
    flattens out nested lists in $expr$. + +
    'Flatten[$expr$, $n$]' +
    stops flattening at level $n$. + +
    'Flatten[$expr$, $n$, $h$]' +
    flattens expressions with head $h$ instead of 'List'. +
    + + >> Flatten[{{a, b}, {c, {d}, e}, {f, {g, h}}}] + = {a, b, c, d, e, f, g, h} + >> Flatten[{{a, b}, {c, {e}, e}, {f, {g, h}}}, 1] + = {a, b, c, {e}, e, f, {g, h}} + >> Flatten[f[a, f[b, f[c, d]], e], Infinity, f] + = f[a, b, c, d, e] + + >> Flatten[{{a, b}, {c, d}}, {{2}, {1}}] + = {{a, c}, {b, d}} + + >> Flatten[{{a, b}, {c, d}}, {{1, 2}}] + = {a, b, c, d} + + Flatten also works in irregularly shaped arrays + >> Flatten[{{1, 2, 3}, {4}, {6, 7}, {8, 9, 10}}, {{2}, {1}}] + = {{1, 4, 6, 8}, {2, 7, 9}, {3, 10}} + + #> Flatten[{{1, 2}, {3, 4}}, {{-1, 2}}] + : Levels to be flattened together in {{-1, 2}} should be lists of positive integers. + = Flatten[{{1, 2}, {3, 4}}, {{-1, 2}}, List] + + #> Flatten[{a, b}, {{1}, {2}}] + : Level 2 specified in {{1}, {2}} exceeds the levels, 1, which can be flattened together in {a, b}. + = Flatten[{a, b}, {{1}, {2}}, List] + + ## Check `n` completion + #> m = {{{1, 2}, {3}}, {{4}, {5, 6}}}; + #> Flatten[m, {{2}, {1}, {3}, {4}}] + : Level 4 specified in {{2}, {1}, {3}, {4}} exceeds the levels, 3, which can be flattened together in {{{1, 2}, {3}}, {{4}, {5, 6}}}. + = Flatten[{{{1, 2}, {3}}, {{4}, {5, 6}}}, {{2}, {1}, {3}, {4}}, List] + + ## Test from issue #251 + #> m = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + #> Flatten[m, {3}] + : Level 3 specified in {3} exceeds the levels, 2, which can be flattened together in {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}. + = Flatten[{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}, {3}, List] + + ## Reproduce strange head behaviour + #> Flatten[{{1}, 2}, {1, 2}] + : Level 2 specified in {1, 2} exceeds the levels, 1, which can be flattened together in {{1}, 2}. + = Flatten[{{1}, 2}, {1, 2}, List] + #> Flatten[a[b[1, 2], b[3]], {1, 2}, b] (* MMA BUG: {{1, 2}} not {1, 2} *) + : Level 1 specified in {1, 2} exceeds the levels, 0, which can be flattened together in a[b[1, 2], b[3]]. + = Flatten[a[b[1, 2], b[3]], {1, 2}, b] + + #> Flatten[{{1, 2}, {3, {4}}}, {{1, 2, 3}}] + : Level 3 specified in {{1, 2, 3}} exceeds the levels, 2, which can be flattened together in {{1, 2}, {3, {4}}}. + = Flatten[{{1, 2}, {3, {4}}}, {{1, 2, 3}}, List] + """ + + messages = { + "flpi": ( + "Levels to be flattened together in `1` " + "should be lists of positive integers." + ), + "flrep": ("Level `1` specified in `2` should not be repeated."), + "fldep": ( + "Level `1` specified in `2` exceeds the levels, `3`, " + "which can be flattened together in `4`." + ), + } + + rules = { + "Flatten[expr_]": "Flatten[expr, Infinity, Head[expr]]", + "Flatten[expr_, n_]": "Flatten[expr, n, Head[expr]]", + } + + summary_text = "flatten out any sequence of levels in a nested list" + + def eval_list(self, expr, n, h, evaluation): + "Flatten[expr_, n_List, h_]" + + # prepare levels + # find max depth which matches `h` + expr, max_depth = walk_levels(expr) + max_depth = {"max_depth": max_depth} # hack to modify max_depth from callback + + def callback(expr, pos): + if len(pos) < max_depth["max_depth"] and ( + isinstance(expr, Atom) or expr.head != h + ): + max_depth["max_depth"] = len(pos) + return expr + + expr, depth = walk_levels(expr, callback=callback, include_pos=True, start=0) + max_depth = max_depth["max_depth"] + + levels = n.to_python() + + # mappings + if isinstance(levels, list) and all(isinstance(level, int) for level in levels): + levels = [levels] + + # verify levels is list of lists of positive ints + if not (isinstance(levels, list) and len(levels) > 0): + evaluation.message("Flatten", "flpi", n) + return + seen_levels = [] + for level in levels: + if not (isinstance(level, list) and len(level) > 0): + evaluation.message("Flatten", "flpi", n) + return + for r in level: + if not (isinstance(r, int) and r > 0): + evaluation.message("Flatten", "flpi", n) + return + if r in seen_levels: + # level repeated + evaluation.message("Flatten", "flrep", r) + return + seen_levels.append(r) + + # complete the level spec e.g. {{2}} -> {{2}, {1}, {3}} + for s in range(1, max_depth + 1): + if s not in seen_levels: + levels.append([s]) + + # verify specified levels are smaller max depth + for level in levels: + for s in level: + if s > max_depth: + evaluation.message("Flatten", "fldep", s, n, max_depth, expr) + return + + # assign new indices to each element + new_indices = {} + + def callback(expr, pos): + if len(pos) == max_depth: + new_depth = tuple(tuple(pos[i - 1] for i in level) for level in levels) + new_indices[new_depth] = expr + return expr + + expr, depth = walk_levels(expr, callback=callback, include_pos=True) + + # build new tree inserting nodes as needed + elements = sorted(new_indices.items()) + + def insert_element(elements): + # gather elements into groups with the same leading index + # e.g. [((0, 0), a), ((0, 1), b), ((1, 0), c), ((1, 1), d)] + # -> [[(0, a), (1, b)], [(0, c), (1, d)]] + leading_index = None + grouped_elements = [] + for index, element in elements: + if index[0] == leading_index: + grouped_elements[-1].append((index[1:], element)) + else: + leading_index = index[0] + grouped_elements.append([(index[1:], element)]) + # for each group of elements we either insert them into the current level + # or make a new level and recurse + new_elements = [] + for group in grouped_elements: + if len(group[0][0]) == 0: # bottom level element or leaf + assert len(group) == 1 + new_elements.append(group[0][1]) + else: + new_elements.append(Expression(h, *insert_element(group))) + + return new_elements + + return Expression(h, *insert_element(elements)) + + def eval(self, expr, n, h, evaluation): + "Flatten[expr_, n_, h_]" + + if n.sameQ(MATHICS3_INFINITY): + n = -1 # a negative number indicates an unbounded level + else: + n_int = n.get_int_value() + # Here we test for negative since in Mathics Flatten[] as opposed to flatten_with_respect_to_head() + # negative numbers (and None) are not allowed. + if n_int is None or n_int < 0: + evaluation.message("Flatten", "flpi", n) + return + n = n_int + + return expr.flatten_with_respect_to_head(h, level=n) + + class GatherBy(_GatherOperation): """ - :WMA link:https://reference.wolfram.com/language/ref/GatherBy.html + + :WMA link: + https://reference.wolfram.com/language/ref/GatherBy.html
    'GatherBy[$list$, $f$]' -
    gathers elements of $list$ into sub lists of items whose image under $f$ identical. +
    gathers elements of $list$ into sub lists of items whose image \ + under $f$ identical.
    'GatherBy[$list$, {$f$, $g$, ...}]' -
    gathers elements of $list$ into sub lists of items whose image under $f$ identical. Then, gathers these sub lists again into sub sub lists, that are identical under $g. +
    gathers elements of $list$ into sub lists of items whose image \ + under $f$ identical. Then, gathers these sub lists again into sub \ + sub lists, that are identical under $g.
    >> GatherBy[{{1, 3}, {2, 2}, {1, 1}}, Total] @@ -455,7 +861,7 @@ class GatherBy(_GatherOperation): summary_text = "gather based on values of a function applied to elements" _bin = _GatherBin - def apply(self, values, func, evaluation): + def eval(self, values, func, evaluation: Evaluation): "%(name)s[values_, func_]" if not self._check_list(values, func, evaluation): @@ -470,7 +876,9 @@ def apply(self, values, func, evaluation): class Join(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Join.html + + :WMA link: + https://reference.wolfram.com/language/ref/Join.html
    'Join[$l1$, $l2$]' @@ -506,7 +914,7 @@ class Join(Builtin): attributes = A_FLAT | A_ONE_IDENTITY | A_PROTECTED summary_text = "join lists together at any level" - def apply(self, lists, evaluation): + def eval(self, lists, evaluation: Evaluation): "Join[lists___]" result = [] @@ -528,16 +936,95 @@ def apply(self, lists, evaluation): return ListExpression() +class PadLeft(_Pad): + """ + :WMA link:https://reference.wolfram.com/language/ref/PadLeft.html + +
    +
    'PadLeft[$list$, $n$]' +
    pads $list$ to length $n$ by adding 0 on the left. +
    'PadLeft[$list$, $n$, $x$]' +
    pads $list$ to length $n$ by adding $x$ on the left. +
    'PadLeft[$list$, {$n1$, $n2, ...}, $x$]' +
    pads $list$ to lengths $n1$, $n2$ at levels 1, 2, ... respectively by adding $x$ on the left. +
    'PadLeft[$list$, $n$, $x$, $m$]' +
    pads $list$ to length $n$ by adding $x$ on the left and adding a margin of $m$ on the right. +
    'PadLeft[$list$, $n$, $x$, {$m1$, $m2$, ...}]' +
    pads $list$ to length $n$ by adding $x$ on the left and adding margins of $m1$, $m2$, ... + on levels 1, 2, ... on the right. +
    'PadLeft[$list$]' +
    turns the ragged list $list$ into a regular list by adding 0 on the left. +
    + + >> PadLeft[{1, 2, 3}, 5] + = {0, 0, 1, 2, 3} + >> PadLeft[x[a, b, c], 5] + = x[0, 0, a, b, c] + >> PadLeft[{1, 2, 3}, 2] + = {2, 3} + >> PadLeft[{{}, {1, 2}, {1, 2, 3}}] + = {{0, 0, 0}, {0, 1, 2}, {1, 2, 3}} + >> PadLeft[{1, 2, 3}, 10, {a, b, c}, 2] + = {b, c, a, b, c, 1, 2, 3, a, b} + >> PadLeft[{{1, 2, 3}}, {5, 2}, x, 1] + = {{x, x}, {x, x}, {x, x}, {3, x}, {x, x}} + """ + + _mode = -1 + summary_text = "pad out by the left a ragged array to make a matrix" + + +class PadRight(_Pad): + """ + :WMA link:https://reference.wolfram.com/language/ref/PadRight.html + +
    +
    'PadRight[$list$, $n$]' +
    pads $list$ to length $n$ by adding 0 on the right. +
    'PadRight[$list$, $n$, $x$]' +
    pads $list$ to length $n$ by adding $x$ on the right. +
    'PadRight[$list$, {$n1$, $n2, ...}, $x$]' +
    pads $list$ to lengths $n1$, $n2$ at levels 1, 2, ... respectively by adding $x$ on the right. +
    'PadRight[$list$, $n$, $x$, $m$]' +
    pads $list$ to length $n$ by adding $x$ on the left and adding a margin of $m$ on the left. +
    'PadRight[$list$, $n$, $x$, {$m1$, $m2$, ...}]' +
    pads $list$ to length $n$ by adding $x$ on the right and adding margins of $m1$, $m2$, ... + on levels 1, 2, ... on the left. +
    'PadRight[$list$]' +
    turns the ragged list $list$ into a regular list by adding 0 on the right. +
    + + >> PadRight[{1, 2, 3}, 5] + = {1, 2, 3, 0, 0} + >> PadRight[x[a, b, c], 5] + = x[a, b, c, 0, 0] + >> PadRight[{1, 2, 3}, 2] + = {1, 2} + >> PadRight[{{}, {1, 2}, {1, 2, 3}}] + = {{0, 0, 0}, {1, 2, 0}, {1, 2, 3}} + >> PadRight[{1, 2, 3}, 10, {a, b, c}, 2] + = {b, c, 1, 2, 3, a, b, c, a, b} + >> PadRight[{{1, 2, 3}}, {5, 2}, x, 1] + = {{x, x}, {x, 1}, {x, x}, {x, x}, {x, x}} + """ + + _mode = 1 + summary_text = "pad out by the right a ragged array to make a matrix" + + class Partition(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Partition.html + + :WMA link: + https://reference.wolfram.com/language/ref/Partition.html
    'Partition[$list$, $n$]'
    partitions $list$ into sublists of length $n$.
    'Parition[$list$, $n$, $d$]' -
    partitions $list$ into sublists of length $n$ which overlap $d$ indicies. +
    partitions $list$ into sublists of length $n$ which overlap $d$ \ + indices.
    >> Partition[{a, b, c, d, e, f}, 2] @@ -560,7 +1047,7 @@ class Partition(Builtin): "Parition[list_, n_, d_, k]": "Partition[list, n, d, {k, k}]", } - def _partition(self, expr, n, d, evaluation): + def _partition(self, expr, n, d, evaluation: Evaluation): assert n > 0 and d > 0 inner = structure("List", expr, evaluation) @@ -581,12 +1068,12 @@ def slices(): return outer(slices()) - def apply_no_overlap(self, li, n, evaluation): + def eval_no_overlap(self, li, n: Integer, evaluation: Evaluation): "Partition[li_List, n_Integer]" # TODO: Error checking return self._partition(li, n.get_int_value(), n.get_int_value(), evaluation) - def apply(self, li, n, d, evaluation): + def eval(self, li, n: Integer, d: Integer, evaluation: Evaluation): "Partition[li_List, n_Integer, d_Integer]" # TODO: Error checking return self._partition(li, n.get_int_value(), d.get_int_value(), evaluation) @@ -594,7 +1081,9 @@ def apply(self, li, n, d, evaluation): class Reverse(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Reverse.html + + :WMA link: + https://reference.wolfram.com/language/ref/Reverse.html
    'Reverse[$expr$]' @@ -655,11 +1144,11 @@ def _reverse( return expr - def apply_top_level(self, expr, evaluation): + def eval_top_level(self, expr, evaluation: Evaluation): "Reverse[expr_]" return Reverse._reverse(expr, 1, (1,), evaluation) - def apply(self, expr, levels, evaluation): + def eval(self, expr, levels, evaluation: Evaluation): "Reverse[expr_, levels_]" if isinstance(levels, Integer): py_levels = [levels.get_int_value()] @@ -699,7 +1188,9 @@ def riffle_lists(items, seps): class Riffle(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Riffle.html + + :WMA link: + https://reference.wolfram.com/language/ref/Riffle.html
    'Riffle[$list$, $x$]' @@ -733,7 +1224,7 @@ class Riffle(Builtin): summary_text = "intersperse additional elements" - def apply(self, list, sep, evaluation): + def eval(self, list, sep, evaluation: Evaluation): "Riffle[list_List, sep_]" if sep.has_form("List", None): @@ -746,7 +1237,9 @@ def apply(self, list, sep, evaluation): class RotateLeft(_Rotate): """ - :WMA link:https://reference.wolfram.com/language/ref/RotateLeft.html + + :WMA link: + https://reference.wolfram.com/language/ref/RotateLeft.html
    'RotateLeft[$expr$]' @@ -756,7 +1249,8 @@ class RotateLeft(_Rotate):
    rotates the items of $expr$' by $n$ items to the left.
    'RotateLeft[$expr$, {$n1$, $n2$, ...}]' -
    rotates the items of $expr$' by $n1$ items to the left at the first level, by $n2$ items to the left at the second level, and so on. +
    rotates the items of $expr$' by $n1$ items to the left at \ + the first level, by $n2$ items to the left at the second level, and so on.
    >> RotateLeft[{1, 2, 3}] @@ -775,7 +1269,9 @@ class RotateLeft(_Rotate): class RotateRight(_Rotate): """ - :WMA link:https://reference.wolfram.com/language/ref/RotateRight.html + + :WMA link: + https://reference.wolfram.com/language/ref/RotateRight.html
    'RotateRight[$expr$]' @@ -802,16 +1298,162 @@ class RotateRight(_Rotate): _sign = -1 +class Split(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Split.html + +
    +
    'Split[$list$]' +
    splits $list$ into collections of consecutive identical elements. +
    'Split[$list$, $test$]' +
    splits $list$ based on whether the function $test$ yields + 'True' on consecutive elements. +
    + + >> Split[{x, x, x, y, x, y, y, z}] + = {{x, x, x}, {y}, {x}, {y, y}, {z}} + + #> Split[{x, x, x, y, x, y, y, z}, x] + = {{x}, {x}, {x}, {y}, {x}, {y}, {y}, {z}} + + Split into increasing or decreasing runs of elements + >> Split[{1, 5, 6, 3, 6, 1, 6, 3, 4, 5, 4}, Less] + = {{1, 5, 6}, {3, 6}, {1, 6}, {3, 4, 5}, {4}} + + >> Split[{1, 5, 6, 3, 6, 1, 6, 3, 4, 5, 4}, Greater] + = {{1}, {5}, {6, 3}, {6, 1}, {6, 3}, {4}, {5, 4}} + + Split based on first element + >> Split[{x -> a, x -> y, 2 -> a, z -> c, z -> a}, First[#1] === First[#2] &] + = {{x -> a, x -> y}, {2 -> a}, {z -> c, z -> a}} + + #> Split[{}] + = {} + + #> A[x__] := 321 /; Length[{x}] == 5; + #> Split[A[x, x, x, y, x, y, y, z]] + = 321 + #> ClearAll[A]; + """ + + rules = { + "Split[list_]": "Split[list, SameQ]", + } + + messages = { + "normal": "Nonatomic expression expected at position `1` in `2`.", + } + summary_text = "split into runs of identical elements" + + def eval(self, mlist, test, evaluation: Evaluation): + "Split[mlist_, test_]" + + expr = Expression(SymbolSplit, mlist, test) + + if isinstance(mlist, Atom): + evaluation.message("Select", "normal", 1, expr) + return + + if not mlist.elements: + return Expression(mlist.head) + + result = [[mlist.elements[0]]] + for element in mlist.elements[1:]: + applytest = Expression(test, result[-1][-1], element) + if applytest.evaluate(evaluation) is SymbolTrue: + result[-1].append(element) + else: + result.append([element]) + + inner = structure("List", mlist, evaluation) + outer = structure(mlist.head, inner, evaluation) + return outer([inner(t) for t in result]) + + +class SplitBy(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/SplitBy.html + +
    +
    'SplitBy[$list$, $f$]' +
    splits $list$ into collections of consecutive elements + that give the same result when $f$ is applied. +
    + + >> SplitBy[Range[1, 3, 1/3], Round] + = {{1, 4 / 3}, {5 / 3, 2, 7 / 3}, {8 / 3, 3}} + + >> SplitBy[{1, 2, 1, 1.2}, {Round, Identity}] + = {{{1}}, {{2}}, {{1}, {1.2}}} + + #> SplitBy[Tuples[{1, 2}, 3], First] + = {{{1, 1, 1}, {1, 1, 2}, {1, 2, 1}, {1, 2, 2}}, {{2, 1, 1}, {2, 1, 2}, {2, 2, 1}, {2, 2, 2}}} + """ + + messages = { + "normal": "Nonatomic expression expected at position `1` in `2`.", + } + + rules = { + "SplitBy[list_]": "SplitBy[list, Identity]", + } + + summary_text = "split based on values of a function applied to elements" + + def eval(self, mlist, func, evaluation: Evaluation): + "SplitBy[mlist_, func_?NotListQ]" + + expr = Expression(SymbolSplit, mlist, func) + + if isinstance(mlist, Atom): + evaluation.message("Select", "normal", 1, expr) + return + + plist = [t for t in mlist.elements] + + result = [[plist[0]]] + prev = Expression(func, plist[0]).evaluate(evaluation) + for element in plist[1:]: + curr = Expression(func, element).evaluate(evaluation) + if curr == prev: + result[-1].append(element) + else: + result.append([element]) + prev = curr + + inner = structure("List", mlist, evaluation) + outer = structure(mlist.head, inner, evaluation) + return outer([inner(t) for t in result]) + + def eval_multiple(self, mlist, funcs, evaluation: Evaluation): + "SplitBy[mlist_, funcs_List]" + expr = Expression(SymbolSplit, mlist, funcs) + + if isinstance(mlist, Atom): + evaluation.message("Select", "normal", 1, expr) + return + + result = mlist + for f in funcs.elements[::-1]: + result = self.eval(result, f, evaluation) + + return result + + class Tally(_GatherOperation): """ :WMA link:https://reference.wolfram.com/language/ref/Tally.html
    'Tally[$list$]' -
    counts and returns the number of occurences of objects and returns the result as a list of pairs {object, count}. +
    counts and returns the number of occurrences of objects and returns \ + the result as a list of pairs {object, count}.
    'Tally[$list$, $test$]' -
    counts the number of occurences of objects and uses $test to determine if two objects should be counted in the same bin. +
    counts the number of occurrences of objects and uses $test to \ + determine if two objects should be counted in the same bin.
    >> Tally[{a, b, c, b, a}] @@ -828,11 +1470,14 @@ class Tally(_GatherOperation): class Union(_SetOperation): """ - :WMA link:https://reference.wolfram.com/language/ref/Union.html + + :WMA link: + https://reference.wolfram.com/language/ref/Union.html
    'Union[$a$, $b$, ...]' -
    gives the union of the given set or sets. The resulting list will be sorted and each element will only occur once. +
    gives the union of the given set or sets. The resulting list \ + will be sorted and each element will only occur once.
    >> Union[{5, 1, 3, 7, 1, 8, 3}] @@ -867,11 +1512,14 @@ def _elementwise(self, a, b, sameQ: Callable[..., bool]): class Intersection(_SetOperation): """ - :WMA link:https://reference.wolfram.com/language/ref/Intersection.html + + :WMA link: + https://reference.wolfram.com/language/ref/Intersection.html
    'Intersection[$a$, $b$, ...]' -
    gives the intersection of the sets. The resulting list will be sorted and each element will only occur once. +
    gives the intersection of the sets. The resulting list \ + will be sorted and each element will only occur once.
    >> Intersection[{1000, 100, 10, 1}, {1, 5, 10, 15}] diff --git a/mathics/builtin/lists.py b/mathics/builtin/lists.py deleted file mode 100644 index d38396e82..000000000 --- a/mathics/builtin/lists.py +++ /dev/null @@ -1,2201 +0,0 @@ -# -*- coding: utf-8 -*- -""" -List Functions - Miscellaneous - -Functions here will eventually get moved to more suitable subsections. -""" - -import heapq -from itertools import chain - -import sympy - -from mathics.algorithm.clusters import ( - AutomaticMergeCriterion, - AutomaticSplitCriterion, - LazyDistances, - PrecomputedDistances, - agglomerate, - kmeans, - optimize, -) -from mathics.algorithm.parts import python_levelspec, walk_levels -from mathics.builtin.base import ( - Builtin, - CountableInteger, - NegativeIntegerException, - Predefined, - SympyFunction, - Test, -) -from mathics.builtin.box.layout import RowBox -from mathics.builtin.numbers.algebra import cancel -from mathics.builtin.options import options_to_rules -from mathics.builtin.scoping import dynamic_scoping -from mathics.core.atoms import ( - Integer, - Integer0, - Integer1, - Integer2, - Number, - Real, - String, - machine_precision, - min_prec, -) -from mathics.core.attributes import A_HOLD_ALL, A_LOCKED, A_PROTECTED, A_READ_PROTECTED -from mathics.core.convert.expression import to_expression, to_mathics_list -from mathics.core.convert.sympy import from_sympy -from mathics.core.exceptions import ( - InvalidLevelspecError, - MessageException, - PartDepthError, - PartError, - PartRangeError, -) -from mathics.core.expression import Expression, structure -from mathics.core.interrupt import BreakInterrupt, ContinueInterrupt, ReturnInterrupt -from mathics.core.list import ListExpression -from mathics.core.symbols import ( - Atom, - Symbol, - SymbolFalse, - SymbolPlus, - SymbolTrue, - strip_context, -) -from mathics.core.systemsymbols import ( - SymbolAlternatives, - SymbolFailed, - SymbolGreaterEqual, - SymbolLess, - SymbolLessEqual, - SymbolMakeBoxes, - SymbolMatchQ, - SymbolRule, - SymbolSequence, - SymbolSubsetQ, -) -from mathics.eval.nevaluator import eval_N -from mathics.eval.numerify import numerify - -SymbolClusteringComponents = Symbol("ClusteringComponents") -SymbolContainsOnly = Symbol("ContainsOnly") -SymbolFindClusters = Symbol("FindClusters") -SymbolKey = Symbol("Key") -SymbolSplit = Symbol("Split") - - -class All(Predefined): - """ - :WMA link:https://reference.wolfram.com/language/ref/All.html - -
    -
    'All' -
    is a possible option value for 'Span', 'Quiet', 'Part' and related functions. 'All' specifies all parts at a particular level. -
    - """ - - summary_text = "all the parts in the level" - - -class ContainsOnly(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/ContainsOnly.html - -
    -
    'ContainsOnly[$list1$, $list2$]' -
    yields True if $list1$ contains only elements that appear in $list2$. -
    - - >> ContainsOnly[{b, a, a}, {a, b, c}] - = True - - The first list contains elements not present in the second list: - >> ContainsOnly[{b, a, d}, {a, b, c}] - = False - - >> ContainsOnly[{}, {a, b, c}] - = True - - #> ContainsOnly[1, {1, 2, 3}] - : List or association expected instead of 1. - = ContainsOnly[1, {1, 2, 3}] - - #> ContainsOnly[{1, 2, 3}, 4] - : List or association expected instead of 4. - = ContainsOnly[{1, 2, 3}, 4] - - Use Equal as the comparison function to have numerical tolerance: - >> ContainsOnly[{a, 1.0}, {1, a, b}, {SameTest -> Equal}] - = True - - #> ContainsOnly[{c, a}, {a, b, c}, IgnoreCase -> True] - : Unknown option IgnoreCase -> True in ContainsOnly. - : Unknown option IgnoreCase in . - = True - """ - - attributes = A_PROTECTED | A_READ_PROTECTED - - messages = { - "lsa": "List or association expected instead of `1`.", - "nodef": "Unknown option `1` for ContainsOnly.", - "optx": "Unknown option `1` in `2`.", - } - - options = { - "SameTest": "SameQ", - } - - summary_text = "test if all the elements of a list appears into another list" - - def check_options(self, expr, evaluation, options): - for key in options: - if key != "System`SameTest": - if expr is None: - evaluation.message("ContainsOnly", "optx", Symbol(key)) - else: - return evaluation.message("ContainsOnly", "optx", Symbol(key), expr) - return None - - def eval(self, list1, list2, evaluation, options={}): - "ContainsOnly[list1_List, list2_List, OptionsPattern[ContainsOnly]]" - - same_test = self.get_option(options, "SameTest", evaluation) - - def sameQ(a, b) -> bool: - """Mathics SameQ""" - result = Expression(same_test, a, b).evaluate(evaluation) - return result is SymbolTrue - - self.check_options(None, evaluation, options) - for a in list1.elements: - if not any(sameQ(a, b) for b in list2.elements): - return SymbolFalse - return SymbolTrue - - def eval_msg(self, e1, e2, evaluation, options={}): - "ContainsOnly[e1_, e2_, OptionsPattern[ContainsOnly]]" - - opts = ( - options_to_rules(options) - if len(options) <= 1 - else [ListExpression(*options_to_rules(options))] - ) - expr = Expression(SymbolContainsOnly, e1, e2, *opts) - - if not isinstance(e1, Symbol) and not e1.has_form("List", None): - evaluation.message("ContainsOnly", "lsa", e1) - return self.check_options(expr, evaluation, options) - - if not isinstance(e2, Symbol) and not e2.has_form("List", None): - evaluation.message("ContainsOnly", "lsa", e2) - return self.check_options(expr, evaluation, options) - - return self.check_options(expr, evaluation, options) - - -class Delete(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Delete.html - -
    -
    'Delete[$expr$, $i$]' -
    deletes the element at position $i$ in $expr$. The position is counted from the end if $i$ is negative. -
    'Delete[$expr$, {$m$, $n$, ...}]' -
    deletes the element at position {$m$, $n$, ...}. -
    'Delete[$expr$, {{$m1$, $n1$, ...}, {$m2$, $n2$, ...}, ...}]' -
    deletes the elements at several positions. -
    - - Delete the element at position 3: - >> Delete[{a, b, c, d}, 3] - = {a, b, d} - - Delete at position 2 from the end: - >> Delete[{a, b, c, d}, -2] - = {a, b, d} - - Delete at positions 1 and 3: - >> Delete[{a, b, c, d}, {{1}, {3}}] - = {b, d} - - Delete in a 2D array: - >> Delete[{{a, b}, {c, d}}, {2, 1}] - = {{a, b}, {d}} - - Deleting the head of a whole expression gives a Sequence object: - >> Delete[{a, b, c}, 0] - = Sequence[a, b, c] - - Delete in an expression with any head: - >> Delete[f[a, b, c, d], 3] - = f[a, b, d] - - Delete a head to splice in its arguments: - >> Delete[f[a, b, u + v, c], {3, 0}] - = f[a, b, u, v, c] - - >> Delete[{a, b, c}, 0] - = Sequence[a, b, c] - - #> Delete[1 + x ^ (a + b + c), {2, 2, 3}] - = 1 + x ^ (a + b) - - #> Delete[f[a, g[b, c], d], {{2}, {2, 1}}] - = f[a, d] - - #> Delete[f[a, g[b, c], d], m + n] - : The expression m + n cannot be used as a part specification. Use Key[m + n] instead. - = Delete[f[a, g[b, c], d], m + n] - - Delete without the position: - >> Delete[{a, b, c, d}] - : Delete called with 1 argument; 2 arguments are expected. - = Delete[{a, b, c, d}] - - Delete with many arguments: - >> Delete[{a, b, c, d}, 1, 2] - : Delete called with 3 arguments; 2 arguments are expected. - = Delete[{a, b, c, d}, 1, 2] - - Delete the element out of range: - >> Delete[{a, b, c, d}, 5] - : Part {5} of {a, b, c, d} does not exist. - = Delete[{a, b, c, d}, 5] - - #> Delete[{a, b, c, d}, {1, 2}] - : Part 2 of {a, b, c, d} does not exist. - = Delete[{a, b, c, d}, {1, 2}] - - Delete the position not integer: - >> Delete[{a, b, c, d}, {1, n}] - : Position specification n in {a, b, c, d} is not a machine-sized integer or a list of machine-sized integers. - = Delete[{a, b, c, d}, {1, n}] - - #> Delete[{a, b, c, d}, {{1}, n}] - : Position specification {n, {1}} in {a, b, c, d} is not a machine-sized integer or a list of machine-sized integers. - = Delete[{a, b, c, d}, {{1}, n}] - - #> Delete[{a, b, c, d}, {{1}, {n}}] - : Position specification n in {a, b, c, d} is not a machine-sized integer or a list of machine-sized integers. - = Delete[{a, b, c, d}, {{1}, {n}}] - """ - - messages = { - # FIXME: This message doesn't exist in more modern WMA, and - # Delete *can* take more than 2 arguments. - "argr": "Delete called with 1 argument; 2 arguments are expected.", - "argt": "Delete called with `1` arguments; 2 arguments are expected.", - "psl": "Position specification `1` in `2` is not a machine-sized integer or a list of machine-sized integers.", - "pkspec": "The expression `1` cannot be used as a part specification. Use `2` instead.", - } - summary_text = "delete elements from a list at given positions" - - def eval_one(self, expr, position: Integer, evaluation): - "Delete[expr_, position_Integer]" - pos = position.value - try: - return delete_one(expr, pos) - except PartRangeError: - evaluation.message("Part", "partw", ListExpression(position), expr) - - def eval(self, expr, positions, evaluation): - "Delete[expr_, positions___]" - positions = positions.get_sequence() - if len(positions) > 1: - return evaluation.message("Delete", "argt", Integer(len(positions) + 1)) - elif len(positions) == 0: - return evaluation.message("Delete", "argr") - - positions = positions[0] - if not positions.has_form("List", None): - return evaluation.message( - "Delete", "pkspec", positions, Expression(SymbolKey, positions) - ) - - # Create new python list of the positions and sort it - positions = ( - [t for t in positions.elements] - if positions.elements[0].has_form("List", None) - else [positions] - ) - positions.sort(key=lambda e: e.get_sort_key(pattern_sort=True)) - newexpr = expr - for position in positions: - pos = [p.get_int_value() for p in position.get_elements()] - if None in pos: - return evaluation.message( - "Delete", "psl", position.elements[pos.index(None)], expr - ) - if len(pos) == 0: - return evaluation.message( - "Delete", "psl", ListExpression(*positions), expr - ) - try: - newexpr = delete_rec(newexpr, pos) - except PartDepthError as exc: - return evaluation.message("Part", "partw", Integer(exc.index), expr) - except PartError: - return evaluation.message("Part", "partw", ListExpression(*pos), expr) - return newexpr - - -class Failure(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Failure.html - -
    -
    Failure[$tag$, $assoc$] -
    represents a failure of a type indicated by $tag$, with details given by the association $assoc$. -
    - """ - - summary_text = "a failure at the level of the interpreter" - - -# From backports in CellsToTeX. This functions provides compatibility to WMA 10. -# TODO: -# * Add doctests -# * Translate to python the more complex rules -# * Complete the support. - - -class Key(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Key.html - -
    -
    Key[$key$] -
    represents a key used to access a value in an association. -
    Key[$key$][$assoc$] -
    -
    - """ - - rules = { - "Key[key_][assoc_Association]": "assoc[key]", - } - summary_text = "indicate a key within a part specification" - - -class Level(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Level.html - -
    -
    'Level[$expr$, $levelspec$]' -
    gives a list of all subexpressions of $expr$ at the - level(s) specified by $levelspec$. -
    - - Level uses standard level specifications: - -
    -
    $n$ -
    levels 1 through $n$ -
    'Infinity' -
    all levels from level 1 -
    '{$n$}' -
    level $n$ only -
    '{$m$, $n$}' -
    levels $m$ through $n$ -
    - - Level 0 corresponds to the whole expression. - - A negative level '-$n$' consists of parts with depth $n$. - - Level -1 is the set of atoms in an expression: - >> Level[a + b ^ 3 * f[2 x ^ 2], {-1}] - = {a, b, 3, 2, x, 2} - - >> Level[{{{{a}}}}, 3] - = {{a}, {{a}}, {{{a}}}} - >> Level[{{{{a}}}}, -4] - = {{{{a}}}} - >> Level[{{{{a}}}}, -5] - = {} - - >> Level[h0[h1[h2[h3[a]]]], {0, -1}] - = {a, h3[a], h2[h3[a]], h1[h2[h3[a]]], h0[h1[h2[h3[a]]]]} - - Use the option 'Heads -> True' to include heads: - >> Level[{{{{a}}}}, 3, Heads -> True] - = {List, List, List, {a}, {{a}}, {{{a}}}} - >> Level[x^2 + y^3, 3, Heads -> True] - = {Plus, Power, x, 2, x ^ 2, Power, y, 3, y ^ 3} - - >> Level[a ^ 2 + 2 * b, {-1}, Heads -> True] - = {Plus, Power, a, 2, Times, 2, b} - >> Level[f[g[h]][x], {-1}, Heads -> True] - = {f, g, h, x} - >> Level[f[g[h]][x], {-2, -1}, Heads -> True] - = {f, g, h, g[h], x, f[g[h]][x]} - """ - - options = { - "Heads": "False", - } - summary_text = "parts specified by a given number of indices" - - def eval(self, expr, ls, evaluation, options={}): - "Level[expr_, ls_, OptionsPattern[Level]]" - - try: - start, stop = python_levelspec(ls) - except InvalidLevelspecError: - evaluation.message("Level", "level", ls) - return - result = [] - - def callback(level): - result.append(level) - return level - - heads = self.get_option(options, "Heads", evaluation) is SymbolTrue - walk_levels(expr, start, stop, heads=heads, callback=callback) - return ListExpression(*result) - - -class LevelQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/LevelQ.html - -
    -
    'LevelQ[$expr$]' -
    tests whether $expr$ is a valid level specification. -
    - - >> LevelQ[2] - = True - >> LevelQ[{2, 4}] - = True - >> LevelQ[Infinity] - = True - >> LevelQ[a + b] - = False - """ - - summary_text = "test whether is a valid level specification" - - def test(self, ls): - try: - start, stop = python_levelspec(ls) - return True - except InvalidLevelspecError: - return False - - -class List(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/List.html - -
    -
    'List[$e1$, $e2$, ..., $ei$]' -
    '{$e1$, $e2$, ..., $ei$}' -
    represents a list containing the elements $e1$...$ei$. -
    - - 'List' is the head of lists: - >> Head[{1, 2, 3}] - = List - - Lists can be nested: - >> {{a, b, {c, d}}} - = {{a, b, {c, d}}} - """ - - attributes = A_LOCKED | A_PROTECTED - summary_text = "specify a list explicitly" - - def eval(self, elements, evaluation): - """List[elements___]""" - # Pick out the elements part of the parameter elements; - # we we will call that `elements_part_of_elements__`. - # Note that the parameter elements may be wrapped in a Sequence[] - # so remove that if when it is present. - elements_part_of_elements__ = elements.get_sequence() - return ListExpression(*elements_part_of_elements__) - - def eval_makeboxes(self, items, f, evaluation): - """MakeBoxes[{items___}, - f:StandardForm|TraditionalForm|OutputForm|InputForm|FullForm]""" - - items = items.get_sequence() - return RowBox(*list_boxes(items, f, evaluation, "{", "}")) - - -class ListQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/ListQ.html - -
    -
    'ListQ[$expr$]' -
    tests whether $expr$ is a 'List'. -
    - - >> ListQ[{1, 2, 3}] - = True - >> ListQ[{{1, 2}, {3, 4}}] - = True - >> ListQ[x] - = False - """ - - summary_text = "test if an expression is a list" - - def test(self, expr): - return expr.get_head_name() == "System`List" - - -class NotListQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/NotListQ.html - -
    -
    'NotListQ[$expr$]' -
    returns true if $expr$ is not a list. -
    - """ - - summary_text = "test if an expression is not a list" - - def test(self, expr): - return expr.get_head_name() != "System`List" - - -def riffle(items, sep): - result = items[:1] - for item in items[1:]: - result.append(sep) - result.append(item) - return result - - -def list_boxes(items, f, evaluation, open=None, close=None): - result = [ - Expression(SymbolMakeBoxes, item, f).evaluate(evaluation) for item in items - ] - if f.get_name() in ("System`OutputForm", "System`InputForm"): - sep = ", " - else: - sep = "," - result = riffle(result, String(sep)) - if len(items) > 1: - result = RowBox(*result) - elif items: - result = result[0] - if result: - result = [result] - else: - result = [] - if open is not None and close is not None: - return [String(open)] + result + [String(close)] - else: - return result - - -class None_(Predefined): - """ - :WMA link:https://reference.wolfram.com/language/ref/None.html - -
    -
    'None' -
    is a possible value for 'Span' and 'Quiet'. -
    - """ - - name = "None" - summary_text = "not any part" - - -class Split(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Split.html - -
    -
    'Split[$list$]' -
    splits $list$ into collections of consecutive identical elements. -
    'Split[$list$, $test$]' -
    splits $list$ based on whether the function $test$ yields - 'True' on consecutive elements. -
    - - >> Split[{x, x, x, y, x, y, y, z}] - = {{x, x, x}, {y}, {x}, {y, y}, {z}} - - #> Split[{x, x, x, y, x, y, y, z}, x] - = {{x}, {x}, {x}, {y}, {x}, {y}, {y}, {z}} - - Split into increasing or decreasing runs of elements - >> Split[{1, 5, 6, 3, 6, 1, 6, 3, 4, 5, 4}, Less] - = {{1, 5, 6}, {3, 6}, {1, 6}, {3, 4, 5}, {4}} - - >> Split[{1, 5, 6, 3, 6, 1, 6, 3, 4, 5, 4}, Greater] - = {{1}, {5}, {6, 3}, {6, 1}, {6, 3}, {4}, {5, 4}} - - Split based on first element - >> Split[{x -> a, x -> y, 2 -> a, z -> c, z -> a}, First[#1] === First[#2] &] - = {{x -> a, x -> y}, {2 -> a}, {z -> c, z -> a}} - - #> Split[{}] - = {} - - #> A[x__] := 321 /; Length[{x}] == 5; - #> Split[A[x, x, x, y, x, y, y, z]] - = 321 - #> ClearAll[A]; - """ - - rules = { - "Split[list_]": "Split[list, SameQ]", - } - - messages = { - "normal": "Nonatomic expression expected at position `1` in `2`.", - } - summary_text = "split into runs of identical elements" - - def eval(self, mlist, test, evaluation): - "Split[mlist_, test_]" - - expr = Expression(SymbolSplit, mlist, test) - - if isinstance(mlist, Atom): - evaluation.message("Select", "normal", 1, expr) - return - - if not mlist.elements: - return Expression(mlist.head) - - result = [[mlist.elements[0]]] - for element in mlist.elements[1:]: - applytest = Expression(test, result[-1][-1], element) - if applytest.evaluate(evaluation) is SymbolTrue: - result[-1].append(element) - else: - result.append([element]) - - inner = structure("List", mlist, evaluation) - outer = structure(mlist.head, inner, evaluation) - return outer([inner(t) for t in result]) - - -class SplitBy(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/SplitBy.html - -
    -
    'SplitBy[$list$, $f$]' -
    splits $list$ into collections of consecutive elements - that give the same result when $f$ is applied. -
    - - >> SplitBy[Range[1, 3, 1/3], Round] - = {{1, 4 / 3}, {5 / 3, 2, 7 / 3}, {8 / 3, 3}} - - >> SplitBy[{1, 2, 1, 1.2}, {Round, Identity}] - = {{{1}}, {{2}}, {{1}, {1.2}}} - - #> SplitBy[Tuples[{1, 2}, 3], First] - = {{{1, 1, 1}, {1, 1, 2}, {1, 2, 1}, {1, 2, 2}}, {{2, 1, 1}, {2, 1, 2}, {2, 2, 1}, {2, 2, 2}}} - """ - - messages = { - "normal": "Nonatomic expression expected at position `1` in `2`.", - } - - rules = { - "SplitBy[list_]": "SplitBy[list, Identity]", - } - - summary_text = "split based on values of a function applied to elements" - - def eval(self, mlist, func, evaluation): - "SplitBy[mlist_, func_?NotListQ]" - - expr = Expression(SymbolSplit, mlist, func) - - if isinstance(mlist, Atom): - evaluation.message("Select", "normal", 1, expr) - return - - plist = [t for t in mlist.elements] - - result = [[plist[0]]] - prev = Expression(func, plist[0]).evaluate(evaluation) - for element in plist[1:]: - curr = Expression(func, element).evaluate(evaluation) - if curr == prev: - result[-1].append(element) - else: - result.append([element]) - prev = curr - - inner = structure("List", mlist, evaluation) - outer = structure(mlist.head, inner, evaluation) - return outer([inner(t) for t in result]) - - def eval_multiple(self, mlist, funcs, evaluation): - "SplitBy[mlist_, funcs_List]" - expr = Expression(SymbolSplit, mlist, funcs) - - if isinstance(mlist, Atom): - evaluation.message("Select", "normal", 1, expr) - return - - result = mlist - for f in funcs.elements[::-1]: - result = self.eval(result, f, evaluation) - - return result - - -class LeafCount(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/LeafCount.html - -
    -
    'LeafCount[$expr$]' -
    returns the total number of indivisible subexpressions in $expr$. -
    - - >> LeafCount[1 + x + y^a] - = 6 - - >> LeafCount[f[x, y]] - = 3 - - >> LeafCount[{1 / 3, 1 + I}] - = 7 - - >> LeafCount[Sqrt[2]] - = 5 - - >> LeafCount[100!] - = 1 - - #> LeafCount[f[a, b][x, y]] - = 5 - - #> NestList[# /. s[x_][y_][z_] -> x[z][y[z]] &, s[s][s][s[s]][s][s], 4]; - #> LeafCount /@ % - = {7, 8, 8, 11, 11} - - #> LeafCount[1 / 3, 1 + I] - : LeafCount called with 2 arguments; 1 argument is expected. - = LeafCount[1 / 3, 1 + I] - """ - - messages = { - "argx": "LeafCount called with `1` arguments; 1 argument is expected.", - } - summary_text = "the total number of atomic subexpressions" - - def eval(self, expr, evaluation): - "LeafCount[expr___]" - - from mathics.core.atoms import Complex, Rational - - elements = [] - - def callback(level): - if isinstance(level, Rational): - elements.extend( - [level.get_head(), level.numerator(), level.denominator()] - ) - elif isinstance(level, Complex): - elements.extend([level.get_head(), level.real, level.imag]) - else: - elements.append(level) - return level - - expr = expr.get_sequence() - if len(expr) != 1: - return evaluation.message("LeafCount", "argx", Integer(len(expr))) - - walk_levels(expr[0], start=-1, stop=-1, heads=True, callback=callback) - return Integer(len(elements)) - - -class _IterationFunction(Builtin): - """ - >> Sum[k, {k, Range[5]}] - = 15 - """ - - attributes = A_HOLD_ALL | A_PROTECTED - allow_loopcontrol = False - throw_iterb = True - - def get_result(self, items): - pass - - def eval_symbol(self, expr, iterator, evaluation): - "%(name)s[expr_, iterator_Symbol]" - iterator = iterator.evaluate(evaluation) - if iterator.has_form(["List", "Range", "Sequence"], None): - elements = iterator.elements - if len(elements) == 1: - return self.apply_max(expr, *elements, evaluation) - elif len(elements) == 2: - if elements[1].has_form(["List", "Sequence"], None): - seq = Expression(SymbolSequence, *(elements[1].elements)) - return self.eval_list(expr, elements[0], seq, evaluation) - else: - return self.eval_range(expr, *elements, evaluation) - elif len(elements) == 3: - return self.eval_iter_nostep(expr, *elements, evaluation) - elif len(elements) == 4: - return self.eval_iter(expr, *elements, evaluation) - - if self.throw_iterb: - evaluation.message(self.get_name(), "iterb") - return - - def eval_range(self, expr, i, imax, evaluation): - "%(name)s[expr_, {i_Symbol, imax_}]" - imax = imax.evaluate(evaluation) - if imax.has_form("Range", None): - # FIXME: this should work as an iterator in Python3, not - # building the sequence explicitly... - seq = Expression(SymbolSequence, *(imax.evaluate(evaluation).elements)) - return self.apply_list(expr, i, seq, evaluation) - elif imax.has_form("List", None): - seq = Expression(SymbolSequence, *(imax.elements)) - return self.eval_list(expr, i, seq, evaluation) - else: - return self.eval_iter(expr, i, Integer1, imax, Integer1, evaluation) - - def eval_max(self, expr, imax, evaluation): - "%(name)s[expr_, {imax_}]" - - # Even though `imax` should be an integeral value, its type does not - # have to be an Integer. - - result = [] - - def do_iteration(): - evaluation.check_stopped() - try: - result.append(expr.evaluate(evaluation)) - except ContinueInterrupt: - if self.allow_loopcontrol: - pass - else: - raise - except BreakInterrupt: - if self.allow_loopcontrol: - raise StopIteration - else: - raise - except ReturnInterrupt as e: - if self.allow_loopcontrol: - return e.expr - else: - raise - - if isinstance(imax, Integer): - try: - for _ in range(imax.value): - do_iteration() - except StopIteration: - pass - - else: - imax = imax.evaluate(evaluation) - imax = numerify(imax, evaluation) - if isinstance(imax, Number): - imax = imax.round() - py_max = imax.get_float_value() - if py_max is None: - if self.throw_iterb: - evaluation.message(self.get_name(), "iterb") - return - - index = 0 - try: - while index < py_max: - do_iteration() - index += 1 - except StopIteration: - pass - - return self.get_result(result) - - def eval_iter_nostep(self, expr, i, imin, imax, evaluation): - "%(name)s[expr_, {i_Symbol, imin_, imax_}]" - return self.eval_iter(expr, i, imin, imax, Integer1, evaluation) - - def eval_iter(self, expr, i, imin, imax, di, evaluation): - "%(name)s[expr_, {i_Symbol, imin_, imax_, di_}]" - - if isinstance(self, SympyFunction) and di.get_int_value() == 1: - whole_expr = to_expression( - self.get_name(), expr, ListExpression(i, imin, imax) - ) - sympy_expr = whole_expr.to_sympy(evaluation=evaluation) - if sympy_expr is None: - return None - - # apply Together to produce results similar to Mathematica - result = sympy.together(sympy_expr) - result = from_sympy(result) - result = cancel(result) - - if not result.sameQ(whole_expr): - return result - return - - index = imin.evaluate(evaluation) - imax = imax.evaluate(evaluation) - di = di.evaluate(evaluation) - - result = [] - compare_type = ( - SymbolGreaterEqual - if Expression(SymbolLess, di, Integer0).evaluate(evaluation).to_python() - else SymbolLessEqual - ) - while True: - cont = Expression(compare_type, index, imax).evaluate(evaluation) - if cont is SymbolFalse: - break - if cont is not SymbolTrue: - if self.throw_iterb: - evaluation.message(self.get_name(), "iterb") - return - - evaluation.check_stopped() - try: - item = dynamic_scoping(expr.evaluate, {i.name: index}, evaluation) - result.append(item) - except ContinueInterrupt: - if self.allow_loopcontrol: - pass - else: - raise - except BreakInterrupt: - if self.allow_loopcontrol: - break - else: - raise - except ReturnInterrupt as e: - if self.allow_loopcontrol: - return e.expr - else: - raise - index = Expression(SymbolPlus, index, di).evaluate(evaluation) - return self.get_result(result) - - def eval_list(self, expr, i, items, evaluation): - "%(name)s[expr_, {i_Symbol, {items___}}]" - items = items.evaluate(evaluation).get_sequence() - result = [] - for item in items: - evaluation.check_stopped() - try: - item = dynamic_scoping(expr.evaluate, {i.name: item}, evaluation) - result.append(item) - except ContinueInterrupt: - if self.allow_loopcontrol: - pass - else: - raise - except BreakInterrupt: - if self.allow_loopcontrol: - break - else: - raise - except ReturnInterrupt as e: - if self.allow_loopcontrol: - return e.expr - else: - raise - return self.get_result(result) - - def eval_multi(self, expr, first, sequ, evaluation): - "%(name)s[expr_, first_, sequ__]" - - sequ = sequ.get_sequence() - name = self.get_name() - return to_expression(name, to_expression(name, expr, *sequ), first) - - -class Insert(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Insert.html - -
    -
    'Insert[$list$, $elem$, $n$]' -
    inserts $elem$ at position $n$ in $list$. When $n$ is negative, the position is counted from the end. -
    - - >> Insert[{a,b,c,d,e}, x, 3] - = {a, b, x, c, d, e} - - >> Insert[{a,b,c,d,e}, x, -2] - = {a, b, c, d, x, e} - """ - - summary_text = "insert an element at a given position" - - def eval(self, expr, elem, n: Integer, evaluation): - "Insert[expr_List, elem_, n_Integer]" - - py_n = n.value - new_list = list(expr.get_elements()) - - position = py_n - 1 if py_n > 0 else py_n + 1 - new_list.insert(position, elem) - return expr.restructure(expr.head, new_list, evaluation, deps=(expr, elem)) - - -def get_tuples(items): - if not items: - yield [] - else: - for item in items[0]: - for rest in get_tuples(items[1:]): - yield [item] + rest - - -class IntersectingQ(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/IntersectingQ.html - -
    -
    'IntersectingQ[$a$, $b$]' -
    gives True if there are any common elements in $a and $b, or False if $a and $b are disjoint. -
    - """ - - rules = {"IntersectingQ[a_List, b_List]": "Length[Intersect[a, b]] > 0"} - summary_text = "test whether two lists have common elements" - - -class DisjointQ(Test): - """ - :WMA link:https://reference.wolfram.com/language/ref/DisjointQ.html - -
    -
    'DisjointQ[$a$, $b$]' -
    gives True if $a and $b are disjoint, or False if $a and $b have any common elements. -
    - """ - - rules = {"DisjointQ[a_List, b_List]": "Not[IntersectingQ[a, b]]"} - summary_text = "test whether two lists do not have common elements" - - -class _NotRectangularException(Exception): - pass - - -class _Rectangular(Builtin): - # A helper for Builtins X that allow X[{a1, a2, ...}, {b1, b2, ...}, ...] to be evaluated - # as {X[{a1, b1, ...}, {a1, b2, ...}, ...]}. - - def rect(self, element): - lengths = [len(element.elements) for element in element.elements] - if all(length == 0 for length in lengths): - return # leave as is, without error - - n_columns = lengths[0] - if any(length != n_columns for length in lengths[1:]): - raise _NotRectangularException() - - transposed = [ - [element.elements[i] for element in element.elements] - for i in range(n_columns) - ] - - return ListExpression( - *[ - Expression(Symbol(self.get_name()), ListExpression(*items)) - for items in transposed - ], - ) - - -class _RankedTake(Builtin): - messages = { - "intpm": "Expected non-negative integer at position `1` in `2`.", - "rank": "The specified rank `1` is not between 1 and `2`.", - } - - options = { - "ExcludedForms": "Automatic", - } - - def _compute(self, t, n, evaluation, options, f=None): - try: - limit = CountableInteger.from_expression(n) - except MessageException as e: - e.message(evaluation) - return - except NegativeIntegerException: - if f: - args = (3, Expression(self.get_name(), t, f, n)) - else: - args = (2, Expression(self.get_name(), t, n)) - evaluation.message(self.get_name(), "intpm", *args) - return - - if limit is None: - return - - if limit == 0: - return ListExpression() - else: - excluded = self.get_option(options, "ExcludedForms", evaluation) - if excluded: - if ( - isinstance(excluded, Symbol) - and excluded.get_name() == "System`Automatic" - ): - - def exclude(item): - if isinstance(item, Symbol) and item.get_name() in ( - "System`None", - "System`Null", - "System`Indeterminate", - ): - return True - elif item.get_head_name() == "System`Missing": - return True - else: - return False - - else: - excluded = Expression(SymbolAlternatives, *excluded.elements) - - def exclude(item): - return ( - Expression(SymbolMatchQ, item, excluded).evaluate( - evaluation - ) - is SymbolTrue - ) - - filtered = [element for element in t.elements if not exclude(element)] - else: - filtered = t.elements - - if limit > len(filtered): - if not limit.is_upper_limit(): - evaluation.message( - self.get_name(), "rank", limit.get_int_value(), len(filtered) - ) - return - else: - py_n = len(filtered) - else: - py_n = limit.get_int_value() - - if py_n < 1: - return ListExpression() - - if f: - heap = [ - (Expression(f, element).evaluate(evaluation), element, i) - for i, element in enumerate(filtered) - ] - element_pos = 1 # in tuple above - else: - heap = [(element, i) for i, element in enumerate(filtered)] - element_pos = 0 # in tuple above - - if py_n == 1: - result = [self._get_1(heap)] - else: - result = self._get_n(py_n, heap) - - return t.restructure("List", [x[element_pos] for x in result], evaluation) - - -class _RankedTakeSmallest(_RankedTake): - def _get_1(self, a): - return min(a) - - def _get_n(self, n, heap): - return heapq.nsmallest(n, heap) - - -class _RankedTakeLargest(_RankedTake): - def _get_1(self, a): - return max(a) - - def _get_n(self, n, heap): - return heapq.nlargest(n, heap) - - -class TakeLargestBy(_RankedTakeLargest): - """ - :WMA link:https://reference.wolfram.com/language/ref/TakeLargestBy.html - -
    -
    'TakeLargestBy[$list$, $f$, $n$]' -
    returns the a sorted list of the $n$ largest items in $list$ - using $f$ to retrieve the items' keys to compare them. -
    - - For details on how to use the ExcludedForms option, see TakeLargest[]. - - >> TakeLargestBy[{{1, -1}, {10, 100}, {23, 7, 8}, {5, 1}}, Total, 2] - = {{10, 100}, {23, 7, 8}} - - >> TakeLargestBy[{"abc", "ab", "x"}, StringLength, 1] - = {abc} - """ - - summary_text = "sublist of n largest elements according to a given criteria" - - def eval(self, element, f, n, evaluation, options): - "TakeLargestBy[element_List, f_, n_, OptionsPattern[TakeLargestBy]]" - return self._compute(element, n, evaluation, options, f=f) - - -class TakeSmallestBy(_RankedTakeSmallest): - """ - :WMA link:https://reference.wolfram.com/language/ref/TakeSmallestBy.html - -
    -
    'TakeSmallestBy[$list$, $f$, $n$]' -
    returns the a sorted list of the $n$ smallest items in $list$ - using $f$ to retrieve the items' keys to compare them. -
    - - For details on how to use the ExcludedForms option, see TakeLargest[]. - - >> TakeSmallestBy[{{1, -1}, {10, 100}, {23, 7, 8}, {5, 1}}, Total, 2] - = {{1, -1}, {5, 1}} - - >> TakeSmallestBy[{"abc", "ab", "x"}, StringLength, 1] - = {x} - """ - - summary_text = "sublist of n largest elements according to a criteria" - - def eval(self, element, f, n, evaluation, options): - "TakeSmallestBy[element_List, f_, n_, OptionsPattern[TakeSmallestBy]]" - return self._compute(element, n, evaluation, options, f=f) - - -class _IllegalPaddingDepth(Exception): - def __init__(self, level): - self.level = level - - -class _Pad(Builtin): - messages = { - "normal": "Expression at position 1 in `` must not be an atom.", - "level": "Cannot pad list `3` which has `4` using padding `1` which specifies `2`.", - "ilsm": "Expected an integer or a list of integers at position `1` in `2`.", - } - - rules = {"%(name)s[l_]": "%(name)s[l, Automatic]"} - - @staticmethod - def _find_dims(expr): - def dive(expr, level): - if isinstance(expr, Expression): - if expr.elements: - return max(dive(x, level + 1) for x in expr.elements) - else: - return level + 1 - else: - return level - - def calc(expr, dims, level): - if isinstance(expr, Expression): - for x in expr.elements: - calc(x, dims, level + 1) - dims[level] = max(dims[level], len(expr.elements)) - - dims = [0] * dive(expr, 0) - calc(expr, dims, 0) - return dims - - @staticmethod - def _build( - element, n, x, m, level, mode - ): # mode < 0 for left pad, > 0 for right pad - if not n: - return element - if not isinstance(element, Expression): - raise _IllegalPaddingDepth(level) - - if isinstance(m, (list, tuple)): - current_m = m[0] if m else 0 - next_m = m[1:] - else: - current_m = m - next_m = m - - def clip(a, d, s): - assert d != 0 - if s < 0: - return a[-d:] # end with a[-1] - else: - return a[:d] # start with a[0] - - def padding(amount, sign): - if amount == 0: - return [] - elif len(n) > 1: - return [ - _Pad._build(ListExpression(), n[1:], x, next_m, level + 1, mode) - ] * amount - else: - return clip(x * (1 + amount // len(x)), amount, sign) - - elements = element.elements - d = n[0] - len(elements) - if d < 0: - new_elements = clip(elements, d, mode) - padding_main = [] - elif d >= 0: - new_elements = elements - padding_main = padding(d, mode) - - if current_m > 0: - padding_margin = padding( - min(current_m, len(new_elements) + len(padding_main)), -mode - ) - - if len(padding_margin) > len(padding_main): - padding_main = [] - new_elements = clip( - new_elements, -(len(padding_margin) - len(padding_main)), mode - ) - elif len(padding_margin) > 0: - padding_main = clip(padding_main, -len(padding_margin), mode) - else: - padding_margin = [] - - if len(n) > 1: - new_elements = ( - _Pad._build(e, n[1:], x, next_m, level + 1, mode) for e in new_elements - ) - - if mode < 0: - parts = (padding_main, new_elements, padding_margin) - else: - parts = (padding_margin, new_elements, padding_main) - - return Expression(element.get_head(), *list(chain(*parts))) - - def _pad(self, in_l, in_n, in_x, in_m, evaluation, expr): - if not isinstance(in_l, Expression): - evaluation.message(self.get_name(), "normal", expr()) - return - - py_n = None - if isinstance(in_n, Symbol) and in_n.get_name() == "System`Automatic": - py_n = _Pad._find_dims(in_l) - elif in_n.get_head_name() == "System`List": - if all(isinstance(element, Integer) for element in in_n.elements): - py_n = [element.get_int_value() for element in in_n.elements] - elif isinstance(in_n, Integer): - py_n = [in_n.get_int_value()] - - if py_n is None: - evaluation.message(self.get_name(), "ilsm", 2, expr()) - return - - if in_x.get_head_name() == "System`List": - py_x = in_x.elements - else: - py_x = [in_x] - - if isinstance(in_m, Integer): - py_m = in_m.get_int_value() - else: - if not all(isinstance(x, Integer) for x in in_m.elements): - evaluation.message(self.get_name(), "ilsm", 4, expr()) - return - py_m = [x.get_int_value() for x in in_m.elements] - - try: - return _Pad._build(in_l, py_n, py_x, py_m, 1, self._mode) - except _IllegalPaddingDepth as e: - - def levels(k): - if k == 1: - return "1 level" - else: - return "%d levels" % k - - evaluation.message( - self.get_name(), - "level", - in_n, - levels(len(py_n)), - in_l, - levels(e.level - 1), - ) - return None - - def eval_zero(self, element, n, evaluation): - "%(name)s[element_, n_]" - return self._pad( - element, - n, - Integer0, - Integer0, - evaluation, - lambda: Expression(self.get_name(), element, n), - ) - - def eval(self, element, n, x, evaluation): - "%(name)s[element_, n_, x_]" - return self._pad( - element, - n, - x, - Integer0, - evaluation, - lambda: Expression(self.get_name(), element, n, x), - ) - - def eval_margin(self, element, n, x, m, evaluation): - "%(name)s[element_, n_, x_, m_]" - return self._pad( - element, - n, - x, - m, - evaluation, - lambda: Expression(self.get_name(), element, n, x, m), - ) - - -class PadLeft(_Pad): - """ - :WMA link:https://reference.wolfram.com/language/ref/PadLeft.html - -
    -
    'PadLeft[$list$, $n$]' -
    pads $list$ to length $n$ by adding 0 on the left. -
    'PadLeft[$list$, $n$, $x$]' -
    pads $list$ to length $n$ by adding $x$ on the left. -
    'PadLeft[$list$, {$n1$, $n2, ...}, $x$]' -
    pads $list$ to lengths $n1$, $n2$ at levels 1, 2, ... respectively by adding $x$ on the left. -
    'PadLeft[$list$, $n$, $x$, $m$]' -
    pads $list$ to length $n$ by adding $x$ on the left and adding a margin of $m$ on the right. -
    'PadLeft[$list$, $n$, $x$, {$m1$, $m2$, ...}]' -
    pads $list$ to length $n$ by adding $x$ on the left and adding margins of $m1$, $m2$, ... - on levels 1, 2, ... on the right. -
    'PadLeft[$list$]' -
    turns the ragged list $list$ into a regular list by adding 0 on the left. -
    - - >> PadLeft[{1, 2, 3}, 5] - = {0, 0, 1, 2, 3} - >> PadLeft[x[a, b, c], 5] - = x[0, 0, a, b, c] - >> PadLeft[{1, 2, 3}, 2] - = {2, 3} - >> PadLeft[{{}, {1, 2}, {1, 2, 3}}] - = {{0, 0, 0}, {0, 1, 2}, {1, 2, 3}} - >> PadLeft[{1, 2, 3}, 10, {a, b, c}, 2] - = {b, c, a, b, c, 1, 2, 3, a, b} - >> PadLeft[{{1, 2, 3}}, {5, 2}, x, 1] - = {{x, x}, {x, x}, {x, x}, {3, x}, {x, x}} - """ - - _mode = -1 - summary_text = "pad out by the left a ragged array to make a matrix" - - -class PadRight(_Pad): - """ - :WMA link:https://reference.wolfram.com/language/ref/PadRight.html - -
    -
    'PadRight[$list$, $n$]' -
    pads $list$ to length $n$ by adding 0 on the right. -
    'PadRight[$list$, $n$, $x$]' -
    pads $list$ to length $n$ by adding $x$ on the right. -
    'PadRight[$list$, {$n1$, $n2, ...}, $x$]' -
    pads $list$ to lengths $n1$, $n2$ at levels 1, 2, ... respectively by adding $x$ on the right. -
    'PadRight[$list$, $n$, $x$, $m$]' -
    pads $list$ to length $n$ by adding $x$ on the left and adding a margin of $m$ on the left. -
    'PadRight[$list$, $n$, $x$, {$m1$, $m2$, ...}]' -
    pads $list$ to length $n$ by adding $x$ on the right and adding margins of $m1$, $m2$, ... - on levels 1, 2, ... on the left. -
    'PadRight[$list$]' -
    turns the ragged list $list$ into a regular list by adding 0 on the right. -
    - - >> PadRight[{1, 2, 3}, 5] - = {1, 2, 3, 0, 0} - >> PadRight[x[a, b, c], 5] - = x[a, b, c, 0, 0] - >> PadRight[{1, 2, 3}, 2] - = {1, 2} - >> PadRight[{{}, {1, 2}, {1, 2, 3}}] - = {{0, 0, 0}, {1, 2, 0}, {1, 2, 3}} - >> PadRight[{1, 2, 3}, 10, {a, b, c}, 2] - = {b, c, 1, 2, 3, a, b, c, a, b} - >> PadRight[{{1, 2, 3}}, {5, 2}, x, 1] - = {{x, x}, {x, 1}, {x, x}, {x, x}, {x, x}} - """ - - _mode = 1 - summary_text = "pad out by the right a ragged array to make a matrix" - - -class _IllegalDistance(Exception): - def __init__(self, distance): - self.distance = distance - - -class _IllegalDataPoint(Exception): - pass - - -def _to_real_distance(d): - if not isinstance(d, (Real, Integer)): - raise _IllegalDistance(d) - - mpd = d.to_mpmath() - if mpd is None or mpd < 0: - raise _IllegalDistance(d) - - return mpd - - -class _PrecomputedDistances(PrecomputedDistances): - # computes all n^2 distances for n points with one big evaluation in the beginning. - - def __init__(self, df, p, evaluation): - distances_form = [df(p[i], p[j]) for i in range(len(p)) for j in range(i)] - distances = eval_N(ListExpression(*distances_form), evaluation) - mpmath_distances = [_to_real_distance(d) for d in distances.elements] - super(_PrecomputedDistances, self).__init__(mpmath_distances) - - -class _LazyDistances(LazyDistances): - # computes single distances only as needed, caches already computed distances. - - def __init__(self, df, p, evaluation): - super(_LazyDistances, self).__init__() - self._df = df - self._p = p - self._evaluation = evaluation - - def _compute_distance(self, i, j): - p = self._p - d = eval_N(self._df(p[i], p[j]), self._evaluation) - return _to_real_distance(d) - - -def _dist_repr(p): - dist_p = repr_p = None - if p.has_form("Rule", 2): - if all(q.get_head_name() == "System`List" for q in p.elements): - dist_p, repr_p = (q.elements for q in p.elements) - elif ( - p.elements[0].get_head_name() == "System`List" - and p.elements[1].get_name() == "System`Automatic" - ): - dist_p = p.elements[0].elements - repr_p = [Integer(i + 1) for i in range(len(dist_p))] - elif p.get_head_name() == "System`List": - if all(q.get_head_name() == "System`Rule" for q in p.elements): - dist_p, repr_p = ([q.elements[i] for q in p.elements] for i in range(2)) - else: - dist_p = repr_p = p.elements - return dist_p, repr_p - - -class _Cluster(Builtin): - options = { - "Method": "Optimize", - "DistanceFunction": "Automatic", - "RandomSeed": "Automatic", - } - - messages = { - "amtd": "`1` failed to pick a suitable distance function for `2`.", - "bdmtd": 'Method in `` must be either "Optimize", "Agglomerate" or "KMeans".', - "intpm": "Positive integer expected at position 2 in ``.", - "list": "Expected a list or a rule with equally sized lists at position 1 in ``.", - "nclst": "Cannot find more clusters than there are elements: `1` is larger than `2`.", - "xnum": "The distance function returned ``, which is not a non-negative real value.", - "rseed": "The random seed specified through `` must be an integer or Automatic.", - "kmsud": "KMeans only supports SquaredEuclideanDistance as distance measure.", - } - - _criteria = { - "Optimize": AutomaticSplitCriterion, - "Agglomerate": AutomaticMergeCriterion, - "KMeans": None, - } - - def _cluster(self, p, k, mode, evaluation, options, expr): - method_string, method = self.get_option_string(options, "Method", evaluation) - if method_string not in ("Optimize", "Agglomerate", "KMeans"): - evaluation.message( - self.get_name(), "bdmtd", Expression(SymbolRule, "Method", method) - ) - return - - dist_p, repr_p = _dist_repr(p) - - if dist_p is None or len(dist_p) != len(repr_p): - evaluation.message(self.get_name(), "list", expr) - return - - if not dist_p: - return ListExpression() - - if k is not None: # the number of clusters k is specified as an integer. - if not isinstance(k, Integer): - evaluation.message(self.get_name(), "intpm", expr) - return - py_k = k.get_int_value() - if py_k < 1: - evaluation.message(self.get_name(), "intpm", expr) - return - if py_k > len(dist_p): - evaluation.message(self.get_name(), "nclst", py_k, len(dist_p)) - return - elif py_k == 1: - return ListExpression(*repr_p) - elif py_k == len(dist_p): - return ListExpression(*[ListExpression(q) for q in repr_p]) - else: # automatic detection of k. choose a suitable method here. - if len(dist_p) <= 2: - return ListExpression(*repr_p) - constructor = self._criteria.get(method_string) - py_k = (constructor, {}) if constructor else None - - seed_string, seed = self.get_option_string(options, "RandomSeed", evaluation) - if seed_string == "Automatic": - py_seed = 12345 - elif isinstance(seed, Integer): - py_seed = seed.get_int_value() - else: - evaluation.message( - self.get_name(), "rseed", Expression(SymbolRule, "RandomSeed", seed) - ) - return - - distance_function_string, distance_function = self.get_option_string( - options, "DistanceFunction", evaluation - ) - if distance_function_string == "Automatic": - from mathics.builtin.tensors import get_default_distance - - distance_function = get_default_distance(dist_p) - if distance_function is None: - name_of_builtin = strip_context(self.get_name()) - evaluation.message( - self.get_name(), - "amtd", - name_of_builtin, - ListExpression(*dist_p), - ) - return - if method_string == "KMeans" and distance_function is not Symbol( - "SquaredEuclideanDistance" - ): - evaluation.message(self.get_name(), "kmsud") - return - - def df(i, j) -> Expression: - return Expression(distance_function, i, j) - - try: - if method_string == "Agglomerate": - clusters = self._agglomerate(mode, repr_p, dist_p, py_k, df, evaluation) - elif method_string == "Optimize": - clusters = optimize( - repr_p, py_k, _LazyDistances(df, dist_p, evaluation), mode, py_seed - ) - elif method_string == "KMeans": - clusters = self._kmeans(mode, repr_p, dist_p, py_k, py_seed, evaluation) - except _IllegalDistance as e: - evaluation.message(self.get_name(), "xnum", e.distance) - return - except _IllegalDataPoint: - name_of_builtin = strip_context(self.get_name()) - evaluation.message( - self.get_name(), - "amtd", - name_of_builtin, - ListExpression(*dist_p), - ) - return - - if mode == "clusters": - return ListExpression(*[ListExpression(*c) for c in clusters]) - elif mode == "components": - return to_mathics_list(*clusters) - else: - raise ValueError("illegal mode %s" % mode) - - def _agglomerate(self, mode, repr_p, dist_p, py_k, df, evaluation): - if mode == "clusters": - clusters = agglomerate( - repr_p, py_k, _PrecomputedDistances(df, dist_p, evaluation), mode - ) - elif mode == "components": - clusters = agglomerate( - repr_p, py_k, _PrecomputedDistances(df, dist_p, evaluation), mode - ) - - return clusters - - def _kmeans(self, mode, repr_p, dist_p, py_k, py_seed, evaluation): - items = [] - - def convert_scalars(p): - for q in p: - if not isinstance(q, (Real, Integer)): - raise _IllegalDataPoint - mpq = q.to_mpmath() - if mpq is None: - raise _IllegalDataPoint - items.append(q) - yield mpq - - def convert_vectors(p): - d = None - for q in p: - if q.get_head_name() != "System`List": - raise _IllegalDataPoint - v = list(convert_scalars(q.elements)) - if d is None: - d = len(v) - elif len(v) != d: - raise _IllegalDataPoint - yield v - - if dist_p[0].is_numeric(evaluation): - numeric_p = [[x] for x in convert_scalars(dist_p)] - else: - numeric_p = list(convert_vectors(dist_p)) - - # compute epsilon similar to Real.__eq__, such that "numbers that differ in their last seven binary digits - # are considered equal" - - prec = min_prec(*items) or machine_precision - eps = 0.5 ** (prec - 7) - - return kmeans(numeric_p, repr_p, py_k, mode, py_seed, eps) - - -class FindClusters(_Cluster): - """ - :WMA link:https://reference.wolfram.com/language/ref/FindClusters.html - -
    -
    'FindClusters[$list$]' -
    returns a list of clusters formed from the elements of $list$. The number of cluster is determined - automatically. -
    'FindClusters[$list$, $k$]' -
    returns a list of $k$ clusters formed from the elements of $list$. -
    - - >> FindClusters[{1, 2, 20, 10, 11, 40, 19, 42}] - = {{1, 2, 20, 10, 11, 19}, {40, 42}} - - >> FindClusters[{25, 100, 17, 20}] - = {{25, 17, 20}, {100}} - - >> FindClusters[{3, 6, 1, 100, 20, 5, 25, 17, -10, 2}] - = {{3, 6, 1, 5, -10, 2}, {100}, {20, 25, 17}} - - >> FindClusters[{1, 2, 10, 11, 20, 21}] - = {{1, 2}, {10, 11}, {20, 21}} - - >> FindClusters[{1, 2, 10, 11, 20, 21}, 2] - = {{1, 2, 10, 11}, {20, 21}} - - >> FindClusters[{1 -> a, 2 -> b, 10 -> c}] - = {{a, b}, {c}} - - >> FindClusters[{1, 2, 5} -> {a, b, c}] - = {{a, b}, {c}} - - >> FindClusters[{1, 2, 3, 1, 2, 10, 100}, Method -> "Agglomerate"] - = {{1, 2, 3, 1, 2, 10}, {100}} - - >> FindClusters[{1, 2, 3, 10, 17, 18}, Method -> "Agglomerate"] - = {{1, 2, 3}, {10}, {17, 18}} - - >> FindClusters[{{1}, {5, 6}, {7}, {2, 4}}, DistanceFunction -> (Abs[Length[#1] - Length[#2]]&)] - = {{{1}, {7}}, {{5, 6}, {2, 4}}} - - >> FindClusters[{"meep", "heap", "deep", "weep", "sheep", "leap", "keep"}, 3] - = {{meep, deep, weep, keep}, {heap, leap}, {sheep}} - - FindClusters' automatic distance function detection supports scalars, numeric tensors, boolean vectors and - strings. - - The Method option must be either "Agglomerate" or "Optimize". If not specified, it defaults to "Optimize". - Note that the Agglomerate and Optimize methods usually produce different clusterings. - - The runtime of the Agglomerate method is quadratic in the number of clustered points n, builds the clustering - from the bottom up, and is exact (no element of randomness). The Optimize method's runtime is linear in n, - Optimize builds the clustering from top down, and uses random sampling. - """ - - summary_text = "divide data into lists of similar elements" - - def eval(self, p, evaluation, options): - "FindClusters[p_, OptionsPattern[%(name)s]]" - return self._cluster( - p, - None, - "clusters", - evaluation, - options, - Expression(SymbolFindClusters, p, *options_to_rules(options)), - ) - - def eval_manual_k(self, p, k: Integer, evaluation, options): - "FindClusters[p_, k_Integer, OptionsPattern[%(name)s]]" - return self._cluster( - p, - k, - "clusters", - evaluation, - options, - Expression(SymbolFindClusters, p, k, *options_to_rules(options)), - ) - - -class ClusteringComponents(_Cluster): - """ - :WMA link:https://reference.wolfram.com/language/ref/ClusteringComponents.html - -
    -
    'ClusteringComponents[$list$]' -
    forms clusters from $list$ and returns a list of cluster indices, in which each - element shows the index of the cluster in which the corresponding element in $list$ - ended up. -
    'ClusteringComponents[$list$, $k$]' -
    forms $k$ clusters from $list$ and returns a list of cluster indices, in which - each element shows the index of the cluster in which the corresponding element in - $list$ ended up. -
    - - For more detailed documentation regarding options and behavior, see FindClusters[]. - - >> ClusteringComponents[{1, 2, 3, 1, 2, 10, 100}] - = {1, 1, 1, 1, 1, 1, 2} - - >> ClusteringComponents[{10, 100, 20}, Method -> "KMeans"] - = {1, 0, 1} - """ - - summary_text = "label data with the index of the cluster it is in" - - def eval(self, p, evaluation, options): - "ClusteringComponents[p_, OptionsPattern[%(name)s]]" - return self._cluster( - p, - None, - "components", - evaluation, - options, - Expression(SymbolClusteringComponents, p, *options_to_rules(options)), - ) - - def eval_manual_k(self, p, k: Integer, evaluation, options): - "ClusteringComponents[p_, k_Integer, OptionsPattern[%(name)s]]" - return self._cluster( - p, - k, - "components", - evaluation, - options, - Expression(SymbolClusteringComponents, p, k, *options_to_rules(options)), - ) - - -class Nearest(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Nearest.html - -
    -
    'Nearest[$list$, $x$]' -
    returns the one item in $list$ that is nearest to $x$. - -
    'Nearest[$list$, $x$, $n$]' -
    returns the $n$ nearest items. - -
    'Nearest[$list$, $x$, {$n$, $r$}]' -
    returns up to $n$ nearest items that are not farther from $x$ than $r$. - -
    'Nearest[{$p1$ -> $q1$, $p2$ -> $q2$, ...}, $x$]' -
    returns $q1$, $q2$, ... but measures the distances using $p1$, $p2$, ... - -
    'Nearest[{$p1$, $p2$, ...} -> {$q1$, $q2$, ...}, $x$]' -
    returns $q1$, $q2$, ... but measures the distances using $p1$, $p2$, ... -
    - - >> Nearest[{5, 2.5, 10, 11, 15, 8.5, 14}, 12] - = {11} - - Return all items within a distance of 5: - - >> Nearest[{5, 2.5, 10, 11, 15, 8.5, 14}, 12, {All, 5}] - = {11, 10, 14} - - >> Nearest[{Blue -> "blue", White -> "white", Red -> "red", Green -> "green"}, {Orange, Gray}] - = {{red}, {white}} - - >> Nearest[{{0, 1}, {1, 2}, {2, 3}} -> {a, b, c}, {1.1, 2}] - = {b} - """ - - messages = { - "amtd": "`1` failed to pick a suitable distance function for `2`.", - "list": "Expected a list or a rule with equally sized lists at position 1 in ``.", - "nimp": "Method `1` is not implemented yet.", - } - - options = { - "DistanceFunction": "Automatic", - "Method": '"Scan"', - } - - rules = { - "Nearest[list_, pattern_]": "Nearest[list, pattern, 1]", - "Nearest[pattern_][list_]": "Nearest[list, pattern]", - } - summary_text = "the nearest element from a list" - - def eval(self, items, pivot, limit, expression, evaluation, options): - "Nearest[items_, pivot_, limit_, OptionsPattern[%(name)s]]" - - method = self.get_option(options, "Method", evaluation) - if not isinstance(method, String) or method.get_string_value() != "Scan": - evaluation("Nearest", "nimp", method) - return - - dist_p, repr_p = _dist_repr(items) - - if dist_p is None or len(dist_p) != len(repr_p): - evaluation.message(self.get_name(), "list", expression) - return - - if limit.has_form("List", 2): - up_to = limit.elements[0] - py_r = limit.elements[1].to_mpmath() - else: - up_to = limit - py_r = None - - if isinstance(up_to, Integer): - py_n = up_to.get_int_value() - elif up_to.get_name() == "System`All": - py_n = None - else: - return - - if not dist_p or (py_n is not None and py_n < 1): - return ListExpression() - - multiple_x = False - - distance_function_string, distance_function = self.get_option_string( - options, "DistanceFunction", evaluation - ) - if distance_function_string == "Automatic": - from mathics.builtin.tensors import get_default_distance - - distance_function = get_default_distance(dist_p) - if distance_function is None: - evaluation.message( - self.get_name(), "amtd", "Nearest", ListExpression(*dist_p) - ) - return - - if pivot.get_head_name() == "System`List": - _, depth_x = walk_levels(pivot) - _, depth_items = walk_levels(dist_p[0]) - - if depth_x > depth_items: - multiple_x = True - - def nearest(x) -> ListExpression: - calls = [Expression(distance_function, x, y) for y in dist_p] - distances = ListExpression(*calls).evaluate(evaluation) - - if not distances.has_form("List", len(dist_p)): - raise ValueError() - - py_distances = [ - (_to_real_distance(d), i) for i, d in enumerate(distances.elements) - ] - - if py_r is not None: - py_distances = [(d, i) for d, i in py_distances if d <= py_r] - - def pick(): - if py_n is None: - candidates = sorted(py_distances) - else: - candidates = heapq.nsmallest(py_n, py_distances) - - for d, i in candidates: - yield repr_p[i] - - return ListExpression(*list(pick())) - - try: - if not multiple_x: - return nearest(pivot) - else: - return ListExpression(*[nearest(t) for t in pivot.elements]) - except _IllegalDistance: - return SymbolFailed - except ValueError: - return SymbolFailed - - -class SubsetQ(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/SubsetQ.html - -
    -
    'SubsetQ[$list1$, $list2$]' -
    returns True if $list2$ is a subset of $list1$, and False otherwise. -
    - - >> SubsetQ[{1, 2, 3}, {3, 1}] - = True - - The empty list is a subset of every list: - >> SubsetQ[{}, {}] - = True - - >> SubsetQ[{1, 2, 3}, {}] - = True - - Every list is a subset of itself: - >> SubsetQ[{1, 2, 3}, {1, 2, 3}] - = True - - #> SubsetQ[{1, 2, 3}, {0, 1}] - = False - - #> SubsetQ[{1, 2, 3}, {1, 2, 3, 4}] - = False - - #> SubsetQ[{1, 2, 3}] - : SubsetQ called with 1 argument; 2 arguments are expected. - = SubsetQ[{1, 2, 3}] - - #> SubsetQ[{1, 2, 3}, {1, 2}, {3}] - : SubsetQ called with 3 arguments; 2 arguments are expected. - = SubsetQ[{1, 2, 3}, {1, 2}, {3}] - - #> SubsetQ[a + b + c, {1}] - : Heads Plus and List at positions 1 and 2 are expected to be the same. - = SubsetQ[a + b + c, {1}] - - #> SubsetQ[{1, 2, 3}, n] - : Nonatomic expression expected at position 2 in SubsetQ[{1, 2, 3}, n]. - = SubsetQ[{1, 2, 3}, n] - - #> SubsetQ[f[a, b, c], f[a]] - = True - """ - - messages = { - # FIXME: This message doesn't exist in more modern WMA, and - # Subset *can* take more than 2 arguments. - "argr": "SubsetQ called with 1 argument; 2 arguments are expected.", - "argrx": "SubsetQ called with `1` arguments; 2 arguments are expected.", - "heads": "Heads `1` and `2` at positions 1 and 2 are expected to be the same.", - "normal": "Nonatomic expression expected at position `1` in `2`.", - } - summary_text = "test if a list is a subset of another list" - - def eval(self, expr, subset, evaluation): - "SubsetQ[expr_, subset___]" - - if isinstance(expr, Atom): - return evaluation.message( - "SubsetQ", "normal", Integer1, Expression(SymbolSubsetQ, expr, subset) - ) - - subset = subset.get_sequence() - if len(subset) > 1: - return evaluation.message("SubsetQ", "argrx", Integer(len(subset) + 1)) - elif len(subset) == 0: - return evaluation.message("SubsetQ", "argr") - - subset = subset[0] - if isinstance(subset, Atom): - return evaluation.message( - "SubsetQ", "normal", Integer2, Expression(SymbolSubsetQ, expr, subset) - ) - if expr.get_head_name() != subset.get_head_name(): - return evaluation.message( - "SubsetQ", "heads", expr.get_head(), subset.get_head() - ) - - if set(subset.elements).issubset(set(expr.elements)): - return SymbolTrue - else: - return SymbolFalse - - -def delete_one(expr, pos): - if isinstance(expr, Atom): - raise PartDepthError(pos) - elements = expr.elements - if pos == 0: - return Expression(SymbolSequence, *elements) - s = len(elements) - truepos = pos - if truepos < 0: - truepos = s + truepos - else: - truepos = truepos - 1 - if truepos < 0 or truepos >= s: - raise PartRangeError - elements = ( - elements[:truepos] - + (to_expression("System`Sequence"),) - + elements[truepos + 1 :] - ) - return to_expression(expr.get_head(), *elements) - - -def delete_rec(expr, pos): - if len(pos) == 1: - return delete_one(expr, pos[0]) - truepos = pos[0] - if truepos == 0 or isinstance(expr, Atom): - raise PartDepthError(pos[0]) - elements = expr.elements - s = len(elements) - if truepos < 0: - truepos = truepos + s - if truepos < 0: - raise PartRangeError - newelement = delete_rec(elements[truepos], pos[1:]) - elements = elements[:truepos] + (newelement,) + elements[truepos + 1 :] - else: - if truepos > s: - raise PartRangeError - newelement = delete_rec(elements[truepos - 1], pos[1:]) - elements = elements[: truepos - 1] + (newelement,) + elements[truepos:] - return Expression(expr.get_head(), *elements) - - -# rules = {'Failure /: MakeBoxes[Failure[tag_, assoc_Association], StandardForm]' : -# 'With[{msg = assoc["MessageTemplate"], msgParam = assoc["MessageParameters"], type = assoc["Type"]}, ToBoxes @ Interpretation["Failure" @ Panel @ Grid[{{Style["\[WarningSign]", "Message", FontSize -> 35], Style["Message:", FontColor->GrayLevel[0.5]], ToString[StringForm[msg, Sequence @@ msgParam], StandardForm]}, {SpanFromAbove, Style["Tag:", FontColor->GrayLevel[0.5]], ToString[tag, StandardForm]},{SpanFromAbove,Style["Type:", FontColor->GrayLevel[0.5]],ToString[type, StandardForm]}},Alignment -> {Left, Top}], Failure[tag, assoc]] /; msg =!= Missing["KeyAbsent", "MessageTemplate"] && msgParam =!= Missing["KeyAbsent", "MessageParameters"] && msgParam =!= Missing["KeyAbsent", "Type"]]', -# } diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index ed906e3ef..2c25b84db 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- - - """ Low level Format definitions """ @@ -15,12 +13,18 @@ from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_READ_PROTECTED from mathics.core.convert.op import operator_to_ascii, operator_to_unicode from mathics.core.element import BaseElement, BoxElementMixin +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.number import dps from mathics.core.symbols import Atom, Symbol from mathics.core.systemsymbols import SymbolInputForm, SymbolOutputForm, SymbolRowBox -from mathics.eval.makeboxes import _boxed_string, format_element +from mathics.eval.makeboxes import ( + NEVER_ADD_PARENTHESIS, + _boxed_string, + format_element, + parenthesize, +) def int_to_s_exp(expr, n): @@ -35,30 +39,6 @@ def int_to_s_exp(expr, n): return s, exp, nonnegative -def parenthesize(precedence, element, element_boxes, when_equal): - from mathics.builtin import builtins_precedence - - while element.has_form("HoldForm", 1): - element = element.elements[0] - - if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): - element_prec = element.elements[2].value - elif element.has_form("PrecedenceForm", 2): - element_prec = element.elements[1].value - # For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332) - elif isinstance(element, (Integer, Real)) and element.value < 0: - element_prec = precedence - else: - element_prec = builtins_precedence.get(element.get_head_name()) - if precedence is not None and element_prec is not None: - if precedence > element_prec or (precedence == element_prec and when_equal): - return Expression( - SymbolRowBox, - ListExpression(String("("), element_boxes, String(")")), - ) - return element_boxes - - # FIXME: op should be a string, so remove the Union. def make_boxes_infix( elements, op: Union[String, list], precedence: int, grouping, form: Symbol @@ -141,7 +121,7 @@ def real_to_s_exp(expr, n): return s, exp, nonnegative -def number_form(expr, n, f, evaluation, options): +def number_form(expr, n, f, evaluation: Evaluation, options: dict): """ Converts a Real or Integer instance to Boxes. @@ -420,28 +400,28 @@ def eval_general(self, expr, f, evaluation): result.append(to_boxes(String(right), evaluation)) return RowBox(*result) - def eval_outerprecedenceform(self, expr, prec, evaluation): - """MakeBoxes[PrecedenceForm[expr_, prec_], - StandardForm|TraditionalForm|OutputForm|InputForm]""" + def eval_outerprecedenceform(self, expr, precedence, form, evaluation): + """MakeBoxes[PrecedenceForm[expr_, precedence_], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" - precedence = prec.get_int_value() - boxes = MakeBoxes(expr) - return parenthesize(precedence, expr, boxes, True) + py_precedence = precedence.get_int_value() + boxes = MakeBoxes(expr, form) + return parenthesize(py_precedence, expr, boxes, True) - def eval_postprefix(self, p, expr, h, prec, f, evaluation): - """MakeBoxes[(p:Prefix|Postfix)[expr_, h_, prec_:None], - f:StandardForm|TraditionalForm|OutputForm|InputForm]""" + def eval_postprefix(self, p, expr, h, precedence, form, evaluation): + """MakeBoxes[(p:Prefix|Postfix)[expr_, h_, precedence_:None], + form:StandardForm|TraditionalForm|OutputForm|InputForm]""" if not isinstance(h, String): - h = MakeBoxes(h, f) + h = MakeBoxes(h, form) - precedence = prec.get_int_value() + py_precedence = precedence.get_int_value() elements = expr.elements if len(elements) == 1: element = elements[0] - element_boxes = MakeBoxes(element, f) - element = parenthesize(precedence, element, element_boxes, True) + element_boxes = MakeBoxes(element, form) + element = parenthesize(py_precedence, element, element_boxes, True) if p.get_name() == "System`Postfix": args = (element, h) else: @@ -449,12 +429,12 @@ def eval_postprefix(self, p, expr, h, prec, f, evaluation): return Expression(SymbolRowBox, ListExpression(*args).evaluate(evaluation)) else: - return MakeBoxes(expr, f).evaluate(evaluation) + return MakeBoxes(expr, form).evaluate(evaluation) def eval_infix( - self, expr, operator, prec: Integer, grouping, form: Symbol, evaluation + self, expr, operator, precedence: Integer, grouping, form: Symbol, evaluation ): - """MakeBoxes[Infix[expr_, operator_, prec_:None, grouping_:None], + """MakeBoxes[Infix[expr_, operator_, precedence_:None, grouping_:None], form:StandardForm|TraditionalForm|OutputForm|InputForm]""" ## FIXME: this should go into a some formatter. @@ -485,7 +465,9 @@ def format_operator(operator) -> Union[String, BaseElement]: return op return operator - precedence = prec.value + py_precedence = ( + precedence.value if hasattr(precedence, "value") else NEVER_ADD_PARENTHESIS + ) grouping = grouping.get_name() if isinstance(expr, Atom): @@ -496,7 +478,9 @@ def format_operator(operator) -> Union[String, BaseElement]: if len(elements) > 1: if operator.has_form("List", len(elements) - 1): operator = [format_operator(op) for op in operator.elements] - return make_boxes_infix(elements, operator, precedence, grouping, form) + return make_boxes_infix( + elements, operator, py_precedence, grouping, form + ) else: encoding_rule = evaluation.definitions.get_ownvalue( "$CharacterEncoding" @@ -518,7 +502,7 @@ def format_operator(operator) -> Union[String, BaseElement]: String(operator_to_unicode.get(op_str, op_str)) ) - return make_boxes_infix(elements, operator, precedence, grouping, form) + return make_boxes_infix(elements, operator, py_precedence, grouping, form) elif len(elements) == 1: return MakeBoxes(elements[0], form) @@ -528,7 +512,9 @@ def format_operator(operator) -> Union[String, BaseElement]: class ToBoxes(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/ToBoxes.html + + :WMA link: + https://reference.wolfram.com/language/ref/ToBoxes.html
    'ToBoxes[$expr$]' diff --git a/mathics/builtin/manipulate.py b/mathics/builtin/manipulate.py index 7c3b8dfeb..fd10692eb 100644 --- a/mathics/builtin/manipulate.py +++ b/mathics/builtin/manipulate.py @@ -1,369 +1,387 @@ # -*- coding: utf-8 -*- +""" +Interactive Manipulation +""" +# This largely is not usable. +# no_doc = True -from mathics import settings -from mathics.builtin.base import Builtin -from mathics.core.atoms import Integer, String -from mathics.core.attributes import A_HOLD_ALL, A_PROTECTED -from mathics.core.convert.python import from_python -from mathics.core.evaluation import Output -from mathics.core.expression import Expression -from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol, strip_context -from mathics.core.systemsymbols import SymbolSet - -try: - from ipykernel.kernelbase import Kernel - - _jupyter = True -except ImportError: - _jupyter = False -try: - from IPython.core.formatters import IPythonDisplayFormatter - from ipywidgets import Box, DOMWidget, FloatSlider, IntSlider, ToggleButtons +# from mathics import settings +# from mathics.builtin.base import Builtin +# from mathics.core.atoms import Integer, String +# from mathics.core.attributes import A_HOLD_ALL, A_PROTECTED +# from mathics.core.convert.python import from_python +# from mathics.core.evaluation import Output +# from mathics.core.expression import Expression +# from mathics.core.list import ListExpression +# from mathics.core.symbols import Symbol, strip_context +# from mathics.core.systemsymbols import SymbolSet - _ipywidgets = True -except ImportError: - # fallback to non-Manipulate-enabled build if we don't have ipywidgets installed. - _ipywidgets = False +# try: +# from ipykernel.kernelbase import Kernel +# _jupyter = True +# except ImportError: +# _jupyter = False -SymbolModule = Symbol("Module") -SymbolReleaseHold = Symbol("ReleaseHold") +# try: +# from IPython.core.formatters import IPythonDisplayFormatter +# from ipywidgets import Box, DOMWidget, FloatSlider, IntSlider, ToggleButtons -""" -A basic implementation of Manipulate[]. There is currently no support for Dynamic[] elements. -This implementation is basically a port from ipywidget.widgets.interaction for Mathics. -""" +# _ipywidgets = True +# except ImportError: +# # fallback to non-Manipulate-enabled build if we don't have ipywidgets installed. +# _ipywidgets = False -def _interactive(interact_f, kwargs_widgets): - # this is a modified version of interactive() in ipywidget.widgets.interaction - - container = Box(_dom_classes=["widget-interact"]) - container.children = [w for w in kwargs_widgets if isinstance(w, DOMWidget)] - - def call_f(name=None, old=None, new=None): - kwargs = dict((widget._kwarg, widget.value) for widget in kwargs_widgets) - try: - interact_f(**kwargs) - except Exception as e: - container.log.warn("Exception in interact callback: %s", e, exc_info=True) - - for widget in kwargs_widgets: - widget.on_trait_change(call_f, "value") - - container.on_displayed(lambda _: call_f(None, None, None)) - - return container - - -class IllegalWidgetArguments(Exception): - def __init__(self, var): - super(IllegalWidgetArguments, self).__init__() - self.var = var - - -class JupyterWidgetError(Exception): - def __init__(self, err): - super(JupyterWidgetError, self).__init__() - self.err = err - - -class ManipulateParameter( - Builtin -): # parses one Manipulate[] parameter spec, e.g. {x, 1, 2}, see _WidgetInstantiator - context = "System`Private`" - - rules = { - # detect x and {x, default} and {x, default, label}. - "System`Private`ManipulateParameter[{s_Symbol, r__}]": "System`Private`ManipulateParameter[{Symbol -> s, Label -> s}, {r}]", - "System`Private`ManipulateParameter[{{s_Symbol, d_}, r__}]": "System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> s}, {r}]", - "System`Private`ManipulateParameter[{{s_Symbol, d_, l_}, r__}]": "System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> l}, {r}]", - # detect different kinds of widgets. on the use of the duplicate key "Default ->", see _WidgetInstantiator.add() - "System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ}]": 'Join[{Type -> "Continuous", Minimum -> min, Maximum -> max, Default -> min}, var]', - "System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ, step_?RealNumberQ}]": 'Join[{Type -> "Discrete", Minimum -> min, Maximum -> max, Step -> step, Default -> min}, var]', - "System`Private`ManipulateParameter[var_, {opt_List}] /; Length[opt] > 0": 'Join[{Type -> "Options", Options -> opt, Default -> Part[opt, 1]}, var]', - } - - summary_text = "interactive manipulation (not implemented yet)" - - -def _manipulate_label(x): # gets the label that is displayed for a symbol or name - if isinstance(x, String): - return x.get_string_value() - elif isinstance(x, Symbol): - return strip_context(x.get_name()) - else: - return str(x) - - -def _create_widget(widget, **kwargs): - try: - return widget(**kwargs) - except Exception as e: - raise JupyterWidgetError(str(e)) - - -class _WidgetInstantiator: - # we do not want to have widget instances (like FloatSlider) get into the evaluation pipeline (e.g. via Expression - # or Atom), since there might be all kinds of problems with serialization of these widget classes. therefore, the - # elegant recursive solution for parsing parameters (like in Table[]) is not feasible here; instead, we must create - # and use the widgets in one "transaction" here, without holding them in expressions or atoms. - - def __init__(self): - self._widgets = [] # the ipywidget widgets to control the manipulated variables - self._parsers = ( - {} - ) # lambdas to decode the widget values into Mathics expressions - - def add(self, expression, evaluation): - expr = Expression("System`Private`ManipulateParameter", expression).evaluate( - evaluation - ) - if ( - expr.get_head_name() != "System`List" - ): # if everything was parsed ok, we get a List - return False - # convert the rules given us by ManipulateParameter[] into a dict. note: duplicate keys - # will be overwritten, the latest one wins. - kwargs = {"evaluation": evaluation} - for rule in expr.elements: - if rule.get_head_name() != "System`Rule" or len(rule.elements) != 2: - return False - kwargs[strip_context(rule.elements[0].to_python()).lower()] = rule.elements[ - 1 - ] - widget = kwargs["type"].get_string_value() - del kwargs["type"] - getattr(self, "_add_%s_widget" % widget.lower())(**kwargs) # create the widget - return True - - def get_widgets(self): - return self._widgets - - def build_callback(self, callback): - parsers = self._parsers - - def new_callback(**kwargs): - callback( - **dict((name, parsers[name](value)) for (name, value) in kwargs.items()) - ) - - return new_callback - - def _add_continuous_widget( - self, symbol, label, default, minimum, maximum, evaluation - ): - minimum_value = minimum.to_python() - maximum_value = maximum.to_python() - if minimum_value > maximum_value: - raise IllegalWidgetArguments(symbol) - else: - defval = min(max(default.to_python(), minimum_value), maximum_value) - widget = _create_widget( - FloatSlider, value=defval, min=minimum_value, max=maximum_value - ) - self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label) - - def _add_discrete_widget( - self, symbol, label, default, minimum, maximum, step, evaluation - ): - minimum_value = minimum.to_python() - maximum_value = maximum.to_python() - step_value = step.to_python() - if ( - minimum_value > maximum_value - or step_value <= 0 - or step_value > (maximum_value - minimum_value) - ): - raise IllegalWidgetArguments(symbol) - else: - default_value = min(max(default.to_python(), minimum_value), maximum_value) - if all(isinstance(x, Integer) for x in [minimum, maximum, default, step]): - widget = _create_widget( - IntSlider, - value=default_value, - min=minimum_value, - max=maximum_value, - step=step_value, - ) - else: - widget = _create_widget( - FloatSlider, - value=default_value, - min=minimum_value, - max=maximum_value, - step=step_value, - ) - self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label) - - def _add_options_widget(self, symbol, options, default, label, evaluation): - formatted_options = [] - for i, option in enumerate(options.elements): - data = evaluation.format_output(option, format="text") - formatted_options.append((data, i)) - - default_index = 0 - for i, option in enumerate(options.elements): - if option.sameQ(default): - default_index = i - - widget = _create_widget( - ToggleButtons, options=formatted_options, value=default_index - ) - self._add_widget( - widget, symbol.get_name(), lambda j: options.elements[j], label - ) - - def _add_widget(self, widget, name, parse, label): - if not widget.description: - widget.description = _manipulate_label(label) - widget._kwarg = name # see _interactive() above - self._parsers[name] = parse - self._widgets.append(widget) - - -class ManipulateOutput(Output): - def max_stored_size(self, settings): - return self.output.max_stored_size(settings) - - def out(self, out): - return self.output.out(out) - - def clear_output(wait=False): - raise NotImplementedError - - def display_data(self, result): - raise NotImplementedError - - -class Manipulate(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Manipulate.html - -
    -
    'Manipulate[$expr1$, {$u$, $u_min$, $u_max$}]' -
    interactively compute and display an expression with different values of $u$. -
    'Manipulate[$expr1$, {$u$, $u_min$, $u_max$, $du$}]' -
    allows $u$ to vary between $u_min$ and $u_max$ in steps of $du$. -
    'Manipulate[$expr1$, {{$u$, $u_init$}, $u_min$, $u_max$, ...}]' -
    starts with initial value of $u_init$. -
    'Manipulate[$expr1$, {{$u$, $u_init$, $u_lbl$}, ...}]' -
    labels the $u$ controll by $u_lbl$. -
    'Manipulate[$expr1$, {$u$, {$u_1$, $u_2$, ...}}]' -
    sets $u$ to take discrete values $u_1$, $u_2$, ... . -
    'Manipulate[$expr1$, {$u$, ...}, {$v$, ...}, ...]' -
    control each of $u$, $v$, ... . -
    - - >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] - : Manipulate[] only works inside a Jupyter notebook. - = Manipulate[N[Sin[y]], {y, 1, 20, 2}] - - >> Manipulate[i ^ 3, {i, {2, x ^ 4, a}}] - : Manipulate[] only works inside a Jupyter notebook. - = Manipulate[i ^ 3, {i, {2, x ^ 4, a}}] - - >> Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}] - : Manipulate[] only works inside a Jupyter notebook. - = Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}] - - >> Manipulate[N[1 / x], {{x, 1}, 0, 2}] - : Manipulate[] only works inside a Jupyter notebook. - = Manipulate[N[1 / x], {{x, 1}, 0, 2}] - - >> Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}] - : Manipulate[] only works inside a Jupyter notebook. - = Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}] - """ - - # TODO: correct in the jupyter interface but can't be checked in tests - """ - #> Manipulate[x, {x}] - = Manipulate[x, {x}] - - #> Manipulate[x, {x, 1, 0}] - : 'Illegal variable range or step parameters for `x`. - = Manipulate[x, {x, 1, 0}] - """ - attributes = ( - A_HOLD_ALL | A_PROTECTED - ) # we'll call ReleaseHold at the time of evaluation below - - messages = { - "jupyter": "Manipulate[] only works inside a Jupyter notebook.", - "imathics": "Your IMathics kernel does not seem to support all necessary operations. " - + "Please check that you have the latest version installed.", - "widgetmake": 'Jupyter widget construction failed with "``".', - "widgetargs": "Illegal variable range or step parameters for ``.", - "widgetdisp": "Jupyter failed to display the widget.", - } - - requires = ("ipywidgets",) - summary_text = "interactively manipulate any expression, graphic, or other object" - - def eval(self, expr, args, evaluation): - "Manipulate[expr_, args__]" - if (not _jupyter) or (not Kernel.initialized()) or (Kernel.instance() is None): - return evaluation.message("Manipulate", "jupyter") - - instantiator = ( - _WidgetInstantiator() - ) # knows about the arguments and their widgets - - for arg in args.get_sequence(): - try: - if not instantiator.add( - arg, evaluation - ): # not a valid argument pattern? - return - except IllegalWidgetArguments as e: - return evaluation.message( - "Manipulate", "widgetargs", strip_context(str(e.var)) - ) - except JupyterWidgetError as e: - return evaluation.message("Manipulate", "widgetmake", e.err) - - clear_output_callback = evaluation.output.clear - display_data_callback = evaluation.output.display # for pushing updates - - try: - clear_output_callback(wait=True) - except NotImplementedError: - return evaluation.message("Manipulate", "imathics") - - def callback(**kwargs): - clear_output_callback(wait=True) - - line_no = evaluation.definitions.get_line_no() - - vars = [ - Expression(SymbolSet, Symbol(name), value) - for name, value in kwargs.items() - ] - evaluatable = Expression( - SymbolReleaseHold, Expression(SymbolModule, ListExpression(*vars), expr) - ) - - result = evaluation.evaluate(evaluatable, timeout=settings.TIMEOUT) - if result: - display_data_callback(data=result.result, metadata={}) - - evaluation.definitions.set_line_no( - line_no - ) # do not increment line_no for manipulate computations - - widgets = instantiator.get_widgets() - if len(widgets) > 0: - box = _interactive( - instantiator.build_callback(callback), widgets - ) # create the widget - formatter = IPythonDisplayFormatter() - if not formatter(box): # make the widget appear on the Jupyter notebook - return evaluation.message("Manipulate", "widgetdisp") - - return Symbol( - "Null" - ) # the interactive output is pushed via kernel.display_data_callback (see above) +# SymbolModule = Symbol("Module") +# SymbolReleaseHold = Symbol("ReleaseHold") + +# """ +# A basic implementation of Manipulate[]. There is currently no support for Dynamic[] elements. +# This implementation is basically a port from ipywidget.widgets.interaction for Mathics. +# """ + + +# def _interactive(interact_f, kwargs_widgets): +# # this is a modified version of interactive() in ipywidget.widgets.interaction + +# container = Box(_dom_classes=["widget-interact"]) +# container.children = [w for w in kwargs_widgets if isinstance(w, DOMWidget)] + +# def call_f(name=None, old=None, new=None): +# kwargs = dict((widget._kwarg, widget.value) for widget in kwargs_widgets) +# try: +# interact_f(**kwargs) +# except Exception as e: +# container.log.warn("Exception in interact callback: %s", e, exc_info=True) + +# for widget in kwargs_widgets: +# widget.on_trait_change(call_f, "value") + +# container.on_displayed(lambda _: call_f(None, None, None)) + +# return container + + +# class IllegalWidgetArguments(Exception): +# def __init__(self, var): +# super(IllegalWidgetArguments, self).__init__() +# self.var = var + + +# class JupyterWidgetError(Exception): +# def __init__(self, err): +# super(JupyterWidgetError, self).__init__() +# self.err = err + + +# class ManipulateParameter( +# Builtin +# ): # parses one Manipulate[] parameter spec, e.g. {x, 1, 2}, see _WidgetInstantiator +# context = "System`Private`" + +# rules = { +# # detect x and {x, default} and {x, default, label}. +# "System`Private`ManipulateParameter[{s_Symbol, r__}]": "System`Private`ManipulateParameter[{Symbol -> s, Label -> s}, {r}]", +# "System`Private`ManipulateParameter[{{s_Symbol, d_}, r__}]": "System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> s}, {r}]", +# "System`Private`ManipulateParameter[{{s_Symbol, d_, l_}, r__}]": "System`Private`ManipulateParameter[{Symbol -> s, Default -> d, Label -> l}, {r}]", +# # detect different kinds of widgets. on the use of the duplicate key "Default ->", see _WidgetInstantiator.add() +# "System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ}]": 'Join[{Type -> "Continuous", Minimum -> min, Maximum -> max, Default -> min}, var]', +# "System`Private`ManipulateParameter[var_, {min_?RealNumberQ, max_?RealNumberQ, step_?RealNumberQ}]": 'Join[{Type -> "Discrete", Minimum -> min, Maximum -> max, Step -> step, Default -> min}, var]', +# "System`Private`ManipulateParameter[var_, {opt_List}] /; Length[opt] > 0": 'Join[{Type -> "Options", Options -> opt, Default -> Part[opt, 1]}, var]', +# } + +# summary_text = "interactive manipulation (not implemented yet)" + + +# def _manipulate_label(x): # gets the label that is displayed for a symbol or name +# if isinstance(x, String): +# return x.get_string_value() +# elif isinstance(x, Symbol): +# return strip_context(x.get_name()) +# else: +# return str(x) + + +# def _create_widget(widget, **kwargs): +# try: +# return widget(**kwargs) +# except Exception as e: +# raise JupyterWidgetError(str(e)) + + +# class _WidgetInstantiator: +# # we do not want to have widget instances (like FloatSlider) get into the evaluation pipeline (e.g. via Expression +# # or Atom), since there might be all kinds of problems with serialization of these widget classes. therefore, the +# # elegant recursive solution for parsing parameters (like in Table[]) is not feasible here; instead, we must create +# # and use the widgets in one "transaction" here, without holding them in expressions or atoms. + +# def __init__(self): +# self._widgets = [] # the ipywidget widgets to control the manipulated variables +# self._parsers = ( +# {} +# ) # lambdas to decode the widget values into Mathics expressions + +# def add(self, expression, evaluation): +# expr = Expression("System`Private`ManipulateParameter", expression).evaluate( +# evaluation +# ) +# if ( +# expr.get_head_name() != "System`List" +# ): # if everything was parsed ok, we get a List +# return False +# # convert the rules given us by ManipulateParameter[] into a dict. note: duplicate keys +# # will be overwritten, the latest one wins. +# kwargs = {"evaluation": evaluation} +# for rule in expr.elements: +# if rule.get_head_name() != "System`Rule" or len(rule.elements) != 2: +# return False +# kwargs[strip_context(rule.elements[0].to_python()).lower()] = rule.elements[ +# 1 +# ] +# widget = kwargs["type"].get_string_value() +# del kwargs["type"] +# getattr(self, "_add_%s_widget" % widget.lower())(**kwargs) # create the widget +# return True + +# def get_widgets(self): +# return self._widgets + +# def build_callback(self, callback): +# parsers = self._parsers + +# def new_callback(**kwargs): +# callback( +# **dict((name, parsers[name](value)) for (name, value) in kwargs.items()) +# ) + +# return new_callback + +# def _add_continuous_widget( +# self, symbol, label, default, minimum, maximum, evaluation +# ): +# minimum_value = minimum.to_python() +# maximum_value = maximum.to_python() +# if minimum_value > maximum_value: +# raise IllegalWidgetArguments(symbol) +# else: +# defval = min(max(default.to_python(), minimum_value), maximum_value) +# widget = _create_widget( +# FloatSlider, value=defval, min=minimum_value, max=maximum_value +# ) +# self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label) + +# def _add_discrete_widget( +# self, symbol, label, default, minimum, maximum, step, evaluation +# ): +# minimum_value = minimum.to_python() +# maximum_value = maximum.to_python() +# step_value = step.to_python() +# if ( +# minimum_value > maximum_value +# or step_value <= 0 +# or step_value > (maximum_value - minimum_value) +# ): +# raise IllegalWidgetArguments(symbol) +# else: +# default_value = min(max(default.to_python(), minimum_value), maximum_value) +# if all(isinstance(x, Integer) for x in [minimum, maximum, default, step]): +# widget = _create_widget( +# IntSlider, +# value=default_value, +# min=minimum_value, +# max=maximum_value, +# step=step_value, +# ) +# else: +# widget = _create_widget( +# FloatSlider, +# value=default_value, +# min=minimum_value, +# max=maximum_value, +# step=step_value, +# ) +# self._add_widget(widget, symbol.get_name(), lambda x: from_python(x), label) + +# def _add_options_widget(self, symbol, options, default, label, evaluation): +# formatted_options = [] +# for i, option in enumerate(options.elements): +# data = evaluation.format_output(option, format="text") +# formatted_options.append((data, i)) + +# default_index = 0 +# for i, option in enumerate(options.elements): +# if option.sameQ(default): +# default_index = i + +# widget = _create_widget( +# ToggleButtons, options=formatted_options, value=default_index +# ) +# self._add_widget( +# widget, symbol.get_name(), lambda j: options.elements[j], label +# ) + +# def _add_widget(self, widget, name, parse, label): +# if not widget.description: +# widget.description = _manipulate_label(label) +# widget._kwarg = name # see _interactive() above +# self._parsers[name] = parse +# self._widgets.append(widget) + + +# class ManipulateOutput(Output): +# def max_stored_size(self, settings): +# return self.output.max_stored_size(settings) + +# def out(self, out): +# return self.output.out(out) + +# def clear_output(wait=False): +# raise NotImplementedError + +# def display_data(self, result): +# raise NotImplementedError + + +# class Manipulate(Builtin): +# """ +# +# :WMA link: +# https://reference.wolfram.com/language/ref/Manipulate.html + +#
    +#
    'Manipulate[$expr1$, {$u$, $u_min$, $u_max$}]' +#
    interactively compute and display an expression with different values of $u$. + +#
    'Manipulate[$expr1$, {$u$, $u_min$, $u_max$, $du$}]' +#
    allows $u$ to vary between $u_min$ and $u_max$ in steps of $du$. + +#
    'Manipulate[$expr1$, {{$u$, $u_init$}, $u_min$, $u_max$, ...}]' +#
    starts with initial value of $u_init$. + +#
    'Manipulate[$expr1$, {{$u$, $u_init$, $u_lbl$}, ...}]' +#
    labels the $u$ controll by $u_lbl$. + +#
    'Manipulate[$expr1$, {$u$, {$u_1$, $u_2$, ...}}]' +#
    sets $u$ to take discrete values $u_1$, $u_2$, ... . + +#
    'Manipulate[$expr1$, {$u$, ...}, {$v$, ...}, ...]' +#
    control each of $u$, $v$, ... . +#
    + +# >> Manipulate[N[Sin[y]], {y, 1, 20, 2}] +# : Manipulate[] only works inside a Jupyter notebook. +# = Manipulate[N[Sin[y]], {y, 1, 20, 2}] + +# >> Manipulate[i ^ 3, {i, {2, x ^ 4, a}}] +# : Manipulate[] only works inside a Jupyter notebook. +# = Manipulate[i ^ 3, {i, {2, x ^ 4, a}}] + +# >> Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}] +# : Manipulate[] only works inside a Jupyter notebook. +# = Manipulate[x ^ y, {x, 1, 20}, {y, 1, 3}] + +# >> Manipulate[N[1 / x], {{x, 1}, 0, 2}] +# : Manipulate[] only works inside a Jupyter notebook. +# = Manipulate[N[1 / x], {{x, 1}, 0, 2}] + +# >> Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}] +# : Manipulate[] only works inside a Jupyter notebook. +# = Manipulate[N[1 / x], {{x, 1}, 0, 2, 0.1}] +# """ + +# # TODO: correct in the jupyter interface but can't be checked in tests +# """ +# #> Manipulate[x, {x}] +# = Manipulate[x, {x}] + +# #> Manipulate[x, {x, 1, 0}] +# : 'Illegal variable range or step parameters for `x`. +# = Manipulate[x, {x, 1, 0}] +# """ +# attributes = ( +# A_HOLD_ALL | A_PROTECTED +# ) # we'll call ReleaseHold at the time of evaluation below + +# messages = { +# "jupyter": "Manipulate[] only works inside a Jupyter notebook.", +# "imathics": "Your IMathics kernel does not seem to support all necessary operations. " +# + "Please check that you have the latest version installed.", +# "widgetmake": 'Jupyter widget construction failed with "``".', +# "widgetargs": "Illegal variable range or step parameters for ``.", +# "widgetdisp": "Jupyter failed to display the widget.", +# } + +# no_doc = True # This largely doesn't work + +# requires = ("ipywidgets",) +# summary_text = "interactively manipulate any expression, graphic, or other object" + +# def eval(self, expr, args, evaluation): +# "Manipulate[expr_, args__]" +# if (not _jupyter) or (not Kernel.initialized()) or (Kernel.instance() is None): +# evaluation.message("Manipulate", "jupyter") +# return + +# instantiator = ( +# _WidgetInstantiator() +# ) # knows about the arguments and their widgets + +# for arg in args.get_sequence(): +# try: +# if not instantiator.add( +# arg, evaluation +# ): # not a valid argument pattern? +# return +# except IllegalWidgetArguments as e: +# evaluation.message( +# "Manipulate", "widgetargs", strip_context(str(e.var)) +# ) +# return +# except JupyterWidgetError as e: +# evaluation.message("Manipulate", "widgetmake", e.err) +# return +# +# clear_output_callback = evaluation.output.clear +# display_data_callback = evaluation.output.display # for pushing updates + +# try: +# clear_output_callback(wait=True) +# except NotImplementedError: +# evaluation.message("Manipulate", "imathics") +# return +# def callback(**kwargs): +# clear_output_callback(wait=True) + +# line_no = evaluation.definitions.get_line_no() + +# vars = [ +# Expression(SymbolSet, Symbol(name), value) +# for name, value in kwargs.items() +# ] +# evaluatable = Expression( +# SymbolReleaseHold, Expression(SymbolModule, ListExpression(*vars), expr) +# ) + +# result = evaluation.evaluate(evaluatable, timeout=settings.TIMEOUT) +# if result: +# display_data_callback(data=result.result, metadata={}) + +# evaluation.definitions.set_line_no( +# line_no +# ) # do not increment line_no for manipulate computations + +# widgets = instantiator.get_widgets() +# if len(widgets) > 0: +# box = _interactive( +# instantiator.build_callback(callback), widgets +# ) # create the widget +# formatter = IPythonDisplayFormatter() +# if not formatter(box): # make the widget appear on the Jupyter notebook +# evaluation.message("Manipulate", "widgetdisp") +# return +# return Symbol( +# "Null" +# ) # the interactive output is pushed via kernel.display_data_callback (see above) diff --git a/mathics/builtin/matrices/constrmatrix.py b/mathics/builtin/matrices/constrmatrix.py index 8e41961e5..9f5b24328 100644 --- a/mathics/builtin/matrices/constrmatrix.py +++ b/mathics/builtin/matrices/constrmatrix.py @@ -7,7 +7,7 @@ import math from mathics.builtin.base import Builtin -from mathics.core.atoms import Integer0, Integer1 +from mathics.core.atoms import Integer0, Integer1, is_integer_rational_or_real from mathics.core.evaluation import Evaluation from mathics.core.list import ListExpression @@ -32,10 +32,18 @@ class BoxMatrix(Builtin): = {{1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}} """ + messages = { + "notre": "The first argument must be a non-complex number or a list of " + "noncomplex numbers.", + } + summary_text = "create a matrix with all its entries set to 1" def eval(self, r, evaluation: Evaluation): - "BoxMatrix[r_?RealNumberQ]" + "BoxMatrix[r_]" + if not is_integer_rational_or_real(r): + evaluation.message(self.get_name(), "notre") + return py_r = abs(r.round_to_float()) s = int(math.floor(1 + 2 * py_r)) return _matrix([[Integer1] * s] * s) diff --git a/mathics/builtin/matrices/partmatrix.py b/mathics/builtin/matrices/partmatrix.py index a4009520d..e9ace7d84 100644 --- a/mathics/builtin/matrices/partmatrix.py +++ b/mathics/builtin/matrices/partmatrix.py @@ -8,6 +8,7 @@ from mathics.builtin.base import Builtin +from mathics.core.evaluation import Evaluation from mathics.core.list import ListExpression @@ -47,7 +48,7 @@ class Diagonal(Builtin): summary_text = "gives a list with the diagonal elements of a given matrix" - def apply(self, expr, diag, evaluation): + def eval(self, expr, diag, evaluation: Evaluation): "Diagonal[expr_List, diag_Integer]" result = [] @@ -61,40 +62,4 @@ def apply(self, expr, diag, evaluation): return ListExpression(*result) -class MatrixQ(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/MatrixQ.html - -
    -
    'MatrixQ[$m$]' -
    gives 'True' if $m$ is a list of equal-length lists. - -
    'MatrixQ[$m$, $f$]' -
    gives 'True' only if '$f$[$x$]' returns 'True' for when applied to \ - element $x$ of the matrix $m$. -
    - - >> MatrixQ[{{1, 3}, {4.0, 3/2}}, NumberQ] - = True - - These are not matrices: - >> MatrixQ[{{1}, {1, 2}}] (* first row should have length two *) - = False - - >> MatrixQ[Array[a, {1, 1, 2}]] - = False - - Supply a test function parameter to generalize and specialize: - >> MatrixQ[{{1, 2}, {3, 4 + 5}}, Positive] - = True - - >> MatrixQ[{{1, 2 I}, {3, 4 + 5}}, Positive] - = False - """ - - rules = { - "MatrixQ[expr_]": "ArrayQ[expr, 2]", - "MatrixQ[expr_, test_]": "ArrayQ[expr, 2, test]", - } - - summary_text = "gives 'True' if the given argument is a list of equal-length lists" +# TODO: add ArrayRules, Indexed, LowerTriangularize, UpperTriangularize diff --git a/mathics/builtin/messages.py b/mathics/builtin/messages.py index f91a5b495..8c9c1b4b1 100644 --- a/mathics/builtin/messages.py +++ b/mathics/builtin/messages.py @@ -1,72 +1,45 @@ """ -Message related functions. - +Message-related functions. """ import typing from typing import Any -from mathics.builtin.base import BinaryOperator, Builtin +from mathics.builtin.base import BinaryOperator, Builtin, Predefined from mathics.core.atoms import String from mathics.core.attributes import A_HOLD_ALL, A_HOLD_FIRST, A_PROTECTED -from mathics.core.evaluation import Message as EvaluationMessage +from mathics.core.evaluation import Evaluation, Message as EvaluationMessage from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolNull from mathics.core.systemsymbols import SymbolMessageName, SymbolQuiet -class Message(Builtin): +class Aborted(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/Message.html + :WMA link:https://reference.wolfram.com/language/ref/Aborted.html
    -
    'Message[$symbol$::$msg$, $expr1$, $expr2$, ...]' -
    displays the specified message, replacing placeholders in - the message text with the corresponding expressions. +
    '$Aborted' +
    is returned by a calculation that has been aborted.
    - - >> a::b = "Hello world!" - = Hello world! - >> Message[a::b] - : Hello world! - >> a::c := "Hello `1`, Mr 00`2`!" - >> Message[a::c, "you", 3 + 4] - : Hello you, Mr 007! """ - attributes = A_HOLD_FIRST | A_PROTECTED - - messages = { - "name": "Message name `1` is not of the form symbol::name or symbol::name::language." - } - summary_text = "display a message" - - def apply(self, symbol, tag, params, evaluation): - "Message[MessageName[symbol_Symbol, tag_String], params___]" - - params = params.get_sequence() - evaluation.message(symbol.name, tag.value, *params) - return SymbolNull - - -def check_message(expr) -> bool: - "checks if an expression is a valid message" - if expr.has_form("MessageName", 2): - symbol, tag = expr.elements - if symbol.get_name() and tag.get_string_value(): - return True - return False + summary_text = "return value for aborted evaluations" + name = "$Aborted" class Check(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Check.html + + :WMA link: + https://reference.wolfram.com/language/ref/Check.html
    'Check[$expr$, $failexpr$]' -
    evaluates $expr$, and returns the result, unless messages were generated, in which case it evaluates and $failexpr$ will be returned. +
    evaluates $expr$, and returns the result, unless messages were \ + generated, in which case it evaluates and $failexpr$ will be returned.
    'Check[$expr$, $failexpr$, {s1::t1,s2::t2,...}]'
    checks only for the specified messages.
    @@ -139,13 +112,14 @@ class Check(Builtin): "argmu": "Check called with 1 argument; 2 or more arguments are expected.", "name": "Message name `1` is not of the form symbol::name or symbol::name::language.", } + summary_text = "discard the result if the evaluation produced messages" - def apply_1_argument(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation): "Check[expr_]" - return evaluation.message("Check", "argmu") + evaluation.message("Check", "argmu") - def apply(self, expr, failexpr, params, evaluation): + def eval_with_fail(self, expr, failexpr, params, evaluation: Evaluation): "Check[expr_, failexpr_, params___]" # Todo: To implement the third form of this function , we need to implement the function $MessageGroups first @@ -188,10 +162,330 @@ def get_msg_list(exprs): pattern = Expression( SymbolMessageName, Symbol(out_msg.symbol), String(out_msg.tag) ) - if pattern in check_messages: - display_fail_expr = True - break - return failexpr if display_fail_expr is True else result + if pattern in check_messages: + display_fail_expr = True + break + return failexpr if display_fail_expr is True else result + + +class Failed(Predefined): + """ + :WMA link:https://reference.wolfram.com/language/ref/$Failed.html +
    +
    '$Failed' +
    is returned by some functions in the event of an error. +
    + + #> Get["nonexistent_file.m"] + : Cannot open nonexistent_file.m. + = $Failed + """ + + summary_text = "retrieved result for failed evaluations" + name = "$Failed" + + +class Failure(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Failure.html + +
    +
    Failure[$tag$, $assoc$] +
    represents a failure of a type indicated by $tag$, with details \ + given by the association $assoc$. +
    + """ + + summary_text = "a failure at the level of the interpreter" + + +class General(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/General.html + +
    +
    'General' +
    is a symbol to which all general-purpose messages are assigned. +
    + + >> General::argr + = `1` called with 1 argument; `2` arguments are expected. + >> Message[Rule::argr, Rule, 2] + : Rule called with 1 argument; 2 arguments are expected. + """ + + messages = { + "argb": ( + "`1` called with `2` arguments; " + "between `3` and `4` arguments are expected." + ), + "argct": "`1` called with `2` arguments.", + "argctu": "`1` called with 1 argument.", + "argr": "`1` called with 1 argument; `2` arguments are expected.", + "argrx": "`1` called with `2` arguments; `3` arguments are expected.", + "argx": "`1` called with `2` arguments; 1 argument is expected.", + "argt": ( + "`1` called with `2` arguments; " "`3` or `4` arguments are expected." + ), + "argtu": ("`1` called with 1 argument; `2` or `3` arguments are expected."), + "base": "Requested base `1` in `2` should be between 2 and `3`.", + "boxfmt": "`1` is not a box formatting type.", + "charcode": "The character encoding `1` is not supported. Use $CharacterEncodings to list supported encodings.", + "color": "`1` is not a valid color or gray-level specification.", + "cxt": "`1` is not a valid context name.", + "divz": "The argument `1` should be nonzero.", + "digit": "Digit at position `1` in `2` is too large to be used in base `3`.", + "exact": "Argument `1` is not an exact number.", + "fnsym": ( + "First argument in `1` is not a symbol " "or a string naming a symbol." + ), + "heads": "Heads `1` and `2` are expected to be the same.", + "ilsnn": ( + "Single or list of non-negative integers expected at " "position `1`." + ), + "indet": "Indeterminate expression `1` encountered.", + "innf": "Non-negative integer or Infinity expected at position `1`.", + "int": "Integer expected.", + "intp": "Positive integer expected.", + "intnn": "Non-negative integer expected.", + "iterb": "Iterator does not have appropriate bounds.", + "ivar": "`1` is not a valid variable.", + "level": ("Level specification `1` is not of the form n, " "{n}, or {m, n}."), + "locked": "Symbol `1` is locked.", + "matsq": "Argument `1` is not a non-empty square matrix.", + "newpkg": "In WL, there is a new package for this.", + "noopen": "Cannot open `1`.", + "nord": "Invalid comparison with `1` attempted.", + "normal": "Nonatomic expression expected.", + "noval": ("Symbol `1` in part assignment does not have an immediate value."), + "obspkg": "In WL, this package is obsolete.", + "openx": "`1` is not open.", + "optb": "Optional object `1` in `2` is not a single blank.", + "ovfl": "Overflow occurred in computation.", + "partd": "Part specification is longer than depth of object.", + "partw": "Part `1` of `2` does not exist.", + "plld": "Endpoints in `1` must be distinct machine-size real numbers.", + "plln": "Limiting value `1` in `2` is not a machine-size real number.", + "pspec": ( + "Part specification `1` is neither an integer nor " "a list of integer." + ), + "seqs": "Sequence specification expected, but got `1`.", + "setp": "Part assignment to `1` could not be made", + "setps": "`1` in the part assignment is not a symbol.", + "span": "`1` is not a valid Span specification.", + "ssym": "`1` is not a symbol or a string.", + "stream": "`1` is not string, InputStream[], or OutputStream[]", + "string": "String expected.", + "sym": "Argument `1` at position `2` is expected to be a symbol.", + "tag": "Rule for `1` can only be attached to `2`.", + "take": "Cannot take positions `1` through `2` in `3`.", + "ucdec": "An invalid unicode sequence was encountered and ignored.", + "vrule": ( + "Cannot set `1` to `2`, " "which is not a valid list of replacement rules." + ), + "write": "Tag `1` in `2` is Protected.", + "wrsym": "Symbol `1` is Protected.", + # TODO: someone please explain why these are different... + # Self-defined messages + "rep": "`1` is not a valid replacement rule.", + "options": "`1` is not a valid list of option rules.", + "timeout": "Timeout reached.", + "syntax": "`1`", + "invalidargs": "Invalid arguments.", + "notboxes": "`1` is not a valid box structure.", + "pyimport": '`1`[] is not available. Python module "`2`" is not installed.', + } + summary_text = "general-purpose messages" + + +class Message(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/Message.html + +
    +
    'Message[$symbol$::$msg$, $expr1$, $expr2$, ...]' +
    displays the specified message, replacing placeholders in + the message text with the corresponding expressions. +
    + + >> a::b = "Hello world!" + = Hello world! + >> Message[a::b] + : Hello world! + >> a::c := "Hello `1`, Mr 00`2`!" + >> Message[a::c, "you", 3 + 4] + : Hello you, Mr 007! + """ + + attributes = A_HOLD_FIRST | A_PROTECTED + + messages = { + "name": "Message name `1` is not of the form symbol::name or symbol::name::language." + } + summary_text = "display a message" + + def eval(self, symbol: Symbol, tag: String, params, evaluation: Evaluation): + "Message[MessageName[symbol_Symbol, tag_String], params___]" + + params = params.get_sequence() + evaluation.message(symbol.name, tag.value, *params) + return SymbolNull + + +def check_message(expr) -> bool: + "checks if an expression is a valid message" + if expr.has_form("MessageName", 2): + symbol, tag = expr.elements + if symbol.get_name() and tag.get_string_value(): + return True + return False + + +class MessageName(BinaryOperator): + """ + :WMA link:https://reference.wolfram.com/language/ref/MessageName.html + +
    +
    'MessageName[$symbol$, $tag$]' +
    '$symbol$::$tag$' +
    identifies a message. +
    + + 'MessageName' is the head of message IDs of the form 'symbol::tag'. + >> FullForm[a::b] + = MessageName[a, "b"] + + The second parameter 'tag' is interpreted as a string. + >> FullForm[a::"b"] + = MessageName[a, "b"] + """ + + attributes = A_HOLD_FIRST | A_PROTECTED + default_formats = False + formats: typing.Dict[str, Any] = {} + messages = {"messg": "Message cannot be set to `1`. It must be set to a string."} + summary_text = "message identifyier" + operator = "::" + precedence = 750 + rules = { + "MakeBoxes[MessageName[symbol_Symbol, tag_String], " + "f:StandardForm|TraditionalForm|OutputForm]": ( + 'RowBox[{MakeBoxes[symbol, f], "::", MakeBoxes[tag, f]}]' + ), + "MakeBoxes[MessageName[symbol_Symbol, tag_String], InputForm]": ( + 'RowBox[{MakeBoxes[symbol, InputForm], "::", tag}]' + ), + } + + def eval(self, symbol: Symbol, tag: String, evaluation: Evaluation): + "MessageName[symbol_Symbol, tag_String]" + + pattern = Expression(SymbolMessageName, symbol, tag) + return evaluation.definitions.get_value( + symbol.get_name(), "System`Messages", pattern, evaluation + ) + + +class Off(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Off.html + +
    +
    'Off[$symbol$::$tag$]' +
    turns a message off so it is no longer printed. +
    + + >> Off[Power::infy] + >> 1 / 0 + = ComplexInfinity + + >> Off[Power::indet, Syntax::com] + >> {0 ^ 0,} + = {Indeterminate, Null} + + #> Off[1] + : Message name 1 is not of the form symbol::name or symbol::name::language. + #> Off[Message::name, 1] + + #> On[Power::infy, Power::indet, Syntax::com] + """ + + attributes = A_HOLD_ALL | A_PROTECTED + summary_text = "turn off a message for printing" + + def eval(self, expr, evaluation: Evaluation): + "Off[expr___]" + + seq = expr.get_sequence() + quiet_messages = set(evaluation.get_quiet_messages()) + + if not seq: + # TODO Off[s::trace] for all symbols + return + + for e in seq: + if isinstance(e, Symbol): + quiet_messages.add(Expression(SymbolMessageName, e, String("trace"))) + elif check_message(e): + quiet_messages.add(e) + else: + evaluation.message("Message", "name", e) + evaluation.set_quiet_messages(quiet_messages) + + return SymbolNull + + +class On(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/On.html + +
    +
    'On[$symbol$::$tag$]' +
    turns a message on for printing. +
    + + >> Off[Power::infy] + >> 1 / 0 + = ComplexInfinity + >> On[Power::infy] + >> 1 / 0 + : Infinite expression 1 / 0 encountered. + = ComplexInfinity + """ + + # TODO + """ + #> On[f::x] + : Message f::x not found. + """ + attributes = A_HOLD_ALL | A_PROTECTED + summary_text = "turn on a message for printing" + + def eval(self, expr, evaluation: Evaluation): + "On[expr___]" + + seq = expr.get_sequence() + quiet_messages = set(evaluation.get_quiet_messages()) + + if not seq: + # TODO On[s::trace] for all symbols + return + + for e in seq: + if isinstance(e, Symbol): + quiet_messages.discard( + Expression(SymbolMessageName, e, String("trace")) + ) + elif check_message(e): + quiet_messages.discard(e) + else: + evaluation.message("Message", "name", e) + evaluation.set_quiet_messages(quiet_messages) + return SymbolNull class Quiet(Builtin): @@ -257,7 +551,7 @@ class Quiet(Builtin): } summary_text = "evaluate without showing messages" - def apply(self, expr, moff, mon, evaluation): + def eval(self, expr, moff, mon, evaluation: Evaluation): "Quiet[expr_, moff_, mon_]" def get_msg_list(expr): @@ -324,149 +618,6 @@ def get_msg_list(expr): evaluation.set_quiet_messages(old_quiet_messages) -class Off(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Off.html - -
    -
    'Off[$symbol$::$tag$]' -
    turns a message off so it is no longer printed. -
    - - >> Off[Power::infy] - >> 1 / 0 - = ComplexInfinity - - >> Off[Power::indet, Syntax::com] - >> {0 ^ 0,} - = {Indeterminate, Null} - - #> Off[1] - : Message name 1 is not of the form symbol::name or symbol::name::language. - #> Off[Message::name, 1] - - #> On[Power::infy, Power::indet, Syntax::com] - """ - - attributes = A_HOLD_ALL | A_PROTECTED - summary_text = "turn off a message for printing" - - def apply(self, expr, evaluation): - "Off[expr___]" - - seq = expr.get_sequence() - quiet_messages = set(evaluation.get_quiet_messages()) - - if not seq: - # TODO Off[s::trace] for all symbols - return - - for e in seq: - if isinstance(e, Symbol): - quiet_messages.add(Expression(SymbolMessageName, e, String("trace"))) - elif check_message(e): - quiet_messages.add(e) - else: - evaluation.message("Message", "name", e) - evaluation.set_quiet_messages(quiet_messages) - - return SymbolNull - - -class On(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/On.html - -
    -
    'On[$symbol$::$tag$]' -
    turns a message on for printing. -
    - - >> Off[Power::infy] - >> 1 / 0 - = ComplexInfinity - >> On[Power::infy] - >> 1 / 0 - : Infinite expression 1 / 0 encountered. - = ComplexInfinity - """ - - # TODO - """ - #> On[f::x] - : Message f::x not found. - """ - attributes = A_HOLD_ALL | A_PROTECTED - summary_text = "turn on a message for printing" - - def apply(self, expr, evaluation): - "On[expr___]" - - seq = expr.get_sequence() - quiet_messages = set(evaluation.get_quiet_messages()) - - if not seq: - # TODO On[s::trace] for all symbols - return - - for e in seq: - if isinstance(e, Symbol): - quiet_messages.discard( - Expression(SymbolMessageName, e, String("trace")) - ) - elif check_message(e): - quiet_messages.discard(e) - else: - evaluation.message("Message", "name", e) - evaluation.set_quiet_messages(quiet_messages) - return SymbolNull - - -class MessageName(BinaryOperator): - """ - :WMA link:https://reference.wolfram.com/language/ref/MessageName.html - -
    -
    'MessageName[$symbol$, $tag$]' -
    '$symbol$::$tag$' -
    identifies a message. -
    - - 'MessageName' is the head of message IDs of the form 'symbol::tag'. - >> FullForm[a::b] - = MessageName[a, "b"] - - The second parameter 'tag' is interpreted as a string. - >> FullForm[a::"b"] - = MessageName[a, "b"] - """ - - attributes = A_HOLD_FIRST | A_PROTECTED - default_formats = False - formats: typing.Dict[str, Any] = {} - messages = {"messg": "Message cannot be set to `1`. It must be set to a string."} - summary_text = "message identifyier" - operator = "::" - precedence = 750 - rules = { - "MakeBoxes[MessageName[symbol_Symbol, tag_String], " - "f:StandardForm|TraditionalForm|OutputForm]": ( - 'RowBox[{MakeBoxes[symbol, f], "::", MakeBoxes[tag, f]}]' - ), - "MakeBoxes[MessageName[symbol_Symbol, tag_String], InputForm]": ( - 'RowBox[{MakeBoxes[symbol, InputForm], "::", tag}]' - ), - } - - def apply(self, symbol, tag, evaluation): - "MessageName[symbol_Symbol, tag_String]" - - pattern = Expression(SymbolMessageName, symbol, tag) - return evaluation.definitions.get_value( - symbol.get_name(), "System`Messages", pattern, evaluation - ) - - class Syntax(Builtin): r""" :WMA link:https://reference.wolfram.com/language/ref/Syntax.html @@ -571,102 +722,3 @@ class Syntax(Builtin): "com": "Warning: comma encountered with no adjacent expression. The expression will be treated as Null (line `4` of `5`).", } summary_text = "syntax messages" - - -class General(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/General.html - -
    -
    'General' -
    is a symbol to which all general-purpose messages are assigned. -
    - - >> General::argr - = `1` called with 1 argument; `2` arguments are expected. - >> Message[Rule::argr, Rule, 2] - : Rule called with 1 argument; 2 arguments are expected. - """ - - messages = { - "argb": ( - "`1` called with `2` arguments; " - "between `3` and `4` arguments are expected." - ), - "argct": "`1` called with `2` arguments.", - "argctu": "`1` called with 1 argument.", - "argr": "`1` called with 1 argument; `2` arguments are expected.", - "argrx": "`1` called with `2` arguments; `3` arguments are expected.", - "argx": "`1` called with `2` arguments; 1 argument is expected.", - "argt": ( - "`1` called with `2` arguments; " "`3` or `4` arguments are expected." - ), - "argtu": ("`1` called with 1 argument; `2` or `3` arguments are expected."), - "base": "Requested base `1` in `2` should be between 2 and `3`.", - "boxfmt": "`1` is not a box formatting type.", - "charcode": "The character encoding `1` is not supported. Use $CharacterEncodings to list supported encodings.", - "color": "`1` is not a valid color or gray-level specification.", - "cxt": "`1` is not a valid context name.", - "divz": "The argument `1` should be nonzero.", - "digit": "Digit at position `1` in `2` is too large to be used in base `3`.", - "exact": "Argument `1` is not an exact number.", - "fnsym": ( - "First argument in `1` is not a symbol " "or a string naming a symbol." - ), - "heads": "Heads `1` and `2` are expected to be the same.", - "ilsnn": ( - "Single or list of non-negative integers expected at " "position `1`." - ), - "indet": "Indeterminate expression `1` encountered.", - "innf": "Non-negative integer or Infinity expected at position `1`.", - "int": "Integer expected.", - "intp": "Positive integer expected.", - "intnn": "Non-negative integer expected.", - "iterb": "Iterator does not have appropriate bounds.", - "ivar": "`1` is not a valid variable.", - "level": ("Level specification `1` is not of the form n, " "{n}, or {m, n}."), - "locked": "Symbol `1` is locked.", - "matsq": "Argument `1` is not a non-empty square matrix.", - "newpkg": "In WL, there is a new package for this.", - "noopen": "Cannot open `1`.", - "nord": "Invalid comparison with `1` attempted.", - "normal": "Nonatomic expression expected.", - "noval": ("Symbol `1` in part assignment does not have an immediate value."), - "obspkg": "In WL, this package is obsolete.", - "openx": "`1` is not open.", - "optb": "Optional object `1` in `2` is not a single blank.", - "ovfl": "Overflow occurred in computation.", - "partd": "Part specification is longer than depth of object.", - "partw": "Part `1` of `2` does not exist.", - "plld": "Endpoints in `1` must be distinct machine-size real numbers.", - "plln": "Limiting value `1` in `2` is not a machine-size real number.", - "pspec": ( - "Part specification `1` is neither an integer nor " "a list of integer." - ), - "seqs": "Sequence specification expected, but got `1`.", - "setp": "Part assignment to `1` could not be made", - "setps": "`1` in the part assignment is not a symbol.", - "span": "`1` is not a valid Span specification.", - "ssym": "`1` is not a symbol or a string.", - "stream": "`1` is not string, InputStream[], or OutputStream[]", - "string": "String expected.", - "sym": "Argument `1` at position `2` is expected to be a symbol.", - "tag": "Rule for `1` can only be attached to `2`.", - "take": "Cannot take positions `1` through `2` in `3`.", - "ucdec": "An invalid unicode sequence was encountered and ignored.", - "vrule": ( - "Cannot set `1` to `2`, " "which is not a valid list of replacement rules." - ), - "write": "Tag `1` in `2` is Protected.", - "wrsym": "Symbol `1` is Protected.", - # TODO: someone please explain why these are different... - # Self-defined messages - "rep": "`1` is not a valid replacement rule.", - "options": "`1` is not a valid list of option rules.", - "timeout": "Timeout reached.", - "syntax": "`1`", - "invalidargs": "Invalid arguments.", - "notboxes": "`1` is not a valid box structure.", - "pyimport": '`1`[] is not available. Python module "`2`" is not installed.', - } - summary_text = "general-purpose messages" diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index c7067396b..5cbbe3871 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -29,6 +29,10 @@ from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression +from mathics.core.expression_predefined import ( + MATHICS3_COMPLEX_INFINITY, + MATHICS3_NEG_INFINITY, +) from mathics.core.list import ListExpression from mathics.core.rules import Pattern from mathics.core.symbols import ( @@ -46,12 +50,10 @@ SymbolAlternatives, SymbolAssumptions, SymbolAutomatic, - SymbolComplexInfinity, SymbolCos, SymbolCosh, SymbolCot, SymbolCoth, - SymbolDirectedInfinity, SymbolEqual, SymbolIndeterminate, SymbolLess, @@ -62,36 +64,9 @@ SymbolTable, SymbolTanh, ) - - -def sympy_factor(expr_sympy): - try: - result = sympy.together(expr_sympy) - result = sympy.factor(result) - except sympy.PolynomialError: - return expr_sympy - return result - - -def cancel(expr): - if expr.has_form("Plus", None): - return Expression(SymbolPlus, *[cancel(element) for element in expr.elements]) - else: - try: - result = expr.to_sympy() - if result is None: - return None - - # result = sympy.powsimp(result, deep=True) - result = sympy.cancel(result) - - # cancel factors out rationals, so we factor them again - result = sympy_factor(result) - - return from_sympy(result) - except sympy.PolynomialError: - # e.g. for non-commutative expressions - return expr +from mathics.eval.numbers import cancel, sympy_factor +from mathics.eval.parts import walk_parts +from mathics.eval.patterns import match def expand(expr, numer=True, denom=False, deep=False, **kwargs): @@ -466,7 +441,8 @@ def _coefficient(name, expr, form, n, evaluation): return Integer0 if not (isinstance(form, Symbol)) and not (isinstance(form, Expression)): - return evaluation.message(name, "ivar", form) + evaluation.message(name, "ivar", form) + return sympy_exprs = expr.to_sympy().as_ordered_terms() sympy_var = form.to_sympy() @@ -559,7 +535,7 @@ class Coefficient(Builtin): def eval_noform(self, expr, evaluation): "Coefficient[expr_]" - return evaluation.message("Coefficient", "argtu") + evaluation.message("Coefficient", "argtu") def eval(self, expr, form, evaluation): "Coefficient[expr_, form_]" @@ -582,7 +558,6 @@ def coeff_power_internal( """ This method returns a list of terms grouped by different powers of the expressions in var_expr. """ - from mathics.builtin.patterns import match if len(var_exprs) == 0: if form == "expr": @@ -811,11 +786,14 @@ def split_coeff_pow(term) -> Tuple[Optional[list], Optional[list]]: class CoefficientArrays(_CoefficientHandler): """ - :WMA link:https://reference.wolfram.com/language/ref/CoefficientArrays.html + + :WMA link: + https://reference.wolfram.com/language/ref/CoefficientArrays.html
    'CoefficientArrays[$polys$, $vars$]' -
    returns a list of arrays of coefficients of the variables $vars$ in the polynomial $poly$. +
    returns a list of arrays of coefficients of the variables $vars$ \ + in the polynomial $poly$.
    >> CoefficientArrays[1 + x^3, x] @@ -841,9 +819,8 @@ class CoefficientArrays(_CoefficientHandler): "array of coefficients associated with a polynomial in many variables" ) - def eval_list(self, polys, varlist, evaluation, options): + def eval_list(self, polys, varlist, evaluation: Evaluation, options: dict): "%(name)s[polys_, varlist_, OptionsPattern[]]" - from mathics.algorithm.parts import walk_parts if polys.has_form("List", None): list_polys = polys.elements @@ -959,7 +936,7 @@ class CoefficientList(Builtin): def eval_noform(self, expr, evaluation): "CoefficientList[expr_]" - return evaluation.message("CoefficientList", "argtu") + evaluation.message("CoefficientList", "argtu") def eval(self, expr, form, evaluation): "CoefficientList[expr_, form_]" @@ -968,7 +945,8 @@ def eval(self, expr, form, evaluation): # check form is not a variable for v in vars: if not (isinstance(v, Symbol)) and not (isinstance(v, Expression)): - return evaluation.message("CoefficientList", "ivar", v) + evaluation.message("CoefficientList", "ivar", v) + return # special cases for expr and form e_null = expr is SymbolNull @@ -988,7 +966,8 @@ def eval(self, expr, form, evaluation): sympy_vars = [v.to_sympy() for v in vars] if not sympy_expr.is_polynomial(*[x for x in sympy_vars]): - return evaluation.message("CoefficientList", "poly", expr) + evaluation.message("CoefficientList", "poly", expr) + return try: sympy_poly, sympy_opt = sympy.poly_from_expr(sympy_expr, sympy_vars) @@ -1036,7 +1015,7 @@ def _nth(poly, dims, exponents): return _nth(sympy_poly, dimensions, []) except sympy.PolificationFailed: - return evaluation.message("CoefficientList", "poly", expr) + evaluation.message("CoefficientList", "poly", expr) class Collect(_CoefficientHandler): @@ -1130,13 +1109,12 @@ class _Expand(Builtin): "opttf": "Value of option `1` -> `2` should be True or False.", } - def convert_options(self, options, evaluation): + def convert_options(self, options: dict, evaluation: Evaluation): modulus = options["System`Modulus"] py_modulus = modulus.get_int_value() if py_modulus is None: - return evaluation.message( - self.get_name(), "modn", Symbol("Modulus"), modulus - ) + evaluation.message(self.get_name(), "modn", Symbol("Modulus"), modulus) + return if py_modulus == 0: py_modulus = None @@ -1146,14 +1124,17 @@ def convert_options(self, options, evaluation): elif trig is SymbolFalse: py_trig = False else: - return evaluation.message(self.get_name(), "opttf", Symbol("Trig"), trig) + evaluation.message(self.get_name(), "opttf", Symbol("Trig"), trig) + return return {"modulus": py_modulus, "trig": py_trig} class Expand(_Expand): """ - :WMA link:https://reference.wolfram.com/language/ref/Expand.html + + :WMA link: + https://reference.wolfram.com/language/ref/Expand.html
    'Expand[$expr$]' @@ -1221,7 +1202,7 @@ class Expand(_Expand): summary_text = "expand out products and powers" - def eval_patt(self, expr, target, evaluation, options): + def eval_patt(self, expr, target, evaluation: Evaluation, options: dict): "Expand[expr_, target_, OptionsPattern[Expand]]" if target.get_head_name() in ("System`Rule", "System`DelayedRule"): @@ -1238,7 +1219,7 @@ def eval_patt(self, expr, target, evaluation, options): kwargs["evaluation"] = evaluation return expand(expr, True, False, **kwargs) - def eval(self, expr, evaluation, options): + def eval(self, expr, evaluation: Evaluation, options: dict): "Expand[expr_, OptionsPattern[Expand]]" kwargs = self.convert_options(options, evaluation) @@ -1285,7 +1266,7 @@ class ExpandAll(_Expand): summary_text = "expand products and powers, including negative integer powers" - def eval_patt(self, expr, target, evaluation, options): + def eval_patt(self, expr, target, evaluation: Evaluation, options: dict): "ExpandAll[expr_, target_, OptionsPattern[Expand]]" if target.get_head_name() in ("System`Rule", "System`DelayedRule"): optname = target.elements[0].get_name() @@ -1301,7 +1282,7 @@ def eval_patt(self, expr, target, evaluation, options): kwargs["evaluation"] = evaluation return expand(expr, numer=True, denom=True, deep=True, **kwargs) - def eval(self, expr, evaluation, options): + def eval(self, expr, evaluation: Evaluation, options: dict): "ExpandAll[expr_, OptionsPattern[ExpandAll]]" kwargs = self.convert_options(options, evaluation) @@ -1335,7 +1316,7 @@ class ExpandDenominator(_Expand): summary_text = "expand just the denominator of a rational expression" - def eval(self, expr, evaluation, options): + def eval(self, expr, evaluation: Evaluation, options: dict): "ExpandDenominator[expr_, OptionsPattern[ExpandDenominator]]" kwargs = self.convert_options(options, evaluation) @@ -1393,12 +1374,12 @@ class Exponent(Builtin): def eval_novar(self, expr, evaluation): "Exponent[expr_]" - return evaluation.message("Exponent", "argtu", Integer1) + evaluation.message("Exponent", "argtu", Integer1) def eval(self, expr, form, h, evaluation): "Exponent[expr_, form_, h_]" if expr == Integer0: - return Expression(SymbolDirectedInfinity, Integer(-1)) + return MATHICS3_NEG_INFINITY if not form.has_form("List", None): # TODO: add ElementProperties in Expression interface refactor branch: @@ -1516,7 +1497,8 @@ def eval_list(self, expr, vars, evaluation): for x in vars.elements: if not (isinstance(x, Atom)): - return evaluation.message("CoefficientList", "ivar", x) + evaluation.message("CoefficientList", "ivar", x) + return sympy_expr = expr.to_sympy() if sympy_expr is None: @@ -1667,7 +1649,7 @@ def eval_power_of_zero(self, b, evaluation): if self.eval(Expression(SymbolLess, Integer0, b), evaluation) is SymbolTrue: return Integer0 if self.eval(Expression(SymbolLess, b, Integer0), evaluation) is SymbolTrue: - return Symbol(SymbolComplexInfinity) + return MATHICS3_COMPLEX_INFINITY if self.eval(Expression(SymbolEqual, b, Integer0), evaluation) is SymbolTrue: return Symbol(SymbolIndeterminate) return Expression(SymbolPower, Integer0, b) @@ -1822,10 +1804,12 @@ def eval(self, s, x, evaluation): "MinimalPolynomial[s_, x_]" variables = find_all_vars(s) if len(variables) > 0: - return evaluation.message("MinimalPolynomial", "nalg", s) + evaluation.message("MinimalPolynomial", "nalg", s) + return if s is SymbolNull: - return evaluation.message("MinimalPolynomial", "nalg", s) + evaluation.message("MinimalPolynomial", "nalg", s) + return sympy_s, sympy_x = s.to_sympy(), x.to_sympy() if sympy_s is None or sympy_x is None: @@ -1936,16 +1920,19 @@ def eval(self, expr, v, evaluation): v = v.get_sequence() if len(v) > 1: - return evaluation.message("PolynomialQ", "argt", Integer(len(v) + 1)) + evaluation.message("PolynomialQ", "argt", Integer(len(v) + 1)) + return elif len(v) == 0: - return evaluation.message("PolynomialQ", "novar") + evaluation.message("PolynomialQ", "novar") + return var = v[0] if var is SymbolNull: return SymbolTrue elif var.has_form("List", None): if len(var.elements) == 0: - return evaluation.message("PolynomialQ", "novar") + evaluation.message("PolynomialQ", "novar") + return sympy_var = [x.to_sympy() for x in var.elements] else: sympy_var = [var.to_sympy()] diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 3f537d1c3..f6a92b2db 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -5,7 +5,8 @@ Originally called infinitesimal calculus or "the calculus of infinitesimals", \ is the mathematical study of continuous change, in the same way that geometry \ -is the study of shape and algebra is the study of generalizations of arithmetic operations. +is the study of shape and algebra is the study of generalizations of \ +arithmetic operations. """ from itertools import product @@ -17,8 +18,8 @@ from mathics.algorithm.integrators import ( _fubini, _internal_adaptative_simpsons_rule, - apply_D_to_Integral, decompose_domain, + eval_D_to_Integral, ) from mathics.algorithm.series import ( build_series, @@ -55,7 +56,7 @@ from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.number import dps, machine_epsilon +from mathics.core.number import MACHINE_EPSILON, dps from mathics.core.rules import Pattern from mathics.core.symbols import ( BaseElement, @@ -234,7 +235,7 @@ class D(SympyFunction): summary_text = "partial derivatives of scalar or vector functions" sympy_name = "Derivative" - def apply(self, f, x, evaluation): + def eval(self, f, x, evaluation: Evaluation): "D[f_, x_?NotListQ]" # Handle partial derivative special cases: @@ -358,7 +359,7 @@ def summand(element, index): else: return Expression(SymbolPlus, *result) - def apply_wrong(self, expr, x, other, evaluation): + def eval_wrong(self, expr, x, other, evaluation: Evaluation): "D[expr_, {x_, other___}]" arg = ListExpression(x, *other.get_sequence()) @@ -439,7 +440,7 @@ class Derivative(PostfixOperator, SympyFunction): r' RowBox[{"(", Sequence @@ Riffle[{n}, ","], ")"}]]]]' ), "MakeBoxes[Derivative[n:1|2][f_], form:OutputForm]": """RowBox[{MakeBoxes[f, form], If[n==1, "'", "''"]}]""", - # The following rules should be applied in the apply method, instead of relying on the pattern matching + # The following rules should be applied in the eval method, instead of relying on the pattern matching # mechanism. "Derivative[0...][f_]": "f", "Derivative[n__Integer][Derivative[m__Integer][f_]] /; Length[{m}] " @@ -553,7 +554,7 @@ class DiscreteLimit(Builtin): } summary_text = "limits of sequences including recurrence and number theory" - def apply(self, f, n, n0, evaluation, options={}): + def eval(self, f, n, n0, evaluation: Evaluation, options: dict = {}): "DiscreteLimit[f_, n_->n0_, OptionsPattern[DiscreteLimit]]" f = f.to_sympy(convert_all_global_functions=True) @@ -611,7 +612,7 @@ class _BaseFinder(Builtin): "Jacobian": "Automatic", } - def apply(self, f, x, x0, evaluation, options): + def eval(self, f, x, x0, evaluation: Evaluation, options: dict): "%(name)s[f_, {x_, x0_}, OptionsPattern[]]" # This is needed to get the right messages options["_isfindmaximum"] = self.__class__ is FindMaximum @@ -693,7 +694,7 @@ def diff(evaluation): else: return ListExpression(Expression(SymbolRule, x, x0)) - def apply_with_x_tuple(self, f, xtuple, evaluation, options): + def eval_with_x_tuple(self, f, xtuple, evaluation: Evaluation, options: dict): "%(name)s[f_, xtuple_, OptionsPattern[]]" f_val = f.evaluate(evaluation) @@ -710,7 +711,7 @@ def apply_with_x_tuple(self, f, xtuple, evaluation, options): options["$$Region"] = (x0, x1) else: return - return self.apply(f, x, x0, evaluation, options) + return self.eval(f, x, x0, evaluation, options) return @@ -1070,7 +1071,7 @@ def from_sympy(self, sympy_name, elements): new_elements = [elements[0]] + args return Expression(Symbol(self.get_name()), *new_elements) - def apply(self, f, xs, evaluation, options): + def eval(self, f, xs, evaluation: Evaluation, options: dict): "Integrate[f_, xs__, OptionsPattern[]]" f_sympy = f.to_sympy() if f_sympy.is_infinite: @@ -1198,9 +1199,9 @@ def apply(self, f, xs, evaluation, options): evaluation.definitions.set_ownvalue("System`$Assumptions", old_assumptions) return result - def apply_D(self, func, domain, var, evaluation, options): + def eval_D(self, func, domain, var, evaluation: Evaluation, options: dict): """D[%(name)s[func_, domain__, OptionsPattern[%(name)s]], var_Symbol]""" - return apply_D_to_Integral( + return eval_D_to_Integral( func, domain, var, evaluation, options, SymbolIntegrate ) @@ -1252,7 +1253,7 @@ class Limit(Builtin): summary_text = "directed and undirected limits" - def apply(self, expr, x, x0, evaluation, options={}): + def eval(self, expr, x, x0, evaluation: Evaluation, options={}): "Limit[expr_, x_->x0_, OptionsPattern[Limit]]" expr = expr.to_sympy() @@ -1269,7 +1270,8 @@ def apply(self, expr, x, x0, evaluation, options={}): elif value == 1: dir_sympy = "-" else: - return evaluation.message("Limit", "ldir", direction) + evaluation.message("Limit", "ldir", direction) + return try: result = sympy.limit(expr, x, x0, dir_sympy) @@ -1387,7 +1389,9 @@ class NIntegrate(Builtin): } ) - def apply_with_func_domain(self, func, domain, evaluation, options): + def eval_with_func_domain( + self, func, domain, evaluation: Evaluation, options: dict + ): "%(name)s[func_, domain__, OptionsPattern[%(name)s]]" if func.is_numeric() and func.is_zero: return Integer0 @@ -1456,7 +1460,7 @@ def apply_with_func_domain(self, func, domain, evaluation, options): if b.get_head_name() == "System`DirectedInfinity": a = a.to_python() b = b.to_python() - le = 1 - machine_epsilon + le = 1 - MACHINE_EPSILON if a == b: nulldomain = True break @@ -1473,7 +1477,7 @@ def apply_with_func_domain(self, func, domain, evaluation, options): return z = a.elements[0].value b = b.value - subdomain2.append([machine_epsilon, 1.0]) + subdomain2.append([MACHINE_EPSILON, 1.0]) coordtransform.append( (lambda u: b - z + z / u, lambda u: -z * u ** (-2.0)) ) @@ -1483,7 +1487,7 @@ def apply_with_func_domain(self, func, domain, evaluation, options): return a = a.value z = b.elements[0].value - subdomain2.append([machine_epsilon, 1.0]) + subdomain2.append([MACHINE_EPSILON, 1.0]) coordtransform.append( (lambda u: a - z + z / u, lambda u: z * u ** (-2.0)) ) @@ -1556,9 +1560,9 @@ def func2_(*u): # be implemented... return from_python(result) - def apply_D(self, func, domain, var, evaluation, options): + def eval_D(self, func, domain, var, evaluation: Evaluation, options: dict): """D[%(name)s[func_, domain__, OptionsPattern[%(name)s]], var_Symbol]""" - return apply_D_to_Integral( + return eval_D_to_Integral( func, domain, var, evaluation, options, SymbolNIntegrate ) @@ -1637,7 +1641,7 @@ class Root(SympyFunction): summary_text = "the i-th root of a polynomial." sympy_name = "CRootOf" - def apply(self, f, i, evaluation): + def eval(self, f, i, evaluation: Evaluation): "Root[f_, i_]" try: @@ -1730,11 +1734,11 @@ class Series(Builtin): summary_text = "power series and asymptotic expansions" - def apply_series(self, f, x, x0, n, evaluation): + def eval_series(self, f, x, x0, n, evaluation: Evaluation): """Series[f_, {x_Symbol, x0_, n_Integer}]""" return build_series(f, x, x0, n, evaluation) - def apply_multivariate_series(self, f, varspec, evaluation): + def eval_multivariate_series(self, f, varspec, evaluation: Evaluation): """Series[f_,varspec__List]""" lastvar = varspec.elements[-1] if not lastvar.has_form("List", 3): @@ -1745,7 +1749,7 @@ def apply_multivariate_series(self, f, varspec, evaluation): if len(varspec.elements) == 1: return inner remain_vars = Expression(SymbolSequence, *varspec.elements[:-1]) - result = self.apply_multivariate_series(inner, remain_vars, evaluation) + result = self.eval_multivariate_series(inner, remain_vars, evaluation) return result return None @@ -1777,12 +1781,14 @@ class SeriesData(Builtin): precedence = 1000 summary_text = "power series of a variable about a point" - def apply_reduce(self, x, x0, data, nummin, nummax, den, evaluation): + def eval_reduce( + self, x, x0, data, nummin: Integer, nummax: Integer, den, evaluation: Evaluation + ): """SeriesData[x_,x0_,data_,nummin_Integer, nummax_Integer, den_Integer]""" # This method tries to reduce the series expansion in two ways: # if x===x0, evaluates the series if x.sameQ(x0): - nummin_val = nummin.get_int_value() + nummin_val = nummin.value if nummin_val > 0: return Integer0 if nummin_val < 0: @@ -1836,7 +1842,17 @@ def apply_reduce(self, x, x0, data, nummin, nummax, den, evaluation): den, ) - def apply_plus(self, x, x0, data, nummin, nummax, den, term, evaluation): + def eval_plus( + self, + x, + x0, + data, + nummin: Integer, + nummax: Integer, + den: Integer, + term, + evaluation: Evaluation, + ): """Plus[SeriesData[x_, x0_, data_, nummin_Integer, nummax_Integer, den_Integer], term__]""" # If the series is null, build a series with the remaining terms if all(Integer0.sameQ(element) for element in data.elements): @@ -1917,7 +1933,9 @@ def apply_plus(self, x, x0, data, nummin, nummax, den, term, evaluation): series_expr = Expression(SymbolPlus, *incompat_series, series_expr) return series_expr - def apply_times(self, x, x0, data, nummin, nummax, den, coeff, evaluation): + def eval_times( + self, x, x0, data, nummin, nummax, den, coeff, evaluation: Evaluation + ): """Times[SeriesData[x_, x0_, data_, nummin_, nummax_, den_], coeff__]""" series = ( data, @@ -1993,7 +2011,9 @@ def apply_times(self, x, x0, data, nummin, nummax, den, coeff, evaluation): series_expr = Expression(SymbolTimes, *incompat_series, series_expr) return series_expr - def apply_derivative(self, x, x0, data, nummin, nummax, den, y, evaluation): + def eval_derivative( + self, x, x0, data, nummin, nummax, den, y, evaluation: Evaluation + ): """D[SeriesData[x_, x0_, data_, nummin_, nummax_, den_], y_]""" series = ( data, @@ -2024,12 +2044,12 @@ def apply_derivative(self, x, x0, data, nummin, nummax, den, y, evaluation): ) return result - def apply_normal(self, x, x0, data, nummin, nummax, den, evaluation): + def eval_normal(self, x, x0, data, nummin, nummax, den, evaluation: Evaluation): """Normal[SeriesData[x_, x0_, data_, nummin_, nummax_, den_]]""" new_data = [] for element in data.elements: if element.has_form("SeriesData", 6): - element = self.apply_normal(*(element.elements), evaluation) + element = self.eval_normal(*(element.elements), evaluation) if element is None: return new_data.extend([element]) @@ -2042,7 +2062,7 @@ def apply_normal(self, x, x0, data, nummin, nummax, den, evaluation): ], ) - def pre_makeboxes(self, x, x0, data, nmin, nmax, den, form, evaluation): + def pre_makeboxes(self, x, x0, data, nmin, nmax, den, form, evaluation: Evaluation): if x0.is_zero: variable = x else: @@ -2087,7 +2107,17 @@ def pre_makeboxes(self, x, x0, data, nmin, nmax, den, form, evaluation): ) return Expression(SymbolInfix, expansion, String("+"), Integer(300), SymbolLeft) - def apply_makeboxes(self, x, x0, data, nmin, nmax, den, form, evaluation): + def eval_makeboxes( + self, + x, + x0, + data, + nmin: Integer, + nmax: Integer, + den: Integer, + form, + evaluation: Evaluation, + ): """MakeBoxes[SeriesData[x_, x0_, data_List, nmin_Integer, nmax_Integer, den_Integer], form:StandardForm|TraditionalForm|OutputForm|InputForm]""" @@ -2201,7 +2231,7 @@ class Solve(Builtin): } summary_text = "find generic solutions for variables" - def apply(self, eqs, vars, evaluation): + def eval(self, eqs, vars, evaluation: Evaluation): "Solve[eqs_, vars_]" vars_original = vars @@ -2231,7 +2261,8 @@ def apply(self, eqs, vars, evaluation): elif eq is SymbolFalse: return ListExpression() elif not eq.has_form("Equal", 2): - return evaluation.message("Solve", "eqf", eqs) + evaluation.message("Solve", "eqf", eqs) + return else: left, right = eq.elements left = left.to_sympy() diff --git a/mathics/builtin/numbers/constants.py b/mathics/builtin/numbers/constants.py index aaf1972d8..787826869 100644 --- a/mathics/builtin/numbers/constants.py +++ b/mathics/builtin/numbers/constants.py @@ -19,7 +19,15 @@ from mathics.builtin.base import Builtin, Predefined, SympyObject from mathics.core.atoms import MachineReal, PrecisionReal from mathics.core.attributes import A_CONSTANT, A_PROTECTED, A_READ_PROTECTED -from mathics.core.number import PrecisionValueError, get_precision, machine_precision +from mathics.core.evaluation import Evaluation +from mathics.core.number import ( + MACHINE_DIGITS, + MAX_MACHINE_NUMBER, + MIN_MACHINE_NUMBER, + PrecisionValueError, + get_precision, + prec, +) from mathics.core.symbols import Atom, Symbol, strip_context from mathics.core.systemsymbols import SymbolIndeterminate @@ -35,8 +43,11 @@ def mp_constant(fn: str, d=None) -> mpmath.mpf: # ask for a certain number of digits, but the # accuracy will be less than that. Figure out # what's up and compensate somehow. - mpmath.mp.dps = int_d = int(d * 3.321928) - return getattr(mpmath, fn)(prec=int_d) + + int_d = prec(d) + with mpmath.workprec(int_d): + result = str(getattr(mpmath, fn)(prec=int_d)) + return result def mp_convert_constant(obj, **kwargs): @@ -80,7 +91,6 @@ def is_constant(self) -> bool: def get_constant(self, precision, evaluation): # first, determine the precision - machine_d = int(0.30103 * machine_precision) d = None if precision: try: @@ -89,7 +99,7 @@ def get_constant(self, precision, evaluation): pass if d is None: - d = machine_d + d = MACHINE_DIGITS # If preference not especified, determine it # from the precision. @@ -102,7 +112,7 @@ def get_constant(self, precision, evaluation): break if preference is None: - if d <= machine_d: + if d <= MACHINE_DIGITS: preference = "numpy" else: preference = "mpmath" @@ -123,7 +133,7 @@ def get_constant(self, precision, evaluation): preference = "" if preference == "numpy": value = numpy_constant(self.numpy_name) - if d == machine_d: + if d == MACHINE_DIGITS: return MachineReal(value) if preference == "sympy": value = sympy_constant(self.sympy_name, d + 2) @@ -224,9 +234,13 @@ class ComplexInfinity(_SympyConstant): """ :Complex Infinity: - https://en.wikipedia.org/wiki/Infinity#Complex_analysis ( + https://en.wikipedia.org/wiki/Infinity#Complex_analysis \ + is an infinite number in the complex plane whose complex argument \ + is unknown or undefined. ( :SymPy: https://docs.sympy.org/latest/modules/core.html?highlight=zoo#complexinfinity, + :MathWorld: + https://mathworld.wolfram.com/ComplexInfinity.html, :WMA: https://reference.wolfram.com/language/ref/ComplexInfinity.html) @@ -370,7 +384,7 @@ class EulerGamma(_MPMathConstant, _NumpyConstant, _SympyConstant):
    'EulerGamma' -
    is Euler's constant \u03b3 with numerial value \u2243 0.577216. +
    is Euler's constant \u03b3 with numerical value \u2243 0.577216.
    >> EulerGamma // N @@ -571,6 +585,59 @@ class Overflow(Builtin): summary_text = "overflow in numeric evaluation" +class MaxMachineNumber(Predefined): + """ + Largest normalizable machine number ( + :WMA: + https://reference.wolfram.com/language/ref/$MaxMachineNumber.html + ) + +
    +
    '$MaxMachineNumber' +
    Represents the largest positive number that can be represented \ + as a normalized machine number in the system. +
    + + The product of '$MaxMachineNumber' and '$MinMachineNumber' is a constant: + >> $MaxMachineNumber * $MinMachineNumber + = 4. + + """ + + name = "$MaxMachineNumber" + summary_text = "largest normalized positive machine number" + + def evaluate(self, evaluation: Evaluation) -> MachineReal: + return MachineReal(MAX_MACHINE_NUMBER) + + +class MinMachineNumber(Predefined): + """ + Smallest normalizable machine number ( + :WMA: + https://reference.wolfram.com/language/ref/$MinMachineNumber.html + ) + +
    +
    '$MinMachineNumber' +
    Represents the smallest positive number that can be represented \ + as a normalized machine number in the system. +
    + + 'MachinePrecision' minus the 'Log' base 10 of this number is the\ + 'Accuracy' of 0`: + >> MachinePrecision -Log[10., $MinMachineNumber]==Accuracy[0`] + = True + + """ + + name = "$MinMachineNumber" + summary_text = "smallest normalized positive machine number" + + def evaluate(self, evaluation: Evaluation) -> MachineReal: + return MachineReal(MIN_MACHINE_NUMBER) + + class Pi(_MPMathConstant, _SympyConstant): """ @@ -585,6 +652,9 @@ class Pi(_MPMathConstant, _SympyConstant):
    is the constant \u03c0.
    + >> Pi + = Pi + >> N[Pi] = 3.14159 diff --git a/mathics/builtin/numbers/diffeqns.py b/mathics/builtin/numbers/diffeqns.py index 17a5c5c40..9f158f48c 100644 --- a/mathics/builtin/numbers/diffeqns.py +++ b/mathics/builtin/numbers/diffeqns.py @@ -8,6 +8,7 @@ from mathics.builtin.base import Builtin from mathics.core.convert.sympy import from_sympy +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol @@ -111,7 +112,7 @@ class DSolve(Builtin): } summary_text = "Differential equation analytical solver." - def apply(self, eqn, y, x, evaluation): + def eval(self, eqn, y, x, evaluation: Evaluation): "DSolve[eqn_, y_, x_]" if eqn.has_form("List", None): @@ -129,7 +130,8 @@ def apply(self, eqn, y, x, evaluation): elif x.has_form("List", 1, None): syms = sorted(x.elements) else: - return evaluation.message("DSolve", "dsvar", x) + evaluation.message("DSolve", "dsvar", x) + return # Fixes pathalogical DSolve[y''[x] == y[x], y, x] try: diff --git a/mathics/builtin/numbers/hyperbolic.py b/mathics/builtin/numbers/hyperbolic.py index b6baac689..20f9f041f 100644 --- a/mathics/builtin/numbers/hyperbolic.py +++ b/mathics/builtin/numbers/hyperbolic.py @@ -3,9 +3,13 @@ """ Hyperbolic Functions -:Hyperbolic functions: https://en.wikipedia.org/wiki/Hyperbolic_functions are analogues of the ordinary trigonometric functions, but defined using the hyperbola rather than the circle. +:Hyperbolic functions: +https://en.wikipedia.org/wiki/Hyperbolic_functions are analogues \ +of the ordinary trigonometric functions, but defined using the hyperbola \ +rather than the circle. -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. """ from typing import Optional @@ -25,7 +29,15 @@ class ArcCosh(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcCosh.html + + :Inverse hyperbolic cosine: + https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#Inverse_hyperbolic_cosine ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#acosh, + :mpmath: + https://mpmath.org/doc/current/functions/hyperbolic.html#acosh, + :WMA: + https://reference.wolfram.com/language/ref/ArcCosh.html)
    'ArcCosh[$z$]' @@ -42,19 +54,30 @@ class ArcCosh(_MPMathFunction): = 0.867015 """ - summary_text = "inverse hyperbolic cosine function" - sympy_name = "acosh" mpmath_name = "acosh" rules = { "ArcCosh[Undefined]": "Undefined", + "ArcCosh[DirectedInfinity[I]]": "Infinity", + "ArcCosh[DirectedInfinity[-I]]": "Infinity", + "ArcCosh[DirectedInfinity[]]": "Infinity", "Derivative[1][ArcCosh]": "1/(Sqrt[#-1]*Sqrt[#+1])&", } + summary_text = "inverse hyperbolic cosine function" + sympy_name = "acosh" class ArcCoth(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcCoth.html + + :Inverse hyperbolic cotangent: + https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#Inverse_hyperbolic_cotangent ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#acoth, + :mpmath: + https://mpmath.org/doc/current/functions/hyperbolic.html#acoth, + :WMA: + https://reference.wolfram.com/language/ref/ArcCoth.html)
    'ArcCoth[$z$]' @@ -87,7 +110,15 @@ class ArcCoth(_MPMathFunction): class ArcCsch(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcCsch.html + + :Inverse hyperbolic cosecant: + https://en.wikipedia.org/wiki/Inverse_hyperbolic_functions#Inverse_hyperbolic_cosecant ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#acsch, + :mpmath: + https://mpmath.org/doc/current/functions/hyperbolic.html#acsch, + :WMA: + https://reference.wolfram.com/language/ref/ArcCsch.html)
    'ArcCsch[$z$]' @@ -110,7 +141,7 @@ class ArcCsch(_MPMathFunction): } summary_text = "inverse hyperbolic cosecant function" - sympy_name = "" + sympy_name = "acsch" def to_sympy(self, expr, **kwargs) -> Optional[SympyExpression]: if len(expr.elements) == 1: @@ -265,8 +296,10 @@ class Coth(_MPMathFunction): class Gudermannian(Builtin): """ - - :Gudermannian function: https://en.wikipedia.org/wiki/Gudermannian_function (:WMA: https://reference.wolfram.com/language/ref/Gudermannian.html, :MathWorld: https://mathworld.wolfram.com/Gudermannian.html) + :Gudermannian function: + https://en.wikipedia.org/wiki/Gudermannian_function ( + :WMA: https://reference.wolfram.com/language/ref/Gudermannian.html, + :MathWorld: https://mathworld.wolfram.com/Gudermannian.html)
    'Gudermannian[$z$]'
    returns the Gudermannian function $gd$($z$). @@ -291,12 +324,8 @@ class Gudermannian(Builtin): "Gudermannian[Undefined]": "Undefined", "Gudermannian[0]": "0", "Gudermannian[2*Pi*I]": "0", - "Gudermannian[6/4*Pi*I]": "DirectedInfinity[-I]", - "Gudermannian[Infinity]": "Pi/2", - "Gudermannian[-Infinity]": "Pi/2", - # Below, we don't use instead of ComplexInfinity that gets - # substituted out for DirectedInfinity[] before we match on - # Gudermannian[...] + "Gudermannian[3 I / 2 Pi]": "DirectedInfinity[-I]", + "Gudermannian[DirectedInfinity[-1]]": "-Pi/2", "Gudermannian[DirectedInfinity[]]": "Indeterminate", "Gudermannian[z_]": "2 ArcTan[Tanh[z / 2]]", # Commented out because := might not work properly @@ -311,7 +340,11 @@ class Gudermannian(Builtin): class InverseGudermannian(Builtin): """ - :Inverse Gudermannian function: https://en.wikipedia.org/wiki/Gudermannian_function (:WMA: https://reference.wolfram.com/language/ref/InverseGudermannian.html, :MathWorld: https://mathworld.wolfram.com/InverseGudermannian.html) + :Inverse Gudermannian function: + https://en.wikipedia.org/wiki/Gudermannian_function ( + :WMA: + https://reference.wolfram.com/language/ref/InverseGudermannian.html, + :MathWorld: https://mathworld.wolfram.com/InverseGudermannian.html)
    'InverseGudermannian[$z$]'
    returns the inverse Gudermannian function $gd$^-1($z$). diff --git a/mathics/builtin/numbers/integer.py b/mathics/builtin/numbers/integer.py index 6b73c8a4a..9b15fc606 100644 --- a/mathics/builtin/numbers/integer.py +++ b/mathics/builtin/numbers/integer.py @@ -165,11 +165,11 @@ def eval_n_b(self, n, b, evaluation): base = self._valid_base(b, evaluation) if not base: return - occurence_count = [0] * base + occurrence_count = [0] * base for digit in _reversed_digits(n.get_int_value(), base): - occurence_count[digit] += 1 + occurrence_count[digit] += 1 # result list is rotated by one element to the left - return to_mathics_list(*(occurence_count[1:] + [occurence_count[0]])) + return to_mathics_list(*(occurrence_count[1:] + [occurrence_count[0]])) class Floor(SympyFunction): @@ -289,17 +289,17 @@ def _parse_string(s, b): return value - def eval(self, l, b, evaluation): - "FromDigits[l_, b_]" - if l.get_head_name() == "System`List": + def eval(self, dl, b, evaluation): + "FromDigits[dl_, b_]" + if dl.get_head_name() == "System`List": value = Integer0 - for element in l.elements: + for element in dl.elements: value = Expression( SymbolPlus, Expression(SymbolTimes, value, b), element ) return value - elif isinstance(l, String): - value = FromDigits._parse_string(l.get_string_value(), b) + elif isinstance(dl, String): + value = FromDigits._parse_string(dl.get_string_value(), b) if value is None: evaluation.message("FromDigits", "nlst") else: @@ -471,13 +471,18 @@ def eval_n_b_length(self, n, b, length, evaluation): class IntegerReverse(_IntBaseBuiltin): """ - :WMA link:https://reference.wolfram.com/language/ref/IntegerReverse.html + + :WMA link: + https://reference.wolfram.com/language/ref/IntegerReverse.html
    'IntegerReverse[$n$]' -
    returns the integer that has the reverse decimal representation of $x$ without sign. +
    returns the integer that has the reverse decimal representation \ + of $x$ without sign. +
    'IntegerReverse[$n$, $b$]' -
    returns the integer that has the reverse base $b$ represenation of $x$ without sign. +
    returns the integer that has the reverse base $b$ representation \ + of $x$ without sign.
    >> IntegerReverse[1234] diff --git a/mathics/builtin/numbers/linalg.py b/mathics/builtin/numbers/linalg.py index 516b7a86e..287b8b9bf 100644 --- a/mathics/builtin/numbers/linalg.py +++ b/mathics/builtin/numbers/linalg.py @@ -9,19 +9,22 @@ from sympy import im, re from mathics.builtin.base import Builtin -from mathics.core.atoms import Integer, Integer0, Real +from mathics.core.atoms import Integer, Integer0 from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.matrix import matrix_data from mathics.core.convert.mpmath import from_mpmath, to_mpmath_matrix from mathics.core.convert.sympy import from_sympy, to_sympy_matrix +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol, SymbolList +from mathics.core.symbols import SymbolList class DesignMatrix(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/DesignMatrix.html + + :WMA link: + https://reference.wolfram.com/language/ref/DesignMatrix.html
    'DesignMatrix[$m$, $f$, $x$]' @@ -63,24 +66,27 @@ class Det(Builtin): summary_text = "determinant of a matrix" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "Det[m_]" matrix = to_sympy_matrix(m) if matrix is None or matrix.cols != matrix.rows or matrix.cols == 0: - return evaluation.message("Det", "matsq", m) + evaluation.message("Det", "matsq", m) + return det = matrix.det() return from_sympy(det) class Eigensystem(Builtin): """ - :Matrix Eigenvalues: https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors \ - (:WMA link:https://reference.wolfram.com/language/ref/Eigensystem.html) + + :Matrix Eigenvalues: + https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors (:WMA: + https://reference.wolfram.com/language/ref/Eigensystem.html)
    -
    'Eigensystem[$m$]' -
    returns the list '{Eigenvalues[$m$], Eigenvectors[$m$]}'. +
    'Eigensystem[$m$]' +
    returns the list '{Eigenvalues[$m$], Eigenvectors[$m$]}'.
    >> Eigensystem[{{1, 1, 0}, {1, 0, 1}, {0, 1, 1}}] @@ -135,7 +141,7 @@ class Eigenvalues(Builtin): def mp_eig(mp_matrix) -> Expression: try: _, ER = mpmath.eig(mp_matrix) - except: + except Exception: return None eigenvalues = ER.tolist() @@ -148,7 +154,7 @@ def mp_eig(mp_matrix) -> Expression: options = {"Method": "sympy"} - def apply(self, m, evaluation, options={}) -> Expression: + def eval(self, m, evaluation, options={}) -> Expression: "Eigenvalues[m_, OptionsPattern[Eigenvalues]]" method = self.get_option(options, "Method", evaluation) @@ -159,10 +165,12 @@ def apply(self, m, evaluation, options={}) -> Expression: sympy_matrix = to_sympy_matrix(m) if sympy_matrix is None: - return evaluation.message("Eigenvalues", "matrix", m, 1) + evaluation.message("Eigenvalues", "matrix", m, 1) + return if sympy_matrix.cols != sympy_matrix.rows or sympy_matrix.cols == 0: - return evaluation.message("Eigenvalues", "matsq", m) + evaluation.message("Eigenvalues", "matsq", m) + return eigenvalues = list(sympy_matrix.eigenvals().items()) if all(v.is_complex for (v, _) in eigenvalues): @@ -226,17 +234,19 @@ class Eigenvectors(Builtin): summary_text = "list of matrix eigenvectors" # TODO: Normalise the eigenvectors - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "Eigenvectors[m_]" matrix = to_sympy_matrix(m) if matrix is None or matrix.cols != matrix.rows or matrix.cols == 0: - return evaluation.message("Eigenvectors", "matsq", m) + evaluation.message("Eigenvectors", "matsq", m) + return # sympy raises an error for some matrices that Mathematica can compute. try: eigenvects = matrix.eigenvects(simplify=True) except NotImplementedError: - return evaluation.message("Eigenvectors", "eigenvecnotimplemented", m) + evaluation.message("Eigenvectors", "eigenvecnotimplemented", m) + return # Try to sort the eigenvectors by their corresponding eigenvalues if all(v.is_complex for (v, _, _) in eigenvects): @@ -312,7 +322,7 @@ class Inverse(Builtin): } summary_text = "inverse matrix" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "Inverse[m_List]" rows = m.elements nrows = len(rows) @@ -330,7 +340,8 @@ def apply(self, m, evaluation): matrix = to_sympy_matrix(m) det = matrix.det() if det == 0: - return evaluation.message("Inverse", "sing", m) + evaluation.message("Inverse", "sing", m) + return inv = matrix.adjugate() / det return from_sympy(inv) @@ -374,21 +385,24 @@ class LeastSquares(Builtin): } summary_text = "least square solver for linear problems" - def apply(self, m, b, evaluation): + def eval(self, m, b, evaluation: Evaluation): "LeastSquares[m_, b_]" matrix = to_sympy_matrix(m) if matrix is None: - return evaluation.message("LeastSquares", "matrix", m, 1) + evaluation.message("LeastSquares", "matrix", m, 1) + return b_vector = to_sympy_matrix(b) if b_vector is None: - return evaluation.message("LeastSquares", "matrix", b, 2) + evaluation.message("LeastSquares", "matrix", b, 2) + return try: solution = matrix.solve_least_squares(b_vector) # default method = Cholesky except NotImplementedError: - return evaluation.message("LeastSquares", "underdetermined") + evaluation.message("LeastSquares", "underdetermined") + return return from_sympy(solution) @@ -515,25 +529,29 @@ class LinearSolve(Builtin): } summary_text = "solves linear systems in matrix form" - def apply(self, m, b, evaluation): + def eval(self, m, b, evaluation: Evaluation): "LinearSolve[m_, b_]" matrix = matrix_data(m) if matrix is None: - return evaluation.message("LinearSolve", "matrix", m, 1) + evaluation.message("LinearSolve", "matrix", m, 1) + return if not b.has_form("List", None): return if len(b.elements) != len(matrix): - return evaluation.message("LinearSolve", "lslc") + evaluation.message("LinearSolve", "lslc") + return for element in b.elements: if element.has_form("List", None): - return evaluation.message("LinearSolve", "matrix", b, 2) + evaluation.message("LinearSolve", "matrix", b, 2) + return system = [mm + [v.to_sympy()] for mm, v in zip(matrix, b.elements)] system = to_sympy_matrix(system) if system is None: - return evaluation.message("LinearSolve", "matrix", b, 2) + evaluation.message("LinearSolve", "matrix", b, 2) + return syms = [sympy.Dummy("LinearSolve_var%d" % k) for k in range(system.cols - 1)] sol = sympy.solve_linear_system(system, *syms) if sol: @@ -546,7 +564,8 @@ def apply(self, m, b, evaluation): ] return from_sympy(sol) else: - return evaluation.message("LinearSolve", "nosol") + evaluation.message("LinearSolve", "nosol") + return class MatrixExp(Builtin): @@ -580,16 +599,18 @@ class MatrixExp(Builtin): # TODO fix precision summary_text = "matrix exponentiation" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "MatrixExp[m_]" sympy_m = to_sympy_matrix(m) if sympy_m is None: - return evaluation.message("MatrixExp", "matrix", m, 1) + evaluation.message("MatrixExp", "matrix", m, 1) + return try: res = sympy_m.exp() except NotImplementedError: - return evaluation.message("MatrixExp", "matrixexpnotimplemented", m) + evaluation.message("MatrixExp", "matrixexpnotimplemented", m) + return return from_sympy(res) @@ -623,11 +644,12 @@ class MatrixPower(Builtin): } summary_text = "power of a matrix" - def apply(self, m, power, evaluation): + def eval(self, m, power, evaluation: Evaluation): "MatrixPower[m_, power_]" sympy_m = to_sympy_matrix(m) if sympy_m is None: - return evaluation.message("MatrixPower", "matrix", m, 1) + evaluation.message("MatrixPower", "matrix", m, 1) + return sympy_power = power.to_sympy() if sympy_power is None: @@ -636,9 +658,11 @@ def apply(self, m, power, evaluation): try: res = sympy_m**sympy_power except NotImplementedError: - return evaluation.message("MatrixPower", "matrixpowernotimplemented", m) + evaluation.message("MatrixPower", "matrixpowernotimplemented", m) + return except ValueError: - return evaluation.message("MatrixPower", "matrixpowernotinvertible", m) + evaluation.message("MatrixPower", "matrixpowernotinvertible", m) + return return from_sympy(res) @@ -668,12 +692,13 @@ class MatrixRank(Builtin): } summary_text = "rank of a matrix" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "MatrixRank[m_]" matrix = to_sympy_matrix(m) if matrix is None: - return evaluation.message("MatrixRank", "matrix", m, 1) + evaluation.message("MatrixRank", "matrix", m, 1) + return rank = len(matrix.rref()[1]) return Integer(rank) @@ -707,12 +732,13 @@ class NullSpace(Builtin): } summary_text = "generators for the null space of a matrix" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "NullSpace[m_]" matrix = to_sympy_matrix(m) if matrix is None: - return evaluation.message("NullSpace", "matrix", m, 1) + evaluation.message("NullSpace", "matrix", m, 1) + return nullspace = matrix.nullspace() # convert n x 1 matrices to vectors @@ -749,12 +775,13 @@ class PseudoInverse(Builtin): } summary_text = "Moore-Penrose pseudoinverse" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "PseudoInverse[m_]" matrix = to_sympy_matrix(m) if matrix is None: - return evaluation.message("PseudoInverse", "matrix", m, 1) + evaluation.message("PseudoInverse", "matrix", m, 1) + return pinv = matrix.pinv() return from_sympy(pinv) @@ -783,16 +810,18 @@ class QRDecomposition(Builtin): } summary_text = "qr decomposition" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "QRDecomposition[m_]" matrix = to_sympy_matrix(m) if matrix is None: - return evaluation.message("QRDecomposition", "matrix", m, 1) + evaluation.message("QRDecomposition", "matrix", m, 1) + return try: Q, R = matrix.QRdecomposition() except sympy.matrices.MatrixError: - return evaluation.message("QRDecomposition", "sympy") + evaluation.message("QRDecomposition", "sympy") + return Q = Q.transpose() return ListExpression(*[from_sympy(Q), from_sympy(R)]) @@ -826,12 +855,13 @@ class RowReduce(Builtin): } summary_text = "matrix reduced row-echelon form" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "RowReduce[m_]" matrix = to_sympy_matrix(m) if matrix is None: - return evaluation.message("RowReduce", "matrix", m, 1) + evaluation.message("RowReduce", "matrix", m, 1) + return reduced = matrix.rref()[0] return from_sympy(reduced) @@ -877,12 +907,13 @@ class SingularValueDecomposition(Builtin): } summary_text = "singular value decomposition" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "SingularValueDecomposition[m_]" matrix = to_mpmath_matrix(m) if matrix is None: - return evaluation.message("SingularValueDecomposition", "matrix", m, 1) + evaluation.message("SingularValueDecomposition", "matrix", m, 1) + return if not any( element.is_inexact() for row in m.elements for element in row.elements @@ -921,11 +952,12 @@ class Tr(Builtin): # TODO: generalize to vectors and higher-rank tensors, and allow function arguments for application - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "Tr[m_]" matrix = to_sympy_matrix(m) if matrix is None or matrix.cols != matrix.rows or matrix.cols == 0: - return evaluation.message("Tr", "matsq", m) + evaluation.message("Tr", "matsq", m) + return tr = matrix.trace() return from_sympy(tr) diff --git a/mathics/builtin/numbers/numbertheory.py b/mathics/builtin/numbers/numbertheory.py index a98d7532c..05431cee2 100644 --- a/mathics/builtin/numbers/numbertheory.py +++ b/mathics/builtin/numbers/numbertheory.py @@ -8,7 +8,7 @@ import sympy from mathics.builtin.base import Builtin, SympyFunction -from mathics.core.atoms import Integer, Integer0, Integer10, Rational +from mathics.core.atoms import Integer, Integer0, Integer10, Rational, Real from mathics.core.attributes import ( A_LISTABLE, A_NUMERIC_FUNCTION, @@ -239,7 +239,7 @@ def eval(self, n, evaluation: Evaluation): *(to_mathics_list(factor, exp) for factor, exp in factors) ) else: - return evaluation.message("FactorInteger", "exact", n) + evaluation.message("FactorInteger", "exact", n) def _fractional_part(self, n, expr, evaluation: Evaluation): diff --git a/mathics/builtin/numbers/randomnumbers.py b/mathics/builtin/numbers/randomnumbers.py index 8a9565a56..100da9b1e 100644 --- a/mathics/builtin/numbers/randomnumbers.py +++ b/mathics/builtin/numbers/randomnumbers.py @@ -8,10 +8,14 @@ import binascii import hashlib +import os import pickle +import time from functools import reduce from operator import mul as operator_mul +import numpy + from mathics.builtin.base import Builtin from mathics.builtin.numpy_utils import instantiate_elements, stack from mathics.core.atoms import Complex, Integer, Real, String @@ -25,48 +29,24 @@ ) from mathics.eval.nevaluator import eval_N -try: - import numpy - - _numpy = True -except ImportError: # no numpy? - _numpy = False - import random - -if _numpy: - import os - import time +# mathics.builtin.__init__.py module scanning logic gets confused +# if we assign numpy.random.get_state to a variable here. so we +# use defs to safely wrap the offending objects. - # mathics.builtin.__init__.py module scanning logic gets confused - # if we assign numpy.random.get_state to a variable here. so we - # use defs to safely wrap the offending objects. - def random_get_state(): - return numpy.random.get_state() +def random_get_state(): + return numpy.random.get_state() - def random_set_state(state): - return numpy.random.set_state(state) - def random_seed(x=None): - if x is None: # numpy does not know how to seed itself randomly - x = int(time.time() * 1000) ^ hash(os.urandom(16)) - # for numpy, seed must be convertible to 32 bit unsigned integer - numpy.random.seed(abs(x) & 0xFFFFFFFF) +def random_set_state(state): + return numpy.random.set_state(state) -else: - random_get_state = random.getstate - random_set_state = random.setstate - random_seed = random.seed - def _create_array(size, f): - # creates an array of the shape 'size' with each element being - # generated through a call to 'f' (which gives a random number - # in our case). - - if size is None or len(size) == 0: - return f() - else: - return [_create_array(size[1:], f) for _ in range(size[0])] +def random_seed(x=None): + if x is None: # numpy does not know how to seed itself randomly + x = int(time.time() * 1000) ^ hash(os.urandom(16)) + # for numpy, seed must be convertible to 32 bit unsigned integer + numpy.random.seed(abs(x) & 0xFFFFFFFF) def get_random_state(): @@ -108,21 +88,7 @@ def seed(self, x=None): random_seed(x) -class NoNumPyRandomEnv(_RandomEnvBase): - def randint(self, a, b, size=None): - return _create_array(size, lambda: random.randint(a, b)) - - def randreal(self, a, b, size=None): - return _create_array(size, lambda: random.uniform(a, b)) - - def randchoice(self, n, size, replace, p): - if replace: - return random.choices([i for i in range(n)], weights=p, k=size) - else: - return random.sample([i for i in range(n)], size) - - -class NumPyRandomEnv(_RandomEnvBase): +class RandomEnv(_RandomEnvBase): def randint(self, a, b, size=None): # return numpy.random.random_integers(a, b, size) return numpy.random.randint(a, b + 1, size) @@ -135,49 +101,6 @@ def randchoice(self, n, size, replace, p): return numpy.random.choice(n, size=size, replace=replace, p=p) -if _numpy: - RandomEnv = NumPyRandomEnv -else: - RandomEnv = NoNumPyRandomEnv - - -class RandomState(Builtin): - """ - :WMA: https://reference.wolfram.com/language/ref/RandomState.html -
    -
    '$RandomState' -
    is a long number representing the internal state of the \ - pseudo-random number generator. -
    - - >> Mod[$RandomState, 10^100] - = ... - >> IntegerLength[$RandomState] - = ... - - So far, it is not possible to assign values to '$RandomState'. - >> $RandomState = 42 - : It is not possible to change the random state. - = 42 - Not even to its own value: - >> $RandomState = $RandomState; - : It is not possible to change the random state. - """ - - name = "$RandomState" - messages = { - "rndst": "It is not possible to change the random state.", - # "`1` is not a valid random state.", - } - summary_text = "internal state of the (pseudo)random number generator" - - def eval(self, evaluation): - "$RandomState" - - with RandomEnv(evaluation): - return Integer(get_random_state()) - - class _RandomBase(Builtin): messages = { "array": ( @@ -201,22 +124,29 @@ def _size_to_python(self, domain, size, evaluation): not all(isinstance(i, int) and i >= 0 for i in py_size) ): expr = Expression(Symbol(self.get_name()), domain, size) - return evaluation.message(self.get_name(), "array", size, expr), None + evaluation.message(self.get_name(), "array", size, expr), None + return return False, py_size class _RandomSelection(_RandomBase): - # implementation note: weights are clipped to numpy floats. this might be different from MMA - # where weights might be handled with full dynamic precision support through the whole computation. - # we try to limit the error by normalizing weights with full precision, and then clipping to float. - # since weights are probabilities into a finite set, this should not make a difference. + # Implementation note: weights are clipped to numpy floats. this + # might be different from MMA where weights might be handled with + # full dynamic precision support through the whole computation. + # we try to limit the error by normalizing weights with full + # precision, and then clipping to float. since weights are + # probabilities into a finite set, this should not make a + # difference. messages = { - "wghtv": "The weights on the left-hand side of `1` has to be a list of non-negative numbers " - + "with the same length as the list of items on the right-hand side.", - "lrwl": "`1` has to be a list of items or a rule of the form weights -> choices.", - "smplen": "RandomSample cannot choose `1` samples, as this are more samples than there are in `2`. " + "wghtv": "The weights on the left-hand side of `1` has to be a list of " + "non-negative numbers with the same length as the list of items " + "on the right-hand side.", + "lrwl": "`1` has to be a list of items or a rule of the form " + "weights -> choices.", + "smplen": "RandomSample cannot choose `1` samples, as this are more samples " + "than there are in `2`. " + "Use RandomChoice to choose items from a set with replacing.", } @@ -230,19 +160,22 @@ def eval(self, domain, size, evaluation): if domain.elements[1].get_head_name() != "System`List" or len( py_weights ) != len(elements): - return evaluation.message(self.get_name(), "wghtv", domain) + evaluation.message(self.get_name(), "wghtv", domain) + return elif domain.get_head_name() == "System`List": # only elements py_weights = None elements = domain.elements else: - return evaluation.message(self.get_name(), "lrwl", domain) + evaluation.message(self.get_name(), "lrwl", domain) + return err, py_size = self._size_to_python(domain, size, evaluation) if py_size is None: return err if not self._replace: # i.e. RandomSample? n_chosen = reduce(operator_mul, py_size, 1) if len(elements) < n_chosen: - return evaluation.message("smplen", size, domain), None + evaluation.message("smplen", size, domain), None + return with RandomEnv(evaluation) as rand: return instantiate_elements( rand.randchoice( @@ -267,33 +200,41 @@ def _weights_to_python(self, weights, evaluation): if norm_weights is None or not all( w.is_numeric(evaluation) for w in norm_weights.elements ): - return evaluation.message(self.get_name(), "wghtv", weights), None + evaluation.message(self.get_name(), "wghtv", weights), None + return weights = norm_weights py_weights = eval_N(weights, evaluation).to_python() if is_proper_spec else None if (py_weights is None) or ( not all(isinstance(w, (int, float)) and w >= 0 for w in py_weights) ): - return evaluation.message(self.get_name(), "wghtv", weights), None + evaluation.message(self.get_name(), "wghtv", weights), None + return return False, py_weights +# FIXME: This class should be removed and put in a Mathematica V.5 compatibility package class Random(Builtin): """ - :WMA: https://reference.wolfram.com/language/ref/Random.html + + :WMA link: + https://reference.wolfram.com/language/ref/Random.html
    'Random[]'
    gives a uniformly distributed pseudorandom Real number in the range 0 to 1.
    'Random[$type$, $range$]' -
    gives a uniformly distributed pseudorandom number of the type $type$, in the specified interval $range$. Possible types are 'Integer', 'Real' or 'Complex'. +
    gives a uniformly distributed pseudorandom number of the type \ + $type$, in the specified interval $range$. Possible types are \ + 'Integer', 'Real' or 'Complex'.
    Legacy function. Superseded by RandomReal, RandomInteger and RandomComplex. """ rules = { + "Random[]": "RandomReal[0, 1]", "Random[Integer]": "RandomInteger[]", "Random[Integer, zmax_Integer]": "RandomInteger[zmax]", "Random[Integer, {zmin_Integer, zmax_Integer}]": "RandomInteger[{zmin, zmax}]", @@ -308,18 +249,81 @@ class Random(Builtin): summary_text = "pick a random number" +class RandomChoice(_RandomSelection): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/RandomChoice.html + +
    + +
    'RandomChoice[$items$]' +
    randomly picks one item from $items$. + +
    'RandomChoice[$items$, $n$]' +
    randomly picks $n$ items from $items$. Each pick in the $n$ picks happens \ + from the given set of $items$, so each item can be picked any number of times. + +
    'RandomChoice[$items$, {$n1$, $n2$, ...}]' +
    randomly picks items from $items$ and arranges the picked items in the \ + nested list structure described by {$n1$, $n2$, ...}. + +
    'RandomChoice[$weights$ -> $items$, $n$]' +
    randomly picks $n$ items from $items$ and uses the corresponding numeric \ + values in $weights$ to determine how probable it is for each item in $items$ \ + to get picked (in the long run, items with higher weights will get picked \ + more often than ones with lower weight). + +
    'RandomChoice[$weights$ -> $items$]' +
    randomly picks one items from $items$ using weights $weights$. + +
    'RandomChoice[$weights$ -> $items$, {$n1$, $n2$, ...}]' +
    randomly picks a structured list of items from $items$ using weights \ + $weights$. +
    + + Note: 'SeedRandom' is used below so we get repeatable "random" numbers that we \ + can test. + + >> SeedRandom[42] + >> RandomChoice[{a, b, c}] + = {c} + >> SeedRandom[42] (* Set for repeatable randomness *) + >> RandomChoice[{a, b, c}, 20] + = {c, a, c, c, a, a, c, b, c, c, c, c, a, c, b, a, b, b, b, b} + >> SeedRandom[42] + >> RandomChoice[{"a", {1, 2}, x, {}}, 10] + = {x, {}, a, x, x, {}, a, a, x, {1, 2}} + >> SeedRandom[42] + >> RandomChoice[{a, b, c}, {5, 2}] + = {{c, a}, {c, c}, {a, a}, {c, b}, {c, c}} + >> SeedRandom[42] + >> RandomChoice[{1, 100, 5} -> {a, b, c}, 20] + = {b, b, b, b, b, b, b, b, b, b, b, c, b, b, b, b, b, b, b, b} + """ + + _replace = True + summary_text = "pick items randomly from a given list" + + class RandomComplex(Builtin): """ - :WMA: https://reference.wolfram.com/language/ref/RandomComplex.html) + + :WMA link: + https://reference.wolfram.com/language/ref/RandomComplex.html +
    'RandomComplex[{$z_min$, $z_max$}]' -
    yields a pseudorandom complex number in the rectangle with complex corners $z_min$ and $z_max$. +
    yields a pseudorandom complex number in the rectangle with complex corners \ + $z_min$ and $z_max$.
    'RandomComplex[$z_max$]' -
    yields a pseudorandom complex number in the rectangle with corners at the origin and at $z_max$. +
    yields a pseudorandom complex number in the rectangle with corners at the \ + origin and at $z_max$.
    'RandomComplex[]' -
    yields a pseudorandom complex number with real and imaginary parts from 0 to 1. +
    yields a pseudorandom complex number with real and imaginary parts from 0 \ + to 1.
    'RandomComplex[$range$, $n$]'
    gives a list of $n$ pseudorandom complex numbers. @@ -397,9 +401,8 @@ def eval(self, zmin, zmax, evaluation): self.to_complex(zmax, evaluation), ) if min_value is None or max_value is None: - return evaluation.message( - "RandomComplex", "unifr", ListExpression(zmin, zmax) - ) + evaluation.message("RandomComplex", "unifr", ListExpression(zmin, zmax)) + return with RandomEnv(evaluation) as rand: real = Real(rand.randreal(min_value.real, max_value.real)) @@ -415,16 +418,16 @@ def eval_list(self, zmin, zmax, ns, evaluation): self.to_complex(zmax, evaluation), ) if min_value is None or max_value is None: - return evaluation.message( - "RandomComplex", "unifr", ListExpression(zmin, zmax) - ) + evaluation.message("RandomComplex", "unifr", ListExpression(zmin, zmax)) + return py_ns = ns.to_python() if not isinstance(py_ns, list): py_ns = [py_ns] if not all([isinstance(i, int) and i >= 0 for i in py_ns]): - return evaluation.message("RandomComplex", "array", ns, expr) + evaluation.message("RandomComplex", "array", ns, expr) + return with RandomEnv(evaluation) as rand: real = rand.randreal(min_value.real, max_value.real, py_ns) @@ -436,7 +439,9 @@ def eval_list(self, zmin, zmax, ns, evaluation): class RandomInteger(Builtin): """ - :WMA: https://reference.wolfram.com/language/ref/RandomInteger.html) + + :WMA link: + https://reference.wolfram.com/language/ref/RandomInteger.html
    'RandomInteger[{$min$, $max$}]'
    yields a pseudorandom integer in the range from $min$ to \ @@ -494,9 +499,8 @@ def eval(self, rmin, rmax, evaluation): "RandomInteger[{rmin_, rmax_}]" if not isinstance(rmin, Integer) or not isinstance(rmax, Integer): - return evaluation.message( - "RandomInteger", "unifr", ListExpression(rmin, rmax) - ) + evaluation.message("RandomInteger", "unifr", ListExpression(rmin, rmax)) + return rmin, rmax = rmin.value, rmax.value with RandomEnv(evaluation) as rand: return Integer(rand.randint(rmin, rmax)) @@ -504,9 +508,8 @@ def eval(self, rmin, rmax, evaluation): def eval_list(self, rmin, rmax, ns, evaluation): "RandomInteger[{rmin_, rmax_}, ns_List]" if not isinstance(rmin, Integer) or not isinstance(rmax, Integer): - return evaluation.message( - "RandomInteger", "unifr", ListExpression(rmin, rmax) - ) + evaluation.message("RandomInteger", "unifr", ListExpression(rmin, rmax)) + return rmin, rmax = rmin.value, rmax.value result = ns.to_python() @@ -516,7 +519,10 @@ def eval_list(self, rmin, rmax, ns, evaluation): class RandomReal(Builtin): """ - :WMA: https://reference.wolfram.com/language/ref/RandomReal.html) + + :WMA link: + https://reference.wolfram.com/language/ref/RandomReal.html +
    'RandomReal[{$min$, $max$}]'
    yields a pseudorandom real number in the range from $min$ to $max$. @@ -579,7 +585,8 @@ def eval(self, xmin, xmax, evaluation): if not ( isinstance(xmin, (Real, Integer)) and isinstance(xmax, (Real, Integer)) ): - return evaluation.message("RandomReal", "unifr", ListExpression(xmin, xmax)) + evaluation.message("RandomReal", "unifr", ListExpression(xmin, xmax)) + return min_value, max_value = xmin.to_python(), xmax.to_python() @@ -592,14 +599,16 @@ def eval_list(self, xmin, xmax, ns, evaluation): if not ( isinstance(xmin, (Real, Integer)) and isinstance(xmax, (Real, Integer)) ): - return evaluation.message("RandomReal", "unifr", ListExpression(xmin, xmax)) + evaluation.message("RandomReal", "unifr", ListExpression(xmin, xmax)) + return min_value, max_value = xmin.to_python(), xmax.to_python() result = ns.to_python() if not all([isinstance(i, int) and i >= 0 for i in result]): expr = Expression(SymbolRandomReal, ListExpression(xmin, xmax), ns) - return evaluation.message("RandomReal", "array", expr, ns) + evaluation.message("RandomReal", "array", expr, ns) + return assert all([isinstance(i, int) for i in result]) @@ -609,9 +618,49 @@ def eval_list(self, xmin, xmax, ns, evaluation): ) +class RandomState(Builtin): + """ + :WMA link: + https://reference.wolfram.com/language/ref/RandomState.html +
    +
    '$RandomState' +
    is a long number representing the internal state of the \ + pseudo-random number generator. +
    + + >> Mod[$RandomState, 10^100] + = ... + >> IntegerLength[$RandomState] + = ... + + So far, it is not possible to assign values to '$RandomState'. + >> $RandomState = 42 + : It is not possible to change the random state. + = 42 + Not even to its own value: + >> $RandomState = $RandomState; + : It is not possible to change the random state. + """ + + name = "$RandomState" + messages = { + "rndst": "It is not possible to change the random state.", + # "`1` is not a valid random state.", + } + summary_text = "internal state of the (pseudo)random number generator" + + def eval(self, evaluation): + "$RandomState" + + with RandomEnv(evaluation): + return Integer(get_random_state()) + + class SeedRandom(Builtin): """ - :WMA: https://reference.wolfram.com/language/ref/SeedRandom.html) + + :WMA link: + https://reference.wolfram.com/language/ref/SeedRandom.html
    'SeedRandom[$n$]'
    resets the pseudorandom generator with seed $n$. @@ -665,7 +714,8 @@ def eval(self, x, evaluation): hashlib.md5(x.get_string_value().encode("utf8")).hexdigest(), 16 ) else: - return evaluation.message("SeedRandom", "seed", x) + evaluation.message("SeedRandom", "seed", x) + return with RandomEnv(evaluation) as rand: rand.seed(value) return SymbolNull @@ -678,94 +728,39 @@ def eval_empty(self, evaluation): return SymbolNull -# If numpy is not in the system, the following classes are going to be redefined as None. flake8 complains about this. -# What should happen here is that, or the classes be defined just if numpy is there, or to use a fallback native -# implementation. - - -class RandomChoice(_RandomSelection): - """ - :WMA: https://reference.wolfram.com/language/ref/RandomChoice.html - -
    - -
    'RandomChoice[$items$]' -
    randomly picks one item from $items$. - -
    'RandomChoice[$items$, $n$]' -
    randomly picks $n$ items from $items$. Each pick in the $n$ picks happens from the \ - given set of $items$, so each item can be picked any number of times. - -
    'RandomChoice[$items$, {$n1$, $n2$, ...}]' -
    randomly picks items from $items$ and arranges the picked items in the nested list \ - structure described by {$n1$, $n2$, ...}. - -
    'RandomChoice[$weights$ -> $items$, $n$]' -
    randomly picks $n$ items from $items$ and uses the corresponding numeric values in \ - $weights$ to determine how probable it is for each item in $items$ to get picked (in the \ - long run, items with higher weights will get picked more often than ones with lower weight). - -
    'RandomChoice[$weights$ -> $items$]' -
    randomly picks one items from $items$ using weights $weights$. - -
    'RandomChoice[$weights$ -> $items$, {$n1$, $n2$, ...}]' -
    randomly picks a structured list of items from $items$ using weights $weights$. -
    - - Note: 'SeedRandom' is used below so we get repeatable "random" numbers that we can test. - - >> SeedRandom[42] - >> RandomChoice[{a, b, c}] - = {c} - >> SeedRandom[42] (* Set for repeatable randomness *) - >> RandomChoice[{a, b, c}, 20] - = {c, a, c, c, a, a, c, b, c, c, c, c, a, c, b, a, b, b, b, b} - >> SeedRandom[42] - >> RandomChoice[{"a", {1, 2}, x, {}}, 10] - = {x, {}, a, x, x, {}, a, a, x, {1, 2}} - >> SeedRandom[42] - >> RandomChoice[{a, b, c}, {5, 2}] - = {{c, a}, {c, c}, {a, a}, {c, b}, {c, c}} - >> SeedRandom[42] - >> RandomChoice[{1, 100, 5} -> {a, b, c}, 20] - = {b, b, b, b, b, b, b, b, b, b, b, c, b, b, b, b, b, b, b, b} - """ - - _replace = True - summary_text = "pick items randomly from a given list" - - class RandomSample(_RandomSelection): """ - :WMA: https://reference.wolfram.com/language/ref/RandomSample.html + :WMA link: + https://reference.wolfram.com/language/ref/RandomSample.html
    'RandomSample[$items$]'
    randomly picks one item from $items$.
    'RandomSample[$items$, $n$]' -
    randomly picks $n$ items from $items$. Each pick in the $n$ picks happens after the \ - previous items picked have been removed from $items$, so each item can be picked at most \ - once. +
    randomly picks $n$ items from $items$. Each pick in the $n$ picks happens \ + after the previous items picked have been removed from $items$, so each item \ + can be picked at most once.
    'RandomSample[$items$, {$n1$, $n2$, ...}]' -
    randomly picks items from $items$ and arranges the picked items in the nested list \ - structure described by {$n1$, $n2$, ...}. \ +
    randomly picks items from $items$ and arranges the picked items in the \ + nested list structure described by {$n1$, $n2$, ...}. \ Each item gets picked at most once.
    'RandomSample[$weights$ -> $items$, $n$]' -
    randomly picks $n$ items from $items$ and uses the corresponding numeric values in \ - $weights$ to determine how probable it is for each item in $items$ to get picked (in the \ - long run, items with higher weights will get picked more often than ones with lower weight). \ - Each item gets picked at most once. +
    randomly picks $n$ items from $items$ and uses the corresponding numeric \ + values in $weights$ to determine how probable it is for each item in $items$ \ + to get picked (in the long run, items with higher weights will get \ + picked more often than ones with lower weight). Each item gets picked at\ + most once.
    'RandomSample[$weights$ -> $items$]'
    randomly picks one items from $items$ using weights $weights$. \ Each item gets picked at most once.
    'RandomSample[$weights$ -> $items$, {$n1$, $n2$, ...}]' -
    randomly picks a structured list of items from $items$ using weights $weights$. Each \ - item gets picked at most once. +
    randomly picks a structured list of items from $items$ using weights $weights$. + Each item gets picked at most once.
    >> SeedRandom[42] @@ -800,9 +795,3 @@ class RandomSample(_RandomSelection): _replace = False summary_text = "pick a sample at random from a list" - - -if not _numpy: # hide symbols from non-numpy envs - _RandomSelection = None - RandomChoice = None # noqa - RandomSample = None # noqa diff --git a/mathics/builtin/numbers/trig.py b/mathics/builtin/numbers/trig.py index 0cd88c306..079976d72 100644 --- a/mathics/builtin/numbers/trig.py +++ b/mathics/builtin/numbers/trig.py @@ -3,7 +3,8 @@ """ Trigonometric 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 @@ -17,18 +18,17 @@ from mathics.builtin.base import Builtin from mathics.core.atoms import Integer, Integer0, IntegerM1, Real from mathics.core.convert.python import from_python +from mathics.core.exceptions import IllegalStepSpecification from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol, SymbolPower -from mathics.core.systemsymbols import SymbolCos, SymbolSin - -SymbolArcCos = Symbol("ArcCos") -SymbolArcSin = Symbol("ArcSin") -SymbolArcTan = Symbol("ArcTan") - - -class _IllegalStepSpecification(Exception): - pass +from mathics.core.symbols import SymbolPower +from mathics.core.systemsymbols import ( + SymbolArcCos, + SymbolArcSin, + SymbolArcTan, + SymbolCos, + SymbolSin, +) class Fold: @@ -163,23 +163,33 @@ def converted_operands(): class AnglePath(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/AnglePath.html + + :WMA link: + https://reference.wolfram.com/language/ref/AnglePath.html
    'AnglePath[{$phi1$, $phi2$, ...}]' -
    returns the points formed by a turtle starting at {0, 0} and angled at 0 degrees going through - the turns given by angles $phi1$, $phi2$, ... and using distance 1 for each step. +
    returns the points formed by a turtle starting at {0, 0} and angled \ + at 0 degrees going through + the turns given by angles $phi1$, $phi2$, ... and using distance 1 \ + for each step. +
    'AnglePath[{{$r1$, $phi1$}, {$r2$, $phi2$}, ...}]' -
    instead of using 1 as distance, use $r1$, $r2$, ... as distances for the respective steps. +
    instead of using 1 as distance, use $r1$, $r2$, ... as distances for \ + the respective steps. +
    'AnglePath[$phi0$, {$phi1$, $phi2$, ...}]'
    starts with direction $phi0$ instead of 0. +
    'AnglePath[{$x$, $y$}, {$phi1$, $phi2$, ...}]'
    starts at {$x, $y} instead of {0, 0}. +
    'AnglePath[{{$x$, $y$}, $phi0$}, {$phi1$, $phi2$, ...}]'
    specifies initial position {$x$, $y$} and initial direction $phi0$. +
    'AnglePath[{{$x$, $y$}, {$dx$, $dy$}}, {$phi1$, $phi2$, ...}]' -
    specifies initial position {$x$, $y$} and a slope {$dx$, $dy$} that is understood to be the - initial direction of the turtle. +
    specifies initial position {$x$, $y$} and a slope {$dx$, $dy$} that is \ + understood to be the initial direction of the turtle.
    >> AnglePath[{90 Degree, 90 Degree, 90 Degree, 90 Degree}] @@ -213,17 +223,17 @@ def _compute(x0, y0, phi0, steps, evaluation): def parse(step): if step.get_head_name() != "System`List": - raise _IllegalStepSpecification + raise IllegalStepSpecification arguments = step.elements if len(arguments) != 2: - raise _IllegalStepSpecification + raise IllegalStepSpecification return arguments else: def parse(step): if step.get_head_name() == "System`List": - raise _IllegalStepSpecification + raise IllegalStepSpecification return None, step try: @@ -232,30 +242,30 @@ def parse(step): ListExpression(x, y) for x, y, _ in fold.fold((x0, y0, phi0), steps) ] return ListExpression(*elements) - except _IllegalStepSpecification: + except IllegalStepSpecification: evaluation.message("AnglePath", "steps", ListExpression(*steps)) - def apply(self, steps, evaluation): + def eval(self, steps, evaluation): "AnglePath[{steps___}]" return AnglePath._compute( Integer0, Integer0, None, steps.get_sequence(), evaluation ) - def apply_phi0(self, phi0, steps, evaluation): + def eval_phi0(self, phi0, steps, evaluation): "AnglePath[phi0_, {steps___}]" return AnglePath._compute( Integer0, Integer0, phi0, steps.get_sequence(), evaluation ) - def apply_xy(self, x, y, steps, evaluation): + def eval_xy(self, x, y, steps, evaluation): "AnglePath[{x_, y_}, {steps___}]" return AnglePath._compute(x, y, None, steps.get_sequence(), evaluation) - def apply_xy_phi0(self, x, y, phi0, steps, evaluation): + def eval_xy_phi0(self, x, y, phi0, steps, evaluation): "AnglePath[{{x_, y_}, phi0_}, {steps___}]" return AnglePath._compute(x, y, phi0, steps.get_sequence(), evaluation) - def apply_xy_dx(self, x, y, dx, dy, steps, evaluation): + def eval_xy_dx(self, x, y, dx, dy, steps, evaluation): "AnglePath[{{x_, y_}, {dx_, dy_}}, {steps___}]" phi0 = Expression(SymbolArcTan, dx, dy) return AnglePath._compute(x, y, phi0, steps.get_sequence(), evaluation) @@ -326,7 +336,15 @@ def _fold(self, state, steps, math): class ArcCos(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcCos.html + Inverse cosine, + :arccosine: + https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Principal_values ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#acot, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#acos, + :WMA: + https://reference.wolfram.com/language/ref/ArcCos.html)
    'ArcCos[$z$]' @@ -341,8 +359,6 @@ class ArcCos(_MPMathFunction): = Pi """ - summary_text = "inverse cosine function" - sympy_name = "acos" mpmath_name = "acos" rules = { @@ -351,11 +367,21 @@ class ArcCos(_MPMathFunction): "ArcCos[Undefined]": "Undefined", "Derivative[1][ArcCos]": "-1/Sqrt[1-#^2]&", } + summary_text = "inverse cosine function" + sympy_name = "acos" class ArcCot(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcCot.html + Inverse cotangent, + :arccotangent: + https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Principal_values ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#acot, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#acot, + :WMA: + https://reference.wolfram.com/language/ref/ArcCot.html)
    'ArcCot[$z$]' @@ -368,8 +394,6 @@ class ArcCot(_MPMathFunction): = Pi / 4 """ - summary_text = "inverse cotangent function" - sympy_name = "acot" mpmath_name = "acot" rules = { @@ -378,11 +402,21 @@ class ArcCot(_MPMathFunction): "ArcCot[Undefined]": "Undefined", "Derivative[1][ArcCot]": "-1/(1+#^2)&", } + summary_text = "inverse cotangent function" + sympy_name = "acot" class ArcCsc(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcCsc.html + Inverse cosecant, + :arccosecant: + https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Principal_values ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#acsc, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#acsc, + :WMA: + https://reference.wolfram.com/language/ref/ArcCsc.html)
    'ArcCsc[$z$]' @@ -395,8 +429,6 @@ class ArcCsc(_MPMathFunction): = -Pi / 2 """ - summary_text = "inverse cosecant function" - sympy_name = "" mpmath_name = "acsc" rules = { @@ -405,6 +437,8 @@ class ArcCsc(_MPMathFunction): "ArcCsc[1]": "Pi / 2", "Derivative[1][ArcCsc]": "-1 / (Sqrt[1 - 1/#^2] * #^2)&", } + summary_text = "inverse cosecant function" + sympy_name = "acsc" def to_sympy(self, expr, **kwargs): if len(expr.elements) == 1: @@ -415,7 +449,15 @@ def to_sympy(self, expr, **kwargs): class ArcSec(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcSec.html + Inverse secant, + :arcsecant: + https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Principal_values ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#sympy.functions.elementary.trigonometric.asec, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#asec, + :WMA: + https://reference.wolfram.com/language/ref/ArcSec.html)
    'ArcSec[$z$]' @@ -438,7 +480,7 @@ class ArcSec(_MPMathFunction): } summary_text = "inverse secant function" - sympy_name = "" + sympy_name = "asec" def to_sympy(self, expr, **kwargs): if len(expr.elements) == 1: @@ -449,7 +491,15 @@ def to_sympy(self, expr, **kwargs): class ArcSin(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcSin.html + Inverse sine, + :arcsine: + https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Principal_values ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#asin, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#asin, + :WMA: + https://reference.wolfram.com/language/ref/ArcSin.html)
    'ArcSin[$z$]' @@ -477,7 +527,15 @@ class ArcSin(_MPMathFunction): class ArcTan(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/ArcTan.html + Inverse tangent, + :arctangent: + https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Principal_values ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#atan, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#atan, + :WMA: + https://reference.wolfram.com/language/ref/ArcTan.html)
    'ArcTan[$z$]' @@ -529,7 +587,15 @@ class ArcTan(_MPMathFunction): class Cos(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Cos.html + + :Cosine: + https://en.wikipedia.org/wiki/Sine_and_cosine ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#cos, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#cos, + :WMA: + https://reference.wolfram.com/language/ref/Cos.html)
    'Cos[$z$]' @@ -555,11 +621,20 @@ class Cos(_MPMathFunction): } summary_text = "cosine function" + sympy_name = "cos" class Cot(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Cot.html + + :Cotangent: + https://en.wikipedia.org/wiki/Trigonometric_functions ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#cot, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#cot, + :WMA: + https://reference.wolfram.com/language/ref/Cot.html)
    'Cot[$z$]' @@ -581,11 +656,20 @@ class Cot(_MPMathFunction): } summary_text = "cotangent function" + sympy_name = "cot" class Csc(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Csc.html + + :Cosecant: + https://en.wikipedia.org/wiki/Trigonometric_functions ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#csc, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#csc, + :WMA: + https://reference.wolfram.com/language/ref/Csc.html)
    'Csc[$z$]' @@ -609,6 +693,7 @@ class Csc(_MPMathFunction): } summary_text = "cosecant function" + sympy_name = "csc" def to_sympy(self, expr, **kwargs): if len(expr.elements) == 1: @@ -619,7 +704,9 @@ def to_sympy(self, expr, **kwargs): class Haversine(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Haversine.html + + :WMA link: + https://reference.wolfram.com/language/ref/Haversine.html
    'Haversine[$z$]' @@ -633,13 +720,15 @@ class Haversine(_MPMathFunction): = -1.15082 + 0.869405 I """ - summary_text = "Haversine function" rules = {"Haversine[z_]": "Power[Sin[z/2], 2]"} + summary_text = "Haversine function" class InverseHaversine(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/InverseHaversine.html + + :WMA link: + https://reference.wolfram.com/language/ref/InverseHaversine.html
    'InverseHaversine[$z$]' @@ -653,13 +742,21 @@ class InverseHaversine(_MPMathFunction): = 1.76459 + 2.33097 I """ - summary_text = "inverse Haversine function" rules = {"InverseHaversine[z_]": "2 * ArcSin[Sqrt[z]]"} + summary_text = "inverse Haversine function" class Sec(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Sec.html + + :Secant: + https://en.wikipedia.org/wiki/Trigonometric_functions ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#sec, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#sec, + :WMA: + https://reference.wolfram.com/language/ref/Sec.html)
    'Sec[$z$]' @@ -674,7 +771,6 @@ class Sec(_MPMathFunction): = 1.85082 """ - summary_text = "secant function" mpmath_name = "sec" rules = { @@ -682,6 +778,9 @@ class Sec(_MPMathFunction): "Sec[0]": "1", } + summary_text = "secant function" + sympy_name = "sec" + def to_sympy(self, expr, **kwargs): if len(expr.elements) == 1: return Expression( @@ -691,7 +790,15 @@ def to_sympy(self, expr, **kwargs): class Sin(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Sin.html + + :Sine: + https://en.wikipedia.org/wiki/Sine_and_cosine ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#sin, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#sin, + :WMA: + https://reference.wolfram.com/language/ref/Sin.html)
    'Sin[$z$]' @@ -714,7 +821,6 @@ class Sin(_MPMathFunction): = 0.8414709848078965066525023216302989996226 """ - summary_text = "sine function" mpmath_name = "sin" rules = { @@ -725,11 +831,21 @@ class Sin(_MPMathFunction): "Sin[0]": "0", "Sin[Undefined]": "Undefined", } + summary_text = "sine function" + sympy_name = "sin" class Tan(_MPMathFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Tan.html + + :Tangent: + https://en.wikipedia.org/wiki/Tangent ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/elementary.html#tan, + :mpmath: + https://mpmath.org/doc/current/functions/trigonometric.html#tan, + :WMA: + https://reference.wolfram.com/language/ref/Tan.html)
    'Tan[$z$]' @@ -753,4 +869,6 @@ class Tan(_MPMathFunction): "Tan[0]": "0", "Tan[Undefined]": "Undefined", } + summary_text = "tangent function" + sympy_name = "tan" diff --git a/mathics/builtin/numeric.py b/mathics/builtin/numeric.py index d769f4d7a..d0bb14222 100644 --- a/mathics/builtin/numeric.py +++ b/mathics/builtin/numeric.py @@ -1,13 +1,15 @@ # cython: language_level=3 # -*- coding: utf-8 -*- -# Note: docstring is flowed in documentation. Line breaks in the docstring will appear in the -# printed output, so be carful not to add then mid-sentence. +# Note: docstring is flowed in documentation. Line breaks in the +# docstring will appear in the printed output, so be careful not to +# add them mid-sentence. Line breaks like \ this work though. """ Numerical Functions -Support for approximate real numbers and exact real numbers represented in algebraic or symbolic form. +Support for approximate real numbers and exact real numbers represented \ +in algebraic or symbolic form. """ import sympy @@ -16,10 +18,11 @@ from mathics.core.atoms import Complex, Integer, Integer0, Rational, Real from mathics.core.attributes import A_LISTABLE, A_NUMERIC_FUNCTION, A_PROTECTED from mathics.core.convert.sympy import from_sympy +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.number import machine_epsilon +from mathics.core.number import MACHINE_EPSILON from mathics.core.symbols import SymbolDivide, SymbolMachinePrecision, SymbolTimes -from mathics.eval.nevaluator import eval_nvalues +from mathics.eval.nevaluator import eval_NValues def chop(expr, delta=10.0 ** (-10.0)): @@ -74,19 +77,21 @@ class Chop(Builtin): summary_text = "set sufficiently small numbers or imaginary parts to zero" - def apply(self, expr, delta, evaluation): + def eval(self, expr, delta, evaluation: Evaluation): "Chop[expr_, delta_:(10^-10)]" delta = delta.round_to_float(evaluation) if delta is None or delta < 0: - return evaluation.message("Chop", "tolnn") + evaluation.message("Chop", "tolnn") + return return chop(expr, delta=delta) class N(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/N.html + :WMA link: + https://reference.wolfram.com/language/ref/N.html
    'N[$expr$, $prec$]' @@ -168,8 +173,8 @@ class N(Builtin): >> % // Precision = 20. - N can also accept an option "Method". This establishes what is the prefered underlying method to - compute numerical values: + N can also accept an option "Method". This establishes what is the \ + prefrered underlying method to compute numerical values: >> N[F[Pi], 30, Method->"numpy"] = F[3.14159265358979300000000000000] >> N[F[Pi], 30, Method->"sympy"] @@ -210,7 +215,7 @@ class N(Builtin): summary_text = "numerical evaluation to specified precision and accuracy" - def apply_with_prec(self, expr, prec, evaluation, options=None): + def eval_with_prec(self, expr, prec, evaluation, options=None): "N[expr_, prec_, OptionsPattern[%(name)s]]" # If options are passed, set the preference in evaluation, and call again @@ -233,27 +238,29 @@ def apply_with_prec(self, expr, prec, evaluation, options=None): if preference: preference_queue.append(preference) try: - result = self.apply_with_prec(expr, prec, evaluation) + result = self.eval_with_prec(expr, prec, evaluation) except Exception: result = None preference_queue.pop() return result - return eval_nvalues(expr, prec, evaluation) + return eval_NValues(expr, prec, evaluation) - def apply_N(self, expr, evaluation): + def eval_N(self, expr, evaluation: Evaluation): """N[expr_]""" # TODO: Specialize for atoms - return eval_nvalues(expr, SymbolMachinePrecision, evaluation) + return eval_NValues(expr, SymbolMachinePrecision, evaluation) class Rationalize(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Rationalize.html + :WMA link: + https://reference.wolfram.com/language/ref/Rationalize.html
    'Rationalize[$x$]' -
    converts a real number $x$ to a nearby rational number with small denominator. +
    converts a real number $x$ to a nearby rational number with \ + small denominator.
    'Rationalize[$x$, $dx$]'
    finds the rational number lies within $dx$ of $x$. @@ -262,7 +269,8 @@ class Rationalize(Builtin): >> Rationalize[2.2] = 11 / 5 - For negative $x$, '-Rationalize[-$x$] == Rationalize[$x$]' which gives symmetric results: + For negative $x$, '-Rationalize[-$x$] == Rationalize[$x$]' which \ + gives symmetric results: >> Rationalize[-11.5, 1] = -11 @@ -298,7 +306,7 @@ class Rationalize(Builtin): summary_text = "find a rational approximation" - def apply(self, x, evaluation): + def eval(self, x, evaluation: Evaluation): "Rationalize[x_]" py_x = x.to_sympy() @@ -326,7 +334,7 @@ def find_approximant(x): tol = c / q**2 if abs(i - x) <= tol: return i - if tol < machine_epsilon: + if tol < MACHINE_EPSILON: break return x @@ -338,10 +346,10 @@ def find_exact(x): ) for i in it: p, q = i.as_numer_denom() - if abs(x - i) < machine_epsilon: + if abs(x - i) < MACHINE_EPSILON: return i - def apply_dx(self, x, dx, evaluation): + def eval_dx(self, x, dx, evaluation: Evaluation): "Rationalize[x_, dx_]" py_x = x.to_sympy() if py_x is None: @@ -353,7 +361,8 @@ def apply_dx(self, x, dx, evaluation): or (not py_dx.is_real) or py_dx.is_negative ): - return evaluation.message("Rationalize", "tolnn", dx) + evaluation.message("Rationalize", "tolnn", dx) + return elif py_dx == 0: return from_sympy(self.find_exact(py_x)) @@ -470,7 +479,7 @@ class Round(Builtin): summary_text = "find closest integer or multiple of" - def apply(self, expr, k, evaluation): + def eval(self, expr, k, evaluation: Evaluation): "Round[expr_?NumericQ, k_?NumericQ]" n = Expression(SymbolDivide, expr, k).round_to_float( diff --git a/mathics/builtin/numpy_utils/__init__.py b/mathics/builtin/numpy_utils/__init__.py index a20511c64..20d32d71b 100755 --- a/mathics/builtin/numpy_utils/__init__.py +++ b/mathics/builtin/numpy_utils/__init__.py @@ -2,12 +2,7 @@ # -*- coding: utf-8 -*- -try: - import numpy - - from mathics.builtin.numpy_utils import with_numpy as numpy_layer -except ImportError: - from mathics.builtin.numpy_utils import without_numpy as numpy_layer +from mathics.builtin.numpy_utils import with_numpy as numpy_layer # we explicitly list all imported symbols so IDEs as PyCharm can properly # do their code intelligence. diff --git a/mathics/builtin/numpy_utils/with_numpy.py b/mathics/builtin/numpy_utils/with_numpy.py index 264de9bcf..652e5bb88 100755 --- a/mathics/builtin/numpy_utils/with_numpy.py +++ b/mathics/builtin/numpy_utils/with_numpy.py @@ -205,7 +205,7 @@ def instantiate_elements(a, new_element, d=1): # all relevant rules for @conditional functions are: # - all "if" branches must exit immediately with "return". # - "if"s must rely on simple binary comparisons, e.g. "b < 4" or "4 > b", or variables -# - the occurence of "elif" is optional, as is the occurence of "else" +# - the occurrence of "elif" is optional, as is the occurrence of "else" # - if "else" is not provided, the provided "if" cases must cover all possible cases, # otherwise there will be undefined results. # - code in @conditional must not reference global variables. diff --git a/mathics/builtin/numpy_utils/without_numpy.py b/mathics/builtin/numpy_utils/without_numpy.py deleted file mode 100755 index 15b6e6e32..000000000 --- a/mathics/builtin/numpy_utils/without_numpy.py +++ /dev/null @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -A couple of helper functions for doing numpy-like stuff without numpy. -""" - -import inspect -import operator -from contextlib import contextmanager -from itertools import chain -from math import ( - atan2 as atan2f, - cos as cosf, - floor as floorf, - sin as sinf, - sqrt as sqrtf, -) - -from mathics.core.list import ListExpression - -# If numpy is not available, we define the following fallbacks that are useful for implementing a similar -# logic in pure python without numpy. They obviously work on regular python array though, not numpy arrays. - - -# -# INTERNAL FUNCTIONS -# - - -def _is_bottom(a): - return any(not isinstance(x, list) for x in a) - - -def _apply(f, a): - if isinstance(a, (list, tuple)): - return [_apply(f, t) for t in a] - else: - return f(a) - - -def _apply_n(f, *p): - if isinstance(p[0], list): - return [_apply_n(f, *q) for q in zip(*p)] - else: - return f(*p) - - -# -# ARRAY CREATION AND REORGANIZATION: STACK, UNSTACK, CONCAT, ... -# - - -def array(a): - return a - - -def unstack(a): - if not a: - return [] - - def length(b): - return max(length(x) for x in b) if not _is_bottom(b) else len(b) - - def split(b, i): - if not _is_bottom(b): - return [split(x, i) for x in b] - else: - return b[i] - - return [split(a, i) for i in range(length(a))] - - -def stack(*a): - if _is_bottom(a): - return list(chain(a)) - else: - return [stack(*[x[i] for x in a]) for i in range(len(a[0]))] - - -def stacked(f, a): - components = unstack(a) - result = f(*components) - return stack(*result) - - -def concat(a, b): - return stack(*(unstack(a) + unstack(b))) - - -def vectorize(a, depth, f): - # depth == 0 means: f will get only scalars. depth == 1 means: f will - # get lists of scalars. - if _is_bottom(a): - if depth == 0: - return [f(x) for x in a] - else: - return f(a) - else: - return [vectorize(x, f, depth) for x in a] - - -# -# MATHEMATICAL OPERATIONS -# - - -def clip(a, t0, t1): - def _eval(x): - return max(t0, min(t1, x)) - - return _apply(_eval, a) - - -def dot_t(u, v): - if not isinstance(v[0], list): - return sum(x * y for x, y in zip(u, v)) - else: - return [sum(x * y for x, y in zip(u, r)) for r in v] - - -def mod(a, b): - return _apply_n(operator.mod, a, b) - - -def sin(a): - return _apply(sinf, a) - - -def cos(a): - return _apply(cosf, a) - - -def arctan2(y, x): - return _apply_n(atan2f, y, x) - - -def sqrt(a): - return _apply(sqrtf, a) - - -def floor(a): - return _apply(floorf, a) - - -def maximum(*a): - return _apply_n(max, *a) - - -def minimum(*a): - return _apply_n(min, *a) - - -# -# PUBLIC HELPER FUNCTIONS -# - - -def is_numpy_available(): - return False - - -def allclose(a, b): - if isinstance(a, list) and isinstance(b, list): - if len(a) != len(b): - return False - return all(allclose(x, y) for x, y in zip(a, b)) - elif isinstance(a, list) or isinstance(b, list): - return False - else: - return abs(a - b) < 1e-12 - - -@contextmanager -def errstate(**kwargs): - yield - - -def instantiate_elements(a, new_element, d=1): - # given a python array 'a' and a python element constructor 'new_element', generate a python array of the - # same shape as 'a' with python elements constructed through 'new_element'. 'new_element' will get called - # if an array of dimension 'd' is reached. - - e = a[0] - depth = 1 - while depth <= d and isinstance(e, list): - e = e[0] - depth += 1 - if d == depth: - elements = [new_element(x) for x in a] - else: - elements = [instantiate_elements(e, new_element, d) for e in a] - return ListExpression(*elements) - - -# -# CONDITIONALS AND PROGRAM FLOW -# - - -def _choose_descend(i, options): - if isinstance(i, (int, float)): - return options[int(i)] # int cast needed for PyPy - else: - return [ - _choose_descend(next_i, [o[k] for o in options]) - for k, next_i in enumerate(i) - ] - - -def choose(i, *options): - assert options - dim = len(options[0]) - columns = [[o[d] for o in options] for d in range(dim)] - return [_choose_descend(i, column) for column in columns] - - -def conditional(*args): # essentially a noop - if len(args) == 1 and callable(args[0]): - f = args[0] # @conditional without arguments? - else: - return lambda f: conditional(f) # with arguments - - if not inspect.isfunction(f): - raise Exception("@conditional can only be applied to functions") - - def wrapper(*a): - return f(*a) - - return wrapper diff --git a/mathics/builtin/optimization.py b/mathics/builtin/optimization.py index cb2d6025d..46d8694f6 100644 --- a/mathics/builtin/optimization.py +++ b/mathics/builtin/optimization.py @@ -1,11 +1,16 @@ # -*- coding: utf-8 -*- """Mathematical Optimization -Mathematical optimization is the selection of a best element, with regard to some criterion, from some set of available alternatives. +Mathematical optimization is the selection of a best element, with regard to \ +some criterion, from some set of available alternatives. -Optimization problems of sorts arise in all quantitative disciplines from computer science and engineering to operations research and economics, and the development of solution methods has been of interest in mathematics for centuries. +Optimization problems of sorts arise in all quantitative disciplines from \ +computer science and engineering to operations research and economics, \ +and the development of solution methods has been of interest in mathematics \ +for centuries. -We intend to provide local and global optimization techniques, both numeric and symbolic. +We intend to provide local and global optimization techniques, both numeric \ +and symbolic. """ # This tells documentation how to sort this module @@ -18,6 +23,7 @@ from mathics.core.attributes import A_CONSTANT, A_PROTECTED, A_READ_PROTECTED from mathics.core.convert.python import from_python from mathics.core.convert.sympy import from_sympy +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol @@ -28,11 +34,14 @@ class Maximize(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Maximize.html + + :WMA link: + https://reference.wolfram.com/language/ref/Maximize.html
    'Maximize[$f$, $x$]' -
    compute the maximum of $f$ respect $x$ that change between $a$ and $b$ +
    compute the maximum of $f$ respect $x$ that change between \ + $a$ and $b$.
    >> Maximize[-2 x^2 - 3 x + 5, x] @@ -48,7 +57,7 @@ class Maximize(Builtin): attributes = A_PROTECTED | A_READ_PROTECTED summary_text = "compute the maximum of a function" - def apply(self, f, vars, evaluation): + def eval(self, f, vars, evaluation: Evaluation): "Maximize[f_?NotListQ, vars_]" dual_f = f.to_sympy() * (-1) @@ -66,7 +75,7 @@ def apply(self, f, vars, evaluation): return from_python(solutions) - def apply_constraints(self, f, vars, evaluation): + def eval_constraints(self, f, vars, evaluation: Evaluation): "Maximize[f_List, vars_]" constraints = [function for function in f.elements] @@ -86,11 +95,14 @@ def apply_constraints(self, f, vars, evaluation): class Minimize(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Minimize.html + + :WMA link: + https://reference.wolfram.com/language/ref/Minimize.html
    'Minimize[$f$, $x$]' -
    compute the minimum of $f$ respect $x$ that change between $a$ and $b$ +
    compute the minimum of $f$ respect $x$ that change between \ + $a$ and $b$.
    >> Minimize[2 x^2 - 3 x + 5, x] @@ -106,7 +118,7 @@ class Minimize(Builtin): attributes = A_PROTECTED | A_READ_PROTECTED summary_text = "compute the minimum of a function" - def apply_onevariable(self, f, x, evaluation): + def eval_onevariable(self, f, x, evaluation: Evaluation): "Minimize[f_?NotListQ, x_?NotListQ]" sympy_x = x.to_sympy() @@ -137,7 +149,7 @@ def apply_onevariable(self, f, x, evaluation): ) ) - def apply_multiplevariable(self, f, vars, evaluation): + def eval_multiplevariable(self, f, vars, evaluation: Evaluation): "Minimize[f_?NotListQ, vars_List]" head_name = vars.get_head_name() @@ -215,7 +227,7 @@ def apply_multiplevariable(self, f, vars, evaluation): ) ) - def apply_constraints(self, f, vars, evaluation): + def eval_constraints(self, f, vars, evaluation: Evaluation): "Minimize[f_List, vars_List]" head_name = vars.get_head_name() vars_or = vars diff --git a/mathics/builtin/optiondoc.py b/mathics/builtin/optiondoc.py index a64a8bf62..09331bced 100644 --- a/mathics/builtin/optiondoc.py +++ b/mathics/builtin/optiondoc.py @@ -258,18 +258,23 @@ class PlotPoints(Builtin): class PlotRange(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/PlotRange.html + + :WMA link: + https://reference.wolfram.com/language/ref/PlotRange.html
    'PlotRange' -
    is a charting option, such as for 'Plot', 'BarChart', 'PieChart', etc. that gives the range of coordinates to include in a plot. +
    is a charting option, such as for 'Plot', 'BarChart', 'PieChart', \ + etc. that gives the range of coordinates to include in a plot.
    • All all points are included.
    • Automatic - outlying points are dropped.
    • $max$ - explicit limit for each function. -
    • {$min$, $max$} - explicit limits for $y$ (2D), $z$ (3D), or array values. -
    • {{$x$_$min$, $x$_$max$}, {{$y_min}, {$y_max}} - explit limits for $x$ and $y$. +
    • {$min$, $max$} - explicit limits for $y$ (2D), $z$ (3D), \ + or array values. +
    • {{$x$_$min$, $x$_$max$}, {{$y_min}, {$y_max}} - explicit limits for \ + $x$ and $y$.
    >> Plot[Sin[Cos[x^2]],{x,-4,4}, PlotRange -> All] diff --git a/mathics/builtin/options.py b/mathics/builtin/options.py index 1db938368..7e01f61f6 100644 --- a/mathics/builtin/options.py +++ b/mathics/builtin/options.py @@ -3,15 +3,15 @@ """ Options Management -A number of functions have various options which control the behavior or the default behavior that function. -Default options can be queried or set. +A number of functions have various options which control the behavior or \ +the default behavior that function. Default options can be queried or set. :WMA link: https://reference.wolfram.com/language/guide/OptionsManagement.html """ -from mathics.builtin.base import Builtin, Test, get_option +from mathics.builtin.base import Builtin, Predefined, Test, get_option from mathics.builtin.image.base import Image from mathics.core.atoms import String from mathics.core.evaluation import Evaluation @@ -19,12 +19,57 @@ from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolList, ensure_context, strip_context from mathics.core.systemsymbols import SymbolRule, SymbolRuleDelayed +from mathics.eval.patterns import Matcher -class Default(Builtin): +class All(Predefined): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/All.html + +
    +
    'All' +
    is an option value for a number of functions indicating to include everything. +
    + + + In list functions, it indicates all levels of the list. + + For example, in + :Part: + /doc/reference-of-built-in-symbols/list-functions/elements-of-lists/part, \ + 'All', extracts into a first column vector the first element of each of the list elements: + + >> {{1, 3}, {5, 7}}[[All, 1]] + = {1, 5} + + While in + :Take: + /doc/reference-of-built-in-symbols/list-functions/elements-of-lists/part, \ + 'All' extracts as a column matrix the first element as a list for each of the list elements: + + >> Take[{{1, 3}, {5, 7}}, All, {1}] + = {{1}, {5}} + + In + :Plot: + /doc/reference-of-built-in-symbols/graphics-and-drawing/plotting-data/plot, \ + setting the + :Mesh: + /doc/reference-of-built-in-symbols/drawing-options-and-option-values/mesh \ + option to 'All' will show the specific plot points: + + >> Plot[x^2, {x, -1, 1}, MaxRecursion->5, Mesh->All] + = -Graphics- + """ - :WMA link:https://reference.wolfram.com/language/ref/Default.html + summary_text = "option value that specify using everything" + + +class Default(Builtin): + """ :WMA link: https://reference.wolfram.com/language/ref/Default.html @@ -111,7 +156,6 @@ class FilterRules(Builtin): def eval(self, rules, pattern, evaluation): "FilterRules[rules_List, pattern_]" - from mathics.builtin.patterns import Matcher match = Matcher(pattern).match @@ -123,10 +167,35 @@ def matched(): return ListExpression(*list(matched())) +class None_(Predefined): + """ + :WMA link:https://reference.wolfram.com/language/ref/None.html + +
    +
    'None' +
    is a setting value for many options. +
    + + Plot3D shows the mesh grid between computed points by default. This the + :Mesh: + /doc/reference-of-built-in-symbols/drawing-option-and-values/mesh option. + + However, you hide the mesh by setting the 'Mesh' option value to 'None': + + >> Plot3D[{x^2 + y^2, -x^2 - y^2}, {x, -2, 2}, {y, -2, 2}, BoxRatios-> Automatic, Mesh->None] + = -Graphics3D- + """ + + name = "None" + summary_text = "option value that disables the option" + + # Has this been removed from WL? I cannot find a WMA link. class NotOptionQ(Test): """ - :WMA link:https://reference.wolfram.com/language/ref/NotOptionQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/NotOptionQ.html
    'NotOptionQ[$expr$]' @@ -147,7 +216,7 @@ class NotOptionQ(Test): summary_text = "test whether an expression does not match the form of a valid option specification" - def test(self, expr): + def test(self, expr) -> bool: if hasattr(expr, "flatten_with_respect_to_head"): expr = expr.flatten_with_respect_to_head(SymbolList) if not expr.has_form("List", None): @@ -162,7 +231,9 @@ def test(self, expr): # Has this been removed from WL? I cannot find a WMA link. class OptionQ(Test): """ - :WMA link:https://reference.wolfram.com/language/ref/OptionQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/OptionQ.html
    'OptionQ[$expr$]' @@ -180,7 +251,7 @@ class OptionQ(Test): >> OptionQ[{a :> True}] = True - Options lists are flattened when are applyied, so + Options lists are flattened when are applied, so >> OptionQ[{a -> True, {b->1, "c"->2}}] = True >> OptionQ[{a -> True, {b->1, c}}] @@ -198,7 +269,7 @@ class OptionQ(Test): "test whether an expression matches the form of a valid option specification" ) - def test(self, expr): + def test(self, expr) -> bool: if hasattr(expr, "flatten_with_respect_to_head"): expr = expr.flatten_with_respect_to_head(SymbolList) if not expr.has_form("List", None): @@ -300,11 +371,9 @@ def eval(self, f, evaluation): class OptionValue(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/OptionValue.html - - :WMA link: - https://reference.wolfram.com/language/ref/OptionValue.html + :WMA link: + https://reference.wolfram.com/language/ref/OptionValue.html
    'OptionValue[$name$]' @@ -348,7 +417,7 @@ class OptionValue(Builtin): } summary_text = "retrieve values of options while executing a function" - def eval_1(self, optname, evaluation): + def eval(self, optname, evaluation): "OptionValue[optname_]" if evaluation.options is None: return @@ -373,11 +442,11 @@ def eval_1(self, optname, evaluation): return Symbol(name) return val - def eval_2(self, f, optname, evaluation): + def eval_with_f(self, f, optname, evaluation): "OptionValue[f_, optname_]" - return self.apply_3(f, None, optname, evaluation) + return self.eval_with_f_and_optvals(f, None, optname, evaluation) - def eval_3(self, f, optvals, optname, evaluation): + def eval_with_f_and_optvals(self, f, optvals, optname, evaluation): "OptionValue[f_, optvals_, optname_]" if type(optname) is String: name = optname.to_python()[1:-1] @@ -428,9 +497,6 @@ def eval_3(self, f, optvals, optname, evaluation): return Symbol(name) return val - # FIXME until we figure out what test/test_evaluation.py fails - apply_3 = eval_3 - class SetOptions(Builtin): """ diff --git a/mathics/builtin/patterns.py b/mathics/builtin/patterns.py index 49521d4fd..a52e1f75a 100644 --- a/mathics/builtin/patterns.py +++ b/mathics/builtin/patterns.py @@ -1,37 +1,38 @@ # -*- coding: utf-8 -*- - """ Rules and Patterns -The concept of transformation rules for arbitrary symbolic patterns is key in Mathics. +The concept of transformation rules for arbitrary symbolic patterns is key \ +in \\Mathics. -Also, functions can get applied or transformed depending on whether or not functions arguments match. +Also, functions can get applied or transformed depending on whether or not \ +functions arguments match. Some examples: ->> a + b + c /. a + b -> t - = c + t ->> a + 2 + b + c + x * y /. n_Integer + s__Symbol + rest_ -> {n, s, rest} - = {2, a, b + c + x y} ->> f[a, b, c, d] /. f[first_, rest___] -> {first, {rest}} - = {a, {b, c, d}} + >> a + b + c /. a + b -> t + = c + t + >> a + 2 + b + c + x * y /. n_Integer + s__Symbol + rest_ -> {n, s, rest} + = {2, a, b + c + x y} + >> f[a, b, c, d] /. f[first_, rest___] -> {first, {rest}} + = {a, {b, c, d}} Tests and Conditions: ->> f[4] /. f[x_?(# > 0&)] -> x ^ 2 - = 16 ->> f[4] /. f[x_] /; x > 0 -> x ^ 2 - = 16 + >> f[4] /. f[x_?(# > 0&)] -> x ^ 2 + = 16 + >> f[4] /. f[x_] /; x > 0 -> x ^ 2 + = 16 Elements in the beginning of a pattern rather match fewer elements: ->> f[a, b, c, d] /. f[start__, end__] -> {{start}, {end}} - = {{a}, {b, c, d}} + >> f[a, b, c, d] /. f[start__, end__] -> {{start}, {end}} + = {{a}, {b, c, d}} Optional arguments using 'Optional': ->> f[a] /. f[x_, y_:3] -> {x, y} - = {a, 3} + >> f[a] /. f[x_, y_:3] -> {x, y} + = {a, 3} Options using 'OptionsPattern' and 'OptionValue': ->> f[y, a->3] /. f[x_, OptionsPattern[{a->2, b->5}]] -> {x, OptionValue[a], OptionValue[b]} - = {y, 3, 5} + >> f[y, a->3] /. f[x_, OptionsPattern[{a->2, b->5}]] -> {x, OptionValue[a], OptionValue[b]} + = {y, 3, 5} The attributes 'Flat', 'Orderless', and 'OneIdentity' affect pattern matching. """ @@ -39,7 +40,8 @@ # This tells documentation how to sort this module sort_order = "mathics.builtin.rules-and-patterns" -from mathics.algorithm.parts import python_levelspec +from typing import Callable, List, Optional as OptionalType, Tuple, Union + from mathics.builtin.base import ( AtomBuiltin, BinaryOperator, @@ -48,7 +50,6 @@ PatternObject, PostfixOperator, ) -from mathics.builtin.lists import InvalidLevelspecError from mathics.core.atoms import Integer, Number, Rational, Real, String from mathics.core.attributes import ( A_HOLD_ALL, @@ -57,15 +58,16 @@ A_PROTECTED, A_SEQUENCE_HOLD, ) -from mathics.core.element import EvalMixin +from mathics.core.element import BaseElement, EvalMixin +from mathics.core.evaluation import Evaluation +from mathics.core.exceptions import InvalidLevelspecError from mathics.core.expression import Expression, SymbolVerbatim from mathics.core.list import ListExpression from mathics.core.pattern import Pattern, StopGenerator from mathics.core.rules import Rule -from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolList, SymbolTrue -from mathics.core.systemsymbols import SymbolBlank, SymbolDispatch - -SymbolDefault = Symbol("Default") +from mathics.core.symbols import Atom, Symbol, SymbolList, SymbolTrue +from mathics.core.systemsymbols import SymbolBlank, SymbolDefault, SymbolDispatch +from mathics.eval.parts import python_levelspec class Rule_(BinaryOperator): @@ -124,7 +126,23 @@ class RuleDelayed(BinaryOperator): summary_text = "a rule that keeps the replacement unevaluated" -def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): +# TODO: disentangle me +def create_rules( + rules_expr: BaseElement, + expr: Expression, + name: str, + evaluation: Evaluation, + extra_args: List = [], +) -> Tuple[Union[List[Rule], BaseElement], bool]: + """ + This function implements `Replace`, `ReplaceAll`, `ReplaceRepeated` and `ReplaceList` eval methods. + `name` controls which of these methods is implemented. These methods applies the rule / list of rules + `rules_expr` over the expression `expr`, using the evaluation context `evaluation`. + + The result is a tuple of two elements. If the second element is `True`, then the first element is the result of the method. + If `False`, the first element of the tuple is a list of rules. + + """ if isinstance(rules_expr, Dispatch): return rules_expr.rules, False elif rules_expr.has_form("Dispatch", None): @@ -184,7 +202,9 @@ def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): class Replace(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Replace.html + + :WMA link: + https://reference.wolfram.com/language/ref/Replace.html
    'Replace[$expr$, $x$ -> $y$]' @@ -245,7 +265,7 @@ class Replace(Builtin): rules = {"Replace[rules_][expr_]": "Replace[expr, rules]"} summary_text = "apply a replacement rule" - def apply_levelspec(self, expr, rules, ls, evaluation, options): + def eval_levelspec(self, expr, rules, ls, evaluation, options): "Replace[expr_, rules_, Optional[Pattern[ls, _?LevelQ], {0}], OptionsPattern[Replace]]" try: rules, ret = create_rules(rules, expr, "Replace", evaluation) @@ -270,7 +290,9 @@ def apply_levelspec(self, expr, rules, ls, evaluation, options): class ReplaceAll(BinaryOperator): """ - :WMA link:https://reference.wolfram.com/language/ref/ReplaceAll.html + + :WMA link: + https://reference.wolfram.com/language/ref/ReplaceAll.html
    'ReplaceAll[$expr$, $x$ -> $y$]' @@ -326,7 +348,7 @@ class ReplaceAll(BinaryOperator): rules = {"ReplaceAll[rules_][expr_]": "ReplaceAll[expr, rules]"} summary_text = "apply a replacement rule on each subexpression" - def apply(self, expr, rules, evaluation): + def eval(self, expr, rules, evaluation: Evaluation): "ReplaceAll[expr_, rules_]" try: rules, ret = create_rules(rules, expr, "ReplaceAll", evaluation) @@ -341,7 +363,9 @@ def apply(self, expr, rules, evaluation): class ReplaceRepeated(BinaryOperator): """ - :WMA link:https://reference.wolfram.com/language/ref/ReplaceRepeated.html + + :WMA link: + https://reference.wolfram.com/language/ref/ReplaceRepeated.html
    'ReplaceRepeated[$expr$, $x$ -> $y$]' @@ -386,7 +410,13 @@ class ReplaceRepeated(BinaryOperator): } summary_text = "iteratively replace until the expression does not change anymore" - def apply_list(self, expr, rules, evaluation, options): + def eval_list( + self, + expr: BaseElement, + rules: BaseElement, + evaluation: Evaluation, + options: dict, + ) -> OptionalType[BaseElement]: "ReplaceRepeated[expr_, rules_, OptionsPattern[ReplaceRepeated]]" try: rules, ret = create_rules(rules, expr, "ReplaceRepeated", evaluation) @@ -421,7 +451,9 @@ def apply_list(self, expr, rules, evaluation, options): class ReplaceList(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/ReplaceList.html + + :WMA link: + https://reference.wolfram.com/language/ref/ReplaceList.html
    'ReplaceList[$expr$, $rules$]' @@ -459,7 +491,9 @@ class ReplaceList(Builtin): } summary_text = "list of possible replacement results" - def apply(self, expr, rules, max, evaluation): + def eval( + self, expr: BaseElement, rules: BaseElement, max: Number, evaluation: Evaluation + ) -> OptionalType[BaseElement]: "ReplaceList[expr_, rules_, max_:Infinity]" if max.get_name() == "System`Infinity": @@ -491,7 +525,9 @@ def apply(self, expr, rules, max, evaluation): class PatternTest(BinaryOperator, PatternObject): """ - :WMA link:https://reference.wolfram.com/language/ref/PatternTest.html + + :WMA link: + https://reference.wolfram.com/language/ref/PatternTest.html
    'PatternTest[$pattern$, $test$]' @@ -514,8 +550,10 @@ class PatternTest(BinaryOperator, PatternObject): precedence = 680 summary_text = "match to a pattern conditioned to a test result" - def init(self, expr): - super(PatternTest, self).init(expr) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(PatternTest, self).init(expr, evaluation=evaluation) # This class has an important effect in the general performance, # since all the rules that requires specify the type of patterns # call it. Then, for simple checks like `NumberQ` or `NumericQ` @@ -538,7 +576,7 @@ def init(self, expr): "System`NonNegative": self.match_nonnegative, } - self.pattern = Pattern.create(expr.elements[0]) + self.pattern = Pattern.create(expr.elements[0], evaluation=evaluation) self.test = expr.elements[1] testname = self.test.get_name() self.test_name = testname @@ -648,7 +686,7 @@ def yield_match(vars_2, rest): self.pattern.match(yield_match, expression, vars, evaluation) - def quick_pattern_test(self, candidate, test, evaluation): + def quick_pattern_test(self, candidate, test, evaluation: Evaluation): if test == "System`NegativePowerQ": return ( candidate.has_form("Power", 2) @@ -706,7 +744,9 @@ def get_match_count(self, vars={}): class Alternatives(BinaryOperator, PatternObject): """ - :WMA link:https://reference.wolfram.com/language/ref/Alternatives.html + + :WMA link: + https://reference.wolfram.com/language/ref/Alternatives.html
    'Alternatives[$p1$, $p2$, ..., $p_i$]' @@ -732,9 +772,13 @@ class Alternatives(BinaryOperator, PatternObject): precedence = 160 summary_text = "match to any of several patterns" - def init(self, expr): - super(Alternatives, self).init(expr) - self.alternatives = [Pattern.create(element) for element in expr.elements] + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(Alternatives, self).init(expr, evaluation=evaluation) + self.alternatives = [ + Pattern.create(element, evaluation=evaluation) for element in expr.elements + ] def match(self, yield_func, expression, vars, evaluation, **kwargs): for alternative in self.alternatives: @@ -763,11 +807,15 @@ class _StopGeneratorExcept(StopGenerator): class Except(PatternObject): """ - :WMA link:https://reference.wolfram.com/language/ref/Except.html + + :WMA link: + https://reference.wolfram.com/language/ref/Except.html
    'Except[$c$]' -
    represents a pattern object that matches any expression except those matching $c$. +
    represents a pattern object that matches any expression except \ + those matching $c$. +
    'Except[$c$, $p$]'
    represents a pattern object that matches $p$ but not $c$.
    @@ -789,13 +837,15 @@ class Except(PatternObject): arg_counts = [1, 2] summary_text = "match to expressions that do not match with a pattern" - def init(self, expr): - super(Except, self).init(expr) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(Except, self).init(expr, evaluation=evaluation) self.c = Pattern.create(expr.elements[0]) if len(expr.elements) == 2: - self.p = Pattern.create(expr.elements[1]) + self.p = Pattern.create(expr.elements[1], evaluation=evaluation) else: - self.p = Pattern.create(Expression(SymbolBlank)) + self.p = Pattern.create(Expression(SymbolBlank), evaluation=evaluation) def match(self, yield_func, expression, vars, evaluation, **kwargs): def except_yield_func(vars, rest): @@ -809,71 +859,11 @@ def except_yield_func(vars, rest): self.p.match(yield_func, expression, vars, evaluation) -class _StopGeneratorMatchQ(StopGenerator): - pass - - -class Matcher: - def __init__(self, form): - if isinstance(form, Pattern): - self.form = form - else: - self.form = Pattern.create(form) - - def match(self, expr, evaluation): - def yield_func(vars, rest): - raise _StopGeneratorMatchQ(True) - - try: - self.form.match(yield_func, expr, {}, evaluation) - except _StopGeneratorMatchQ: - return True - return False - - -def match(expr, form, evaluation): - return Matcher(form).match(expr, evaluation) - - -class MatchQ(Builtin): - """ - - :WMA link:https://reference.wolfram.com/language/ref/MatchQ.html - -
    -
    'MatchQ[$expr$, $form$]' -
    tests whether $expr$ matches $form$. -
    - - >> MatchQ[123, _Integer] - = True - >> MatchQ[123, _Real] - = False - >> MatchQ[_Integer][123] - = True - >> MatchQ[3, Pattern[3]] - : First element in pattern Pattern[3] is not a valid pattern name. - = False - """ - - rules = {"MatchQ[form_][expr_]": "MatchQ[expr, form]"} - summary_text = "test whether an expression matches a pattern" - - def apply(self, expr, form, evaluation): - "MatchQ[expr_, form_]" - - try: - if match(expr, form, evaluation): - return SymbolTrue - return SymbolFalse - except PatternError as e: - evaluation.message(e.name, e.tag, *(e.args)) - return SymbolFalse - - class Verbatim(PatternObject): """ - :WMA link:https://reference.wolfram.com/language/ref/Verbatim.html + + :WMA link: + https://reference.wolfram.com/language/ref/Verbatim.html
    'Verbatim[$expr$]' @@ -895,8 +885,10 @@ class Verbatim(PatternObject): arg_counts = [1, 2] summary_text = "take the pattern elements as literals" - def init(self, expr): - super(Verbatim, self).init(expr) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(Verbatim, self).init(expr, evaluation=evaluation) self.content = expr.elements[0] def match(self, yield_func, expression, vars, evaluation, **kwargs): @@ -929,9 +921,11 @@ class HoldPattern(PatternObject): attributes = A_HOLD_ALL | A_PROTECTED summary_text = "took the expression as a literal pattern" - def init(self, expr): - super(HoldPattern, self).init(expr) - self.pattern = Pattern.create(expr.elements[0]) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(HoldPattern, self).init(expr, evaluation=evaluation) + self.pattern = Pattern.create(expr.elements[0], evaluation=evaluation) def match(self, yield_func, expression, vars, evaluation, **kwargs): # for new_vars, rest in self.pattern.match( @@ -1008,15 +1002,17 @@ class Pattern_(PatternObject): } summary_text = "a named pattern" - def init(self, expr): + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: if len(expr.elements) != 2: self.error("patvar", expr) varname = expr.elements[0].get_name() if varname is None or varname == "": self.error("patvar", expr) - super(Pattern_, self).init(expr) + super(Pattern_, self).init(expr, evaluation=evaluation) self.varname = varname - self.pattern = Pattern.create(expr.elements[1]) + self.pattern = Pattern.create(expr.elements[1], evaluation=evaluation) def __repr__(self): return "" % repr(self.pattern) @@ -1122,9 +1118,11 @@ class Optional(BinaryOperator, PatternObject): precedence = 140 summary_text = "an optional argument with a default value" - def init(self, expr): - super(Optional, self).init(expr) - self.pattern = Pattern.create(expr.elements[0]) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(Optional, self).init(expr, evaluation=evaluation) + self.pattern = Pattern.create(expr.elements[0], evaluation=evaluation) if len(expr.elements) == 2: self.default = expr.elements[1] else: @@ -1167,7 +1165,12 @@ def get_match_count(self, vars={}): return (0, 1) -def get_default_value(name, evaluation, k=None, n=None): +def get_default_value( + name: str, + evaluation: Evaluation, + k: OptionalType[int] = None, + n: OptionalType[int] = None, +): pos = [] if k is not None: pos.append(k) @@ -1191,8 +1194,10 @@ def get_default_value(name, evaluation, k=None, n=None): class _Blank(PatternObject): arg_counts = [0, 1] - def init(self, expr): - super(_Blank, self).init(expr) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(_Blank, self).init(expr, evaluation=evaluation) if expr.elements: self.head = expr.elements[0] else: @@ -1241,7 +1246,14 @@ class Blank(_Blank): } summary_text = "match to any single expression" - def match(self, yield_func, expression, vars, evaluation, **kwargs): + def match( + self, + yield_func: Callable, + expression: Expression, + vars: dict, + evaluation: Evaluation, + **kwargs + ): if not expression.has_form("Sequence", 0): if self.head is not None: if expression.get_head().sameQ(self.head): @@ -1297,7 +1309,14 @@ class BlankSequence(_Blank): } summary_text = "match to a non-empty sequence of elements" - def match(self, yield_func, expression, vars, evaluation, **kwargs): + def match( + self, + yield_func: Callable, + expression: Expression, + vars: dict, + evaluation: Evaluation, + **kwargs + ): elements = expression.get_sequence() if not elements: return @@ -1354,7 +1373,14 @@ class BlankNullSequence(_Blank): } summary_text = "match to a sequence of zero or more elements" - def match(self, yield_func, expression, vars, evaluation, **kwargs): + def match( + self, + yield_func: Callable, + expression: Expression, + vars: dict, + evaluation: Evaluation, + **kwargs + ): elements = expression.get_sequence() if self.head: ok = True @@ -1410,11 +1436,15 @@ class Repeated(PostfixOperator, PatternObject): operator = ".." precedence = 170 - summary_text = "match to one or more occurences of a pattern" - - def init(self, expr, min=1): - self.pattern = Pattern.create(expr.elements[0]) + summary_text = "match to one or more occurrences of a pattern" + def init( + self, + expr: Expression, + min: int = 1, + evaluation: OptionalType[Evaluation] = None, + ): + self.pattern = Pattern.create(expr.elements[0], evaluation=evaluation) self.max = None self.min = min if len(expr.elements) == 2: @@ -1484,10 +1514,12 @@ class RepeatedNull(Repeated): operator = "..." precedence = 170 - summary_text = "match to zero or more occurences of a pattern" + summary_text = "match to zero or more occurrences of a pattern" - def init(self, expr): - super(RepeatedNull, self).init(expr, min=0) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(RepeatedNull, self).init(expr, min=0, evaluation=evaluation) class Shortest(Builtin): @@ -1511,11 +1543,14 @@ class Shortest(Builtin): class Longest(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Longest.html + + :WMA link: + https://reference.wolfram.com/language/ref/Longest.html
    'Longest[$pat$]' -
    is a pattern object that matches the longest sequence consistent with the pattern $p$. +
    is a pattern object that matches the longest sequence consistent \ + with the pattern $p$.
    >> StringCases["aabaaab", Longest["a" ~~ __ ~~ "b"]] = {aabaaab} @@ -1529,7 +1564,9 @@ class Longest(Builtin): class Condition(BinaryOperator, PatternObject): """ - :WMA link:https://reference.wolfram.com/language/ref/Condition.html + + :WMA link: + https://reference.wolfram.com/language/ref/Condition.html
    'Condition[$pattern$, $expr$]' @@ -1560,17 +1597,26 @@ class Condition(BinaryOperator, PatternObject): precedence = 130 summary_text = "conditional definition" - def init(self, expr): - super(Condition, self).init(expr) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(Condition, self).init(expr, evaluation=evaluation) self.test = expr.elements[1] # if (expr.elements[0].get_head_name() == "System`Condition" and # len(expr.elements[0].elements) == 2): # self.test = Expression(SymbolAnd, self.test, expr.elements[0].elements[1]) # self.pattern = Pattern.create(expr.elements[0].elements[0]) # else: - self.pattern = Pattern.create(expr.elements[0]) + self.pattern = Pattern.create(expr.elements[0], evaluation=evaluation) - def match(self, yield_func, expression, vars, evaluation, **kwargs): + def match( + self, + yield_func: Callable, + expression: Expression, + vars: dict, + evaluation: Evaluation, + **kwargs + ): # for new_vars, rest in self.pattern.match(expression, vars, # evaluation): def yield_match(new_vars, rest): @@ -1584,18 +1630,21 @@ def yield_match(new_vars, rest): class OptionsPattern(PatternObject): """ - :WMA link:https://reference.wolfram.com/language/ref/OptionsPattern.html + + :WMA link: + https://reference.wolfram.com/language/ref/OptionsPattern.html
    'OptionsPattern[$f$]' -
    is a pattern that stands for a sequence of options given - to a function, with default values taken from 'Options[$f$]'. +
    is a pattern that stands for a sequence of options given \ + to a function, with default values taken from 'Options[$f$]'. \ The options can be of the form '$opt$->$value$' or '$opt$:>$value$', and might be in arbitrarily nested lists. +
    'OptionsPattern[{$opt1$->$value1$, ...}]'
    takes explicit default values from the given list. The - list may also contain symbols $f$, for which 'Options[$f$]' is - taken into account; it may be arbitrarily nested. + list may also contain symbols $f$, for which 'Options[$f$]' is \ + taken into account; it may be arbitrarily nested. \ 'OptionsPattern[{}]' does not use any default values.
    @@ -1642,8 +1691,10 @@ class OptionsPattern(PatternObject): arg_counts = [0, 1] summary_text = "a sequence of optional named arguments" - def init(self, expr): - super(OptionsPattern, self).init(expr) + def init( + self, expr: Expression, evaluation: OptionalType[Evaluation] = None + ) -> None: + super(OptionsPattern, self).init(expr, evaluation=evaluation) try: self.defaults = expr.elements[0] except IndexError: @@ -1651,7 +1702,14 @@ def init(self, expr): # function. Set to not None in self.match self.defaults = None - def match(self, yield_func, expression, vars, evaluation, **kwargs): + def match( + self, + yield_func: Callable, + expression: Expression, + vars: dict, + evaluation: Evaluation, + **kwargs + ): if self.defaults is None: self.defaults = kwargs.get("head") if self.defaults is None: @@ -1683,13 +1741,18 @@ def match(self, yield_func, expression, vars, evaluation, **kwargs): new_vars["_option_" + name] = value yield_func(new_vars, None) - def get_match_count(self, vars={}): + def get_match_count(self, vars: dict = {}): return (0, None) def get_match_candidates( - self, elements, expression, attributes, evaluation, vars={} + self, + elements: Tuple[BaseElement], + expression: Expression, + attributes: int, + evaluation: Evaluation, + vars: dict = {}, ): - def _match(element): + def _match(element: Expression): return element.has_form(("Rule", "RuleDelayed"), 2) or element.has_form( "List", None ) @@ -1700,7 +1763,7 @@ def _match(element): class Dispatch(Atom): class_head_name = "System`Dispatch" - def __init__(self, rulelist, evaluation): + def __init__(self, rulelist: Expression, evaluation: Evaluation) -> None: self.src = ListExpression(*rulelist) self.rules = [Rule(rule.elements[0], rule.elements[1]) for rule in rulelist] self._elements = None @@ -1715,7 +1778,7 @@ def get_atom_name(self): def __repr__(self): return "dispatch" - def atom_to_boxes(self, f, evaluation): + def atom_to_boxes(self, f: Symbol, evaluation: Evaluation): from mathics.builtin.box.layout import RowBox from mathics.eval.makeboxes import format_element @@ -1725,13 +1788,15 @@ def atom_to_boxes(self, f, evaluation): class DispatchAtom(AtomBuiltin): """ - :WMA link:https://reference.wolfram.com/language/ref/DispatchAtom.html + + :WMA link: + https://reference.wolfram.com/language/ref/DispatchAtom.html
    'Dispatch[$rulelist$]' -
    Introduced for compatibility. Currently, it just return $rulelist$. - In the future, it should return an optimized DispatchRules atom, - containing an optimized set of rules. +
    Introduced for compatibility. Currently, it just return $rulelist$. \ + In the future, it should return an optimized DispatchRules atom, \ + containing an optimized set of rules.
    >> rules = {{a_,b_}->a^b, {1,2}->3., F[x_]->x^2}; >> F[2] /. rules @@ -1751,7 +1816,9 @@ class DispatchAtom(AtomBuiltin): def __repr__(self): return "dispatchatom" - def apply_create(self, rules, evaluation): + def eval_create( + self, rules: ListExpression, evaluation: Evaluation + ) -> OptionalType[BaseElement]: """Dispatch[rules_List]""" # TODO: # The next step would be to enlarge this method, in order to @@ -1773,7 +1840,7 @@ def apply_create(self, rules, evaluation): all_list = all(rule.has_form("List", None) for rule in rules) if all_list: - elements = [self.apply_create(rule, evaluation) for rule in rules] + elements = [self.eval_create(rule, evaluation) for rule in rules] return ListExpression(*elements) flatten_list = [] for rule in rules: @@ -1795,7 +1862,7 @@ def apply_create(self, rules, evaluation): except Exception: return - def apply_normal(self, dispatch, evaluation): + def eval_normal(self, dispatch: Dispatch, evaluation: Evaluation) -> ListExpression: """Normal[dispatch_Dispatch]""" if isinstance(dispatch, Dispatch): return dispatch.src diff --git a/mathics/builtin/physchemdata.py b/mathics/builtin/physchemdata.py index 6f12885c4..9f0812e6c 100644 --- a/mathics/builtin/physchemdata.py +++ b/mathics/builtin/physchemdata.py @@ -11,6 +11,7 @@ from mathics.builtin.base import Builtin from mathics.core.atoms import Integer, String from mathics.core.convert.python import from_python +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol, strip_context @@ -27,9 +28,9 @@ def load_element_data(): datadir = mathics_scanner.__file__[:-11] element_file = open(os.path.join(datadir, "data/element.csv"), "r") - except: - print(os.path.join(datadir, "data/element.csv"), " not found.") - return None + except Exception: + raise NoElementDataFile("data/elements.csv is not available.") + reader = csvreader(element_file, delimiter="\t") element_data = [] for row in reader: @@ -40,9 +41,6 @@ def load_element_data(): _ELEMENT_DATA = load_element_data() -if _ELEMENT_DATA is None: - raise NoElementDataFile("data/elements.csv is not available.") - class ElementData(Builtin): """ @@ -113,16 +111,16 @@ class ElementData(Builtin): summary_text = "Data about chemical elements" - def apply_all(self, evaluation): + def eval_all(self, evaluation: Evaluation): "ElementData[All]" iprop = _ELEMENT_DATA[0].index("StandardName") return from_python([element[iprop] for element in _ELEMENT_DATA[1:]]) - def apply_all_properties(self, evaluation): + def eval_all_properties(self, evaluation: Evaluation): 'ElementData[All, "Properties"]' return from_python(sorted(_ELEMENT_DATA[0])) - def apply_name(self, expr, prop, evaluation): + def eval_name(self, expr, prop, evaluation: Evaluation): "ElementData[expr_, prop_]" if isinstance(expr, String): diff --git a/mathics/builtin/procedural.py b/mathics/builtin/procedural.py index 735f70675..916302f09 100644 --- a/mathics/builtin/procedural.py +++ b/mathics/builtin/procedural.py @@ -3,17 +3,21 @@ """ Procedural Programming -Procedural programming is a programming paradigm, derived from imperative programming, based on the concept of the procedure call. This term is sometimes compared and contrasted with Functional Programming. +Procedural programming is a programming paradigm, derived from imperative \ +programming, based on the concept of the procedure call. This term is \ +sometimes compared and contrasted with Functional Programming. -Procedures (a type of routine or subroutine) simply contain a series of computational steps to be carried out. Any given procedure might be called at any point during a program's execution, including by other procedures or itself. +Procedures (a type of routine or subroutine) simply contain a series of \ +computational steps to be carried out. Any given procedure might be called \ +at any point during a program's execution, including by other procedures \ +or itself. -Procedural functions are integrated into Mathics symbolic programming environment. +Procedural functions are integrated into \Mathics symbolic programming \ +environment. """ -from mathics.builtin.base import BinaryOperator, Builtin -from mathics.builtin.lists import _IterationFunction -from mathics.builtin.patterns import match +from mathics.builtin.base import BinaryOperator, Builtin, IterationFunction from mathics.core.attributes import ( A_HOLD_ALL, A_HOLD_REST, @@ -30,13 +34,15 @@ ) from mathics.core.symbols import Symbol, SymbolFalse, SymbolNull, SymbolTrue from mathics.core.systemsymbols import SymbolMatchQ +from mathics.eval.patterns import match SymbolWhich = Symbol("Which") class Abort(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Abort.html + :WMA link: + https://reference.wolfram.com/language/ref/Abort.html
    'Abort[]' @@ -49,7 +55,7 @@ class Abort(Builtin): summary_text = "generate an abort" - def apply(self, evaluation): + def eval(self, evaluation): "Abort[]" raise AbortInterrupt @@ -75,7 +81,7 @@ class Break(Builtin): summary_text = "exit a 'For', 'While', or 'Do' loop" - def apply(self, evaluation): + def eval(self, evaluation): "Break[]" raise BreakInterrupt @@ -116,7 +122,7 @@ class Catch(Builtin): summary_text = "handle an exception raised by a 'Throw'" - def apply_expr(self, expr, evaluation): + def eval_expr(self, expr, evaluation): "Catch[expr_]" try: ret = expr.evaluate(evaluation) @@ -124,7 +130,7 @@ def apply_expr(self, expr, evaluation): return e.value return ret - def apply_with_form_and_fn(self, expr, form, f, evaluation): + def eval_with_form_and_fn(self, expr, form, f, evaluation): "Catch[expr_, form_, f__:Identity]" try: ret = expr.evaluate(evaluation) @@ -196,7 +202,7 @@ class CompoundExpression(BinaryOperator): summary_text = "execute expressions in sequence" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation): "CompoundExpression[expr___]" items = expr.get_sequence() @@ -236,13 +242,13 @@ class Continue(Builtin): summary_text = "continue with the next iteration in a 'For', 'While' or 'Do' loop" - def apply(self, evaluation): + def eval(self, evaluation): "Continue[]" raise ContinueInterrupt -class Do(_IterationFunction): +class Do(IterationFunction): """ :WMA link:https://reference.wolfram.com/language/ref/Do.html @@ -330,7 +336,7 @@ class For(Builtin): } summary_text = "a 'For' loop" - def apply(self, start, test, incr, body, evaluation): + def eval(self, start, test, incr, body, evaluation): "For[start_, test_, incr_, body_]" while test.evaluate(evaluation) is SymbolTrue: evaluation.check_stopped() @@ -383,7 +389,7 @@ class If(Builtin): attributes = A_HOLD_REST | A_PROTECTED summary_text = "test if a condition is true, false, or of unknown truth value" - def apply_2(self, condition, t, evaluation): + def eval(self, condition, t, evaluation): "If[condition_, t_]" if condition is SymbolTrue: @@ -391,7 +397,7 @@ def apply_2(self, condition, t, evaluation): elif condition is SymbolFalse: return SymbolNull - def apply_3(self, condition, t, f, evaluation): + def eval_with_false(self, condition, t, f, evaluation): "If[condition_, t_, f_]" if condition is SymbolTrue: @@ -399,7 +405,7 @@ def apply_3(self, condition, t, f, evaluation): elif condition is SymbolFalse: return f.evaluate(evaluation) - def apply_4(self, condition, t, f, u, evaluation): + def eval_with_false_and_other(self, condition, t, f, u, evaluation): "If[condition_, t_, f_, u_]" if condition is SymbolTrue: @@ -425,7 +431,7 @@ class Interrupt(Builtin): summary_text = "interrupt evaluation and return '$Aborted'" - def apply(self, evaluation): + def eval(self, evaluation): "Interrupt[]" raise AbortInterrupt @@ -433,7 +439,8 @@ def apply(self, evaluation): class Return(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Return.html + :WMA link: + https://reference.wolfram.com/language/ref/Return.html
    'Return[$expr$]' @@ -474,7 +481,7 @@ class Return(Builtin): summary_text = "return from a function" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation): "Return[expr_]" raise ReturnInterrupt(expr) @@ -482,11 +489,13 @@ def apply(self, expr, evaluation): class Switch(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Switch.html + :WMA link: + https://reference.wolfram.com/language/ref/Switch.html
    'Switch[$expr$, $pattern1$, $value1$, $pattern2$, $value2$, ...]' -
    yields the first $value$ for which $expr$ matches the corresponding $pattern$. +
    yields the first $value$ for which $expr$ matches the corresponding \ + $pattern$.
    >> Switch[2, 1, x, 2, y, 3, z] @@ -522,7 +531,7 @@ class Switch(Builtin): summary_text = "switch based on a value, with patterns allowed" - def apply(self, expr, rules, evaluation): + def eval(self, expr, rules, evaluation): "Switch[expr_, rules___]" rules = rules.get_sequence() @@ -537,11 +546,14 @@ def apply(self, expr, rules, evaluation): class Which(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Which.html + + :WMA link: + https://reference.wolfram.com/language/ref/Which.html
    'Which[$cond1$, $expr1$, $cond2$, $expr2$, ...]' -
    yields $expr1$ if $cond1$ evaluates to 'True', $expr2$ if $cond2$ evaluates to 'True', etc. +
    yields $expr1$ if $cond1$ evaluates to 'True', $expr2$ if $cond2$ \ + evaluates to 'True', etc.
    >> n = 5; @@ -571,7 +583,7 @@ class Which(Builtin): attributes = A_HOLD_ALL | A_PROTECTED summary_text = "test which of a sequence of conditions are true" - def apply(self, items, evaluation): + def eval(self, items, evaluation): "Which[items___]" items = items.get_sequence() @@ -597,7 +609,8 @@ def apply(self, items, evaluation): class While(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/While.html + :WMA link: + https://reference.wolfram.com/language/ref/While.html
    'While[$test$, $body$]' @@ -623,7 +636,7 @@ class While(Builtin): "While[test_]": "While[test, Null]", } - def apply(self, test, body, evaluation): + def eval(self, test, body, evaluation): "While[test_, body_]" while test.evaluate(evaluation) is SymbolTrue: @@ -641,11 +654,13 @@ def apply(self, test, body, evaluation): class Throw(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Throw.html + :WMA link: + https://reference.wolfram.com/language/ref/Throw.html
    'Throw[`value`]' -
    stops evaluation and returns `value` as the value of the nearest enclosing 'Catch'. +
    stops evaluation and returns `value` as the value of the nearest \ + enclosing 'Catch'.
    'Catch[`value`, `tag`]'
    is caught only by `Catch[expr,form]`, where tag matches form. @@ -668,10 +683,10 @@ class Throw(Builtin): summary_text = "throw an expression to be caught by a surrounding 'Catch'" - def apply1(self, value, evaluation): + def eval1(self, value, evaluation): "Throw[value_]" raise WLThrowInterrupt(value) - def apply_with_tag(self, value, tag, evaluation): + def eval_with_tag(self, value, tag, evaluation): "Throw[value_, tag_]" raise WLThrowInterrupt(value, tag) diff --git a/mathics/builtin/quantities.py b/mathics/builtin/quantities.py index 40250f95c..20f24adbb 100644 --- a/mathics/builtin/quantities.py +++ b/mathics/builtin/quantities.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- - +""" +Units and Quantities +""" from pint import UnitRegistry @@ -12,24 +14,42 @@ A_READ_PROTECTED, ) from mathics.core.convert.expression import to_mathics_list +from mathics.core.convert.python import from_python +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol -from mathics.core.systemsymbols import SymbolRowBox +from mathics.core.systemsymbols import SymbolQuantity, SymbolRowBox -SymbolQuantity = Symbol("Quantity") +# This tells documentation how to sort this module +sort_order = "mathics.builtin.units-and-quantites" ureg = UnitRegistry() Q_ = ureg.Quantity +def get_converted_magnitude(magnitude_expr, evaluation: Evaluation) -> float: + """ + The Python "pint" library mixes in a Python numeric value as a multiplier inside + a Mathics Expression. here we pick out that multiplier and + convert it from a Python numeric to a Mathics numeric. + """ + magnitude_elements = list(magnitude_expr.elements) + magnitude_elements[1] = from_python(magnitude_elements[1]) + magnitude_expr._elements = tuple(magnitude_elements) + # FIXME: consider returning an int when that is possible + return magnitude_expr.evaluate(evaluation).get_float_value() + + class KnownUnitQ(Test): """ - :WMA link:https://reference.wolfram.com/language/ref/KnownUnitQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/KnownUnitQ.html
    -
    'KnownUnitQ[$unit$]' -
    returns True if $unit$ is a canonical unit, and False otherwise. +
    'KnownUnitQ[$unit$]' +
    returns True if $unit$ is a canonical unit, and False otherwise.
    >> KnownUnitQ["Feet"] @@ -39,9 +59,9 @@ class KnownUnitQ(Test): = False """ - summary_text = "check if its argument is a canonical unit." + summary_text = "tests whether its argument is a canonical unit." - def test(self, expr): + def test(self, expr) -> bool: def validate(unit): try: Q_(1, unit) @@ -55,13 +75,16 @@ def validate(unit): class Quantity(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Quantity.html + + :WMA link: + https://reference.wolfram.com/language/ref/Quantity.html
    -
    'Quantity[$magnitude$, $unit$]' -
    represents a quantity with size $magnitude$ and unit specified by $unit$. -
    'Quantity[$unit$]' -
    assumes the magnitude of the specified $unit$ to be 1. +
    'Quantity[$magnitude$, $unit$]' +
    represents a quantity with size $magnitude$ and unit specified by $unit$. + +
    'Quantity[$unit$]' +
    assumes the magnitude of the specified $unit$ to be 1.
    >> Quantity["Kilogram"] @@ -89,33 +112,35 @@ class Quantity(Builtin): messages = { "unkunit": "Unable to interpret unit specification `1`.", } - summary_text = "quantity with units" + summary_text = "represents a quantity with units" - def validate(self, unit, evaluation): + def validate(self, unit, evaluation: Evaluation): if KnownUnitQ(unit).evaluate(evaluation) is Symbol("False"): return False return True - def apply_makeboxes(self, mag, unit, f, evaluation): + def eval_makeboxes(self, mag, unit, f, evaluation: Evaluation): "MakeBoxes[Quantity[mag_, unit_String], f:StandardForm|TraditionalForm|OutputForm|InputForm]" - q_unit = unit.get_string_value().lower() + q_unit = unit.value.lower() if self.validate(unit, evaluation): - return Expression(SymbolRowBox, ListExpression(mag, " ", q_unit)) + return Expression( + SymbolRowBox, ListExpression(mag, String(" "), String(q_unit)) + ) else: return Expression( SymbolRowBox, to_mathics_list(SymbolQuantity, "[", mag, ",", q_unit, "]"), ) - def apply_n(self, mag, unit, evaluation): + def eval_n(self, mag, unit, evaluation: Evaluation): "Quantity[mag_, unit_String]" if self.validate(unit, evaluation): if mag.has_form("List", None): results = [] for i in range(len(mag.elements)): - quantity = Q_(mag.elements[i], unit.get_string_value().lower()) + quantity = Q_(mag.elements[i], unit.value.lower()) results.append( Expression( SymbolQuantity, quantity.magnitude, String(quantity.units) @@ -123,30 +148,33 @@ def apply_n(self, mag, unit, evaluation): ) return ListExpression(*results) else: - quantity = Q_(mag, unit.get_string_value().lower()) + quantity = Q_(mag, unit.value.lower()) return Expression( - "Quantity", quantity.magnitude, String(quantity.units) + SymbolQuantity, quantity.magnitude, String(quantity.units) ) else: - return evaluation.message("Quantity", "unkunit", unit) + evaluation.message("Quantity", "unkunit", unit) - def apply_1(self, unit, evaluation): + def eval(self, unit, evaluation: Evaluation): "Quantity[unit_]" if not isinstance(unit, String): - return evaluation.message("Quantity", "unkunit", unit) + evaluation.message("Quantity", "unkunit", unit) else: - return self.apply_n(Integer1, unit, evaluation) + return self.eval_n(Integer1, unit, evaluation) class QuantityMagnitude(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/QuantityMagnitude.html + + :WMA link: + https://reference.wolfram.com/language/ref/QuantityMagnitude.html
    -
    'QuantityMagnitude[$quantity$]' -
    gives the amount of the specified $quantity$. -
    'QuantityMagnitude[$quantity$, $unit$]' -
    gives the value corresponding to $quantity$ when converted to $unit$. +
    'QuantityMagnitude[$quantity$]' +
    gives the amount of the specified $quantity$. + +
    'QuantityMagnitude[$quantity$, $unit$]' +
    gives the value corresponding to $quantity$ when converted to $unit$.
    >> QuantityMagnitude[Quantity["Kilogram"]] @@ -178,9 +206,9 @@ class QuantityMagnitude(Builtin): = QuantityMagnitude[Quantity[3,mater]] """ - summary_text = "The magnitude associated to a quantity." + summary_text = "get magnitude associated with a quantity." - def apply(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation): "QuantityMagnitude[expr_]" def get_magnitude(elements): @@ -199,13 +227,13 @@ def get_magnitude(elements): else: return get_magnitude(expr.elements) - def apply_unit(self, expr, unit, evaluation): + def eval_unit(self, expr, unit, evaluation: Evaluation): "QuantityMagnitude[expr_, unit_]" - def get_magnitude(elements, targetUnit, evaluation): - quanity = Q_(elements[0], elements[1].get_string_value()) - converted_quantity = quanity.to(targetUnit) - q_mag = converted_quantity.magnitude.evaluate(evaluation).get_float_value() + def get_magnitude(elements, targetUnit, evaluation: Evaluation): + quantity = Q_(elements[0], elements[1].get_string_value()) + converted_quantity = quantity.to(targetUnit) + q_mag = get_converted_magnitude(converted_quantity.magnitude, evaluation) # Displaying the magnitude in Integer form if the convert rate is an Integer if q_mag - int(q_mag) > 0: @@ -243,10 +271,12 @@ def get_magnitude(elements, targetUnit, evaluation): class QuantityQ(Test): """ - :WMA link:https://reference.wolfram.com/language/ref/QuantityQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/QuantityQ.html
    -
    'QuantityQ[$expr$]' -
    return True if $expr$ is a valid Association object, and False otherwise. +
    'QuantityQ[$expr$]' +
    return True if $expr$ is a valid Association object, and False otherwise.
    >> QuantityQ[Quantity[3, "Meters"]] @@ -260,9 +290,9 @@ class QuantityQ(Test): = False """ - summary_text = "checks if the argument is a quantity" + summary_text = "tests whether its the argument is a quantity" - def test(self, expr): + def test(self, expr) -> bool: def validate_unit(unit): try: Q_(1, unit) @@ -288,16 +318,18 @@ def validate(elements): else: return False - return expr.get_head_name() == "System`Quantity" and validate(expr.elements) + return expr.get_head() == SymbolQuantity and validate(expr.elements) class QuantityUnit(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/QuantityUnit.html + + :WMA link: + https://reference.wolfram.com/language/ref/QuantityUnit.html
    -
    'QuantityUnit[$quantity$]' -
    returns the unit associated with the specified $quantity$. +
    'QuantityUnit[$quantity$]' +
    returns the unit associated with the specified $quantity$.
    >> QuantityUnit[Quantity["Kilogram"]] @@ -316,7 +348,7 @@ class QuantityUnit(Builtin): summary_text = "the unit associated to a quantity" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation): "QuantityUnit[expr_]" def get_unit(elements): @@ -339,13 +371,16 @@ def get_unit(elements): class UnitConvert(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/UnitConvert.html + + :WMA link: + https://reference.wolfram.com/language/ref/UnitConvert.html
    -
    'UnitConvert[$quantity$, $targetunit$] ' -
    converts the specified $quantity$ to the specified $targetunit$. -
    'UnitConvert[quantity]' -
    converts the specified $quantity$ to its "SIBase" units. +
    'UnitConvert[$quantity$, $targetunit$] ' +
    converts the specified $quantity$ to the specified $targetunit$. + +
    'UnitConvert[quantity]' +
    converts the specified $quantity$ to its "SIBase" units.
    Convert from miles to kilometers: @@ -373,25 +408,25 @@ class UnitConvert(Builtin): messages = { "argrx": "UnitConvert called with `1` arguments; 2 arguments are expected" } - summary_text = "Conversion between units." + summary_text = "convert between units." - def apply(self, expr, toUnit, evaluation): + def eval(self, expr, toUnit, evaluation: Evaluation): "UnitConvert[expr_, toUnit_]" - def convert_unit(leaves, target): + def convert_unit(elements, target): - mag = leaves[0] - unit = leaves[1].get_string_value() + mag = elements[0] + unit = elements[1].get_string_value() quantity = Q_(mag, unit) converted_quantity = quantity.to(target) - q_mag = converted_quantity.magnitude.evaluate(evaluation).get_float_value() + q_mag = get_converted_magnitude(converted_quantity.magnitude, evaluation) # Displaying the magnitude in Integer form if the convert rate is an Integer if q_mag - int(q_mag) > 0: - return Expression(SymbolQuantity, Real(q_mag), target) + return Expression(SymbolQuantity, Real(q_mag), String(target)) else: - return Expression(SymbolQuantity, Integer(q_mag), target) + return Expression(SymbolQuantity, Integer(q_mag), String(target)) if len(evaluation.out) > 0: return @@ -415,7 +450,7 @@ def convert_unit(leaves, target): else: return convert_unit(expr.elements, targetUnit) - def apply_base_unit(self, expr, evaluation): + def eval_base_unit(self, expr, evaluation: Evaluation): "UnitConvert[expr_]" def convert_unit(elements): @@ -426,8 +461,10 @@ def convert_unit(elements): quantity = Q_(mag, unit) converted_quantity = quantity.to_base_units() + mag = get_converted_magnitude(converted_quantity.magnitude, evaluation) + return Expression( - "Quantity", + SymbolQuantity, converted_quantity.magnitude, String(converted_quantity.units), ) diff --git a/mathics/builtin/quantum_mechanics/angular.py b/mathics/builtin/quantum_mechanics/angular.py index 5f7148a3d..b063c7081 100644 --- a/mathics/builtin/quantum_mechanics/angular.py +++ b/mathics/builtin/quantum_mechanics/angular.py @@ -1,7 +1,12 @@ """ Angular Momentum -:Angular momentum: https://en.wikipedia.org/wiki/Angular_momentum in physics is the rotational analog of linear momentum. It is an important quantity in physics because it is a conserved quantity the total angular momentum of a closed system remains constant. + +:Angular momentum: +https://en.wikipedia.org/wiki/Angular_momentum in physics \ +is the rotational analog of linear momentum. It is an important quantity \ +in physics because it is a conserved quantity the total angular momentum \ +of a closed system remains constant. """ from typing import List, Optional @@ -25,11 +30,18 @@ class ClebschGordan(SympyFunction): """ - :Clebsch-Gordan coefficients matrices: https://en.wikipedia.org/wiki/Clebsch%E2%80%93Gordan_coefficients (:SymPy: https://docs.sympy.org/latest/modules/physics/quantum/cg.html, :WMA: https://reference.wolfram.com/language/ref/ClebschGordan) + + :Clebsch-Gordan coefficients matrices: + https://en.wikipedia.org/wiki/Clebsch%E2%80%93Gordan_coefficients ( + :SymPy: + https://docs.sympy.org/latest/modules/physics/quantum/cg.html, + :WMA: + https://reference.wolfram.com/language/ref/ClebschGordan)
    'ClebschGordan[{$j1$, $m1$}, {$j2$, $m2$}, {$j$ $m$}]' -
    returns the Clebsch-Gordan coefficient for the decomposition of |$j$,$m$> in terms of |$j1$, $m$>, |$j2$, $m2$>. +
    returns the Clebsch-Gordan coefficient for the decomposition of |$j$,$m$> \ + in terms of |$j1$, $m$>, |$j2$, $m2$>.
    >> ClebschGordan[{3 / 2, 3 / 2}, {1 / 2, -1 / 2}, {1, 1}] @@ -52,7 +64,7 @@ class ClebschGordan(SympyFunction): summary_text = "Clebsch-Gordan coefficient" sympy_name = "physics.quantum.cg.CG" - def apply( + def eval( self, j1m1: ListExpression, j2m2: ListExpression, @@ -75,7 +87,13 @@ def apply( class PauliMatrix(SympyFunction): """ - :Pauli matrices: https://en.wikipedia.org/wiki/Pauli_matrices (:SymPy: https://docs.sympy.org/latest/modules/physics/matrices.html#sympy.physics.matrices.msigma, :WMA: https://reference.wolfram.com/language/ref/PauliMatrix.html) + + :Pauli matrices: + https://en.wikipedia.org/wiki/Pauli_matrices ( + :SymPy: + https://docs.sympy.org/latest/modules/physics/matrices.html#sympy.physics.matrices.msigma, + :WMA: + https://reference.wolfram.com/language/ref/PauliMatrix.html)
    'PauliMatrix[$k$]' @@ -103,7 +121,7 @@ class PauliMatrix(SympyFunction): summary_text = "Pauli spin matrix" sympy_name = "physics.matrices.msigma" - def apply(self, k: Integer, evaluation: Evaluation) -> Optional[Evaluation]: + def eval(self, k: Integer, evaluation: Evaluation) -> Optional[Evaluation]: "PauliMatrix[k_]" py_k = k.value if 0 <= py_k <= 4: @@ -116,7 +134,13 @@ def apply(self, k: Integer, evaluation: Evaluation) -> Optional[Evaluation]: class SixJSymbol(SympyFunction): """ - :6-j symbol: https://en.wikipedia.org/wiki/6-j_symbol (:SymPy: https://docs.sympy.org/latest/modules/physics/wigner.html#sympy.physics.wigner.wigner_6j, :WMA: https://reference.wolfram.com/language/ref/SixJSymbol.html) + + :6-j symbol: + https://en.wikipedia.org/wiki/6-j_symbol ( + :SymPy: + https://docs.sympy.org/latest/modules/physics/wigner.html#sympy.physics.wigner.wigner_6j, + :WMA: + https://reference.wolfram.com/language/ref/SixJSymbol.html)
    'SixJSymbol[{$j1, $j2$, $j3$}, {$j4$, $j5$, $j6$}]' @@ -166,7 +190,7 @@ class SixJSymbol(SympyFunction): summary_text = "values of the Wigner 6-j symbol" sympy_name = "physics.wigner.wigner_6j" - def apply(self, j13: ListExpression, j46: ListExpression, evaluation: Evaluation): + def eval(self, j13: ListExpression, j46: ListExpression, evaluation: Evaluation): "SixJSymbol[j13_List, j46_List]" sympy_js = [] i = 0 @@ -194,7 +218,13 @@ def apply(self, j13: ListExpression, j46: ListExpression, evaluation: Evaluation class ThreeJSymbol(SympyFunction): """ - :3-j symbol: https://en.wikipedia.org/wiki/3-j_symbol (:SymPy: https://docs.sympy.org/latest/modules/physics/wigner.html#sympy.physics.wigner.wigner_3j, :WMA: https://reference.wolfram.com/language/ref/ThreeJSymbol.html) + + :3-j symbol: + https://en.wikipedia.org/wiki/3-j_symbol ( + :SymPy: + https://docs.sympy.org/latest/modules/physics/wigner.html#sympy.physics.wigner.wigner_3j, + :WMA: + https://reference.wolfram.com/language/ref/ThreeJSymbol.html)
    'ThreeJSymbol[{$j1, $m1}, {$j2$, $m2$}, {$j3$, $m3$}]' @@ -242,7 +272,7 @@ class ThreeJSymbol(SympyFunction): summary_text = "values of the Wigner 3-j symbol" sympy_name = "physics.wigner.wigner_3j" - def apply( + def eval( self, j12: ListExpression, j34: ListExpression, diff --git a/mathics/builtin/recurrence.py b/mathics/builtin/recurrence.py index afc0cb0d3..f6c215b26 100644 --- a/mathics/builtin/recurrence.py +++ b/mathics/builtin/recurrence.py @@ -5,7 +5,8 @@ """ # This tells documentation how to sort this module -# Here we are also hiding "moments" since this erroneously appears at the top level. +# Here we are also hiding "moments" since this erroneously appears at the +# top level. sort_order = "mathics.builtin.solving-recurrence-equations" @@ -15,6 +16,7 @@ from mathics.core.atoms import IntegerM1 from mathics.core.attributes import A_CONSTANT from mathics.core.convert.sympy import from_sympy, sympy_symbol_prefix +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol, SymbolPlus, SymbolTimes @@ -23,7 +25,9 @@ class RSolve(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/RSolve.html + + :WMA link: + https://reference.wolfram.com/language/ref/RSolve.html
    'RSolve[$eqn$, $a$[$n$], $n$]' @@ -34,7 +38,7 @@ class RSolve(Builtin): >> RSolve[a[n] == a[n+1], a[n], n] = {{a[n] -> C[0]}} - No boundary conditions gives two general paramaters: + No boundary conditions gives two general parameters: >> RSolve[{a[n + 2] == a[n]}, a, n] = {{a -> (Function[{n}, C[0] + C[1] (-1) ^ n])}} @@ -63,7 +67,7 @@ class RSolve(Builtin): } summary_text = "recurrence equations solver" - def apply(self, eqns, a, n, evaluation): + def eval(self, eqns, a, n, evaluation: Evaluation): "RSolve[eqns_, a_, n_]" # TODO: Do this with rules? diff --git a/mathics/builtin/scipy_utils/integrators.py b/mathics/builtin/scipy_utils/integrators.py index 62f13cebd..72dde20fe 100644 --- a/mathics/builtin/scipy_utils/integrators.py +++ b/mathics/builtin/scipy_utils/integrators.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import sys from mathics.builtin.base import check_requires_list from mathics.core.util import IS_PYPY diff --git a/mathics/builtin/scoping.py b/mathics/builtin/scoping.py index bce039a1b..c5b79cadc 100644 --- a/mathics/builtin/scoping.py +++ b/mathics/builtin/scoping.py @@ -68,7 +68,9 @@ def dynamic_scoping(func, vars, evaluation: Evaluation): class Begin(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Begin.html + + :WMA link: + https://reference.wolfram.com/language/ref/Begin.html
    'Begin'[$context$] @@ -108,14 +110,17 @@ class Begin(Builtin): class BeginPackage(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/BeginPackage.html + + :WMA link: + https://reference.wolfram.com/language/ref/BeginPackage.html
    'BeginPackage'[$context$]
    starts the package given by $context$.
    - The $context$ argument must be a valid context name. 'BeginPackage' changes the values of '$Context' and '$ContextPath', setting the current context to $context$. + The $context$ argument must be a valid context name. 'BeginPackage' changes \ + the values of '$Context' and '$ContextPath', setting the current context to $context$. ## >> BeginPackage["test`"] ## = test` @@ -146,7 +151,8 @@ class Block(Builtin):
    'Block[{$x$, $y$, ...}, $expr$]' -
    temporarily removes the definitions of the given variables, evaluates $expr$, and restores the original definitions afterwards. +
    temporarily removes the definitions of the given variables, evaluates \ + $expr$, and restores the original definitions afterwards.
    'Block[{$x$=$x0$, $y$=$y0$, ...}, $expr$]'
    assigns temporary values to the variables during the evaluation of $expr$. @@ -191,7 +197,7 @@ class Block(Builtin): } summary_text = "evaluate an expression using local values for some given symbols" - def apply(self, vars, expr, evaluation): + def eval(self, vars, expr, evaluation: Evaluation): "Block[vars_, expr_]" vars = dict(get_scoping_vars(vars, "Block", evaluation)) @@ -244,7 +250,7 @@ class Contexts(Builtin): summary_text = "list all the defined contexts" - def apply(self, evaluation): + def eval(self, evaluation: Evaluation): "Contexts[]" contexts = set() @@ -256,7 +262,9 @@ def apply(self, evaluation): class ContextPath_(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/$ContextPath.html + + :WMA link + :https://reference.wolfram.com/language/ref/$ContextPath.html
    '$ContextPath'
    is the search path for contexts. @@ -284,11 +292,14 @@ class ContextPath_(Predefined): class ContextPathStack(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/ContextPathStack.html + + :WMA link: + https://reference.wolfram.com/language/ref/ContextPathStack.html
    'System`Private`$ContextPathStack' -
    is an internal variable tracking the values of '$ContextPath' saved by 'Begin' and 'BeginPackage'. +
    is an internal variable tracking the values of '$ContextPath' \ + saved by 'Begin' and 'BeginPackage'.
    """ @@ -303,7 +314,9 @@ class ContextPathStack(Builtin): class ContextStack(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/ContextStack.html + + :WMA link: + https://reference.wolfram.com/language/ref/ContextStack.html
    'System`Private`$ContextStack'
    is an internal variable tracking the values of '$Context' @@ -352,14 +365,17 @@ class End(Builtin): class EndPackage(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/EndPackage.html + + :WMA link: + https://reference.wolfram.com/language/ref/EndPackage.html
    'EndPackage[]'
    marks the end of a package, undoing a previous 'BeginPackage'.
    - After 'EndPackage', the values of '$Context' and '$ContextPath' at the time of the 'BeginPackage' call are restored, with the new package\'s context prepended to $ContextPath. + After 'EndPackage', the values of '$Context' and '$ContextPath' at the \ + time of the 'BeginPackage' call are restored, with the new package\'s context prepended to $ContextPath. """ messages = { @@ -385,11 +401,15 @@ class EndPackage(Builtin): class Module(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Module.html + + :WMA link: + https://reference.wolfram.com/language/ref/Module.html
    'Module[{$vars$}, $expr$]' -
    localizes variables by giving them a temporary name of the form 'name$number', where number is the current value of '$ModuleNumber'. Each time a module is evaluated, '$ModuleNumber' is incremented. +
    localizes variables by giving them a temporary name of the form \ + 'name$number', where number is the current value of '$ModuleNumber'. \ + Each time a module is evaluated, '$ModuleNumber' is incremented.
    ## FIXME: fix and go over @@ -435,7 +455,7 @@ class Module(Builtin): } summary_text = "generates symbols with names of the form x$nnn to represent each local variable." - def apply(self, vars, expr, evaluation): + def eval(self, vars, expr, evaluation: Evaluation): "Module[vars_, expr_]" scoping_vars = get_scoping_vars(vars, "Module", evaluation) @@ -456,7 +476,9 @@ def apply(self, vars, expr, evaluation): class ModuleNumber_(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/$ModuleNumber.html + + :WMA link: + https://reference.wolfram.com/language/ref/$ModuleNumber.html
    '$ModuleNumber'
    is the current "serial number" to be used for local module variables. @@ -466,7 +488,8 @@ class ModuleNumber_(Predefined):
    • '$ModuleNumber' is incremented every time 'Module' or 'Unique' is called.
    • a Mathics session starts with '$ModuleNumber' set to 1. -
    • You can reset $ModuleNumber to a positive machine integer, but if you do so, naming conflicts may lead to inefficiencies. +
    • You can reset $ModuleNumber to a positive machine integer, but if \ + you do so, naming conflicts may lead to inefficiencies.
    • ## Fixme: go over and adjuset @@ -499,7 +522,9 @@ class ModuleNumber_(Predefined): class Unique(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/Unique.html + + :WMA link: + https://reference.wolfram.com/language/ref/Unique.html
      'Unique[]' @@ -517,25 +542,16 @@ class Unique(Predefined): Create a unique symbol with no particular name: >> Unique[] - = $1 - - >> Unique[sym] - = sym$1 + = $... Create a unique symbol whose name begins with x: >> Unique["x"] - = x2 - - #> $3 = 3; - #> Unique[] - = $4 + = x... #> Unique[{}] = {} - #> Unique[{x, x}] - = {x$2, x$3} - + ## FIXME: include the rest of these in test/builtin/test-unique.py ## Each use of Unique[symbol] increments $ModuleNumber: ## >> {$ModuleNumber, Unique[x], $ModuleNumber} ## = ... @@ -588,7 +604,7 @@ class Unique(Predefined): seq_number = 1 summary_text = "generate a new symbols with a unique name" - def apply(self, evaluation): + def eval(self, evaluation: Evaluation): "Unique[]" new_name = "$%d" % (self.seq_number) @@ -599,14 +615,15 @@ def apply(self, evaluation): self.seq_number += 1 return Symbol(new_name) - def apply_symbol(self, vars, attributes, evaluation): + def eval_symbol(self, vars, attributes, evaluation: Evaluation): "Unique[vars_, attributes___]" from mathics.core.parser import is_symbol_name attributes = attributes.get_sequence() if len(attributes) > 1: - return evaluation.message("Unique", "argrx", Integer(len(attributes) + 1)) + evaluation.message("Unique", "argrx", Integer(len(attributes) + 1)) + return # Check valid symbol variables symbols = vars.elements if vars.has_form("List", None) else [vars] @@ -614,7 +631,8 @@ def apply_symbol(self, vars, attributes, evaluation): if not isinstance(symbol, Symbol): text = symbol.get_string_value() if text is None or not is_symbol_name(text): - return evaluation.message("Unique", "usym", symbol) + evaluation.message("Unique", "usym", symbol) + return # Check valid attributes attrs = [] @@ -652,11 +670,14 @@ def apply_symbol(self, vars, attributes, evaluation): class With(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/With.html + + :WMA link: + https://reference.wolfram.com/language/ref/With.html
      'With[{$x$=$x0$, $y$=$y0$, ...}, $expr$]' -
      specifies that all occurrences of the symbols $x$, $y$, ... in $expr$ should be replaced by $x0$, $y0$, ... +
      specifies that all occurrences of the symbols $x$, $y$, ... in \ + $expr$ should be replaced by $x0$, $y0$, ...
      ## >> n = 10 @@ -699,7 +720,7 @@ class With(Builtin): } summary_text = "replace variables by some constant values" - def apply(self, vars, expr, evaluation): + def eval(self, vars, expr, evaluation: Evaluation): "With[vars_, expr_]" vars = dict(get_scoping_vars(vars, "With", evaluation)) diff --git a/mathics/builtin/sparse.py b/mathics/builtin/sparse.py index 9ce476712..d2511fda7 100644 --- a/mathics/builtin/sparse.py +++ b/mathics/builtin/sparse.py @@ -1,32 +1,40 @@ # -*- coding: utf-8 -*- """ -SparseArray Functions +Sparse Array Functions """ -from mathics.algorithm.parts import walk_parts from mathics.builtin.base import Builtin from mathics.core.atoms import Integer, Integer0 +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol -from mathics.core.systemsymbols import SymbolAutomatic, SymbolRule, SymbolTable - -SymbolSparseArray = Symbol("SparseArray") +from mathics.core.systemsymbols import ( + SymbolAutomatic, + SymbolRule, + SymbolSparseArray, + SymbolTable, +) +from mathics.eval.parts import walk_parts class SparseArray(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/SparseArray.html + + :WMA link: + https://reference.wolfram.com/language/ref/SparseArray.html
      -
      'SparseArray[$rules$]' -
      Builds a sparse array acording to the list of $rules$. -
      'SparseArray[$rules$, $dims$]' -
      Builds a sparse array of dimensions $dims$ acording to the $rules$. -
      'SparseArray[$list$]' -
      Builds a sparse representation of $list$. +
      'SparseArray[$rules$]' +
      Builds a sparse array according to the list of $rules$. + +
      'SparseArray[$rules$, $dims$]' +
      Builds a sparse array of dimensions $dims$ according to the $rules$. + +
      'SparseArray[$list$]' +
      Builds a sparse representation of $list$.
      >> SparseArray[{{1, 2} -> 1, {2, 1} -> 1}] @@ -47,7 +55,7 @@ class SparseArray(Builtin): } summary_text = "an array by the values of the non-zero elements" - def list_to_sparse(self, array, evaluation): + def list_to_sparse(self, array, evaluation: Evaluation): # TODO: Simplify and modularize this method. elements = [] @@ -114,11 +122,11 @@ def list_to_sparse(self, array, evaluation): ListExpression(*rules), ) - def apply_dimensions(self, dims, default, data, evaluation): + def eval_dimensions(self, dims, default, data, evaluation: Evaluation): """System`Dimensions[System`SparseArray[System`Automatic, dims_List, default_, data_List]]""" return dims - def apply_normal(self, dims, default, data, evaluation): + def eval_normal(self, dims, default, data, evaluation: Evaluation): """System`Normal[System`SparseArray[System`Automatic, dims_List, default_, data_List]]""" its = [ListExpression(n) for n in dims.elements] table = Expression(SymbolTable, default, *its) @@ -130,7 +138,7 @@ def apply_normal(self, dims, default, data, evaluation): walk_parts([table], pos.elements, evaluation, val) return table - def find_dimensions(self, rules, evaluation): + def find_dimensions(self, rules, evaluation: Evaluation): dims = None for rule in rules: pos = rule.elements[0] @@ -146,7 +154,7 @@ def find_dimensions(self, rules, evaluation): return return ListExpression(*[Integer(d) for d in dims]) - def apply_1(self, rules, evaluation): + def eval_with_rules(self, rules, evaluation: Evaluation): """SparseArray[rules_List]""" if not (rules.has_form("List", None) and len(rules.elements) > 0): if rules is Symbol("Automatic"): @@ -164,13 +172,17 @@ def apply_1(self, rules, evaluation): dims = self.find_dimensions(rules.elements, evaluation) if dims is None: return - return self.apply_3(rules, dims, Integer0, evaluation) + return self.eval_with_rules_dims_and_default( + rules, dims, Integer0, evaluation + ) return self.list_to_sparse(rules, evaluation) - def apply_2(self, rules, dims, evaluation): + def eval_with_rules_and_dims(self, rules, dims, evaluation: Evaluation): """SparseArray[rules_List, dims_List]""" - return self.apply_3(rules, dims, Integer0, evaluation) + return self.eval_with_rules_dims_and_default(rules, dims, Integer0, evaluation) - def apply_3(self, rules, dims, default, evaluation): + def eval_with_rules_dims_and_default( + self, rules, dims, default, evaluation: Evaluation + ): """SparseArray[rules_List, dims_List, default_]""" return Expression(SymbolSparseArray, SymbolAutomatic, dims, default, rules) diff --git a/mathics/builtin/specialfns/bessel.py b/mathics/builtin/specialfns/bessel.py index 3a861eb9c..efd9de159 100644 --- a/mathics/builtin/specialfns/bessel.py +++ b/mathics/builtin/specialfns/bessel.py @@ -15,10 +15,11 @@ A_READ_PROTECTED, ) from mathics.core.convert.mpmath import from_mpmath +from mathics.core.evaluation import Evaluation from mathics.core.number import ( + FP_MANTISA_BINARY_DIGITS, PrecisionValueError, get_precision, - machine_precision, prec as _prec, ) @@ -32,7 +33,10 @@ class _Bessel(_MPMathFunction): class AiryAi(_MPMathFunction): """ - :Airy function of the first kind: https://en.wikipedia.org/wiki/Airy_function (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.airyai, :WMA: https://reference.wolfram.com/language/ref/AiryAi.html) + :Airy function of the first kind: + https://en.wikipedia.org/wiki/Airy_function ( + :SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.airyai, + :WMA: https://reference.wolfram.com/language/ref/AiryAi.html)
      'AiryAi[$x$]'
      returns the Airy function Ai($x$). @@ -63,7 +67,11 @@ class AiryAi(_MPMathFunction): class AiryAiPrime(_MPMathFunction): """ - Derivative of Airy function (:Sympy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.airyaiprime, :WMA link:https://reference.wolfram.com/language/ref/AiryAiPrime.html) + Derivative of Airy function ( + :Sympy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.airyaiprime, + :WMA link: + https://reference.wolfram.com/language/ref/AiryAiPrime.html)
      'AiryAiPrime[$x$]'
      returns the derivative of the Airy function 'AiryAi[$x$]'. @@ -132,7 +140,7 @@ class AiryAiZero(Builtin): summary_text = "kth zero of the Airy's function Ai" - def apply_N(self, k, precision, evaluation): + def eval_N(self, k, precision, evaluation: Evaluation): "N[AiryAiZero[k_Integer], precision_]" try: @@ -141,7 +149,7 @@ def apply_N(self, k, precision, evaluation): return if d is None: - p = machine_precision + p = FP_MANTISA_BINARY_DIGITS else: p = _prec(d) @@ -149,7 +157,7 @@ def apply_N(self, k, precision, evaluation): with mpmath.workprec(p): result = mpmath.airyaizero(k_int) - return from_mpmath(result, d) + return from_mpmath(result, precision=p) class AiryBi(_MPMathFunction): @@ -258,7 +266,7 @@ class AiryBiZero(Builtin): summary_text = "kth zero of the Airy's function Bi" - def apply_N(self, k: Integer, precision, evaluation): + def eval_N(self, k: Integer, precision, evaluation: Evaluation): "N[AiryBiZero[k_Integer], precision_]" try: @@ -267,7 +275,7 @@ def apply_N(self, k: Integer, precision, evaluation): return if d is None: - p = machine_precision + p = FP_MANTISA_BINARY_DIGITS else: p = _prec(d) @@ -275,12 +283,18 @@ def apply_N(self, k: Integer, precision, evaluation): with mpmath.workprec(p): result = mpmath.airybizero(k_int) - return from_mpmath(result, d) + return from_mpmath(result, precision=p) class AngerJ(_Bessel): """ - :Anger function: https://en.wikipedia.org/wiki/Anger_function (:mpmath: https://mpmath.org/doc/current/functions/bessel.html#mpmath.angerj, :WMA: https://reference.wolfram.com/language/ref/AngerJ.html) + + :Anger function: + https://en.wikipedia.org/wiki/Anger_function ( + :mpmath: + https://mpmath.org/doc/current/functions/bessel.html#mpmath.angerj, + :WMA: + https://reference.wolfram.com/language/ref/AngerJ.html)
      'AngerJ[$n$, $z$]'
      returns the Anger function J_$n$($z$). @@ -307,28 +321,43 @@ class BesselI(_Bessel): """ - :Modified Bessel function of the first kind: https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_first_kind:_J%CE%B1 (:Sympy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.besseli, :WMA: https://reference.wolfram.com/language/ref/BesselI.html) + + :Modified Bessel function of the first kind: + https://en.wikipedia.org/ + wiki/Bessel_function#Bessel_functions_of_the_first_kind:_J%CE%B1 ( + :Sympy: + https://docs.sympy.org/latest/modules/functions/ + special.html#sympy.functions.special.bessel.besseli, + :WMA: + https://reference.wolfram.com/language/ref/BesselI.html) -
      -
      'BesselI[$n$, $z$]' -
      returns the modified Bessel function of the first kind I_$n$($z$). -
      +
      +
      'BesselI[$n$, $z$]' +
      returns the modified Bessel function of the first kind I_$n$($z$). +
      - >> BesselI[1.5, 4] - = 8.17263 + >> BesselI[0, 0] + = 1 - >> Plot[BesselI[0, x], {x, 0, 5}] - = -Graphics- - """ + >> BesselI[1.5, 4] + = 8.17263 - rules = { - "Derivative[0, 1][BesselI]": "((BesselI[-1 + #1, #2] + BesselI[1 + #1, #2])/2)&", - } + >> Plot[BesselI[0, x], {x, 0, 5}] + = -Graphics- + + The special case of half-integer index is expanded using Rayleigh's formulas: + >> BesselI[3/2, x] + = Sqrt[2] Sqrt[x] (-Sinh[x] / x ^ 2 + Cosh[x] / x) / Sqrt[Pi] + """ mpmath_name = "besseli" rules = { "BesselI[Undefined, x_]": "Undefined", "BesselI[y_, Undefined]": "Undefined", + # FIXME: these are not respected. Why? + "BesselI[x_, -I Infinity]": "0", + "BesselI[x_, Infinity]": "0", + "Derivative[0, 1][BesselI]": "((BesselI[-1 + #1, #2] + BesselI[1 + #1, #2])/2)&", } sympy_name = "besseli" summary_text = "Bessel's function of the second kind" @@ -336,7 +365,13 @@ class BesselI(_Bessel): class BesselJ(_Bessel): """ - :Bessel function of the first kind: https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_first_kind:_J%CE%B1 (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.besselj, :WMA: https://reference.wolfram.com/language/ref/BesselJ.html) + + :Bessel function of the first kind: + https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_first_kind:_J%CE%B1 ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.besselj, + :WMA: + https://reference.wolfram.com/language/ref/BesselJ.html)
      'BesselJ[$n$, $z$]' @@ -352,17 +387,19 @@ class BesselJ(_Bessel): >> D[BesselJ[n, z], z] = -BesselJ[1 + n, z] / 2 + BesselJ[-1 + n, z] / 2 - #> BesselJ[0., 0.] + >> BesselJ[0., 0.] = 1. >> Plot[BesselJ[0, x], {x, 0, 10}] = -Graphics- - """ - # TODO: Sympy Backend is not as powerful as Mathematica - """ + The special case of half-integer index is expanded using Rayleigh's formulas: >> BesselJ[1/2, x] - = Sqrt[2 / Pi] Sin[x] / Sqrt[x] + = Sqrt[2] Sin[x] / (Sqrt[x] Sqrt[Pi]) + + Some integrals can be expressed in terms of Bessel functions: + >> Integrate[Cos[3 Sin[w]], {w, 0, Pi}] + = Pi BesselJ[0, 3] """ mpmath_name = "besselj" @@ -379,7 +416,13 @@ class BesselJ(_Bessel): class BesselK(_Bessel): """ - :Modified Bessel function of the second kind: https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions:_I%CE%B1,_K%CE%B1 (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.besselk, :WMA:https://reference.wolfram.com/language/ref/BesselJ.html) + + :Modified Bessel function of the second kind: + https://en.wikipedia.org/wiki/Bessel_function#Modified_Bessel_functions:_I%CE%B1,_K%CE%B1 ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.besselk, + :WMA: + https://reference.wolfram.com/language/ref/BesselJ.html)
      'BesselK[$n$, $z$]' @@ -391,6 +434,11 @@ class BesselK(_Bessel): >> Plot[BesselK[0, x], {x, 0, 5}] = -Graphics- + + The special case of half-integer index is expanded using Rayleigh's formulas: + >> BesselK[-3/2, x] + = Sqrt[2] Sqrt[x] Sqrt[Pi] (E ^ (-x) / x ^ 2 + E ^ (-x) / x) / 2 + """ mpmath_name = "besselk" @@ -407,8 +455,13 @@ class BesselK(_Bessel): class BesselY(_Bessel): """ - :Bessel function of the second kind: https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_second_kind:_Y%CE%B1 (:SymPy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.bessely, :WMA:https://reference.wolfram.com/language/ref/BesselY.html) - + + :Bessel function of the second kind: + https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_second_kind:_Y%CE%B1 ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.bessely, + :WMA: + https://reference.wolfram.com/language/ref/BesselY.html)
      'BesselY[$n$, $z$]' @@ -418,19 +471,20 @@ class BesselY(_Bessel): >> BesselY[1.5, 4] = 0.367112 - ## Returns ComplexInfinity instead - ## #> BesselY[0., 0.] - ## = -Infinity + >> BesselY[0., 0.] + = -Infinity >> Plot[BesselY[0, x], {x, 0, 10}] = -Graphics- - """ - # TODO: Special Values - """ + The special case of half-integer index is expanded using Rayleigh's formulas: + >> BesselY[-3/2, x] + = Sqrt[2] Sqrt[x] (-Sin[x] / x ^ 2 + Cos[x] / x) / Sqrt[Pi] + >> BesselY[0, 0] = -Infinity """ + rules = { "Derivative[0,1][BesselY]": "(BesselY[-1 + #1, #2] / 2 - BesselY[1 + #1, #2] / 2)&", } @@ -539,7 +593,13 @@ class HankelH2(_Bessel): class KelvinBei(_Bessel): """ - :Kelvin function bei: https://en.wikipedia.org/wiki/Kelvin_functions#bei(x) (:mpmath: https://mpmath.org/doc/current/functions/bessel.html#bei, :WMA: https://reference.wolfram.com/language/ref/KelvinBei.html) + + :Kelvin function bei: + https://en.wikipedia.org/wiki/Kelvin_functions#bei(x) ( + :mpmath: + https://mpmath.org/doc/current/functions/bessel.html#bei, + :WMA: + https://reference.wolfram.com/language/ref/KelvinBei.html)
      'KelvinBei[$z$]' @@ -574,7 +634,13 @@ class KelvinBei(_Bessel): class KelvinBer(_Bessel): """ - :Kelvin function ber: https://en.wikipedia.org/wiki/Kelvin_functions#ber(x) (:mpmath: https://mpmath.org/doc/current/functions/bessel.html#ber, :WMA: https://reference.wolfram.com/language/ref/KelvinBer.html) + + :Kelvin function ber: + https://en.wikipedia.org/wiki/Kelvin_functions#ber(x) ( + :mpmath: + https://mpmath.org/doc/current/functions/bessel.html#ber, + :WMA: + https://reference.wolfram.com/language/ref/KelvinBer.html)
      'KelvinBer[$z$]'
      returns the Kelvin function ber($z$). @@ -609,7 +675,13 @@ class KelvinBer(_Bessel): class KelvinKei(_Bessel): """ - :Kelvin function kei: https://en.wikipedia.org/wiki/Kelvin_functions#kei(x) (:mpmath: https://mpmath.org/doc/current/functions/bessel.html#kei, :WMA: https://reference.wolfram.com/language/ref/KelvinKei.html) + + :Kelvin function kei: + https://en.wikipedia.org/wiki/Kelvin_functions#kei(x) ( + :mpmath: + https://mpmath.org/doc/current/functions/bessel.html#kei, + :WMA: + https://reference.wolfram.com/language/ref/KelvinKei.html)
      'KelvinKei[$z$]' @@ -676,7 +748,13 @@ class KelvinKer(_Bessel): class SphericalBesselJ(_Bessel): """ - :Spherical Bessel function of the first kind: https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions (:Sympy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.jn, :WMA: https://reference.wolfram.com/language/ref/SphericalBesselJ.html) + + :Spherical Bessel function of the first kind: + https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions ( + :Sympy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.jn, + :WMA: + https://reference.wolfram.com/language/ref/SphericalBesselJ.html)
      'SphericalBesselJ[$n$, $z$]' @@ -699,7 +777,13 @@ class SphericalBesselJ(_Bessel): class SphericalBesselY(_Bessel): """ - :Spherical Bessel function of the first kind: https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions (:Sympy: https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.yn, :WMA: https://reference.wolfram.com/language/ref/SphericalBesselY.html) + + :Spherical Bessel function of the first kind: + https://en.wikipedia.org/wiki/Bessel_function#Spherical_Bessel_functions ( + :Sympy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.bessel.yn, + :WMA: + https://reference.wolfram.com/language/ref/SphericalBesselY.html)
      'SphericalBesselY[$n$, $z$]' diff --git a/mathics/builtin/specialfns/elliptic.py b/mathics/builtin/specialfns/elliptic.py index 23bf3b64f..0b9009462 100644 --- a/mathics/builtin/specialfns/elliptic.py +++ b/mathics/builtin/specialfns/elliptic.py @@ -1,8 +1,13 @@ """ Elliptic Integrals -In integral calculus, an :elliptic integral: https://en.wikipedia.org/wiki/Elliptic_integral is one of a number of related functions defined as the value of certain integral. Their name originates from their originally arising in connection with the problem of finding the arc length of an ellipse. These functions often are used in cryptography to encode and decode messages. +In integral calculus, an :elliptic integral: +https://en.wikipedia.org/wiki/Elliptic_integral is one of a number of \ +related functions defined as the value of certain integral. Their name \ +originates from their originally arising in connection with the problem of \ +finding the arc length of an ellipse. +These functions often are used in cryptography to encode and decode messages. """ import sympy @@ -17,7 +22,12 @@ class EllipticE(SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/EllipticE.html + + :Elliptic complete elliptic integral of the second kind: + https://en.wikipedia.org/wiki/Elliptic_integral#Complete_elliptic_integral_of_the_second_kind (:SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.elliptic_integrals.elliptic_e, + :WMA: + https://reference.wolfram.com/language/ref/EllipticE.html)
      'EllipticE[$m$]' @@ -46,30 +56,38 @@ class EllipticE(SympyFunction): summary_text = "elliptic integral of the second kind E(ϕ|m)" sympy_name = "elliptic_e" - def apply_default(self, args, evaluation): + def eval_default(self, args, evaluation): "%(name)s[args___]" evaluation.message("EllipticE", "argt", Integer(len(args.elements))) - def apply_m(self, m, evaluation): + def eval_m(self, m, evaluation): "%(name)s[m_]" sympy_arg = numerify(m, evaluation).to_sympy() try: return from_sympy(sympy.elliptic_e(sympy_arg)) - except: + except Exception: return - def apply_phi_m(self, phi, m, evaluation): + def eval_phi_m(self, phi, m, evaluation): "%(name)s[phi_, m_]" sympy_args = [numerify(a, evaluation).to_sympy() for a in (phi, m)] try: return from_sympy(sympy.elliptic_e(*sympy_args)) - except: + except Exception: return class EllipticF(SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/EllipticF.html + + :Complete elliptic integral of the first kind: + https://en.wikipedia.org/wiki/\ +Elliptic_integral#Complete_elliptic_integral_of_the_first_kind ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/\ +special.html#sympy.functions.special.elliptic_integrals.elliptic_f, + :WMA: + https://reference.wolfram.com/language/ref/EllipticF.html)
      'EllipticF[$phi$, $m$]' @@ -79,7 +97,7 @@ class EllipticF(SympyFunction): >> EllipticF[0.3, 0.8] = 0.303652 - EllipticF is zero when the firt argument is zero: + EllipticF is zero when the first argument is zero: >> EllipticF[0, 0.8] = 0 @@ -92,22 +110,27 @@ class EllipticF(SympyFunction): summary_text = "elliptic integral F(ϕ|m)" sympy_name = "elliptic_f" - def apply_default(self, args, evaluation): + def eval_default(self, args, evaluation): "%(name)s[args___]" evaluation.message("EllipticE", "argx", Integer(len(args.elements))) - def apply(self, phi, m, evaluation): + def eval(self, phi, m, evaluation): "%(name)s[phi_, m_]" sympy_args = [numerify(a, evaluation).to_sympy() for a in (phi, m)] try: return from_sympy(sympy.elliptic_f(*sympy_args)) - except: + except Exception: return class EllipticK(SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/EllipticK.html + + :Complete elliptic integral of the first kind: + https://en.wikipedia.org/wiki/Elliptic_integral#Complete_elliptic_integral_of_the_first_kind (:SymPy: + https://docs.sympy.org/latest/modules/functions/special.html, + :WMA: + https://reference.wolfram.com/language/ref/EllipticK.html)
      'EllipticK[$m$]' @@ -128,28 +151,33 @@ class EllipticK(SympyFunction): attributes = A_NUMERIC_FUNCTION | A_LISTABLE | A_PROTECTED messages = { - "argx": "EllipticE called with `` arguments; 1 argument is expected.", + "argx": "EllipticK called with `` arguments; 1 argument is expected.", } summary_text = "elliptic integral of the first kind K(m)" sympy_name = "elliptic_k" - def apply_default(self, args, evaluation): + def eval_default(self, args, evaluation): "%(name)s[args___]" evaluation.message("EllipticK", "argx", Integer(len(args.elements))) - def apply(self, m, evaluation): + def eval(self, m, evaluation): "%(name)s[m_]" args = numerify(m, evaluation).get_sequence() sympy_args = [a.to_sympy() for a in args] try: return from_sympy(sympy.elliptic_k(*sympy_args)) - except: + except Exception: return class EllipticPi(SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/EllipticPi.html + + :Complete elliptic integral of the third kind: + https://en.wikipedia.org/wiki/Elliptic_integral#Incomplete_elliptic_integral_of_the_third_kind (:SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.elliptic_integrals.elliptic_pi, + :WMA: + https://reference.wolfram.com/language/ref/EllipticPi.html)
      'EllipticPi[$n$, $m$]' @@ -172,20 +200,20 @@ class EllipticPi(SympyFunction): summary_text = "elliptic integral of the third kind P(n|m)" sympy_name = "elliptic_pi" - def apply_default(self, args, evaluation): + def eval_default(self, args, evaluation): "%(name)s[args___]" evaluation.message("EllipticPi", "argt", Integer(len(args.elements))) - def apply_n_m(self, n, m, evaluation): + def eval_n_m(self, n, m, evaluation): "%(name)s[n_, m_]" sympy_m = to_numeric_sympy_args(m, evaluation)[0] sympy_n = to_numeric_sympy_args(n, evaluation)[0] try: return from_sympy(sympy.elliptic_pi(sympy_m, sympy_n)) - except: + except Exception: return - def apply_n_phi_m(self, n, phi, m, evaluation): + def eval_n_phi_m(self, n, phi, m, evaluation): "%(name)s[n_, phi_, m_]" sympy_n = to_numeric_sympy_args(n, evaluation)[0] sympy_phi = to_numeric_sympy_args(m, evaluation)[0] @@ -193,5 +221,5 @@ def apply_n_phi_m(self, n, phi, m, evaluation): try: result = from_sympy(sympy.elliptic_pi(sympy_n, sympy_phi, sympy_m)) return result - except: + except Exception: return diff --git a/mathics/builtin/specialfns/gamma.py b/mathics/builtin/specialfns/gamma.py index 086f7a3d7..038b786fa 100644 --- a/mathics/builtin/specialfns/gamma.py +++ b/mathics/builtin/specialfns/gamma.py @@ -6,13 +6,9 @@ import mpmath import sympy -from mathics.builtin.arithmetic import ( - _MPMathFunction, - _MPMathMultiFunction, - call_mpmath, -) +from mathics.builtin.arithmetic import _MPMathFunction, _MPMathMultiFunction from mathics.builtin.base import PostfixOperator, SympyFunction -from mathics.core.atoms import Integer, Integer0, Integer1, Number +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 from mathics.core.convert.python import from_python @@ -20,20 +16,20 @@ from mathics.core.expression import Expression from mathics.core.number import dps, min_prec from mathics.core.symbols import Symbol, SymbolSequence -from mathics.core.systemsymbols import ( - SymbolAutomatic, - SymbolComplexInfinity, - SymbolDirectedInfinity, - SymbolGamma, - SymbolIndeterminate, -) +from mathics.core.systemsymbols import SymbolAutomatic, SymbolGamma +from mathics.eval.arithmetic import call_mpmath from mathics.eval.nevaluator import eval_N from mathics.eval.numerify import numerify class Beta(_MPMathMultiFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Beta.html + + :Euler beta function: + https://en.wikipedia.org/wiki/Beta_function (:SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.beta_functions.beta, + :WMA: + https://reference.wolfram.com/language/ref/Beta.html)
      'Beta[$a$, $b$]' @@ -75,8 +71,8 @@ def from_sympy(self, sympy_name, elements): else: return Expression(Symbol(self.get_name()), *elements) - # sympy does not handles Beta for integer arguments. - def apply_2(self, a, b, evaluation): + # SymPy does not handles Beta for integer arguments. + def eval(self, a, b, evaluation): """Beta[a_, b_]""" if not (a.is_numeric() and b.is_numeric()): return @@ -85,7 +81,7 @@ def apply_2(self, a, b, evaluation): gamma_a_plus_b = Expression(SymbolGamma, a + b) return gamma_a * gamma_b / gamma_a_plus_b - def apply_3(self, z, a, b, evaluation): + def eval_with_z(self, z, a, b, evaluation): """Beta[z_, a_, b_]""" # Here I needed to do that because the order of the arguments in WL # is different from the order in mpmath. Most of the code is the same @@ -107,17 +103,6 @@ def apply_3(self, z, a, b, evaluation): return result = call_mpmath(mpmath_function, tuple(float_args)) - if isinstance(result, (mpmath.mpc, mpmath.mpf)): - if mpmath.isinf(result) and isinstance(result, mpmath.mpc): - result = SymbolComplexInfinity - elif mpmath.isinf(result) and result > 0: - result = Expression(SymbolDirectedInfinity, Integer1) - elif mpmath.isinf(result) and result < 0: - result = Expression(SymbolDirectedInfinity, Integer(-1)) - elif mpmath.isnan(result): - result = SymbolIndeterminate - else: - result = from_mpmath(result) else: prec = min_prec(*args) d = dps(prec) @@ -126,9 +111,7 @@ def apply_3(self, z, a, b, evaluation): mpmath_args = [x.to_mpmath() for x in args] if None in mpmath_args: return - result = call_mpmath(mpmath_function, tuple(mpmath_args)) - if isinstance(result, (mpmath.mpc, mpmath.mpf)): - result = from_mpmath(result, d) + result = call_mpmath(mpmath_function, tuple(mpmath_args), prec) return result @@ -215,7 +198,7 @@ class Factorial2(PostfixOperator, _MPMathFunction): summary_text = "semi-factorial" options = {"Method": "Automatic"} - def apply(self, number, evaluation, options={}): + def eval(self, number, evaluation, options={}): "Factorial2[number_?NumberQ, OptionsPattern[%(name)s]]" try: @@ -247,7 +230,8 @@ def fact2_generic(x): convert_from_fn = from_sympy fact2_fn = getattr(sympy, self.sympy_name) else: - return evaluation.message("Factorial2", "unknownp", preference) + evaluation.message("Factorial2", "unknownp", preference) + return try: result = fact2_fn(number_arg) @@ -256,9 +240,8 @@ def fact2_generic(x): # Maybe an even negative number? Try generic routine if is_automatic and fact2_generic: return from_python(fact2_generic(number_arg)) - return evaluation.message( - "Factorial2", "ndf", preference, str(sys.exc_info()[1]) - ) + evaluation.message("Factorial2", "ndf", preference, str(sys.exc_info()[1])) + return return convert_from_fn(result) @@ -271,6 +254,10 @@ class Gamma(_MPMathMultiFunction): https://mpmath.org/doc/current/functions/gamma.html#gamma, :WMA:https://reference.wolfram.com/language/ref/Gamma.html) + The gamma function is one commonly used extension of the factorial function \ + applied to complex numbers, and is defined for all complex numbers except \ + the non-positive integers. +
      'Gamma[$z$]'
      is the gamma function on the complex number $z$. @@ -357,10 +344,11 @@ def from_sympy(self, sympy_name, elements): class LogGamma(_MPMathMultiFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/LogGamma.html - - In number theory the logarithm of the gamma function often appears. For positive real numbers, this can be evaluated as 'Log[Gamma[$z$]]'. - + :log-gamma function: + https://en.wikipedia.org/wiki/Gamma_function#The_log-gamma_function ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.gamma_functions.loggamma, + :WMA:https://reference.wolfram.com/language/ref/LogGamma.html)
      'LogGamma[$z$]'
      is the logarithm of the gamma function on the complex number $z$. @@ -402,22 +390,53 @@ def get_sympy_names(self): class Pochhammer(SympyFunction): """ - :WMA link:https://reference.wolfram.com/language/ref/Pochhammer.html + :Rising factorial: + https://en.wikipedia.org/wiki/Falling_and_rising_factorials ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/combinatorial.html#risingfactorial, + :WMA: + https://reference.wolfram.com/language/ref/Pochhammer.html) + + The Pochhammer symbol or rising factorial often appears in series \ + expansions for hypergeometric functions. - The Pochhammer symbol or rising factorial often appears in series expansions for hypergeometric functions. - The Pochammer symbol has a definie value even when the gamma functions which appear in its definition are infinite. + The Pochammer symbol has a definite value even when the gamma \ + functions which appear in its definition are infinite.
      'Pochhammer[$a$, $n$]' -
      is the Pochhammer symbol (a)_n. +
      is the Pochhammer symbol $a_n$.
      - >> Pochhammer[4, 8] - = 6652800 + Product of the first 3 numbers: + >> Pochhammer[1, 3] + = 6 + + 'Pochhammer[1, $n$]' is \ + the same as Pochhammer[2, $n$-1] since 1 is a multiplicative identity. + + >> Pochhammer[1, 3] == Pochhammer[2, 2] + = True + + Although sometimes 'Pochhammer[0, $n$]' is taken to be 1, in Mathics it is 0: + >> Pochhammer[0, n] + = 0 + + Pochhammer uses Gamma for non-Integer values of $n$: + + >> Pochhammer[1, 3.001] + = 6.00754 + + >> Pochhammer[1, 3.001] == Pochhammer[2, 2.001] + = True + + >> Pochhammer[1.001, 3] == 1.001 2.001 3.001 + = True """ attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED rules = { + "Pochhammer[0, n_]": "0", # Wikipedia says it should be 1 though. "Pochhammer[a_, n_]": "Gamma[a + n] / Gamma[a]", "Derivative[1,0][Pochhammer]": "(Pochhammer[#1, #2]*(-PolyGamma[0, #1] + PolyGamma[0, #1 + #2]))&", "Derivative[0,1][Pochhammer]": "(Pochhammer[#1, #2]*PolyGamma[0, #1 + #2])&", @@ -428,7 +447,12 @@ class Pochhammer(SympyFunction): class PolyGamma(_MPMathMultiFunction): r""" - :WMA link:https://reference.wolfram.com/language/ref/PolyGamma.html + :Polygamma function: + https://en.wikipedia.org/wiki/Polygamma_function ( + :SymPy: + https://docs.sympy.org/latest/modules/functions/special.html#sympy.functions.special.gamma_functions.polygamma, + :WMA: + https://reference.wolfram.com/language/ref/PolyGamma.html) PolyGamma is a meromorphic function on the complex numbers and is defined as a derivative of the logarithm of the gamma function.
      @@ -464,13 +488,17 @@ class PolyGamma(_MPMathMultiFunction): class StieltjesGamma(SympyFunction): - r""" - :WMA link:https://reference.wolfram.com/language/ref/StieltjesGamma.html + """ + :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, + :WMA: + https://reference.wolfram.com/language/ref/StieltjesGamma.html) - PolyGamma is a meromorphic function on the complex numbers and is defined as a derivative of the logarithm of the gamma function.
      'StieltjesGamma[$n$]' -
      returns the Stieljs contstant for $n$. +
      returns the Stieltjes constant for $n$.
      'StieltjesGamma[$n$, $a$]'
      gives the generalized Stieltjes constant of its parameters diff --git a/mathics/builtin/specialfns/zeta.py b/mathics/builtin/specialfns/zeta.py index 8fc4ea063..85059404c 100644 --- a/mathics/builtin/specialfns/zeta.py +++ b/mathics/builtin/specialfns/zeta.py @@ -40,7 +40,7 @@ def eval(self, z, s, a, evaluation): py_a = a.to_python() try: return from_mpmath(mpmath.lerchphi(py_z, py_s, py_a)) - except: + except Exception: pass # return sympy.expand_func(sympy.lerchphi(py_z, py_s, py_a)) diff --git a/mathics/builtin/statistics/base.py b/mathics/builtin/statistics/base.py new file mode 100644 index 000000000..8ec4fccf5 --- /dev/null +++ b/mathics/builtin/statistics/base.py @@ -0,0 +1,45 @@ +""" +Base classes for Descriptive Statistics +""" +from mathics.builtin.base import Builtin +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol + +# No user docs here. +no_doc = True + + +class NotRectangularException(Exception): + pass + + +class Rectangular(Builtin): + """ + A base class for statics builtin functions X that allow X[{a1, a2, ...}, {b1, b2, ...}, ...] + to be evaluated as + {X[{a1, b1, ...}, {a1, b2, ...}, ...]}. + """ + + no_doc = True + + def rect(self, element: ListExpression): + lengths = [len(element.elements) for element in element.elements] + if all(length == 0 for length in lengths): + return # leave as is, without error + + n_columns = lengths[0] + if any(length != n_columns for length in lengths[1:]): + raise NotRectangularException() + + transposed = [ + [element.elements[i] for element in element.elements] + for i in range(n_columns) + ] + + return ListExpression( + *[ + Expression(Symbol(self.get_name()), ListExpression(*items)) + for items in transposed + ], + ) diff --git a/mathics/builtin/statistics/dependency.py b/mathics/builtin/statistics/dependency.py index fdc0e75f3..899e77e12 100644 --- a/mathics/builtin/statistics/dependency.py +++ b/mathics/builtin/statistics/dependency.py @@ -10,26 +10,33 @@ sort_order = "mathics.builtin.special-moments" from mathics.builtin.base import Builtin -from mathics.builtin.lists import _NotRectangularException, _Rectangular +from mathics.builtin.statistics.base import NotRectangularException, Rectangular from mathics.core.atoms import Integer +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolDivide -from mathics.core.systemsymbols import SymbolDot, SymbolMean, SymbolSubtract - -SymbolConjugate = Symbol("Conjugate") -SymbolCovariance = Symbol("Covariance") +from mathics.core.systemsymbols import ( + SymbolConjugate, + SymbolCovariance, + SymbolDot, + SymbolMean, + SymbolStandardDeviation, + SymbolSubtract, + SymbolVariance, +) # Something is weird here. No System`. And we can't use what is in # SymbolSqrt from systemsymbols? SymbolSqrt = Symbol("Sqrt") -SymbolStandardDeviation = Symbol("StandardDeviation") -SymbolVariance = Symbol("Variance") - class Correlation(Builtin): """ - :Pearson correlation coefficient:https://en.wikipedia.org/wiki/Pearson_correlation_coefficient (:WMA: https://reference.wolfram.com/language/ref/Correlation.html) + + :Pearson correlation coefficient: + https://en.wikipedia.org/wiki/Pearson_correlation_coefficient ( + :WMA: + https://reference.wolfram.com/language/ref/Correlation.html)
      'Correlation[$a$, $b$]' @@ -48,7 +55,7 @@ class Correlation(Builtin): } summary_text = "Pearson's correlation of a pair of datasets" - def apply(self, a, b, evaluation): + def eval(self, a, b, evaluation: Evaluation): "Correlation[a_List, b_List]" if len(a.elements) != len(b.elements): @@ -65,7 +72,11 @@ def apply(self, a, b, evaluation): class Covariance(Builtin): """ - :Covariance: https://en.wikipedia.org/wiki/Covariance (:WMA: https://reference.wolfram.com/language/ref/Covariance.html) + + :Covariance: + https://en.wikipedia.org/wiki/Covariance ( + :WMA: + https://reference.wolfram.com/language/ref/Covariance.html)
      'Covariance[$a$, $b$]'
      computes the covariance between the equal-sized vectors $a$ and $b$. @@ -81,7 +92,7 @@ class Covariance(Builtin): } summary_text = "covariance matrix for a pair of datasets" - def apply(self, a, b, evaluation): + def eval(self, a, b, evaluation: Evaluation): "Covariance[a_List, b_List]" if len(a.elements) != len(b.elements): @@ -100,12 +111,17 @@ def apply(self, a, b, evaluation): ) -class StandardDeviation(_Rectangular): +class StandardDeviation(Rectangular): """ - :Standard deviation: https://en.wikipedia.org/wiki/Standard_deviation (:WMA: https://reference.wolfram.com/language/ref/StandardDeviation.html) + + :Standard deviation: + https://en.wikipedia.org/wiki/Standard_deviation ( + :WMA: + https://reference.wolfram.com/language/ref/StandardDeviation.html)
      'StandardDeviation[$list$]' -
      computes the standard deviation of $list. $list$ may consist of numerical values or symbols. Numerical values may be real or complex. +
      computes the standard deviation of $list. $list$ may consist of \ + numerical values or symbols. Numerical values may be real or complex. StandardDeviation[{{$a1$, $a2$, ...}, {$b1$, $b2$, ...}, ...}] will yield {StandardDeviation[{$a1$, $b1$, ...}, StandardDeviation[{$a2$, $b2$, ...}], ...}. @@ -130,24 +146,30 @@ class StandardDeviation(_Rectangular): } summary_text = "standard deviation of a dataset" - def apply(self, l, evaluation): - "StandardDeviation[l_List]" - if len(l.elements) <= 1: - evaluation.message("StandardDeviation", "shlen", l) - elif all(element.get_head_name() == "System`List" for element in l.elements): + def eval(self, li, evaluation: Evaluation): + "StandardDeviation[li_List]" + if len(li.elements) <= 1: + evaluation.message("StandardDeviation", "shlen", li) + elif all(element.get_head_name() == "System`List" for element in li.elements): try: - return self.rect(l) - except _NotRectangularException: + return self.rect(li) + except NotRectangularException: evaluation.message( - "StandardDeviation", "rectt", Expression(SymbolStandardDeviation, l) + "StandardDeviation", + "rectt", + Expression(SymbolStandardDeviation, li), ) else: - return Expression(SymbolSqrt, Expression(SymbolVariance, l)) + return Expression(SymbolSqrt, Expression(SymbolVariance, li)) -class Variance(_Rectangular): +class Variance(Rectangular): """ - :Variance: https://en.wikipedia.org/wiki/Variance (:WMA: https://reference.wolfram.com/language/ref/Variance.html) + + :Variance: + https://en.wikipedia.org/wiki/Variance ( + :WMA: + https://reference.wolfram.com/language/ref/Variance.html)
      'Variance[$list$]'
      computes the variance of $list. $list$ may consist of numerical values or symbols. Numerical values may be real or complex. @@ -180,21 +202,21 @@ class Variance(_Rectangular): # for the general formulation of real and complex variance below, see for example # https://en.wikipedia.org/wiki/Variance#Generalizations - def apply(self, l, evaluation): - "Variance[l_List]" - if len(l.elements) <= 1: - evaluation.message("Variance", "shlen", l) - elif all(element.get_head_name() == "System`List" for element in l.elements): + def eval(self, li, evaluation: Evaluation): + "Variance[li_List]" + if len(li.elements) <= 1: + evaluation.message("Variance", "shlen", li) + elif all(element.get_head_name() == "System`List" for element in li.elements): try: - return self.rect(l) - except _NotRectangularException: - evaluation.message("Variance", "rectt", Expression(SymbolVariance, l)) + return self.rect(li) + except NotRectangularException: + evaluation.message("Variance", "rectt", Expression(SymbolVariance, li)) else: - d = Expression(SymbolSubtract, l, Expression(SymbolMean, l)) + d = Expression(SymbolSubtract, li, Expression(SymbolMean, li)) return Expression( SymbolDivide, Expression(SymbolDot, d, Expression(SymbolConjugate, d)), - Integer(len(l.elements) - 1), + Integer(len(li.elements) - 1), ) diff --git a/mathics/builtin/statistics/general.py b/mathics/builtin/statistics/general.py index debcc14b5..1ad3726da 100644 --- a/mathics/builtin/statistics/general.py +++ b/mathics/builtin/statistics/general.py @@ -12,7 +12,11 @@ class CentralMoment(Builtin): """ - :Central moment: https://en.wikipedia.org/wiki/Central_moment (:WMA: https://reference.wolfram.com/language/ref/CentralMoment.html) + + :Central moment: + https://en.wikipedia.org/wiki/Central_moment ( + :WMA: + https://reference.wolfram.com/language/ref/CentralMoment.html)
      'CentralMoment[$list$, $r$]' @@ -43,7 +47,7 @@ class CentralMoment(Builtin): # summary_text = "moment of distributions and data" # sympy_name = "Moment" -# def apply_sample_r(self, sample, r, evaluation): +# def eval_sample_r(self, sample, r, evaluation: Evaluation): # "%(name)s[sample_List, r_]" # sympy_sample = sample.to_sympy() # sympy_r = r.to_sympy() diff --git a/mathics/builtin/statistics/location.py b/mathics/builtin/statistics/location.py index 4225cbdab..2999289a1 100644 --- a/mathics/builtin/statistics/location.py +++ b/mathics/builtin/statistics/location.py @@ -4,15 +4,19 @@ from mathics.algorithm.introselect import introselect from mathics.builtin.base import Builtin -from mathics.builtin.lists import _NotRectangularException, _Rectangular +from mathics.builtin.statistics.base import NotRectangularException, Rectangular from mathics.core.atoms import Integer2 +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.symbols import Symbol, SymbolDivide, SymbolPlus +from mathics.core.symbols import SymbolDivide, SymbolPlus +from mathics.core.systemsymbols import SymbolMedian class Mean(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Mean.html + + :WMA link: + https://reference.wolfram.com/language/ref/Mean.html
      'Mean[$list$]' @@ -35,12 +39,11 @@ class Mean(Builtin): } -SymbolMedian = Symbol("Median") - - -class Median(_Rectangular): +class Median(Rectangular): """ - :WMA link:https://reference.wolfram.com/language/ref/Median.html + + :WMA link: + https://reference.wolfram.com/language/ref/Median.html
      'Median[$list$]' @@ -62,14 +65,14 @@ class Median(_Rectangular): messages = {"rectn": "Expected a rectangular array of numbers at position 1 in ``."} summary_text = "central value of a dataset" - def apply(self, data, evaluation): + def eval(self, data, evaluation: Evaluation): "Median[data_List]" if not data.elements: return if all(element.get_head_name() == "System`List" for element in data.elements): try: return self.rect(data) - except _NotRectangularException: + except NotRectangularException: evaluation.message("Median", "rectn", Expression(SymbolMedian, data)) elif all(element.is_numeric(evaluation) for element in data.elements): v = data.get_mutable_elements() # copy needed for introselect diff --git a/mathics/builtin/statistics/orderstats.py b/mathics/builtin/statistics/orderstats.py index 2ed6712e1..5a8f63f0c 100644 --- a/mathics/builtin/statistics/orderstats.py +++ b/mathics/builtin/statistics/orderstats.py @@ -1,20 +1,25 @@ """ Order Statistics -In statistics, an :order statistic: https://en.wikipedia.org/wiki/Order_statistic gives the $k$-th smmallest value. +In statistics, an :order statistic: +https://en.wikipedia.org/wiki/Order_statistic gives \ +the $k$-th smallest value. -Together with :rank statistics: https://en.wikipedia.org/wiki/Ranking these are fundamental tools in non-parametric statistics and inference. +Together with :rank statistics: +https://en.wikipedia.org/wiki/Ranking these are \ +fundamental tools in non-parametric statistics and inference. -Important special cases of order statistics are finding minimum and maximum value of a sample and sample quantiles. +Important special cases of order statistics are finding \ +minimum and maximum value of a sample and sample quantiles. """ from mpmath import ceil as mpceil, floor as mpfloor from mathics.algorithm.introselect import introselect from mathics.builtin.base import Builtin -from mathics.builtin.lists import _RankedTakeLargest, _RankedTakeSmallest +from mathics.builtin.list.math import _RankedTakeLargest, _RankedTakeSmallest from mathics.core.atoms import Atom, Integer, Symbol, SymbolTrue -from mathics.core.expression import Expression +from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression from mathics.core.symbols import SymbolFloor, SymbolPlus, SymbolTimes from mathics.core.systemsymbols import SymbolSubtract @@ -27,8 +32,15 @@ class Quantile(Builtin): """ - :Quantile: https://en.wikipedia.org/wiki/Quantile (:WMA: https://reference.wolfram.com/language/ref/Quantile.html) - In statistics and probability, quantiles are cut points dividing the range of a probability distribution into continuous intervals with equal probabilities, or dividing the observations in a sample in the same way. + + :Quantile: + https://en.wikipedia.org/wiki/Quantile ( + :WMA: + https://reference.wolfram.com/language/ref/Quantile.html) + + In statistics and probability, quantiles are cut points dividing the \ + range of a probability distribution into continuous intervals with \ + equal probabilities, or dividing the observations in a sample in the same way. Quantile is also known as value at risk (VaR) or fractile.
      @@ -42,7 +54,9 @@ class Quantile(Builtin): If $x$ is an integer, the result is '$s$[[$x$]]', where $s$='Sort[list,Less]'. - Otherwise, the result is 's[[Floor[x]]]+(s[[Ceiling[x]]]-s[[Floor[x]]])(c+dFractionalPart[x])', with the indices taken to be 1 or n if they are out of range. + Otherwise, the result is \ + 's[[Floor[x]]]+(s[[Ceiling[x]]]-s[[Floor[x]]])(c+dFractionalPart[x])', \ + with the indices taken to be 1 or n if they are out of range. The default choice of parameters is '{{0,0},{1,0}}'.
      @@ -75,7 +89,7 @@ class Quantile(Builtin): } summary_text = "cut points dividing the range of a probability distribution into continuous intervals" - def apply(self, data, qs, a, b, c, d, evaluation): + def eval(self, data, qs, a, b, c, d, evaluation: Evaluation): """Quantile[data_List, qs_List, {{a_, b_}, {c_, d_}}]""" n = len(data.elements) @@ -141,7 +155,10 @@ def ranked(i): class Quartiles(Builtin): """ - :Quartile: https://en.wikipedia.org/wiki/Quartile (:WMA: https://reference.wolfram.com/language/ref/Quartiles.html) + :Quartile: + https://en.wikipedia.org/wiki/Quartile ( + :WMA: + https://reference.wolfram.com/language/ref/Quartiles.html)
      'Quartiles[$list$]'
      returns the 1/4, 1/2, and 3/4 quantiles of $list$. @@ -177,7 +194,7 @@ class RankedMax(Builtin): } summary_text = "the n-th largest item" - def apply(self, element, n: Integer, evaluation): + def eval(self, element, n: Integer, evaluation: Evaluation): "RankedMax[element_List, n_Integer]" py_n = n.value if py_n < 1: @@ -194,11 +211,14 @@ def apply(self, element, n: Integer, evaluation): class RankedMin(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/RankedMin.html + :WMA link: + https://reference.wolfram.com/language/ref/RankedMin.html
      'RankedMin[$list$, $n$]' -
      returns the $n$th smallest element of $list$ (with $n$ = 1 yielding the smallest element, $n$ = 2 yielding the second smallest element, and so on). +
      returns the $n$th smallest element of $list$ (with \ + $n$ = 1 yielding the smallest element, $n$ = 2 yielding \ + the second smallest element, and so on).
      >> RankedMin[{482, 17, 181, -12}, 2] @@ -211,7 +231,7 @@ class RankedMin(Builtin): } summary_text = "the n-th smallest item" - def apply(self, element, n: Integer, evaluation): + def eval(self, element, n: Integer, evaluation: Evaluation): "RankedMin[element_List, n_Integer]" py_n = n.value if py_n < 1: @@ -230,7 +250,8 @@ class Sort(Builtin):
      'Sort[$list$]' -
      sorts $list$ (or the elements of any other expression) according to canonical ordering. +
      sorts $list$ (or the elements of any other expression) according \ + to canonical ordering.
      'Sort[$list$, $p$]'
      sorts using $p$ to determine the order of two elements. @@ -258,7 +279,7 @@ class Sort(Builtin): summary_text = "sort lexicographically or with any comparison function" - def apply(self, list, evaluation): + def eval(self, list, evaluation: Evaluation): "Sort[list_]" if isinstance(list, Atom): @@ -267,7 +288,7 @@ def apply(self, list, evaluation): new_elements = sorted(list.elements) return list.restructure(list.head, new_elements, evaluation) - def apply_predicate(self, list, p, evaluation): + def eval_predicate(self, list, p, evaluation: Evaluation): "Sort[list_, p_]" if isinstance(list, Atom): @@ -292,7 +313,9 @@ def __gt__(self, other): class TakeLargest(_RankedTakeLargest): """ - :WMA link:https://reference.wolfram.com/language/ref/TakeLargest.html + + :WMA link: + https://reference.wolfram.com/language/ref/TakeLargest.html
      'TakeLargest[$list$, $f$, $n$]' @@ -314,7 +337,7 @@ class TakeLargest(_RankedTakeLargest): summary_text = "sublist of n largest elements" - def apply(self, element, n, evaluation, options): + def eval(self, element, n, evaluation, options): "TakeLargest[element_List, n_, OptionsPattern[TakeLargest]]" return self._compute(element, n, evaluation, options) @@ -336,7 +359,7 @@ class TakeSmallest(_RankedTakeSmallest): summary_text = "sublist of n smallest elements" - def apply(self, element, n, evaluation, options): + def eval(self, element, n, evaluation, options): "TakeSmallest[element_List, n_, OptionsPattern[TakeSmallest]]" return self._compute(element, n, evaluation, options) diff --git a/mathics/builtin/string/characters.py b/mathics/builtin/string/characters.py index 778266b2c..37e0b9a70 100644 --- a/mathics/builtin/string/characters.py +++ b/mathics/builtin/string/characters.py @@ -8,12 +8,15 @@ from mathics.core.atoms import String from mathics.core.attributes import A_LISTABLE, A_PROTECTED, A_READ_PROTECTED from mathics.core.convert.expression import to_mathics_list +from mathics.core.evaluation import Evaluation from mathics.core.list import ListExpression class Characters(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Characters.html + + :WMA link: + https://reference.wolfram.com/language/ref/Characters.html
      'Characters["$string$"]' @@ -39,7 +42,7 @@ class Characters(Builtin): attributes = A_LISTABLE | A_PROTECTED summary_text = "list the characters in a string" - def apply(self, string, evaluation): + def eval(self, string, evaluation: Evaluation): "Characters[string_String]" return to_mathics_list(*string.value, elements_conversion_fn=String) @@ -47,7 +50,9 @@ def apply(self, string, evaluation): class CharacterRange(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/CharacterRange.html + + :WMA link: + https://reference.wolfram.com/language/ref/CharacterRange.html
      'CharacterRange["$a$", "$b$"]' @@ -68,7 +73,7 @@ class CharacterRange(Builtin): summary_text = "range of characters with successive character codes" - def apply(self, start, stop, evaluation): + def eval(self, start, stop, evaluation: Evaluation): "CharacterRange[start_String, stop_String]" if len(start.value) != 1 or len(stop.value) != 1: @@ -81,11 +86,14 @@ def apply(self, start, stop, evaluation): class DigitQ(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/DigitQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/DigitQ.html
      'DigitQ[$string$]' -
      yields 'True' if all the characters in the $string$ are digits, and yields 'False' otherwise. +
      yields 'True' if all the characters in the $string$ are \ + digits, and yields 'False' otherwise.
      @@ -113,11 +121,14 @@ class DigitQ(Builtin): class LetterQ(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/LetterQ.html + + :WMA link: + https://reference.wolfram.com/language/ref/LetterQ.html
      'LetterQ[$string$]' -
      yields 'True' if all the characters in the $string$ are letters, and yields 'False' otherwise. +
      yields 'True' if all the characters in the $string$ are \ + letters, and yields 'False' otherwise.
      >> LetterQ["m"] @@ -166,7 +177,7 @@ class LowerCaseQ(Test): summary_text = "test wether all the characters are lower-case letters" - def test(self, s): + def test(self, s) -> bool: return isinstance(s, String) and all(c.islower() for c in s.get_string_value()) @@ -186,7 +197,7 @@ class ToLowerCase(Builtin): attributes = A_LISTABLE | A_PROTECTED summary_text = "turn all the letters into lower case" - def apply(self, s, evaluation): + def eval(self, s, evaluation: Evaluation): "ToLowerCase[s_String]" return String(s.get_string_value().lower()) @@ -207,7 +218,7 @@ class ToUpperCase(Builtin): attributes = A_LISTABLE | A_PROTECTED summary_text = "turn all the letters into upper case" - def apply(self, s, evaluation): + def eval(self, s, evaluation: Evaluation): "ToUpperCase[s_String]" return String(s.get_string_value().upper()) @@ -229,7 +240,7 @@ class UpperCaseQ(Test): = True """ - summary_text = "test wether all the characters are upper-case letters" + summary_text = "test whether all the characters are upper-case letters" - def test(self, s): + def test(self, s) -> bool: return isinstance(s, String) and all(c.isupper() for c in s.get_string_value()) diff --git a/mathics/builtin/string/charcodes.py b/mathics/builtin/string/charcodes.py index 09cdaee6a..54a022f8a 100644 --- a/mathics/builtin/string/charcodes.py +++ b/mathics/builtin/string/charcodes.py @@ -9,12 +9,13 @@ from mathics.builtin.base import Builtin from mathics.core.atoms import Integer, Integer1, String from mathics.core.convert.expression import to_mathics_list +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol -SymbolFromCharacterCode = Symbol("FromCharacterCode") -SymbolToCharacterCode = Symbol("ToCharacterCode") +SymbolFromCharacterCode = Symbol("System`FromCharacterCode") +SymbolToCharacterCode = Symbol("System`ToCharacterCode") def pack_bytes(codes): @@ -27,7 +28,9 @@ def unpack_bytes(codes): class ToCharacterCode(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/ToCharacterCode.html + + :WMA link: + https://reference.wolfram.com/language/ref/ToCharacterCode.html
      'ToCharacterCode["$string$"]' @@ -81,7 +84,7 @@ class ToCharacterCode(Builtin): } summary_text = "convert a string to a list of character codes" - def _encode(self, string, encoding, evaluation): + def _encode(self, string, encoding, evaluation: Evaluation): exp = Expression(SymbolToCharacterCode, string) if string.has_form("List", None): @@ -118,13 +121,13 @@ def convert(s): elif isinstance(string, str): return convert(string) - def apply_default(self, string, evaluation): + def eval_default(self, string, evaluation: Evaluation): "ToCharacterCode[string_]" return self._encode(string, "Unicode", evaluation) - def apply(self, string, encoding, evaluation): + def eval(self, string, encoding: String, evaluation: Evaluation): "ToCharacterCode[string_, encoding_String]" - return self._encode(string, encoding.get_string_value(), evaluation) + return self._encode(string, encoding.value, evaluation) class _InvalidCodepointError(ValueError): @@ -133,15 +136,19 @@ class _InvalidCodepointError(ValueError): class FromCharacterCode(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/FromCharacterCode.html + + :WMA link: + https://reference.wolfram.com/language/ref/FromCharacterCode.html
      -
      'FromCharacterCode[$n$]' -
      returns the character corresponding to Unicode codepoint $n$. -
      'FromCharacterCode[{$n1$, $n2$, ...}]' -
      returns a string with characters corresponding to $n_i$. -
      'FromCharacterCode[{{$n11$, $n12$, ...}, {$n21$, $n22$, ...}, ...}]' -
      returns a list of strings. +
      'FromCharacterCode[$n$]' +
      returns the character corresponding to Unicode codepoint $n$. + +
      'FromCharacterCode[{$n1$, $n2$, ...}]' +
      returns a string with characters corresponding to $n_i$. + +
      'FromCharacterCode[{{$n11$, $n12$, ...}, {$n21$, $n22$, ...}, ...}]' +
      returns a list of strings.
      >> FromCharacterCode[100] @@ -210,7 +217,7 @@ class FromCharacterCode(Builtin): } summary_text = "convert from a list of character codes to a string" - def _decode(self, n, encoding, evaluation): + def _decode(self, n, encoding: str, evaluation: Evaluation): exp = Expression(SymbolFromCharacterCode, n) py_encoding = to_python_encoding(encoding) @@ -258,9 +265,8 @@ def convert_codepoint_list(li): else: pyn = n.get_int_value() if not (isinstance(pyn, int) and pyn > 0 and pyn < sys.maxsize): - return evaluation.message( - "FromCharacterCode", "intnm", exp, Integer1 - ) + evaluation.message("FromCharacterCode", "intnm", exp, Integer1) + return return String(convert_codepoint_list([n])) except _InvalidCodepointError: return @@ -270,10 +276,10 @@ def convert_codepoint_list(li): assert False, "can't get here" - def apply_default(self, n, evaluation): + def eval_default(self, n, evaluation: Evaluation): "FromCharacterCode[n_]" return self._decode(n, "Unicode", evaluation) - def apply(self, n, encoding, evaluation): + def eval(self, n, encoding: String, evaluation: Evaluation): "FromCharacterCode[n_, encoding_String]" - return self._decode(n, encoding.get_string_value(), evaluation) + return self._decode(n, encoding.value, evaluation) diff --git a/mathics/builtin/string/operations.py b/mathics/builtin/string/operations.py index bc3eca85d..96d604443 100644 --- a/mathics/builtin/string/operations.py +++ b/mathics/builtin/string/operations.py @@ -4,11 +4,8 @@ Operations on Strings """ -import hashlib import re -import zlib -from mathics.algorithm.parts import convert_seq, python_seq from mathics.builtin.atomic.strings import ( _evaluate_match, _parallel_match, @@ -17,7 +14,7 @@ to_regex, ) from mathics.builtin.base import BinaryOperator, Builtin -from mathics.core.atoms import ByteArrayAtom, Integer, Integer1, String +from mathics.core.atoms import Integer, Integer1, String from mathics.core.attributes import ( A_FLAT, A_LISTABLE, @@ -26,120 +23,29 @@ A_READ_PROTECTED, ) from mathics.core.convert.python import from_python -from mathics.core.expression import Expression, string_list +from mathics.core.evaluation import Evaluation +from mathics.core.expression import BoxError, Expression, string_list +from mathics.core.expression_predefined import MATHICS3_INFINITY from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol, SymbolFalse, SymbolList, SymbolTrue +from mathics.core.symbols import SymbolFalse, SymbolFullForm, SymbolList, SymbolTrue from mathics.core.systemsymbols import ( SymbolAll, - SymbolByteArray, - SymbolDirectedInfinity, SymbolOutputForm, + SymbolStringInsert, + SymbolStringJoin, + SymbolStringPosition, + SymbolStringRiffle, + SymbolStringSplit, ) from mathics.eval.makeboxes import format_element - -SymbolStringInsert = Symbol("StringInsert") -SymbolStringJoin = Symbol("StringJoin") -SymbolStringPosition = Symbol("StringPosition") -SymbolStringRiffle = Symbol("StringRiffle") -SymbolStringSplit = Symbol("StringSplit") - - -class _ZLibHash: # make zlib hashes behave as if they were from hashlib - def __init__(self, fn): - self._bytes = b"" - self._fn = fn - - def update(self, bytes): - self._bytes += bytes - - def hexdigest(self): - return format(self._fn(self._bytes), "x") - - -class Hash(Builtin): - """ - :Hash function:https://en.wikipedia.org/wiki/Hash_function \ - (:WMA link:https://reference.wolfram.com/language/ref/Hash.html) - -
      -
      'Hash[$expr$]' -
      returns an integer hash for the given $expr$. - -
      'Hash[$expr$, $type$]' -
      returns an integer hash of the specified $type$ for the given $expr$. -
      The types supported are "MD5", "Adler32", "CRC32", "SHA", "SHA224", "SHA256", "SHA384", and "SHA512". - -
      'Hash[$expr$, $type$, $format$]' -
      Returns the hash in the specified format. -
      - - > Hash["The Adventures of Huckleberry Finn"] - = 213425047836523694663619736686226550816 - - > Hash["The Adventures of Huckleberry Finn", "SHA256"] - = 95092649594590384288057183408609254918934351811669818342876362244564858646638 - - > Hash[1/3] - = 56073172797010645108327809727054836008 - - > Hash[{a, b, {c, {d, e, f}}}] - = 135682164776235407777080772547528225284 - - > Hash[SomeHead[3.1415]] - = 58042316473471877315442015469706095084 - - >> Hash[{a, b, c}, "xyzstr"] - = Hash[{a, b, c}, xyzstr, Integer] - """ - - attributes = A_PROTECTED | A_READ_PROTECTED - - rules = { - "Hash[expr_]": 'Hash[expr, "MD5", "Integer"]', - "Hash[expr_, type_String]": 'Hash[expr, type, "Integer"]', - } - - summary_text = "compute hash codes for a string" - - # FIXME md2 - _supported_hashes = { - "Adler32": lambda: _ZLibHash(zlib.adler32), - "CRC32": lambda: _ZLibHash(zlib.crc32), - "MD5": hashlib.md5, - "SHA": hashlib.sha1, - "SHA224": hashlib.sha224, - "SHA256": hashlib.sha256, - "SHA384": hashlib.sha384, - "SHA512": hashlib.sha512, - } - - @staticmethod - def compute(user_hash, py_hashtype, py_format): - hash_func = Hash._supported_hashes.get(py_hashtype) - if hash_func is None: # unknown hash function? - return # in order to return original Expression - h = hash_func() - user_hash(h.update) - res = h.hexdigest() - if py_format in ("HexString", "HexStringLittleEndian"): - return String(res) - res = int(res, 16) - if py_format == "DecimalString": - return String(str(res)) - elif py_format == "ByteArray": - return Expression(SymbolByteArray, ByteArrayAtom(res)) - return Integer(res) - - def apply(self, expr, hashtype, outformat, evaluation): - "Hash[expr_, hashtype_String, outformat_String]" - return Hash.compute( - expr.user_hash, hashtype.get_string_value(), outformat.get_string_value() - ) +from mathics.eval.parts import convert_seq, python_seq class StringDrop(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/StringDrop.html + + :WMA link: + https://reference.wolfram.com/language/ref/StringDrop.html
      'StringDrop["$string$", $n$]' @@ -177,31 +83,37 @@ class StringDrop(Builtin): summary_text = "drop a part of a string" - def apply_with_n(self, string, n, evaluation): + def eval_with_n(self, string, n, evaluation): "StringDrop[string_,n_Integer]" if not isinstance(string, String): - return evaluation.message("StringDrop", "strse") + evaluation.message("StringDrop", "strse") + return if isinstance(n, Integer): pos = n.value if pos > len(string.get_string_value()): - return evaluation.message("StringDrop", "drop", 1, pos, string) + evaluation.message("StringDrop", "drop", 1, pos, string) + return if pos < -len(string.get_string_value()): - return evaluation.message("StringDrop", "drop", pos, -1, string) + evaluation.message("StringDrop", "drop", pos, -1, string) + return if pos > 0: return String(string.get_string_value()[pos:]) if pos < 0: return String(string.get_string_value()[:(pos)]) if pos == 0: return string - return evaluation.message("StringDrop", "mseqs") + evaluation.message("StringDrop", "mseqs") + return - def apply_with_ni_nf(self, string, ni, nf, evaluation): + def eval_with_ni_nf(self, string, ni, nf, evaluation): "StringDrop[string_,{ni_Integer,nf_Integer}]" if not isinstance(string, String): - return evaluation.message("StringDrop", "strse", string) + evaluation.message("StringDrop", "strse", string) + return if ni.value == 0 or nf.value == 0: - return evaluation.message("StringDrop", "drop", ni, nf) + evaluation.message("StringDrop", "drop", ni, nf) + return fullstring = string.get_string_value() lenfullstring = len(fullstring) posi = ni.value @@ -212,36 +124,43 @@ def apply_with_ni_nf(self, string, ni, nf, evaluation): posf = lenfullstring + posf + 1 if posf > lenfullstring or posi > lenfullstring or posf <= 0 or posi <= 0: # positions out or range - return evaluation.message("StringDrop", "drop", ni, nf, fullstring) + evaluation.message("StringDrop", "drop", ni, nf, fullstring) + return if posf < posi: return string # this is what actually mma does return String(fullstring[: (posi - 1)] + fullstring[posf:]) - def apply_with_ni(self, string, ni, evaluation): + def eval_with_ni(self, string, ni, evaluation): "StringDrop[string_,{ni_Integer}]" if not isinstance(string, String): - return evaluation.message("StringDrop", "strse", string) + evaluation.message("StringDrop", "strse", string) + return if ni.value == 0: - return evaluation.message("StringDrop", "drop", ni, ni) + evaluation.message("StringDrop", "drop", ni, ni) + return fullstring = string.get_string_value() lenfullstring = len(fullstring) posi = ni.value if posi < 0: posi = lenfullstring + posi + 1 if posi > lenfullstring or posi <= 0: - return evaluation.message("StringDrop", "drop", ni, ni, fullstring) + evaluation.message("StringDrop", "drop", ni, ni, fullstring) + return return String(fullstring[: (posi - 1)] + fullstring[posi:]) - def apply(self, string, something, evaluation): + def eval(self, string, something, evaluation): "StringDrop[string_,something___]" if not isinstance(string, String): - return evaluation.message("StringDrop", "strse") - return evaluation.message("StringDrop", "mseqs") + evaluation.message("StringDrop", "strse") + return + evaluation.message("StringDrop", "mseqs") class StringInsert(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/StringInsert.html + + :WMA link: + https://reference.wolfram.com/language/ref/StringInsert.html
      'StringInsert["$string$", "$snew$", $n$]' @@ -361,13 +280,25 @@ def _insert(self, str, add, lpos, evaluation): add_string = String(add) lpos_element = Integer(lpos[0]) if len(lpos) == 1 else from_python(lpos) evaluation.message("StringInsert", "ins", Integer(pos), str_string) - return evaluation.format_output( + + # In Mathics-server, evaluation.format_output is modified. + # Let's avoid to use it if we want a front-end independent result. + # Eventually, we are going to replace this by a `MakeBoxes` call. + def do_format_output(expr, evaluation): + try: + boxed_expr = format_element(expr, evaluation, SymbolOutputForm) + except BoxError: + boxed_expr = format_element(expr, evaluation, SymbolFullForm) + return boxed_expr.boxes_to_text() + + return do_format_output( Expression( SymbolStringInsert, str_string, add_string, lpos_element, - ) + ), + evaluation, ) # Create new list of position which are rearranged @@ -386,14 +317,15 @@ def _insert(self, str, add, lpos, evaluation): return result - def apply(self, strsource, strnew, pos, evaluation): + def eval(self, strsource, strnew, pos, evaluation): "StringInsert[strsource_, strnew_, pos_]" exp = Expression(SymbolStringInsert, strsource, strnew, pos) py_strnew = strnew.get_string_value() if py_strnew is None: - return evaluation.message("StringInsert", "string", Integer(2), exp) + evaluation.message("StringInsert", "string", Integer(2), exp) + return # Check and create list of position listpos = [] @@ -405,19 +337,22 @@ def apply(self, strsource, strnew, pos, evaluation): for i, posi in enumerate(elements): py_posi = posi.get_int_value() if py_posi is None: - return evaluation.message("StringInsert", "psl", pos, exp) + evaluation.message("StringInsert", "psl", pos, exp) + return listpos.append(py_posi) else: py_pos = pos.get_int_value() if py_pos is None: - return evaluation.message("StringInsert", "psl", pos, exp) + evaluation.message("StringInsert", "psl", pos, exp) + return listpos.append(py_pos) # Check and perform the insertion if strsource.has_form("List", None): py_strsource = [sub.get_string_value() for sub in strsource.elements] if any(sub is None for sub in py_strsource): - return evaluation.message("StringInsert", "strse", Integer1, exp) + evaluation.message("StringInsert", "strse", Integer1, exp) + return return ListExpression( *[ String(self._insert(s, py_strnew, listpos, evaluation)) @@ -427,7 +362,8 @@ def apply(self, strsource, strnew, pos, evaluation): else: py_strsource = strsource.get_string_value() if py_strsource is None: - return evaluation.message("StringInsert", "strse", Integer1, exp) + evaluation.message("StringInsert", "strse", Integer1, exp) + return return String(self._insert(py_strsource, py_strnew, listpos, evaluation)) @@ -457,7 +393,7 @@ class StringJoin(BinaryOperator): precedence = 600 summary_text = "join strings together" - def apply(self, items, evaluation): + def eval(self, items, evaluation): "StringJoin[items___]" result = "" if hasattr(items, "flatten_with_respect_to_head"): @@ -498,7 +434,7 @@ class StringLength(Builtin): summary_text = "length of a string (in Unicode characters)" - def apply(self, str, evaluation): + def eval(self, str, evaluation): "StringLength[str_]" if not isinstance(str, String): evaluation.message("StringLength", "string") @@ -580,17 +516,17 @@ class StringPosition(Builtin): summary_text = "range of positions where substrings match a pattern" - def apply(self, string, patt, evaluation, options): + def eval(self, string, patt, evaluation: Evaluation, options: dict): "StringPosition[string_, patt_, OptionsPattern[StringPosition]]" - return self.apply_n( + return self.eval_n( string, patt, - Expression(SymbolDirectedInfinity, Integer1), + MATHICS3_INFINITY, evaluation, options, ) - def apply_n(self, string, patt, n, evaluation, options): + def eval_n(self, string, patt, n, evaluation: Evaluation, options: dict): "StringPosition[string_, patt_, n:(_Integer|DirectedInfinity[1]), OptionsPattern[StringPosition]]" expr = Expression(SymbolStringPosition, string, patt, n) @@ -600,7 +536,8 @@ def apply_n(self, string, patt, n, evaluation, options): else: py_n = n.get_int_value() if py_n is None or py_n < 0: - return evaluation.message("StringPosition", "innf", expr, Integer(3)) + evaluation.message("StringPosition", "innf", expr, Integer(3)) + return # check options if options["System`Overlaps"] is SymbolTrue: @@ -623,7 +560,8 @@ def apply_n(self, string, patt, n, evaluation, options): for p in patts: py_p = to_regex(p, evaluation) if py_p is None: - return evaluation.message("StringExpression", "invld", p, patt) + evaluation.message("StringExpression", "invld", p, patt) + return re_patts.append(py_p) compiled_patts = [re.compile(re_patt) for re_patt in re_patts] @@ -689,7 +627,7 @@ class StringReplace(_StringFind): >> StringReplace["xyzwxyzwxxyzxyzw", {"xyz" -> "A", "w" -> "BCD"}] = ABCDABCDxAABCD - Only replace the first 2 occurences: + Only replace the first 2 occurrences: >> StringReplace["xyxyxyyyxxxyyxy", "xy" -> "A", 2] = AAxyyyxxxyyxy @@ -777,7 +715,7 @@ def cases(): return Expression(SymbolStringJoin, *list(cases())) - def apply(self, string, rule, n, evaluation, options): + def eval(self, string, rule, n, evaluation: Evaluation, options: dict): "%(name)s[string_, rule_, OptionsPattern[%(name)s], n_:System`Private`Null]" # this pattern is a slight hack to get around missing Shortest/Longest. return self._apply(string, rule, n, evaluation, options, False) @@ -799,7 +737,7 @@ class StringReverse(Builtin): attributes = A_LISTABLE | A_PROTECTED summary_text = "reverses the order of the characters in a string" - def apply(self, string, evaluation): + def eval(self, string, evaluation): "StringReverse[string_String]" return String(string.get_string_value()[::-1]) @@ -880,7 +818,7 @@ class StringRiffle(Builtin): summary_text = "assemble a string from a list, inserting delimiters" - def apply(self, liststr, seps, evaluation): + def eval(self, liststr, seps, evaluation): "StringRiffle[liststr_, seps___]" separators = seps.get_sequence() exp = ( @@ -891,22 +829,27 @@ def apply(self, liststr, seps, evaluation): # Validate separators if len(separators) > 1: - return evaluation.message("StringRiffle", "mulsep") + evaluation.message("StringRiffle", "mulsep") + return elif len(separators) == 1: if separators[0].has_form("List", None): if len(separators[0].elements) != 3 or any( not isinstance(s, String) for s in separators[0].elements ): - return evaluation.message("StringRiffle", "string", Integer(2), exp) + evaluation.message("StringRiffle", "string", Integer(2), exp) + return elif not isinstance(separators[0], String): - return evaluation.message("StringRiffle", "string", Integer(2), exp) + evaluation.message("StringRiffle", "string", Integer(2), exp) + return # Validate list of string if not liststr.has_form("List", None): evaluation.message("StringRiffle", "list", Integer1, exp) - return evaluation.message("StringRiffle", "argmu", exp) + evaluation.message("StringRiffle", "argmu", exp) + return elif any(element.has_form("List", None) for element in liststr.elements): - return evaluation.message("StringRiffle", "sublist") + evaluation.message("StringRiffle", "sublist") + return # Determine the separation token left, right = "", "" @@ -1006,21 +949,22 @@ class StringSplit(Builtin): summary_text = "split strings at whitespace, or at a pattern" - def apply(self, string, patt, evaluation, options): + def eval(self, string, patt, evaluation: Evaluation, options: dict): "StringSplit[string_, patt_, OptionsPattern[%(name)s]]" if string.get_head_name() == "System`List": elements = [ - self.apply(s, patt, evaluation, options) for s in string.elements + self.eval(s, patt, evaluation, options) for s in string.elements ] return ListExpression(*elements) py_string = string.get_string_value() if py_string is None: - return evaluation.message( + evaluation.message( "StringSplit", "strse", Integer1, Expression(SymbolStringSplit, string) ) + return if patt.has_form("List", None): patts = patt.get_elements() @@ -1030,7 +974,8 @@ def apply(self, string, patt, evaluation, options): for p in patts: py_p = to_regex(p, evaluation) if py_p is None: - return evaluation.message("StringExpression", "invld", p, patt) + evaluation.message("StringExpression", "invld", p, patt) + return re_patts.append(py_p) flags = re.MULTILINE @@ -1129,11 +1074,12 @@ class StringTake(Builtin): summary_text = "sub-string from a range of positions" - def apply(self, string, seqspec, evaluation): + def eval(self, string, seqspec, evaluation): "StringTake[string_String, seqspec_]" result = string.get_string_value() if result is None: - return evaluation.message("StringTake", "strse") + evaluation.message("StringTake", "strse") + return if isinstance(seqspec, Integer): pos = seqspec.get_int_value() @@ -1145,21 +1091,23 @@ def apply(self, string, seqspec, evaluation): seq = convert_seq(seqspec) if seq is None: - return evaluation.message("StringTake", "mseqs") + evaluation.message("StringTake", "mseqs") + return start, stop, step = seq py_slice = python_seq(start, stop, step, len(result)) if py_slice is None: - return evaluation.message("StringTake", "take", start, stop, string) + evaluation.message("StringTake", "take", start, stop, string) + return return String(result[py_slice]) - def apply_strings(self, strings, spec, evaluation): + def eval_strings(self, strings, spec, evaluation): "StringTake[strings__, spec_]" result_list = [] for string in strings.elements: - result = self.apply(string, spec, evaluation) + result = self.eval(string, spec, evaluation) if result is None: return None result_list.append(result) @@ -1184,11 +1132,11 @@ class StringTrim(Builtin): summary_text = "trim whitespace etc. from strings" - def apply(self, s, evaluation): + def eval(self, s, evaluation): "StringTrim[s_String]" return String(s.get_string_value().strip(" \t\n")) - def apply_pattern(self, s, patt, expression, evaluation): + def eval_pattern(self, s, patt, expression, evaluation): "StringTrim[s_String, patt_]" text = s.get_string_value() if not text: @@ -1196,7 +1144,8 @@ def apply_pattern(self, s, patt, expression, evaluation): py_patt = to_regex(patt, evaluation) if py_patt is None: - return evaluation.message("StringExpression", "invld", patt, expression) + evaluation.message("StringExpression", "invld", patt, expression) + return if not py_patt.startswith(r"\A"): left_patt = r"\A" + py_patt diff --git a/mathics/builtin/string/patterns.py b/mathics/builtin/string/patterns.py index 601bfa7c5..cb98e9d76 100644 --- a/mathics/builtin/string/patterns.py +++ b/mathics/builtin/string/patterns.py @@ -16,6 +16,7 @@ from mathics.builtin.base import BinaryOperator, Builtin from mathics.core.atoms import Integer1, String from mathics.core.attributes import A_FLAT, A_LISTABLE, A_ONE_IDENTITY, A_PROTECTED +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue @@ -171,19 +172,19 @@ class StringCases(_StringFind):
      'StringCases["$string$", $pattern$]' -
      gives all occurences of $pattern$ in $string$. +
      gives all occurrences of $pattern$ in $string$.
      'StringReplace["$string$", $pattern$ -> $form$]' -
      gives all instances of $form$ that stem from occurences of $pattern$ in $string$. +
      gives all instances of $form$ that stem from occurrences of $pattern$ in $string$.
      'StringCases["$string$", {$pattern1$, $pattern2$, ...}]' -
      gives all occurences of $pattern1$, $pattern2$, .... +
      gives all occurrences of $pattern1$, $pattern2$, ....
      'StringReplace["$string$", $pattern$, $n$]' -
      gives only the first $n$ occurences. +
      gives only the first $n$ occurrences.
      'StringReplace[{"$string1$", "$string2$", ...}, $pattern$]' -
      gives occurences in $string1$, $string2$, ... +
      gives occurrences in $string1$, $string2$, ...
      >> StringCases["axbaxxb", "a" ~~ x_ ~~ "b"] @@ -205,7 +206,7 @@ class StringCases(_StringFind): = {abc} #> StringCases["abc-abc xyz-uvw", Shortest[x : WordCharacter .. ~~ "-" ~~ x : LetterCharacter] -> x] - : Ignored restriction given for x in x : LetterCharacter as it does not match previous occurences of x. + : Ignored restriction given for x in x : LetterCharacter as it does not match previous occurrences of x. = {abc} >> StringCases["abba", {"a" -> 10, "b" -> 20}, 2] @@ -223,7 +224,7 @@ class StringCases(_StringFind): } summary_text = "occurrences of string patterns in a string" - def _find(self, py_stri, py_rules, py_n, flags, evaluation): + def _find(self, py_stri, py_rules, py_n, flags, evaluation: Evaluation): def cases(): for match, form in _parallel_match(py_stri, py_rules, flags, py_n): if form is None: @@ -233,7 +234,7 @@ def cases(): return ListExpression(*list(cases())) - def apply(self, string, rule, n, evaluation, options): + def eval(self, string, rule, n, evaluation: Evaluation, options: dict): "%(name)s[string_, rule_, OptionsPattern[%(name)s], n_:System`Private`Null]" # this pattern is a slight hack to get around missing Shortest/Longest. return self._apply(string, rule, n, evaluation, options, True) @@ -264,11 +265,11 @@ class StringExpression(BinaryOperator): messages = { "invld": "Element `1` is not a valid string or pattern element in `2`.", - "cond": "Ignored restriction given for `1` in `2` as it does not match previous occurences of `1`.", + "cond": "Ignored restriction given for `1` in `2` as it does not match previous occurrences of `1`.", } summary_text = "an arbitrary string expression" - def apply(self, args, evaluation): + def eval(self, args, evaluation: Evaluation): "StringExpression[args__String]" args = args.get_sequence() args = [arg.get_string_value() for arg in args] @@ -376,7 +377,7 @@ class StringFreeQ(Builtin): summary_text = "test whether a string is free of substrings matching a pattern" - def apply(self, string, patt, evaluation, options): + def eval(self, string, patt, evaluation: Evaluation, options: dict): "StringFreeQ[string_, patt_, OptionsPattern[%(name)s]]" return _pattern_search( self.__class__.__name__, string, patt, evaluation, options, False @@ -389,7 +390,7 @@ class StringMatchQ(Builtin): https://reference.wolfram.com/language/ref/StringMatchQ.html
      -
      'StringMatchQ["string", $patern$]' +
      'StringMatchQ["string", $pattern$]'
      checks is "string" matches $pattern$
      @@ -461,25 +462,27 @@ class StringMatchQ(Builtin): } summary_text = "test whether a string matches a pattern" - def apply(self, string, patt, evaluation, options): + def eval(self, string, patt, evaluation: Evaluation, options: dict): "StringMatchQ[string_, patt_, OptionsPattern[%(name)s]]" py_string = string.get_string_value() if py_string is None: - return evaluation.message( + evaluation.message( "StringMatchQ", "strse", Integer1, Expression(SymbolStringMatchQ, string, patt), ) + return re_patt = to_regex(patt, evaluation, abbreviated_patterns=True) if re_patt is None: - return evaluation.message( + evaluation.message( "StringExpression", "invld", patt, Expression(SymbolStringExpression, patt), ) + return re_patt = anchor_pattern(re_patt) diff --git a/mathics/builtin/system.py b/mathics/builtin/system.py index 236342b20..b2feca05d 100644 --- a/mathics/builtin/system.py +++ b/mathics/builtin/system.py @@ -23,26 +23,12 @@ try: import psutil -except: +except ImportError: have_psutil = False else: have_psutil = True -class Aborted(Predefined): - """ - :WMA link:https://reference.wolfram.com/language/ref/Aborted.html - -
      -
      '$Aborted' -
      is returned by a calculation that has been aborted. -
      - """ - - summary_text = "return value for aborted evaluations" - name = "$Aborted" - - class CommandLine(Predefined): """ :WMA link:https://reference.wolfram.com/language/ref/$CommandLine.html @@ -84,23 +70,6 @@ def eval(self, var, evaluation): return String(os.environ[env_var]) -class Failed(Predefined): - """ - :WMA link:https://reference.wolfram.com/language/ref/$Failed.html -
      -
      '$Failed' -
      is returned by some functions in the event of an error. -
      - - #> Get["nonexistent_file.m"] - : Cannot open nonexistent_file.m. - = $Failed - """ - - summary_text = "retrieved result for failed evaluations" - name = "$Failed" - - class GetEnvironment(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/GetEnvironment.html @@ -415,7 +384,7 @@ class UserName(Predefined): def evaluate(self, evaluation) -> String: try: user = os.getlogin() - except: + except Exception: import pwd user = pwd.getpwuid(os.getuid())[0] diff --git a/mathics/builtin/tensors.py b/mathics/builtin/tensors.py index aca59104d..57e8198ab 100644 --- a/mathics/builtin/tensors.py +++ b/mathics/builtin/tensors.py @@ -3,22 +3,30 @@ """ Tensors -A :tensor: https://en.wikipedia.org/wiki/Tensor is an algebraic object that describes a (multilinear) relationship between sets of algebraic objects related to a vector space. Objects that tensors may map between include vectors and scalars, and even other tensors. - -There are many types of tensors, including scalars and vectors (which are the simplest tensors), dual vectors, multilinear maps between vector spaces, and even some operations such as the dot product. Tensors are defined independent of any basis, although they are often referred to by their components in a basis related to a particular coordinate system. - -Mathics represents tensors of vectors and matrices as lists; tensors of any rank can be handled. +A :tensor: https://en.wikipedia.org/wiki/Tensor is an algebraic \ +object that describes a (multilinear) relationship between sets of algebraic \ +objects related to a vector space. Objects that tensors may map between \ +include vectors and scalars, and even other tensors. + +There are many types of tensors, including scalars and vectors (which are \ +the simplest tensors), dual vectors, multilinear maps between vector spaces, \ +and even some operations such as the dot product. Tensors are defined \ +independent of any basis, although they are often referred to by their \ +components in a basis related to a particular coordinate system. + +Mathics3 represents tensors of vectors and matrices as lists; tensors \ +of any rank can be handled. """ -from mathics.algorithm.parts import get_part from mathics.builtin.base import BinaryOperator, Builtin from mathics.core.atoms import Integer, String from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_PROTECTED +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.rules import Pattern from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolTrue +from mathics.eval.parts import get_part def get_default_distance(p): @@ -74,11 +82,13 @@ def get_dimensions(expr, head=None): class ArrayDepth(Builtin): """ - :WMA: https://reference.wolfram.com/language/ref/ArrayDepth.html + :WMA link: + https://reference.wolfram.com/language/ref/ArrayDepth.html
      'ArrayDepth[$a$]' -
      returns the depth of the non-ragged array $a$, defined as 'Length[Dimensions[$a$]]'. +
      returns the depth of the non-ragged array $a$, defined as \ + 'Length[Dimensions[$a$]]'.
      >> ArrayDepth[{{a,b},{c,d}}] @@ -94,75 +104,6 @@ class ArrayDepth(Builtin): summary_text = "the rank of a tensor" -class ArrayQ(Builtin): - """ - :WMA: https://reference.wolfram.com/language/ref/ArrayQ.html - -
      -
      'ArrayQ[$expr$]' -
      tests whether $expr$ is a full array. - -
      'ArrayQ[$expr$, $pattern$]' -
      also tests whether the array depth of $expr$ matches $pattern$. - -
      'ArrayQ[$expr$, $pattern$, $test$]' -
      furthermore tests whether $test$ yields 'True' for all elements of $expr$. - 'ArrayQ[$expr$]' is equivalent to 'ArrayQ[$expr$, _, True&]'. -
      - - >> ArrayQ[a] - = False - >> ArrayQ[{a}] - = True - >> ArrayQ[{{{a}},{{b,c}}}] - = False - >> ArrayQ[{{a, b}, {c, d}}, 2, SymbolQ] - = True - """ - - rules = { - "ArrayQ[expr_]": "ArrayQ[expr, _, True&]", - "ArrayQ[expr_, pattern_]": "ArrayQ[expr, pattern, True&]", - } - - summary_text = "test whether an object is a tensor of a given rank" - - def apply(self, expr, pattern, test, evaluation): - "ArrayQ[expr_, pattern_, test_]" - - pattern = Pattern.create(pattern) - - dims = [len(expr.get_elements())] # to ensure an atom is not an array - - def check(level, expr): - if not expr.has_form("List", None): - test_expr = Expression(test, expr) - if test_expr.evaluate(evaluation) != SymbolTrue: - return False - level_dim = None - else: - level_dim = len(expr.elements) - - if len(dims) > level: - if dims[level] != level_dim: - return False - else: - dims.append(level_dim) - if level_dim is not None: - for element in expr.elements: - if not check(level + 1, element): - return False - return True - - if not check(0, expr): - return SymbolFalse - - depth = len(dims) - 1 # None doesn't count - if not pattern.does_match(Integer(depth), evaluation): - return SymbolFalse - return SymbolTrue - - class Dimensions(Builtin): """ :WMA: https://reference.wolfram.com/language/ref/Dimensions.html @@ -196,7 +137,7 @@ class Dimensions(Builtin): summary_text = "the dimensions of a tensor" - def apply(self, expr, evaluation): + def eval(self, expr, evaluation: Evaluation): "Dimensions[expr_]" return ListExpression(*[Integer(dim) for dim in get_dimensions(expr)]) @@ -285,7 +226,7 @@ class Inner(Builtin): summary_text = "generalized inner product" - def apply(self, f, list1, list2, g, evaluation): + def eval(self, f, list1, list2, g, evaluation: Evaluation): "Inner[f_, list1_, list2_, g_]" m = get_dimensions(list1) @@ -369,7 +310,7 @@ class Outer(Builtin): summary_text = "generalized outer product" - def apply(self, f, lists, evaluation): + def eval(self, f, lists, evaluation: Evaluation): "Outer[f_, lists__]" lists = lists.get_sequence() @@ -554,7 +495,7 @@ class Transpose(Builtin): summary_text = "transpose to rearrange indices in any way" - def apply(self, m, evaluation): + def eval(self, m, evaluation: Evaluation): "Transpose[m_?MatrixQ]" result = [] @@ -565,28 +506,3 @@ def apply(self, m, evaluation): else: result[col_index].append(item) return ListExpression(*[ListExpression(*row) for row in result]) - - -# Should be in Elements of Vectors, but we don't have this since other operations -# are subsumed by Elements of Lists. -class VectorQ(Builtin): - """ - :WMA link: https://reference.wolfram.com/language/ref/VectorQ.html - -
      -
      'VectorQ[$v$]' -
      returns 'True' if $v$ is a list of elements which are not themselves lists. - -
      'VectorQ[$v$, $f$]' -
      returns 'True' if $v$ is a vector and '$f$[$x$]' returns 'True' for each element $x$ of $v$. -
      - - >> VectorQ[{a, b, c}] - = True - """ - - rules = { - "VectorQ[expr_]": "ArrayQ[expr, 1]", - "VectorQ[expr_, test_]": "ArrayQ[expr, 1, test]", - } - summary_text = "test whether an object is a vector" diff --git a/mathics/builtin/testing_expressions/__init__.py b/mathics/builtin/testing_expressions/__init__.py new file mode 100644 index 000000000..b700bb3a2 --- /dev/null +++ b/mathics/builtin/testing_expressions/__init__.py @@ -0,0 +1,9 @@ +""" +Testing Expressions + + +There are a number of functions for testing Expressions. + +Functions that "ask a question" have names that end in "Q". \ +They return 'True' for an explicit answer, and 'False' otherwise. +""" diff --git a/mathics/builtin/comparison.py b/mathics/builtin/testing_expressions/equality_inequality.py similarity index 75% rename from mathics/builtin/comparison.py rename to mathics/builtin/testing_expressions/equality_inequality.py index 297c0101f..f95289f17 100644 --- a/mathics/builtin/comparison.py +++ b/mathics/builtin/testing_expressions/equality_inequality.py @@ -1,35 +1,17 @@ # -*- coding: utf-8 -*- """ -Testing Expressions - -There are a number of functions for testing Expressions. - -Functions that "ask a question" have names that end in "Q". \ -They return 'True' for an explicit answer, and 'False' otherwise. +Equality and Inequality """ -# This tells documentation how to sort this module -sort_order = "mathics.builtin.testing-expressions" - from typing import Any, Optional import sympy from mathics.builtin.base import BinaryOperator, Builtin, SympyFunction from mathics.builtin.numbers.constants import mp_convert_constant -from mathics.core.atoms import ( - COMPARE_PREC, - Complex, - Integer, - Integer0, - Integer1, - IntegerM1, - Number, - String, -) +from mathics.core.atoms import COMPARE_PREC, Integer, Integer1, Number, String from mathics.core.attributes import ( A_FLAT, - A_LISTABLE, A_NUMERIC_FUNCTION, A_ONE_IDENTITY, A_ORDERLESS, @@ -37,19 +19,26 @@ ) from mathics.core.convert.expression import to_expression, to_numeric_args from mathics.core.expression import Expression +from mathics.core.expression_predefined import ( + MATHICS3_COMPLEX_INFINITY, + MATHICS3_INFINITY, + MATHICS3_NEG_INFINITY, +) from mathics.core.number import dps from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolList, SymbolTrue from mathics.core.systemsymbols import ( SymbolAnd, - SymbolComplexInfinity, SymbolDirectedInfinity, + SymbolExactNumberQ, SymbolInequality, SymbolInfinity, + SymbolMaxExtraPrecision, SymbolMaxPrecision, SymbolSign, ) from mathics.eval.nevaluator import eval_N from mathics.eval.numerify import numerify +from mathics.eval.testing_expressions import do_cmp, do_cplx_equal, is_number operators = { "System`Less": (-1,), @@ -60,108 +49,6 @@ "System`Unequal": (-1, 1), } -SymbolExactNumberQ = Symbol("ExactNumberQ") -SymbolMaxExtraPrecision = Symbol("$MaxExtraPrecision") - - -def cmp(a, b) -> int: - "Returns 0 if a == b, -1 if a < b and 1 if a > b" - return (a > b) - (a < b) - - -def do_cmp(x1, x2) -> Optional[int]: - - # don't attempt to compare complex numbers - for x in (x1, x2): - # TODO: Send message General::nord - if isinstance(x, Complex) or ( - x.has_form("DirectedInfinity", 1) and isinstance(x.elements[0], Complex) - ): - return None - - s1 = x1.to_sympy() - s2 = x2.to_sympy() - - # Use internal comparisons only for Real which is uses - # WL's interpretation of equal (which allows for slop - # in the least significant digit of precision), and use - # use sympy for everything else - if s1.is_Float and s2.is_Float: - if x1 == x2: - return 0 - if x1 < x2: - return -1 - return 1 - - # we don't want to compare anything that - # cannot be represented as a numeric value - if s1.is_number and s2.is_number: - if s1 == s2: - return 0 - if s1 < s2: - return -1 - return 1 - - return None - - -def do_cplx_equal(x, y) -> Optional[int]: - if isinstance(y, Complex): - x, y = y, x - if isinstance(x, Complex): - if isinstance(y, Complex): - c = do_cmp(x.real, y.real) - if c is None: - return - if c != 0: - return False - c = do_cmp(x.imag, y.imag) - if c is None: - return - if c != 0: - return False - else: - return True - else: - c = do_cmp(x.imag, Integer0) - if c is None: - return - if c != 0: - return False - c = do_cmp(x.real, y.real) - if c is None: - return - if c != 0: - return False - else: - return True - c = do_cmp(x, y) - if c is None: - return None - return c == 0 - - -def expr_max(elements): - result = Expression(SymbolDirectedInfinity, IntegerM1) - for element in elements: - c = do_cmp(element, result) - if c > 0: - result = element - return result - - -def expr_min(elements): - result = Expression(SymbolDirectedInfinity, Integer1) - for element in elements: - c = do_cmp(element, result) - if c < 0: - result = element - return result - - -def is_number(sympy_value) -> bool: - return hasattr(sympy_value, "is_number") or isinstance(sympy_value, sympy.Float) - class _InequalityOperator(BinaryOperator): precedence = 290 @@ -227,6 +114,8 @@ def get_pairs(args): yield (args[i], args[j]) def expr_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]: + if rhs is lhs: + return True if isinstance(rhs, Expression): lhs, rhs = rhs, lhs if not isinstance(lhs, Expression): @@ -246,34 +135,23 @@ def expr_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]: return True def infty_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]: - if rhs.get_head().sameQ(SymbolDirectedInfinity): - lhs, rhs = rhs, lhs - if not lhs.get_head().sameQ(SymbolDirectedInfinity): + if ( + lhs.get_head() is not SymbolDirectedInfinity + or rhs.get_head() is not SymbolDirectedInfinity + ): return None - if rhs.sameQ(SymbolInfinity) or rhs.sameQ(SymbolComplexInfinity): - if len(lhs.elements) == 0: - return True - else: - return self.equal2( - to_expression(SymbolSign, lhs.elements[0]), Integer1, max_extra_prec - ) - if rhs.is_numeric(): - return False - elif isinstance(rhs, Atom): + lhs_elements, rhs_elements = lhs.elements, rhs.elements + + if len(lhs_elements) != len(rhs_elements): return None - if rhs.get_head().sameQ(lhs.get_head()): - dir1 = dir2 = Integer1 - if len(lhs.elements) == 1: - dir1 = lhs.elements[0] - if len(rhs.elements) == 1: - dir2 = rhs.elements[0] - if self.equal2(dir1, dir2, max_extra_prec): - return True - # Now, compare the signs: - dir1_sign = Expression(SymbolSign, dir1) - dir2_sign = Expression(SymbolSign, dir2) - return self.equal2(dir1_sign, dir2_sign, max_extra_prec) - return + # Both are complex infinity? + if len(lhs_elements) == 0: + return True + if len(lhs_elements) == 1: + # Check directions: Notice that they are already normalized... + return self.equal2(lhs_elements[0], rhs_elements[0], max_extra_prec) + # DirectedInfinity with more than two elements cannot be compared here... + return None def sympy_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]: try: @@ -317,12 +195,12 @@ def equal2(self, lhs: Any, rhs: Any, max_extra_prec=None) -> Optional[bool]: """ Two-argument Equal[] """ + if lhs is rhs or lhs.sameQ(rhs): + return True if hasattr(lhs, "equal2"): result = lhs.equal2(rhs) if result is not None: return result - elif lhs.sameQ(rhs): - return True # TODO: Check $Assumptions # Still we didn't have a result. Try with the following # tests @@ -411,7 +289,7 @@ def eval(self, items, evaluation): results.append(element) if not results: - return Expression(SymbolDirectedInfinity, Integer(-self.sense)) + return MATHICS3_INFINITY if self.sense < 0 else MATHICS3_NEG_INFINITY if len(results) == 1: return results.pop() if len(results) < len(items): @@ -560,7 +438,7 @@ class Equal(_EqualityOperator, _SympyComparison): >> a = b; a == b = True - Comparision to mismatched types is False: + Comparison to mismatched types is False: >> Equal[11, "11"] = False @@ -571,8 +449,8 @@ class Equal(_EqualityOperator, _SympyComparison): >> {1, 2} == {1, 2, 3} = False - For chains of equalities, the comparison is done amongs all the pairs. The evaluation is successful - only if the equality is satisfied over all the pairs: + For chains of equalities, the comparison is done amongst all the pairs. \ + The evaluation is successful only if the equality is satisfied over all the pairs: >> g[1] == g[1] == g[1] = True @@ -726,7 +604,7 @@ class Less(_ComparisonOperator, _SympyComparison): >> 2/18 < 1/5 < Pi/10 = True - Using less on an undfined symbol value: + Using less on an undefined symbol value: >> 1 < 3 < x < 2 = 1 < 3 < x < 2 """ @@ -836,114 +714,6 @@ class Min(_MinMax): summary_text = "the minimum value" -class Negative(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Negative.html - -
      -
      'Negative[$x$]' -
      returns 'True' if $x$ is a negative real number. -
      - >> Negative[0] - = False - >> Negative[-3] - = True - >> Negative[10/7] - = False - >> Negative[1+2I] - = False - >> Negative[a + b] - = Negative[a + b] - #> Negative[-E] - = True - #> Negative[Sin[{11, 14}]] - = {True, False} - """ - - attributes = A_LISTABLE | A_PROTECTED - - rules = { - "Negative[x_?NumericQ]": "If[x < 0, True, False, False]", - } - summary_text = "test whether an expression is a negative number" - - -class NonNegative(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/NonNegative.html - -
      -
      'NonNegative[$x$]' -
      returns 'True' if $x$ is a positive real number or zero. -
      - - >> {Positive[0], NonNegative[0]} - = {False, True} - """ - - attributes = A_LISTABLE | A_PROTECTED - - rules = { - "NonNegative[x_?NumericQ]": "If[x >= 0, True, False, False]", - } - summary_text = "test whether an expression is a non-negative number" - - -class NonPositive(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/NonPositive.html - -
      -
      'NonPositive[$x$]' -
      returns 'True' if $x$ is a negative real number or zero. -
      - - >> {Negative[0], NonPositive[0]} - = {False, True} - """ - - attributes = A_LISTABLE | A_PROTECTED - - rules = { - "NonPositive[x_?NumericQ]": "If[x <= 0, True, False, False]", - } - summary_text = "test whether an expression is a non-positive number" - - -class Positive(Builtin): - """ - :WMA link:https://reference.wolfram.com/language/ref/Positive.html - -
      -
      'Positive[$x$]' -
      returns 'True' if $x$ is a positive real number. -
      - - >> Positive[1] - = True - - 'Positive' returns 'False' if $x$ is zero or a complex number: - >> Positive[0] - = False - >> Positive[1 + 2 I] - = False - - #> Positive[Pi] - = True - #> Positive[x] - = Positive[x] - #> Positive[Sin[{11, 14}]] - = {False, True} - """ - - attributes = A_LISTABLE | A_PROTECTED - - rules = { - "Positive[x_?NumericQ]": "If[x > 0, True, False, False]", - } - summary_text = "test whether an expression is a positive number" - - class SameQ(_ComparisonOperator): """ :WMA link:https://reference.wolfram.com/language/ref/SameQ.html @@ -951,13 +721,15 @@ class SameQ(_ComparisonOperator):
      'SameQ[$x$, $y$]'
      '$x$ === $y$' -
      returns 'True' if $x$ and $y$ are structurally identical. - Commutative properties apply, so if $x$ === $y$ then $y$ === $x$. +
      returns 'True' if $x$ and $y$ are structurally identical. \ + Commutative properties apply, so if $x$ === $y$ then $y$ === $x$.
        -
      • 'SameQ' requires exact correspondence between expressions, expet that it still considers 'Real' numbers equal if they differ in their last binary digit. +
      • 'SameQ' requires exact correspondence between expressions, expect that \ + it still considers 'Real' numbers equal if they differ in their last \ + binary digit.
      • $e1$ === $e2$ === $e3$ gives 'True' if all the $ei$'s are identical.
      • 'SameQ[]' and 'SameQ[$expr$]' always yield 'True'.
      @@ -971,7 +743,8 @@ class SameQ(_ComparisonOperator): >> SameQ[a] === SameQ[] === True = True - Unlike 'Equal', 'SameQ' only yields 'True' if $x$ and $y$ have the same type: + Unlike 'Equal', 'SameQ' only yields 'True' if $x$ and $y$ have the same \ + type: >> {1==1., 1===1.} = {True, False} @@ -1034,21 +807,24 @@ class TrueQ(Builtin): class Unequal(_EqualityOperator, _SympyComparison): """ - :WMA link:https://reference.wolfram.com/language/ref/Unequal.html + + :WMA link: + https://reference.wolfram.com/language/ref/Unequal.html
      'Unequal[$x$, $y$]' or $x$ != $y$ or $x$ \u2260 $y$ -
      is 'False' if $x$ and $y$ are known to be equal, or 'True' if $x$ and $y$ are known to be unequal. +
      is 'False' if $x$ and $y$ are known to be equal, or 'True' if $x$ \ + and $y$ are known to be unequal. Commutative properties apply so if $x$ != $y$ then $y$ != $x$. - For any expression $x$ and $y$, Unequal[$x$, $y$] == Not[Equal[$x$, $y$]]. + For any expression $x$ and $y$, 'Unequal[$x$, $y$]' == 'Not[Equal[$x$, $y$]]'.
      >> 1 != 1. = False - Comparsion can be chained: + Comparisons can be chained: >> 1 != 2 != 3 = True @@ -1059,7 +835,7 @@ class Unequal(_EqualityOperator, _SympyComparison): >> Unequal["11", "11"] = False - Comparision to mismatched types is True: + Comparison to mismatched types is True: >> Unequal[11, "11"] = True diff --git a/mathics/builtin/testing_expressions/expression_tests.py b/mathics/builtin/testing_expressions/expression_tests.py new file mode 100644 index 000000000..258bfd1e1 --- /dev/null +++ b/mathics/builtin/testing_expressions/expression_tests.py @@ -0,0 +1,69 @@ +""" +Expression Tests +""" +from mathics.builtin.base import Builtin, PatternError, Test +from mathics.core.evaluation import Evaluation +from mathics.core.symbols import SymbolFalse, SymbolTrue +from mathics.eval.patterns import match + + +class ListQ(Test): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/ListQ.html + +
      +
      'ListQ[$expr$]' +
      tests whether $expr$ is a 'List'. +
      + + >> ListQ[{1, 2, 3}] + = True + >> ListQ[{{1, 2}, {3, 4}}] + = True + >> ListQ[x] + = False + """ + + summary_text = "test if an expression is a list" + + def test(self, expr) -> bool: + return expr.get_head_name() == "System`List" + + +class MatchQ(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/MatchQ.html + +
      +
      'MatchQ[$expr$, $form$]' +
      tests whether $expr$ matches $form$. +
      + + >> MatchQ[123, _Integer] + = True + >> MatchQ[123, _Real] + = False + >> MatchQ[_Integer][123] + = True + >> MatchQ[3, Pattern[3]] + : First element in pattern Pattern[3] is not a valid pattern name. + = False + """ + + rules = {"MatchQ[form_][expr_]": "MatchQ[expr, form]"} + summary_text = "test whether an expression matches a pattern" + + def eval(self, expr, form, evaluation: Evaluation): + "MatchQ[expr_, form_]" + + try: + if match(expr, form, evaluation): + return SymbolTrue + return SymbolFalse + except PatternError as e: + evaluation.message(e.name, e.tag, *(e.args)) + return SymbolFalse diff --git a/mathics/builtin/testing_expressions/list_oriented.py b/mathics/builtin/testing_expressions/list_oriented.py new file mode 100644 index 000000000..74b92c511 --- /dev/null +++ b/mathics/builtin/testing_expressions/list_oriented.py @@ -0,0 +1,395 @@ +""" +List-Oriented Tests +""" + +from mathics.builtin.base import Builtin, Test +from mathics.core.atoms import Integer, Integer1, Integer2 +from mathics.core.evaluation import Evaluation +from mathics.core.exceptions import InvalidLevelspecError +from mathics.core.expression import Expression +from mathics.core.rules import Pattern +from mathics.core.symbols import Atom, SymbolFalse, SymbolTrue +from mathics.core.systemsymbols import SymbolSubsetQ +from mathics.eval.parts import python_levelspec + + +class ArrayQ(Builtin): + """ + + :WMA: + https://reference.wolfram.com/language/ref/ArrayQ.html + +
      +
      'ArrayQ[$expr$]' +
      tests whether $expr$ is a full array. + +
      'ArrayQ[$expr$, $pattern$]' +
      also tests whether the array depth of $expr$ matches $pattern$. + +
      'ArrayQ[$expr$, $pattern$, $test$]' +
      furthermore tests whether $test$ yields 'True' for all elements of $expr$. + 'ArrayQ[$expr$]' is equivalent to 'ArrayQ[$expr$, _, True&]'. +
      + + >> ArrayQ[a] + = False + >> ArrayQ[{a}] + = True + >> ArrayQ[{{{a}},{{b,c}}}] + = False + >> ArrayQ[{{a, b}, {c, d}}, 2, SymbolQ] + = True + """ + + rules = { + "ArrayQ[expr_]": "ArrayQ[expr, _, True&]", + "ArrayQ[expr_, pattern_]": "ArrayQ[expr, pattern, True&]", + } + + summary_text = "test whether an object is a tensor of a given rank" + + def eval(self, expr, pattern, test, evaluation: Evaluation): + "ArrayQ[expr_, pattern_, test_]" + + pattern = Pattern.create(pattern) + + dims = [len(expr.get_elements())] # to ensure an atom is not an array + + def check(level, expr): + if not expr.has_form("List", None): + test_expr = Expression(test, expr) + if test_expr.evaluate(evaluation) != SymbolTrue: + return False + level_dim = None + else: + level_dim = len(expr.elements) + + if len(dims) > level: + if dims[level] != level_dim: + return False + else: + dims.append(level_dim) + if level_dim is not None: + for element in expr.elements: + if not check(level + 1, element): + return False + return True + + if not check(0, expr): + return SymbolFalse + + depth = len(dims) - 1 # None doesn't count + if not pattern.does_match(Integer(depth), evaluation): + return SymbolFalse + return SymbolTrue + + +class DisjointQ(Test): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/DisjointQ.html + +
      +
      'DisjointQ[$a$, $b$]' +
      gives True if $a$ and $b$ are disjoint, or False if $a$ and \ + $b$ have any common elements. +
      + """ + + rules = {"DisjointQ[a_List, b_List]": "Not[IntersectingQ[a, b]]"} + summary_text = "test whether two lists do not have common elements" + + +class IntersectingQ(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/IntersectingQ.html + +
      +
      'IntersectingQ[$a$, $b$]' +
      gives True if there are any common elements in $a and $b, or \ + False if $a and $b are disjoint. +
      + """ + + rules = {"IntersectingQ[a_List, b_List]": "Length[Intersect[a, b]] > 0"} + summary_text = "test whether two lists have common elements" + + +class LevelQ(Test): + """ +
      +
      'LevelQ[$expr$]' +
      tests whether $expr$ is a valid level specification. This function \ + is primarily used in function patterns for specifying type of a \ + parameter. +
      + + >> LevelQ[2] + = True + >> LevelQ[{2, 4}] + = True + >> LevelQ[Infinity] + = True + >> LevelQ[a + b] + = False + + We will define MyMap with the "level" parameter as a synonym for the \ + Builtin Map equivalent: + + >> MyMap[f_, expr_, Pattern[levelspec, _?LevelQ]] := Map[f, expr, levelspec] + + >> MyMap[f, {{a, b}, {c, d}}, {2}] + = {{f[a], f[b]}, {f[c], f[d]}} + + >> Map[f, {{a, b}, {c, d}}, {2}] + = {{f[a], f[b]}, {f[c], f[d]}} + + But notice that when we pass an invalid level specification, MyMap \ + does not match and therefore does not pass the arguments through to 'Map'. \ + So we do not see the error message that 'Map' would normally produce + + >> Map[f, {{a, b}, {c, d}}, x] + : Level specification x is not of the form n, {n}, or {m, n}. + = Map[f, {{a, b}, {c, d}}, x] + + >> MyMap[f, {{a, b}, {c, d}}, {1, 2, 3}] + = MyMap[f, {{a, b}, {c, d}}, {1, 2, 3}] + """ + + summary_text = "test whether is a valid level specification" + + def test(self, ls) -> bool: + try: + start, stop = python_levelspec(ls) + return True + except InvalidLevelspecError: + return False + + +class MatrixQ(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/MatrixQ.html + +
      +
      'MatrixQ[$m$]' +
      gives 'True' if $m$ is a list of equal-length lists. + +
      'MatrixQ[$m$, $f$]' +
      gives 'True' only if '$f$[$x$]' returns 'True' for when applied to \ + element $x$ of the matrix $m$. +
      + + >> MatrixQ[{{1, 3}, {4.0, 3/2}}, NumberQ] + = True + + These are not matrices: + >> MatrixQ[{{1}, {1, 2}}] (* first row should have length two *) + = False + + >> MatrixQ[Array[a, {1, 1, 2}]] + = False + + Supply a test function parameter to generalize and specialize: + >> MatrixQ[{{1, 2}, {3, 4 + 5}}, Positive] + = True + + >> MatrixQ[{{1, 2 I}, {3, 4 + 5}}, Positive] + = False + """ + + rules = { + "MatrixQ[expr_]": "ArrayQ[expr, 2]", + "MatrixQ[expr_, test_]": "ArrayQ[expr, 2, test]", + } + + summary_text = "gives 'True' if the given argument is a list of equal-length lists" + + +class MemberQ(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/MemberQ.html + +
      +
      'MemberQ[$list$, $pattern$]' +
      returns 'True' if $pattern$ matches any element of $list$, or 'False' otherwise. +
      + + >> MemberQ[{a, b, c}, b] + = True + >> MemberQ[{a, b, c}, d] + = False + >> MemberQ[{"a", b, f[x]}, _?NumericQ] + = False + >> MemberQ[_List][{{}}] + = True + """ + + rules = { + "MemberQ[list_, pattern_]": ("Length[Select[list, MatchQ[#, pattern]&]] > 0"), + "MemberQ[pattern_][expr_]": "MemberQ[expr, pattern]", + } + summary_text = "test whether an element is a member of a list" + + +class NotListQ(Test): + """ +
      +
      'NotListQ[$expr$]' +
      returns 'True' if $expr$ is not a list. This function is primarily \ + used in function patterns for specifying type of a parameter. +
      + + Consider this definition for taking the deriviate 'Sin' of a function: + + >> MyD[Sin[f_],x_?NotListQ] := D[f,x]*Cos[f] + = + + We use "MyD" above to distinguish it from the Builtin 'D'. Now let's try it: + + >> MyD[Sin[2 x], x] + = 2 Cos[2 x] + + And compare it with the Builtin deriviative function 'D': + + >> D[Sin[2 x], x] + = 2 Cos[2 x] + + Note however the pattern only matches if the $x$ parameter is not a list: + + >> MyD[{Sin[2], Sin[4]}, {1, 2}] + = MyD[{Sin[2], Sin[4]}, {1, 2}] + + """ + + summary_text = "test if an expression is not a list" + + def test(self, expr) -> bool: + return expr.get_head_name() != "System`List" + + +class SubsetQ(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/SubsetQ.html + +
      +
      'SubsetQ[$list1$, $list2$]' +
      returns True if $list2$ is a subset of $list1$, and False otherwise. +
      + + >> SubsetQ[{1, 2, 3}, {3, 1}] + = True + + The empty list is a subset of every list: + >> SubsetQ[{}, {}] + = True + + >> SubsetQ[{1, 2, 3}, {}] + = True + + Every list is a subset of itself: + >> SubsetQ[{1, 2, 3}, {1, 2, 3}] + = True + + #> SubsetQ[{1, 2, 3}, {0, 1}] + = False + + #> SubsetQ[{1, 2, 3}, {1, 2, 3, 4}] + = False + + #> SubsetQ[{1, 2, 3}] + : SubsetQ called with 1 argument; 2 arguments are expected. + = SubsetQ[{1, 2, 3}] + + #> SubsetQ[{1, 2, 3}, {1, 2}, {3}] + : SubsetQ called with 3 arguments; 2 arguments are expected. + = SubsetQ[{1, 2, 3}, {1, 2}, {3}] + + #> SubsetQ[a + b + c, {1}] + : Heads Plus and List at positions 1 and 2 are expected to be the same. + = SubsetQ[a + b + c, {1}] + + #> SubsetQ[{1, 2, 3}, n] + : Nonatomic expression expected at position 2 in SubsetQ[{1, 2, 3}, n]. + = SubsetQ[{1, 2, 3}, n] + + #> SubsetQ[f[a, b, c], f[a]] + = True + """ + + messages = { + # FIXME: This message doesn't exist in more modern WMA, and + # Subset *can* take more than 2 arguments. + "argr": "SubsetQ called with 1 argument; 2 arguments are expected.", + "argrx": "SubsetQ called with `1` arguments; 2 arguments are expected.", + "heads": "Heads `1` and `2` at positions 1 and 2 are expected to be the same.", + "normal": "Nonatomic expression expected at position `1` in `2`.", + } + summary_text = "test if a list is a subset of another list" + + def eval(self, expr, subset, evaluation: Evaluation): + "SubsetQ[expr_, subset___]" + + if isinstance(expr, Atom): + evaluation.message( + "SubsetQ", "normal", Integer1, Expression(SymbolSubsetQ, expr, subset) + ) + return + + subset = subset.get_sequence() + if len(subset) > 1: + evaluation.message("SubsetQ", "argrx", Integer(len(subset) + 1)) + return + elif len(subset) == 0: + evaluation.message("SubsetQ", "argr") + return + + subset = subset[0] + if isinstance(subset, Atom): + evaluation.message( + "SubsetQ", "normal", Integer2, Expression(SymbolSubsetQ, expr, subset) + ) + return + if expr.get_head_name() != subset.get_head_name(): + evaluation.message("SubsetQ", "heads", expr.get_head(), subset.get_head()) + return + + if set(subset.elements).issubset(set(expr.elements)): + return SymbolTrue + else: + return SymbolFalse + + +class VectorQ(Builtin): + """ + :WMA link: + https://reference.wolfram.com/language/ref/VectorQ.html + +
      +
      'VectorQ[$v$]' +
      returns 'True' if $v$ is a list of elements which are not themselves lists. + +
      'VectorQ[$v$, $f$]' +
      returns 'True' if $v$ is a vector and '$f$[$x$]' returns 'True' for each element $x$ of $v$. +
      + + >> VectorQ[{a, b, c}] + = True + """ + + rules = { + "VectorQ[expr_]": "ArrayQ[expr, 1]", + "VectorQ[expr_, test_]": "ArrayQ[expr, 1, test]", + } + summary_text = "test whether an object is a vector" + + +# TODO DuplicateFreeQ diff --git a/mathics/builtin/logic.py b/mathics/builtin/testing_expressions/logic.py similarity index 87% rename from mathics/builtin/logic.py rename to mathics/builtin/testing_expressions/logic.py index fa3ec2b54..1501b3c11 100644 --- a/mathics/builtin/logic.py +++ b/mathics/builtin/testing_expressions/logic.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- - - +""" +Logical Combinations +""" from mathics.builtin.base import BinaryOperator, Builtin, Predefined, PrefixOperator -from mathics.builtin.lists import InvalidLevelspecError, python_levelspec, walk_levels from mathics.core.attributes import ( A_FLAT, A_HOLD_ALL, @@ -11,6 +11,8 @@ A_ORDERLESS, A_PROTECTED, ) +from mathics.core.evaluation import Evaluation +from mathics.core.exceptions import InvalidLevelspecError from mathics.core.expression import Expression from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue from mathics.core.systemsymbols import ( @@ -21,6 +23,7 @@ SymbolOr, SymbolXor, ) +from mathics.eval.parts import python_levelspec, walk_levels class _ShortCircuit(Exception): @@ -40,7 +43,7 @@ def _short_circuit(self, what): def _no_short_circuit(self): raise NotImplementedError - def apply(self, expr, test, level, evaluation): + def eval(self, expr, test, level, evaluation: Evaluation): "%(name)s[expr_, test_, level_]" try: @@ -65,7 +68,8 @@ def callback(node): class And(BinaryOperator): """ - :WMA link:https://reference.wolfram.com/language/ref/And.html + :WMA link: + https://reference.wolfram.com/language/ref/And.html
      'And[$expr1$, $expr2$, ...]' @@ -94,7 +98,7 @@ class And(BinaryOperator): # "And[pred1___, a_, pred2___, a_, pred3___]": "And[pred1, a, pred2, pred3]", # } - def apply(self, args, evaluation): + def eval(self, args, evaluation: Evaluation): "And[args___]" args = args.get_sequence() @@ -116,7 +120,9 @@ def apply(self, args, evaluation): class AnyTrue(_ManyTrue): """ - :WMA link:https://reference.wolfram.com/language/ref/AnyTrue.html + + :WMA link: + https://reference.wolfram.com/language/ref/AnyTrue.html
      'AnyTrue[{$expr1$, $expr2$, ...}, $test$]' @@ -204,7 +210,7 @@ class Equivalent(BinaryOperator): If all expressions do not evaluate to 'True' or 'False', 'Equivalent' \ returns a result in symbolic form: >> Equivalent[a, b, c] - = a \u29E6 b \u29E6 c + = a \\[Equivalent] b \\[Equivalent] c Otherwise, 'Equivalent' returns a result in DNF >> Equivalent[a, b, True, c] = a && b && c @@ -219,7 +225,7 @@ class Equivalent(BinaryOperator): precedence = 205 summary_text = "logic equivalence" - def apply(self, args, evaluation): + def eval(self, args, evaluation: Evaluation): "Equivalent[args___]" args = args.get_sequence() @@ -244,7 +250,9 @@ def apply(self, args, evaluation): class False_(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/False.html + + :WMA link: + https://reference.wolfram.com/language/ref/False.html
      'False' @@ -278,7 +286,7 @@ class Implies(BinaryOperator): If an expression does not evaluate to 'True' or 'False', 'Implies' returns a result in symbolic form: >> Implies[a, Implies[b, Implies[True, c]]] - = a \u21D2 b \u21D2 c + = a Implies b Implies c """ operator = "\u21D2" @@ -286,7 +294,7 @@ class Implies(BinaryOperator): grouping = "Right" summary_text = "logic implication" - def apply(self, x, y, evaluation): + def eval(self, x, y, evaluation: Evaluation): "Implies[x_, y_]" result0 = x.evaluate(evaluation) @@ -300,15 +308,21 @@ def apply(self, x, y, evaluation): class NoneTrue(_ManyTrue): """ - :WMA link:https://reference.wolfram.com/language/ref/NoneTrue.html + + :WMA link: + https://reference.wolfram.com/language/ref/NoneTrue.html
      -
      'NoneTrue[{$expr1$, $expr2$, ...}, $test$]' -
      returns True if no application of $test$ to $expr1$, $expr2$, ... evaluates to True. -
      'NoneTrue[$list$, $test$, $level$]' -
      returns True if no application of $test$ to items of $list$ at $level$ evaluates to True. -
      'NoneTrue[$test$]' -
      gives an operator that may be applied to expressions. +
      'NoneTrue[{$expr1$, $expr2$, ...}, $test$]' +
      returns True if no application of $test$ to $expr1$, $expr2$, ... \ + evaluates to True. + +
      'NoneTrue[$list$, $test$, $level$]' +
      returns True if no application of $test$ to items of $list$ at \ + $level$ evaluates to True. + +
      'NoneTrue[$test$]' +
      gives an operator that may be applied to expressions.
      >> NoneTrue[{1, 3, 5}, EvenQ] @@ -362,7 +376,7 @@ class Or(BinaryOperator): # "Or[a_, a_]": "a", # "Or[pred1___, a_, pred2___, a_, pred3___]": "Or[pred1, a, pred2, pred3]", # } - def apply(self, args, evaluation): + def eval(self, args, evaluation: Evaluation): "Or[args___]" args = args.get_sequence() @@ -384,12 +398,14 @@ def apply(self, args, evaluation): class Nand(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/Nand.html + :WMA link: + https://reference.wolfram.com/language/ref/Nand.html
      -
      'Nand[$expr1$, $expr2$, ...]' -
      $expr1$ \u22BC $expr2$ \u22BC ... -
      Implements the logical NAND function. The same as 'Not[And['$expr1$, $expr2$, ...']]' +
      'Nand[$expr1$, $expr2$, ...]' + +
      $expr1$ \u22BC $expr2$ \u22BC ... +
      Implements the logical NAND function. The same as 'Not[And['$expr1$, $expr2$, ...']]'
      >> Nand[True, False] = True @@ -407,9 +423,10 @@ class Nor(Builtin): :WMA link:https://reference.wolfram.com/language/ref/Nor.html
      -
      'Nor[$expr1$, $expr2$, ...]' -
      $expr1$ \u22BD $expr2$ \u22BD ... -
      Implements the logical NOR function. The same as 'Not[Or['$expr1$, $expr2$, ...']]' +
      'Nor[$expr1$, $expr2$, ...]' + +
      $expr1$ \u22BD $expr2$ \u22BD ... +
      Implements the logical NOR function. The same as 'Not[Or['$expr1$, $expr2$, ...']]'
      >> Nor[True, False] = False @@ -487,7 +504,7 @@ class Xor(BinaryOperator): If an expression does not evaluate to 'True' or 'False', 'Xor' returns a result in symbolic form: >> Xor[a, False, b] - = a \u22BB b + = a \\[Xor] b #> Xor[] = False #> Xor[a] @@ -497,7 +514,7 @@ class Xor(BinaryOperator): #> Xor[True] = True #> Xor[a, b] - = a \u22BB b + = a \\[Xor] b """ attributes = A_FLAT | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED @@ -505,7 +522,7 @@ class Xor(BinaryOperator): precedence = 215 summary_text = "logic (exclusive) disjunction" - def apply(self, args, evaluation): + def eval(self, args, evaluation: Evaluation): "Xor[args___]" args = args.get_sequence() diff --git a/mathics/builtin/testing_expressions/numerical_properties.py b/mathics/builtin/testing_expressions/numerical_properties.py new file mode 100644 index 000000000..8de45e8b1 --- /dev/null +++ b/mathics/builtin/testing_expressions/numerical_properties.py @@ -0,0 +1,571 @@ +""" +Numerical Properties +""" +from itertools import combinations + +import sympy + +from mathics.builtin.base import Builtin, SympyFunction, Test +from mathics.core.atoms import Integer, Integer0, Number +from mathics.core.attributes import A_LISTABLE, A_NUMERIC_FUNCTION, A_PROTECTED +from mathics.core.convert.python import from_bool, from_python +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.symbols import BooleanType, SymbolFalse, SymbolTrue +from mathics.core.systemsymbols import SymbolExpandAll, SymbolSimplify +from mathics.eval.nevaluator import eval_N + + +class CoprimeQ(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/CoprimeQ.html + +
      +
      'CoprimeQ[$x$, $y$]' +
      tests whether $x$ and $y$ are coprime by computing their greatest \ + common divisor. +
      + + >> CoprimeQ[7, 9] + = True + + >> CoprimeQ[-4, 9] + = True + + >> CoprimeQ[12, 15] + = False + + CoprimeQ also works for complex numbers + >> CoprimeQ[1+2I, 1-I] + = True + + >> CoprimeQ[4+2I, 6+3I] + = True + + >> CoprimeQ[2, 3, 5] + = True + + >> CoprimeQ[2, 4, 5] + = False + """ + + attributes = A_LISTABLE | A_PROTECTED + summary_text = "test whether elements are coprime" + + def eval(self, args, evaluation: Evaluation): + "CoprimeQ[args__]" + + py_args = [arg.to_python() for arg in args.get_sequence()] + if not all(isinstance(i, int) or isinstance(i, complex) for i in py_args): + return SymbolFalse + + if all(sympy.gcd(n, m) == 1 for (n, m) in combinations(py_args, 2)): + return SymbolTrue + else: + return SymbolFalse + + +class EvenQ(Test): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/EvenQ.html + +
      +
      'EvenQ[$x$]' +
      returns 'True' if $x$ is even, and 'False' otherwise. +
      + + >> EvenQ[4] + = True + >> EvenQ[-3] + = False + >> EvenQ[n] + = False + """ + + attributes = A_LISTABLE | A_PROTECTED + summary_text = "test whether one number is divisible by the other" + + def test(self, n) -> bool: + value = n.get_int_value() + return value is not None and value % 2 == 0 + + +class ExactNumberQ(Test): + """ + :WMA link: + https://reference.wolfram.com/language/ref/ExactNumberQ.html + +
      +
      'ExactNumberQ[$expr$]' +
      returns 'True' if $expr$ is an exact real or complex number, and returns + 'False' otherwise. +
      + + >> ExactNumberQ[10] + = True + + 'ExactNumber[]' of a Real or MachineReal is 'False' + >> ExactNumberQ[10.0] + = False + + 'ExactNumberQ' for complex numbers: + >> ExactNumberQ[I] + = True + + >> ExactNumberQ[1 + I] + = True + + but not when composed with a Real: + >> ExactNumberQ[1. + I] + = False + + + 'ExactNumber[]' is 'True' for Rational numbers: + >> ExactNumberQ[5/6] + = True + + >> ExactNumberQ[4 * I + 5/6] + = True + + """ + + attributes = A_PROTECTED + + summary_text = "test if an expression is an exact real or complex number" + + def test(self, expr) -> bool: + """ + This function is the the eval() function for a Test subclass. + It is called by Test.eval(). + Note that this function must return a bool, not a BaseExpression. + """ + return isinstance(expr, Number) and not expr.is_inexact() + + +class InexactNumberQ(Test): + """ + :WMA link: + https://reference.wolfram.com/language/ref/InexactNumberQ.html + +
      +
      'InexactNumberQ[$expr$]' +
      returns 'True' if $expr$ is not an exact real or complex number + number, and 'False' otherwise. +
      + + >> InexactNumberQ[a] + = False + >> InexactNumberQ[3.0] + = True + >> InexactNumberQ[2/3] + = False + + 'InexactNumberQ' is 'True' for complex numbers: + + >> InexactNumberQ[4.0+I] + = True + """ + + summary_text = "test if an expression is an not exact real or complex number" + + def test(self, expr) -> bool: + """ + This function is the the eval() function for a Test subclass. + It is called by Test.eval(). + Note that this function must return a bool, not a BaseExpression. + """ + return isinstance(expr, Number) and expr.is_inexact() + + +class IntegerQ(Test): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/IntegerQ.html + +
      +
      'IntegerQ[$expr$]' +
      returns 'True' if $expr$ is an integer, and 'False' otherwise. +
      + + >> IntegerQ[3] + = True + >> IntegerQ[Pi] + = False + """ + + summary_text = "test whether an expression is an integer" + + def test(self, expr): + return isinstance(expr, Integer) + + +class MachineNumberQ(Test): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/MachineNumberQ.html + +
      +
      'MachineNumberQ[$expr$]' +
      returns 'True' if $expr$ is a machine-precision real or complex number. +
      + + = True + >> MachineNumberQ[3.14159265358979324] + = False + >> MachineNumberQ[1.5 + 2.3 I] + = True + >> MachineNumberQ[2.71828182845904524 + 3.14159265358979324 I] + = False + #> MachineNumberQ[1.5 + 3.14159265358979324 I] + = True + #> MachineNumberQ[1.5 + 5 I] + = True + """ + + summary_text = "test if expression is a machine precision real or complex number" + + def test(self, expr) -> bool: + return expr.is_machine_precision() + + +class Negative(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/Negative.html + +
      +
      'Negative[$x$]' +
      returns 'True' if $x$ is a negative real number. +
      + >> Negative[0] + = False + >> Negative[-3] + = True + >> Negative[10/7] + = False + >> Negative[1+2I] + = False + >> Negative[a + b] + = Negative[a + b] + #> Negative[-E] + = True + #> Negative[Sin[{11, 14}]] + = {True, False} + """ + + attributes = A_LISTABLE | A_PROTECTED + + rules = { + "Negative[x_?NumericQ]": "If[x < 0, True, False, False]", + } + summary_text = "test whether an expression is a negative number" + + +class NonNegative(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/NonNegative.html + +
      +
      'NonNegative[$x$]' +
      returns 'True' if $x$ is a positive real number or zero. +
      + + >> {Positive[0], NonNegative[0]} + = {False, True} + """ + + attributes = A_LISTABLE | A_PROTECTED + + rules = { + "NonNegative[x_?NumericQ]": "If[x >= 0, True, False, False]", + } + summary_text = "test whether an expression is a non-negative number" + + +class NonPositive(Builtin): + """ + :WMA link:https://reference.wolfram.com/language/ref/NonPositive.html + +
      +
      'NonPositive[$x$]' +
      returns 'True' if $x$ is a negative real number or zero. +
      + + >> {Negative[0], NonPositive[0]} + = {False, True} + """ + + attributes = A_LISTABLE | A_PROTECTED + + rules = { + "NonPositive[x_?NumericQ]": "If[x <= 0, True, False, False]", + } + summary_text = "test whether an expression is a non-positive number" + + +class NumberQ(Test): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/NumberQ.html + +
      +
      'NumberQ[$expr$]' +
      returns 'True' if $expr$ is an explicit number, and 'False' \ + otherwise. +
      + + >> NumberQ[3+I] + = True + >> NumberQ[5!] + = True + >> NumberQ[Pi] + = False + """ + + summary_text = "test whether an expression is a number" + + def test(self, expr) -> bool: + return isinstance(expr, Number) + + +class NumericQ(Builtin): + """ + :WMA link: + https://reference.wolfram.com/language/ref/NumericQ.html + +
      +
      'NumericQ[$expr$]' +
      tests whether $expr$ represents a numeric quantity. +
      + + >> NumericQ[2] + = True + >> NumericQ[Sqrt[Pi]] + = True + >> NumberQ[Sqrt[Pi]] + = False + + It is possible to set that a symbol is numeric or not by assign a boolean value + to ``NumericQ`` + >> NumericQ[a]=True + = True + >> NumericQ[a] + = True + >> NumericQ[Sin[a]] + = True + + Clear and ClearAll do not restore the default value. + + >> Clear[a]; NumericQ[a] + = True + >> ClearAll[a]; NumericQ[a] + = True + >> NumericQ[a]=False; NumericQ[a] + = False + NumericQ can only set to True or False + >> NumericQ[a] = 37 + : Cannot set NumericQ[a] to 37; the lhs argument must be a symbol and the rhs must be True or False. + = 37 + """ + + messages = { + "argx": "NumericQ called with `1` arguments; 1 argument is expected.", + "set": "Cannot set `1` to `2`; the lhs argument must be a symbol and the rhs must be True or False.", + } + summary_text = "test whether an expression is a number" + + def eval(self, expr, evaluation): + "NumericQ[expr_]" + return from_bool(expr.is_numeric(evaluation)) + + +class OddQ(Test): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/OddQ.html + +
      +
      'OddQ[$x$]' +
      returns 'True' if $x$ is odd, and 'False' otherwise. +
      + + >> OddQ[-3] + = True + >> OddQ[0] + = False + """ + + attributes = A_LISTABLE | A_PROTECTED + summary_text = "test whether elements are odd numbers" + + def test(self, n) -> bool: + value = n.get_int_value() + return value is not None and value % 2 != 0 + + +class PossibleZeroQ(SympyFunction): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/PossibleZeroQ.html + +
      +
      'PossibleZeroQ[$expr$]' +
      returns 'True' if basic symbolic and numerical methods suggest that \ + expr has value zero, and 'False' otherwise. +
      + + Test whether a numeric expression is zero: + >> PossibleZeroQ[E^(I Pi/4) - (-1)^(1/4)] + = True + + The determination is approximate. + + Test whether a symbolic expression is likely to be identically zero: + >> PossibleZeroQ[(x + 1) (x - 1) - x^2 + 1] + = True + + + >> PossibleZeroQ[(E + Pi)^2 - E^2 - Pi^2 - 2 E Pi] + = True + + Show that a numeric expression is nonzero: + >> PossibleZeroQ[E^Pi - Pi^E] + = False + + >> PossibleZeroQ[1/x + 1/y - (x + y)/(x y)] + = True + + Decide that a numeric expression is zero, based on approximate computations: + >> PossibleZeroQ[2^(2 I) - 2^(-2 I) - 2 I Sin[Log[4]]] + = True + + >> PossibleZeroQ[Sqrt[x^2] - x] + = False + """ + + summary_text = "test whether an expression is estimated to be zero" + attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED + + sympy_name = "_iszero" + + def eval(self, expr, evaluation): + "%(name)s[expr_]" + from sympy.matrices.utilities import _iszero + + sympy_expr = expr.to_sympy() + result = _iszero(sympy_expr) + if result is None: + # try expanding the expression + exprexp = Expression(SymbolExpandAll, expr).evaluate(evaluation) + exprexp = exprexp.to_sympy() + result = _iszero(exprexp) + if result is None: + # Can't get exact answer, so try approximate equal + numeric_val = eval_N(expr, evaluation) + if numeric_val and hasattr(numeric_val, "is_approx_zero"): + result = numeric_val.is_approx_zero + elif not numeric_val.is_numeric(evaluation): + return ( + SymbolTrue + if Expression(SymbolSimplify, expr).evaluate(evaluation) == Integer0 + else SymbolFalse + ) + + return from_python(result) + + +class Positive(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/Positive.html + +
      +
      'Positive[$x$]' +
      returns 'True' if $x$ is a positive real number. +
      + + >> Positive[1] + = True + + 'Positive' returns 'False' if $x$ is zero or a complex number: + >> Positive[0] + = False + >> Positive[1 + 2 I] + = False + + #> Positive[Pi] + = True + #> Positive[x] + = Positive[x] + #> Positive[Sin[{11, 14}]] + = {False, True} + """ + + attributes = A_LISTABLE | A_PROTECTED + + rules = { + "Positive[x_?NumericQ]": "If[x > 0, True, False, False]", + } + summary_text = "test whether an expression is a positive number" + + +class PrimeQ(SympyFunction): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/PrimeQ.html + +
      +
      'PrimeQ[$n$]' +
      returns 'True' if $n$ is a prime number. +
      + + For very large numbers, 'PrimeQ' uses probabilistic prime testing, so it might be wrong sometimes + (a number might be composite even though 'PrimeQ' says it is prime). + The algorithm might be changed in the future. + + >> PrimeQ[2] + = True + >> PrimeQ[-3] + = True + >> PrimeQ[137] + = True + >> PrimeQ[2 ^ 127 - 1] + = True + + #> PrimeQ[1] + = False + #> PrimeQ[2 ^ 255 - 1] + = False + + All prime numbers between 1 and 100: + >> Select[Range[100], PrimeQ] + = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97} + + 'PrimeQ' has attribute 'Listable': + >> PrimeQ[Range[20]] + = {False, True, True, False, True, False, True, False, False, False, True, False, True, False, False, False, True, False, True, False} + """ + + attributes = A_LISTABLE | A_NUMERIC_FUNCTION | A_PROTECTED + sympy_name = "isprime" + summary_text = "test whether elements are prime numbers" + + def eval(self, n, evaluation: Evaluation) -> BooleanType: + "PrimeQ[n_]" + + n = n.get_int_value() + if n is None: + return SymbolFalse + + n = abs(n) + return SymbolTrue if sympy.isprime(n) else SymbolFalse diff --git a/mathics/builtin/trace.py b/mathics/builtin/trace.py index a523baa35..3cca7c8c4 100644 --- a/mathics/builtin/trace.py +++ b/mathics/builtin/trace.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - """ Tracing Built-in Functions @@ -7,7 +6,8 @@ getting evaluated and where the time is spent in evaluation. With this, it may be possible for both users and implementers to follow \ -how Mathics arrives at its results, or guide how to speed up expression evaluation. +how Mathics3 arrives at its results, or guide how to speed up expression \ +evaluation. """ @@ -24,7 +24,7 @@ from mathics.core.symbols import SymbolFalse, SymbolNull, SymbolTrue, strip_context -def traced_do_replace(self, expression, vars, options, evaluation): +def traced_do_replace(self, expression, vars, options: dict, evaluation: Evaluation): if options and self.check_options: if not self.check_options(options, evaluation): return None @@ -80,7 +80,7 @@ class ClearTrace(Builtin): summary_text = "clear any statistics collected for Built-in functions" - def apply(self, evaluation): + def eval(self, evaluation: Evaluation): "%(name)s[]" TraceBuiltins.function_stats: "defaultdict" = defaultdict( @@ -124,7 +124,7 @@ class PrintTrace(_TraceBase): summary_text = "print statistics collected for Built-in functions" - def apply(self, evaluation, options={}): + def eval(self, evaluation, options={}): "%(name)s[OptionsPattern[%(name)s]]" TraceBuiltins.dump_tracing_stats( @@ -229,7 +229,7 @@ def disable_trace(evaluation) -> None: BuiltinRule.do_replace = TraceBuiltins.do_replace_copy evaluation.definitions = TraceBuiltins.definitions_copy - def apply(self, expr, evaluation, options={}): + def eval(self, expr, evaluation, options={}): "%(name)s[expr_, OptionsPattern[%(name)s]]" # Reset function_stats @@ -295,12 +295,12 @@ class TraceBuiltinsVariable(Builtin): summary_text = "enable or disable Built-in function evaluation statistics" - def apply_get(self, evaluation): + def eval_get(self, evaluation: Evaluation): "%(name)s" return self.value - def apply_set(self, value, evaluation): + def eval_set(self, value, evaluation: Evaluation): "%(name)s = value_" if value is SymbolTrue: @@ -339,7 +339,7 @@ class TraceEvaluation(Builtin): } summary_text = "trace the succesive evaluations" - def apply(self, expr, evaluation, options): + def eval(self, expr, evaluation: Evaluation, options: dict): "TraceEvaluation[expr_, OptionsPattern[]]" curr_trace_evaluation = evaluation.definitions.trace_evaluation curr_time_by_steps = evaluation.definitions.timing_trace_evaluation @@ -393,11 +393,11 @@ class TraceEvaluationVariable(Builtin): summary_text = "enable or disable displaying the steps to get the result" - def apply_get(self, evaluation): + def eval_get(self, evaluation: Evaluation): "%(name)s" return from_bool(evaluation.definitions.trace_evaluation) - def apply_set(self, value, evaluation): + def eval_set(self, value, evaluation: Evaluation): "%(name)s = value_" if value is SymbolTrue: evaluation.definitions.trace_evaluation = True diff --git a/mathics/builtin/vectors/__init__.py b/mathics/builtin/vectors/__init__.py index 51ffd7e09..4e1ad3afe 100644 --- a/mathics/builtin/vectors/__init__.py +++ b/mathics/builtin/vectors/__init__.py @@ -1,11 +1,15 @@ """ Operations on Vectors -In mathematics and physics, a vector is a term that refers colloquially to some quantities that cannot be expressed by a single number. It is also a row or column of a matrix. +In mathematics and physics, a vector is a term that refers colloquially to \ +some quantities that cannot be expressed by a single number. It is also a \ +row or column of a matrix. -In computer science, it is an array datas structure consiting of collection of elements identified by at least on array index or key. +In computer science, it is an array data structure consisting of collection \ +of elements identified by at least on array index or key. -In Mathics vectors as are Lists. one never needs to distinguish between row and column vectors. As with other objects vectors can mix number and symbolic elements. +In \Mathics vectors as are Lists. One never needs to distinguish between row \ +and column vectors. As with other objects vectors can mix number and symbolic elements. Vectors can be long, dense, or sparse. diff --git a/mathics/builtin/vectors/math_ops.py b/mathics/builtin/vectors/math_ops.py index 941efec61..57e0ddb7a 100644 --- a/mathics/builtin/vectors/math_ops.py +++ b/mathics/builtin/vectors/math_ops.py @@ -9,7 +9,7 @@ from mathics.builtin.base import Builtin, SympyFunction from mathics.core.attributes import A_PROTECTED from mathics.core.convert.sympy import from_sympy, to_sympy_matrix -from mathics.eval.math_ops import eval_2_Norm, eval_p_norm +from mathics.eval.math_ops import eval_Norm, eval_Norm_p class Cross(Builtin): @@ -32,7 +32,7 @@ class Cross(Builtin): >> Cross[{x1, y1, z1}, {x2, y2, z2}] = {y1 z2 - y2 z1, -x1 z2 + x2 z1, x1 y2 - x2 y1} - Cross is antisymmetric, so: + 'Cross' is antisymmetric, so: >> Cross[{x, y}] = {-y, x} @@ -43,6 +43,7 @@ class Cross(Builtin): = {-Sqrt[3], 1} Visualize this: + >> Graphics[{Arrow[{{0, 0}, v1}], Red, Arrow[{{0, 0}, v2}]}, Axes -> True] = -Graphics- @@ -60,8 +61,9 @@ class Cross(Builtin): "their length." ) } + rules = {"Cross[{x_, y_}]": "{-y, x}"} - summary_text = "vector cross product" + summary_text = "get vector cross product" def eval(self, a, b, evaluation): "Cross[a_, b_]" @@ -69,12 +71,14 @@ def eval(self, a, b, evaluation): b = to_sympy_matrix(b) if a is None or b is None: - return evaluation.message("Cross", "nonn1") + evaluation.message("Cross", "nonn1") + return try: res = a.cross(b) except sympy.ShapeError: - return evaluation.message("Cross", "nonn1") + evaluation.message("Cross", "nonn1") + return return from_sympy(res) @@ -119,14 +123,15 @@ class Curl(SympyFunction): D[f2, x1] - D[f1, x2] }""", } - summary_text = "curl vector operator" + summary_text = "get vector curl" sympy_name = "curl" class Norm(Builtin): """ - :Matrix norms induced by vector p-norms: https://en.wikipedia.org/wiki/Matrix_norm#Matrix_norms_induced_by_vector_p-norms ( + :Matrix norms induced by vector p-norms: + https://en.wikipedia.org/wiki/Matrix_norm#Matrix_norms_induced_by_vector_p-norms ( :SymPy: https://docs.sympy.org/latest/modules/matrices/matrices.html#sympy.matrices.matrices.MatrixBase.norm, :WMA: @@ -140,7 +145,7 @@ class Norm(Builtin):
      computes the 2-norm of matrix m.
      - The Norm of of a vector is its Euclidian distance: + The 'Norm' of of a vector is its Euclidean distance: >> Norm[{x, y, z}] = Sqrt[Abs[x] ^ 2 + Abs[y] ^ 2 + Abs[z] ^ 2] @@ -161,7 +166,7 @@ class Norm(Builtin): For complex numbers, 'Norm[$z$]' is 'Abs[$z$]': >> Norm[1 + I] = Sqrt[2] - so the norm is always real even when the input is complex. + So the norm is always real, even when the input is complex. 'Norm'[$m$,"Frobenius"] gives the Frobenius norm of $m$: @@ -184,15 +189,15 @@ class Norm(Builtin): "Norm[m_?NumberQ]": "Abs[m]", "Norm[m_?VectorQ, DirectedInfinity[1]]": "Max[Abs[m]]", } - summary_text = "norm of a vector or matrix" + summary_text = "get norm of a vector or matrix" - def eval_two_norm(self, m, evaluation): + def eval(self, m, evaluation): "Norm[m_]" - return eval_2_Norm(m, evaluation) + return eval_Norm(m, evaluation) - def eval_p_norm(self, m, p, evaluation): + def eval_with_p(self, m, p, evaluation): "Norm[m_, p_]" - return eval_p_norm(m, p, evaluation) + return eval_Norm_p(m, p, evaluation) # TODO: Div diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index 0f6c13b91..ab10eacee 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -6,7 +6,6 @@ from functools import reduce from typing import Optional, Tuple -from mathics.algorithm.parts import walk_parts from mathics.core.atoms import Atom, Integer from mathics.core.attributes import A_LOCKED, A_PROTECTED, attribute_string_to_number from mathics.core.element import BaseElement @@ -35,6 +34,7 @@ SymbolPattern, SymbolRuleDelayed, ) +from mathics.eval.parts import walk_parts class AssignmentException(Exception): diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index 0a9f3630c..f2b5c91a1 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -10,7 +10,13 @@ import sympy from mathics.core.element import BoxElementMixin, ImmutableValueMixin -from mathics.core.number import dps, machine_digits, machine_precision, min_prec, prec +from mathics.core.number import ( + FP_MANTISA_BINARY_DIGITS, + MACHINE_PRECISION_VALUE, + dps, + min_prec, + prec, +) from mathics.core.symbols import ( Atom, NumericOperators, @@ -155,7 +161,6 @@ class Integer(Number): # clearing the cache and the object store which might be useful in implementing # Builtin Share[]. def __new__(cls, value) -> "Integer": - n = int(value) self = cls._integers.get(value) if self is None: @@ -243,14 +248,19 @@ def to_sympy(self, **kwargs): def to_python(self, *args, **kwargs): return self.value - def round(self, d=None) -> Union["MachineReal", "PrecisionReal"]: + def round(self, d: Optional[int] = None) -> Union["MachineReal", "PrecisionReal"]: + """ + Produce a Real approximation of ``self`` with decimal precision ``d``. + If ``d`` is ``None``, and self.value fits in a float, + returns a ``MachineReal`` number. + Is the low-level equivalent to ``N[self, d]``. + """ if d is None: d = self.value.bit_length() - if d <= machine_precision: + if d <= FP_MANTISA_BINARY_DIGITS: return MachineReal(float(self.value)) else: - # machine_precision / log_2(10) + 1 - d = machine_digits + d = MACHINE_PRECISION_VALUE return PrecisionReal(sympy.Float(self.value, d)) def get_int_value(self) -> int: @@ -291,32 +301,36 @@ class Real(Number): # __new__ rather than __init__ is used here because the kind of # object created differs based on contents of "value". - def __new__(cls, value, p=None) -> "Real": + def __new__(cls, value, p: int = None) -> "Real": """ Return either a MachineReal or a PrecisionReal object. - Or raise a TypeError + Or raise a TypeError. + p is the number of binary digits of precision. """ if isinstance(value, str): value = str(value) if p is None: digits = ("".join(re.findall("[0-9]+", value))).lstrip("0") if digits == "": # Handle weird Mathematica zero case - p = max(prec(len(value.replace("0.", ""))), machine_precision) + p = max( + prec(len(value.replace("0.", ""))), FP_MANTISA_BINARY_DIGITS + ) else: - p = prec(len(digits.zfill(dps(machine_precision)))) + p = prec(len(digits.zfill(dps(FP_MANTISA_BINARY_DIGITS)))) elif isinstance(value, sympy.Float): if p is None: p = value._prec + 1 elif isinstance(value, (Integer, sympy.Number, mpmath.mpf, float, int)): - if p is not None and p > machine_precision: + if p is not None and p > FP_MANTISA_BINARY_DIGITS: value = str(value) else: raise TypeError("Unknown number type: %s (type %s)" % (value, type(value))) # return either machine precision or arbitrary precision real - if p is None or p == machine_precision: + if p is None or p == FP_MANTISA_BINARY_DIGITS: return MachineReal.__new__(MachineReal, value) else: + # TODO: check where p is set in value: return PrecisionReal.__new__(PrecisionReal, value) def __eq__(self, other) -> bool: @@ -334,8 +348,8 @@ def __eq__(self, other) -> bool: def __hash__(self): # ignore last 7 binary digits when hashing - _prec = self.get_precision() - return hash(("Real", self.to_sympy().n(dps(_prec)))) + _prec = dps(self.get_precision()) + return hash(("Real", self.to_sympy().n(_prec))) def __ne__(self, other) -> bool: # Real is a total order @@ -349,8 +363,8 @@ def is_nan(self, d=None) -> bool: def user_hash(self, update): # ignore last 7 binary digits when hashing - _prec = self.get_precision() - update(b"System`Real>" + str(self.to_sympy().n(dps(_prec))).encode("utf8")) + _prec = dps(self.get_precision()) + update(b"System`Real>" + str(self.to_sympy().n(_prec)).encode("utf8")) # Has to come before PrecisionReal @@ -402,7 +416,7 @@ def do_copy(self) -> "MachineReal": def get_precision(self) -> float: """Returns the default specification for precision in N and other numerical functions.""" - return machine_precision + return FP_MANTISA_BINARY_DIGITS def get_float_value(self, permit_complex=False) -> float: return self.value @@ -431,7 +445,10 @@ def make_boxes(self, form): def is_zero(self) -> bool: return self.value == 0.0 - def round(self, d=None) -> "MachineReal": + def round(self, d: Optional[int] = None) -> "MachineReal": + """ + Produce a Real approximation of ``self`` with decimal precision ``d``. + """ return self def sameQ(self, other) -> bool: @@ -530,12 +547,11 @@ def make_boxes(self, form): self, dps(self.get_precision()), None, None, _number_form_options ) - def round(self, d=None) -> Union[MachineReal, "PrecisionReal"]: + def round(self, d: Optional[int] = None) -> Union[MachineReal, "PrecisionReal"]: if d is None: return MachineReal(float(self.value)) - else: - d = min(dps(self.get_precision()), d) - return PrecisionReal(self.value.n(d)) + _prec = min(prec(d), self.value._prec) + return PrecisionReal(sympy.Float(self.value, precision=_prec)) def sameQ(self, other) -> bool: """Mathics SameQ for PrecisionReal""" @@ -672,12 +688,16 @@ class Complex(Number): # clearing the cache and the object store which might be useful in implementing # Builtin Share[]. def __new__(cls, real, imag): - if isinstance(real, Complex) or not isinstance(real, Number): - raise ValueError("Argument 'real' must be a Real number.") + if not isinstance(real, (Integer, Real, Rational)): + raise ValueError( + f"Argument 'real' must be an Integer, Real, or Rational type; is {real}." + ) if imag is SymbolInfinity: return SymbolI * SymbolInfinity - if isinstance(imag, Complex) or not isinstance(imag, Number): - raise ValueError("Argument 'imag' must be a Real number.") + if not isinstance(imag, (Integer, Real, Rational)): + raise ValueError( + f"Argument 'image' must be an Integer, Real, or Rational type; is {imag}." + ) if imag.sameQ(Integer0): return real @@ -690,7 +710,6 @@ def __new__(cls, real, imag): value = (real, imag) self = cls._complex_numbers.get(value) if self is None: - self = super().__new__(cls) self.real = real self.imag = imag @@ -841,7 +860,6 @@ class Rational(Number): # clearing the cache and the object store which might be useful in implementing # Builtin Share[]. def __new__(cls, numerator, denominator=1) -> "Rational": - value = sympy.Rational(numerator, denominator) key = (cls, value) self = cls._rationals.get(key) @@ -924,6 +942,9 @@ def is_zero(self) -> bool: RationalOneHalf = Rational(1, 2) +RationalMinusOneHalf = Rational(-1, 2) +MATHICS3_COMPLEX_I = Complex(Integer0, Integer1) +MATHICS3_COMPLEX_I_NEG = Complex(Integer0, IntegerM1) class String(Atom, BoxElementMixin): @@ -1013,3 +1034,10 @@ def __new__(cls, value): if math.inf == value: self.value = "math.inf" return self + + +def is_integer_rational_or_real(expr) -> bool: + """ + Return True is expr is either an Integer, Rational, or Real. + """ + return isinstance(expr, (Integer, Rational, Real)) diff --git a/mathics/core/convert/expression.py b/mathics/core/convert/expression.py index ec93b08ab..fc96fd537 100644 --- a/mathics/core/convert/expression.py +++ b/mathics/core/convert/expression.py @@ -66,7 +66,7 @@ def to_expression_with_specialization( def to_mathics_list( *elements: Any, elements_conversion_fn: Callable = from_python, is_literal=False -) -> Expression: +) -> ListExpression: """ This is an expression constructor for list that can be used when the elements are not Mathics objects. For example: diff --git a/mathics/core/convert/mpmath.py b/mathics/core/convert/mpmath.py index 655189cf4..485fd2740 100644 --- a/mathics/core/convert/mpmath.py +++ b/mathics/core/convert/mpmath.py @@ -1,37 +1,58 @@ # -*- coding: utf-8 -*- from functools import lru_cache +from typing import Optional, Union import mpmath import sympy from mathics.core.atoms import Complex, MachineReal, MachineReal0, PrecisionReal +from mathics.core.element import BaseElement +from mathics.core.expression_predefined import ( + MATHICS3_COMPLEX_INFINITY, + MATHICS3_I_INFINITY, + MATHICS3_I_NEG_INFINITY, + MATHICS3_INFINITY, + MATHICS3_NEG_INFINITY, +) +from mathics.core.systemsymbols import SymbolIndeterminate @lru_cache(maxsize=1024) -def from_mpmath(value, prec=None, acc=None): - "Converts mpf or mpc to Number." +def from_mpmath( + value: Union[mpmath.mpf, mpmath.mpc], + precision: Optional[int] = None, +) -> BaseElement: + """ + Converts mpf or mpc to Number. + The optional parameter `precision` represents + the binary precision. + """ + if mpmath.isnan(value): + return SymbolIndeterminate if isinstance(value, mpmath.mpf): - # if accuracy is given, override - # prec: - if acc is not None: - prec = acc - if value != 0.0: - offset = mpmath.log(-value if value < 0.0 else value, 10) - prec += offset - if prec is None: + if mpmath.isinf(value): + return MATHICS3_INFINITY if value > 0 else MATHICS3_NEG_INFINITY + if precision is None: return MachineReal(float(value)) # If the error if of the order of the number, the number # is compatible with 0. - if prec < 1.0: + if precision < 1: return MachineReal0 # HACK: use str here to prevent loss of precision - return PrecisionReal(sympy.Float(str(value), prec)) + return PrecisionReal(sympy.Float(str(value), precision=precision - 1)) elif isinstance(value, mpmath.mpc): if value.imag == 0.0: - return from_mpmath(value.real, prec, acc) - real = from_mpmath(value.real, prec, acc) - imag = from_mpmath(value.imag, prec, acc) + return from_mpmath(value.real, precision=precision) + val_re, val_im = value.real, value.imag + if mpmath.isinf(val_re): + if mpmath.isinf(val_im): + return MATHICS3_COMPLEX_INFINITY + return MATHICS3_INFINITY if val_re > 0 else MATHICS3_NEG_INFINITY + elif mpmath.isinf(val_im): + return MATHICS3_I_INFINITY if val_im > 0 else MATHICS3_I_NEG_INFINITY + real = from_mpmath(val_re, precision=precision) + imag = from_mpmath(val_im, precision=precision) return Complex(real, imag) else: raise TypeError(type(value)) diff --git a/mathics/core/convert/python.py b/mathics/core/convert/python.py index d8ed24dc6..5fc1061f5 100644 --- a/mathics/core/convert/python.py +++ b/mathics/core/convert/python.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Conversions between Python and Mathics +Conversions between Python and Mathics3 """ from typing import Any @@ -9,7 +9,7 @@ from mathics.core.number import get_type from mathics.core.symbols import ( BaseElement, - Symbol, + BooleanType, SymbolFalse, SymbolNull, SymbolTrue, @@ -17,9 +17,9 @@ from mathics.core.systemsymbols import SymbolByteArray, SymbolRule -def from_bool(arg: bool) -> Symbol: +def from_bool(arg: bool) -> BooleanType: """ - Conversion from a bool to something Mathics can use. + Conversion from a bool to something Mathics3 can use. """ return SymbolTrue if arg else SymbolFalse diff --git a/mathics/core/convert/sympy.py b/mathics/core/convert/sympy.py index 84c29c1b4..980da1d69 100644 --- a/mathics/core/convert/sympy.py +++ b/mathics/core/convert/sympy.py @@ -168,7 +168,7 @@ def from_sympy(expr): from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.expression import Expression from mathics.core.list import ListExpression - from mathics.core.number import machine_precision + from mathics.core.number import FP_MANTISA_BINARY_DIGITS from mathics.core.symbols import Symbol, SymbolNull if isinstance(expr, (tuple, list)): @@ -240,7 +240,7 @@ def from_sympy(expr): return SymbolIndeterminate return Rational(numerator, denominator) elif isinstance(expr, sympy.Float): - if expr._prec == machine_precision: + if expr._prec == FP_MANTISA_BINARY_DIGITS: return MachineReal(float(expr)) return Real(expr) elif isinstance(expr, sympy.core.numbers.NaN): @@ -252,10 +252,14 @@ def from_sympy(expr): elif expr is sympy.false: return SymbolFalse - elif expr.is_number and all([x.is_Number for x in expr.as_real_imag()]): - # Hack to convert 3 * I to Complex[0, 3] - return Complex(*[from_sympy(arg) for arg in expr.as_real_imag()]) - elif expr.is_Add: + if expr.is_number and all([x.is_Number for x in expr.as_real_imag()]): + # Hack to convert * I to Complex[0, ] + try: + return Complex(*[from_sympy(arg) for arg in expr.as_real_imag()]) + except ValueError: + # The exception happens if one of the components is infinity + pass + if expr.is_Add: return to_expression( SymbolPlus, *sorted([from_sympy(arg) for arg in expr.args]) ) diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 984a05294..2faccb55a 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- - import base64 import bisect import os import pickle import re from collections import defaultdict +from os.path import join as osp_join from typing import List, Optional from mathics_scanner.tokeniser import full_names_pattern @@ -47,12 +47,19 @@ def valuesname(name) -> str: def autoload_files( defs, root_dir_path: str, autoload_dir: str, block_global_definitions: bool = True ): + """ + Load Mathics code from the autoload-folder files. + """ from mathics.core.evaluation import Evaluation - # Load symbols from the autoload folder - for root, dirs, files in os.walk(os.path.join(root_dir_path, autoload_dir)): - for path in [os.path.join(root, f) for f in files if f.endswith(".m")]: + for root, dirs, files in os.walk(osp_join(root_dir_path, autoload_dir)): + for path in [osp_join(root, f) for f in files if f.endswith(".m")]: + # Autoload definitions should be go in the System context + # by default, rather than the Global context. + defs.set_current_context("System`") Expression(SymbolGet, String(path)).evaluate(Evaluation(defs)) + # Restore default context to Global + defs.set_current_context("Global`") if block_global_definitions: # Move any user definitions created by autoloaded files to @@ -67,6 +74,13 @@ def autoload_files( if name.startswith("Global`"): raise ValueError("autoload defined %s." % name) + # Move the user definitions to builtin: + for symbol_name in defs.user: + defs.builtin[symbol_name] = defs.get_definition(symbol_name) + + defs.user = {} + defs.clear_cache() + class Definitions: """ @@ -110,7 +124,7 @@ def __init__( # Rocky: this smells of something not quite right in terms of # modularity. import mathics.format # noqa - from mathics.core.pymathics import PyMathicsLoadException, load_pymathics_module + from mathics.eval.pymathics import PyMathicsLoadException, load_pymathics_module self.printforms = list(PrintForms) self.outputforms = list(OutputForms) @@ -145,22 +159,6 @@ def __init__( autoload_files(self, ROOT_DIR, "autoload") - # Move any user definitions created by autoloaded files to - # builtins, and clear out the user definitions list. This - # means that any autoloaded definitions become shared - # between users and no longer disappear after a Quit[]. - # - # Autoloads that accidentally define a name in Global` - # could cause confusion, so check for this. - # - for name in self.user: - if name.startswith("Global`"): - raise ValueError("autoload defined %s." % name) - - self.builtin.update(self.user) - self.user = {} - self.clear_cache() - def clear_cache(self, name=None): # the definitions cache (self.definitions_cache) caches (incomplete and complete) names -> Definition(), # e.g. "xy" -> d and "MyContext`xy" -> d. we need to clear this cache if a Definition() changes (which diff --git a/mathics/core/element.py b/mathics/core/element.py index 194ab7fc1..ec569638b 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -128,7 +128,7 @@ def is_literal(self) -> bool: class KeyComparable: """ - Some Mathics/WL Symbols have an "OrderLess" attribute + Some Mathics3/WL Symbols have an "OrderLess" attribute which is used in the evaluation process to arrange items in a list. To do that, we need a way to compare Symbols, and that is what @@ -141,7 +141,7 @@ class KeyComparable: mixed into other classes. Each class should provide a `get_sort_key()` method which - is the primative from which all other comparsions are based on. + is the primative from which all other comparisons are based on. """ # FIXME: return type should be a specific kind of Tuple, not a list. @@ -163,8 +163,8 @@ def get_sort_key(self) -> list: then self comes before expr. - The values in the positions of the list/tuple are used to indicate how comparison should be - treated for specific element classes. + The values in the positions of the list/tuple are used to indicate how + comparison should be treated for specific element classes. """ raise NotImplementedError @@ -344,7 +344,7 @@ def get_sequence(self) -> Union[tuple, list]: # Below, we special-case for SymbolSequence. Here is an example to suggest why. # Suppose we have this evaluation method: # - # def apply(x, evaluation): + # def eval(x, evaluation: Evaluation): # """F[x__]""" # args = x.get_sequence() # diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index d69d1b13c..a45b172f3 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -5,14 +5,14 @@ import time from queue import Queue from threading import Thread, stack_size as set_thread_stack_size -from typing import Tuple +from typing import List, Optional, Tuple, Union from mathics_scanner import TranslateError from mathics import settings from mathics.core.atoms import Integer, String from mathics.core.convert.python import from_python -from mathics.core.element import KeyComparable, ensure_context +from mathics.core.element import BaseElement, KeyComparable, ensure_context from mathics.core.interrupt import ( AbortInterrupt, BreakInterrupt, @@ -76,12 +76,12 @@ def _thread_target(request, queue) -> None: def python_recursion_depth(n) -> int: - # convert Mathics recursion depth to Python recursion depth. this estimates how many Python calls - # we need at worst to process one Mathics recursion. + # convert Mathics3 recursion depth to Python recursion depth. this estimates how many Python calls + # we need at worst to process one Mathics3 recursion. return 200 + 30 * n -def python_stack_size(n) -> int: # n is a Mathics recursion depth +def python_stack_size(n) -> int: # n is a Mathics3 recursion depth # python_stack_frame_size is the (maximum) number of bytes Python needs for one call on the stack. python_stack_frame_size = 512 # value estimated experimentally return python_recursion_depth(n) * python_stack_frame_size @@ -119,12 +119,12 @@ def run_with_timeout_and_stack(request, timeout, evaluation): # for a detailed discussion of this. # # To reduce this problem, we make use of specific properties of - # the Mathics evaluator: if we set "evaluation.timeout", the + # the Mathics3 evaluator: if we set "evaluation.timeout", the # next call to "Expression.evaluate" in the thread will finish it # immediately. # # However this still will not terminate long-running processes - # in Sympy or or libraries called by Mathics that might hang or run + # in Sympy or or libraries called by Mathics3 that might hang or run # for a long time. thread.join(timeout) if thread.is_alive(): @@ -142,7 +142,7 @@ def run_with_timeout_and_stack(request, timeout, evaluation): raise result[0].with_traceback(result[1], result[2]) -class Out(KeyComparable): +class _Out(KeyComparable): def __init__(self) -> None: self.is_message = False self.is_print = False @@ -152,80 +152,6 @@ def get_sort_key(self) -> Tuple[bool, bool, str]: return (self.is_message, self.is_print, self.text) -class Message(Out): - def __init__(self, symbol, tag, text: str) -> None: - super(Message, self).__init__() - self.is_message = True - self.symbol = symbol - self.tag = tag - self.text = text - - def __str__(self) -> str: - return "{}::{}: {}".format(self.symbol, self.tag, self.text) - - def __eq__(self, other) -> bool: - return self.is_message == other.is_message and self.text == other.text - - def get_data(self): - return { - "message": True, - "symbol": self.symbol, - "tag": self.tag, - "prefix": "%s::%s" % (self.symbol, self.tag), - "text": self.text, - } - - -class Print(Out): - def __init__(self, text) -> None: - super(Print, self).__init__() - self.is_print = True - self.text = text - - def __str__(self) -> str: - return self.text - - def __eq__(self, other) -> bool: - return self.is_message == other.is_message and self.text == other.text - - def get_data(self): - return { - "message": False, - "text": self.text, - } - - -class Result: - def __init__(self, out, result, line_no, last_eval=None, form=None) -> None: - self.out = out - self.result = result - self.line_no = line_no - self.last_eval = last_eval - self.form = form - - def get_data(self): - return { - "out": [out.get_data() for out in self.out], - "result": self.result, - "line": self.line_no, - "form": self.form, - } - - -class Output: - def max_stored_size(self, settings) -> int: - return settings.MAX_STORED_SIZE - - def out(self, out): - pass - - def clear(self, wait): - raise NotImplementedError - - def display(self, data, metadata): - raise NotImplementedError - - class Evaluation: def __init__( self, definitions=None, output=None, format="text", catch_interrupt=True @@ -269,10 +195,21 @@ def parse_evaluate(self, query, timeout=None): return self.evaluate(expr, timeout) def parse_feeder(self, feeder): - return self.parse_feeder_returning_code(feeder)[0] + return self.parse_feeder_returning_code_and_messages(feeder)[0] - def parse_feeder_returning_code(self, feeder): - "Parse a single expression from feeder and print the messages." + def parse_feeder_returning_code(self, feeder) -> tuple: + """ + Parse a single expression from feeder, print the messages it produces and + return the result and the source code for this. + """ + return self.parse_feeder_returning_code_and_messages(feeder)[:2] + + def parse_feeder_returning_code_and_messages(self, feeder) -> tuple: + """ + Parse a single expression from feeder, print the messages it produces and + return the result, the source code for this and evaluated + messages created in evaluation. + """ from mathics.core.parser.util import parse_returning_code try: @@ -282,12 +219,12 @@ def parse_feeder_returning_code(self, feeder): self.stopped = False source_code = "" result = None - feeder.send_messages(self) - return result, source_code + messages = feeder.send_messages(self) + return result, source_code, messages def evaluate(self, query, timeout=None, format=None): - """Evaluate a Mathics expression and return the - result of evaluation. + """ + Evaluate a Mathics3 expression and return the result of evaluation. On return self.exc_result will contain status of various exception type of result like $Aborted, Overflow, Break, or Continue. @@ -442,7 +379,17 @@ def get_stored_result(self, eval_result, output_forms): def stop(self) -> None: self.stopped = True - def format_output(self, expr, format=None): + def format_output( + self, expr: BaseElement, format: Optional[str] = None + ) -> Union[BaseElement, str]: + """ + This function takes an expression `expr` and + a format `format`. If `format` is None, then returns `expr`. Otherwise, + produce an str with the proper format. + + Notice that this function can be overwritten by the front-ends, so it should not be + used in Builtin classes where it is expected a front-end independent result. + """ from mathics.eval.makeboxes import format_element if format is None: @@ -499,7 +446,12 @@ def get_quiet_messages(self): return [] return value.elements - def message(self, symbol_name: str, tag, *args) -> None: + def message(self, symbol_name: str, tag, *msgs) -> "Message": + """ + Format message given its components, ``symbol``, ``tag`` + + + """ from mathics.core.expression import Expression # Allow evaluation.message('MyBuiltin', ...) (assume @@ -519,7 +471,7 @@ def message(self, symbol_name: str, tag, *args) -> None: symbol_shortname = self.definitions.shorten_name(symbol) if settings.DEBUG_PRINT: - print("MESSAGE: %s::%s (%s)" % (symbol_shortname, tag, args)) + print("MESSAGE: %s::%s (%s)" % (symbol_shortname, tag, msgs)) text = self.definitions.get_value(symbol, "System`Messages", pattern, self) if text is None: @@ -532,12 +484,14 @@ def message(self, symbol_name: str, tag, *args) -> None: text = String("Message %s::%s not found." % (symbol_shortname, tag)) text = self.format_output( - Expression(SymbolStringForm, text, *(from_python(arg) for arg in args)), + Expression(SymbolStringForm, text, *(from_python(arg) for arg in msgs)), "text", ) - self.out.append(Message(symbol_shortname, tag, text)) + message = Message(symbol_shortname, tag, text) + self.out.append(message) self.output.out(self.out[-1]) + return message def print_out(self, text) -> None: from mathics.core.convert.python import from_python @@ -554,12 +508,12 @@ def print_out(self, text) -> None: if settings.DEBUG_PRINT: print("OUT: " + text) - def error(self, symbol, tag, *args) -> None: + def error(self, symbol, tag, *msgs) -> None: # Temporarily reset the recursion limit, to allow the message being # formatted self.recursion_depth, depth = 0, self.recursion_depth try: - self.message(symbol, tag, *args) + self.message(symbol, tag, *msgs) finally: self.recursion_depth = depth raise AbortInterrupt @@ -620,3 +574,116 @@ def publish(self, tag, *args, **kwargs) -> None: for listener in listeners: if listener(*args, **kwargs): break + + +# TODO: rethink what we want/need here +class Message(_Out): + def __init__(self, symbol: Union[Symbol, str], tag: str, text: str) -> None: + """ + A Mathics3 message of some sort. symbol_or_string can either be a symbol or a + string. + + Symbol: classifies which predefined or variable this comes from? If there is none + use a string. + tag: a short slug string that indicates the kind of message + + In Django we need to use a string for symbol, since we need something that is JSON serializable + and a Mathics3 Symbol is not like this. + """ + super(Message, self).__init__() + self.is_message = True # Why do we need this? + self.symbol = symbol + self.tag = tag + self.text = text + + def __str__(self) -> str: + return f"{self.symbol}::{self.tag}: {self.text}" + + def __eq__(self, other) -> bool: + return self.is_message == other.is_message and self.text == other.text + + def get_data(self): + return { + "message": True, + "symbol": self.symbol, + "tag": self.tag, + "prefix": "%s::%s" % (self.symbol, self.tag), + "text": self.text, + } + + +class Print(_Out): + def __init__(self, text) -> None: + super(Print, self).__init__() + self.is_print = True + self.text = text + + def __str__(self) -> str: + return self.text + + def __eq__(self, other) -> bool: + return self.is_message == other.is_message and self.text == other.text + + def get_data(self): + return { + "message": False, + "text": self.text, + } + + +class Output: + def max_stored_size(self, settings) -> int: + return settings.MAX_STORED_SIZE + + def out(self, out): + pass + + def clear(self, wait): + raise NotImplementedError + + def display(self, data, metadata): + raise NotImplementedError + + +OutputLines = List[str] + + +class Result: + """ + A structure containing the result of an evaluation. + + In particular, there are the following fields: + + result: the actual result produced. + out: a list of additional output strings. These are warning or error messages. See "form" + for exactly what they are. + form: is the *format* of the result which tags the kind of result . + Think of this as something like a mime/type. Some formats: + + * SyntaxErrors + * SVG images + * PNG images + * text + * MathML + * None - defaults to text + + In the future "form" will be renamed "format" or something like this. + """ + + def __init__( + self, out: OutputLines, result, line_no: int, last_eval=None, form=None + ) -> None: + self.out = out + self.result = result + self.line_no = line_no + self.last_eval = last_eval + self.form = form + + # FIXME: consider using a named tuple + def get_data(self) -> dict: + return { + "out": [out.get_data() for out in self.out], + "result": self.result, + "line": self.line_no, + "form": self.form, + } diff --git a/mathics/core/exceptions.py b/mathics/core/exceptions.py index 002a36686..06a0e2ff3 100644 --- a/mathics/core/exceptions.py +++ b/mathics/core/exceptions.py @@ -9,6 +9,10 @@ class BoxExpressionError(Exception): BoxConstructError = BoxExpressionError +class IllegalStepSpecification(Exception): + pass + + class InvalidLevelspecError(Exception): pass diff --git a/mathics/core/expression.py b/mathics/core/expression.py index b25a62d77..2efe3da2f 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -54,6 +54,7 @@ SymbolDirectedInfinity, SymbolFunction, SymbolMinus, + SymbolOverflow, SymbolPattern, SymbolPower, SymbolSequence, @@ -233,6 +234,9 @@ def __init__( self._sequences = None self._cache = None + # self.copy creates this + self.original: Optional[Expression] = None + def __getnewargs__(self): return (self._head, self._elements) @@ -454,9 +458,9 @@ def evaluate( """ Apply transformation rules and expression evaluation to ``evaluation`` via ``rewrite_apply_eval_step()`` until that method tells us to stop, - or unti we hit an $IterationLimit or TimeConstrained limit. + or until we hit an $IterationLimit or TimeConstrained limit. - Evaluation is a recusive:``rewrite_apply_eval_step()`` may call us. + Evaluation is recursive:``rewrite_apply_eval_step()`` may call us. """ if evaluation.timeout: return @@ -862,7 +866,13 @@ def get_sort_key(self, pattern_sort=False) -> tuple: exps[name] = exps.get(name, 0) + 1 elif self.has_form("Power", 2): var = self._elements[0].get_name() - exp = self._elements[1].round_to_float() + # TODO: Check if this is the expected behaviour. + # round_to_float is an attribute of Expression, + # but not for Atoms. + try: + exp = self._elements[1].round_to_float() + except AttributeError: + exp = None if var and exp is not None: exps[var] = exps.get(var, 0) + exp if exps: @@ -1260,7 +1270,11 @@ def rules(): yield rule for rule in rules(): - result = rule.apply(new, evaluation, fully=False) + try: + result = rule.apply(new, evaluation, fully=False) + except OverflowError: + evaluation.message("General", "ovfl") + return Expression(SymbolOverflow), False if result is not None: if not isinstance(result, EvalMixin): return result, False @@ -1325,13 +1339,13 @@ def sameQ(self, other: BaseElement) -> bool: return False if self is other: return True - if not self._head.sameQ(other.get_head()): + if not self._head.sameQ(other._head): return False - if len(self._elements) != len(other.get_elements()): + if len(self._elements) != len(other._elements): return False return all( (id(element) == id(oelement) or element.sameQ(oelement)) - for element, oelement in zip(self._elements, other.get_elements()) + for element, oelement in zip(self._elements, other._elements) ) def sequences(self): diff --git a/mathics/core/expression_predefined.py b/mathics/core/expression_predefined.py new file mode 100644 index 000000000..dd564aa16 --- /dev/null +++ b/mathics/core/expression_predefined.py @@ -0,0 +1,35 @@ +from typing import Tuple + +from mathics.core.atoms import ( + MATHICS3_COMPLEX_I, + MATHICS3_COMPLEX_I_NEG, + Complex, + Integer, + Integer0, + Integer1, + IntegerM1, + String, +) +from mathics.core.element import BaseElement, ElementsProperties +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.systemsymbols import SymbolDirectedInfinity + + +class PredefinedExpression(Expression): + def __init__( + self, + head: BaseElement, + *elements: Tuple[BaseElement], + ): + elements_properties = ElementsProperties(True, True, True) + super().__init__(head, *elements, elements_properties=elements_properties) + + +MATHICS3_COMPLEX_INFINITY = PredefinedExpression(SymbolDirectedInfinity) +MATHICS3_INFINITY = PredefinedExpression(SymbolDirectedInfinity, Integer1) +MATHICS3_NEG_INFINITY = PredefinedExpression(SymbolDirectedInfinity, IntegerM1) +MATHICS3_I_INFINITY = PredefinedExpression(SymbolDirectedInfinity, MATHICS3_COMPLEX_I) +MATHICS3_I_NEG_INFINITY = PredefinedExpression( + SymbolDirectedInfinity, MATHICS3_COMPLEX_I_NEG +) diff --git a/mathics/core/list.py b/mathics/core/list.py index 19cb603e3..60f3fa126 100644 --- a/mathics/core/list.py +++ b/mathics/core/list.py @@ -19,7 +19,7 @@ class ListExpression(Expression): - *elements - optional: the remaining elements Keyword Arguments: - - element_properties -- properties of the collection of elements + - elements_properties -- properties of the collection of elements - literal_values -- if this is not None, then it is a tuple of Python values """ diff --git a/mathics/core/number.py b/mathics/core/number.py index ba166ed61..61a8018d4 100644 --- a/mathics/core/number.py +++ b/mathics/core/number.py @@ -2,38 +2,47 @@ # cython: language_level=3 import string -from math import ceil, log, log2 +from math import ceil, log +from sys import float_info from typing import List, Optional import mpmath import sympy +from mathics.core.element import BaseElement from mathics.core.symbols import ( SymbolMachinePrecision, SymbolMaxPrecision, SymbolMinPrecision, ) -C = log2(10) # ~ 3.3219280948873626 +LOG2_10 = mpmath.log(10.0, 2.0) # ~ 3.3219280948873626 -# Number of bits of machine precision. -# Note this is a float, not an int. -# WMA uses real values for precision, to take into account the internal representation of numbers. -# This is why $MachinePrecision is not 16, but 15.9546` -machine_precision = 53.0 -machine_digits = int(machine_precision / C) +# Number of digits in the mantisa of a normalized floatting point number: +FP_MANTISA_BINARY_DIGITS = float_info.mant_dig # ~53 -machine_epsilon = 2 ** (1 - machine_precision) +# the (integer) number of decimal digits hold by a +# normalized floatting point number. +MACHINE_DIGITS = float_info.dig # ~15 +# the difference between 1. and the next +# representable floatting point number: +MACHINE_EPSILON = float_info.epsilon +# the number of accurate decimal digits hold by a normalized floatting point number. +MACHINE_PRECISION_VALUE = float_info.mant_dig / LOG2_10 -def reconstruct_digits(bits) -> int: - """ - Number of digits needed to reconstruct a number with given bits of precision. - >>> reconstruct_digits(53) - 17 - """ - return int(ceil(bits / C) + 1) +# Maximum normalized float +MAX_MACHINE_NUMBER = float_info.max + +# Minimum positive normalized float +MIN_MACHINE_NUMBER = float_info.min + +# the accuracy associated to 0.` +ZERO_MACHINE_ACCURACY = -mpmath.log(MIN_MACHINE_NUMBER, 10.0) + MACHINE_PRECISION_VALUE + +# the (integer) number of decimal digits needed to reconstruct a floatting point number. +RECONSTRUCT_MACHINE_PRECISION_DIGITS = int(ceil(float_info.mant_dig / LOG2_10) + 1) class PrecisionValueError(Exception): @@ -111,20 +120,29 @@ def sameQ(v1, v2) -> bool: def dps(prec) -> int: - return max(1, int(round(int(prec) / C - 1))) + return max(1, int(round(int(prec) / LOG2_10 - 1))) def prec(dps) -> int: - return max(1, int(round((int(dps) + 1) * C))) + return max(1, int(round((int(dps) + 1) * LOG2_10))) -def min_prec(*args): - result = None - for arg in args: - prec = arg.get_precision() - if result is None or (prec is not None and prec < result): - result = prec - return result +def min_prec(*args: BaseElement) -> Optional[float]: + """ + Returns the precision of the expression with the minimum precision. + If all the expressions are exact or non numeric, return None. + + If one of the expressions is an inexact value with zero + nominal value, then its accuracy is used instead. For example, + ```min_prec(1, 0.``4) ``` returns 4. + + Notice that this behaviour is different that the one obtained + using mathics.core.numbers.eval_Precision. + """ + args_prec = (arg.get_precision() for arg in args) + return min( + (arg_prec for arg_prec in args_prec if arg_prec is not None), default=None + ) def pickle_mp(value): diff --git a/mathics/core/parser/README.md b/mathics/core/parser/README.md index 15c0c6673..2eaaf48bd 100644 --- a/mathics/core/parser/README.md +++ b/mathics/core/parser/README.md @@ -4,8 +4,7 @@ The Mathics parser is an operator precedence parser that implements the precedence climbing method. The AST (Abstract Syntax -Tree) produced after parsing is a kind of [M-expression](M-expression - tuple: if suffix is None: # MachineReal/PrecisionReal is determined by number of digits # in the mantissa - d = len(man) - 2 # one less for decimal point - if d < reconstruct_digits(machine_precision): + # if the number of digits is less than 17, then MachineReal is used. + # If more digits are provided, then PrecisionReal is used. + digits = len(man) - 2 + if digits < RECONSTRUCT_MACHINE_PRECISION_DIGITS: return "MachineReal", sign * float(s) else: return ( "PrecisionReal", ("DecimalString", str("-" + s if sign == -1 else s)), - d, + digits, ) elif suffix == "": return "MachineReal", sign * float(s) @@ -118,10 +120,15 @@ def convert_Number(self, node: AST_Number) -> tuple: # so ``` 0`3 === 0 ``` and ``` 0.`3 === 0.`4 ``` if node.value == "0": return "Integer", 0 + + s_float = float(s) + prec = float(suffix) + if s_float == 0.0: + return "MachineReal", sign * s_float return ( "PrecisionReal", ("DecimalString", str("-" + s if sign == -1 else s)), - float(suffix), + prec, ) # Put into standard form mantissa * base ^ n @@ -149,7 +156,7 @@ def convert_Number(self, node: AST_Number) -> tuple: prec10 = acc10 else: prec10 = acc10 + log10(abs(x)) - if prec10 < reconstruct_digits(machine_precision): + if prec10 < RECONSTRUCT_MACHINE_PRECISION_DIGITS: prec10 = None elif suffix == "": prec10 = None diff --git a/mathics/core/parser/feed.py b/mathics/core/parser/feed.py index f4177b906..0661def7d 100644 --- a/mathics/core/parser/feed.py +++ b/mathics/core/parser/feed.py @@ -8,10 +8,12 @@ class MathicsLineFeeder(LineFeeder): - def send_messages(self, evaluation): + def send_messages(self, evaluation) -> list: + evaluated_messages = [] for message in self.messages: - evaluation.message(*message) + evaluated_messages.append(evaluation.message(*message)) self.messages = [] + return evaluated_messages class MathicsSingleLineFeeder(SingleLineFeeder, MathicsLineFeeder): diff --git a/mathics/core/parser/parser.py b/mathics/core/parser/parser.py index d9d249ef4..84aada633 100644 --- a/mathics/core/parser/parser.py +++ b/mathics/core/parser/parser.py @@ -1,7 +1,14 @@ # -*- coding: utf-8 -*- +""" +Precedence-climbing Parsing routines for grammar symbols. + +See README.md or +https://mathics-development-guide.readthedocs.io/en/latest/extending/code-overview/scanning-and-parsing.html#parser +""" import string +from typing import Optional, Union from mathics_scanner import ( InvalidSyntaxError, @@ -9,8 +16,19 @@ TranslateError, is_symbol_name, ) - -from mathics.core.parser.ast import Filename, Node, Number, String, Symbol +from mathics_scanner.tokeniser import Token + +from mathics.core.parser.ast import ( + Filename, + Node, + NullString, + NullSymbol, + Number, + Number1, + NumberM1, + String, + Symbol, +) from mathics.core.parser.operators import ( all_ops, binary_ops, @@ -95,7 +113,7 @@ def backtrack(self, pos): self.tokeniser.pos = pos self.current_token = None - def parse_e(self): + def parse_e(self) -> Union[Node, Optional[list]]: result = [] while self.next().tag != "END": result.append(self.parse_exp(0)) @@ -106,7 +124,7 @@ def parse_e(self): else: return None - def parse_exp(self, p): + def parse_exp(self, p: int): result = self.parse_p() while True: if self.bracket_depth > 0: @@ -177,22 +195,22 @@ def parse_box(self, p): else: result = new_result if result is None: - result = String("") + result = NullString return result - def parse_seq(self): + def parse_seq(self) -> list: result = [] while True: token = self.next_noend() tag = token.tag if tag == "RawComma": self.tokeniser.feeder.message("Syntax", "com") - result.append(Symbol("Null")) + result.append(NullSymbol) self.consume() elif tag in ("RawRightAssociation", "RawRightBrace", "RawRightBracket"): if result: self.tokeniser.feeder.message("Syntax", "com") - result.append(Symbol("Null")) + result.append(NullSymbol) break else: result.append(self.parse_exp(0)) @@ -205,7 +223,7 @@ def parse_seq(self): break return result - def parse_inequality(self, expr1, token, p): + def parse_inequality(self, expr1, token: Token, p: int) -> Optional[Node]: tag = token.tag q = flat_binary_ops[tag] if q < p: @@ -231,7 +249,7 @@ def parse_inequality(self, expr1, token, p): expr1 = Node(tag, expr1, expr2).flatten() return expr1 - def parse_binary(self, expr1, token, p): + def parse_binary(self, expr1, token: Token, p: int) -> Optional[Node]: tag = token.tag q = binary_ops[tag] if q < p: @@ -253,7 +271,7 @@ def parse_binary(self, expr1, token, p): result.flatten() return result - def parse_postfix(self, expr1, token, p): + def parse_postfix(self, expr1, token: Token, p: int) -> Optional[Node]: tag = token.tag q = postfix_ops[tag] if q < p: @@ -261,6 +279,9 @@ def parse_postfix(self, expr1, token, p): self.consume() return Node(tag, expr1) + def parse_ternary(self, expr1, token: Token, p: int) -> Optional[Node]: + raise NotImplementedError + # P methods # # p_xxx methods are called from parse_p. @@ -288,7 +309,7 @@ def p_RawLeftParenthesis(self, token): result.parenthesised = True return result - def p_RawLeftBrace(self, token): + def p_RawLeftBrace(self, token) -> Node: self.consume() self.bracket_depth += 1 seq = self.parse_seq() @@ -296,7 +317,7 @@ def p_RawLeftBrace(self, token): self.bracket_depth -= 1 return Node("List", *seq) - def p_RawLeftAssociation(self, token): + def p_RawLeftAssociation(self, token) -> Node: self.consume() self.bracket_depth += 1 seq = self.parse_seq() @@ -304,7 +325,7 @@ def p_RawLeftAssociation(self, token): self.bracket_depth -= 1 return Node("Association", *seq) - def p_LeftRowBox(self, token): + def p_LeftRowBox(self, token) -> Node: self.consume() children = [] self.box_depth += 1 @@ -315,7 +336,7 @@ def p_LeftRowBox(self, token): children.append(newnode) token = self.next() if len(children) == 0: - result = String("") + result = NullString elif len(children) == 1: result = children[0] else: @@ -326,7 +347,7 @@ def p_LeftRowBox(self, token): result.parenthesised = True return result - def p_Number(self, token): + def p_Number(self, token) -> Number: s = token.text # sign @@ -373,26 +394,26 @@ def p_Number(self, token): self.consume() return result - def p_String(self, token): + def p_String(self, token) -> String: result = String(token.text[1:-1]) self.consume() return result - def p_Symbol(self, token): + def p_Symbol(self, token) -> Symbol: symbol_name = special_symbols.get(token.text, token.text) result = Symbol(symbol_name, context=None) self.consume() return result - def p_Filename(self, token): + def p_Filename(self, token) -> Filename: result = Filename(token.text) self.consume() return result def p_Span(self, token): - return self.e_Span(Number("1"), token, 0) + return self.e_Span(Number1, token, 0) - def p_Integral(self, token): + def p_Integral(self, token) -> Node: self.consume() inner_prec, outer_prec = all_ops["Sum"] + 1, all_ops["Power"] - 1 expr1 = self.parse_exp(inner_prec) @@ -400,7 +421,7 @@ def p_Integral(self, token): expr2 = self.parse_exp(outer_prec) return Node("Integrate", expr1, expr2) - def p_Pattern(self, token): + def p_Pattern(self, token) -> Node: self.consume() text = token.text if "." in text: @@ -437,7 +458,7 @@ def p_Minus(self, token): expr.value = "-" + expr.value return expr else: - return Node("Times", Number("1", sign=-1), expr).flatten() + return Node("Times", NumberM1, expr).flatten() def p_Plus(self, token): self.consume() @@ -445,17 +466,17 @@ def p_Plus(self, token): # note flattening here even flattens e.g. + a + b return Node("Plus", self.parse_exp(q)).flatten() - def p_PlusMinus(self, token): + def p_PlusMinus(self, token) -> Node: self.consume() q = prefix_ops["Minus"] return Node("PlusMinus", self.parse_exp(q)) - def p_MinusPlus(self, token): + def p_MinusPlus(self, token) -> Node: self.consume() q = prefix_ops["Minus"] return Node("MinusPlus", self.parse_exp(q)) - def p_Out(self, token): + def p_Out(self, token) -> Node: self.consume() text = token.text if text == "%": @@ -466,11 +487,11 @@ def p_Out(self, token): n = text[1:] return Node("Out", Number(n)) - def p_Slot(self, token): + def p_Slot(self, token) -> Node: self.consume() text = token.text if len(text) == 1: - n = Number("1") + n = Number1 else: n = text[1:] if n.isdigit(): @@ -479,7 +500,7 @@ def p_Slot(self, token): n = String(n) return Node("Slot", n) - def p_SlotSequence(self, token): + def p_SlotSequence(self, token) -> Node: self.consume() text = token.text if len(text) == 2: @@ -488,17 +509,17 @@ def p_SlotSequence(self, token): n = text[2:] return Node("SlotSequence", Number(n)) - def p_Increment(self, token): + def p_Increment(self, token) -> Node: self.consume() q = prefix_ops["PreIncrement"] return Node("PreIncrement", self.parse_exp(q)) - def p_Decrement(self, token): + def p_Decrement(self, token) -> Node: self.consume() q = prefix_ops["PreDecrement"] return Node("PreDecrement", self.parse_exp(q)) - def p_PatternTest(self, token): + def p_PatternTest(self, token) -> Node: self.consume() q = prefix_ops["Definition"] child = self.parse_exp(q) @@ -506,7 +527,7 @@ def p_PatternTest(self, token): "Information", child, Node("Rule", Symbol("LongForm"), Symbol("False")) ) - def p_Information(self, token): + def p_Information(self, token) -> Node: self.consume() q = prefix_ops["Information"] child = self.parse_exp(q) @@ -523,7 +544,7 @@ def p_Information(self, token): # Used for binary and ternary operators. # return None if precedence is too low. - def e_Span(self, expr1, token, p): + def e_Span(self, expr1, token, p) -> Optional[Node]: q = ternary_ops["Span"] if q < p: return None @@ -559,7 +580,7 @@ def e_Span(self, expr1, token, p): self.feeder.messages = messages return Node("Span", expr1, expr2) - def e_RawLeftBracket(self, expr, token, p): + def e_RawLeftBracket(self, expr, token: Token, p: int) -> Optional[Node]: q = all_ops["Part"] if q < p: return None @@ -581,7 +602,7 @@ def e_RawLeftBracket(self, expr, token, p): result.parenthesised = True return result - def e_Infix(self, expr1, token, p): + def e_Infix(self, expr1, token, p) -> Optional[Node]: q = ternary_ops["Infix"] if q < p: return None @@ -591,7 +612,7 @@ def e_Infix(self, expr1, token, p): expr3 = self.parse_exp(q + 1) return Node(expr2, expr1, expr3) - def e_Postfix(self, expr1, token, p): + def e_Postfix(self, expr1, token: Token, p: int) -> Optional[Node]: q = left_binary_ops["Postfix"] if q < p: return None @@ -600,7 +621,7 @@ def e_Postfix(self, expr1, token, p): expr2 = self.parse_exp(q + 1) return Node(expr2, expr1) - def e_Prefix(self, expr1, token, p): + def e_Prefix(self, expr1, token: Token, p: int) -> Optional[Node]: q = 640 if 640 < p: return None @@ -608,16 +629,16 @@ def e_Prefix(self, expr1, token, p): expr2 = self.parse_exp(q) return Node(expr1, expr2) - def e_ApplyList(self, expr1, token, p): + def e_ApplyList(self, expr1, token: Token, p: int) -> Optional[Node]: q = right_binary_ops["Apply"] if q < p: return None self.consume() expr2 = self.parse_exp(q) - expr3 = Node("List", Number("1")) + expr3 = Node("List", Number1) return Node("Apply", expr1, expr2, expr3) - def e_Function(self, expr1, token, p): + def e_Function(self, expr1, token: Token, p: int) -> Optional[Node]: q = postfix_ops["Function"] if q < p: return None @@ -629,7 +650,7 @@ def e_Function(self, expr1, token, p): expr2 = self.parse_exp(q) return Node("Function", expr1, expr2) - def e_RawColon(self, expr1, token, p): + def e_RawColon(self, expr1, token: Token, p: int) -> Optional[Node]: head_name = expr1.get_head_name() if head_name == "Symbol": head = "Pattern" @@ -650,7 +671,7 @@ def e_RawColon(self, expr1, token, p): expr2 = self.parse_exp(q + 1) return Node(head, expr1, expr2) - def e_Semicolon(self, expr1, token, p): + def e_Semicolon(self, expr1, token: Token, p: int) -> Optional[Node]: q = flat_binary_ops["CompoundExpression"] if q < p: return None @@ -663,7 +684,7 @@ def e_Semicolon(self, expr1, token, p): # So that e.g. 'x = 1;' doesn't wait for newline in the frontend tag = self.next().tag if tag == "END" and self.bracket_depth == 0: - expr2 = Symbol("Null") + expr2 = NullSymbol return Node("CompoundExpression", expr1, expr2).flatten() # XXX look for next expr otherwise backtrack @@ -672,10 +693,10 @@ def e_Semicolon(self, expr1, token, p): except TranslateError: self.backtrack(pos) self.feeder.messages = messages - expr2 = Symbol("Null") + expr2 = NullSymbol return Node("CompoundExpression", expr1, expr2).flatten() - def e_Minus(self, expr1, token, p): + def e_Minus(self, expr1, token: Token, p: int) -> Optional[Node]: q = left_binary_ops["Subtract"] if q < p: return None @@ -684,10 +705,10 @@ def e_Minus(self, expr1, token, p): if isinstance(expr2, Number) and not expr2.value.startswith("-"): expr2.value = "-" + expr2.value else: - expr2 = Node("Times", Number("1", sign=-1), expr2).flatten() + expr2 = Node("Times", NumberM1, expr2).flatten() return Node("Plus", expr1, expr2).flatten() - def e_TagSet(self, expr1, token, p): + def e_TagSet(self, expr1, token: Token, p: int) -> Optional[Node]: q = all_ops["Set"] if q < p: return None @@ -711,14 +732,14 @@ def e_TagSet(self, expr1, token, p): expr3 = self.parse_exp(q + 1) return Node(head, expr1, expr2, expr3) - def e_Unset(self, expr1, token, p): + def e_Unset(self, expr1, token: Token, p: int) -> Optional[Node]: q = all_ops["Set"] if q < p: return None self.consume() return Node("Unset", expr1) - def e_Derivative(self, expr1, token, p): + def e_Derivative(self, expr1, token: Token, p: int) -> Optional[Node]: q = postfix_ops["Derivative"] if q < p: return None @@ -729,17 +750,15 @@ def e_Derivative(self, expr1, token, p): head = Node("Derivative", Number(str(n))) return Node(head, expr1) - def e_Divide(self, expr1, token, p): + def e_Divide(self, expr1, token: Token, p: int): q = left_binary_ops["Divide"] if q < p: return None self.consume() expr2 = self.parse_exp(q + 1) - return Node( - "Times", expr1, Node("Power", expr2, Number("1", sign=-1)) - ).flatten() + return Node("Times", expr1, Node("Power", expr2, NumberM1)).flatten() - def e_Alternatives(self, expr1, token, p): + def e_Alternatives(self, expr1, token: Token, p: int) -> Optional[Node]: q = flat_binary_ops["Alternatives"] if q < p: return None @@ -747,7 +766,7 @@ def e_Alternatives(self, expr1, token, p): expr2 = self.parse_exp(q + 1) return Node("Alternatives", expr1, expr2).flatten() - def e_MessageName(self, expr1, token, p): + def e_MessageName(self, expr1, token: Token, p: int) -> Node: elements = [expr1] while self.next().tag == "MessageName": self.consume() @@ -771,7 +790,7 @@ def e_MessageName(self, expr1, token, p): # The first argument may be None if the LHS is absent. # Used for boxes. - def b_SqrtBox(self, box0, token, p): + def b_SqrtBox(self, box0, token: Token, p: int) -> Optional[Node]: if box0 is not None: return None self.consume() @@ -784,12 +803,12 @@ def b_SqrtBox(self, box0, token, p): else: return Node("SqrtBox", box1) - def b_SuperscriptBox(self, box1, token, p): + def b_SuperscriptBox(self, box1, token: Token, p: int) -> Optional[Node]: q = misc_ops["SuperscriptBox"] if q < p: return None if box1 is None: - box1 = String("") + box1 = NullString self.consume() box2 = self.parse_box(q) if self.next().tag == "OtherscriptBox": @@ -799,12 +818,12 @@ def b_SuperscriptBox(self, box1, token, p): else: return Node("SuperscriptBox", box1, box2) - def b_SubscriptBox(self, box1, token, p): + def b_SubscriptBox(self, box1, token: Token, p: int) -> Optional[Node]: q = misc_ops["SubscriptBox"] if q < p: return None if box1 is None: - box1 = String("") + box1 = NullString self.consume() box2 = self.parse_box(q) if self.next().tag == "OtherscriptBox": @@ -814,12 +833,12 @@ def b_SubscriptBox(self, box1, token, p): else: return Node("SubscriptBox", box1, box2) - def b_UnderscriptBox(self, box1, token, p): + def b_UnderscriptBox(self, box1, token: Token, p: int) -> Optional[Node]: q = misc_ops["UnderscriptBox"] if q < p: return None if box1 is None: - box1 = String("") + box1 = NullString self.consume() box2 = self.parse_box(q) if self.next().tag == "OtherscriptBox": @@ -829,17 +848,17 @@ def b_UnderscriptBox(self, box1, token, p): else: return Node("UnderscriptBox", box1, box2) - def b_FractionBox(self, box1, token, p): + def b_FractionBox(self, box1, token: Token, p: int) -> Optional[Node]: q = misc_ops["FractionBox"] if q < p: return None if box1 is None: - box1 = String("") + box1 = NullString self.consume() box2 = self.parse_box(q + 1) return Node("FractionBox", box1, box2) - def b_FormBox(self, box1, token, p): + def b_FormBox(self, box1, token: Token, p: int) -> Optional[Node]: q = misc_ops["FormBox"] if q < p: return None @@ -853,12 +872,12 @@ def b_FormBox(self, box1, token, p): box2 = self.parse_box(q) return Node("FormBox", box2, box1) - def b_OverscriptBox(self, box1, token, p): + def b_OverscriptBox(self, box1, token: Token, p: int) -> Optional[Node]: q = misc_ops["OverscriptBox"] if q < p: return None if box1 is None: - box1 = String("") + box1 = NullString self.consume() box2 = self.parse_box(q) if self.next().tag == "OtherscriptBox": diff --git a/mathics/core/pattern.py b/mathics/core/pattern.py index c42d6d12f..f287d9815 100644 --- a/mathics/core/pattern.py +++ b/mathics/core/pattern.py @@ -3,7 +3,7 @@ # -*- coding: utf-8 -*- from itertools import chain -from typing import Optional +from typing import Callable, List, Optional, Tuple from mathics.core.atoms import Integer from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_ORDERLESS @@ -75,42 +75,121 @@ class Pattern: When the pattern matches, the symbol is bound to the parameter ``x``. """ + # TODO: In WMA, when a Pattern is created, the attributes + # from the head are read from the evaluation context and + # stored as a part of a rule. + # + # As Patterns are nested structures, the factory not only needs + # the attributes of the head, but also the full evaluation context + # which is needed to create patterns for its elements. + # + # + # For instance, `rule=Times[c__, Plus[Q[a_],Q[b_]]]->Q[c*(a+b)]` + # builds the pattern `Times[c__, Plus[Q[a_],Q[b_]]]`. + # The constructor of the pattern then creates recursively + # `c__` + # `Plus[Q[a_],Q[b_]]` + # `Plus` + # `Q[a_]` + # `Q` + # `a_` + # `Q[b_]` + # `Q` + # `b_` + # + # Also, when the initial Definitions object for the evaluation + # context is created, many rules must be created without an + # evaluation context available. For that case, we still + # must be able to create Patten objects without the evaluation context. + # + # In any case, just by caching the attributes in the first use of + # the pattern there is a win ~5% in performance. + # + # A better implementation would take into account the attributes + # to specialize the match method. + # + # + # Corner case: `Alternaties` + # ========================== + # + # Notice also that the case of `Alternatives` is a corner case, + # where attributes are readed at the moment of the rule application: + # + # For example, in WMA, let's consider this example + # ``` + # In[1]:= SetAttributes[P,Orderless]; + # In[2]:= rule=Alternatives[P,Q][_Integer,_Symbol]->True; + # ``` + # + # At this point, the rule `rule` was created. As the head of the pattern + # is an expression, it does not provides special attributes to the pattern. + # As expected, the pattern does not match with `Q[a, 1]` because the order of the + # parameters: + # ``` + # In[3]:= Q[a, 1]/.rule + # Out[3]= Q[a, 1] + # ``` + # + # On the other hand, it does take into account the attributes of `P`: + # + # ``` + # In[4]:= P[a, 1]/.rule + # Out[4]= True + # ``` + # These attributes are not stored in the rule: if we remove the attribute + # ``` + # In[5]:= Attributes[P]={}; + # ``` + # + # the attribute is not used anymore, and the rule application fails: + # + # ``` + # In[6]:= P[a, 1]/.rule + # Out[6]= P[a, 1] + # `` + # + # + @staticmethod - def create(expr: BaseElement) -> "Pattern": + def create(expr: BaseElement, evaluation: Optional[Evaluation] = None) -> "Pattern": """ If ``expr`` is listed in ``pattern_object`` return the pattern found there. Otherwise, if ``expr`` is an ``Atom``, create and return ``AtomPattern`` for ``expr``. Otherwise, create and return and ``ExpressionPattern`` for ``expr``. """ - name = expr.get_head_name() pattern_object = pattern_objects.get(name) if pattern_object is not None: - return pattern_object(expr) + return pattern_object(expr, evaluation=evaluation) if isinstance(expr, Atom): - return AtomPattern(expr) + return AtomPattern(expr, evaluation) else: - return ExpressionPattern(expr) + return ExpressionPattern(expr, evaluation) def match( self, - yield_func, - expression, - vars, - evaluation, - head=None, - element_index=None, - element_count=None, - fully=True, + yield_func: Callable, + expression: BaseElement, + vars: dict, + evaluation: Evaluation, + head: Symbol = None, + element_index: int = None, + element_count: int = None, + fully: bool = True, ): """ Check if the expression matches the pattern (self). If it does, calls `yield_func`. - vars collects subexpressions associated to subpatterns. - head ? - element_index ? - element_count ? - fully is used in match_elements, for the case of Orderless patterns. + vars collects subexpressions associated to named subpatterns. + head: Symbol. Provided by match_element, used by `Optional`. + element_index: int the position + element_count: int and the number of optional elements. Used by `Optional` + for calling `get_default_value`. + + Note: this complexity would disappear if Defaults would be stored as in WMA + at the creation time of the object. + + fully is used in `match_element`, for the case of Orderless patterns. """ raise NotImplementedError @@ -118,7 +197,7 @@ def does_match( self, expression: BaseElement, evaluation: Evaluation, - vars=Optional[dict], + vars: Optional[dict] = None, fully: bool = True, ) -> bool: @@ -147,7 +226,7 @@ def get_name(self): def get_head_name(self): return self.expr.get_head_name() - def sameQ(self, other) -> bool: + def sameQ(self, other: BaseElement) -> bool: """Mathics SameQ""" return self.expr.sameQ(other.expr) @@ -157,7 +236,7 @@ def get_head(self): def get_elements(self): return self.expr.get_elements() - def get_sort_key(self, pattern_sort=False) -> tuple: + def get_sort_key(self, pattern_sort: bool = False) -> tuple: return self.expr.get_sort_key(pattern_sort=pattern_sort) def get_lookup_name(self): @@ -176,12 +255,22 @@ def has_form(self, *args): return self.expr.has_form(*args) def get_match_candidates( - self, elements, expression, attributes, evaluation, vars={} + self, + elements: Tuple[BaseElement], + expression: BaseElement, + attributes: int, + evaluation: Evaluation, + vars: dict = {}, ): return [] def get_match_candidates_count( - self, elements, expression, attributes, evaluation, vars={} + self, + elements: Tuple[BaseElement], + expression: BaseElement, + attributes: int, + evaluation: Evaluation, + vars: dict = {}, ): return len( self.get_match_candidates( @@ -191,7 +280,7 @@ def get_match_candidates_count( class AtomPattern(Pattern): - def __init__(self, expr): + def __init__(self, expr: Atom, evaluation: Optional[Evaluation] = None) -> None: self.atom = expr self.expr = expr if isinstance(expr, Symbol): @@ -222,21 +311,26 @@ def get_match_symbol_candidates( def match( self, - yield_func, - expression, - vars, - evaluation, - head=None, - element_index=None, - element_count=None, - fully=True, + yield_func: Callable, + expression: BaseElement, + vars: dict, + evaluation: Evaluation, + head: Optional[Symbol] = None, + element_index: Optional[int] = None, + element_count: Optional[int] = None, + fully: bool = True, ): if isinstance(expression, Atom) and expression.sameQ(self.atom): # yield vars, None yield_func(vars, None) def get_match_candidates( - self, elements, expression, attributes, evaluation, vars={} + self, + elements: Tuple[BaseElement], + expression: BaseElement, + attributes: int, + evaluation: Evaluation, + vars: dict = {}, ): return [ element @@ -244,7 +338,7 @@ def get_match_candidates( if (isinstance(element, Atom) and element.sameQ(self.atom)) ] - def get_match_count(self, vars={}): + def get_match_count(self, vars: dict = {}): return (1, 1) @@ -258,17 +352,20 @@ class ExpressionPattern(Pattern): def match( self, - yield_func, - expression, - vars, - evaluation, - head=None, - element_index=None, - element_count=None, - fully=True, + yield_func: Callable, + expression: BaseElement, + vars: dict, + evaluation: Evaluation, + head: Optional[Symbol] = None, + element_index: Optional[int] = None, + element_count: Optional[int] = None, + fully: bool = True, ): evaluation.check_stopped() - attributes = self.head.get_attributes(evaluation.definitions) + if self.attributes is None: + self.attributes = self.head.get_attributes(evaluation.definitions) + attributes = self.attributes + if not A_FLAT & attributes: fully = True if not isinstance(expression, Atom): @@ -443,7 +540,13 @@ def yield_head(head_vars, _): fully=fully, ) - def get_pre_choices(self, yield_choice, expression, attributes, vars): + def get_pre_choices( + self, + yield_choice: Callable, + expression: BaseElement, + attributes: int, + vars: dict, + ): """ If not Orderless, call yield_choice with vars as the parameter. """ @@ -474,7 +577,7 @@ def get_pre_choices(self, yield_choice, expression, attributes, vars): for element in expression.elements: expr_groups[element] = expr_groups.get(element, 0) + 1 - def per_name(yield_name, groups, vars): + def per_name(yield_name: Callable, groups: Tuple, vars: dict): """ Yields possible variable settings (dictionaries) for the remaining pattern groups @@ -544,12 +647,16 @@ def yield_next(next): else: yield_choice(vars) - def __init__(self, expr): - self.head = Pattern.create(expr.head) + def __init__(self, expr: Expression, evaluation: Optional[Evaluation] = None): + head = expr.head + self.attributes = ( + None if evaluation is None else head.get_attributes(evaluation.definition) + ) + self.head = Pattern.create(head) self.elements = [Pattern.create(element) for element in expr.elements] self.expr = expr - def filter_elements(self, head_name): + def filter_elements(self, head_name: str): head_name = ensure_context(head_name) return [ element for element in self.elements if element.get_head_name() == head_name @@ -558,17 +665,17 @@ def filter_elements(self, head_name): def __repr__(self): return "" % self.expr - def get_match_count(self, vars={}): + def get_match_count(self, vars: dict = {}): return (1, 1) def get_wrappings( self, - yield_func, - items, - max_count, - expression, - attributes, - include_flattened=True, + yield_func: Callable, + items: Tuple, + max_count: Optional[int], + expression: Expression, + attributes: int, + include_flattened: bool = True, ): if len(items) == 1: yield_func(items[0]) @@ -588,19 +695,19 @@ def get_wrappings( def match_element( self, - yield_func, - element, - rest_elements, - rest_expression, - vars, - expression, - attributes, - evaluation, - element_index=1, - element_count=None, - first=False, - fully=True, - depth=1, + yield_func: Callable, + element: BaseElement, + rest_elements: Tuple, + rest_expression: Tuple[List, List], + vars: dict, + expression: BaseElement, + attributes: int, + evaluation: Evaluation, + element_index: int = 1, + element_count: Optional[int] = None, + first: bool = False, + fully: bool = True, + depth: int = 1, ): if rest_expression is None: @@ -764,7 +871,12 @@ def yield_wrapping(item): ) def get_match_candidates( - self, elements, expression, attributes, evaluation, vars={} + self, + elements: Tuple[BaseElement], + expression: BaseElement, + attributes: int, + evaluation: Evaluation, + vars: dict = {}, ): """ Finds possible elements that could match the pattern, ignoring future @@ -780,7 +892,12 @@ def get_match_candidates( ] def get_match_candidates_count( - self, elements, expression, attributes, evaluation, vars={} + self, + elements: Tuple[BaseElement], + expression: BaseElement, + attributes: int, + evaluation: Evaluation, + vars: dict = {}, ): """ Finds possible elements that could match the pattern, ignoring future diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 47e33a349..a920cf46a 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -3,8 +3,10 @@ from inspect import signature from itertools import chain +from typing import Callable, Optional -from mathics.core.element import KeyComparable +from mathics.core.element import BaseElement, KeyComparable +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.pattern import Pattern, StopGenerator from mathics.core.symbols import strip_context @@ -26,20 +28,33 @@ class BaseRule(KeyComparable): """ This is the base class from which all other Rules are derived from. - Rules are part of the rewriting system of Mathics. See https://en.wikipedia.org/wiki/Rewriting + Rules are part of the rewriting system of Mathics. See + https://en.wikipedia.org/wiki/Rewriting - This class is not complete in of itself and subclasses should adapt or fill in - what is needed. In particular ``do_replace()`` needs to be implemented. + This class is not complete in of itself and subclasses should + adapt or fill in what is needed. In particular ``do_replace()`` + needs to be implemented. Important subclasses: BuiltinRule and Rule. + """ - def __init__(self, pattern, system=False) -> None: - self.pattern = Pattern.create(pattern) + def __init__( + self, + pattern: Expression, + system: bool = False, + evaluation: Optional[Evaluation] = None, + ) -> None: + self.pattern = Pattern.create(pattern, evaluation=evaluation) self.system = system def apply( - self, expression, evaluation, fully=True, return_list=False, max_list=None + self, + expression: BaseElement, + evaluation: Evaluation, + fully: bool = True, + return_list: bool = False, + max_list: Optional[int] = None, ): result_list = [] # count = 0 @@ -130,11 +145,19 @@ class Rule(BaseRule): ``G[1.^2, a^2]`` """ - def __init__(self, pattern, replace, system=False) -> None: - super(Rule, self).__init__(pattern, system=system) + def __init__( + self, + pattern: Expression, + replace: Expression, + system=False, + evaluation: Optional[Evaluation] = None, + ) -> None: + super(Rule, self).__init__(pattern, system=system, evaluation=evaluation) self.replace = replace - def do_replace(self, expression, vars, options, evaluation): + def do_replace( + self, expression: BaseElement, vars: dict, options: dict, evaluation: Evaluation + ): new = self.replace.replace_vars(vars) new.options = options @@ -197,8 +220,16 @@ class BuiltinRule(BaseRule): This will cause `Expression.evalate() to perform an additional ``rewrite_apply_eval()`` step. """ - def __init__(self, name, pattern, function, check_options, system=False) -> None: - super(BuiltinRule, self).__init__(pattern, system=system) + def __init__( + self, + name: str, + pattern: Expression, + function: Callable, + check_options: Callable, + system: bool = False, + evaluation: Optional[Evaluation] = None, + ) -> None: + super(BuiltinRule, self).__init__(pattern, system=system, evaluation=evaluation) self.name = name self.function = function self.check_options = check_options @@ -206,7 +237,9 @@ def __init__(self, name, pattern, function, check_options, system=False) -> None # If you update this, you must also update traced_do_replace # (that's in the same file TraceBuiltins is) - def do_replace(self, expression, vars, options, evaluation): + def do_replace( + self, expression: BaseElement, vars: dict, options: dict, evaluation: Evaluation + ): if options and self.check_options: if not self.check_options(options, evaluation): return None diff --git a/mathics/core/structure.py b/mathics/core/structure.py index d10f9be43..39ab20dbb 100644 --- a/mathics/core/structure.py +++ b/mathics/core/structure.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from mathics.core.symbols import SymbolList - class Structure: """ diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index 61f70add1..cdfd6bf9d 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -19,7 +19,6 @@ sympy_symbol_prefix = "_Mathics_User_" sympy_slot_prefix = "_Mathics_Slot_" - # FIXME: This is repeated below class NumericOperators: """ @@ -329,57 +328,82 @@ def replace_slots(self, slots, evaluation) -> "Atom": class Symbol(Atom, NumericOperators, EvalMixin): - """ - Note: Symbol is right now used in a couple of ways which in the - future may be separated. + """A Symbol is a kind of Atom that acts as a symbolic variable. - A Symbol is a kind of Atom that acts as a symbolic variable or - symbolic constant. + All Symbols have a name that can be converted to string. - All Symbols have a name that can be converted to string form. + A Variable Symbol is a ``Symbol`` that is associated with a + ``Definition`` that has an ``OwnValue`` that determines its + evaluation value. - Inside a session, a Symbol can be associated with a ``Definition`` - that determines its evaluation value. + A Function Symbol, like a Variable Symbol, is a ``Symbol`` that is + also associated with a ``Definition``. But it has a ``DownValue`` + that is used in its evaluation. - We also have Symbols which are immutable or constant; here the - definitions are fixed. The predefined Symbols ``True``, ``False``, - and ``Null`` are like this. + A Function Symbol, like a Variable Symbol, is a ``Symbol`` that is + also associated with a ``Definition``. But it has a ``DownValue`` + that is used in its evaluation. - Also there are situations where the Symbol acts like Python's - intern() built-in function or Lisp's Symbol without its modifyable - property list. Here, the only attribute we care about is the name - which is unique across all mentions and uses, and therefore - needs it only to be stored as a single object in the system. + We also have Symbols which, in contrast to Variables Symbols, have + a constant value that cannot change. System`True and System`False + are like this. - Note that the mathics.core.parser.Symbol works exactly this way. + These however are in class SymbolConstant. See that class for + more information. - This aspect may or may not be true for the Symbolic Variable use case too. + Symbol acts like Python's intern() built-in function or Lisp's + Symbol without its modifyable property list. Here, the only + attribute we care about is the value which is unique across all + mentions and uses, and therefore needs it only to be stored as a + single object in the system. + + Note that the mathics.core.parser.Symbol works exactly this way. """ name: str hash: str sympy_dummy: Any - defined_symbols = {} + + # Dictionary of Symbols defined so far. + # We use this for object uniqueness. + # The key is the Symbol object's string name, and the + # diectionary's value is the Mathics object for the Symbol. + _symbols = {} + class_head_name = "System`Symbol" # __new__ instead of __init__ is used here because we want # to return the same object for a given "name" value. - def __new__(cls, name: str, sympy_dummy=None, value=None): + def __new__(cls, name: str, sympy_dummy=None): """ - Allocate an object ensuring that for a given `name` we get back the same object. + Allocate an object ensuring that for a given ``name`` and ``cls`` we get back the same object, + id(object) is the same and its object.__hash__() is the same. + + SymbolConstant's like System`True and System`False set + ``value`` to something other than ``None``. + """ name = ensure_context(name) - self = cls.defined_symbols.get(name, None) + + # A lot of the below code is similar to + # the corresponding for numeric constants like Integer, Real. + self = cls._symbols.get(name) + if self is None: - self = super(Symbol, cls).__new__(cls) + self = super().__new__(cls) self.name = name + # Cache object so we don't allocate again. + cls._symbols[name] = self + # Set a value for self.__hash__() once so that every time - # it is used this is fast. - # This tuple with "Symbol" is used to give a different hash - # than the hash that would be returned if just string name were - # used. - self.hash = hash(("Symbol", name)) + # it is used this is fast. Note that in contrast to the + # cached object key, the hash key needs to be unique across *all* + # Python objects, so we include the class in the + # event that different objects have the same Python value. + # For example, this can happen with String constants. + + self.hash = hash((cls, name)) # TODO: revise how we convert sympy.Dummy # symbols. @@ -392,25 +416,8 @@ def __new__(cls, name: str, sympy_dummy=None, value=None): # value attribute. self.sympy_dummy = sympy_dummy - # This is something that still I do not undestand: - # here we are adding another attribute to this class, - # which is not clear where is it going to be used, but - # which can be different to None just three specific instances: - # * ``System`True`` -> True - # * ``System`False`` -> False - # * ``System`Null`` -> None - # - # My guess is that this property should be set for - # ``PredefinedSymbol`` but not for general symbols. - # - # Like it is now, it looks so misterious as - # self.sympy_dummy, for which I have to dig into the - # code to see even what type of value should be expected - # for it. - self._value = value self._short_name = strip_context(name) - cls.defined_symbols[name] = self return self def __eq__(self, other) -> bool: @@ -631,24 +638,59 @@ def to_sympy(self, **kwargs): return sympy.Symbol(sympy_symbol_prefix + self.name) return builtin.to_sympy(self, **kwargs) - @property - def value(self) -> Any: - return self._value - -class PredefinedSymbol(Symbol): +class SymbolConstant(Symbol): """ - A Predefined Symbol of the Mathics system. + A Symbol Constant is Symbol of the Mathics system whose value can't + be changed and has a corresponding Python representation. - A Symbol which is defined because it is used somewhere in the - Mathics system as a built-in name, Attribute, Property, Option, - or a Symbolic Constant. + Therefore, like an ``Integer`` constant such as ``Integer0``, we don't + need to go through ``Definitions`` to get its Python-equivalent value. - In contrast to Symbol where the name might not have been added to - a list of known Symbol names or where the name might get deleted, - this never occurs here. + For example for the ``SymbolConstant`` ``System`True``, has its + value set to the Python ``True`` value. + + Note this is not the same thing as a Symbolic Constant like ``Pi``, + which doesn't have an (exact) Python equivalent representation. + Also, Pi *can* be Unprotected and changed, while True, cannot. + + Also note that ``SymbolConstant`` differs from ``Symbol`` in that + Symbol has no value field (even when its value happens to be + representable in Python. Symbols need to go through Definitions + get a Symbol's current value, based on the current context and the + state of prior operations on that Symbol/Definition binding. + + In sum, SymbolConstant is partly like Symbol, and partly like + Numeric constants. """ + # Dictionary of SymbolConstants defined so far. + # We use this for object uniqueness. + # The key is the SymbolConstant's value, and the + # diectionary's value is the Mathics object representing that Python value. + _symbol_constants = {} + + # We use __new__ here to unsure that two Integer's that have the same value + # return the same object. + def __new__(cls, name, value): + + name = ensure_context(name) + self = cls._symbol_constants.get(name) + if self is None: + self = super().__new__(cls, name) + self._value = value + + # Cache object so we don't allocate again. + self._symbol_constants[name] = self + + # Set a value for self.__hash__() once so that every time + # it is used this is fast. Note that in contrast to the + # cached object key, the hash key needs to be unique across all + # Python objects, so we include the class in the + # event that different objects have the same Python value + self.hash = hash((cls, name)) + return self + @property def is_literal(self) -> bool: """ @@ -676,6 +718,15 @@ def is_uncertain_final_definitions(self, definitions) -> bool: """ return False + @property + def value(self): + return self._value + + +# A BooleanType is a special form of SymbolConstant where the value +# of the constant is either SymbolTrue or SymbolFalse. +BooleanType = SymbolConstant + def symbol_set(*symbols: Tuple[Symbol]) -> FrozenSet[Symbol]: """ @@ -689,10 +740,10 @@ def symbol_set(*symbols: Tuple[Symbol]) -> FrozenSet[Symbol]: # Symbols used in this module. -# Note, below we are only setting PredefinedSymbol for Symbols which +# Note, below we are only setting SymbolConstant for Symbols which # are both predefined and have the Locked attribute. -# An experiment using PredefinedSymbol("Pi") in the Python code and +# An experiment using SymbolConstant("Pi") in the Python code and # running: # {Pi, Unprotect[Pi];Pi=4; Pi, Pi=.; Pi } # show that this does not change the output in any way. @@ -702,9 +753,9 @@ def symbol_set(*symbols: Tuple[Symbol]) -> FrozenSet[Symbol]: # more of the below and in systemsymbols # PredefineSymbol. -SymbolFalse = PredefinedSymbol("System`False", value=False) -SymbolList = PredefinedSymbol("System`List") -SymbolTrue = PredefinedSymbol("System`True", value=True) +SymbolFalse = SymbolConstant("System`False", value=False) +SymbolList = SymbolConstant("System`List", value=list) +SymbolTrue = SymbolConstant("System`True", value=True) SymbolAbs = Symbol("Abs") SymbolDivide = Symbol("Divide") diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 9b0aea436..e78c487e8 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -30,6 +30,9 @@ SymbolAnd = Symbol("System`And") SymbolAppend = Symbol("System`Append") SymbolApply = Symbol("System`Apply") +SymbolArcCos = Symbol("System`ArcCos") +SymbolArcSin = Symbol("System`ArcSin") +SymbolArcTan = Symbol("System`ArcTan") SymbolAssociation = Symbol("System`Association") SymbolAssumptions = Symbol("System`$Assumptions") SymbolAttributes = Symbol("System`Attributes") @@ -41,11 +44,13 @@ SymbolBreak = Symbol("System`Break") SymbolByteArray = Symbol("System`ByteArray") SymbolC = Symbol("System`C") -SymbolCatalan = Symbol("System`Cases") +SymbolCases = Symbol("System`Cases") SymbolCatalan = Symbol("System`Catalan") SymbolCeiling = Symbol("System`Ceiling") +SymbolClusteringComponents = Symbol("System`ClusteringComponents") SymbolColorConvert = Symbol("System`ColorConvert") SymbolColorData = Symbol("System`ColorData") +SymbolColorQuantize = Symbol("System`ColorQuantize") SymbolCompile = Symbol("System`Compile") SymbolCompiledFunction = Symbol("System`CompiledFunction") SymbolComplex = Symbol("System`Complex") @@ -53,6 +58,7 @@ SymbolCondition = Symbol("System`Condition") SymbolConditionalExpression = Symbol("System`ConditionalExpression") SymbolConjugate = Symbol("System`Conjugate") +SymbolContainsOnly = Symbol("System`ContainsOnly") SymbolContext = Symbol("System`$Context") SymbolContextPath = Symbol("System`$ContextPath") SymbolContinue = Symbol("System`Continue") @@ -60,7 +66,9 @@ SymbolCosh = Symbol("System`Cosh") SymbolCot = Symbol("System`Cot") SymbolCoth = Symbol("System`Coth") +SymbolCovariance = Symbol("System`Covariance") SymbolD = Symbol("System`D") +SymbolDefault = Symbol("System`Default") SymbolDefinition = Symbol("System`Definition") SymbolDerivative = Symbol("System`Derivative") SymbolDirectedInfinity = Symbol("System`DirectedInfinity") @@ -72,12 +80,14 @@ SymbolEqual = Symbol("System`Equal") SymbolEquivalent = Symbol("System`Equivalent") SymbolEulerGamma = Symbol("System`EulerGamma") +SymbolExactNumberQ = Symbol("System`ExactNumberQ") SymbolExpandAll = Symbol("System`ExpandAll") SymbolExport = Symbol("System`Export") SymbolExportString = Symbol("System`ExportString") SymbolFaceForm = Symbol("System`FaceForm") SymbolFactorial = Symbol("System`Factorial") SymbolFailed = Symbol("System`$Failed") +SymbolFindClusters = Symbol("System`FindClusters") SymbolFloor = Symbol("System`Floor") SymbolFormat = Symbol("System`Format") SymbolFractionBox = Symbol("System`FractionBox") @@ -111,6 +121,7 @@ SymbolLength = Symbol("System`Length") SymbolLess = Symbol("System`Less") SymbolLessEqual = Symbol("System`LessEqual") +SymbolKey = Symbol("System`Key") SymbolLine = Symbol("System`Line") SymbolLog = Symbol("System`Log") SymbolLog10 = Symbol("System`Log10") @@ -118,13 +129,16 @@ SymbolMachinePrecision = Symbol("System`MachinePrecision") SymbolMakeBoxes = Symbol("System`MakeBoxes") SymbolMap = Symbol("System`Map") +SymbolMapThread = Symbol("System`MapThread") SymbolMatchQ = Symbol("System`MatchQ") SymbolMatrixQ = Symbol("System`MatrixQ") SymbolMathMLForm = Symbol("System`MathMLForm") SymbolMatrixPower = Symbol("System`MatrixPower") SymbolMax = Symbol("System`Max") SymbolMaxPrecision = Symbol("System`$MaxPrecision") +SymbolMaxExtraPrecision = Symbol("System`$MaxExtraPrecision") SymbolMean = Symbol("System`Mean") +SymbolMedian = Symbol("System`Median") SymbolMemberQ = Symbol("System`MemberQ") SymbolMessageName = Symbol("System`MessageName") SymbolMessages = Symbol("System`Messages") @@ -136,7 +150,9 @@ SymbolNeeds = Symbol("System`Needs") SymbolNone = Symbol("System`None") SymbolNorm = Symbol("System`Norm") +SymbolNormal = Symbol("System`Normal") SymbolNot = Symbol("System`Not") +SymbolNothing = Symbol("System`Nothing") SymbolNumberForm = Symbol("System`NumberForm") SymbolNumberQ = Symbol("System`NumberQ") SymbolNumericQ = Symbol("System`NumericQ") @@ -162,7 +178,10 @@ SymbolPolygon = Symbol("System`Polygon") SymbolPossibleZeroQ = Symbol("System`PossibleZeroQ") SymbolPrecision = Symbol("System`Precision") +SymbolQuantity = Symbol("System`Quantity") SymbolQuiet = Symbol("System`Quiet") +SymbolQuotient = Symbol("System`Quotient") +SymbolQuotientRemainder = Symbol("System`QuotientRemainder") SymbolRGBColor = Symbol("System`RGBColor") SymbolRandomComplex = Symbol("System`RandomComplex") SymbolRandomReal = Symbol("System`RandomReal") @@ -173,6 +192,8 @@ SymbolRepeated = Symbol("System`Repeated") SymbolRepeatedNull = Symbol("System`RepeatedNull") SymbolReturn = Symbol("System`Return") +SymbolReverse = Symbol("System`Reverse") +SymbolRight = Symbol("System`Right") SymbolRound = Symbol("System`Round") SymbolRow = Symbol("System`Row") SymbolRowBox = Symbol("System`RowBox") @@ -188,11 +209,20 @@ SymbolSin = Symbol("System`Sin") SymbolSinh = Symbol("System`Sinh") SymbolSlot = Symbol("System`Slot") +SymbolSparseArray = Symbol("System`SparseArray") +SymbolSplit = Symbol("System`Split") SymbolSqrt = Symbol("System'Sqrt") SymbolSqrtBox = Symbol("System`SqrtBox") +SymbolStandardDeviation = Symbol("System`StandardDeviation") SymbolStandardForm = Symbol("System`StandardForm") +SymbolStringExpression = Symbol("System`StringExpression") SymbolStringForm = Symbol("System`StringForm") +SymbolStringInsert = Symbol("System`StringInsert") +SymbolStringJoin = Symbol("System`StringJoin") +SymbolStringPosition = Symbol("System`StringPosition") SymbolStringQ = Symbol("System`StringQ") +SymbolStringRiffle = Symbol("System`StringRiffle") +SymbolStringSplit = Symbol("System`StringSplit") SymbolStyle = Symbol("System`Style") SymbolSubValues = Symbol("System`SubValues") SymbolSubsetQ = Symbol("System`SubsetQ") @@ -213,4 +243,5 @@ SymbolUnequal = Symbol("System`Unequal") SymbolUnevaluated = Symbol("System`Unevaluated") SymbolUpValues = Symbol("System`UpValues") +SymbolVariance = Symbol("System`Variance") SymbolXor = Symbol("System`Xor") diff --git a/mathics/data/.gitignore b/mathics/data/.gitignore index 280c1b447..850313a22 100644 --- a/mathics/data/.gitignore +++ b/mathics/data/.gitignore @@ -1,2 +1,2 @@ -/doc_tex_data.pcl +/doc_latex_data.pcl /op-tables.json diff --git a/mathics/data/ExampleData/InventionNo1.xml b/mathics/data/ExampleData/InventionNo1.xml old mode 100755 new mode 100644 diff --git a/mathics/data/ExampleData/TextRecognize.png b/mathics/data/ExampleData/TextRecognize.png index 3b1d25c40..ac599e9de 100644 Binary files a/mathics/data/ExampleData/TextRecognize.png and b/mathics/data/ExampleData/TextRecognize.png differ diff --git a/mathics/data/ExampleData/copyright.csv b/mathics/data/ExampleData/copyright.csv index 4f1a59c5e..22cb8aa26 100644 --- a/mathics/data/ExampleData/copyright.csv +++ b/mathics/data/ExampleData/copyright.csv @@ -5,13 +5,13 @@ EinsteinSzilLetter.txt Public Domain http://en.wikipedia.org/wiki/File:Einstein- sunflowers.jpg Public Domain United States Department of Agriculture - Agricultural Research Service Taken from http://en.wikipedia.org/wiki/File:Sunflowers.jpg as of 2012/09/13 MadTeaParty.gif Public Domain http://www.gutenberg.org/files/114/114-h/114-h.htm Illustration by Sir John Tenniel for 'Alice in Wonderland'. Taken from Project Guttenberg as of 2012/09/25 BloodToilTearsSweat.txt Public Domain http://www.fiftiesweb.com/usa/winston-churchill-blood-toil.htm May 13, 1940 Winston Churchill "Blood, Toil, Tears and Sweat". Taken as of 2012/09/25 -lena.tif non-free : overlooked http://sipi.usc.edu/database/ Famous 'lena' or 'lenna' test image moon.tif Public Domain http://photojournal.jpl.nasa.gov/target/moon Taken from Nasa JPL 'PhotoJournal' as of 2012/09/25 and resized using ImageMagik-6.7.9 ExampleData.txt GNU Free Documentation License 1.2 Own Work Created By Angus Griffith with a simple Python script numberdata.csv GNU Free Documentation License 1.2 Own Work Created By Angus Griffith with a simple Python script -colors.json GNU Free Documentation License 1.2 Own Work Created Manually By Angus Griffith +colors.json GNU Free Documentation License 1.2 Own Work Created Manually By Angus Griffith Testosterone.svg Public Domain http://en.wikipedia.org/wiki/File:Testosteron.svg Taken from http://en.wikipedia.org/wiki/Testosterone on 2013/03/03 InventionNo1.xml GNU Free Documentation License http://openmusicscore.org/index.php?option=com_content&view=article&id=125:invention-no-1-bwv-772-bach-johann-sebastian&catid=84&Itemid=531 -Namespaces.xml CC BY-NC-SA License http://edutechwiki.unige.ch/en/XML_namespace "A larger example of namespace scoping" with comments removed +Namespaces.xml W3C License https://www.w3.org/TR/REC-xml-names/#defaulting "A larger example of namespace scoping" with comments removed Middlemarch.txt The Project Gutenberg License https://archive.org/details/middlemarch00145gut Chapter 75 of George Eliot's novel Middlemarch in ISO Latin 1 (8859-1) encoding PrimeMeridian.html CC-BY-SA and GFDL https://en.wikipedia.org/wiki/Prime_meridian HTML of the English Wikipedia entry for "Prime Meridian" as of 2016/11/02; also see https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content +hedy.tif Public Domain https://es.wikipedia.org/wiki/Hedy_Lamarr#/media/Archivo:Hedy_Lamarr_in_The_Heavenly_Body_1944.jpg Image obtained in Wikipedia in B&W, colorized in https://deepai.org/machine-learning-model/colorizer and exported to .tif (from .jpeg) by GIMP diff --git a/mathics/data/ExampleData/hedy.tif b/mathics/data/ExampleData/hedy.tif new file mode 100644 index 000000000..1254f3464 Binary files /dev/null and b/mathics/data/ExampleData/hedy.tif differ diff --git a/mathics/data/ExampleData/lena.tif b/mathics/data/ExampleData/lena.tif deleted file mode 100644 index ffe5c835d..000000000 Binary files a/mathics/data/ExampleData/lena.tif and /dev/null differ diff --git a/mathics/data/ExampleData/numberdata.csv b/mathics/data/ExampleData/numberdata.csv old mode 100755 new mode 100644 diff --git a/mathics/doc/.gitignore b/mathics/doc/.gitignore new file mode 100644 index 000000000..c813fcae9 --- /dev/null +++ b/mathics/doc/.gitignore @@ -0,0 +1 @@ +/version-info.tex diff --git a/mathics/doc/common_doc.py b/mathics/doc/common_doc.py index 1a30fce4c..e437ee54e 100644 --- a/mathics/doc/common_doc.py +++ b/mathics/doc/common_doc.py @@ -23,7 +23,7 @@ More importantly, this code should be replaced by Sphinx and autodoc. Things are such a mess, that it is too difficult to contemplate this right now. """ - +import importlib import os.path as osp import pkgutil import re @@ -36,6 +36,7 @@ from mathics.core.evaluation import Message, Print from mathics.core.util import IS_PYPY from mathics.doc.utils import slugify +from mathics.eval.pymathics import pymathics_builtins_by_module, pymathics_modules # These are all the XML/HTML-like tags that documentation supports. ALLOWED_TAGS = ( @@ -143,7 +144,7 @@ def get_module_doc(module: ModuleType) -> tuple: return title, text -def get_results_by_test(test_expr: str, full_test_key: list, doc_data: dict) -> list: +def get_results_by_test(test_expr: str, full_test_key: list, doc_data: dict) -> dict: """ Sometimes test numbering is off, either due to bugs or changes since the data was read. @@ -283,6 +284,7 @@ def skip_module_doc(module, modules_seen) -> bool: return ( module.__doc__ is None or module in modules_seen + or module.__name__.split(".")[0] not in ("mathics", "pymathics") or hasattr(module, "no_doc") and module.no_doc ) @@ -346,7 +348,7 @@ def gather_tests( class Documentation: - def __init__(self, part: str, title: str, doc=None): + def __init__(self, part, title: str, doc=None): self.doc = doc self.guide_sections = [] self.part = part @@ -356,6 +358,312 @@ def __init__(self, part: str, title: str, doc=None): self.title = title part.chapters_by_slug[self.slug] = self + def add_section( + self, + chapter, + section_name: str, + section_object, + operator, + is_guide: bool = False, + in_guide: bool = False, + ): + """ + Adds a DocSection or DocGuideSection + object to the chapter, a DocChapter object. + "section_object" is either a Python module or a Class object instance. + """ + 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 + if not section_object.__doc__: + return + if is_guide: + section = self.doc_guide_section_fn( + chapter, + section_name, + section_object.__doc__, + section_object, + installed=installed, + ) + chapter.guide_sections.append(section) + else: + section = self.doc_section_fn( + chapter, + section_name, + section_object.__doc__, + operator=operator, + installed=installed, + in_guide=in_guide, + ) + chapter.sections.append(section) + + return section + + def add_subsection( + self, + chapter, + section, + subsection_name: str, + instance, + operator=None, + in_guide=False, + ): + 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 + + """ + Append a subsection for ``instance`` into ``section.subsections`` + """ + installed = True + for package in getattr(instance, "requires", []): + try: + importlib.import_module(package) + except ImportError: + installed = False + break + + # 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 + summary_text = ( + instance.summary_text if hasattr(instance, "summary_text") else "" + ) + subsection = self.doc_subsection_fn( + chapter, + section, + subsection_name, + instance.__doc__, + operator=operator, + installed=installed, + in_guide=in_guide, + summary_text=summary_text, + ) + section.subsections.append(subsection) + + def doc_part(self, title, modules, builtins_by_module, start): + """ + Produce documentation for a "Part" - reference section or + possibly Pymathics modules + """ + builtin_part = self.doc_part_fn(self, title, is_reference=start) + modules_seen = set([]) + + want_sorting = True + if want_sorting: + module_collection_fn = lambda x: sorted( + modules, + key=lambda module: module.sort_order + if hasattr(module, "sort_order") + else module.__name__, + ) + else: + module_collection_fn = lambda x: x + for module in module_collection_fn(modules): + if skip_module_doc(module, modules_seen): + continue + title, text = get_module_doc(module) + chapter = self.doc_chapter_fn( + builtin_part, title, self.doc_fn(text, title, None) + ) + + builtins = builtins_by_module.get(module.__name__, None) + if builtins is None: + builtins = [] + for name, items in builtins_by_module.items(): + if name.startswith(module.__name__): + builtins.extend(items) + + sections = [ + builtin for builtin in builtins if not skip_doc(builtin.__class__) + ] + + if module.__file__.endswith("__init__.py"): + # We have a Guide Section. + name = get_doc_name_from_module(module) + guide_section = self.add_section( + chapter, name, module, operator=None, is_guide=True + ) + submodules = [ + value + for value in module.__dict__.values() + if isinstance(value, ModuleType) + ] + + sorted_submodule = lambda x: sorted( + submodules, + key=lambda submodule: submodule.sort_order + if hasattr(submodule, "sort_order") + else submodule.__name__, + ) + + # Add sections in the guide section... + for submodule in sorted_submodule(submodules): + if skip_module_doc(submodule, modules_seen): + continue + elif IS_PYPY and submodule.__name__ == "builtins": + # PyPy seems to add this module on its own, + # but it is not something that can be importable + continue + + submodule_name = get_doc_name_from_module(submodule) + section = self.add_section( + chapter, + submodule_name, + submodule, + operator=None, + is_guide=False, + in_guide=True, + ) + modules_seen.add(submodule) + guide_section.subsections.append(section) + + builtins = builtins_by_module.get(submodule.__name__, []) + subsections = [builtin for builtin in builtins] + for instance in subsections: + if hasattr(instance, "no_doc") and instance.no_doc: + continue + + modules_seen.add(instance) + name = instance.get_name(short=True) + + self.add_subsection( + chapter, + section, + instance.get_name(short=True), + instance, + instance.get_operator(), + in_guide=True, + ) + else: + self.doc_sections(sections, modules_seen, chapter) + builtin_part.chapters.append(chapter) + self.parts.append(builtin_part) + + def doc_sections(self, sections, modules_seen, chapter): + for instance in sections: + if instance not in modules_seen and ( + not hasattr(instance, "no_doc") or not instance.no_doc + ): + name = instance.get_name(short=True) + self.add_section( + chapter, + name, + instance, + instance.get_operator(), + is_guide=False, + in_guide=False, + ) + modules_seen.add(instance) + + def gather_doctest_data(self): + """ + Extract doctest data from various static XML-like doc files, Mathics3 Built-in functions + (inside mathics.builtin), and external Mathics3 Modules. + + The extracted structure is stored in ``self``. + """ + + # First gather data from static XML-like files. This constitutes "Part 1" of the + # documentation. + files = listdir(self.doc_dir) + files.sort() + appendix = [] + + for file in files: + part_title = file[2:] + if part_title.endswith(".mdoc"): + part_title = part_title[: -len(".mdoc")] + part = self.doc_part_fn(self, part_title) + text = open(osp.join(self.doc_dir, file), "rb").read().decode("utf8") + text = filter_comments(text) + chapters = CHAPTER_RE.findall(text) + for title, text in chapters: + chapter = self.doc_chapter_fn(part, title) + text += '
      ' + sections = SECTION_RE.findall(text) + for pre_text, title, text in sections: + if title: + section = self.doc_section_fn( + chapter, title, text, operator=None, installed=True + ) + chapter.sections.append(section) + subsections = SUBSECTION_RE.findall(text) + for subsection_title in subsections: + subsection = self.doc_subsection_fn( + chapter, + section, + subsection_title, + text, + ) + section.subsections.append(subsection) + pass + pass + else: + section = None + if not chapter.doc: + chapter.doc = self.doc_fn(pre_text, title, section) + pass + + part.chapters.append(chapter) + if file[0].isdigit(): + self.parts.append(part) + else: + part.is_appendix = True + appendix.append(part) + + # Next extract data that has been loaded into Mathics3 when it runs. + # This is information from `mathics.builtin`. + # This is Part 2 of the documentation. + + for title, modules, builtins_by_module, start in [ + ( + "Reference of Built-in Symbols", + builtin.modules, + builtin.builtins_by_module, + True, + ) + ]: + self.doc_part(title, modules, builtins_by_module, start) + + # Now extract external Mathics3 Modules that have been loaded via + # LoadModule, or eval_LoadModule. + + # This is Part 3 of the documentation. + + for title, modules, builtins_by_module, start in [ + ( + "Mathics3 Modules", + pymathics_modules, + pymathics_builtins_by_module, + True, + ) + ]: + self.doc_part(title, modules, builtins_by_module, start) + + # Now extract Appendix information. This include License text + + # This is the final Part of the documentation. + + for part in appendix: + self.parts.append(part) + + # Via the wanderings above, collect all tests that have been + # seen. + # + # Each test is accessble by its part + chapter + section and test number + # in that section. + for tests in self.get_tests(): + for test in tests.tests: + test.key = (tests.part, tests.chapter, tests.section, test.index) + return + def get_part(self, part_slug): return self.parts_by_slug.get(part_slug) @@ -458,7 +766,14 @@ def all_sections(self): class DocSection: def __init__( - self, chapter, title, text, operator=None, installed=True, in_guide=False + self, + chapter, + title: str, + text: str, + operator, + installed=True, + in_guide=False, + summary_text="", ): self.chapter = chapter @@ -469,7 +784,9 @@ def __init__( self.slug = slugify(title) self.subsections = [] self.subsections_by_slug = {} + self.summary_text = summary_text self.title = title + if text.count("
      ") != text.count("
      "): raise ValueError( "Missing opening or closing
      tag in " @@ -556,6 +873,7 @@ def __init__( operator=None, installed=True, in_guide=False, + summary_text="", ): """ Information that goes into a subsection object. This can be a written text, or @@ -571,6 +889,11 @@ def __init__( the "section" name for the class Read (the subsection) inside it. """ + title_summary_text = re.split(" -- ", title) + n = len(title_summary_text) + self.title = title_summary_text[0] if n > 0 else "" + self.summary_text = title_summary_text[1] if n > 1 else summary_text + self.doc = XMLDoc(text, title, section) self.chapter = chapter self.in_guide = in_guide @@ -582,10 +905,18 @@ def __init__( self.subsections = [] self.title = title + if section: + chapter = section.chapter + part = chapter.part + # Note: we elide section.title + key_prefix = (part.title, chapter.title, title) + else: + key_prefix = None + if in_guide: # Tests haven't been picked out yet from the doc string yet. # Gather them here. - self.items = gather_tests(text, DocTests, DocTest, DocText) + self.items = gather_tests(text, DocTests, DocTest, DocText, key_prefix) else: self.items = [] @@ -688,257 +1019,41 @@ def __str__(self): return self.test +# FIXME: think about - do we need this? Or can we use DjangoMathicsDocumentation and +# LatTeXMathicsDocumentation only? class MathicsMainDocumentation(Documentation): + """ + This module is used for creating test data and saving it to a Python Pickle file + and running tests that appear in the documentation (doctests). + + There are other classes DjangoMathicsDocumentation and LaTeXMathicsDocumentation + that format the data accumulated here. In fact I think those can sort of serve + instead of this. + """ + def __init__(self, want_sorting=False): + self.doc_chapter_fn = DocChapter self.doc_dir = settings.DOC_DIR - self.latex_file = settings.DOC_LATEX_FILE + self.doc_fn = XMLDoc + self.doc_guide_section_fn = DocGuideSection + self.doc_part_fn = DocPart + self.doc_section_fn = DocSection + self.doc_subsection_fn = DocSubsection + self.doctest_latex_pcl_path = settings.DOCTEST_LATEX_DATA_PCL self.parts = [] self.parts_by_slug = {} self.pymathics_doc_loaded = False - self.doc_data_file = settings.get_doc_tex_data_path(should_be_readable=True) - self.title = "Overview" - files = listdir(self.doc_dir) - files.sort() - appendix = [] - - for file in files: - part_title = file[2:] - if part_title.endswith(".mdoc"): - part_title = part_title[: -len(".mdoc")] - part = DocPart(self, part_title) - text = open(osp.join(self.doc_dir, file), "rb").read().decode("utf8") - text = filter_comments(text) - chapters = CHAPTER_RE.findall(text) - for title, text in chapters: - chapter = DocChapter(part, title) - text += '
      ' - sections = SECTION_RE.findall(text) - for pre_text, title, text in sections: - if title: - section = DocSection( - chapter, title, text, operator=None, installed=True - ) - chapter.sections.append(section) - subsections = SUBSECTION_RE.findall(text) - for subsection_title in subsections: - subsection = DocSubsection( - chapter, - section, - subsection_title, - text, - ) - section.subsections.append(subsection) - pass - pass - else: - section = None - if not chapter.doc: - chapter.doc = XMLDoc(pre_text, title, section) - - part.chapters.append(chapter) - if file[0].isdigit(): - self.parts.append(part) - else: - part.is_appendix = True - appendix.append(part) - - for title, modules, builtins_by_module, start in [ - ( - "Reference of Built-in Symbols", - builtin.modules, - builtin.builtins_by_module, - True, - ) - ]: # nopep8 - # ("Reference of optional symbols", optional.modules, - # optional.optional_builtins_by_module, False)]: - - builtin_part = DocPart(self, title, is_reference=start) - modules_seen = set() - if want_sorting: - module_collection_fn = lambda x: sorted( - modules, - key=lambda module: module.sort_order - if hasattr(module, "sort_order") - else module.__name__, - ) - else: - module_collection_fn = lambda x: x - - for module in module_collection_fn(modules): - if skip_module_doc(module, modules_seen): - continue - title, text = get_module_doc(module) - chapter = DocChapter(builtin_part, title, XMLDoc(text, title, None)) - builtins = builtins_by_module[module.__name__] - # FIXME: some Box routines, like RowBox *are* - # documented - sections = [ - builtin - for builtin in builtins - if not builtin.__class__.__name__.endswith("Box") - ] - if module.__file__.endswith("__init__.py"): - # We have a Guide Section. - name = get_doc_name_from_module(module) - guide_section = self.add_section( - chapter, name, module, operator=None, is_guide=True - ) - submodules = [ - value - for value in module.__dict__.values() - if isinstance(value, ModuleType) - ] - - # Add sections in the guide section... - for submodule in submodules: - # FIXME add an additional mechanism in the module - # to allow a docstring and indicate it is not to go in the - # user manual - if submodule.__doc__ is None: - continue - elif IS_PYPY and submodule.__name__ == "builtins": - # PyPy seems to add this module on its own, - # but it is not something that can be importable - continue - - if submodule in modules_seen: - continue - - section = self.add_section( - chapter, - get_doc_name_from_module(submodule), - submodule, - operator=None, - is_guide=False, - in_guide=True, - ) - modules_seen.add(submodule) - guide_section.subsections.append(section) - builtins = builtins_by_module[submodule.__name__] - - subsections = [ - builtin - for builtin in builtins - if not builtin.__class__.__name__.endswith("Box") - ] - for instance in subsections: - modules_seen.add(instance) - name = instance.get_name(short=True) - self.add_subsection( - chapter, - section, - instance.get_name(short=True), - instance, - instance.get_operator(), - in_guide=True, - ) - else: - for instance in sections: - if instance not in modules_seen: - name = instance.get_name(short=True) - self.add_section( - chapter, - instance.get_name(short=True), - instance, - instance.get_operator(), - is_guide=False, - in_guide=False, - ) - modules_seen.add(instance) - pass - pass - pass - builtin_part.chapters.append(chapter) - self.parts.append(builtin_part) - - for part in appendix: - self.parts.append(part) - - # set keys of tests - for tests in self.get_tests(want_sorting=want_sorting): - for test in tests.tests: - test.key = (tests.part, tests.chapter, tests.section, test.index) - - def add_section( - self, - chapter, - section_name: str, - section_object, - operator, - is_guide: bool = False, - in_guide: bool = False, - ): - """ - Adds a DocSection or DocGuideSection - object to the chapter, a DocChapter object. - "section_object" is either a Python module or a Class object instance. - """ - 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 - if not section_object.__doc__: - return - if is_guide: - section = DocGuideSection( - chapter, - section_name, - section_object.__doc__, - section_object, - installed=installed, - ) - chapter.guide_sections.append(section) - else: - section = DocSection( - chapter, - section_name, - section_object.__doc__, - operator=operator, - installed=installed, - in_guide=in_guide, - ) - chapter.sections.append(section) - - return section - - def add_subsection( - self, - chapter, - section, - subsection_name: str, - instance, - operator=None, - in_guide=False, - ): - 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( - chapter, - section, - subsection_name, - instance.__doc__, - operator=operator, - installed=installed, - in_guide=in_guide, + self.doc_data_file = settings.get_doctest_latex_data_path( + should_be_readable=True ) - section.subsections.append(subsection) - - def load_pymathics_doc(self): - # This has always been broken. Revisit after revising Pymathics. - return + self.title = "Overview" class XMLDoc: """A class to hold our internal XML-like format data. - The `latex()` method can turn this into LaTeX. + Specialized classes like LaTeXDoc or and DjangoDoc provide methods for + getting formatted output. For LaTeXDoc ``latex()`` is added while for + DjangoDoc ``html()`` is added Mathics core also uses this in getting usage strings (`??`). """ @@ -1019,6 +1134,7 @@ def test_indices(self): class DocTests: def __init__(self): self.tests = [] + self.text = "" def get_tests(self): return self.tests diff --git a/mathics/doc/documentation/1-Manual.mdoc b/mathics/doc/documentation/1-Manual.mdoc index 24be7b831..9b3477920 100644 --- a/mathics/doc/documentation/1-Manual.mdoc +++ b/mathics/doc/documentation/1-Manual.mdoc @@ -36,30 +36,36 @@ Performance of \Mathics is not, right now, practical in large-scale projects and
      -Some of the features of \Mathics are: +Some of the features of \Mathics tries to be compatible with Wolfram-Language kernel within the confines of the Python ecosystem. + +Given this, it is a powerful functional programming language, driven by pattern matching and rule application. + +Primitive types include rationals, complex numbers, and arbitrary-precision numbers. Other primitive types such as images or graphs, or NLP come from the various Python libraries that \Mathics uses. + +Outside of the "core" \Mathics kernel (which has a only primitive command-line interface), in separate github projects, as add-ons, there is: +
        -
      • a powerful functional programming language, -
      • a system driven by pattern matching and rules application, -
      • rationals, complex numbers, and arbitrary-precision arithmetic, -
      • lots of list and structure manipulation routines, -
      • an interactive graphical user interface right in the Web browser using MathML (apart from a command line interface), -
      • creation of graphics (e.g. plots) and display in the browser using SVG for 2D graphics and three.js for 3D graphics, -
      • export of results to \LaTeX (using Asymptote for graphics), -
      • an easy way of defining new functions in Python and which hooks into Python libraries -
      • an integrated documentation and testing system. +
      • a Django-based web server +
      • a command-line interface using either prompt-toolkit, or GNU Readline +
      • a :Mathics3 module for Graphs:https://pypi.org/project/pymathics-graph/ (via :NetworkX:https://networkx.org/), +
      • a :Mathics3 module for NLP:https://pypi.org/project/pymathics-natlang/ (via :nltk:https://www.nltk.org/, :spacy:https://spacy.io/, and others) +
      • a :A docker container:https://hub.docker.com/r/mathicsorg/mathics which bundles all of the above
      -The first alpha versions of \Mathics were done in 2011 by Jan Pöschko. He worked on it for a couple of years to about v0.5 which had 386 built-in symbols. Currently there are over a 1,000. +The first alpha versions of \Mathics were done in 2011 by Jan Pöschko. He worked on it for a couple of years to about the v0.5 release in 2012. By then, it had 386 built-in symbols. Currently there are over a 1,000 and even more when \Mathics modules are included. -After that, Angus Griffith took over primary leadership and rewrote the parser to pretty much the stage it is in now. He and later Ben Jones worked on it from 2013 to about 2017 to the v1.0 release. Towards the end of this period, Bernhard Liebl worked on this mostly focused on graphics. +After that, Angus Griffith took over primary leadership and rewrote the parser to pretty much the stage it is in now. He and later Ben Jones worked on it from 2013 to about 2017 to the v1.0 release. Towards the end of this period, Bernhard Liebl worked on this, mostly focused on graphics. A :docker image of the v.9 release: https://hub.docker.com/r/arkadi/mathics can be found on dockerhub. -The project was largely abandoned in its Python 2.7 state around 2017. Subsequently it was picked up by the current developers. A list of authors and contributors can be found in the -:AUTHORS.txt: https://github.com/Mathics3/mathics-core/blob/master/AUTHORS.txt file. +Around 2017, the project was largely abandoned in its largely Python 2.7 state, with support for Python 3.2-3.5 via six. + +Subsequently, around mid 2020, it was picked up by the current developers. A list of authors and contributors can be found in the +:AUTHORS.txt: +https://github.com/Mathics3/mathics-core/blob/master/AUTHORS.txt file.
      @@ -70,7 +76,7 @@ While we always could use help, such as in Python programming, improving Documen
      • Ensure this document is complete and accurate. We could use help to ensure all of the Builtin functions described properly and fully, and that they have link to corresponding Wiki, Sympy, WMA and/or mpath links. Make sure the builtin summaries and examples clear and useful.
      • -
      • We could use help in LaTeX styling, and going over this document to remove overful boxes and things of that nature. We could also use help and our use of Asymptote. The are some graphics primitives such as for polyhedra that haven't been implemented. Similar graphics options are sometimes missing in Aymptote that we have available in other graphics backends.
      • +
      • We could use help in LaTeX styling, and going over this document to remove overful boxes and things of that nature. We could also use help and our use of Asymptote. The are some graphics primitives such as for polyhedra that haven't been implemented. Similar graphics options are sometimes missing in Asymptote that we have available in other graphics backends.
      • add another graphics backend: it could be a javascript library like jsfiddle
      • @@ -168,8 +174,8 @@ Of course, \Mathics has complex numbers: ('!' denotes the factorial function.) The precision of numerical evaluation can be set: - >> N[Pi, 100] - = 3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068 + >> N[Pi, 30] + = 3.14159265358979323846264338328 Division by zero is forbidden: >> 1 / 0 @@ -1060,9 +1066,7 @@ Three-dimensional plots are supported as well: - - - +
        Let\'s sketch the function diff --git a/mathics/doc/documentation/images/.gitignore b/mathics/doc/documentation/images/.gitignore new file mode 100644 index 000000000..b890c95fa --- /dev/null +++ b/mathics/doc/documentation/images/.gitignore @@ -0,0 +1 @@ +/classes.pdf diff --git a/mathics/doc/images.sh b/mathics/doc/images.sh index ceb9d502c..b5cbeca18 100755 --- a/mathics/doc/images.sh +++ b/mathics/doc/images.sh @@ -1,15 +1,31 @@ #!/bin/bash # The program create PDF images that can be imbedded into the # Mathics manual. In particular the Mathics heptatom logo and the -# Mathics logo with a showdow that extends a little bit down forward right. -mkdir -p "tex/images" +# Mathics logo with a shadow that extends a little bit down forward right. + + +bs=${BASH_SOURCE[0]} +mydir=$(dirname $bs) +cd $mydir +mydir=$(pwd) + +if [[ -n $DOCTEST_LATEX_DATA_PCL ]]; then + LATEX_DIR=$(basename $DOCTEST_LATEX_DATA_PCL) +else + LATEX_DIR=${mydir}/latex +fi +IMAGE_DIR=${LATEX_DIR}/images + +if [[ ! -d "$IMAGE_DIR" ]] ; then + mkdir -p $IMAGE_DIR +fi for filename in $(find documentation/images/ -name "*.eps"); do - pdf="$(dirname "$filename")/$(basename "$filename" .eps).pdf" + pdf="${LATEX_DIR}/$(basename "$filename" .eps).pdf" epstopdf "$filename" - mv "$pdf" "tex/images/" + mv "$pdf" $IMAGE_DIR done -for filename in images/logo-{heptatom,text-nodrop}.svg; do - inkscape $filename --export-filename="tex/$(basename "$filename" .svg).pdf" --batch-process +for filename in ${mydir}/images/logo-{heptatom,text-nodrop}.svg; do + inkscape $filename --export-filename="latex/$(basename "$filename" .svg).pdf" --batch-process done diff --git a/mathics/doc/latex/.gitignore b/mathics/doc/latex/.gitignore index d635b1dbf..60872e954 100644 --- a/mathics/doc/latex/.gitignore +++ b/mathics/doc/latex/.gitignore @@ -1,5 +1,5 @@ /core-version.tex -/doc_tex_data.pcl +/doc_latex_data.pcl /documentation.tex /documentation.tex-before-sed /images/ @@ -10,6 +10,7 @@ /mathics-*.dvi /mathics-*.eps /mathics-*.pdf +/mathics-*.ps /mathics-*.tex /mathics-test.aux /mathics-test.dvi diff --git a/mathics/doc/latex/Makefile b/mathics/doc/latex/Makefile index e7f9d3c12..8a6f6e726 100644 --- a/mathics/doc/latex/Makefile +++ b/mathics/doc/latex/Makefile @@ -6,22 +6,27 @@ LATEXMK ?= latexmk BASH ?= /bin/bash #-quiet -DOC_TEX_DATA_PCL ?= $(HOME)/.local/var/mathics/doc_tex_data.pcl +# Location of Python Pickle file containg doctest tests and test results formatted for LaTeX +DOCTEST_LATEX_DATA_PCL ?= $(HOME)/.local/var/mathics/doctest_latex_data.pcl + +# Variable indicating Mathics3 Modules you have available on your system, in latex2doc option format +# MATHICS3_MODULE_OPTION ?= --load-module pymathics.graph,pymathics.natlang +MATHICS3_MODULE_OPTION ?= #: Default target: Make everything all doc texdoc: mathics.pdf #: Create internal Document Data from .mdoc and Python builtin module docstrings -doc-data $(DOC_TEX_DATA_PCL): - MATHICS_CHARACTER_ENCODING="" $(PYTHON) doc-extract.py --output --keep-going --want-sorting) +doc-data $(DOCTEST_LATEX_DATA_PCL): + MATHICS_CHARACTER_ENCODING="" $(PYTHON) doc-extract.py --output --keep-going --want-sorting $(MATHICS3_MODULE_OPTION) #: Build mathics PDF -mathics.pdf: mathics.tex documentation.tex logo-text-nodrop.pdf logo-heptatom.pdf version-info.tex $(DOC_TEX_DATA_PCL) +mathics.pdf: mathics.tex documentation.tex logo-text-nodrop.pdf logo-heptatom.pdf version-info.tex $(DOCTEST_LATEX_DATA_PCL) $(LATEXMK) --verbose -f -pdf -pdflatex="$(XETEX) -halt-on-error" mathics #: File containing version information version-info.tex: doc2latex.py - $(PYTHON) doc2latex.py && $(BASH) ./sed-hack.sh + $(PYTHON) doc2latex.py $(MATHICS3_MODULE_OPTION )&& $(BASH) ./sed-hack.sh #: Build test PDF mathics-test.pdf: mathics-test.tex testing.tex @@ -32,9 +37,9 @@ mathics-test.pdf: mathics-test.tex testing.tex logo-heptatom.pdf logo-text-nodrop.pdf: (cd .. && $(BASH) ./images.sh) -#: The build of the documentation which is derived from docstrings in the Python code -documentation.tex: $(DOC_TEX_DATA_PCL) - $(PYTHON) ./doc2latex.py +#: The build of the documentation which is derived from docstrings in the Python code and doctest data +documentation.tex: $(DOCTEST_LATEX_DATA_PCL) + $(PYTHON) ./doc2latex.py $(MATHICS3_MODULE_OPTION) && $(BASH) ./sed-hack.sh #: Same as mathics.pdf pdf latex: mathics.pdf @@ -46,6 +51,6 @@ clean: rm -f mathics.fdb_latexmk mathics.ilg mathics.ind mathics.maf mathics.pre || true rm -f mathics_*.* || true rm -f mathics-test.asy mathics-test.aux mathics-test.idx mathics-test.log mathics-test.mtc mathicsest.mtc* mathics-test.out mathics-test.toc || true - rm -f documentation.tex $(DOC_TEX_DATA_PCL) || true + rm -f documentation.tex $(DOCTEST_LATEX_DATA_PCL) || true rm -f mathics.pdf mathics.dvi test-mathics.pdf test-mathics.dvi || true rm -f mathics-test.pdf mathics-test.dvi version-info.tex || true diff --git a/mathics/doc/latex/README.rst b/mathics/doc/latex/README.rst index c6ec33dd6..38f7e0699 100644 --- a/mathics/doc/latex/README.rst +++ b/mathics/doc/latex/README.rst @@ -18,13 +18,13 @@ Workflow The overall top-level LaTeX document is ``mathic.tex``. The pulls in ``documentation.tex`` which is automatically generated from the Python program ``doc2latex.py`` and that in turn gets its data from -``doc_tex_data.pcl`` which in turn gets its data from ``../documentation/*.mdoc``. +``doc_latex_data.pcl`` which in turn gets its data from ``../documentation/*.mdoc``. Here is a flow of the data:: doc/documentation/*.mdoc --+ | - bultins/*.py -------------+--> doc_tex_data.pcl ---> documentation.tex -+ + bultins/*.py -------------+--> doc_latex_data.pcl -> documentation.tex -+ docpipeline.py doc2latex.py | | doc/images/*.svg -------------> doc/tex/log*.pdf ------------------------+------------------------------> mathics.pdf diff --git a/mathics/doc/latex/doc-extract.py b/mathics/doc/latex/doc-extract.py index f216089ff..e3b9ddabb 100644 --- a/mathics/doc/latex/doc-extract.py +++ b/mathics/doc/latex/doc-extract.py @@ -41,6 +41,7 @@ from mathics.core.systemsymbols import SymbolExport, SymbolImage from mathics.doc.common_doc import sorted_chapters from mathics.doc.latex_doc import LaTeXDocTest, LaTeXMathicsMainDocumentation +from mathics.eval.pymathics import PyMathicsLoadException, eval_LoadModule from mathics.timing import show_lru_cache_statistics builtins = builtins_dict() @@ -78,7 +79,7 @@ def format_expr(test: LaTeXDocTest, result: Result, evaluation: Evaluation): except: pass - result.result = expr.format(evaluation, "System`OutputForm") + # result.result = expr.format(evaluation, "System`OutputForm") class TestOutput(Output): @@ -134,13 +135,7 @@ def test_case(test, tests, index=0, subindex=0, quiet=False, section=None) -> bo test, wanted_out, wanted = test.test, test.outs, test.result def fail(why): - print_and_log( - f"""{sep} -{why} -""".encode( - "utf-8" - ) - ) + print_and_log(f"""{sep}\n{why}""".encode("utf-8")) return False if not quiet: @@ -407,7 +402,7 @@ def test_all( if generate_output: if texdatafolder is None: texdatafolder = osp.dirname( - settings.get_doc_tex_data_path( + settings.get_doctest_latex_data_path( should_be_readable=False, create_parent=True ) ) @@ -472,18 +467,18 @@ def test_all( def load_doc_data(): - doc_tex_data_path = settings.get_doc_tex_data_path(should_be_readable=True) - print(f"Loading internal document data from {doc_tex_data_path}") - with open_ensure_dir(doc_tex_data_path, "rb") as doc_data_file: + doc_latex_data_path = settings.get_doctest_latex_data_path(should_be_readable=True) + print(f"Loading internal document data from {doc_latex_data_path}") + with open_ensure_dir(doc_latex_data_path, "rb") as doc_data_file: return pickle.load(doc_data_file) def save_doc_data(output_data): - doc_tex_data_path = settings.get_doc_tex_data_path( + doc_latex_data_path = settings.get_doctest_latex_data_path( should_be_readable=False, create_parent=True ) - print(f"Writing internal document data to {doc_tex_data_path}") - with open(settings.DOC_USER_TEX_DATA_PATH, "wb") as output_file: + print(f"Writing internal document data to {doc_latex_data_path}") + with open(settings.DOCTEST_LATEX_DATA_PCL, "wb") as output_file: pickle.dump(output_data, output_file, 4) @@ -589,6 +584,20 @@ def main(): action="store_true", help="print cache statistics", ) + parser.add_argument( + "--load-module", + "-l", + dest="pymathics", + metavar="MATHIC3-MODULES", + help="load Mathics3 module MATHICS3-MODULES. " + "You can list multiple Mathics3 Modules by adding a comma (and no space) in between " + "module names.", + ) + parser.add_argument( + "--want-sorting", + action="store_true", + help="For backward compatibility. Not used now...", + ) global logfile args = parser.parse_args() @@ -600,6 +609,21 @@ def main(): global documentation documentation = LaTeXMathicsMainDocumentation() + + if args.pymathics: + for module_name in args.pymathics.split(","): + try: + eval_LoadModule(module_name, definitions) + except PyMathicsLoadException: + print(f"Python module {module_name} is not a Mathics3 module.") + + except Exception as e: + print(f"Python import errors with: {e}.") + else: + print(f"Mathics3 Module {module_name} loaded") + + documentation.gather_doctest_data() + if args.sections: sections = set(args.sections.split(",")) test_sections( diff --git a/mathics/doc/latex/doc2latex.py b/mathics/doc/latex/doc2latex.py index 53baff243..38bcfa0e6 100755 --- a/mathics/doc/latex/doc2latex.py +++ b/mathics/doc/latex/doc2latex.py @@ -1,7 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -Reads in Pickle'd file and write LaTeX file containing the entire User Manual +"""Writes a LaTeX file containing the entire User Manual. + +The information for this comes from: + +* the docstrings from loading in Mathics3 core (mathics) + +* the docstrings from loading Mathics3 modules that have been specified + on the command line + +* doctest tests and test result that have been stored in a Python + Pickle file, from a privious docpipeline.py run. Ideally the + Mathics3 Modules given to docpipeline.py are the same as + given on the command line for this program """ import os @@ -10,6 +21,7 @@ import subprocess import sys from argparse import ArgumentParser +from typing import Dict, Optional from mpmath import __version__ as mpmathVersion from numpy import __version__ as NumPyVersion @@ -17,29 +29,57 @@ import mathics from mathics import __version__, settings, version_string +from mathics.core.definitions import Definitions from mathics.doc.latex_doc import LaTeXMathicsMainDocumentation, open_ensure_dir +from mathics.eval.pymathics import PyMathicsLoadException, eval_LoadModule # Global variables logfile = None +# Input doctest PCL FILE. This contains just the +# tests and test results. +# +# This information is stitched in with information comes from +# docstrings that are loaded from load Mathics builtins and external modules. + +DOCTEST_LATEX_DATA_PCL = settings.DOCTEST_LATEX_DATA_PCL + +# Output location information +DOC_LATEX_DIR = os.environ.get("DOC_LATEX_DIR", settings.DOC_LATEX_DIR) DOC_LATEX_FILE = os.environ.get("DOC_LATEX_FILE", settings.DOC_LATEX_FILE) DOC_LATEX_DIR = osp.dirname(DOC_LATEX_FILE) -def extract_doc_from_source(quiet=False): +def read_doctest_data(quiet=False) -> Optional[Dict[tuple, dict]]: """ - Write internal (pickled) TeX doc mdoc files and example data in docstrings. + Read doctest information from PCL file and return this. + This is a wrapper around laod_doctest_data(). """ if not quiet: - print(f"Extracting internal doc data for {version_string}") + print(f"Extracting internal doctest data for {version_string}") try: - return load_doc_data(settings.get_doc_tex_data_path(should_be_readable=True)) + return load_doctest_data( + settings.get_doctest_latex_data_path(should_be_readable=True) + ) except KeyboardInterrupt: print("\nAborted.\n") return -def load_doc_data(data_path, quiet=False): +def load_doctest_data(data_path, quiet=False) -> Dict[tuple, dict]: + """ + Read doctest information from PCL file and return this. + + The return value is a dictionary of test results. The key is a tuple + of: + * Part name, + * Chapter name, + * [Guide Section name], + * Section name, + * Subsection name, + * test number + and the value is a dictionary of a Result.getdata() dictionary. + """ if not quiet: print(f"Loading LaTeX internal data from {data_path}") with open_ensure_dir(data_path, "rb") as doc_data_fp: @@ -97,7 +137,7 @@ def write_latex( ) content = content.encode("utf-8") doc.write(content) - DOC_VERSION_FILE = osp.join(osp.dirname(DOC_LATEX_FILE), "version-info.tex") + DOC_VERSION_FILE = osp.join(DOC_LATEX_DIR, "version-info.tex") if not quiet: print(f"Writing Mathics Core Version Information to {DOC_VERSION_FILE}") with open(DOC_VERSION_FILE, "w") as doc: @@ -133,6 +173,15 @@ def main(): help="only test SECTION(s). " "You can list multiple chapters by adding a comma (and no space) in between chapter names.", ) + parser.add_argument( + "--load-module", + "-l", + dest="pymathics", + metavar="MATHIC3-MODULES", + help="load Mathics3 module MATHICS3-MODULES. " + "You can list multiple Mathics3 Modules by adding a comma (and no space) in between " + "module names.", + ) parser.add_argument( "--parts", "-p", @@ -149,9 +198,25 @@ def main(): help="Don't show formatting progress tests", ) args = parser.parse_args() - doc_data = extract_doc_from_source(quiet=args.quiet) + + # LoadModule Mathics3 modules to pull in modules, and + # their docstrings + if args.pymathics: + definitions = Definitions(add_builtin=True) + for module_name in args.pymathics.split(","): + try: + eval_LoadModule(module_name, definitions) + except PyMathicsLoadException: + print(f"Python module {module_name} is not a Mathics3 module.") + + except Exception as e: + print(f"Python import errors with: {e}.") + else: + print(f"Mathics3 Module {module_name} loaded") + + doctest_data = read_doctest_data(quiet=args.quiet) write_latex( - doc_data, + doctest_data, quiet=args.quiet, filter_parts=args.parts, filter_chapters=args.chapters, diff --git a/mathics/doc/latex/logo-heptatom.pdf b/mathics/doc/latex/logo-heptatom.pdf new file mode 100644 index 000000000..f5952d3a5 Binary files /dev/null and b/mathics/doc/latex/logo-heptatom.pdf differ diff --git a/mathics/doc/latex/logo-text-nodrop.pdf b/mathics/doc/latex/logo-text-nodrop.pdf new file mode 100644 index 000000000..cc7c99486 Binary files /dev/null and b/mathics/doc/latex/logo-text-nodrop.pdf differ diff --git a/mathics/doc/latex/mathics.tex b/mathics/doc/latex/mathics.tex index 4ee0f5c44..74d325e45 100644 --- a/mathics/doc/latex/mathics.tex +++ b/mathics/doc/latex/mathics.tex @@ -23,6 +23,7 @@ %\usepackage[utf8]{inputenc} \usepackage[T1]{fontenc} +\usepackage{gensymb} % For \degree. usepackage needs to be early. \usepackage{lmodern} \usepackage[english]{babel} \usepackage{makeidx} @@ -37,7 +38,6 @@ \usepackage{graphics} \usepackage{listings} \usepackage{paralist} -\usepackage{textcomp} \usepackage{mathpazo} \usepackage[mathpazo]{flexisym} \usepackage{breqn} @@ -282,8 +282,13 @@ \input{documentation.tex} +\part*{Appendices} \printindex \begin{colophon} +% Add this into table of contents +% The page reference may come out wrong and refer to the index. +% If so, adjust by hand, or diff file. +\addcontentsline{toc}{chapter}{Colophon} \begin{description} \item[Mathics3 Core] \hfill \\ \MathicsCoreVersion \item[Python] \hfill \\ \PythonVersion diff --git a/mathics/doc/latex/sed-hack.sh b/mathics/doc/latex/sed-hack.sh index ce034542a..358e8648e 100755 --- a/mathics/doc/latex/sed-hack.sh +++ b/mathics/doc/latex/sed-hack.sh @@ -1,6 +1,14 @@ #!/bin/bash set -x # Brute force convert Unicode characters in LaTeX that it can't handle +# Workaround for messages of the form: +# Missing character: There is no ⩵ ("2A75) in font pplr7t! +# Mathics3 MakeBox rules should handle this but they don't. + +# Characters that only work in math mode we convert back +# to their ASCII equivalent. Otherwise, since we don't +# understand context, it might not be right to +# use a math-mode designation. if [[ -f documentation.tex ]] ; then cp documentation.tex{,-before-sed} fi @@ -11,6 +19,27 @@ sed -i -e s/”/''/g documentation.tex sed -i -e s/″/''/g documentation.tex # sed -i -e s/\\′/'/g documentation.text #sed -i -e s/′/'/ documentation.tex +sed -i -e 's/–/--/g' documentation.tex + +# Greek +sed -i -e 's/Φ/$\\\\Phi$/g' documentation.tex +sed -i -e 's/ϕ/phi/g' documentation.tex sed -i -e s/μ/$\\\\mu$/g documentation.tex -sed -i -e s/–/--/g documentation.tex -sed -i -e s/Φ/$\\\\Phi$/g documentation.tex + +sed -i -e 's/⧴/:>/g' documentation.tex +sed -i -e 's/—/-/g' documentation.tex +sed -i -e 's/≤/<=/g' documentation.tex +sed -i -e 's/≠/!=/g' documentation.tex +sed -i -e 's/⩵/==/g' documentation.tex +sed -i -e 's/∧/&&/g' documentation.tex +sed -i -e 's/⧦/\\\\Equiv/g' documentation.tex +sed -i -e 's/⊻/xor/g' documentation.tex +sed -i -e 's/∧/&&/g' documentation.tex +sed -i -e 's/‖/||/g' documentation.tex +sed -i -e 's/→/->/g' documentation.tex + +# This kind of tick mark appears in latitude/longitude "minute" tick marks of ExampleData/PrimeMeridian.html +sed -i -e "s/′/'/g" documentation.tex + +# assumes LaTeX gensymb package +sed -i -e "s/°/\\\\degree{}/g" documentation.tex diff --git a/mathics/doc/latex_doc.py b/mathics/doc/latex_doc.py index 1e0e57632..039c9ab46 100644 --- a/mathics/doc/latex_doc.py +++ b/mathics/doc/latex_doc.py @@ -61,7 +61,7 @@ # We keep track of the number of \begin{asy}'s we see so that # we can assocation asymptote file numbers with where they are # in the document -asy_count = 0 +next_asy_number = 1 ITALIC_RE = re.compile(r"(?s)<(?Pi)>(?P.*?)") @@ -82,8 +82,18 @@ r"(?P.*?)\\end\{(?P=tag)\}" ) -LATEX_TESTOUT_DELIM_RE = re.compile(r",") -NUMBER_RE = re.compile(r"(\d*(? str: tag = match.group("tag") content = match.group("content") + # + # Sometimes it happens that the URL does not + # fit in 80 characters. Then, to avoid that + # flake8 complains, and also to have a + # nice and readable ASCII representation, + # we would like to split the URL in several, + # lines, having indentation spaces. + # + # The following line removes these extra + # characters, which would spoil the URL, + # producing a single line, space-free string. + # + content = content.replace(" ", "").replace("\n", "") if tag == "em": return r"\emph{%s}" % content elif tag == "url": @@ -369,13 +392,13 @@ def repl_text(match): return text def repl_out_delim(match): - return ",\\allowbreak{}" + return ",\\allowbreak{} " def repl_number(match): guard = r"\allowbreak{}" inter_groups_pre = r"\,\discretionary{\~{}}{\~{}}{}" inter_groups_post = r"\discretionary{\~{}}{\~{}}{}" - number = match.group(1) + number = match.group(1) + match.group(2) parts = number.split(".") if len(number) <= 3: return number @@ -553,7 +576,7 @@ def latex(self, doc_data: dict) -> str: The key for doc_data is the part/chapter/section{/subsection} test number and the value contains Result object data turned into a dictionary. - In partuclar, each test in the test sequence includes the, input test, + In particular, each test in the test sequence includes the, input test, the result produced and any additional error output. The LaTeX-formatted string fragment is returned. """ @@ -591,6 +614,7 @@ def latex(self, doc_data: dict) -> str: ) # Next output expression-result info. + print(result_dict) test_result_text = result_dict["result"] if test_result_text: # is not None and result['result'].strip(): @@ -598,9 +622,9 @@ def latex(self, doc_data: dict) -> str: # number of we have seen so far. And add that as a comment too # Each asymptote output has a file name with a number. if test_result_text.find("\\begin{asy}") >= 0: - global asy_count - asy_count += 1 - text += f" %% mathics-{asy_count}.asy\n" + global next_asy_number + next_asy_number += 1 + text += f" %% mathics-{next_asy_number}.asy\n" elif result_dict["form"] == "PNG": text += " \\includegraphics{%s}\n" % test_result_text else: @@ -692,13 +716,23 @@ def latex(self, doc_data: dict): class LaTeXMathicsMainDocumentation(MathicsMainDocumentation): - def __init__(self): + def __init__(self, want_sorting=False): + + self.doc_data_file = settings.get_doctest_latex_data_path( + should_be_readable=True + ) self.doc_dir = settings.DOC_DIR self.latex_file = settings.DOC_LATEX_FILE + self.doc_fn = LaTeXDoc + self.doc_guide_section_fn = LaTeXDocGuideSection + self.doc_chapter_fn = DocChapter + self.doc_part_fn = LaTeXDocPart + self.doc_section_fn = LaTeXDocSection + self.doc_subsection_fn = LaTeXDocSubsection + self.doctest_latex_pcl_path = settings.DOCTEST_LATEX_DATA_PCL self.parts = [] self.parts_by_slug = {} self.pymathics_doc_loaded = False - self.doc_data_file = settings.get_doc_tex_data_path(should_be_readable=True) self.title = "Overview" files = listdir(self.doc_dir) files.sort() @@ -758,12 +792,14 @@ def __init__(self): builtin_part = LaTeXDocPart(self, title, is_reference=start) modules_seen = set() - module_collection_fn = lambda x: sorted( - modules, - key=lambda module: module.sort_order - if hasattr(module, "sort_order") - else module.__name__, - ) + + def module_collection_fn(x): + return sorted( + modules, + key=lambda module: module.sort_order + if hasattr(module, "sort_order") + else module.__name__, + ) for module in module_collection_fn(modules): if skip_module_doc(module, modules_seen): @@ -792,12 +828,13 @@ def __init__(self): if isinstance(value, ModuleType) ] - sorted_submodule = lambda x: sorted( - submodules, - key=lambda submodule: submodule.sort_order - if hasattr(submodule, "sort_order") - else submodule.__name__, - ) + def sorted_submodule(tuple_submodules: tuple): + return sorted( + tuple_submodules, + key=lambda submodule: submodule.sort_order + if hasattr(submodule, "sort_order") + else submodule.__name__, + ) # Add sections in the guide section... for submodule in sorted_submodule(submodules): @@ -990,7 +1027,10 @@ def latex( if self.is_reference: chapter_fn = sorted_chapters else: - chapter_fn = lambda x: x + + def chapter_fn(x): + return x + result = "\n\n\\part{%s}\n\n" % escape_latex(self.title) + ( "\n".join( chapter.latex(doc_data, quiet, filter_sections=filter_sections) @@ -1197,6 +1237,7 @@ def __init__( operator=None, installed=True, in_guide=False, + summary_text="", ): """ Information that goes into a subsection object. This can be a written text, or diff --git a/mathics/docpipeline.py b/mathics/docpipeline.py index 4f2df1d82..82274c996 100644 --- a/mathics/docpipeline.py +++ b/mathics/docpipeline.py @@ -16,6 +16,7 @@ import sys from argparse import ArgumentParser from datetime import datetime +from typing import Dict import mathics import mathics.settings @@ -25,6 +26,7 @@ from mathics.core.evaluation import Evaluation, Output from mathics.core.parser import MathicsSingleLineFeeder from mathics.doc.common_doc import MathicsMainDocumentation +from mathics.eval.pymathics import PyMathicsLoadException, eval_LoadModule from mathics.timing import show_lru_cache_statistics builtins = builtins_dict() @@ -40,7 +42,7 @@ def max_stored_size(self, settings): # Global variables definitions = None documentation = None -check_partial_enlapsed_time = False +check_partial_elapsed_time = False logfile = None @@ -79,8 +81,10 @@ def compare(result, wanted) -> bool: stars = "*" * 10 -def test_case(test, tests, index=0, subindex=0, quiet=False, section=None) -> bool: - global check_partial_enlapsed_time +def test_case( + test, tests, index=0, subindex=0, quiet=False, section=None, format="text" +) -> bool: + global check_partial_elapsed_time test, wanted_out, wanted = test.test, test.outs, test.result def fail(why): @@ -101,11 +105,13 @@ def fail(why): print(f"{index:4d} ({subindex:2d}): TEST {test}".encode("utf-8")) feeder = MathicsSingleLineFeeder(test, "") - evaluation = Evaluation(definitions, catch_interrupt=False, output=TestOutput()) + evaluation = Evaluation( + definitions, catch_interrupt=False, output=TestOutput(), format=format + ) try: time_parsing = datetime.now() query = evaluation.parse_feeder(feeder) - if check_partial_enlapsed_time: + if check_partial_elapsed_time: print(" parsing took", datetime.now() - time_parsing) if query is None: # parsed expression is None @@ -113,7 +119,7 @@ def fail(why): out = evaluation.out else: result = evaluation.evaluate(query) - if check_partial_enlapsed_time: + if check_partial_elapsed_time: print(" evaluation took", datetime.now() - time_parsing) out = result.out result = result.result @@ -126,7 +132,7 @@ def fail(why): time_comparing = datetime.now() comparison_result = compare(result, wanted) - if check_partial_enlapsed_time: + if check_partial_elapsed_time: print(" comparison took ", datetime.now() - time_comparing) if not comparison_result: print("result =!=wanted") @@ -149,7 +155,7 @@ def fail(why): if not got == wanted and wanted.text != "...": output_ok = False break - if check_partial_enlapsed_time: + if check_partial_elapsed_time: print(" comparing messages took ", datetime.now() - time_comparing) if not output_ok: return fail( @@ -203,7 +209,7 @@ def test_tests( # FIXME: move this to common routine -def create_output(tests, doc_data, format="latex"): +def create_output(tests, doctest_data, format="latex"): definitions.reset_user_definitions() for test in tests.tests: if test.private: @@ -219,8 +225,11 @@ def create_output(tests, doc_data, format="latex"): if result is None: result = [] else: - result = [result.get_data()] - doc_data[key] = { + result_data = result.get_data() + result_data["form"] = format + result = [result_data] + + doctest_data[key] = { "query": test.test, "results": result, } @@ -233,12 +242,13 @@ def test_chapters( generate_output=False, reload=False, want_sorting=False, + keep_going=False, ): failed = 0 index = 0 chapter_names = ", ".join(chapters) print(f"Testing chapter(s): {chapter_names}") - output_data = load_doc_data() if reload else {} + output_data = load_doctest_data() if reload else {} prev_key = [] for tests in documentation.get_tests(): if tests.chapter in chapters: @@ -262,7 +272,8 @@ def test_chapters( if index == 0: print_and_log(f"No chapters found named {chapter_names}.") elif failed > 0: - print_and_log("%d test%s failed." % (failed, "s" if failed != 1 else "")) + if not (keep_going and format == "latex"): + print_and_log("%d test%s failed." % (failed, "s" if failed != 1 else "")) else: print_and_log("All tests passed.") @@ -274,14 +285,16 @@ def test_sections( generate_output=False, reload=False, want_sorting=False, + keep_going=False, ): failed = 0 index = 0 section_names = ", ".join(sections) print(f"Testing section(s): {section_names}") sections |= {"$" + s for s in sections} - output_data = load_doc_data() if reload else {} + output_data = load_doctest_data() if reload else {} prev_key = [] + format = "latex" if generate_output else "text" for tests in documentation.get_tests(): if tests.section in sections: for test in tests.tests: @@ -293,22 +306,23 @@ def test_sections( if test.ignore: continue index += 1 - if not test_case(test, tests, index, quiet=quiet): + if not test_case(test, tests, index, quiet=quiet, format=format): failed += 1 if stop_on_failure: break - if generate_output and failed == 0: - create_output(tests, output_data) + if generate_output and (failed == 0 or keep_going): + create_output(tests, output_data, format=format) print() if index == 0: print_and_log(f"No sections found named {section_names}.") elif failed > 0: - print_and_log("%d test%s failed." % (failed, "s" if failed != 1 else "")) + if not (keep_going and format == "latex"): + print_and_log("%d test%s failed." % (failed, "s" if failed != 1 else "")) else: print_and_log("All tests passed.") - if generate_output and (failed == 0): - save_doc_data(output_data) + if generate_output and (failed == 0 or keep_going): + save_doctest_data(output_data) def open_ensure_dir(f, *args, **kwargs): @@ -338,7 +352,7 @@ def test_all( if generate_output: if texdatafolder is None: texdatafolder = osp.dirname( - settings.get_doc_tex_data_path( + settings.get_doctest_latex_data_path( should_be_readable=False, create_parent=True ) ) @@ -392,7 +406,7 @@ def test_all( print_and_log(" - %s in %s / %s" % (section, part, chapter)) if generate_output and (failed == 0 or doc_even_if_error): - save_doc_data(output_data) + save_doctest_data(output_data) return True if failed == 0: @@ -402,32 +416,54 @@ def test_all( return sys.exit(1) # Travis-CI knows the tests have failed -def load_doc_data(): - doc_tex_data_path = settings.get_doc_tex_data_path(should_be_readable=True) - print(f"Loading internal document data from {doc_tex_data_path}") - with open_ensure_dir(doc_tex_data_path, "rb") as doc_data_file: - return pickle.load(doc_data_file) +def load_doctest_data() -> Dict[tuple, dict]: + """ + Load doctest tests and test results from Python PCL file. + + See ``save_doctest_data()`` for the format of the loaded PCL data + (a dict). + """ + doctest_latex_data_path = settings.get_doctest_latex_data_path( + should_be_readable=True + ) + print(f"Loading internal doctest data from {doctest_latex_data_path}") + with open_ensure_dir(doctest_latex_data_path, "rb") as doctest_data_file: + return pickle.load(doctest_data_file) -def save_doc_data(output_data): - doc_tex_data_path = settings.get_doc_tex_data_path( +def save_doctest_data(output_data: Dict[tuple, dict]): + """ + Save doctest tests and test results to a Python PCL file. + + ``output_data`` is a dictionary of test results. The key is a tuple + of: + * Part name, + * Chapter name, + * [Guide Section name], + * Section name, + * Subsection name, + * test number + and the value is a dictionary of a Result.getdata() dictionary. + """ + doctest_latex_data_path = settings.get_doctest_latex_data_path( should_be_readable=False, create_parent=True ) - print(f"Writing internal document data to {doc_tex_data_path}") - with open(settings.DOC_USER_TEX_DATA_PATH, "wb") as output_file: + print(f"Writing internal document data to {doctest_latex_data_path}") + with open(doctest_latex_data_path, "wb") as output_file: pickle.dump(output_data, output_file, 4) -def extract_doc_from_source(quiet=False, reload=False): +def write_doctest_data(quiet=False, reload=False): """ - Write internal (pickled) doc files and example data in docstrings. + Get doctest information, which involves running the tests to obtain + test results and write out both the tests and the test results. """ if not quiet: print(f"Extracting internal doc data for {version_string}") print("This may take a while...") try: - output_data = load_doc_data() if reload else {} + output_data = load_doctest_data() if reload else {} for tests in documentation.get_tests(): create_output(tests, output_data) except KeyboardInterrupt: @@ -435,13 +471,13 @@ def extract_doc_from_source(quiet=False, reload=False): return print("done.\n") - save_doc_data(output_data) + save_doctest_data(output_data) def main(): global definitions global logfile - global check_partial_enlapsed_time + global check_partial_elapsed_time definitions = Definitions(add_builtin=True) parser = ArgumentParser(description="Mathics test suite.", add_help=False) @@ -484,16 +520,18 @@ def main(): help="stores the output in [logfilename]. ", ) parser.add_argument( - "--pymathics", + "--load-module", "-l", dest="pymathics", - action="store_true", - help="also checks pymathics modules.", + metavar="MATHIC3-MODULES", + help="load Mathics3 module MATHICS3-MODULES. " + "You can list multiple Mathics3 Modules by adding a comma (and no space) in between " + "module names.", ) parser.add_argument( "--time-each", "-d", - dest="enlapsed_times", + dest="elapsed_times", action="store_true", help="check the time that take each test to parse, evaluate and compare.", ) @@ -552,11 +590,12 @@ def main(): action="store_true", help="print cache statistics", ) - # FIXME: there is some weird interacting going on with - # mathics when tests in sorted order. Some of the Plot - # show a noticeable 2 minute delay in processing. - # I think the problem is in Mathics itself rather than - # sorting, but until we figure that out, use + # FIXME: historically was weird interacting going on with + # mathics when tests in sorted order. Possibly a + # mpmath precsion reset bug. + # We see a noticeable 2 minute delay in processing. + # WHile the problem is in Mathics itself rather than + # sorting, until we get this fixed, use # sort as an option only. For normal testing we don't # want it for speed. But for document building which is # rarely done, we do want sorting of the sections and chapters. @@ -570,8 +609,8 @@ def main(): args = parser.parse_args() - if args.enlapsed_times: - check_partial_enlapsed_time = True + if args.elapsed_times: + check_partial_elapsed_time = True # If a test for a specific section is called # just test it if args.logfilename: @@ -579,32 +618,42 @@ def main(): global documentation documentation = MathicsMainDocumentation(want_sorting=args.want_sorting) + + # LoadModule Mathics3 modules + if args.pymathics: + for module_name in args.pymathics.split(","): + print("trying to load ", module_name) + try: + eval_LoadModule(module_name, definitions) + except PyMathicsLoadException: + print(f"Python module {module_name} is not a Mathics3 module.") + + except Exception as e: + print(f"Python import errors with: {e}.") + else: + print(f"Mathics3 Module {module_name} loaded") + + documentation.gather_doctest_data() + if args.sections: sections = set(args.sections.split(",")) - if args.pymathics: # in case the section is in a pymathics module... - documentation.load_pymathics_doc() test_sections( sections, stop_on_failure=args.stop_on_failure, generate_output=args.output, reload=args.reload, + keep_going=args.keep_going, ) elif args.chapters: chapters = set(args.chapters.split(",")) - if args.pymathics: # in case the section is in a pymathics module... - documentation.load_pymathics_doc() test_chapters( chapters, stop_on_failure=args.stop_on_failure, reload=args.reload ) else: - # if we want to check also the pymathics modules - if args.pymathics: - print("Building pymathics documentation object") - documentation.load_pymathics_doc() - elif args.doc_only: - extract_doc_from_source( + if args.doc_only: + write_doctest_data( quiet=args.quiet, reload=args.reload, ) diff --git a/mathics/eval/__init__.py b/mathics/eval/__init__.py index 5186dd92e..e66f87c70 100644 --- a/mathics/eval/__init__.py +++ b/mathics/eval/__init__.py @@ -1,8 +1,9 @@ """ Mathics Evaluation Functions -Routines here are core operations or functions that implement evaluation. If there -were an instruction interpreter, these would be the instructions. +Routines here are core operations or functions that implement +evaluation. If there were an instruction interpreter, these functions +that start "eval_" would be the interpreter instructions. These operatations then should include the most commonly-used Builtin-functions like ``N[]`` and routines in support of performing those evaluation operations/instructions. @@ -11,11 +12,12 @@ It may be even be that some of the functions here should be written in faster language like C, Cython, or Rust. -""" -# Ideally, this module should depend on modules inside ``mathics.core`` but not in modules stored in ``mathics.builtin`` to avoid circular references. +""" +# This module should not depend on ``mathics.builtin``. Dependence goes only the other way around # ``evaluation``, ``_rewrite_apply_eval_step``, ``set`` that in the current implementation # requires to introduce local imports. -# This also would make easier to test and profile classes that store Expression-like objects and methods that produce the evaluation. + +# Moving evaluation routines out of builtins allows us to test and profile code here. diff --git a/mathics/eval/arithmetic.py b/mathics/eval/arithmetic.py new file mode 100644 index 000000000..881eebf7d --- /dev/null +++ b/mathics/eval/arithmetic.py @@ -0,0 +1,320 @@ +from typing import Callable, Optional, Tuple + +import mpmath +import sympy + +from mathics.core.atoms import ( + Complex, + Integer, + Integer0, + Integer1, + Integer2, + IntegerM1, + Number, + Rational, + RationalOneHalf, + Real, +) +from mathics.core.convert.mpmath import from_mpmath +from mathics.core.convert.sympy import from_sympy +from mathics.core.element import BaseElement +from mathics.core.expression import Expression +from mathics.core.number import FP_MANTISA_BINARY_DIGITS, SpecialValueError, min_prec +from mathics.core.symbols import Symbol, SymbolPlus, SymbolPower, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolComplexInfinity, + SymbolDirectedInfinity, + SymbolIndeterminate, +) + + +# @lru_cache(maxsize=4096) +def call_mpmath( + mpmath_function: Callable, mpmath_args: tuple, prec: Optional[int] = None +): + """ + calls the mpmath_function with mpmath_args parms + if prec=None, use floating point arithmetic. + Otherwise, work with prec bits of precision. + """ + if prec is None: + prec = FP_MANTISA_BINARY_DIGITS + with mpmath.workprec(prec): + try: + result_mp = mpmath_function(*mpmath_args) + if prec != FP_MANTISA_BINARY_DIGITS: + return from_mpmath(result_mp, prec) + return from_mpmath(result_mp) + except ValueError as exc: + text = str(exc) + if text == "gamma function pole": + return SymbolComplexInfinity + else: + raise + except ZeroDivisionError: + return + except SpecialValueError as exc: + return Symbol(exc.name) + + +def eval_Abs(expr: BaseElement) -> Optional[BaseElement]: + """ + if expr is a number, return the absolute value. + """ + if isinstance(expr, (Integer, Rational, Real)): + if expr.value >= 0: + return expr + return eval_Times(IntegerM1, expr) + if isinstance(expr, Complex): + re, im = expr.real, expr.imag + sqabs = eval_Plus(eval_Times(re, re), eval_Times(im, im)) + return Expression(SymbolPower, sqabs, RationalOneHalf) + return None + + +def eval_Sign(expr: BaseElement) -> Optional[BaseElement]: + """ + if expr is a number, return its sign. + """ + if isinstance(expr, (Integer, Rational, Real)): + if expr.value > 0: + return Integer1 + elif expr.value == 0: + return Integer0 + else: + return IntegerM1 + + if isinstance(expr, Complex): + re, im = expr.real, expr.imag + sqabs = eval_Plus(eval_Times(re, re), eval_Times(im, im)) + return eval_Times( + expr, Expression(SymbolPower, sqabs, eval_Times(IntegerM1, RationalOneHalf)) + ) + return None + + +def eval_mpmath_function( + mpmath_function: Callable, *args: Tuple[Number], prec: Optional[int] = None +) -> Optional[Number]: + """ + Call the mpmath function `mpmath_function` with the arguments `args` + working with precision `prec`. If `prec` is `None`, work with machine + precision. + + Return a Mathics Number or None if the evaluation failed. + """ + if prec is None: + # if any argument has machine precision then the entire calculation + # is done with machine precision. + float_args = [arg.round().get_float_value(permit_complex=True) for arg in args] + if None in float_args: + return + + return call_mpmath(mpmath_function, tuple(float_args)) + else: + with mpmath.workprec(prec): + # to_mpmath seems to require that the precision is set from outside + mpmath_args = [x.to_mpmath() for x in args] + if None in mpmath_args: + return + return call_mpmath(mpmath_function, tuple(mpmath_args), prec) + + +def eval_Plus(*items: Tuple[BaseElement]) -> BaseElement: + "evaluate Plus for general elements" + elements = [] + last_item = last_count = None + + prec = min_prec(*items) + is_machine_precision = any(item.is_machine_precision() for item in items) + numbers = [] + + def append_last(): + if last_item is not None: + if last_count == 1: + elements.append(last_item) + else: + if last_item.has_form("Times", None): + elements.append( + Expression( + SymbolTimes, from_sympy(last_count), *last_item.elements + ) + ) + else: + elements.append( + Expression(SymbolTimes, from_sympy(last_count), last_item) + ) + + for item in items: + if isinstance(item, Number): + numbers.append(item) + else: + count = rest = None + if item.has_form("Times", None): + for element in item.elements: + if isinstance(element, Number): + count = element.to_sympy() + rest = item.get_mutable_elements() + rest.remove(element) + if len(rest) == 1: + rest = rest[0] + else: + rest.sort() + rest = Expression(SymbolTimes, *rest) + break + if count is None: + count = sympy.Integer(1) + rest = item + if last_item is not None and last_item == rest: + last_count = last_count + count + else: + append_last() + last_item = rest + last_count = count + append_last() + if numbers: + # TODO: reorganize de conditions to avoid compute unnecesary + # quantities. In particular, is we check mathine_precision, + # we do not need to evaluate prec. + if prec is not None: + if is_machine_precision: + numbers = [item.to_mpmath() for item in numbers] + number = mpmath.fsum(numbers) + number = from_mpmath(number) + else: + # TODO: If there are Complex numbers in `numbers`, + # and we are not working in machine precision, compute the sum of the real and imaginary + # parts separately, to preserve precision. For example, + # 1.`2 + 1.`3 I should produce + # Complex[1.`2, 1.`3] + # but with this implementation returns + # Complex[1.`2, 1.`2] + # + # TODO: if the precision are not equal for each number, + # we should estimate the result precision by computing the sum of individual errors + # prec = sum(abs(n.value) * 2**(-n.value._prec) for n in number if n.value._prec is not None)/sum(abs(n)) + with mpmath.workprec(prec): + numbers = [item.to_mpmath() for item in numbers] + number = mpmath.fsum(numbers) + number = from_mpmath(number, precision=prec) + else: + number = from_sympy(sum(item.to_sympy() for item in numbers)) + else: + number = Integer0 + + if not number.sameQ(Integer0): + elements.insert(0, number) + + if not elements: + return Integer0 + elif len(elements) == 1: + return elements[0] + else: + elements.sort() + return Expression(SymbolPlus, *elements) + + +def eval_Times(*items): + elements = [] + numbers = [] + # This variable tracks DirectInfinity[] factors. + infinity_factor = False + + # These quantities only have sense if there are numeric terms. + # Also, prec is only needed if is_machine_precision is not True. + prec = min_prec(*items) + is_machine_precision = any(item.is_machine_precision() for item in items) + + # find numbers and simplify Times -> Power + for item in items: + if isinstance(item, Number): + numbers.append(item) + continue + if item.get_head() is SymbolDirectedInfinity: + infinity_factor = True + if item is SymbolIndeterminate: + return item + if elements and item == elements[-1]: + elements[-1] = Expression(SymbolPower, elements[-1], Integer2) + elif ( + elements + and item.has_form("Power", 2) + and elements[-1].has_form("Power", 2) + and item.elements[0].sameQ(elements[-1].elements[0]) + ): + elements[-1] = Expression( + SymbolPower, + elements[-1].elements[0], + Expression(SymbolPlus, item.elements[1], elements[-1].elements[1]), + ) + elif ( + elements + and item.has_form("Power", 2) + and item.elements[0].sameQ(elements[-1]) + ): + elements[-1] = Expression( + SymbolPower, + elements[-1], + Expression(SymbolPlus, item.elements[1], Integer1), + ) + elif ( + elements + and elements[-1].has_form("Power", 2) + and elements[-1].elements[0].sameQ(item) + ): + elements[-1] = Expression( + SymbolPower, + item, + Expression(SymbolPlus, Integer1, elements[-1].elements[1]), + ) + else: + elements.append(item) + + if numbers: + if prec is not None: + if is_machine_precision: + numbers = [item.to_mpmath() for item in numbers] + number = mpmath.fprod(numbers) + number = from_mpmath(number) + else: + with mpmath.workprec(prec): + numbers = [item.to_mpmath() for item in numbers] + number = mpmath.fprod(numbers) + number = from_mpmath(number, precision=prec) + else: + number = sympy.Mul(*[item.to_sympy() for item in numbers]) + number = from_sympy(number) + else: + number = Integer1 + + if number.sameQ(Integer1): + number = None + elif number.is_zero: + if not infinity_factor: + return number + # else, they are handled using the DirectedInfinity upvalues rules. + elif number.sameQ(IntegerM1) and elements and elements[0].has_form("Plus", None): + elements[0] = Expression( + elements[0].get_head(), + *[ + Expression(SymbolTimes, IntegerM1, element) + for element in elements[0].elements + ], + ) + number = None + + if number is not None: + elements.insert(0, number) + + if not elements: + # if infinity_factor: + # return SymbolComplexInfinity + return Integer1 + + if len(elements) == 1: + ret = elements[0] + else: + ret = Expression(SymbolTimes, *elements) + # if infinity_factor: + # return Expression(SymbolDirectedInfinity, ret) + return ret diff --git a/mathics/eval/distance.py b/mathics/eval/distance.py new file mode 100644 index 000000000..34c041e7b --- /dev/null +++ b/mathics/eval/distance.py @@ -0,0 +1,43 @@ +""" +Distance-related evaluation functions and exception classes +""" +from mathics.core.atoms import Integer, Real + + +class IllegalDataPoint(Exception): + pass + + +class IllegalDistance(Exception): + def __init__(self, distance): + self.distance = distance + + +def dist_repr(p) -> tuple: + dist_p = repr_p = None + if p.has_form("Rule", 2): + if all(q.get_head_name() == "System`List" for q in p.elements): + dist_p, repr_p = (q.elements for q in p.elements) + elif ( + p.elements[0].get_head_name() == "System`List" + and p.elements[1].get_name() == "System`Automatic" + ): + dist_p = p.elements[0].elements + repr_p = [Integer(i + 1) for i in range(len(dist_p))] + elif p.get_head_name() == "System`List": + if all(q.get_head_name() == "System`Rule" for q in p.elements): + dist_p, repr_p = ([q.elements[i] for q in p.elements] for i in range(2)) + else: + dist_p = repr_p = p.elements + return dist_p, repr_p + + +def to_real_distance(d): + if not isinstance(d, (Real, Integer)): + raise IllegalDistance(d) + + mpd = d.to_mpmath() + if mpd is None or mpd < 0: + raise IllegalDistance(d) + + return mpd diff --git a/mathics/eval/image.py b/mathics/eval/image.py index 3c3d48f3e..18bb6dee6 100644 --- a/mathics/eval/image.py +++ b/mathics/eval/image.py @@ -4,8 +4,9 @@ helper functions for images """ +import functools from operator import itemgetter -from typing import List, Optional +from typing import List, Optional, Tuple, Union import numpy import PIL @@ -164,6 +165,51 @@ def get_image_size_spec(old_size, new_size) -> Optional[float]: return None +def image_pixels(matrix): + try: + pixels = numpy.array(matrix, dtype="float64") + except ValueError: # irregular array, e.g. {{0, 1}, {0, 1, 1}} + return None + shape = pixels.shape + if len(shape) == 2 or (len(shape) == 3 and shape[2] in (1, 3, 4)): + return pixels + else: + return None + + +def linearize_numpy_array(a: numpy.array) -> Tuple[numpy.array, int]: + """ + Transforms a numpy array numpy array and return the array and the number + of dimensions in the array + + A binary search is used. + """ + + orig_shape = a.shape + a = a.reshape((functools.reduce(lambda x, y: x * y, a.shape),)) # 1 dimension + + u = numpy.unique(a) + n = len(u) + + lower = numpy.ndarray(a.shape, dtype=int) + lower.fill(0) + upper = numpy.ndarray(a.shape, dtype=int) + upper.fill(n - 1) + + h = numpy.sort(u) + q = n # worst case partition size + + while q > 2: + m = numpy.right_shift(lower + upper, 1) + f = a <= h[m] + # (lower, m) vs (m + 1, upper) + lower = numpy.where(f, lower, m + 1) + upper = numpy.where(f, m, upper) + q = (q + 1) // 2 + + return numpy.where(a == h[lower], lower, upper).reshape(orig_shape), n + + def matrix_to_numpy(a): def matrix(): for y in a.elements: @@ -185,7 +231,7 @@ def numpy_to_matrix(pixels): return pixels.tolist() -def pixels_as_float(pixels): +def pixels_as_float(pixels) -> Union[numpy.float64, numpy.float32]: dtype = pixels.dtype if dtype in (numpy.float32, numpy.float64): return pixels @@ -199,7 +245,7 @@ def pixels_as_float(pixels): raise NotImplementedError -def pixels_as_ubyte(pixels): +def pixels_as_ubyte(pixels) -> numpy.uint8: dtype = pixels.dtype if dtype in (numpy.float32, numpy.float64): pixels = numpy.maximum(numpy.minimum(pixels, 1.0), 0.0) @@ -238,13 +284,15 @@ def resize_width_height( from mathics.builtin.image.base import Image if resampling_name not in resampling_names2PIL.keys(): - return evaluation.message("ImageResize", "imgrsm", resampling_name) + evaluation.message("ImageResize", "imgrsm", resampling_name) + return resample = resampling_names2PIL[resampling_name] # perform the resize if hasattr(image, "pillow"): if resampling_name not in resampling_names2PIL.keys(): - return evaluation.message("ImageResize", "imgrsm", resampling_name) + evaluation.message("ImageResize", "imgrsm", resampling_name) + return pillow = image.pillow.resize(size=(width, height), resample=resample) pixels = numpy.asarray(pillow) return Image(pixels, image.color_space, pillow=pillow) @@ -273,7 +321,8 @@ def resize_width_height( # s = sx # if err > 1.5: # # TODO overcome this limitation - # return evaluation.message("ImageResize", "gaussaspect") + # evaluation.message("ImageResize", "gaussaspect") + # return # elif s > 1: # pixels = transform.pyramid_expand( # image.pixels, upscale=s, multichannel=multichannel diff --git a/mathics/eval/lists.py b/mathics/eval/lists.py new file mode 100644 index 000000000..130d9ffb1 --- /dev/null +++ b/mathics/eval/lists.py @@ -0,0 +1,91 @@ +from mathics.builtin.box.layout import RowBox +from mathics.core.atoms import String +from mathics.core.convert.expression import to_expression +from mathics.core.exceptions import PartDepthError, PartRangeError +from mathics.core.expression import Expression +from mathics.core.symbols import Atom +from mathics.core.systemsymbols import SymbolMakeBoxes, SymbolSequence + + +def delete_one(expr, pos): + if isinstance(expr, Atom): + raise PartDepthError(pos) + elements = expr.elements + if pos == 0: + return Expression(SymbolSequence, *elements) + s = len(elements) + truepos = pos + if truepos < 0: + truepos = s + truepos + else: + truepos = truepos - 1 + if truepos < 0 or truepos >= s: + raise PartRangeError + elements = ( + elements[:truepos] + + (to_expression("System`Sequence"),) + + elements[truepos + 1 :] + ) + return to_expression(expr.get_head(), *elements) + + +def delete_rec(expr, pos): + if len(pos) == 1: + return delete_one(expr, pos[0]) + truepos = pos[0] + if truepos == 0 or isinstance(expr, Atom): + raise PartDepthError(pos[0]) + elements = expr.elements + s = len(elements) + if truepos < 0: + truepos = truepos + s + if truepos < 0: + raise PartRangeError + newelement = delete_rec(elements[truepos], pos[1:]) + elements = elements[:truepos] + (newelement,) + elements[truepos + 1 :] + else: + if truepos > s: + raise PartRangeError + newelement = delete_rec(elements[truepos - 1], pos[1:]) + elements = elements[: truepos - 1] + (newelement,) + elements[truepos:] + return Expression(expr.get_head(), *elements) + + +def get_tuples(items): + if not items: + yield [] + else: + for item in items[0]: + for rest in get_tuples(items[1:]): + yield [item] + rest + + +def list_boxes(items, f, evaluation, open=None, close=None): + result = [ + Expression(SymbolMakeBoxes, item, f).evaluate(evaluation) for item in items + ] + if f.get_name() in ("System`OutputForm", "System`InputForm"): + sep = ", " + else: + sep = "," + result = riffle(result, String(sep)) + if len(items) > 1: + result = RowBox(*result) + elif items: + result = result[0] + if result: + result = [result] + else: + result = [] + if open is not None and close is not None: + return [String(open)] + result + [String(close)] + else: + return result + + +def riffle(items, sep): + result = items[:1] + for item in items[1:]: + result.append(sep) + result.append(item) + return result diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 7c51be55f..52665c5e0 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -7,9 +7,9 @@ import typing -from typing import Any +from typing import Any, Dict, Type -from mathics.core.atoms import Complex, Integer, Rational, String, SymbolI +from mathics.core.atoms import Complex, Integer, Rational, Real, String, SymbolI from mathics.core.convert.expression import to_expression_with_specialization from mathics.core.definitions import OutputForms from mathics.core.element import BaseElement, BoxElementMixin, EvalMixin @@ -38,9 +38,27 @@ SymbolMinus, SymbolOutputForm, SymbolRational, + SymbolRowBox, SymbolStandardForm, ) +# An operator precedence value that will ensure that whatever operator +# this is attached to does not have parenthesis surrounding it. +# Operator precedence values are integers; If if an operator +# "op" is greater than the surrounding precedence, then "op" +# will be surrounded by parenthesis, e.g. ... (...op...) ... +# In named-characters.yml of mathics-scanner we start at 0. +# However, negative values would also work. +NEVER_ADD_PARENTHESIS = 0 + +# These Strings are used in Boxing output +StringElipsis = String("...") +StringLParen = String("(") +StringRParen = String(")") +StringRepeated = String("..") + +builtins_precedence: Dict[Symbol, int] = {} + element_formatters = {} @@ -51,18 +69,36 @@ def _boxed_string(string: str, **options): return StyleBox(String(string), **options) -def eval_makeboxes(self, expr, evaluation, f=SymbolStandardForm): +def eval_fullform_makeboxes( + self, expr, evaluation: Evaluation, form=SymbolStandardForm +) -> Expression: """ - This function takes the definitions prodived by the evaluation + This function takes the definitions provided by the evaluation object, and produces a boxed form for expr. + + Basically: MakeBoxes[expr // FullForm] + """ + # This is going to be reimplemented. + expr = Expression(SymbolFullForm, expr) + return Expression(SymbolMakeBoxes, expr, form).evaluate(evaluation) + + +def eval_makeboxes( + self, expr, evaluation: Evaluation, form=SymbolStandardForm +) -> Expression: + """ + This function takes the definitions provided by the evaluation + object, and produces a boxed fullform for expr. + + Basically: MakeBoxes[expr // form] """ # This is going to be reimplemented. - return Expression(SymbolMakeBoxes, expr, f).evaluate(evaluation) + return Expression(SymbolMakeBoxes, expr, form).evaluate(evaluation) def format_element( element: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs -) -> BaseElement: +) -> Type[BaseElement]: """ Applies formats associated to the expression, and then calls Makeboxes """ @@ -82,14 +118,14 @@ def format_element( def do_format( element: BaseElement, evaluation: Evaluation, form: Symbol -) -> BaseElement: +) -> Type[BaseElement]: do_format_method = element_formatters.get(type(element), do_format_element) return do_format_method(element, evaluation, form) def do_format_element( element: BaseElement, evaluation: Evaluation, form: Symbol -) -> BaseElement: +) -> Type[BaseElement]: """ Applies formats associated to the expression and removes superfluous enclosing formats. @@ -106,7 +142,7 @@ def do_format_element( # removes the format from the expression. if head in OutputForms and len(expr.elements) == 1: expr = elements[0] - if not (form is SymbolOutputForm and head is SymbolStandardForm): + if not form.sameQ(head): form = head include_form = True @@ -124,7 +160,7 @@ def do_format_element( Expression( SymbolPostfix, ListExpression(elements[0]), - String(".."), + StringRepeated, Integer(170), ), ) @@ -137,7 +173,7 @@ def do_format_element( Expression( SymbolPostfix, Expression(SymbolList, elements[0]), - String("..."), + StringElipsis, Integer(170), ), ) @@ -207,7 +243,7 @@ def format_expr(expr): def do_format_rational( element: BaseElement, evaluation: Evaluation, form: Symbol -) -> BaseElement: +) -> Type[BaseElement]: if form is SymbolFullForm: return do_format_expression( Expression( @@ -232,7 +268,7 @@ def do_format_rational( def do_format_complex( element: BaseElement, evaluation: Evaluation, form: Symbol -) -> BaseElement: +) -> Type[BaseElement]: if form is SymbolFullForm: return do_format_expression( Expression( @@ -260,7 +296,7 @@ def do_format_complex( def do_format_expression( element: BaseElement, evaluation: Evaluation, form: Symbol -) -> BaseElement: +) -> Type[BaseElement]: # # not sure how much useful is this format_cache # if element._format_cache is None: # element._format_cache = {} @@ -279,6 +315,41 @@ def do_format_expression( return expr +def parenthesize( + precedence: int, element: Type[BaseElement], element_boxes, when_equal: bool +) -> Type[Expression]: + """ + "Determines if ``element_boxes`` needs to be surrounded with parenthesis. + This is done based on ``precedence`` and the computed preceence of + ``element``. The adjusted ListExpression is returned. + + If when_equal is True, parentheses will be added if the two + precedence values are equal. + """ + while element.has_form("HoldForm", 1): + element = element.elements[0] + + if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): + element_prec = element.elements[2].value + elif element.has_form("PrecedenceForm", 2): + element_prec = element.elements[1].value + # If "element" is a negative number, we need to parenthesize the number. (Fixes #332) + elif isinstance(element, (Integer, Real)) and element.value < 0: + # Force parenthesis by adjusting the surrounding context's precedence value, + # We can't change the precedence for the number since it, doesn't + # have a precedence value. + element_prec = precedence + else: + element_prec = builtins_precedence.get(element.get_head()) + if precedence is not None and element_prec is not None: + if precedence > element_prec or (precedence == element_prec and when_equal): + return Expression( + SymbolRowBox, + ListExpression(StringLParen, element_boxes, StringRParen), + ) + return element_boxes + + element_formatters[Rational] = do_format_rational element_formatters[Complex] = do_format_complex element_formatters[Expression] = do_format_expression diff --git a/mathics/eval/math_ops.py b/mathics/eval/math_ops.py index 7925fe7d6..17b7dbbc8 100644 --- a/mathics/eval/math_ops.py +++ b/mathics/eval/math_ops.py @@ -7,22 +7,23 @@ from mathics.core.symbols import Symbol -def eval_2_Norm(m: Expression, evaluation: Evaluation) -> Optional[Expression]: +def eval_Norm(m: Expression, evaluation: Evaluation) -> Optional[Expression]: """ - 2-Norm[] evaluation function + Norm[m] evaluation function - the 2-norm of matrix m """ sympy_m = to_sympy_matrix(m) if sympy_m is None: - return evaluation.message("Norm", "nvm") + evaluation.message("Norm", "nvm") + return return from_sympy(sympy_m.norm()) -def eval_p_norm( +def eval_Norm_p( m: Expression, p: Expression, evaluation: Evaluation ) -> Optional[Expression]: """ - p2-Norm[] evaluation function + Norm[m, p] evaluation function - the p-norm of matrix m. """ if isinstance(p, Symbol): sympy_p = p.to_sympy() @@ -33,20 +34,23 @@ def eval_p_norm( elif isinstance(p, (Real, Integer)) and p.to_python() >= 1: sympy_p = p.to_sympy() else: - return evaluation.message("Norm", "ptype", p) + evaluation.message("Norm", "ptype", p) + return if sympy_p is None: return matrix = to_sympy_matrix(m) if matrix is None: - return evaluation.message("Norm", "nvm") + evaluation.message("Norm", "nvm") + return if len(matrix) == 0: return try: res = matrix.norm(sympy_p) except NotImplementedError: - return evaluation.message("Norm", "normnotimplemented") + evaluation.message("Norm", "normnotimplemented") + return return from_sympy(res) diff --git a/mathics/eval/nevaluator.py b/mathics/eval/nevaluator.py index 845d42029..17e4f8d39 100644 --- a/mathics/eval/nevaluator.py +++ b/mathics/eval/nevaluator.py @@ -34,7 +34,7 @@ def eval_N( Equivalent to Expression(SymbolN, expression).evaluate(evaluation) """ evaluated_expression = expression.evaluate(evaluation) - result = eval_nvalues(evaluated_expression, prec, evaluation) + result = eval_NValues(evaluated_expression, prec, evaluation) if result is None: return expression if isinstance(result, Number): @@ -42,15 +42,14 @@ def eval_N( return result.evaluate(evaluation) -def eval_nvalues( +def eval_NValues( expr: BaseElement, prec: BaseElement, evaluation: Evaluation ) -> Optional[BaseElement]: """ - Looks for the numeric value of ```expr`` with precision ``prec`` by appling NValues rules + Looks for the numeric value of ```expr`` with precision ``prec`` by applying NValues rules stored in ``evaluation.definitions``. - If `prec` can not be evaluated as a number, returns None, otherwise, returns an expression. + If ``prec`` can not be evaluated as a number, returns None, otherwise, returns an expression. """ - # The first step is to determine the precision goal try: # Here ``get_precision`` is called with ``show_messages`` @@ -67,14 +66,14 @@ def eval_nvalues( # If expr is a List, or a Rule (or maybe expressions with heads for # which we are sure do not have NValues or special attributes) - # just apply `eval_nvalues` to each element and return the new list. + # just apply `eval_NValues` to each element and return the new list. if expr.get_head_name() in ("System`List", "System`Rule"): elements = expr.elements # FIXME: incorporate these lines into Expression call result = Expression(expr.head) new_elements = [ - eval_nvalues(element, prec, evaluation) for element in expr.elements + eval_NValues(element, prec, evaluation) for element in expr.elements ] result.elements = tuple( new_element if new_element else element @@ -91,7 +90,7 @@ def eval_nvalues( # Here we look for the NValues associated to the # lookup_name of the expression. # If a rule is found and successfuly applied, - # reevaluate the result and apply `eval_nvalues` again. + # reevaluate the result and apply `eval_NValues` again. # This should be implemented as a loop instead of # recursively. name = expr.get_lookup_name() @@ -103,7 +102,7 @@ def eval_nvalues( if result is not None: if not result.sameQ(nexpr): result = result.evaluate(evaluation) - result = eval_nvalues(result, prec, evaluation) + result = eval_NValues(result, prec, evaluation) return result # If we are here, is because there are not NValues that matches @@ -113,7 +112,7 @@ def eval_nvalues( return expr else: # Otherwise, look at the attributes, determine over which elements - # we need to apply `eval_nvalues`, and rebuild the expression with + # we need to apply `eval_NValues`, and rebuild the expression with # the results. attributes = expr.head.get_attributes(evaluation.definitions) head = expr.head @@ -130,11 +129,11 @@ def eval_nvalues( else: eval_range = range(len(elements)) - newhead = eval_nvalues(head, prec, evaluation) + newhead = eval_NValues(head, prec, evaluation) head = head if newhead is None else newhead for index in eval_range: - new_element = eval_nvalues(elements[index], prec, evaluation) + new_element = eval_NValues(elements[index], prec, evaluation) if new_element: elements[index] = new_element diff --git a/mathics/eval/numbers.py b/mathics/eval/numbers.py new file mode 100644 index 000000000..7389ac8d3 --- /dev/null +++ b/mathics/eval/numbers.py @@ -0,0 +1,140 @@ +from typing import Optional + +import mpmath +import sympy + +from mathics.core.atoms import Complex, MachineReal, PrecisionReal +from mathics.core.convert.sympy import from_sympy +from mathics.core.element import BaseElement +from mathics.core.expression import Expression +from mathics.core.number import MACHINE_PRECISION_VALUE, ZERO_MACHINE_ACCURACY, dps +from mathics.core.symbols import SymbolPlus + + +def eval_Accuracy(z: BaseElement) -> Optional[float]: + """ + Determine the accuracy of an expression expr. + If z is a Real value, returns a Python float value + representing the difference between the number of + significant decimal figures (Precision) and log_10(z). + + For example, + ``` + 12.345`2 + ``` + which is equivalent to 12.`2 has an accuracy of: + ``` + 0.908509 == 2. - log(10, 12.345) + ``` + + If the expression contains Real values, returns + the minimal accuracy of all the numbers in the expression. + + Otherwise returns None, representing infinite accuracy. + """ + if isinstance(z, MachineReal): + if z.is_zero: + return ZERO_MACHINE_ACCURACY + z_f = z.to_python() + log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10) + return MACHINE_PRECISION_VALUE - log10_z + + if isinstance(z, PrecisionReal): + if z.is_zero: + return float(dps(z.get_precision())) + z_f = z.to_python() + log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10) + return dps(z.get_precision()) - log10_z + + if isinstance(z, Complex): + acc_real = eval_Accuracy(z.real) + acc_imag = eval_Accuracy(z.imag) + if acc_real is None: + return acc_imag + if acc_imag is None: + return acc_real + + return -mpmath.log(10 ** (-2 * acc_real) + 10 ** (-2 * acc_imag), 10.0) * 0.5 + + if isinstance(z, Expression): + elem_accuracies = (eval_Accuracy(z_elem) for z_elem in z.elements) + return min((acc for acc in elem_accuracies if acc is not None), default=None) + return None + + +def eval_Precision(z: BaseElement) -> Optional[float]: + """ + Determine the precision of an expression expr. + If z is a Real value, returns the number of significant + decimal figures of z. For example, + ``` + 12.345`2 + ``` + which is equivalent to 12.`2 has a precision of 2. + + If the expression contains Real values, returns + the minimal accuracy of all the numbers in the expression. + + If z is PrecisionReal(0.), the precision is 0. In that case, + the field "precision" is interpreted as "accuracy". + + Otherwise returns None, representing infinite precision. + """ + + if isinstance(z, MachineReal): + return MACHINE_PRECISION_VALUE + + if isinstance(z, PrecisionReal): + if z.is_zero: + return 0.0 + return float(dps(z.get_precision())) + + if isinstance(z, Complex): + prec_real = eval_Precision(z.real) + prec_imag = eval_Precision(z.imag) + if prec_real is None or prec_imag == prec_real: + return prec_imag + if prec_imag is None: + return prec_real + # both numbers have different precision. + # Evaluate the accuracy and add the log of + # the module. + acc = eval_Accuracy(z) + abs_sq = z.real.value**2 + z.imag.value**2 + return acc + mpmath.log(abs_sq, 10.0) * 0.5 + + if isinstance(z, Expression): + elem_prec = (eval_Precision(z_elem) for z_elem in z.elements) + return min((prec for prec in elem_prec if prec is not None), default=None) + + return None + + +def cancel(expr): + if expr.has_form("Plus", None): + return Expression(SymbolPlus, *[cancel(element) for element in expr.elements]) + else: + try: + result = expr.to_sympy() + if result is None: + return None + + # result = sympy.powsimp(result, deep=True) + result = sympy.cancel(result) + + # cancel factors out rationals, so we factor them again + result = sympy_factor(result) + + return from_sympy(result) + except sympy.PolynomialError: + # e.g. for non-commutative expressions + return expr + + +def sympy_factor(expr_sympy): + try: + result = sympy.together(expr_sympy) + result = sympy.factor(result) + except sympy.PolynomialError: + return expr_sympy + return result diff --git a/mathics/algorithm/parts.py b/mathics/eval/parts.py similarity index 94% rename from mathics/algorithm/parts.py rename to mathics/eval/parts.py index 0ea7fefd7..3ecf356d1 100644 --- a/mathics/algorithm/parts.py +++ b/mathics/eval/parts.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -Algorithms to access and manipulate elements in nested lists / expressions +Evaluation methods for accessing and manipulating elements in nested lists / expressions """ from typing import List @@ -16,16 +16,20 @@ PartRangeError, ) from mathics.core.expression import Expression +from mathics.core.expression_predefined import MATHICS3_INFINITY from mathics.core.list import ListExpression from mathics.core.subexpression import SubExpression from mathics.core.symbols import Atom, Symbol, SymbolList -from mathics.core.systemsymbols import SymbolDirectedInfinity, SymbolInfinity - -SymbolNothing = Symbol("Nothing") +from mathics.core.systemsymbols import ( + SymbolDirectedInfinity, + SymbolInfinity, + SymbolNothing, +) +from mathics.eval.patterns import Matcher def get_part(expression: BaseElement, indices: List[int]) -> BaseElement: - """Extract part of ``expression`` specified by ``indicies`` and + """Extract part of ``expression`` specified by ``indices`` and return that. """ @@ -274,7 +278,7 @@ def _list_parts(exprs, selectors, evaluation): yield unwrap(picked) -def _parts(expr, selectors, evaluation): +def parts(expr, selectors, evaluation) -> list: """ Select from the `Expression` expr those elements indicated by the `selectors`. @@ -313,7 +317,7 @@ def walk_parts(list_of_list, indices, evaluation, assign_rhs=None): return result else: try: - result = _parts(walk_list, _part_selectors(indices), evaluation) + result = parts(walk_list, _part_selectors(indices), evaluation) except MessageException as e: e.message(evaluation) return False @@ -391,7 +395,7 @@ def python_levelspec(levelspec): def value_to_level(expr): value = expr.get_int_value() if value is None: - if expr == Expression(SymbolDirectedInfinity, Integer1): + if expr.sameQ(MATHICS3_INFINITY): return None else: raise InvalidLevelspecError @@ -549,13 +553,16 @@ def deletecases_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): """ This function walks the expression `expr` and deleting occurrencies of `pattern` - If levelspec specifies a number, only those positions with `levelspec` "coordinates" are return. By default, it just return occurences in the first level. + If levelspec specifies a number, only those positions with + `levelspec` "coordinates" are return. By default, it just return + occurrences in the first level. - If a tuple (nmin, nmax) is provided, it just return those occurences with a number of "coordinates" between nmin and nmax. - n indicates the number of occurrences to return. By default, it returns all the occurences. + If a tuple (nmin, nmax) is provided, it just return those + occurrences with a number of "coordinates" between nmin and nmax. + n indicates the number of occurrences to return. By default, it + returns all the occurrences. """ nothing = SymbolNothing - from mathics.builtin.patterns import Matcher match = Matcher(pattern) match = match.match @@ -617,12 +624,16 @@ def deletecases_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): def find_matching_indices_with_levelspec(expr, pattern, evaluation, levelspec=1, n=-1): """ This function walks the expression `expr` looking for a pattern `pattern` - and returns the positions of each occurence. + and returns the positions of each occurrence. - If levelspec specifies a number, only those positions with `levelspec` "coordinates" are return. By default, it just return occurences in the first level. + If levelspec specifies a number, only those positions with + `levelspec` "coordinates" are return. By default, it just return + occurrences in the first level. - If a tuple (nmin, nmax) is provided, it just return those occurences with a number of "coordinates" between nmin and nmax. - n indicates the number of occurrences to return. By default, it returns all the occurences. + If a tuple (nmin, nmax) is provided, it just return those + occurrences with a number of "coordinates" between nmin and nmax. + n indicates the number of occurrences to return. By default, it + returns all the occurrences. """ from mathics.builtin.patterns import Matcher diff --git a/mathics/eval/patterns.py b/mathics/eval/patterns.py new file mode 100644 index 000000000..8e975b634 --- /dev/null +++ b/mathics/eval/patterns.py @@ -0,0 +1,28 @@ +from mathics.core.evaluation import Evaluation +from mathics.core.pattern import Pattern, StopGenerator + + +class _StopGeneratorMatchQ(StopGenerator): + pass + + +class Matcher: + def __init__(self, form): + if isinstance(form, Pattern): + self.form = form + else: + self.form = Pattern.create(form) + + def match(self, expr, evaluation: Evaluation): + def yield_func(vars, rest): + raise _StopGeneratorMatchQ(True) + + try: + self.form.match(yield_func, expr, {}, evaluation) + except _StopGeneratorMatchQ: + return True + return False + + +def match(expr, form, evaluation: Evaluation): + return Matcher(form).match(expr, evaluation) diff --git a/mathics/eval/plot.py b/mathics/eval/plot.py index 32467626f..691616bbc 100644 --- a/mathics/eval/plot.py +++ b/mathics/eval/plot.py @@ -12,24 +12,22 @@ from mathics.builtin.numeric import chop from mathics.builtin.options import options_to_rules from mathics.builtin.scoping import dynamic_scoping -from mathics.core.atoms import Integer, Integer0, Real, String +from mathics.core.atoms import Integer, Integer0, Real from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.symbols import SymbolN, SymbolPower, SymbolTrue +from mathics.core.symbols import SymbolN, SymbolTrue from mathics.core.systemsymbols import ( SymbolGraphics, SymbolHue, SymbolLine, SymbolLog10, SymbolLogPlot, - SymbolMessageName, SymbolPoint, SymbolPolygon, - SymbolQuiet, ) RealPoint6 = Real(0.6) @@ -102,15 +100,10 @@ def quiet_f(*args): return quiet_f expr: Optional[Type[BaseElement]] = Expression(SymbolN, expr).evaluate(evaluation) - quiet_expr = Expression( - SymbolQuiet, - expr, - ListExpression(Expression(SymbolMessageName, SymbolPower, String("infy"))), - ) def quiet_f(*args): vars = {arg_name: Real(arg) for arg_name, arg in zip(arg_names, args)} - value = dynamic_scoping(quiet_expr.evaluate, vars, evaluation) + value = dynamic_scoping(expr.evaluate, vars, evaluation) if list_is_expected: if value.has_form("List", None): value = [extract_pyreal(item) for item in value.elements] @@ -391,6 +384,7 @@ def get_points_range(points): # like the Hue. graphics = [] + prev_quiet_all, evaluation.quiet_all = evaluation.quiet_all, True for index, f in enumerate(functions): points = [] xvalues = [] # x value for each point in points @@ -561,6 +555,8 @@ def find_excl(excl): mesh_points = [to_mathics_list(xx, yy) for xx, yy in points] graphics.append(Expression(SymbolPoint, ListExpression(*mesh_points))) + # Restore the quiet_all state + evaluation.quiet_all = prev_quiet_all return Expression( SymbolGraphics, ListExpression(*graphics), *options_to_rules(options) ) diff --git a/mathics/core/pymathics.py b/mathics/eval/pymathics.py similarity index 52% rename from mathics/core/pymathics.py rename to mathics/eval/pymathics.py index 213d47d4a..6cc920238 100644 --- a/mathics/core/pymathics.py +++ b/mathics/eval/pymathics.py @@ -1,15 +1,20 @@ -# -*- coding: utf-8 -*- """ -Pymathics module handling +PyMathics3 module handling """ import importlib +import inspect import sys -from mathics.core.evaluation import Evaluation +from mathics.builtin import name_is_builtin_symbol +from mathics.builtin.base import Builtin +from mathics.core.definitions import Definitions -# This dict probably does not belong here. -pymathics = {} +# The below set and dictionary are used in document generation +# for Pymathics modules. +# The are similar to "builtin_by_module" and "builtin_modules" of mathics.builtins. +pymathics_modules = set() +pymathics_builtins_by_module = {} class PyMathicsLoadException(Exception): @@ -18,24 +23,9 @@ def __init__(self, module): self.module = module -# Why do we need this? -def eval_clear_pymathics_modules(): - global pymathics - from mathics.builtin import builtins_by_module - - for key in list(builtins_by_module.keys()): - if not key.startswith("mathics."): - del builtins_by_module[key] - for key in pymathics: - del pymathics[key] - - pymathics = {} - return None - - -def eval_load_module(module_name: str, evaluation: Evaluation) -> str: +def eval_LoadModule(module_name: str, definitions: Definitions) -> str: try: - load_pymathics_module(evaluation.definitions, module_name) + load_pymathics_module(definitions, module_name) except (PyMathicsLoadException, ImportError): raise else: @@ -46,24 +36,24 @@ def eval_load_module(module_name: str, evaluation: Evaluation) -> str: # reference manual where PackletManager appears first in # the list, it seems to be preferable to add this PyMathics # at the beginning. - context_path = list(evaluation.definitions.get_context_path()) + context_path = list(definitions.get_context_path()) if "Pymathics`" not in context_path: context_path.insert(0, "Pymathics`") - evaluation.definitions.set_context_path(context_path) + definitions.set_context_path(context_path) return module_name -def load_pymathics_module(definitions, module): +def load_pymathics_module(definitions, module_name: str): """ Loads Mathics builtin objects and their definitions from an external Python module in the pymathics module namespace. """ from mathics.builtin import Builtin, builtins_by_module, name_is_builtin_symbol - if module in sys.modules: - loaded_module = importlib.reload(sys.modules[module]) + if module_name in sys.modules: + loaded_module = importlib.reload(sys.modules[module_name]) else: - loaded_module = importlib.import_module(module) + loaded_module = importlib.import_module(module_name) builtins_by_module[loaded_module.__name__] = [] vars = set( @@ -74,10 +64,10 @@ def load_pymathics_module(definitions, module): newsymbols = {} if not ("pymathics_version_data" in vars): - raise PyMathicsLoadException(module) + raise PyMathicsLoadException(module_name) for name in vars - set(("pymathics_version_data", "__version__")): var = name_is_builtin_symbol(loaded_module, name) - if name_is_builtin_symbol: + if var is not None: instance = var(expression=False) if isinstance(instance, Builtin): if not var.context: @@ -97,4 +87,31 @@ def load_pymathics_module(definitions, module): if onload: onload(definitions) + update_pymathics(loaded_module) + pymathics_modules.add(loaded_module) return loaded_module + + +def update_pymathics(module): + """ + Update variables used in documentation to include Pymathics + """ + module_vars = dir(module) + + for name in module_vars: + builtin_class = name_is_builtin_symbol(module, name) + module_name = module.__name__ + + # Add Builtin classes to pymathics_builtins + if builtin_class is not None: + instance = builtin_class(expression=False) + + if isinstance(instance, Builtin): + submodules = pymathics_builtins_by_module.get(module_name, []) + submodules.append(instance) + pymathics_builtins_by_module[module_name] = submodules + + # Add submodules to pymathics_builtins + module_var = getattr(module, name) + if inspect.ismodule(module_var) and module_var.__name__.startswith("pymathics"): + update_pymathics(module_var) diff --git a/mathics/eval/scoping.py b/mathics/eval/scoping.py new file mode 100644 index 000000000..08b4b73ee --- /dev/null +++ b/mathics/eval/scoping.py @@ -0,0 +1,55 @@ +from mathics.core.evaluation import Evaluation +from mathics.core.symbols import Symbol, fully_qualified_symbol_name + + +def dynamic_scoping(func, vars, evaluation: Evaluation): + """ + Changes temporarily the value of a set of symbols listed in vars, + and evaluates func(evaluation) + """ + original_definitions = {} + for var_name, new_def in vars.items(): + assert fully_qualified_symbol_name(var_name) + original_definitions[var_name] = evaluation.definitions.get_user_definition( + var_name + ) + evaluation.definitions.reset_user_definition(var_name) + if new_def is not None: + new_def = new_def.evaluate(evaluation) + evaluation.definitions.set_ownvalue(var_name, new_def) + try: + result = func(evaluation) + finally: + for name, definition in original_definitions.items(): + evaluation.definitions.add_user_definition(name, definition) + return result + + +def get_scoping_vars(var_list, msg_symbol="", evaluation=None): + def message(tag, *args): + if msg_symbol and evaluation: + evaluation.message(msg_symbol, tag, *args) + + if not var_list.has_form("List", None): + message("lvlist", var_list) + return + vars = var_list.elements + scoping_vars = set() + for var in vars: + var_name = None + if var.has_form("Set", 2): + var_name = var.elements[0].get_name() + new_def = var.elements[1] + if evaluation: + new_def = new_def.evaluate(evaluation) + elif isinstance(var, Symbol): + var_name = var.get_name() + new_def = None + if not var_name: + message("lvsym", var) + continue + if var_name in scoping_vars: + message("dup", Symbol(var_name)) + else: + scoping_vars.add(var_name) + yield var_name, new_def diff --git a/mathics/eval/testing_expressions.py b/mathics/eval/testing_expressions.py new file mode 100644 index 000000000..c15471f48 --- /dev/null +++ b/mathics/eval/testing_expressions.py @@ -0,0 +1,106 @@ +from typing import Optional + +import sympy + +from mathics.core.atoms import Complex, Integer0, Integer1, IntegerM1 +from mathics.core.expression import Expression +from mathics.core.systemsymbols import SymbolDirectedInfinity + + +def cmp(a, b) -> int: + "Returns 0 if a == b, -1 if a < b and 1 if a > b" + return (a > b) - (a < b) + + +def do_cmp(x1, x2) -> Optional[int]: + + # don't attempt to compare complex numbers + for x in (x1, x2): + # TODO: Send message General::nord + if isinstance(x, Complex) or ( + x.has_form("DirectedInfinity", 1) and isinstance(x.elements[0], Complex) + ): + return None + + s1 = x1.to_sympy() + s2 = x2.to_sympy() + + # Use internal comparisons only for Real which is uses + # WL's interpretation of equal (which allows for slop + # in the least significant digit of precision), and use + # use sympy for everything else + if s1.is_Float and s2.is_Float: + if x1 == x2: + return 0 + if x1 < x2: + return -1 + return 1 + + # we don't want to compare anything that + # cannot be represented as a numeric value + if s1.is_number and s2.is_number: + if s1 == s2: + return 0 + if s1 < s2: + return -1 + return 1 + + return None + + +def do_cplx_equal(x, y) -> Optional[int]: + if isinstance(y, Complex): + x, y = y, x + if isinstance(x, Complex): + if isinstance(y, Complex): + c = do_cmp(x.real, y.real) + if c is None: + return + if c != 0: + return False + c = do_cmp(x.imag, y.imag) + if c is None: + return + if c != 0: + return False + else: + return True + else: + c = do_cmp(x.imag, Integer0) + if c is None: + return + if c != 0: + return False + c = do_cmp(x.real, y.real) + if c is None: + return + if c != 0: + return False + else: + return True + c = do_cmp(x, y) + if c is None: + return None + return c == 0 + + +def expr_max(elements): + result = Expression(SymbolDirectedInfinity, IntegerM1) + for element in elements: + c = do_cmp(element, result) + if c > 0: + result = element + return result + + +def expr_min(elements): + result = Expression(SymbolDirectedInfinity, Integer1) + for element in elements: + c = do_cmp(element, result) + if c < 0: + result = element + return result + + +def is_number(sympy_value) -> bool: + return hasattr(sympy_value, "is_number") or isinstance(sympy_value, sympy.Float) diff --git a/mathics/format/asy.py b/mathics/format/asy.py index 5223cbba8..3b328ee4b 100644 --- a/mathics/format/asy.py +++ b/mathics/format/asy.py @@ -411,7 +411,11 @@ def inset_box(self, **options) -> str: x, y = self.pos.pos() opacity_value = self.opacity.opacity if self.opacity else None content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) - pen = asy_create_pens(edge_color=self.color, edge_opacity=opacity_value) + # FIXME: don't hard code text_style_opts, but allow these to be adjustable. + font_size = 3 + pen = asy_create_pens( + edge_color=self.color, edge_opacity=opacity_value, fontsize=font_size + ) asy = """// InsetBox label("$%s$", (%s,%s), (%s,%s), %s);\n""" % ( content, diff --git a/mathics/format/asy_fns.py b/mathics/format/asy_fns.py index f6f2935dd..85f484bf3 100644 --- a/mathics/format/asy_fns.py +++ b/mathics/format/asy_fns.py @@ -5,6 +5,9 @@ """ from itertools import chain +from typing import Optional, Type, Union + +RealType = Type[Union[int, float]] def asy_add_bezier_fn(self) -> str: @@ -103,13 +106,14 @@ def asy_color(self): def asy_create_pens( - edge_color=None, - face_color=None, - edge_opacity=None, - face_opacity=None, + edge_color: Optional[str] = None, + face_color: Optional[str] = None, + edge_opacity: Optional[RealType] = None, + face_opacity: Optional[RealType] = None, stroke_width=None, is_face_element=False, dotfactor=None, + fontsize: Optional[RealType] = None, ) -> str: """ Return an asymptote string fragment that creates a drawing pen. @@ -132,6 +136,8 @@ def asy_create_pens( opacity = edge_opacity if opacity is not None and opacity != 1: pen += f"+opacity({asy_number(opacity)})" + if fontsize is not None: + pen += f"+fontsize({fontsize})" if stroke_width is not None: pen += f"+linewidth({asy_number(stroke_width)})" result.append(pen) diff --git a/mathics/format/latex.py b/mathics/format/latex.py index 46b3f9ea6..ac074bbbb 100644 --- a/mathics/format/latex.py +++ b/mathics/format/latex.py @@ -163,6 +163,7 @@ def boxes_to_tex(box, **options): elements = self._elements evaluation = box_options.get("evaluation") items, options = self.get_array(elements, evaluation) + new_box_options = box_options.copy() new_box_options["inside_list"] = True column_alignments = options["System`ColumnAlignments"].get_name() @@ -175,12 +176,21 @@ def boxes_to_tex(box, **options): except KeyError: # invalid column alignment raise BoxConstructError - column_count = 0 + column_count = 1 for row in items: - column_count = max(column_count, len(row)) + if isinstance(row, tuple): + column_count = max(column_count, len(row)) + result = r"\begin{array}{%s} " % (column_alignments * column_count) for index, row in enumerate(items): - result += " & ".join(boxes_to_tex(item, **new_box_options) for item in row) + if isinstance(row, tuple): + result += " & ".join(boxes_to_tex(item, **new_box_options) for item in row) + else: + result += r"\multicolumn{%s}{%s}{%s}" % ( + str(column_count), + column_alignments, + boxes_to_tex(row, **new_box_options), + ) if index != len(items) - 1: result += "\\\\ " result += r"\end{array}" diff --git a/mathics/format/mathml.py b/mathics/format/mathml.py index 8e432fa27..2e1fa574f 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -131,6 +131,8 @@ def boxes_to_mathml(box, **options): elements = self._elements evaluation = box_options.get("evaluation") items, options = self.get_array(elements, evaluation) + num_fields = max(len(item) if isinstance(item, tuple) else 1 for item in items) + attrs = {} column_alignments = options["System`ColumnAlignments"].get_name() try: @@ -148,10 +150,11 @@ def boxes_to_mathml(box, **options): new_box_options["inside_list"] = True for row in items: result += "" - for item in row: - result += ( - f"{boxes_to_mathml(item, **new_box_options)}" - ) + if isinstance(row, tuple): + for item in row: + result += f"{boxes_to_mathml(item, **new_box_options)}" + else: + result += f"{boxes_to_mathml(row, **new_box_options)}" result += "\n" result += "" # print(f"gridbox: {result}") diff --git a/mathics/format/svg.py b/mathics/format/svg.py index 0dc4fa702..1e18e472f 100644 --- a/mathics/format/svg.py +++ b/mathics/format/svg.py @@ -250,10 +250,24 @@ def components(): add_conversion_fn(FilledCurveBox, filled_curve_box) -def graphics_box(self, leaves=None, **options) -> str: +def graphics_box(self, elements=None, **options: dict) -> str: + """ + Top-level SVG routine takes ``elements`` and ``options`` and turns + this into a SVG string, including the .. tag. + + ``elements`` could be a ``GraphicsElements`` object, + a tuple or a list. + + Options is a dictionary of Graphics options dictionary. Intersting Graphics options keys: + + ``data``: a tuple bounding box information as well as a copy of ``elements``. If given + this supercedes the information in the ``elements`` parameter. + + ``evaluation``: an ``Evaluation`` object that can be used when further evaluation is needed. + """ - if not leaves: - leaves = self._elements + if not elements: + elements = self._elements data = options.get("data", None) if data: @@ -269,7 +283,9 @@ def graphics_box(self, leaves=None, **options) -> str: height, ) = data else: - elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) + elements, calc_dimensions = self._prepare_elements( + elements, options, neg_y=True + ) ( xmin, xmax, @@ -293,28 +309,21 @@ def graphics_box(self, leaves=None, **options) -> str: self.boxheight = options.get("height", self.boxheight) if self.background_color is not None: + # FIXME: tests don't seem to cover this secton of code. # Wrap svg_elements in a rectangle - svg_body = '%s' % ( - xmin, - ymin, - self.boxwidth, - self.boxheight, - self.background_color.to_css()[0], - svg_body, - ) + svg_body = f""" + - %s - -""" % ( - " ".join("%f" % t for t in (xmin, ymin, self.boxwidth, self.boxheight)), - svg_body, - ) + + svg_main = wrap_svg_body(self.boxwidth, self.boxheight, xmin, ymin, svg_body) # print("svg_main", svg_main) return svg_main # , width, height @@ -534,3 +543,24 @@ def _roundbox(self): add_conversion_fn(_RoundBox) + + +def wrap_svg_body( + box_width: float, box_height: float, x_min: float, y_min: float, svg_body: str +) -> str: + """ + Wraps ``svg`` into an SVG tag ... + ``box_width`` and ``box_height`` are pixel units. These together with + x_min, and y_min also form the viewBox attribute. + + The wrapped SVG text is returned as a string. + """ + svg_str = f""" + + {svg_body} + +""" + return svg_str diff --git a/mathics/format/text.py b/mathics/format/text.py index 7a60c9202..3d4be51e4 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -66,21 +66,37 @@ def gridbox(self, elements=None, **box_options) -> str: result = "" if not items: return "" - widths = [0] * len(items[0]) + try: + widths = [0] * max(1, max(len(row) for row in items if isinstance(row, tuple))) + except ValueError: + widths = [0] + cells = [ [ # TODO: check if this evaluation is necesary. boxes_to_text(item, **box_options).splitlines() for item in row ] + if isinstance(row, tuple) + else [boxes_to_text(row, **box_options).splitlines()] for row in items ] - for row in cells: + + # compute widths + full_width = 0 + for i, row in enumerate(cells): for index, cell in enumerate(row): if index >= len(widths): raise BoxConstructError - for line in cell: - widths[index] = max(widths[index], len(line)) + if not isinstance(items[i], tuple): + for line in cell: + full_width = max(full_width, len(line)) + else: + for line in cell: + widths[index] = max(widths[index], len(line)) + + full_width = max(sum(widths), full_width) + for row_index, row in enumerate(cells): if row_index > 0: result += "\n" @@ -95,10 +111,12 @@ def gridbox(self, elements=None, **box_options) -> str: else: text = "" line += text - if cell_index < len(row) - 1: - line += " " * (widths[cell_index] - len(text)) - # if cell_index < len(row) - 1: - line += " " + if isinstance(items[row_index], tuple): + if cell_index < len(row) - 1: + line += " " * (widths[cell_index] - len(text)) + # if cell_index < len(row) - 1: + line += " " + if line_exists: result += line + "\n" else: diff --git a/mathics/packages/DiscreteMath/CombinatoricaV0.9.m b/mathics/packages/DiscreteMath/CombinatoricaV0.9.m index eda004224..fd4b2f482 100644 --- a/mathics/packages/DiscreteMath/CombinatoricaV0.9.m +++ b/mathics/packages/DiscreteMath/CombinatoricaV0.9.m @@ -14,9 +14,10 @@ 350 Bridge Parkway, Redwood City CA 94065. ISBN 0-201-50943-1. For ordering information, call 1-800-447-2226. -These programs can be obtained on Macintosh and MS-DOS disks by sending -$15.00 to Discrete Mathematics Disk, Wolfram Research Inc., -PO Box 6059, Champaign, IL 61826-9905. (217)-398-0700. +These (and related) programs are available by anonymous ftp.cs.sunysb.edu +in the pub/Combinatorica directory. They can also be obtained on +Macintosh and MS-DOS disks by sending $15.00 to Discrete Mathematics Disk, +Wolfram Research Inc., PO Box 6059, Champaign, IL 61826-9905. (217)-398-0700. Any comments, bug reports, or requests to get on the Combinatorica mailing list should be forwarded to: @@ -32,13 +33,13 @@ *) (* :Context: DiscreteMath`Combinatorica` *) -(* :Package Version: .9 (2/29/92 Beta Release) -*) +(* :Package Version: .91 (3/23/95 Beta Release) + *) (**** Note: some very small changes have been made to make this -to work with Mathics 1.1.1 ****) +to work with Mathics3 ****) -(* :Copyright: Copyright 1990, 1991, 1992 by Steven S. Skiena +(* :Copyright: Copyright 1990--1995 by Steven S. Skiena This package may be copied in its entirety for nonprofit purposes only. Sale, other than for the direct cost of the media, is prohibited. This @@ -54,6 +55,7 @@ incidental, or consequential damages. *) (* :History: + Version .9 by Steven S. Skiena, February 1992. Version .8 by Steven S. Skiena, July 1991. Version .7 by Steven S. Skiena, January 1991. Version .6 by Steven S. Skiena, June 1990. @@ -77,13 +79,10 @@ and Graph Theory with Mathematica", Addison-Wesley Publishing Co. *) -(* :Mathematica Version: 0.9.0 for Mathics - This is Mathematica Version 0.9 adapted for Mathics. +(* :Mathematica Version: 2.3 *) -BeginPackage["DiscreteMath`CombinatoricaV0.9`"] -Unprotect[All] -Unprotect[Subsets] +BeginPackage["DiscreteMath`CombinatoricaV0.91`"] Graph::usage = "Graph[g,v] is the header for a graph object where g is an adjacency matrix and v is a list of vertices." @@ -137,7 +136,7 @@ ChromaticNumber::usage = "ChromaticNumber[g] computes the chromatic number of the graph, the fewest number of colors necessary to color the graph." -ChromaticPolynomial::usage = "ChromaticPolynomial[g,z] returns the chromatic polynomial P(z) of graph g, which counts the number of ways to color g with exactly z colors." +ChromaticPolynomial::usage = "ChromaticPolynomial[g,z] returns the chromatic polynomial P(z) of graph g, which counts the number of ways to color g with at most z colors." CirculantGraph::usage = "CirculantGraph[n,l] constructs a circulant graph on n vertices, meaning the ith vertex is adjacent to the (i+j)th and (i-j)th vertex, for each j in list l." @@ -599,6 +598,8 @@ (* Section 1.1.1 Lexicographically Ordered Permutions, Pages 3-4 *) +LexicographicPermutations[{}] := {{}} + LexicographicPermutations[{l_}] := {{l}} LexicographicPermutations[{a_,b_}] := {{a,b},{b,a}} @@ -626,30 +627,16 @@ RankPermutation[p_?PermutationQ] := (p[[1]]-1) (Length[Rest[p]]!) + RankPermutation[ Map[(If[#>p[[1]], #-1, #])&, Rest[p]] ] -(* UP, and UnrankPermutation come from the V2.1 code. - There is some problem in the v0.9 code and rather than try to fix that - we use the newer version - *) -UP[r_Integer, n_Integer] := - Module[{r1 = r, q = n!, i}, - Table[r1 = Mod[r1, q]; - q = q/(n - i + 1); - Quotient[r1, q] + 1, - {i, n} - ] - ] -UnrankPermutation[r_Integer, {}] := {} -UnrankPermutation[r_Integer, l_List] := - Module[{s = l, k, t, p = UP[Mod[r, Length[l]!], Length[l]], i}, - Table[k = s[[t = p[[i]] ]]; - s = Delete[s, t]; - k, - {i, Length[ p ]} - ] - ] -UnrankPermutation[r_Integer, n_Integer?Positive] := - UnrankPermutation[r, Range[n]] -NthPermutation[r_Integer, l_List] := UnrankPermutation[r, l] +NthPermutation[n1_Integer,l_List] := + Block[{k, n=n1, s=l, i}, + Table[ + n = Mod[n,(i+1)!]; + k = s [[Quotient[n,i!]+1]]; + s = Complement[s,{k}]; + k, + {i,Length[l]-1,0,-1} + ] + ] NextPermutation[p_?PermutationQ] := NthPermutation[ RankPermutation[p]+1, Sort[p] ] @@ -658,7 +645,7 @@ (*** FIXME: ListPlot[ RandomPermutation1[30]] -shows that RandomPermutaion1 isn't good. Therefore we use RandomPermutation2 +shows that RandomPermutaiton1 isn't good. Therefore we use RandomPermutation2 for RandomPermutation. ****) @@ -675,6 +662,7 @@ p ] +(* rocky: RandomPermutation1 not random, so use RandomPermutation2 *) RandomPermutation[n_Integer?Positive] := RandomPermutation2[n] (* Section 1.1.4 Permutation from Transpostions, Page 11 *) @@ -723,6 +711,8 @@ Solution[space_List,index_List,count_Integer] := Module[{i}, Table[space[[ i,index[[i]] ]], {i,count}] ] +DistinctPermutations[s_List] := Permutations[s] /; (Length[s] == 1) + DistinctPermutations[s_List] := Module[{freq,alph=Union[s],n=Length[s]}, freq = Map[ (Count[s,#])&, alph]; @@ -797,7 +787,7 @@ ReflexiveQ[r_?SquareMatrixQ] := Module[{i}, Apply[And, Table[(r[[i,i]]!=0),{i,Length[r]}] ] ] -TransitiveQ[r_?SquareMatrixQ] := TransitiveQ[ Graph[v,RandomVertices[Length[r]]] ] +TransitiveQ[r_?SquareMatrixQ] := TransitiveQ[ Graph[r,RandomVertices[Length[r]]] ] TransitiveQ[r_Graph] := IdenticalQ[r,TransitiveClosure[r]] SymmetricQ[r_?SquareMatrixQ] := (r === Transpose[r]) @@ -904,7 +894,8 @@ (* 1.3.1 Inversion Vectors, Page 27 *) FromInversionVector[vec_List] := - Block[{n=Length[vec]+1,i,p={n}}, + Module[{n=Length[vec]+1,i,p}, + p={n}; Do [ p = Insert[p, i, vec[[i]]+1], {i,n-1,1,-1} @@ -1040,8 +1031,7 @@ Join[ prev, Map[(Append[#,First[l]])&,Reverse[prev]] ] ] -(* We have a builtin that does this. -GrayCode doesn't work? +(* rocky hacked: is already in Mathics3 Subsets[l_List] := GrayCode[l] Subsets[n_Integer] := GrayCode[Range[n]] *) @@ -1095,7 +1085,7 @@ ] ] ]] - ] + ] /; (k <= Length[set]) PartitionQ[p_List] := (Min[p]>0) && Apply[And, Map[IntegerQ,p]] @@ -1133,7 +1123,7 @@ Show[ Graphics[ Join[ - {PointSize[ Min[0.04,1/(2 Max[p])] ]}, + {PointSize[ Min[0.05,1/(2 Max[p])] ]}, Table[Point[{i,j}], {j,n}, {i,p[[j]]}] ], {AspectRatio -> 1, PlotRange -> All} @@ -1141,6 +1131,8 @@ ] ] +TransposePartition[{}] := {} + TransposePartition[p_List] := Module[{s=Select[p,(#>0)&], i, row, r}, row = Length[s]; @@ -1176,32 +1168,23 @@ ] ] +(* from Paul Chase *) + RandomPartition[n_Integer?Positive] := - Module[{mult = Table[0,{n}],j,d,m = n}, - While[ m != 0, - {j,d} = NextPartitionElement[m]; - m -= j d; - mult[[d]] += j; - ]; - Flatten[Map[(Table[#,{mult[[#]]}])&,Reverse[Range[n]]]] - ] - -NextPartitionElement[n_Integer] := - Module[{d=0,j,m,z=RandomInteger[] n PartitionsP[n],done=False,flag}, - While[!done, - d++; m = n; j = 0; flag = False; - While[ !flag, - j++; m -=d; - If[ m > 0, - z -= d PartitionsP[m]; - If[ z <= 0, flag=done=True], - flag = True; - If[m==0, z -=d; If[z <= 0, done = True]] - ]; - ]; - ]; - {j,d} - ] + Module[{mult = Table[0, {n}], j, d, r=n, z}, + While[ (r > 0), + d = 1; j = 0; + z = Random[] r PartitionsP[r]; + While [z >= 0, + j++; + If [r-j*d < 0, {j=1; d++;}]; + z -= j*PartitionsP[r-j*d]; + ]; + r -= j d; + mult[[j]] += d; + ]; + Reverse[Flatten[Table[Table[j, {mult[[j]]}], {j, Length[mult]}]]] + ] NumberOfCompositions[n_,k_] := Binomial[ n+k-1, n ] @@ -1250,10 +1233,11 @@ ShapeOfTableau[t_List] := Map[Length,t] +(* Section 2.3.1 Insertion and Deletion, Page 64 *) InsertIntoTableau[e_Integer,{}] := { {e} } InsertIntoTableau[e_Integer, t1_?TableauQ] := - Module[{item=e,row=0,col,t=t1}, + Block[{item=e,row=0,col,t=t1}, While [row < Length[t], row++; If [Last[t[[row]]] <= item, @@ -2693,25 +2677,67 @@ Graph[reduction,Vertices[g]] ] -HasseDiagram[g_Graph] := - Module[{r,rank,m,stages,freq=Table[0,{V[g]}]}, - r = TransitiveReduction[ RemoveSelfLoops[g] ]; - rank = RankGraph[ - MakeUndirected[r], - Select[Range[V[g]],(InDegree[r,#]==0)&] - ]; - m = Max[rank]; - rank = MapAt[(m)&,rank,Position[OutDegree[r],0]]; - stages = Distribution[ rank ]; - Graph[ - Edges[r], - Table[ - m = ++ freq[[ rank[[i]] ]]; - {(m-1) + (1-stages[[rank[[i]] ]])/2, rank[[i]]}, - {i,V[g]} +(*thanks Christoph Strnadl*) + +HasseDiagram[g_,fak_:1] := + Module[{r, rank, m, stages, freq=Table[0,{V[g]}], + adjm, first}, + r = TransitiveReduction[ RemoveSelfLoops[g] ]; + adjm = ToAdjacencyLists[r]; + rank = Table[ 0,{ V[g]} ]; + first = Select[ Range[ V[g]], InDegree[r,#]==0& ]; + rank = MakeLevel[ first, 1, adjm, rank]; + first = Max[rank]; + stages = Distribution[ rank ]; + Graph[ + Edges[r], + Table[ + m = ++ freq[[ rank[[i]] ]]; + { ((m-1) + (1-stages[[rank[[i]] ]])/2) fak^(first-rank[[i]]), + rank[[i]] }, + {i, V[g]} ] ] - ] /; AcyclicQ[RemoveSelfLoops[g],Directed] + ] /; AcyclicQ[ RemoveSelfLoops[g],Directed ] + +(* + * SetLevel[{p1,p2,...},lvl,rank] sets the positions p1, p2,.. of + * list rank to the level lvl, if the old entry at that position + * is less than level. + *) +SetLevel[l_List,lvl_,rank_List] := + Module[ {r=rank}, + If[ r[[#]] < lvl, r[[#]] = lvl ] & /@ l; + r + ] + +(* + * MakeLevel[l,level,adjm,rank] constructs recursively the ranks of + * each vertex according to the adjacency matrix adjm of the graph. + * rank is the current ranking, level the new level to assign and + * l = {v1,v2,..} the list of vertices to be set to level. + *) +MakeLevel[{},_,_,rank_] := rank + +MakeLevel[l_List,lvl_,adjm_List,r_List] := + Module[ {rank=r, v, lst=l }, + rank = SetLevel[lst,lvl,rank]; (* make this level ready *) + While[ lst != {}, + v = First[lst]; + rank = MakeLevel[adjm[[v]], lvl+1,adjm,rank]; + lst = Rest[lst]; + ]; + rank + ] + +(* + * HasseDiagram[g] renders a graph corresponding to the HasseDiagram of + * the partial order induced by the directed graph g. + * HasseDiagram[g,fac] renders the HasseDiagram in which each vertex' + * position is stretched by factor fac. In each stage that factor + * is taken to the power of the distance to the 1 element. + *) + TopologicalSort[g_Graph] := Module[{g1 = RemoveSelfLoops[g],e,indeg,zeros,v}, @@ -3180,38 +3206,6 @@ (aj < Max[b]) ] -KSetPartitions::usage = "KSetPartitions[set, k] returns the list of set partitions of set with k blocks. KSetPartitions[n, k] returns the list of set partitions of {1, 2, ..., n} with k blocks. If all set partitions of a set are needed, use the function SetPartitions." -KSetPartitions[{}, 0] := {{}} -KSetPartitions[s_List, 0] := {} -KSetPartitions[s_List, k_Integer] := {} /; (k > Length[s]) -KSetPartitions[s_List, k_Integer] := {Map[{#} &, s]} /; (k === Length[s]) -KSetPartitions[s_List, k_Integer] := - Block[{$RecursionLimit = Infinity}, - Join[Map[Prepend[#, {First[s]}] &, KSetPartitions[Rest[s], k - 1]], - Flatten[ - Map[Table[Prepend[Delete[#, j], Prepend[#[[j]], s[[1]]]], - {j, Length[#]} - ]&, - KSetPartitions[Rest[s], k] - ], 1 - ] - ] - ] /; (k > 0) && (k < Length[s]) - -KSetPartitions[0, 0] := {{}} -KSetPartitions[0, k_Integer?Positive] := {} -KSetPartitions[n_Integer?Positive, 0] := {} -KSetPartitions[n_Integer?Positive, k_Integer?Positive] := KSetPartitions[Range[n], k] - -SetPartitions::usage = "SetPartitions[set] returns the list of set partitions of set. SetPartitions[n] returns the list of set partitions of {1, 2, ..., n}. If all set partitions with a fixed number of subsets are needed use KSetPartitions." - -SetPartitions[{}] := {{}} -SetPartitions[s_List] := Flatten[Table[KSetPartitions[s, i], {i, Length[s]}], 1] - -SetPartitions[0] := {{}} -SetPartitions[n_Integer?Positive] := SetPartitions[Range[n]] - - End[] Protect[ diff --git a/mathics/packages/Utilities/CleanSlate.m b/mathics/packages/Utilities/CleanSlate.m new file mode 100644 index 000000000..ecf21fda1 --- /dev/null +++ b/mathics/packages/Utilities/CleanSlate.m @@ -0,0 +1,514 @@ +(* :Title: CleanSlate *) + +(* :Author: + Todd Gayley + internet: tgayley@mcs.net +*) + +(* :Version: 1.1.3 *) + +(* :Copyright: + Copyright 1992-2000, Todd Gayley. + Permission is hereby granted to modify and/or make copies of + this file for any purpose other than direct profit, or as part + of a commercial product, provided this copyright notice is left + intact. Sale, other than for the cost of media, is prohibited. + Permission is hereby granted to reproduce part or all of + this file, provided that the source is acknowledged. +*) + +(* :History: + Modified May 1993 in several small ways. The major change is that + now all Unprotecting by the package code is done using string + arguments to Unprotect, thus circumventing the Unprotect patch + without having to explicitly remove the patch by altering the + downvalues of Unprotect. + V1.1.1, September 1997: fix problem with deleting temporary symbols, + use Block instead of Module to avoid incrementing $ModuleNumber. + V1.1.3, August 1998: Handle Experimental` and Developer` contexts. +*) + +(* :Context: Utilities`CleanSlate` *) + +(* :Mathematica Version: 4.0 *) + +(* :Warning: + CleanSlate might be considered a "dangerous" function, given what + it tries to do. Although it is well-tested, use it at your own + risk. +*) + +(* :Discussion: + +PURPOSE + +The purpose of CleanSlate is to provide an easy and complete way to accomplish +two goals: 1) free memory, and 2) clear values of symbols, so that you need not +worry about tripping over some preexisting definition for a symbol. The basic +command exported from the package, CleanSlate[], tries to do everything +possible to return the kernel to the state it was in when the CleanSlate.m +package was initially read in (usually, this is at the end of the startup +process, but, as discussed below, it can be read in at other times as well). Of +course, short of actually restarting, there is no way to do this, but I hope +that CleanSlate comes as close as possible. I think it will be adequate for +most user's needs. + +BRIEF SUMMARY + +There are 3 functions exported from the package: CleanSlate, CleanSlateExcept, +and ClearInOut. + +ClearInOut[] simply clears the In[] and Out[] values, and resets the $Line +number to 1 (so new input begins as In[1]). It is called internally by +CleanSlate and CleanSlateExcept. Once this function has been executed, you can +no longer refer to older input or output (if ClearInOut[] is executed as +In[32], then you cannot refer to %30, for example). It does not affect the +values of any symbols, though, so it is a relatively "nondestructive" attempt +to free memory By itself, it usually results in only a minimal recovery of +memory, but in some cases (e.g., graphics) the savings can be large. + +CleanSlate and CleanSlateExcept share the same basic purging engine (the +private function CleanSlateEngine), differing only in the way they calculate +which contexts to send to this engine. These functions will be discussed in +much greater detail below, but their basic use is as follows. CleanSlate[] +tries to purge everything that has happened since the CleanSlate package was +read in. You can also specify specific contexts for purging with +CleanSlate["Context1`","Context2`", ...]. Only the listed contexts, along with +all of their subcontexts, will be affected. Thus, if you don't specify a +context or contexts, CleanSlate will assume you want the complete job. +CleanSlateExcept["Context1`","Context2`", ...] allows you to specify a set of +contexts to be spared from purging. Everything other than what you list will be +purged. At the end of the process, the functions print a list of the contexts +purged and the approximate amount of memory freed. The return value is the new +$ContextPath. + +CleanSlate and CleanSlateExcept take one option, Verbose, which can be set to +True or False. The default is Verbose->True, which specifies that they print +their usual diagnostic messages. + +CleanSlate and CleanSlateExcept have some basic error-checking code built into +them, to prevent incorrect use. In particular, they catch any invalid +parameters (such as a misspelled context). For consistency, they take their +input in the same form as the Mathematica functions that take contexts as +parameters (Begin and BeginPackage): a sequence (not a list) of strings, each +specifying a context name. + +CleanSlate and Share: Mathematica version 2.1 has a command, Share[], which can +free significant amounts of memory. Share and CleanSlate do not conflict, and +in fact they are ideally used together. Run Share after CleanSlate to produce +the maximum recovery of memory. Share generally executes much more slowly than +CleanSlate, however, so you might not want to use it routinely. + +HOW TO USE IT + +CleanSlate.m is designed to be read in at the end of the startup process. This +is best accomplished by putting it as the last thing in the file init.m. The +code can be simply pasted into this file, or you can just put <False] -- Same as CleanSlate[], but don't + print diagnostic output + ClearInOut[] -- Just clear In and Out values + +WHAT IS MEANT BY "PURGING"? + +Essentially, "purging" means wiping out all trace of the context's existence. +This is basically a 3-step process. Step 1 is to map Unprotect, ClearAll, and +Remove over all symbols in the context (and any subcontexts). Step 2 is to try +to remove any rules that the context may have defined for System symbols. Step +3 is to remove the context from $ContextPath and $Packages (if it is present in +$Packages). The Global` context, however, is not removed from $ContextPath or +$Packages. + +Some packages "overload" System functions (i.e., those in the System` context) +with additional rules. A good example is the package Algebra`ReIm`, which adds +new rules for Re, Im, Abs, Conjugate, and Args. To effectively remove this +package, we would need to remove these additional rules as well. CleanSlate +uses a clever (I think) scheme that enables it to strip out rules a package +adds for System functions. The basic mechanism involves substituting my own +function for Unprotect. In this way, it can intercept all attempts to Unprotect +system symbols (a necessary prelude to adding rules), noting which symbols are +being unprotected and which context is doing it. After this information has +been recorded, the built-in Unprotect is called. + +There is more extensive documentation included as a separate file. + +I thank Larry Calmer, Jack Lee, Emily Martin, Robby Villegas, Dave Withoff, +and my beta-testers. + +**********) + +(* ================== CODE BEGINS ===================== *) + +System`startupPath = $ContextPath; +System`startupGlobals = Flatten[Names[#<>"*"]& /@ Contexts["Global`*"]]; +System`startupPackages = $Packages; + +BeginPackage["Utilities`CleanSlate`"]; + +Unprotect[CleanSlate,CleanSlateExcept,ClearInOut]; + +CleanSlate::usage = "CleanSlate[] purges all symbols and their values in \ +all contexts that have been added to the context search path \ +($ContextPath), since the CleanSlate package was read in. This includes \ +user-defined symbols (in the Global` context) as well as any packages \ +that may have been read in. It also removes most, but possibly not all, of \ +the additional rules for System symbols that these packages may have \ +defined. It also clears the In[] and Out[] values, and resets the $Line \ +number, so new input begins as In[1]. \ +CleanSlate[\"Context1`\",\"Context2`\"] purges only the listed contexts."; + +CleanSlateExcept::usage = "CleanSlateExcept[\"Context1`\",\"Context2`\"] \ +purges all symbols and their values in all contexts that have been added to \ +the context search path ($ContextPath) since the CleanSlate package was \ +read in, except for the listed contexts. It also removes most, but possibly \ +not all, of the additional rules for System symbols that purged packages \ +may have defined. It also clears the In[] and Out[] values, and resets the \ +$Line number, so new input begins as In[1]."; + +CleanSlate::cntxtpth = "Error in $Contextpath. The $ContextPath is shorter \ +than it was when the CleanSlate package was read in. CleanSlate cannot be \ +run within a package (i.e., between BeginPackage..EndPackage pairs)."; + +CleanSlate::notcntxt = "A context you have given is either misspelled, \ +incorrectly specified, or is not on $ContextPath."; + +CleanSlate::nopurge = "The context `1` cannot be purged, because it was \ +present when the CleanSlate package was initally read in."; + +CleanSlate::noself = "CleanSlate cannot purge its own context."; + +CleanSlate::syntax = "CleanSlate takes arguments of the form \ +\"Context1``\", \"Context2``\"."; + +CleanSlateExcept::syntax = "CleanSlateExcept takes arguments of the form \ +\"Context1``\", \"Context2``\"."; + +ClearInOut::usage = "ClearInOut[] clears the In[] and Out[] values, and \ +resets the $Line number, so new input begins as In[1]. This can produce a \ +modest recovery of memory, but you will no longer be able to refer to \ +output generated previously."; + +System`Verbose::usage = "Verbose is an option for CleanSlate and CleanSlateExcept \ +that specifies whether they print diagnostic output. It can be set to True or \ +False. The default is Verbose->True."; + +Options[CleanSlate] = {Verbose->True}; +Options[CleanSlateExcept] = {Verbose->True}; + +(**************** Private` *******************) + +Begin["`Private`"]; + +initialPath = System`startupPath; +initialGlobals = System`startupGlobals; +initialPackages = System`startupPackages + +Remove[System`startupPath]; (* Clean up these; no longer needed *) +Remove[System`startupGlobals]; +Remove[System`startupPackages]; + +(* "Patch" Unprotect, so we can see who is modifying System symbols. + + Actually, this ugly form of patching is no longer needed; it + persists only for historical reasons. I could + use the trivial "rule with Condition that fails but performs side + effect" type of patch. But CleanSlate has been used for years + as part of the WRI tester. I don't want to touch it now. +*) + +alteredSystemSymbols = {}; +Unprotect["Unprotect"]; +Unprotect[x__Symbol] := + Block[ {old, result, pos}, + Scan[ Function[sym, + If[Context[sym] == "System`", + If[ MemberQ[ alteredSystemSymbols, $Context, {2} ], + pos = Flatten[ Position[alteredSystemSymbols, $Context] + ] + {0,1}; + alteredSystemSymbols = + ReplacePart[ alteredSystemSymbols, + (alteredSystemSymbols[[Sequence@@pos]] + ~Union~ {Hold[sym]}), + pos + ], + (* else *) + If[ !StringMatchQ[$Context,"System`*"], + alteredSystemSymbols = alteredSystemSymbols ~Union~ + {{$Context,{Hold[sym]}}} + ] + ] + ], + {HoldAll} + ], + Hold[x] + ]; + Unprotect["Unprotect"]; + old = DownValues[Unprotect]; + DownValues[Unprotect] = Select[DownValues[Unprotect], + FreeQ[#,"an unlikely string"]&]; + result = Unprotect[x]; + DownValues[Unprotect] = old; + Protect[Unprotect]; + result + ]; +Unprotect[{x__Symbol}] := Unprotect[x]; +Protect[Unprotect]; + +(* Note: now that my Unprotect does not intercept string arguments, I could + avoid the need to fiddle with the downvalues of Unprotect by just + converting to strings and passing to Unprotect. Here's the code: + Flatten@ReleaseHold@Map[Function[z, + Unprotect[Evaluate@ToString@HoldForm@z], + {HoldAll} + ],Hold@x,{-1}] + +*) + +(*************** ClearInOut ***************) + +ClearInOut[] := ( Unprotect["In","Out","InString","MessageList"]; + Clear[In,Out,InString,MessageList]; + Protect[In,Out,InString,MessageList]; + $Line=0; + ) + +(****************** CleanSlate ******************) + +CleanSlate[opt___?OptionQ] := CleanSlateExcept[opt] + +CleanSlate[] := CleanSlateExcept[Verbose -> (Verbose /. Options[CleanSlate])] + +CleanSlate[cntxtstopurge__String, opt___?OptionQ] := + Block[ { contextsToPurge = {cntxtstopurge} + ~ Complement ~ (initialPath + ~ Complement ~ {"Global`"}) + ~ Complement ~ {"Utilities`CleanSlate`"}, + vbose = Verbose /. {opt} /. Options[CleanSlate] + }, + + If[ !MatchQ[vbose, True | False], + Message[CleanSlate::opttf, Verbose, vbose]; + vbose = True + ]; + If[First[#] =!= Verbose, + Message[CleanSlate::optx, First[#], InString[$Line]]; + ]& /@ {opt}; + If[ MemberQ[initialPath ~Complement~ {"Global`", "Utilities`CleanSlate`"},#], + Message[CleanSlate::nopurge,#]; + Abort[]; + ]& /@ {cntxtstopurge}; + If[ MemberQ[ {cntxtstopurge}, "Utilities`CleanSlate`"], + Message[CleanSlate::noself]; + Abort[]; + ]; + ErrorChecking[cntxtstopurge]; + CleanSlateEngine[contextsToPurge, vbose] + ] + +(**************** CleanSlateExcept ****************) + +CleanSlateExcept[cntxtstospare___String, opt___?OptionQ] := + Block[ { contextsToPurge = $ContextPath + ~ Complement ~ (initialPath + ~ Complement ~ {"Global`"}) + ~ Complement ~ {"Utilities`CleanSlate`"} + ~ Complement ~ {cntxtstospare}, + vbose = Verbose /. {opt} /. Options[CleanSlateExcept] + }, + If[ !MatchQ[vbose, True | False], + Message[CleanSlate::opttf, Verbose, vbose]; + vbose = True + ]; + If[First[#] =!= Verbose, + Message[CleanSlate::optx, First[#], InString[$Line]]; + ]& /@ {opt}; + ErrorChecking[cntxtstospare]; + CleanSlateEngine[contextsToPurge, vbose] + ] + +(**** trap syntax errors: *****) + +CleanSlate[__] := Message[CleanSlate::syntax,"`","`"] + +CleanSlateExcept[__] := Message[CleanSlateExcept::syntax,"`","`"] + +ErrorChecking[params___String] := ( + If[ Sort[$ContextPath] != $ContextPath ~Union~ {params}, + Message[CleanSlate::notcntxt]; Abort[] + ]; + If[ Sort[$ContextPath] != $ContextPath ~Union~ initialPath, + Message[CleanSlate::cntxtpth]; Abort[] + ]; +) + +(*** Contexts containing kernel functions. Should not be purged. ***) + +$AdditionalKernelContexts = { + "Experimental`", + "Developer`", + "Algebra`SymmetricPolynomials`", + "NumberTheory`AlgebraicNumberFields`", + "Optimization`MPSData`", + "JLink`", + (* the following Statistics packages include some + functions defined in the kernel *) + "HierarchicalClustering`", + "LinearRegression`" + }; + +(****************** CleanSlateEngine ******************) +(*** (the main purging function) ****) + +CleanSlateEngine[contextsToPurge_List, vbose_] := + Block[ { initialMem = MemoryInUse[], + memoryFreed, + systemSymbolsToCheck, + allPurgedContexts, + unpurgeableContexts, + flag, + protected + }, + + + (* These contexts, new in 3.5, have some quirks. They will not get + * purged, though they will be removed from the Context Path if they + * weren't on it when CleanSlate loaded. *) + unpurgeableContexts = $AdditionalKernelContexts; + + allPurgedContexts = Flatten[ Contexts[#<>"*"]& /@ contextsToPurge ]; + nonglobalsToPurge = (#<>"*"&) + /@ Flatten[ Contexts[#<>"*"]& + /@ (contextsToPurge ~Complement~ Join[ {"Global`"}, unpurgeableContexts] ) + ]; + globalsToPurge = Flatten[ Names[#<>"*"]& /@ Contexts["Global`*"] + ] ~Complement~ initialGlobals; + + (Unprotect[#];ClearAll[#])& /@ nonglobalsToPurge; + + (* Global` context has to be treated a bit differently, because + we need to preserve any symbols that may have existed at the + time CleanSlate was read in. + *) + + If[ MemberQ[contextsToPurge, "Global`"], + (Unprotect[#];ClearAll[#])& /@ globalsToPurge; + If[Names[#] =!= {}, Remove[#]]& /@ globalsToPurge; + ]; + + If[Names[#] =!= {}, Remove[#]]& /@ nonglobalsToPurge; + + (* Hard-coded hack for Calculus`EllipticIntegrate` *) + + If[MemberQ[contextsToPurge, "Calculus`EllipticIntegrate`"], + DownValues[Integrate`TableMatch] = + DeleteCases[DownValues[Integrate`TableMatch], z:(x_ :> _) /; + StringMatchQ[ToString@FullForm@x,"*Removed[*"] ] + ]; + + (* Now go after any rules defined for System symbols *) + + systemSymbolsToCheck = {}; + alteredSystemSymbols = + Select[ alteredSystemSymbols, + If[ MemberQ[ allPurgedContexts, #[[1]] ], + systemSymbolsToCheck = systemSymbolsToCheck ~Union~ #[[2]]; + False, + True + ]& + ]; + + Scan[ Function[sym, + protected = Unprotect@Evaluate@ToString@HoldForm@sym; + If[ MemberQ[Attributes[sym], ReadProtected], + ClearAttributes[sym,ReadProtected]; + flag=True, + flag=False + ]; + DownValues[sym] = DeleteCases[DownValues[sym], z:(x_ :> _) /; + StringMatchQ[ToString@FullForm@x,"*Removed[*"] ]; + UpValues[sym] = DeleteCases[UpValues[sym], z:(x_ :> _) /; + StringMatchQ[ToString@FullForm@x,"*Removed[*"] ]; + FormatValues[sym] = DeleteCases[FormatValues[sym],z:(x_:>_) /; + StringMatchQ[ToString@FullForm@x,"*Removed[*"] ]; + SubValues[sym] = DeleteCases[SubValues[sym], z:(x_ :> _) /; + StringMatchQ[ToString@FullForm@x,"*Removed[*"] ]; + If[flag, SetAttributes[sym, {ReadProtected}]]; + Protect[Evaluate[protected]], + {HoldAll} + ], + systemSymbolsToCheck, {2} + ]; + + (* Clean up some potentially large lists that are no longer needed *) + + Clear[globalsToPurge, nonglobalsToPurge, allPurgedContexts]; + + ClearInOut[]; + + (* Print some useful information *) + + If[ vbose, + Print[" (CleanSlate) Contexts purged: ", + contextsToPurge ~ Complement ~ unpurgeableContexts ]; + memoryFreed = Quotient[ initialMem - MemoryInUse[], 1024]; + Print[" (CleanSlate) Approximate kernel memory recovered: ", + If[ memoryFreed > 0, + ToString[memoryFreed]<>" Kb", + "0 Kb" + ] + ] + ]; + + (* Reset $Packages to reflect the removed contexts *) + + protected = Unprotect["$Packages"]; + $Packages = Select[ $Packages, + (! MemberQ[contextsToPurge,#] || #=="Global`")& + ]; + $Packages=initialPackages; + Protect[Evaluate[protected]]; + + (* Reset the $ContextPath to reflect the removed contexts, and + return its new value as the result of CleanSlate *) + + $ContextPath = Select[ $ContextPath, + (!MemberQ[contextsToPurge,#] || #=="Global`")& + ] + ] + +End[]; (* Private *) + +Protect[ClearInOut, CleanSlate, CleanSlateExcept]; + +EndPackage[]; (* CleanSlate *) + +(* ================ END OF CODE ================== *) diff --git a/mathics/session.py b/mathics/session.py index 6d3054cf8..36a88e619 100644 --- a/mathics/session.py +++ b/mathics/session.py @@ -60,7 +60,7 @@ def __init__( add_builtin=True, catch_interrupt=False, form="InputForm", - character_encoding=Optional[str], + character_encoding: Optional[str] = None, ): if character_encoding is not None: mathics.settings.SYSTEM_CHARACTER_ENCODING = character_encoding diff --git a/mathics/settings.py b/mathics/settings.py index 554535f4d..970f12ced 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -41,23 +41,29 @@ def get_srcdir(): # from checked-out source and that is where this should be put. LOCAL_ROOT_DIR = get_srcdir() -# Location of internal document data. Currently this is in Python -# Pickle form, but storing this in JSON if possible would be preferable and faster +# Location of doctests and test results formated for LaTeX. This data +# is stoared as a Python Pickle format, but storing this in JSON if +# possible would be preferable and faster -# We need two versions, one in the user space which is updated with +# We need two versions of doctest data, one is in the user space which is updated with # local packages installed and is user writable. -DOC_USER_TEX_DATA_PATH = os.environ.get( - "DOC_USER_TEX_DATA_PATH", osp.join(DATA_DIR, "doc_tex_data.pcl") + + +DOCTEST_LATEX_DATA_PCL = os.environ.get( + "DOCTEST_LATEX_DATA_PCL", osp.join(DATA_DIR, "doctest_latex_data.pcl") ) -# We need another version as a fallback, and that is distributed with the +# We need another version of doctest data as a fallback, and that is distributed with the # package. It is note user writable and not in the user space. -DOC_SYSTEM_TEX_DATA_PATH = os.environ.get( - "DOC_SYSTEM_TEX_DATA_PATH", osp.join(LOCAL_ROOT_DIR, "data", "doc_tex_data.pcl") + +DOCTEST_SYSTEM_LATEX_DATA_PCL = os.environ.get( + "DOCTEST_SYSTEM_LATEX_DATA_PCL", + osp.join(LOCAL_ROOT_DIR, "data", "doctest_latex_data.pcl"), ) DOC_DIR = osp.join(LOCAL_ROOT_DIR, "doc", "documentation") -DOC_LATEX_FILE = osp.join(LOCAL_ROOT_DIR, "doc", "latex", "documentation.tex") +DOC_LATEX_DIR = osp.join(LOCAL_ROOT_DIR, "doc", "latex") +DOC_LATEX_FILE = osp.join(DOC_LATEX_DIR, "documentation.tex") # Set this True if you prefer 12 hour time to be the default TIME_12HOUR = False @@ -76,23 +82,23 @@ def get_srcdir(): SYSTEM_CHARACTER_ENCODING = "UTF-8" if character_encoding == "utf-8" else "ASCII" -def get_doc_tex_data_path(should_be_readable=False, create_parent=False) -> str: - """Returns a string path where we can find Python Pickle data for LaTeX +def get_doctest_latex_data_path(should_be_readable=False, create_parent=False) -> str: + """Returns a string path where we can find Python Pickle doctest data for LaTeX processing. If `should_be_readable` is True, the we will check to see whether this file is - readable (which also means it exists). If not, we'll return the `DOC_SYSTEM_DATA_PATH`. + readable (which also means it exists). If not, we'll return the `DOCTEST_SYSTEM_DATA_PATH`. """ - doc_user_tex_data_path = Path(DOC_USER_TEX_DATA_PATH) - base_config_dir = doc_user_tex_data_path.parent + doc_user_latex_data_pcl = Path(DOCTEST_LATEX_DATA_PCL) + base_config_dir = doc_user_latex_data_pcl.parent if not base_config_dir.is_dir() and create_parent: Path("base_config_dir").mkdir(parents=True, exist_ok=True) if should_be_readable: return ( - DOC_USER_TEX_DATA_PATH - if doc_user_tex_data_path.is_file() - else DOC_SYSTEM_TEX_DATA_PATH + DOCTEST_LATEX_DATA_PCL + if doc_user_latex_data_pcl.is_file() + else DOCTEST_SYSTEM_LATEX_DATA_PCL ) else: - return DOC_USER_TEX_DATA_PATH + return DOCTEST_LATEX_DATA_PCL diff --git a/mathics/version.py b/mathics/version.py index 5db67b59c..11bfc1b45 100644 --- a/mathics/version.py +++ b/mathics/version.py @@ -5,4 +5,4 @@ # well as importing into Python. That's why there is no # space around "=" below. # fmt: off -__version__="5.0.3dev0" # noqa +__version__="6.0.2dev0" # noqa diff --git a/requirements-full.txt b/requirements-full.txt index 634447ef3..4514496f2 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -1,6 +1,8 @@ # Optional packages which add functionality or speed things up +ipywidgets # For Manipulate +lxml # for HTML parsing used in builtin/fileformats/html psutil # SystemMemory and MemoryAvailable +pyocr # Used for TextRecognize scikit-image >= 0.17 # FindMinimum can use this; used by Image as well -lxml # for HTML parsing used in builtin/fileformats/html +unidecode # Used in Transliterate wordcloud # Used in builtin/image.py by WordCloud() -pyocr # Used for TextRecognize diff --git a/setup.cfg b/setup.cfg index 51277b097..3a03fc384 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ description_file = README.rst [flake8] # About max-line-length setting: # Our homegrown autodoc has brain-dead line wrapping which forces long lines in docstrings -max-line-length = 300 +max-line-length = 80 max-complexity = 12 select = E,F,W,C,B,B9 ignore = diff --git a/setup.py b/setup.py index 9be50f59b..9917daa4a 100644 --- a/setup.py +++ b/setup.py @@ -39,28 +39,28 @@ sys, "pypy_version_info" ) -INSTALL_REQUIRES = ["Mathics-Scanner >= 1.3.0.dev0", "pillow"] +INSTALL_REQUIRES = [ + "Mathics-Scanner >= 1.3.0", +] # Ensure user has the correct Python version # Address specific package dependencies based on Python version -if sys.version_info < (3, 6): +if sys.version_info < (3, 7): print("Mathics does not support Python %d.%d" % sys.version_info[:2]) sys.exit(-1) -elif sys.version_info[:2] == (3, 6): - INSTALL_REQUIRES += [ - "recordclass", - "numpy", - "llvmlite<0.37", - "sympy>=1.8,<1.12", - ] - if is_PyPy: - print("Mathics does not support PyPy Python 3.6" % sys.version_info[:2]) - sys.exit(-1) -else: - INSTALL_REQUIRES += ["numpy<=1.24", "llvmlite", "sympy>=1.8, < 1.12"] -if not is_PyPy: - INSTALL_REQUIRES += ["recordclass"] +INSTALL_REQUIRES += [ + "numpy<=1.24", + "llvmlite", + "sympy>=1.8, < 1.12", + # Pillow 9.1.0 supports BigTIFF with big-endian byte order. + # ExampleData image hedy.tif is in this format. + # Pillow 9.2 handles sunflowers.jpg + "pillow >= 9.2", +] + +# if not is_PyPy: +# INSTALL_REQUIRES += ["recordclass"] def get_srcdir(): @@ -143,6 +143,7 @@ def read(*rnames): "pint", "python-dateutil", "requests", + "setuptools", ] print(f'Installation requires "{", ".join(INSTALL_REQUIRES)}') @@ -173,6 +174,7 @@ def subdirs(root, file="*.*", depth=10): "mathics.builtin.box", "mathics.builtin.colors", "mathics.builtin.distance", + "mathics.builtin.exp_structure", "mathics.builtin.drawing", "mathics.builtin.fileformats", "mathics.builtin.files_io", @@ -191,6 +193,7 @@ def subdirs(root, file="*.*", depth=10): "mathics.builtin.specialfns", "mathics.builtin.statistics", "mathics.builtin.string", + "mathics.builtin.testing_expressions", "mathics.builtin.vectors", "mathics.eval", "mathics.doc", diff --git a/test/builtin/arithmetic/test_abs.py b/test/builtin/arithmetic/test_abs.py index b22f47049..c523bf1d3 100644 --- a/test/builtin/arithmetic/test_abs.py +++ b/test/builtin/arithmetic/test_abs.py @@ -4,10 +4,48 @@ """ from test.helper import check_evaluation +import pytest -def test_abs(): - for str_expr, str_expected in [ - ("Abs[a - b]", "Abs[a - b]"), - ("Abs[Sqrt[3]]", "Sqrt[3]"), - ]: - check_evaluation(str_expr, str_expected) + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("Abs[a - b]", "Abs[a - b]", None), + ("Abs[Sqrt[3]]", "Sqrt[3]", None), + ("Abs[Sqrt[3]/5]", "Sqrt[3]/5", None), + ("Abs[-2/3]", "2/3", None), + ("Abs[2+3 I]", "Sqrt[13]", None), + ("Abs[2.+3 I]", "3.60555", None), + ("Abs[Undefined]", "Undefined", None), + ("Abs[E]", "E", None), + ("Abs[Pi]", "Pi", None), + ("Abs[Conjugate[x]]", "Abs[x]", None), + ("Abs[4^(2 Pi)]", "4^(2 Pi)", None), + ], +) +def test_abs(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("Sign[a - b]", "Sign[a - b]", None), + ("Sign[Sqrt[3]]", "1", None), + ("Sign[0]", "0", None), + ("Sign[0.]", "0", None), + ("Sign[(1 + I)]", "(1/2 + I/2)Sqrt[2]", None), + ("Sign[(1. + I)]", "(0.707107 + 0.707107 I)", None), + ("Sign[(1 + I)/Sqrt[2]]", "(1 + I)/Sqrt[2]", None), + ("Sign[(1 + I)/Sqrt[2.]]", "(0.707107 + 0.707107 I)", None), + ("Sign[-2/3]", "-1", None), + ("Sign[2+3 I]", "(2 + 3 I)/(13^(1/2))", None), + ("Sign[2.+3 I]", "0.5547 + 0.83205 I", None), + ("Sign[4^(2 Pi)]", "1", None), + # FIXME: add rules to handle this kind of case + # ("Sign[I^(2 Pi)]", "I^(2 Pi)", None), + # ("Sign[4^(2 Pi I)]", "1", None), + ], +) +def test_sign(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) diff --git a/test/builtin/arithmetic/test_basic.py b/test/builtin/arithmetic/test_basic.py new file mode 100644 index 000000000..d99b0b9dc --- /dev/null +++ b/test/builtin/arithmetic/test_basic.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.arithmetic.basic +""" +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("1. + 2. + 3.", "6.", None), + ("1 + 2/3 + 3/5", "34 / 15", None), + ("1 - 2/3 + 3/5", "14 / 15", None), + ("1. - 2/3 + 3/5", "0.933333", None), + ("1 - 2/3 + 2 I", "1 / 3 + 2 I", None), + ("1. - 2/3 + 2 I", "0.333333 + 2. I", None), + ( + "a + 2 a + 3 a q", + "3 a + 3 a q", + "WMA do not collect the common factor `a` in the last expression neither", + ), + ("a - 2 a + 3 a q", "-a + 3 a q", None), + ("a - (5+ a+ 2 b) + 3 a q", "-5 + 3 a q - 2 b", "WMA distribute the sign (-)"), + ( + "a - 2 (5+ a+ 2 b) + 3 a q", + "a + 3 a q - 2 (5 + a + 2 b)", + "WMA do not distribute neither in the general case", + ), + ], +) +def test_add(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg, hold_expected=True) + + +@pytest.mark.parametrize( + ( + "str_expr", + "str_expected", + ), + [ + ("E^(3+I Pi)", "-E ^ 3"), + ("E^(I Pi/2)", "I"), + ("E^1", "E"), + ("log2=Log[2.]; E^log2", "2."), + ("log2=Log[2.]; Chop[E^(log2+I Pi)]", "-2."), + ("log2=.; E^(I Pi/4)", "E ^ (I / 4 Pi)"), + ("E^(.25 I Pi)", "0.707107 + 0.707107 I"), + ], +) +def test_exponential(str_expr, str_expected): + check_evaluation(str_expr, str_expected, hold_expected=True) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("1. 2. 3.", "6.", None), + ("1 * 2/3 * 3/5", "2 / 5", None), + ("1 (- 2/3) ( 3/5)", "-2 / 5", None), + ("1. (- 2/3) ( 3 / 5)", "-0.4", None), + ("1 (- 2/3) (2 I)", "-4 I / 3", None), + ("1. (- 2/3) (2 I)", "0. - 1.33333 I", None), + ("a ( 2 a) ( 3 a q)", "6 a ^ 3 q", None), + ("a (- 2 a) ( 3 Sqrt[a] q)", "-6 a ^ (5 / 2) q", None), + ( + "a (5+ a+ 2 b) (3 a q)", + "3 a ^ 2 q (5 + a + 2 b)", + "WMA distribute the sign (-)", + ), + ( + "a (- 2 (5+ a+ 2 b)) * (3 a q)", + "-6 a ^ 2 q (5 + a + 2 b)", + "WMA do not distribute neither in the general case", + ), + ( + "a b a^2 / (2 a)^(3/2)", + "Sqrt[2] a ^ (3 / 2) b / 4", + "WMA do not distribute neither in the general case", + ), + ( + "a b a^2 / (a)^(3/2)", + "a ^ (3 / 2) b", + "WMA do not distribute neither in the general case", + ), + ( + "a b a^2 / (a b)^(3/2)", + "a ^ 3 b / (a b) ^ (3 / 2)", + "WMA do not distribute neither in the general case", + ), + ( + "a b a ^ 2 (a b)^(-3 / 2)", + "a ^ 3 b / (a b) ^ (3 / 2)", + "Goes to the previous case because of the rule in Power", + ), + ( + "a b Infinity", + "a b Infinity", + "Goes to the previous case because of the rule in Power", + ), + ( + "a b 0 * Infinity", + "Indeterminate", + "Goes to the previous case because of the rule in Power", + ), + ( + "a b ComplexInfinity", + "ComplexInfinity", + "Goes to the previous case because of the rule in Power", + ), + ], +) +def test_multiply(str_expr, str_expected, msg): + check_evaluation( + str_expr, + str_expected, + failure_message=msg, + hold_expected=True, + to_string_expr=True, + ) + + +@pytest.mark.skip("DirectedInfinity Precedence needs going over") +@pytest.mark.parametrize( + ( + "str_expr", + "str_expected", + "msg", + ), + [ + ( + "a b DirectedInfinity[1. + 2. I]", + "a b ((0.447214 + 0.894427 I) Infinity)", + "symbols times floating point complex directed infinity", + ), + ("a b DirectedInfinity[I]", "a b (I Infinity)", ""), + ( + "a b (-1 + 2 I) Infinity", + "a b ((-1 / 5 + 2 I / 5) Sqrt[5] Infinity)", + "symbols times algebraic exact factor times infinity", + ), + ( + "a b (-1 + 2 Pi I) Infinity", + "a b (Infinity (-1 + 2 I Pi) / Sqrt[1 + 4 Pi ^ 2])", + "complex irrational exact", + ), + ( + "a b DirectedInfinity[(1 + 2 I)/ Sqrt[5]]", + "a b ((1 / 5 + 2 I / 5) Sqrt[5] Infinity)", + "symbols times algebraic complex directed infinity", + ), + ("a b DirectedInfinity[q]", "a b (q Infinity)", ""), + # Failing tests + # Problem with formatting. Parenthezise are missing... + # ("a b DirectedInfinity[-I]", "a b (-I Infinity)", ""), + # ("a b DirectedInfinity[-3]", "a b (-Infinity)", ""), + ], +) +def test_directed_infinity_precedence(str_expr, str_expected, msg): + check_evaluation( + str_expr, + str_expected, + failure_message=msg, + hold_expected=True, + to_string_expr=True, + ) + + +@pytest.mark.parametrize( + ( + "str_expr", + "str_expected", + "msg", + ), + [ + ("2^0", "1", None), + ("(2/3)^0", "1", None), + ("2.^0", "1.", None), + ("2^1", "2", None), + ("(2/3)^1", "2 / 3", None), + ("2.^1", "2.", None), + ("2^(3)", "8", None), + ("(1/2)^3", "1 / 8", None), + ("2^(-3)", "1 / 8", None), + ("(1/2)^(-3)", "8", None), + ("(-7)^(5/3)", "-7 (-7) ^ (2 / 3)", None), + ("3^(1/2)", "Sqrt[3]", None), + # WMA do not rationalize numbers + ("(1/5)^(1/2)", "Sqrt[5] / 5", None), + # WMA do not rationalize numbers + ("(3)^(-1/2)", "Sqrt[3] / 3", None), + ("(1/3)^(-1/2)", "Sqrt[3]", None), + ("(5/3)^(1/2)", "Sqrt[5 / 3]", None), + ("(5/3)^(-1/2)", "Sqrt[3 / 5]", None), + ("1/Sqrt[Pi]", "1 / Sqrt[Pi]", None), + ("I^(2/3)", "(-1) ^ (1 / 3)", None), + # In WMA, the next test would return ``-(-I)^(2/3)`` + # which is less compact and elegant... + # ("(-I)^(2/3)", "(-1) ^ (-1 / 3)", None), + ("(2+3I)^3", "-46 + 9 I", None), + ("(1.+3. I)^.6", "1.46069 + 1.35921 I", None), + ("3^(1+2 I)", "3 ^ (1 + 2 I)", None), + ("3.^(1+2 I)", "-1.75876 + 2.43038 I", None), + ("3^(1.+2 I)", "-1.75876 + 2.43038 I", None), + # In WMA, the following expression returns + # ``(Pi/3)^I``. By now, this is handled by + # sympy, which produces the result + ("(3/Pi)^(-I)", "(3 / Pi) ^ (-I)", None), + # Association rules + # ('(a^"w")^2', 'a^(2 "w")', "Integer power of a power with string exponent"), + ('(a^2)^"w"', '(a ^ 2) ^ "w"', None), + ('(a^2)^"w"', '(a ^ 2) ^ "w"', None), + ("(a^2)^(1/2)", "Sqrt[a ^ 2]", None), + ("(a^(1/2))^2", "a", None), + ("(a^(1/2))^2", "a", None), + ("(a^(3/2))^3.", "(a ^ (3 / 2)) ^ 3.", None), + # ("(a^(1/2))^3.", "a ^ 1.5", "Power associativity rational, real"), + # ("(a^(.3))^3.", "a ^ 0.9", "Power associativity for real powers"), + ("(a^(1.3))^3.", "(a ^ 1.3) ^ 3.", None), + # Exponentials involving expressions + ("(a^(p-2 q))^3", "a ^ (3 p - 6 q)", None), + ("(a^(p-2 q))^3.", "(a ^ (p - 2 q)) ^ 3.", None), + ], +) +def test_power(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) diff --git a/test/builtin/arithmetic/test_element.py b/test/builtin/arithmetic/test_element.py new file mode 100644 index 000000000..ad89fb1d2 --- /dev/null +++ b/test/builtin/arithmetic/test_element.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.arithmetic.Element +""" +from test.helper import check_evaluation + +import pytest + +test_set = "{elem, {1, 2, 4, 1.3, Pi, a, True, Sqrt[5]-2, Sin[3]}}" + +domains = { + "Integers": ( + "{True, True, True, False, False, " "Element[a, Integers], False, False, False}" + ), + "Primes": ( + "{False, True, False, False, False, " "Element[a, Primes], False, False, False}" + ), + "Rationals": ( + "{True, True, True, Element[1.3, Rationals], False, " + "Element[a, Rationals], False, False, False}" + ), + "Reals": ( + "{True, True, True, True, True, " "Element[a, Reals], False, True, True}" + ), + "Complexes": ( + "{True, True, True, True, True, " "Element[a, Complexes], False, True, True}" + ), + "Algebraics": ( + "{True, True, True, Element[1.3, Algebraics], False, " + "Element[a, Algebraics], False, True, False}" + ), + "Booleans": ( + "{False, False, False, False, False, " + "Element[a, Booleans], True, False, False}" + ), +} + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ( + f"Table[Element[elem, {key}], {test_set}]", + domains[key], + key, + ) + for key in domains + ], +) +def test_element(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) diff --git a/test/builtin/atomic/test_numbers.py b/test/builtin/atomic/test_numbers.py index 9039abc63..da4d17a16 100644 --- a/test/builtin/atomic/test_numbers.py +++ b/test/builtin/atomic/test_numbers.py @@ -4,9 +4,15 @@ In particular, RealDigits[] and N[] """ - from test.helper import check_evaluation +import pytest + +from mathics.core.number import MACHINE_PRECISION_VALUE, ZERO_MACHINE_ACCURACY + +ZERO_MACHINE_ACCURACY_STR = str(ZERO_MACHINE_ACCURACY) +DEFAUT_ACCURACY_10_STR = str(MACHINE_PRECISION_VALUE - 1) + def test_realdigits(): for str_expr, str_expected in ( @@ -177,43 +183,125 @@ def test_n(): check_evaluation(str_expr, str_expected) -def test_accuracy(): - for str_expr, str_expected in ( - ("0`4", "0"), - ("Accuracy[0.0]", "15."), - ("Accuracy[0.000000000000000000000000000000000000]", "36."), - ("Accuracy[-0.0]", "15."), - # In WMA, this gives 36. Seems to be a rounding issue - # ("Accuracy[-0.000000000000000000000000000000000000]", "36."), - ("1.0000000000000000 // Accuracy", "15."), - ("1.00000000000000000 // Accuracy", "17."), - # Returns the accuracy of ```2.4``` - (" 0.4 + 2.4 I // Accuracy", "14.6198"), - ("Accuracy[2 + 3 I]", "Infinity"), - ('Accuracy["abc"]', "Infinity"), +@pytest.mark.parametrize( + ("str_expr", "str_expected"), + [ + # Accuracy for 0 + ("0", "Infinity"), + ("0.", ZERO_MACHINE_ACCURACY_STR), + ("0.00", ZERO_MACHINE_ACCURACY_STR), + ("0.00`", ZERO_MACHINE_ACCURACY_STR), + ("0.00`2", ZERO_MACHINE_ACCURACY_STR), + ("0.00`20", ZERO_MACHINE_ACCURACY_STR), + ("0.00000000000000000000", "20."), + ("0.``2", "2."), + ("0.``20", "20."), + ("-0.`2", ZERO_MACHINE_ACCURACY_STR), + ("-0.`20", ZERO_MACHINE_ACCURACY_STR), + ("-0.``2", "2."), + ("-0.``20", "20."), + # Now for non-zero numbers + ("10", "Infinity"), + ("10.", DEFAUT_ACCURACY_10_STR), + ("10.00", DEFAUT_ACCURACY_10_STR), + ("10.00`", DEFAUT_ACCURACY_10_STR), + ("10.00`2", "1."), + ("10.00`20", "19."), + ("10.00000000000000000000", "20."), + ("10.``2", "2."), + ("10.``20", "20."), + # For some reason, the following test + # would fail in WMA + ("1. I", "Accuracy[1.]"), + (" 0.4 + 2.4 I", "$MachinePrecision-Log[10, Abs[.4+2.4 I]]"), + ("2 + 3 I", "Infinity"), + ('"abc"', "Infinity"), # Returns the accuracy of ``` 3.2`3 ``` - ('Accuracy[F["a", 2, 3.2`3]]', "2.49482"), - ('Accuracy[{{a, 2, 3.2`},{2.1`5, 3.2`3, "a"}}]', "2.49482"), - # Another case of issues with rounding. In Mathics, this returns - # 2.67776 - # ('Accuracy[{{a, 2, 3.2`},{2.1``3, 3.2``5, "a"}}]', '3.'), - ): - check_evaluation(str_expr, str_expected) + ('F["a", 2, 3.2`3]', "Accuracy[3.2`3]"), + ("F[1.3, Pi, A]", "15.8406"), + ('{{a, 2, 3.2`},{2.1`5, 3.2`3, "a"}}', "Accuracy[3.2`3]"), + ('{{a, 2, 3.2`},{2.1``3, 3.2``5, "a"}}', "Accuracy[2.1``3]"), + ("{1, 0.}", ZERO_MACHINE_ACCURACY_STR), + ("{1, 0.``5}", "5."), + ], +) +def test_accuracy(str_expr, str_expected): + check_evaluation(f"Accuracy[{str_expr}]", str_expected) -def test_precision(): - for str_expr, str_expected in ( - ("0`4", "0"), - ("Precision[0.0]", "MachinePrecision"), - ("Precision[0.000000000000000000000000000000000000]", "0."), - ("Precision[-0.0]", "MachinePrecision"), - ("Precision[-0.000000000000000000000000000000000000]", "0."), - ("1.0000000000000000 // Precision", "MachinePrecision"), - ("1.00000000000000000 // Precision", "17."), - (" 0.4 + 2.4 I // Precision", "MachinePrecision"), - ("Precision[2 + 3 I]", "Infinity"), - ('Precision["abc"]', "Infinity"), - ('Precision[F["a", 2, 3.2`3]]', "3."), - ('Precision[{{a,2,3.2`},{2.1`5, 2.`3, "a"}}]', "3."), - ): - check_evaluation(str_expr, str_expected) +@pytest.mark.parametrize( + ("str_expr", "str_expected"), + [ + # Precision for 0 + ("0", "Infinity"), + ("0.", "MachinePrecision"), + ("0.00", "MachinePrecision"), + ("0.00`", "MachinePrecision"), + ("0.00`2", "MachinePrecision"), + ("0.00`20", "MachinePrecision"), + ("0.00000000000000000000", "0."), + ("0.``2", "0."), + ("0.``20", "0."), + ("-0.`2", "MachinePrecision"), + ("-0.`20", "MachinePrecision"), + ("-0.``2", "0."), + ("-0.``20", "0."), + # Now for non-zero numbers + ("10", "Infinity"), + ("10.", "MachinePrecision"), + ("10.00", "MachinePrecision"), + ("10.00`", "MachinePrecision"), + ("10.00`2", "2."), + ("10.00`20", "20."), + ("10.00000000000000000000", "21."), + ("10.``2", "3."), + ("10.``20", "21."), + # Returns the precision of ```2.4``` + (" 0.4 + 2.4 I", "MachinePrecision"), + ("2 + 3 I", "Infinity"), + ('"abc"', "Infinity"), + # Returns the precision of ``` 3.2`3 ``` + ('F["a", 2, 3.2`3]', "3."), + ('{{a, 2, 3.2`},{2.1`5, 3.2`3, "a"}}', "3."), + ('{{a, 2, 3.2`},{2.1``3, 3.2``5, "a"}}', "3."), + ("{1, 0.}", "MachinePrecision"), + ("{1, 0.``5}", "0."), + ], +) +def test_precision(str_expr, str_expected): + check_evaluation(f"Precision[{str_expr}]", str_expected) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + ("N[Sqrt[2], 41]//Precision", "41.", "first round sqrt[2`41]"), + ("N[Sqrt[2], 40]//Precision", "40.", "first round sqrt[2`40]"), + ("N[Sqrt[2], 41]//Precision", "41.", "second round sqrt[2`41]"), + ("N[Sqrt[2], 40]//Precision", "40.", "second round sqrt[2`40]"), + ( + "N[Sqrt[2], 41]", + '"1.4142135623730950488016887242096980785697"', + "third round sqrt[2`41]", + ), + ( + "Precision/@Table[N[Pi,p],{p, {5, 100, MachinePrecision, 20}}]", + "{5., 100., MachinePrecision, 20.}", + None, + ), + ( + "Precision/@Table[N[Sin[1],p],{p, {5, 100, MachinePrecision, 20}}]", + "{5., 100., MachinePrecision, 20.}", + None, + ), + ("N[Sqrt[2], 40]", '"1.414213562373095048801688724209698078570"', None), + ("N[Sqrt[2], 4]", '"1.414"', None), + ("N[Pi, 40]", '"3.141592653589793238462643383279502884197"', None), + ("N[Pi, 4]", '"3.142"', None), + ("N[Pi, 41]", '"3.1415926535897932384626433832795028841972"', None), + ("N[Sqrt[2], 41]", '"1.4142135623730950488016887242096980785697"', None), + ], +) +def test_change_prec(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) diff --git a/test/builtin/box/test_custom_boxexpression.py b/test/builtin/box/test_custom_boxexpression.py index 0223a19d0..9ac6116fb 100644 --- a/test/builtin/box/test_custom_boxexpression.py +++ b/test/builtin/box/test_custom_boxexpression.py @@ -4,6 +4,7 @@ from mathics.builtin.box.expression import BoxExpression from mathics.builtin.graphics import GRAPHICS_OPTIONS from mathics.core.attributes import A_HOLD_ALL, A_PROTECTED, A_READ_PROTECTED +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import Symbol @@ -41,7 +42,7 @@ class CustomAtom(Predefined): "N[System`CustomAtom]": "37", } - def apply_to_boxes(self, evaluation): + def eval_to_boxes(self, evaluation): "System`MakeBoxes[System`CustomAtom, StandardForm|TraditionalForm|OutputForm|InputForm]" return CustomBoxExpression(evaluation=evaluation) @@ -60,7 +61,7 @@ def init(self, *elems, **options): def to_expression(self): return Expression(SymbolCustomGraphicsBox, *self.elements) - def apply_box(self, expr, evaluation, options): + def eval_box(self, expr, evaluation: Evaluation, options: dict): """System`MakeBoxes[System`Graphics[System`expr_, System`OptionsPattern[System`Graphics]], System`StandardForm|System`TraditionalForm|System`OutputForm]""" instance = CustomGraphicsBox(*(expr.elements), evaluation=evaluation) diff --git a/test/builtin/colors/test_colors.py b/test/builtin/colors/test_colors.py index 1bde91131..9b13723ce 100755 --- a/test/builtin/colors/test_colors.py +++ b/test/builtin/colors/test_colors.py @@ -319,7 +319,7 @@ def testConversions(self): def testImageConversions(self): # test that f([x, y, ...]) = [f(x), f(y), ...] for rectangular image arrays. - for name, convert in colors.conversions.items(): + for name, convert in colors.CONVERSIONS.items(): if name.find("CMYK") < 0: self._checkImageConversion( 4, lambda p: vectorize(p, 1, lambda q: stacked(convert, q)) diff --git a/test/builtin/drawing/test_image.py b/test/builtin/drawing/test_image.py index 9107f046e..eb778d69c 100644 --- a/test/builtin/drawing/test_image.py +++ b/test/builtin/drawing/test_image.py @@ -9,14 +9,13 @@ import pytest -from mathics.builtin.base import check_requires_list from mathics.core.symbols import SymbolNull # Note we test with tif, jpg, and gif. Add others? image_tests = [ - ('lena = Import["ExampleData/lena.tif"];', None, ""), - ("BinaryImageQ[lena]", "False", ""), - ("BinaryImageQ[Binarize[lena]]", "True", ""), + ('hedy = Import["ExampleData/hedy.tif"];', None, ""), + ("BinaryImageQ[hedy]", "False", ""), + ("BinaryImageQ[Binarize[hedy]]", "True", ""), ( """ein = Import["ExampleData/Einstein.jpg"]; ImageDimensions[ein]""", "{615, 768}", diff --git a/test/builtin/image/__init__.py b/test/builtin/image/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/test/builtin/image/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/test/builtin/image/test_image_colors.py b/test/builtin/image/test_image_colors.py new file mode 100644 index 000000000..a8377d3a6 --- /dev/null +++ b/test/builtin/image/test_image_colors.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.image.colors + +Largely tests error messages when parameters are incorrect. +""" +from test.helper import check_evaluation, session + +import pytest + +img = session.evaluate('img = Import["ExampleData/hedy.tif"]') + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "assert_failure_msg"), + [ + # FIXME: Setting "img" above in session sometimes fails. So do it again. + ('img = Import["ExampleData/hedy.tif"];', "Null", ""), + ( + """Binarize["a"]""", + """Binarize[a]""", + "imginv: Expecting an image instead of a.", + ), + ( + """Binarize[1, 3]""", + """Binarize[1, 3]""", + "imginv: Expecting an image instead of 1.", + ), + ( + """Binarize[img, I]""", + """Binarize[-Image-, I]""", + ( + "arg2: The argument I should be a real number or a pair of " + "real numbers." + ), + ), + ], +) +def test_binarize(str_expr, str_expected, assert_failure_msg): + check_evaluation( + str_expr, str_expected, hold_expected=True, failure_message=assert_failure_msg + ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "assert_failure_msg"), + [ + ( + """ColorQuantize[2, 0]""", + """ColorQuantize[2, 0]""", + "imginv: Expecting an image instead of 2.", + ), + ( + "ColorQuantize[img, I]", + "ColorQuantize[-Image-, I]", + ( + "intp: Positive integer expected at position 2 in " + "ColorQuantize[-Image-, I]" + ), + ), + ( + "ColorQuantize[img, -1]", + "ColorQuantize[-Image-, -1]", + ( + "intp: Positive integer expected at position 2 in " + "ColorQuantize[-Image-, -1]" + ), + ), + ], +) +def test_color_quantize(str_expr, str_expected, assert_failure_msg): + check_evaluation( + str_expr, str_expected, hold_expected=True, failure_message=assert_failure_msg + ) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "assert_failure_msg"), + [ + ( + "ColorSeparate[1]", + "ColorSeparate[1]", + "imginv: Expecting an image instead of 1.", + ), + ], +) +def test_color_separate(str_expr, str_expected, assert_failure_msg): + check_evaluation( + str_expr, str_expected, hold_expected=True, failure_message=assert_failure_msg + ) diff --git a/test/builtin/list/constructing.py b/test/builtin/list/constructing.py new file mode 100644 index 000000000..e974ab38e --- /dev/null +++ b/test/builtin/list/constructing.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.list.constructing +""" +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "failure_message"), + [ + ( + "Table[x, {x,0,1/3}]", + "{0}", + None, + ), + ( + "Table[x, {x, -0.2, 3.9}]", + "{-0.2, 0.8, 1.8, 2.8, 3.8}", + None, + ), + ], +) +def test_array(str_expr, str_expected, failure_message): + check_evaluation(str_expr, str_expected, failure_message) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "failure_message"), + [ + ( + "Array[f, {2, 3}, {1, 2, 3}]", + "Array[f, {2, 3}, {1, 2, 3}]", + "plen: {2, 3} and {1, 2, 3} should have the same length.", + ), + ( + "Array[f, a]", + "Array[f, a]", + "ilsnn: Single or list of non-negative integers expected at position 2.", + ), + ( + "Array[f, 2, b]", + "Array[f, 2, b]", + "ilsnn: Single or list of non-negative integers expected at position 3.", + ), + ], +) +def test_range(str_expr, str_expected, failure_message): + check_evaluation(str_expr, str_expected, failure_message) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "failure_message"), + [ + ( + "Table[x, {x,0,1/3}]", + "{0}", + None, + ), + ( + "Table[x, {x, -0.2, 3.9}]", + "{-0.2, 0.8, 1.8, 2.8, 3.8}", + None, + ), + ], +) +def test_table(str_expr, str_expected, failure_message): + check_evaluation(str_expr, str_expected, failure_message) diff --git a/test/builtin/matrix/__init__.py b/test/builtin/matrix/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/test/builtin/matrix/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/test/builtin/matrix/constrmatrix.py b/test/builtin/matrix/constrmatrix.py new file mode 100644 index 000000000..67773d638 --- /dev/null +++ b/test/builtin/matrix/constrmatrix.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.matrix.constrmatrix +""" +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "failure_message"), + [ + ( + """BoxMatrix["a"]""", + """BoxMatrix["a"]""", + "notre: The first argument must be a non-complex number or a list of" + " noncomplex numbers.", + ), + ], +) +def test_boxmatrix(str_expr, str_expected, failure_message): + check_evaluation(str_expr, str_expected, failure_message) diff --git a/test/builtin/numbers/test_hyperbolic.py b/test/builtin/numbers/test_hyperbolic.py new file mode 100644 index 000000000..78e4288a4 --- /dev/null +++ b/test/builtin/numbers/test_hyperbolic.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.numbers.hyperbolic + +These simple verify various rules from +from symja_android_library/symja_android_library/rules/Gudermannian.m +""" +from test.helper import check_evaluation + + +def test_gudermannian(): + for str_expr, str_expected in ( + ("Gudermannian[Undefined]", "Undefined"), + ("Gudermannian[0]", "0"), + ("Gudermannian[2 Pi I]", "0"), + # FIXME: Mathics can't handle Rule substitution + ("Gudermannian[6/4 Pi I]", "DirectedInfinity[-I]"), + ("Gudermannian[Infinity]", "Pi/2"), + # FIXME: rule does not work + ("Gudermannian[-Infinity]", "-Pi/2"), + ("Gudermannian[ComplexInfinity]", "Indeterminate"), + # FIXME Tanh[1 / 2] doesn't eval but Tanh[0.5] does + ("Gudermannian[z]", "2 ArcTan[Tanh[z / 2]]"), + ): + check_evaluation(str_expr, str_expected) diff --git a/test/builtin/numbers/test_trig.py b/test/builtin/numbers/test_trig.py new file mode 100644 index 000000000..5aee15cb7 --- /dev/null +++ b/test/builtin/numbers/test_trig.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.numbers.trig + +For this to work we also make use of rules from +mathics/autoload/rules/trig.m +""" +from test.helper import check_evaluation + + +def test_ArcCos(): + for str_expr, str_expected in ( + ("ArcCos[I Infinity]", "-I Infinity"), + ("ArcCos[-I Infinity]", "I Infinity"), + ("ArcCos[0]", "1/2 Pi"), + ("ArcCos[1/2]", "1/3 Pi"), + ("ArcCos[-1/2]", "2/3 Pi"), + ("ArcCos[1/2 Sqrt[2]]", "1/4 Pi"), + ("ArcCos[-1/2 Sqrt[2]]", "3/4 Pi"), + ("ArcCos[1/2 Sqrt[3]]", "1/6 Pi"), + ("ArcCos[-1/2 Sqrt[3]]", "5/6 Pi"), + ("ArcCos[(1 + Sqrt[3]) / (2*Sqrt[2])]", "1/12 Pi"), + ): + check_evaluation(str_expr, str_expected) diff --git a/test/builtin/specialfns/__init__.py b/test/builtin/specialfns/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/test/builtin/specialfns/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/test/builtin/specialfns/test_bessel.py b/test/builtin/specialfns/test_bessel.py new file mode 100644 index 000000000..b6201d76e --- /dev/null +++ b/test/builtin/specialfns/test_bessel.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Unit tests for mathics.builtins.arithmetic.bessel +""" +from test.helper import check_evaluation + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "assert_failure_msg"), + # Basically special rules from autoload/rules/Bessel.m that are not covered + # by SymPy. + [ + ( + "BesselI[1/2,z]", + "Sqrt[2] Sinh[z] / (Sqrt[z] Sqrt[Pi])", + "BesselI 1/2 rule", + ), + ( + "BesselI[-1/2,z]", + "Sqrt[2] Cosh[z] / (Sqrt[z] Sqrt[Pi])", + "BesselI -1/2 rule", + ), + ("BesselJ[-1/2,z]", "Sqrt[2] Cos[z] / (Sqrt[z] Sqrt[Pi])", "BesselJ -1/2 rule"), + ("BesselJ[1/2,z]", "Sqrt[2] Sin[z] / (Sqrt[z] Sqrt[Pi])", "BesselJ 1/2 rule"), + ], +) +def test_add(str_expr, str_expected, assert_failure_msg): + check_evaluation( + str_expr, str_expected, hold_expected=True, failure_message=assert_failure_msg + ) diff --git a/test/builtin/test_scoping.py b/test/builtin/test_scoping.py new file mode 100644 index 000000000..d0a1251f7 --- /dev/null +++ b/test/builtin/test_scoping.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +Unit tests from mathics.builtin.scoping. +""" + +from test.helper import session + +from mathics.core.symbols import Symbol + + +def test_unique(): + """ + test Unique + """ + + # test Unique[] + symbol = session.evaluate("Unique[]") + assert isinstance( + symbol, Symbol + ), f"Unique[] should return a Symbol; got {type(symbol)}" + symbol_set = set([symbol]) + for i in range(5): + symbol = session.evaluate("Unique[]") + assert ( + symbol not in symbol_set + ), "Unique[] should return different symbols; {symbol.name} is duplicated" + symbol_set.add(symbol) + + # test Unique[] + symbol_prefix = symbol.name[0] + + for i in range(5): + symbol = session.evaluate(f"Unique[{symbol_prefix}]") + assert ( + symbol not in symbol_set + ), "Unique[{symbol_prefix}] should return different symbols; {symbol.name} is duplicated" diff --git a/test/consistency-and-style/test_duplicate_builtins.py b/test/consistency-and-style/test_duplicate_builtins.py index b052c15d3..9a592a756 100644 --- a/test/consistency-and-style/test_duplicate_builtins.py +++ b/test/consistency-and-style/test_duplicate_builtins.py @@ -34,13 +34,13 @@ def test_check_duplicated(): builtins_by_name.get(name, None) is None ), f"{name} defined in {module} already defined in {builtins_by_name[name]}." """ - if builtins_by_name.get(name, None) is not None: - print( - f"\n{name} defined in {module} already defined in {builtins_by_name[name]}." - ) - msg = ( - msg - + f"\n{name} defined in {module} already defined in {builtins_by_name[name]}." - ) + # if builtins_by_name.get(name, None) is not None: + # print( + # f"\n{name} defined in {module} already defined in {builtins_by_name[name]}." + # ) + # msg = ( + # msg + # + f"\n{name} defined in {module} already defined in {builtins_by_name[name]}." + # ) builtins_by_name[name] = module assert msg == "", msg diff --git a/test/consistency-and-style/test_summary_text.py b/test/consistency-and-style/test_summary_text.py index 8c0429233..2c8eacf23 100644 --- a/test/consistency-and-style/test_summary_text.py +++ b/test/consistency-and-style/test_summary_text.py @@ -184,6 +184,8 @@ def test_summary_text_available(module_name): """ grammar_OK = True module = modules[module_name] + if hasattr(module, "no_doc") and module.no_doc is True: + return vars = dir(module) for name in vars: var = name_is_builtin_symbol(module, name) diff --git a/mathics/builtin/pymathics.py b/test/core/convert/__init__.py similarity index 100% rename from mathics/builtin/pymathics.py rename to test/core/convert/__init__.py diff --git a/test/core/convert/mpmath.py b/test/core/convert/mpmath.py new file mode 100644 index 000000000..2ca95b30b --- /dev/null +++ b/test/core/convert/mpmath.py @@ -0,0 +1,61 @@ +from mpmath import mpc, mpf +from sympy import Float as SympyFloat + +from mathics.core.atoms import ( + Complex, + Integer0, + Integer1, + IntegerM1, + MachineReal, + PrecisionReal, + Rational, + Real, +) +from mathics.core.convert.mpmath import from_mpmath +from mathics.core.expression import Expression +from mathics.core.systemsymbols import SymbolDirectedInfinity, SymbolIndeterminate + + +def test_infinity(): + vals = [ + (mpf("+inf"), Expression(SymbolDirectedInfinity, Integer1)), + (mpf("-inf"), Expression(SymbolDirectedInfinity, IntegerM1)), + ( + mpc(1.0, "inf"), + Expression(SymbolDirectedInfinity, Complex(Integer0, Integer1)), + ), + ( + mpc(1.0, "-inf"), + Expression(SymbolDirectedInfinity, Complex(Integer0, IntegerM1)), + ), + (mpc("inf", 1), Expression(SymbolDirectedInfinity, Integer1)), + (mpc("-inf", 1), Expression(SymbolDirectedInfinity, IntegerM1)), + (mpf("nan"), SymbolIndeterminate), + ] + for val_in, val_out in vals: + print([val_in, val_out, from_mpmath(val_in)]) + assert val_out.sameQ(from_mpmath(val_in)) + + +def test_from_to_mpmath(): + vals = [ + (Integer1, MachineReal(1.0)), + (Rational(1, 3), MachineReal(1.0 / 3.0)), + (MachineReal(1.2), MachineReal(1.2)), + (PrecisionReal(SympyFloat(1.3, 10)), PrecisionReal(SympyFloat(1.3, 10))), + (PrecisionReal(SympyFloat(1.3, 30)), PrecisionReal(SympyFloat(1.3, 30))), + (Complex(Integer1, IntegerM1), Complex(Integer1, IntegerM1)), + (Complex(Integer1, Real(-1.0)), Complex(Integer1, Real(-1.0))), + (Complex(Real(1.0), Real(-1.0)), Complex(Real(1.0), Real(-1.0))), + ( + Complex(MachineReal(1.0), PrecisionReal(SympyFloat(-1.0, 10))), + Complex(MachineReal(1.0), PrecisionReal(SympyFloat(-1.0, 10))), + ), + ( + Complex(MachineReal(1.0), PrecisionReal(SympyFloat(-1.0, 30))), + Complex(MachineReal(1.0), PrecisionReal(SympyFloat(-1.0, 30))), + ), + ] + for val1, val2 in vals: + print((val1, val2)) + assert val2.sameQ(from_mpmath(val1.to_mpmath())) diff --git a/test/core/test_expression.py b/test/core/test_expression.py index cb568e173..3878de520 100644 --- a/test/core/test_expression.py +++ b/test/core/test_expression.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- -from test.helper import check_evaluation +from test.helper import check_evaluation, evaluate_value import pytest from mathics.builtin.base import check_requires_list +from mathics.core.expression import Expression +from mathics.core.symbols import Symbol, SymbolPlus, SymbolTimes # FIXME: come up with an example that doesn't require skimage. @@ -25,3 +27,35 @@ def test_canonical_sort(): r"SortBy[Table[IntegerDigits[2^n], {n, 10}], First]", r"{{1, 6}, {1, 2, 8}, {1, 0, 2, 4}, {2}, {2, 5, 6}, {3, 2}, {4}, {5, 1, 2}, {6, 4}, {8}}", ) + + +def test_Expression_sameQ(): + """ + Test Expression.SameQ + """ + symbolX = Symbol("X") + expr_plus = Expression(SymbolPlus, symbolX, Symbol("Y")) + assert ( + expr_plus.sameQ(expr_plus) == True + ), "should pass when head and elements are the same" + + assert ( + expr_plus.sameQ(symbolX) == False + ), "should fail because 'other' in Expression.SameQ() is not an Expression" + + expr_times = Expression(SymbolTimes, symbolX, Symbol("Y")) + + assert ( + expr_plus.sameQ(expr_times) == False + ), "should fail when Expression head's mismatch" + + expr_plus_copy = Expression(SymbolPlus, symbolX, Symbol("Y")) + assert ( + expr_plus.sameQ(expr_plus_copy) == True + ), "should pass when Expressions are different Python objects, but otherwise the same" + + # Try where we compare and expression with something that contains itself + nested_expr = Expression(SymbolPlus, expr_plus) + assert ( + nested_expr.sameQ(expr_plus) == False + ), "should fail when one expression has the other embedded in it" diff --git a/test/core/test_pickle.py b/test/core/test_pickle.py new file mode 100644 index 000000000..0e02ad857 --- /dev/null +++ b/test/core/test_pickle.py @@ -0,0 +1,33 @@ +import io +import pickle + +import pytest + +from mathics.builtin.box.graphics import GraphicsBox +from mathics.core.atoms import Integer, MachineReal, PrecisionReal, String +from mathics.core.expression import Expression +from mathics.core.symbols import Atom, Symbol, strip_context +from mathics.core.systemsymbols import SymbolGet + +test_elements = { + "Symbol": Symbol("System`A"), + "Expression": Expression(Symbol("Global`F"), Symbol("Global`x")), + "NestedExpression": Expression( + Symbol("Global`F"), Expression(Symbol("Global`F"), Symbol("Global`x")) + ), + "Integer": Integer(37), + "String": String("hello world"), + "MachineReal": MachineReal("3.2"), + "PrecisionReal": PrecisionReal("3.2"), + "GraphicsBox": GraphicsBox(), +} + + +def test_pickle_elements(): + for key, val in test_elements.items(): + print(key) + file_dump = io.BytesIO(b"") + pickle.dump(val, file_dump) + file_load = io.BytesIO(file_dump.getvalue()) + load_val = pickle.load(file_load) + assert val.sameQ(load_val) diff --git a/test/core/test_rules.py b/test/core/test_rules.py new file mode 100644 index 000000000..87f93ab8d --- /dev/null +++ b/test/core/test_rules.py @@ -0,0 +1,182 @@ +from test.helper import check_evaluation, evaluate_value + +import pytest + +""" +In WL, pattern matching and rule application is dependent on the evaluation context. +This happens at two levels. On the one hand, patterns like `PatternTest[pat, test]` +matches with `expr` depending both on the `pat` and the result of the evaluation of `test`. + +On the other hand, attributes like `Orderless` or `Flat` in the head of the pattern +also affects how patterns are applied to expressions. However, in WMA, the effect +of these parameters are established in the point in which a rule is created, and not +when it is applied. + +For example, if we execute in WMA: + +``` +In[1]:= rule = Q[a, _Symbol, _Integer]->True; SetAttributes[Q, {Orderless}]; Q[a,1,b]/.rule +Out[1]=Q[1, a, b] +``` +the application fails because it does not take into account the `Orderless` attribute, because +the rule was created *before* the attribute is set. +On the other hand, +``` +In[2]:=SetAttributes[Q, {Orderless}]; rule = Q[a, _Symbol, _Integer]->True; Attributes[Q]={}; Q[a,1,b]/.rule +Out[2]= True +``` +because it ignores that the attribute is clean at the time in which the rule is applied. + + +In Mathics, on the other hand, attributes are taken into accout just at the moment of the replacement, +so the output of both expressions are the opposite. + + +This set of tests are proposed to drive the behaviour of Rules in Mathics closer to the one in WMA. +In particular, the way in which `Orderless` and `Flat` attributes affects evaluation are currently tested. + +For the case of `Flat`, there is still another issue in Mathics, since by not it is not taken into account +at the pattern matching level. For example, in WMA, + +``` +In[3]:=SetAttributes[Q,{Flat}]; rule=Q[a,_Integer]->True; Q[a,1,b]/.rule +Out[3]=Q[True, b] +``` + +The xfail mark can be removed once these issues get fixed. +""" + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + ( + "rule = Q[a, _Symbol, _Integer]->True;\ + ruled = Dispatch[{rule}];\ + {Q[a,1,b], Q[a,1,b]/.rule, Q[a,1,b]/.ruled}", + "{Q[a,1,b], Q[a,1,b], Q[a,1,b]}", + "1. Check the rules. Here are not applied.", + ), + ( + "SetAttributes[Q, {Orderless}];\ + {Q[a,1,b], Q[a,1,b]/.rule, Q[a, 1, b]/.ruled}", + "{Q[1, a, b], Q[a, 1, b], Q[a, 1, b]}", + "2. Set the attribute. Application is not affected.", + ), + ( + "rule = Q[a, _Symbol, _Integer]->True;\ + ruled = Dispatch[{rule}];\ + {Q[a, 1, b], Q[a, 1, b]/.rule, Q[a, 1, b]/.ruled}", + "{Q[1, a, b], True, True}", + "3 .Rebuilt rules. Rules applied.", + ), + ( + "Attributes[Q] = {};\ + {Q[a, 1, b], Q[a, 1, b]/.rule, Q[a, 1, b]/.ruled}", + "{Q[a, 1, b], True, True}", + "4. Unset the attribute. Application is not affected.", + ), + ( + "rule = Q[a, _Symbol, _Integer]->True;\ + ruled = Dispatch[{rule}];\ + {Q[a, 1, b], Q[a, 1, b]/.rule, Q[a, 1, b]/.ruled}", + "{Q[a, 1, b], Q[a, 1, b], Q[a, 1, b]}", + "5. Rebuilt rules. Rules applied.", + ), + (None, None, None), + ], +) +@pytest.mark.xfail +def test_orderless_on_rules(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + ( + "rule = Q[_Integer,_Symbol]->True;\ + ruled = Dispatch[{rule}];\ + {Q[a,1,b]/.rule, Q[a,1,b]/.ruled}", + "{Q[a,1,b],Q[a,1,b]}", + "1. Check the rules. Here are not applied.", + ), + ( + "SetAttributes[Q, {Flat}];\ + {Q[a,1,b]/.rule, Q[a,1,b]/.ruled}", + "{Q[a,1,b], Q[a,1,b]}", + "2. Set the attribute. Application is not affected.", + ), + ( + "rule = Q[_Integer,_Symbol]->True;\ + ruled = Dispatch[{rule}];\ + {Q[a, 1, b]/.rule, Q[a, 1, b]/.ruled}", + "{Q[a, True],Q[a, True]}", + "3 .Rebuilt rules. Rules applied.", + ), + ( + "Attributes[Q] = {};\ + {Q[a,1,b]/.rule, Q[a,1,b]/.ruled}", + "{Q[a,1,b], Q[a,1,b]}", + "4. Unset the attribute. Application is not affected.", + ), + ( + "rule = Q[a, _Integer,_Symbol]->True;\ + ruled = Dispatch[{rule}];\ + {Q[a,1,b]/.rule, Q[a,1,b]/.ruled}", + "{Q[a, 1, b],Q[a, 1, b]}", + "5. Rebuilt rules. Rules applied.", + ), + (None, None, None), + ], +) +@pytest.mark.xfail +def test_flat_on_rules(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + (None, None, None), + ( + "rule = Q[x_,y_.]->{x, y};\ + ruled = Dispatch[{rule}];\ + {Q[a]/.rule, Q[a]/.ruled}", + "{Q[a],Q[a]}", + "1. Check the rules. Here are not applied.", + ), + ( + "Default[Q]=37;\ + {Q[a]/.rule, Q[a]/.ruled}", + "{Q[a], Q[a]}", + "2. Set the Default value. Application is not affected.", + ), + ( + "rule = Q[x_,y_.]->{x,y};\ + ruled = Dispatch[{rule}];\ + {Q[a]/.rule, Q[a]/.ruled}", + "{{a, 37}, {a, 37}}", + "3 .Rebuilt rules. Rules applied.", + ), + ( + "Default[Q] = .;\ + {Q[a]/.rule, Q[a]/.ruled}", + "{{a, 37}, {a, 37}}", + "4. Unset the attribute. Application is not affected.", + ), + ( + "rule = Q[x_,y_.]->{x,y};\ + ruled = Dispatch[{rule}];\ + {Q[a]/.rule, Q[a]/.ruled}", + "{Q[a],Q[a]}", + "5. Rebuilt rules. Rules not applied.", + ), + (None, None, None), + ], +) +@pytest.mark.xfail +def test_default_optional_on_rules(str_expr, str_expected, msg): + check_evaluation(str_expr, str_expected, failure_message=msg) diff --git a/test/format/test_format.py b/test/format/test_format.py index 5d3539399..161ebc5df 100644 --- a/test/format/test_format.py +++ b/test/format/test_format.py @@ -560,12 +560,12 @@ "System`InputForm": "Graphics[{}]", "System`OutputForm": "-Graphics-", }, - "mathml": { - "System`StandardForm": '', - "System`TraditionalForm": '', - "System`InputForm": "Graphics [ { } ]", - "System`OutputForm": '', - }, + # "mathml": { + # "System`StandardForm": '', + # "System`TraditionalForm": '', + # "System`InputForm": "Graphics [ { } ]", + # "System`OutputForm": '', + # }, "latex": { "System`StandardForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(5.8333cm, 5.8333cm);\n\n\nclip(box((-1,-1), (1,1)));\n\n\\end{asy}\n', "System`TraditionalForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(5.8333cm, 5.8333cm);\n\n\nclip(box((-1,-1), (1,1)));\n\n\\end{asy}\n', @@ -581,17 +581,17 @@ "System`InputForm": "Graphics[{Text[Power[a, b], {0, 0}]}]", "System`OutputForm": "-Graphics-", }, - "mathml": { - "System`StandardForm": '', - "System`TraditionalForm": '', - "System`InputForm": "Graphics [ { Text [ Power [ a b ] { 0 0 } ] } ]", - "System`OutputForm": '', - }, + # "mathml": { + # "System`StandardForm": '', + # "System`TraditionalForm": '', + # "System`InputForm": "Graphics [ { Text [ Power [ a b ] { 0 0 } ] } ]", + # "System`OutputForm": '', + # }, "latex": { - "System`StandardForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(4.9cm, 5.8333cm);\n\n// InsetBox\nlabel("$a^b$", (147.0,175.0), (0,0), rgb(0, 0, 0));\n\nclip(box((136.5,162.5), (157.5,187.5)));\n\n\\end{asy}\n', - "System`TraditionalForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(4.9cm, 5.8333cm);\n\n// InsetBox\nlabel("$a^b$", (147.0,175.0), (0,0), rgb(0, 0, 0));\n\nclip(box((136.5,162.5), (157.5,187.5)));\n\n\\end{asy}\n', + "System`StandardForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(4.9cm, 5.8333cm);\n\n// InsetBox\nlabel("$a^b$", (147.0,175.0), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((136.5,162.5), (157.5,187.5)));\n\n\\end{asy}\n', + "System`TraditionalForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(4.9cm, 5.8333cm);\n\n// InsetBox\nlabel("$a^b$", (147.0,175.0), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((136.5,162.5), (157.5,187.5)));\n\n\\end{asy}\n', "System`InputForm": "\\text{Graphics}\\left[\\left\\{\\text{Text}\\left[\\text{Power}\\left[a, b\\right], \\left\\{0, 0\\right\\}\\right]\\right\\}\\right]", - "System`OutputForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(4.9cm, 5.8333cm);\n\n// InsetBox\nlabel("$a^b$", (147.0,175.0), (0,0), rgb(0, 0, 0));\n\nclip(box((136.5,162.5), (157.5,187.5)));\n\n\\end{asy}\n', + "System`OutputForm": '\n\\begin{asy}\nusepackage("amsmath");\nsize(4.9cm, 5.8333cm);\n\n// InsetBox\nlabel("$a^b$", (147.0,175.0), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((136.5,162.5), (157.5,187.5)));\n\n\\end{asy}\n', }, }, "TableForm[{Graphics[{Text[a^b,{0,0}]}], Graphics[{Text[a^b,{0,0}]}]}]": { @@ -602,17 +602,17 @@ "System`InputForm": "TableForm[{Graphics[{Text[Power[a, b], {0, 0}]}], Graphics[{Text[Power[a, b], {0, 0}]}]}]", "System`OutputForm": "-Graphics-\n\n-Graphics-\n", }, - "mathml": { - "System`StandardForm": '\n\n\n', - "System`TraditionalForm": '\n\n\n', - "System`InputForm": "TableForm [ { Graphics [ { Text [ Power [ a b ] { 0 0 } ] } ] Graphics [ { Text [ Power [ a b ] { 0 0 } ] } ] } ]", - "System`OutputForm": '\n\n\n', - }, + # "mathml": { + # "System`StandardForm": '\n\n\n', + # "System`TraditionalForm": '\n\n\n', + # "System`InputForm": "TableForm [ { Graphics [ { Text [ Power [ a b ] { 0 0 } ] } ] Graphics [ { Text [ Power [ a b ] { 0 0 } ] } ] } ]", + # "System`OutputForm": '\n\n\n', + # }, "latex": { - "System`StandardForm": '\\begin{array}{c} \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\\\ \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\end{array}', - "System`TraditionalForm": '\\begin{array}{c} \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\\\ \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\end{array}', + "System`StandardForm": '\\begin{array}{c} \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\\\ \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\end{array}', + "System`TraditionalForm": '\\begin{array}{c} \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\\\ \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\end{array}', "System`InputForm": "\\text{TableForm}\\left[\\left\\{\\text{Graphics}\\left[\\left\\{\\text{Text}\\left[\\text{Power}\\left[a, b\\right], \\left\\{0, 0\\right\\}\\right]\\right\\}\\right], \\text{Graphics}\\left[\\left\\{\\text{Text}\\left[\\text{Power}\\left[a, b\\right], \\left\\{0, 0\\right\\}\\right]\\right\\}\\right]\\right\\}\\right]", - "System`OutputForm": '\\begin{array}{c} \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\\\ \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\end{array}', + "System`OutputForm": '\\begin{array}{c} \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\\\ \n\\begin{asy}\nusepackage("amsmath");\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel("$a^b$", (73.5,87.5), (0,0), rgb(0, 0, 0)+fontsize(3));\n\nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\end{array}', }, }, } @@ -626,22 +626,38 @@ def load_tests(key): """ global all_tests global MATHML_STRICT + + def is_fragile(assert_msg: str) -> bool: + """ + Return True if assert_msg indicates we have a fragile test, and False otherwise + """ + return assert_msg.endswith("Fragile!") + mandatory_tests = [] fragile_tests = [] for expr in all_test: base_msg = all_test[expr]["msg"] - expected_fmt = all_test[expr][key] - fragil_set = len(base_msg) > 8 and base_msg[-8:] == "Fragile!" + expected_fmt = all_test[expr].get(key, None) + test_is_fragile = is_fragile(base_msg) + + # Some fragile tests have been commented out. + # If we have a fragile test where the output has not + # been adjusted, then skip it. + # + if expected_fmt is None: + assert is_fragile(base_msg) + continue + for form in expected_fmt: tst = expected_fmt[form] - fragile = fragil_set + form_is_fragile = test_is_fragile must_be = False if not isinstance(tst, str): tst, extra_msg = tst if len(extra_msg) > 7 and extra_msg[:7] == "must be": must_be = True - elif len(extra_msg) > 8 and extra_msg[-8:] == "Fragile!": - fragile = True + elif is_fragile(extra_msg): + form_is_fragile = True msg = base_msg + " - " + extra_msg else: msg = base_msg @@ -649,9 +665,9 @@ def load_tests(key): # discard Fragile for "text", "latex" or if # MATHML_STRICT is True if key != "mathml" or MATHML_STRICT: - fragile = False + form_is_fragile = False full_test = (expr, tst, Symbol(form), msg) - if fragile or must_be: + if form_is_fragile or must_be: fragile_tests.append(full_test) else: mandatory_tests.append(full_test) diff --git a/test/helper.py b/test/helper.py index c7f7b2344..025dd3ca4 100644 --- a/test/helper.py +++ b/test/helper.py @@ -37,28 +37,34 @@ def check_evaluation( Helper function to test Mathics expression against its results - Compares the expressions represented by ``str_expr`` and ``str_expected`` by evaluating - the first, and optionally, the second. + Compares the expressions represented by ``str_expr`` and ``str_expected`` by + evaluating the first, and optionally, the second. - to_string_expr: If ``True`` (default value) the result of the evaluation is converted - into a Python string. Otherwise, the expression is kept as an Expression - object. If this argument is set to ``None``, the session is reset. + to_string_expr: If ``True`` (default value) the result of the evaluation is + converted into a Python string. Otherwise, the expression is kept + as an Expression object. + If this argument is set to ``None``, the session is reset. - failure_message (str): message shown in case of failure - hold_expected (bool): If ``False`` (default value) the ``str_expected`` is evaluated. Otherwise, - the expression is considered literally. + failure_message: message shown in case of failure + hold_expected: If ``False`` (default value) the ``str_expected`` is evaluated. + Otherwise, the expression is considered literally. to_string_expected: If ``True`` (default value) the expected expression is - evaluated and then converted to a Python string. result of the evaluation is converted - into a Python string. If ``False``, the expected expression is kept as an Expression object. - - to_python_expected: If ``True``, and ``to_string_expected`` is ``False``, the result of evaluating ``str_expr`` - is compared against the result of the evaluation of ``str_expected``, converted into a - Python object. - - expected_messages ``Optional[tuple[str]]``: If a tuple of strings are passed into this parameter, messages and prints raised during - the evaluation of ``str_expr`` are compared with the elements of the list. If ``None``, this comparison - is ommited. + evaluated and then converted to a Python string. result of the + evaluation is converted into a Python string. + If ``False``, the expected expression is kept as an Expression + object. + + to_python_expected: If ``True``, and ``to_string_expected`` is ``False``, the result + of evaluating ``str_expr``is compared against the result of the + evaluation of ``str_expected``, converted into a + Python object. + + expected_messages: If a tuple of strings are passed into + this parameter, messages and prints raised during + the evaluation of ``str_expr`` are compared with the elements of + the list. If ``None``, this comparison + is omitted. """ if str_expr is None: reset_session() diff --git a/test/package/test_combinatorica.py b/test/package/test_combinatorica.py index 2c104050f..d2c267d0e 100644 --- a/test/package/test_combinatorica.py +++ b/test/package/test_combinatorica.py @@ -382,12 +382,11 @@ def test_special_classes_of_permutations_1_4(): "{0, 1, 2, 9, 44, 265, 1854, 14833, 133496, 1334961}", "NumberOfDerangements; 1.4.2, Page 33", ), - # This works, interactively, but not in test. Why? - # ( - # "Table[ N[ NumberOfDerangements[i]/(i!) ], {i, 1, 10} ]", - # "{0., 0.5, 0.333333, 0.375, 0.366667, 0.368056, 0.367857, 0.367882, 0.367879, 0.367879}", - # "Confused Secretary 1.4.2, Page 34", - # ), + ( + "Table[ N[ NumberOfDerangements[i]/(i!) ], {i, 1, 10} ]", + "{0., 0.5, 0.333333, 0.375, 0.366667, 0.368056, 0.367857, 0.367882, 0.367879, 0.367879}", + "Confused Secretary 1.4.2, Page 34", + ), ( "Table[Round[n!/N[E]], {n, 1, 10}]", "{0, 1, 2, 9, 44, 265, 1854, 14833, 133496, 1334961}", @@ -496,7 +495,10 @@ def test_2_1_to_2_3(): for str_expr, str_expected, message in ( ( - # 2.1.1 - 2.1.3 are broken + # 2.1.1 uses Partitions which is broken + # 2.1.2 Ferrers Diagrams can't be tested easily and robustly here + # easily + # 2.1.3 uses Partitions which is broken "PartitionsP[10]", "NumberOfPartitions[10]", "Counting Partitions 2.1.4, Page 57", @@ -509,12 +511,12 @@ def test_2_1_to_2_3(): ( "TableauQ[{{1,2,5}, {3,4,5}, {6}}]", "True", - "Young Tableau 2.3, Page 63", + "Young Tableau 2.3, Page 64", ), ( "TableauQ[{{1,2,5,9,10}, {5,4,7,13}, {4,8,12},{11}}]", "False", - "Young Tableau 2.3, Page 63", + "Young Tableau 2.3, Page 64", ), # Need to not evaluate expected which reformats \n's # ( diff --git a/test/test_evaluation.py b/test/test_evaluation.py index 96ffdae43..daaba75ba 100644 --- a/test/test_evaluation.py +++ b/test/test_evaluation.py @@ -3,7 +3,7 @@ import pytest -from .helper import check_evaluation, evaluate +from .helper import check_evaluation, evaluate, session @pytest.mark.parametrize( @@ -200,37 +200,55 @@ def test_system_specific_long_integer(): Close[str]; res] """ ) - for str_expr, str_expected, message in ( + + test_input_and_name = ( ( - r'WRb[{1885507541, 4157323149}, Table["UnsignedInteger32", {2}]]', - r"{213, 143, 98, 112, 141, 183, 203, 247}", + 'WRb[{1885507541, 4157323149}, Table["UnsignedInteger32", {2}]]', "UnsignedInteger32", ), ( - r'WRb[{384206740, 1676316040}, Table["UnsignedInteger32", {2}]]', - r"{148, 135, 230, 22, 136, 141, 234, 99}", + 'WRb[{384206740, 1676316040}, Table["UnsignedInteger32", {2}]]', "UnsignedInteger32 - 2nd test", ), ( - r'WRb[7079445437368829279, "UnsignedInteger64"]', - r"{95, 5, 33, 229, 29, 62, 63, 98}", + 'WRb[7079445437368829279, "UnsignedInteger64"]', "UnsignedInteger64", ), ( - r'WRb[5381171935514265990, "UnsignedInteger64"]', - r"{134, 9, 161, 91, 93, 195, 173, 74}", + 'WRb[5381171935514265990, "UnsignedInteger64"]', "UnsignedInteger64 - 2nd test", ), ( - r'WRb[293382001665435747348222619884289871468, "UnsignedInteger128"]', - r"{108, 78, 217, 150, 88, 126, 152, 101, 231, 134, 176, 140, 118, 81, 183, 220}", + 'WRb[293382001665435747348222619884289871468, "UnsignedInteger128"]', "UnsignedInteger128", ), ( - r'WRb[253033302833692126095975097811212718901, "UnsignedInteger128"]', - r"{53, 83, 116, 79, 81, 100, 60, 126, 202, 52, 241, 48, 5, 113, 92, 190}", + 'WRb[253033302833692126095975097811212718901, "UnsignedInteger128"]', "UnsignedInteger128 - 2nd test", ), + ) + + is_little_endian = session.evaluate("$ByteOrdering").value == -1 + if is_little_endian: + expected = ( + "{213, 143, 98, 112, 141, 183, 203, 247}", + "{148, 135, 230, 22, 136, 141, 234, 99}", + "{95, 5, 33, 229, 29, 62, 63, 98}", + "{134, 9, 161, 91, 93, 195, 173, 74}", + "{108, 78, 217, 150, 88, 126, 152, 101, 231, 134, 176, 140, 118, 81, 183, 220}", + "{53, 83, 116, 79, 81, 100, 60, 126, 202, 52, 241, 48, 5, 113, 92, 190}", + ) + else: + expected = ( + "{112, 98, 143, 213, 247, 203, 183, 141}", + "{22, 230, 135, 148, 99, 234, 141, 136}", + "{98, 63, 62, 29, 229, 33, 5, 95}", + "{74, 173, 195, 93, 91, 161, 9, 134}", + "{101, 152, 126, 88, 150, 217, 78, 108, 220, 183, 81, 118, 140, 176, 134, 231}", + "{126, 60, 100, 81, 79, 116, 83, 53, 190, 92, 113, 5, 48, 241, 52, 202}", + ) + + for i, (str_expr, message) in enumerate(test_input_and_name): # This works but the $Precision is coming out UnsignedInt128 rather tha # UnsignedInt32 # ( @@ -240,9 +258,7 @@ def test_system_specific_long_integer(): # " {-0.0832756, 0.765142, 0.638454}}", # "Eigenvalues via mpmath", # ), - ): - - check_evaluation(str_expr, str_expected, message) + check_evaluation(str_expr, expected[i], message) # import os.path as osp diff --git a/test/test_evaluators.py b/test/test_evaluators.py index 69fed3a7e..6257b2544 100644 --- a/test/test_evaluators.py +++ b/test/test_evaluators.py @@ -2,7 +2,7 @@ import pytest -from mathics.eval.nevaluator import eval_N, eval_nvalues +from mathics.eval.nevaluator import eval_N, eval_NValues from mathics.eval.numerify import numerify as eval_numerify from mathics.session import MathicsSession @@ -57,10 +57,10 @@ def test_eval_N(str_expr, prec, str_expected): "str_expr, prec, str_expected, setup", [ ("1", "$MachinePrecision", "1.000000000", None), - # eval_nvalues does not call `evaluate` over the input expression. So + # eval_NValues does not call `evaluate` over the input expression. So # 2/9 is not evaluated to a Rational number, but kept as a division. ("2/9", "$MachinePrecision", "2.000000`5*9.0000000000`5^(-1.`)", None), - # eval_nvalues does not call `evaluate` at the end neither. So + # eval_NValues does not call `evaluate` at the end neither. So # Sqrt[2]->Sqrt[2.0`] ("Sqrt[2]", "$MachinePrecision", "Sqrt[2.0`]", None), ("Pi", "$MachinePrecision", "3.141592653589793`15", None), @@ -77,13 +77,13 @@ def test_eval_N(str_expr, prec, str_expected): ("F[b, 2/9]", "5", "F[1.20`3, 2.*9.^(-1.`)]", "N[b,_]=1.2`3"), ], ) -def test_eval_nvalues(str_expr, prec, str_expected, setup): +def test_eval_NValues(str_expr, prec, str_expected, setup): if setup: session.evaluate(setup) expr_in = session.evaluate(f"Hold[{str_expr}]").elements[0] prec = session.evaluate(prec) expr_expected = session.evaluate(f"Hold[{str_expected}]").elements[0] - result = eval_nvalues(expr_in, prec, evaluation) + result = eval_NValues(expr_in, prec, evaluation) session.evaluate("ClearAll[a,b,c]") assert expr_expected.sameQ(result) @@ -95,7 +95,7 @@ def test_eval_nvalues(str_expr, prec, str_expected, setup): ("{1, 1.}", "{1, 1.}", None), ("{1.000123`6, 1.0001`4, 2/9}", "{1.000123`6, 1.0001`4, .22222`4}", None), ("F[1.000123`6, 1.0001`4, 2/9]", "F[1.000123`6, 1.0001`4, .22222`4]", None), - # eval_nvalues does not call `evaluate` over the input expression. So + # eval_NValues does not call `evaluate` over the input expression. So # 2/9 is not evaluated to a Rational number, but kept as a division. ("2/9", "2 * 9 ^ (-1)", None), ("Sqrt[2]", "Sqrt[2]", None),