├── .clang-format ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── asciidtype ├── .clang-format ├── .flake8 ├── .gitignore ├── README.md ├── asciidtype │ ├── __init__.py │ ├── scalar.py │ └── src │ │ ├── asciidtype_main.c │ │ ├── casts.c │ │ ├── casts.h │ │ ├── dtype.c │ │ ├── dtype.h │ │ ├── umath.c │ │ └── umath.h ├── meson.build ├── pyproject.toml ├── reinstall.sh └── tests │ ├── conftest.py │ └── test_asciidtype.py ├── asv_benchmarks ├── .gitignore ├── asv.conf.json └── benchmarks │ ├── __init__.py │ └── strings.py ├── metadatadtype ├── .clang-format ├── .flake8 ├── .gitignore ├── README.md ├── meson.build ├── metadatadtype │ ├── __init__.py │ ├── scalar.py │ └── src │ │ ├── casts.c │ │ ├── casts.h │ │ ├── dtype.c │ │ ├── dtype.h │ │ ├── metadatadtype_main.c │ │ ├── umath.c │ │ └── umath.h ├── pyproject.toml ├── reinstall.sh └── tests │ ├── conftest.py │ └── test_metadatadtype.py ├── mpfdtype ├── .gitignore ├── README.md ├── meson.build ├── mpfdtype │ ├── __init__.py │ ├── src │ │ ├── casts.cpp │ │ ├── casts.h │ │ ├── dtype.c │ │ ├── dtype.h │ │ ├── mpfdtype_main.c │ │ ├── numbers.cpp │ │ ├── numbers.h │ │ ├── ops.hpp │ │ ├── scalar.c │ │ ├── scalar.h │ │ ├── terrible_hacks.c │ │ ├── terrible_hacks.h │ │ ├── umath.cpp │ │ └── umath.h │ └── tests │ │ ├── conftest.py │ │ ├── test_array.py │ │ └── test_scalar.py ├── pyproject.toml └── reinstall.sh ├── quaddtype ├── README.md ├── meson.build ├── numpy_quaddtype │ ├── __init__.py │ └── src │ │ ├── casts.cpp │ │ ├── casts.h │ │ ├── dragon4.c │ │ ├── dragon4.h │ │ ├── dtype.c │ │ ├── dtype.h │ │ ├── ops.hpp │ │ ├── quad_common.h │ │ ├── quaddtype_main.c │ │ ├── scalar.c │ │ ├── scalar.h │ │ ├── scalar_ops.cpp │ │ ├── scalar_ops.h │ │ ├── umath.cpp │ │ └── umath.h ├── pyproject.toml ├── reinstall.sh └── tests │ └── test_quaddtype.py ├── stringdtype ├── .clang-format ├── .gitignore ├── README.md ├── meson.build ├── pyproject.toml ├── reinstall.sh ├── stringdtype │ ├── __init__.py │ ├── scalar.py │ └── src │ │ ├── casts.c │ │ ├── casts.h │ │ ├── dtype.c │ │ ├── dtype.h │ │ ├── main.c │ │ ├── static_string.c │ │ ├── static_string.h │ │ ├── umath.c │ │ └── umath.h └── tests │ ├── conftest.py │ └── test_stringdtype.py └── unytdtype ├── .clang-format ├── .gitignore ├── README.md ├── meson.build ├── pyproject.toml ├── reinstall.sh ├── tests ├── conftest.py └── test_unytdtype.py └── unytdtype ├── __init__.py ├── scalar.py └── src ├── casts.c ├── casts.h ├── dtype.c ├── dtype.h ├── umath.c ├── umath.h └── unytdtype_main.c /.clang-format: -------------------------------------------------------------------------------- 1 | # A clang-format style that approximates Python's PEP 7 2 | # Useful for IDE integration 3 | # 4 | # Based on Paul Ganssle's version at 5 | # https://gist.github.com/pganssle/0e3a5f828b4d07d79447f6ced8e7e4db 6 | # and modified for NumPy 7 | BasedOnStyle: Google 8 | AlignAfterOpenBracket: Align 9 | AllowShortEnumsOnASingleLine: false 10 | AllowShortIfStatementsOnASingleLine: false 11 | AlwaysBreakAfterReturnType: TopLevel 12 | BreakBeforeBraces: Stroustrup 13 | ColumnLimit: 100 14 | ContinuationIndentWidth: 8 15 | DerivePointerAlignment: false 16 | IndentWidth: 4 17 | IncludeBlocks: Regroup 18 | IncludeCategories: 19 | - Regex: '^[<"](Python|structmember|pymem)\.h' 20 | Priority: -3 21 | CaseSensitive: true 22 | - Regex: '^"numpy/' 23 | Priority: -2 24 | - Regex: '^"(npy_pycompat|npy_config)' 25 | Priority: -1 26 | - Regex: '^"[[:alnum:]_.]+"' 27 | Priority: 1 28 | - Regex: '^<[[:alnum:]_.]+"' 29 | Priority: 2 30 | Language: Cpp 31 | PointerAlignment: Right 32 | ReflowComments: true 33 | SpaceBeforeParens: ControlStatements 34 | SpacesInParentheses: false 35 | StatementMacros: [PyObject_HEAD, PyObject_VAR_HEAD, PyObject_HEAD_EXTRA] 36 | TabWidth: 4 37 | UseTab: Never 38 | SortIncludes: Never 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Numpy User DTypes CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup Python 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: "3.10" 20 | - name: Install build and test dependencies 21 | run: | 22 | python -m pip install -U pip build pytest unyt wheel meson ninja meson-python patchelf pandas numpy 23 | - name: Install asciidtype 24 | working-directory: asciidtype 25 | run: | 26 | CFLAGS="-Werror" python -m pip install . --no-build-isolation 27 | - name: Run asciidtype tests 28 | working-directory: asciidtype 29 | run: | 30 | pytest -vvv --color=yes 31 | - name: Install metadatadtype 32 | working-directory: metadatadtype 33 | run: | 34 | python -m build --no-isolation --wheel -Cbuilddir=build 35 | find ./dist/*.whl | xargs python -m pip install 36 | - name: Run metadatadtype tests 37 | working-directory: metadatadtype 38 | run: | 39 | pytest -vvv --color=yes 40 | - name: install mpfdtype 41 | working-directory: mpfdtype 42 | run: | 43 | sudo apt install libmpfr-dev -y 44 | CFLAGS="-Werror" python -m pip install . --no-build-isolation 45 | - name: Run mpfdtype tests 46 | working-directory: mpfdtype 47 | run: | 48 | pytest -vvv --color=yes 49 | - name: Install unytdtype 50 | working-directory: unytdtype 51 | run: | 52 | python -m build --no-isolation --wheel -Cbuilddir=build 53 | find ./dist/*.whl | xargs python -m pip install 54 | - name: Run unytdtype tests 55 | working-directory: unytdtype 56 | run: | 57 | pytest -vvv --color=yes 58 | - name: Install quaddtype dependencies 59 | run: | 60 | sudo apt-get update 61 | sudo apt-get install -y libmpfr-dev libssl-dev libfftw3-dev 62 | - name: Install SLEEF 63 | run: | 64 | git clone https://github.com/shibatch/sleef.git 65 | cd sleef 66 | cmake -S . -B build -DSLEEF_BUILD_QUAD:BOOL=ON -DSLEEF_BUILD_SHARED_LIBS:BOOL=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON 67 | cmake --build build/ --clean-first -j 68 | sudo cmake --install build --prefix /usr 69 | - name: Install quaddtype 70 | working-directory: quaddtype 71 | run: | 72 | LDFLAGS="-Wl,-rpath,/usr/lib" python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -Csetup-args="-Dbuildtype=debug" 73 | - name: Run quaddtype tests 74 | working-directory: quaddtype 75 | run: | 76 | pytest -vvv --color=yes 77 | - name: Install stringdtype 78 | working-directory: stringdtype 79 | run: | 80 | if [ -d "build/" ] 81 | then 82 | rm -r build 83 | fi 84 | meson setup build 85 | python -m build --no-isolation --wheel -Cbuilddir=build --config-setting='compile-args=-v' -Csetup-args="-Dbuildtype=debug" 86 | find ./dist/*.whl | xargs python -m pip install 87 | - name: Run stringdtype tests 88 | working-directory: stringdtype 89 | run: | 90 | pytest -s -vvv --color=yes 91 | pip uninstall -y pandas 92 | pytest -s -vvv --color=yes 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .mesonpy-native-file.ini 132 | compile_commands.json 133 | 134 | .ruff-cache/ 135 | .asv 136 | .vscode/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: generate-compilation-database-metadatadtype 5 | name: Generate compilation database [metadatadtype] 6 | files: metadatadtype/(meson\.build$|.*\.(c|h)$) 7 | language: system 8 | require_serial: true 9 | entry: | 10 | bash -c 'cd metadatadtype && mkdir -p build && pip install build meson-python patchelf wheel && meson setup build && python -m build --wheel --no-isolation -Cbuilddir=build'; 11 | fail_fast: false 12 | - id: generate-compilation-database-asciidtype 13 | name: Generate compilation database [asciidtype] 14 | files: asciidtype/(meson\.build$|.*\.(c|h)$) 15 | language: system 16 | require_serial: true 17 | entry: | 18 | bash -c 'cd asciidtype && mkdir -p build && pip install build meson-python patchelf wheel && meson setup build && python -m build --wheel --no-isolation -Cbuilddir=build'; 19 | fail_fast: false 20 | - id: generate-compilation-database-quaddtype 21 | name: Generate compilation database [quaddtype] 22 | files: quaddtype/(meson\.build$|.*\.(c|h)$) 23 | language: system 24 | require_serial: true 25 | entry: | 26 | bash -c 'cd quaddtype && mkdir -p build && pip install build meson-python patchelf wheel && meson setup build && python -m build --wheel --no-isolation -Cbuilddir=build'; 27 | fail_fast: false 28 | - id: generate-compilation-database-unytdtype 29 | name: Generate compilation database [unytdtype] 30 | files: unytdtype/(meson\.build$|.*\.(c|h)$) 31 | language: system 32 | require_serial: true 33 | entry: | 34 | bash -c 'cd unytdtype && mkdir -p build && pip install build meson-python patchelf wheel && meson setup build && python -m build --wheel --no-isolation -Cbuilddir=build'; 35 | fail_fast: false 36 | - id: generate-compilation-database-stringdtype 37 | name: Generate compilation database [stringdtype] 38 | files: stringdtype/(meson\.build$|.*\.(c|h)$) 39 | language: system 40 | require_serial: true 41 | entry: | 42 | bash -c 'cd stringdtype && mkdir -p build && pip install build meson-python patchelf wheel && meson setup build && python -m build --wheel --no-isolation -Cbuilddir=build'; 43 | fail_fast: false 44 | - repo: https://github.com/pocc/pre-commit-hooks 45 | rev: v1.3.5 46 | hooks: 47 | - id: clang-tidy 48 | name: clang-tidy [metadatadtype] 49 | args: [-p=metadatadtype/build] 50 | files: metadatadtype/(.*\.(c|h)$) 51 | - id: clang-tidy 52 | name: clang-tidy [quaddtype] 53 | args: [-p=quaddtype/build] 54 | files: quaddtype/(.*\.(c|h)$) 55 | - id: clang-tidy 56 | name: clang-tidy [unytdtype] 57 | args: [-p=unytdtype/build] 58 | files: unytdtype/(.*\.(c|h)$) 59 | - id: clang-tidy 60 | name: clang-tidy [stringdtype] 61 | args: [-p=stringdtype/build] 62 | files: stringdtype/(.*\.(c|h)$) 63 | - id: clang-format 64 | args: ['--no-diff', -i] 65 | # - id: oclint 66 | # - id: cppcheck 67 | - repo: https://github.com/pre-commit/pre-commit-hooks 68 | rev: v4.4.0 69 | hooks: 70 | - id: trailing-whitespace 71 | - id: end-of-file-fixer 72 | - id: check-yaml 73 | - id: check-added-large-files 74 | - id: check-ast 75 | - repo: https://github.com/charliermarsh/ruff-pre-commit 76 | rev: v0.0.254 77 | hooks: 78 | - id: ruff 79 | - repo: https://github.com/pre-commit/mirrors-prettier 80 | rev: v3.0.0-alpha.6 81 | hooks: 82 | - id: prettier 83 | types: 84 | [ 85 | markdown, 86 | yaml, 87 | ] 88 | - repo: https://github.com/pycqa/isort 89 | rev: 5.12.0 90 | hooks: 91 | - id: isort 92 | name: isort (python) 93 | - id: isort 94 | name: isort (cython) 95 | types: [cython] 96 | - id: isort 97 | name: isort (pyi) 98 | types: [pyi] 99 | - repo: https://github.com/psf/black 100 | rev: 23.1.0 101 | hooks: 102 | - id: black 103 | name: 'black for asciidtype' 104 | files: '^asciidtype/.*\.py' 105 | - id: black 106 | name: 'black for metadatadtype' 107 | files: '^metadatadtype/.*\.py' 108 | - id: black 109 | name: 'black for mpfdtype' 110 | files: '^mpfdtype/.*\.py' 111 | - id: black 112 | name: 'black for quaddtype' 113 | files: '^quaddtype/.*\.py' 114 | - id: black 115 | name: 'black for stringdtype' 116 | files: '^stringdtype/.*\.py' 117 | - id: black 118 | name: 'black for unytdtype' 119 | files: '^unytdtype/.*\.py' 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, NumPy Developers. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the NumPy Developers nor the names of any 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # numpy-user-dtypes 2 | 3 | Repository for development of dtypes making use of the [NEP 4 | 42](https://numpy.org/neps/nep-0042-new-dtypes.html) extensible dtype API. See 5 | the readme files in each example dtype for build instructions. 6 | 7 | These dtypes are not meant for real-world use yet. The dtype API is not 8 | finalized and the dtypes in this repository are still active works in progress. 9 | -------------------------------------------------------------------------------- /asciidtype/.clang-format: -------------------------------------------------------------------------------- 1 | # A clang-format style that approximates Python's PEP 7 2 | # Useful for IDE integration 3 | # 4 | # Based on Paul Ganssle's version at 5 | # https://gist.github.com/pganssle/0e3a5f828b4d07d79447f6ced8e7e4db 6 | # and modified for NumPy 7 | BasedOnStyle: Google 8 | AlignAfterOpenBracket: Align 9 | AllowShortEnumsOnASingleLine: false 10 | AllowShortIfStatementsOnASingleLine: false 11 | AlwaysBreakAfterReturnType: TopLevel 12 | BreakBeforeBraces: Stroustrup 13 | ColumnLimit: 79 14 | ContinuationIndentWidth: 8 15 | DerivePointerAlignment: false 16 | IndentWidth: 4 17 | IncludeBlocks: Regroup 18 | IncludeCategories: 19 | - Regex: '^[<"](Python|structmember|pymem)\.h' 20 | Priority: -3 21 | CaseSensitive: true 22 | - Regex: '^"numpy/' 23 | Priority: -2 24 | - Regex: '^"(npy_pycompat|npy_config)' 25 | Priority: -1 26 | - Regex: '^"[[:alnum:]_.]+"' 27 | Priority: 1 28 | - Regex: '^<[[:alnum:]_.]+"' 29 | Priority: 2 30 | Language: Cpp 31 | PointerAlignment: Right 32 | ReflowComments: true 33 | SpaceBeforeParens: ControlStatements 34 | SpacesInParentheses: false 35 | StatementMacros: [PyObject_HEAD, PyObject_VAR_HEAD, PyObject_HEAD_EXTRA] 36 | TabWidth: 4 37 | UseTab: Never 38 | -------------------------------------------------------------------------------- /asciidtype/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | per-file-ignores = __init__.py:F401 3 | max-line-length = 160 4 | -------------------------------------------------------------------------------- /asciidtype/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .mesonpy*.ini 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /asciidtype/README.md: -------------------------------------------------------------------------------- 1 | # A dtype that stores ASCII data 2 | 3 | This is a simple proof-of-concept dtype using the (as of late 2022) experimental 4 | [new dtype 5 | implementation](https://numpy.org/neps/nep-0041-improved-dtype-support.html) in 6 | NumPy. 7 | 8 | ## Building 9 | 10 | Ensure Meson and NumPy are installed in the python environment you would like to use: 11 | 12 | ``` 13 | $ python3 -m pip install meson meson-python numpy build patchelf 14 | ``` 15 | 16 | Build with meson, create a wheel, and install it 17 | 18 | ``` 19 | $ rm -r dist/ 20 | $ meson build 21 | $ python -m build --wheel -Cbuilddir=build 22 | $ python -m pip install dist/asciidtype*.whl 23 | ``` 24 | 25 | The `mesonpy` build backend for pip [does not currently support editable 26 | installs](https://github.com/mesonbuild/meson-python/issues/47), so `pip install 27 | -e .` will not work. 28 | -------------------------------------------------------------------------------- /asciidtype/asciidtype/__init__.py: -------------------------------------------------------------------------------- 1 | """A dtype for working with ASCII data 2 | 3 | This is an example usage of the experimental new dtype API 4 | in Numpy and is not intended for any real purpose. 5 | """ 6 | 7 | from .scalar import ASCIIScalar # isort: skip 8 | from ._asciidtype_main import ASCIIDType 9 | 10 | __all__ = [ 11 | "ASCIIDType", 12 | "ASCIIScalar", 13 | ] 14 | -------------------------------------------------------------------------------- /asciidtype/asciidtype/scalar.py: -------------------------------------------------------------------------------- 1 | """A scalar type needed by the dtype machinery.""" 2 | 3 | 4 | class ASCIIScalar(str): 5 | def __new__(cls, value, dtype): 6 | instance = super().__new__(cls, value) 7 | instance.dtype = dtype 8 | return instance 9 | 10 | def partition(self, sep): 11 | ret = super().partition(sep) 12 | return (str(ret[0]), str(ret[1]), str(ret[2])) 13 | 14 | def rpartition(self, sep): 15 | ret = super().rpartition(sep) 16 | return (str(ret[0]), str(ret[1]), str(ret[2])) 17 | -------------------------------------------------------------------------------- /asciidtype/asciidtype/src/asciidtype_main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL asciidtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL asciidtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #include "numpy/ndarraytypes.h" 8 | #include "numpy/arrayobject.h" 9 | #include "numpy/ufuncobject.h" 10 | #include "numpy/dtype_api.h" 11 | 12 | #include "dtype.h" 13 | #include "umath.h" 14 | 15 | static struct PyModuleDef moduledef = { 16 | PyModuleDef_HEAD_INIT, 17 | .m_name = "asciidtype_main", 18 | .m_size = -1, 19 | }; 20 | 21 | /* Module initialization function */ 22 | PyMODINIT_FUNC 23 | PyInit__asciidtype_main(void) 24 | { 25 | import_array(); 26 | import_umath(); 27 | 28 | PyObject *m = PyModule_Create(&moduledef); 29 | if (m == NULL) { 30 | return NULL; 31 | } 32 | 33 | PyObject *mod = PyImport_ImportModule("asciidtype"); 34 | if (mod == NULL) { 35 | goto error; 36 | } 37 | ASCIIScalar_Type = 38 | (PyTypeObject *)PyObject_GetAttrString(mod, "ASCIIScalar"); 39 | Py_DECREF(mod); 40 | 41 | if (ASCIIScalar_Type == NULL) { 42 | goto error; 43 | } 44 | 45 | if (init_ascii_dtype() < 0) { 46 | goto error; 47 | } 48 | 49 | if (PyModule_AddObject(m, "ASCIIDType", (PyObject *)&ASCIIDType) < 0) { 50 | goto error; 51 | } 52 | 53 | if (init_ufuncs() == -1) { 54 | goto error; 55 | } 56 | 57 | return m; 58 | 59 | error: 60 | Py_DECREF(m); 61 | return NULL; 62 | } 63 | -------------------------------------------------------------------------------- /asciidtype/asciidtype/src/casts.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_CASTS_H 2 | #define _NPY_CASTS_H 3 | 4 | PyArrayMethod_Spec ** 5 | get_casts(void); 6 | 7 | #endif /* _NPY_CASTS_H */ 8 | -------------------------------------------------------------------------------- /asciidtype/asciidtype/src/dtype.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_DTYPE_H 2 | #define _NPY_DTYPE_H 3 | 4 | typedef struct { 5 | PyArray_Descr base; 6 | long size; 7 | } ASCIIDTypeObject; 8 | 9 | extern PyArray_DTypeMeta ASCIIDType; 10 | extern PyTypeObject *ASCIIScalar_Type; 11 | 12 | ASCIIDTypeObject * 13 | new_asciidtype_instance(long size); 14 | 15 | int 16 | init_ascii_dtype(void); 17 | 18 | #endif /*_NPY_DTYPE_H*/ 19 | -------------------------------------------------------------------------------- /asciidtype/asciidtype/src/umath.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL asciidtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL asciidtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #define NO_IMPORT_ARRAY 8 | #define NO_IMPORT_UFUNC 9 | #include "numpy/ndarraytypes.h" 10 | #include "numpy/arrayobject.h" 11 | #include "numpy/ufuncobject.h" 12 | #include "numpy/dtype_api.h" 13 | 14 | #include "dtype.h" 15 | #include "string.h" 16 | #include "umath.h" 17 | 18 | static int 19 | ascii_add_strided_loop(PyArrayMethod_Context *context, char *const data[], 20 | npy_intp const dimensions[], npy_intp const strides[], 21 | NpyAuxData *NPY_UNUSED(auxdata)) 22 | { 23 | PyArray_Descr *const *descrs = context->descriptors; 24 | long in1_size = ((ASCIIDTypeObject *)descrs[0])->size; 25 | long in2_size = ((ASCIIDTypeObject *)descrs[1])->size; 26 | long out_size = ((ASCIIDTypeObject *)descrs[2])->size; 27 | 28 | npy_intp N = dimensions[0]; 29 | char *in1 = data[0], *in2 = data[1], *out = data[2]; 30 | npy_intp in1_stride = strides[0], in2_stride = strides[1], 31 | out_stride = strides[2]; 32 | 33 | while (N--) { 34 | size_t in1_len = strnlen(in1, in1_size); 35 | size_t in2_len = strnlen(in2, in2_size); 36 | strncpy(out, in1, in1_len); 37 | strncpy(out + in1_len, in2, in2_len); 38 | if (in1_len + in2_len < out_size) { 39 | out[in1_len + in2_len] = '\0'; 40 | } 41 | in1 += in1_stride; 42 | in2 += in2_stride; 43 | out += out_stride; 44 | } 45 | 46 | return 0; 47 | } 48 | 49 | static NPY_CASTING 50 | ascii_add_resolve_descriptors(PyObject *NPY_UNUSED(self), 51 | PyArray_DTypeMeta *dtypes[], 52 | PyArray_Descr *given_descrs[], 53 | PyArray_Descr *loop_descrs[], 54 | npy_intp *NPY_UNUSED(view_offset)) 55 | { 56 | long op1_size = ((ASCIIDTypeObject *)given_descrs[0])->size; 57 | long op2_size = ((ASCIIDTypeObject *)given_descrs[1])->size; 58 | long out_size = op1_size + op2_size; 59 | 60 | /* the input descriptors can be used as-is */ 61 | Py_INCREF(given_descrs[0]); 62 | loop_descrs[0] = given_descrs[0]; 63 | Py_INCREF(given_descrs[1]); 64 | loop_descrs[1] = given_descrs[1]; 65 | 66 | /* create new DType instance to hold the output */ 67 | loop_descrs[2] = (PyArray_Descr *)new_asciidtype_instance(out_size); 68 | if (loop_descrs[2] == NULL) { 69 | return -1; 70 | } 71 | 72 | return NPY_SAFE_CASTING; 73 | } 74 | 75 | int 76 | init_add_ufunc(PyObject *numpy) 77 | { 78 | PyObject *add = PyObject_GetAttrString(numpy, "add"); 79 | if (add == NULL) { 80 | return -1; 81 | } 82 | 83 | /* 84 | * Initialize spec for addition 85 | */ 86 | static PyArray_DTypeMeta *add_dtypes[3] = {&ASCIIDType, &ASCIIDType, 87 | &ASCIIDType}; 88 | 89 | static PyType_Slot add_slots[] = { 90 | {NPY_METH_resolve_descriptors, &ascii_add_resolve_descriptors}, 91 | {NPY_METH_strided_loop, &ascii_add_strided_loop}, 92 | {0, NULL}}; 93 | 94 | PyArrayMethod_Spec AddSpec = { 95 | .name = "ascii_add", 96 | .nin = 2, 97 | .nout = 1, 98 | .dtypes = add_dtypes, 99 | .slots = add_slots, 100 | .flags = 0, 101 | .casting = NPY_SAFE_CASTING, 102 | }; 103 | 104 | /* register ufunc */ 105 | if (PyUFunc_AddLoopFromSpec(add, &AddSpec) < 0) { 106 | Py_DECREF(add); 107 | return -1; 108 | } 109 | Py_DECREF(add); 110 | return 0; 111 | } 112 | 113 | static int 114 | ascii_equal_strided_loop(PyArrayMethod_Context *context, char *const data[], 115 | npy_intp const dimensions[], npy_intp const strides[], 116 | NpyAuxData *NPY_UNUSED(auxdata)) 117 | { 118 | PyArray_Descr *const *descrs = context->descriptors; 119 | long in1_size = ((ASCIIDTypeObject *)descrs[0])->size; 120 | long in2_size = ((ASCIIDTypeObject *)descrs[1])->size; 121 | 122 | npy_intp N = dimensions[0]; 123 | char *in1 = data[0], *in2 = data[1]; 124 | npy_bool *out = (npy_bool *)data[2]; 125 | npy_intp in1_stride = strides[0], in2_stride = strides[1], 126 | out_stride = strides[2]; 127 | 128 | while (N--) { 129 | *out = (npy_bool)1; 130 | char *_in1 = in1; 131 | char *_in2 = in2; 132 | npy_bool *_out = out; 133 | in1 += in1_stride; 134 | in2 += in2_stride; 135 | out += out_stride; 136 | if (in1_size > in2_size) { 137 | if (_in1[in2_size] != '\0') { 138 | *_out = (npy_bool)0; 139 | continue; 140 | } 141 | if (strncmp(_in1, _in2, in2_size) != 0) { 142 | *_out = (npy_bool)0; 143 | } 144 | } 145 | else { 146 | if (in2_size > in1_size) { 147 | if (_in2[in1_size] != '\0') { 148 | *_out = (npy_bool)0; 149 | continue; 150 | } 151 | } 152 | if (strncmp(_in1, _in2, in1_size) != 0) { 153 | *_out = (npy_bool)0; 154 | } 155 | } 156 | } 157 | 158 | return 0; 159 | } 160 | 161 | static NPY_CASTING 162 | ascii_equal_resolve_descriptors(PyObject *NPY_UNUSED(self), 163 | PyArray_DTypeMeta *dtypes[], 164 | PyArray_Descr *given_descrs[], 165 | PyArray_Descr *loop_descrs[], 166 | npy_intp *NPY_UNUSED(view_offset)) 167 | { 168 | Py_INCREF(given_descrs[0]); 169 | loop_descrs[0] = given_descrs[0]; 170 | Py_INCREF(given_descrs[1]); 171 | loop_descrs[1] = given_descrs[1]; 172 | 173 | loop_descrs[2] = PyArray_DescrFromType(NPY_BOOL); // cannot fail 174 | 175 | return NPY_SAFE_CASTING; 176 | } 177 | 178 | static char *equal_name = "ascii_equal"; 179 | 180 | int 181 | init_equal_ufunc(PyObject *numpy) 182 | { 183 | PyObject *equal = PyObject_GetAttrString(numpy, "equal"); 184 | if (equal == NULL) { 185 | return -1; 186 | } 187 | 188 | /* 189 | * Initialize spec for equality 190 | */ 191 | PyArray_DTypeMeta **eq_dtypes = malloc(3 * sizeof(PyArray_DTypeMeta *)); 192 | eq_dtypes[0] = &ASCIIDType; 193 | eq_dtypes[1] = &ASCIIDType; 194 | eq_dtypes[2] = &PyArray_BoolDType; 195 | 196 | static PyType_Slot eq_slots[] = { 197 | {NPY_METH_resolve_descriptors, &ascii_equal_resolve_descriptors}, 198 | {NPY_METH_strided_loop, &ascii_equal_strided_loop}, 199 | {0, NULL}}; 200 | 201 | PyArrayMethod_Spec *EqualSpec = malloc(sizeof(PyArrayMethod_Spec)); 202 | 203 | EqualSpec->name = equal_name; 204 | EqualSpec->nin = 2; 205 | EqualSpec->nout = 1; 206 | EqualSpec->casting = NPY_SAFE_CASTING; 207 | EqualSpec->flags = 0; 208 | EqualSpec->dtypes = eq_dtypes; 209 | EqualSpec->slots = eq_slots; 210 | 211 | if (PyUFunc_AddLoopFromSpec(equal, EqualSpec) < 0) { 212 | Py_DECREF(equal); 213 | free(eq_dtypes); 214 | free(EqualSpec); 215 | return -1; 216 | } 217 | 218 | Py_DECREF(equal); 219 | free(eq_dtypes); 220 | free(EqualSpec); 221 | return 0; 222 | } 223 | 224 | int 225 | init_ufuncs(void) 226 | { 227 | PyObject *numpy = PyImport_ImportModule("numpy"); 228 | if (numpy == NULL) { 229 | return -1; 230 | } 231 | 232 | if (init_add_ufunc(numpy) < 0) { 233 | goto error; 234 | } 235 | 236 | if (init_equal_ufunc(numpy) < 0) { 237 | goto error; 238 | } 239 | 240 | return 0; 241 | 242 | error: 243 | Py_DECREF(numpy); 244 | return -1; 245 | } 246 | -------------------------------------------------------------------------------- /asciidtype/asciidtype/src/umath.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_UFUNC_H 2 | #define _NPY_UFUNC_H 3 | 4 | int 5 | init_ufuncs(void); 6 | 7 | #endif /*_NPY_UFUNC_H */ 8 | -------------------------------------------------------------------------------- /asciidtype/meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'asciidtype', 3 | 'c', 4 | ) 5 | 6 | py_mod = import('python') 7 | py = py_mod.find_installation() 8 | 9 | incdir_numpy = run_command(py, 10 | [ 11 | '-c', 12 | 'import numpy; print(numpy.get_include())' 13 | ], 14 | check: true 15 | ).stdout().strip() 16 | 17 | includes = include_directories( 18 | [ 19 | incdir_numpy, 20 | 'asciidtype/src' 21 | ] 22 | ) 23 | 24 | srcs = [ 25 | 'asciidtype/src/casts.c', 26 | 'asciidtype/src/casts.h', 27 | 'asciidtype/src/dtype.c', 28 | 'asciidtype/src/asciidtype_main.c', 29 | 'asciidtype/src/umath.c', 30 | 'asciidtype/src/umath.h', 31 | ] 32 | 33 | py.install_sources( 34 | [ 35 | 'asciidtype/__init__.py', 36 | 'asciidtype/scalar.py' 37 | ], 38 | subdir: 'asciidtype', 39 | pure: false 40 | ) 41 | 42 | py.extension_module( 43 | '_asciidtype_main', 44 | srcs, 45 | c_args: ['-g', '-O0'], 46 | install: true, 47 | subdir: 'asciidtype', 48 | include_directories: includes 49 | ) 50 | -------------------------------------------------------------------------------- /asciidtype/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "meson>=0.63.0", 4 | "meson-python", 5 | "patchelf", 6 | "wheel", 7 | "numpy", 8 | ] 9 | build-backend = "mesonpy" 10 | 11 | [tool.black] 12 | line-length = 79 13 | 14 | [project] 15 | name = "asciidtype" 16 | description = "A dtype for ASCII data" 17 | version = "0.0.1" 18 | readme = 'README.md' 19 | author = "Nathan Goldbaum" 20 | requires-python = ">=3.9.0" 21 | dependencies = [ 22 | "numpy", 23 | ] 24 | -------------------------------------------------------------------------------- /asciidtype/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | IFS=$'\n\t' 4 | 5 | if [ -d "build/" ] 6 | then 7 | rm -r build 8 | fi 9 | 10 | #meson setup build -Db_sanitize=address,undefined 11 | meson setup build 12 | python -m pip uninstall -y asciidtype 13 | python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -Csetup-args="-Dbuildtype=debug" 14 | #python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' 15 | -------------------------------------------------------------------------------- /asciidtype/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["NUMPY_EXPERIMENTAL_DTYPE_API"] = "1" 4 | -------------------------------------------------------------------------------- /asciidtype/tests/test_asciidtype.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import re 4 | import tempfile 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from asciidtype import ASCIIDType, ASCIIScalar 10 | 11 | 12 | def test_dtype_creation(): 13 | dtype = ASCIIDType(4) 14 | assert str(dtype) == "ASCIIDType(4)" 15 | 16 | 17 | def test_scalar_creation(): 18 | dtype = ASCIIDType(7) 19 | ASCIIScalar("string", dtype) 20 | 21 | 22 | def test_creation_with_explicit_dtype(): 23 | dtype = ASCIIDType(7) 24 | arr = np.array(["hello", "this", "is", "an", "array"], dtype=dtype) 25 | assert repr(arr) == ( 26 | "array(['hello', 'this', 'is', 'an', 'array'], dtype=ASCIIDType(7))" 27 | ) 28 | 29 | 30 | def test_creation_from_scalar(): 31 | data = [ 32 | ASCIIScalar("hello", ASCIIDType(6)), 33 | ASCIIScalar("array", ASCIIDType(7)), 34 | ] 35 | arr = np.array(data) 36 | assert repr(arr) == ("array(['hello', 'array'], dtype=ASCIIDType(7))") 37 | 38 | 39 | def test_creation_truncation(): 40 | inp = ["hello", "this", "is", "an", "array"] 41 | 42 | dtype = ASCIIDType(5) 43 | arr = np.array(inp, dtype=dtype) 44 | assert repr(arr) == ( 45 | "array(['hello', 'this', 'is', 'an', 'array'], dtype=ASCIIDType(5))" 46 | ) 47 | 48 | dtype = ASCIIDType(4) 49 | arr = np.array(inp, dtype=dtype) 50 | assert repr(arr) == ( 51 | "array(['hell', 'this', 'is', 'an', 'arra'], dtype=ASCIIDType(4))" 52 | ) 53 | 54 | dtype = ASCIIDType(1) 55 | arr = np.array(inp, dtype=dtype) 56 | assert repr(arr) == ( 57 | "array(['h', 't', 'i', 'a', 'a'], dtype=ASCIIDType(1))" 58 | ) 59 | assert arr.tobytes() == b"htiaa" 60 | 61 | dtype = ASCIIDType() 62 | arr = np.array(["hello", "this", "is", "an", "array"], dtype=dtype) 63 | assert repr(arr) == ("array(['', '', '', '', ''], dtype=ASCIIDType(0))") 64 | assert arr.tobytes() == b"" 65 | 66 | 67 | def test_casting_to_asciidtype(): 68 | for dtype in (None, ASCIIDType(5)): 69 | arr = np.array(["this", "is", "an", "array"], dtype=dtype) 70 | 71 | assert repr(arr.astype(ASCIIDType(7))) == ( 72 | "array(['this', 'is', 'an', 'array'], dtype=ASCIIDType(7))" 73 | ) 74 | 75 | assert repr(arr.astype(ASCIIDType(5))) == ( 76 | "array(['this', 'is', 'an', 'array'], dtype=ASCIIDType(5))" 77 | ) 78 | 79 | assert repr(arr.astype(ASCIIDType(4))) == ( 80 | "array(['this', 'is', 'an', 'arra'], dtype=ASCIIDType(4))" 81 | ) 82 | 83 | assert repr(arr.astype(ASCIIDType(1))) == ( 84 | "array(['t', 'i', 'a', 'a'], dtype=ASCIIDType(1))" 85 | ) 86 | 87 | # assert repr(arr.astype(ASCIIDType())) == ( 88 | # "array(['', '', '', '', ''], dtype=ASCIIDType(0))" 89 | # ) 90 | 91 | 92 | def test_casting_safety(): 93 | arr = np.array(["this", "is", "an", "array"]) 94 | assert repr(arr.astype(ASCIIDType(6), casting="safe")) == ( 95 | "array(['this', 'is', 'an', 'array'], dtype=ASCIIDType(6))" 96 | ) 97 | assert repr(arr.astype(ASCIIDType(5), casting="safe")) == ( 98 | "array(['this', 'is', 'an', 'array'], dtype=ASCIIDType(5))" 99 | ) 100 | with pytest.raises( 101 | TypeError, 102 | match=re.escape( 103 | "Cannot cast array data from dtype(' 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL metadatadtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL metadatadtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #define NO_IMPORT_ARRAY 8 | #define NO_IMPORT_UFUNC 9 | #include "numpy/arrayobject.h" 10 | #include "numpy/dtype_api.h" 11 | #include "numpy/ndarraytypes.h" 12 | 13 | #include "casts.h" 14 | #include "dtype.h" 15 | 16 | /* 17 | * And now the actual cast code! Starting with the "resolver" which tells 18 | * us about cast safety. 19 | * Note also the `view_offset`! It is a way for you to tell NumPy, that this 20 | * cast does not require anything at all, but the cast can simply be done as 21 | * a view. 22 | * For `arr.astype()` it might mean returning a view (eventually, not yet). 23 | * For ufuncs, it already means that they don't have to do a cast at all! 24 | */ 25 | static NPY_CASTING 26 | metadata_to_metadata_resolve_descriptors( 27 | PyObject *NPY_UNUSED(self), PyArray_DTypeMeta *NPY_UNUSED(dtypes[2]), 28 | PyArray_Descr *given_descrs[2], PyArray_Descr *loop_descrs[2], 29 | npy_intp *view_offset) 30 | { 31 | PyObject *meta1 = NULL; 32 | PyObject *meta2 = NULL; 33 | if (given_descrs[1] == NULL) { 34 | meta1 = given_descrs[0]->metadata; 35 | Py_INCREF(given_descrs[0]); 36 | loop_descrs[1] = given_descrs[0]; 37 | } 38 | else { 39 | meta1 = given_descrs[0]->metadata; 40 | meta2 = given_descrs[1]->metadata; 41 | Py_INCREF(given_descrs[1]); 42 | loop_descrs[1] = given_descrs[1]; 43 | } 44 | Py_INCREF(given_descrs[0]); 45 | loop_descrs[0] = given_descrs[0]; 46 | 47 | if (meta2 != NULL) { 48 | int comp = PyObject_RichCompareBool(meta1, meta2, Py_EQ); 49 | if (comp == -1) { 50 | return -1; 51 | } 52 | if (comp == 1) { 53 | // identical metadata so no need to cast, views are OK 54 | *view_offset = 0; 55 | return NPY_NO_CASTING; 56 | } 57 | } 58 | 59 | /* Should this use safe casting? */ 60 | return NPY_SAFE_CASTING; 61 | } 62 | 63 | static int 64 | metadata_to_float64like_contiguous(PyArrayMethod_Context *NPY_UNUSED(context), 65 | char *const data[], 66 | npy_intp const dimensions[], 67 | npy_intp const NPY_UNUSED(strides[]), 68 | NpyAuxData *NPY_UNUSED(auxdata)) 69 | { 70 | npy_intp N = dimensions[0]; 71 | double *in = (double *)data[0]; 72 | double *out = (double *)data[1]; 73 | 74 | while (N--) { 75 | *out = *in; 76 | out++; 77 | in++; 78 | } 79 | 80 | return 0; 81 | } 82 | 83 | static int 84 | metadata_to_float64like_strided(PyArrayMethod_Context *NPY_UNUSED(context), 85 | char *const data[], 86 | npy_intp const dimensions[], 87 | npy_intp const strides[], 88 | NpyAuxData *NPY_UNUSED(auxdata)) 89 | { 90 | npy_intp N = dimensions[0]; 91 | char *in = data[0]; 92 | char *out = data[1]; 93 | npy_intp in_stride = strides[0]; 94 | npy_intp out_stride = strides[1]; 95 | 96 | while (N--) { 97 | *(double *)out = *(double *)in; 98 | in += in_stride; 99 | out += out_stride; 100 | } 101 | 102 | return 0; 103 | } 104 | 105 | static int 106 | metadata_to_float64like_unaligned(PyArrayMethod_Context *NPY_UNUSED(context), 107 | char *const data[], 108 | npy_intp const dimensions[], 109 | npy_intp const strides[], 110 | NpyAuxData *NPY_UNUSED(auxdata)) 111 | { 112 | npy_intp N = dimensions[0]; 113 | char *in = data[0]; 114 | char *out = data[1]; 115 | npy_intp in_stride = strides[0]; 116 | npy_intp out_stride = strides[1]; 117 | 118 | while (N--) { 119 | memcpy(out, in, sizeof(double)); // NOLINT 120 | in += in_stride; 121 | out += out_stride; 122 | } 123 | 124 | return 0; 125 | } 126 | 127 | static int 128 | metadata_to_metadata_get_loop(PyArrayMethod_Context *NPY_UNUSED(context), 129 | int aligned, int NPY_UNUSED(move_references), 130 | const npy_intp *strides, 131 | PyArrayMethod_StridedLoop **out_loop, 132 | NpyAuxData **NPY_UNUSED(out_transferdata), 133 | NPY_ARRAYMETHOD_FLAGS *flags) 134 | { 135 | int contig = 136 | (strides[0] == sizeof(double) && strides[1] == sizeof(double)); 137 | 138 | if (aligned && contig) { 139 | *out_loop = (PyArrayMethod_StridedLoop 140 | *)&metadata_to_float64like_contiguous; 141 | } 142 | else if (aligned) { 143 | *out_loop = 144 | (PyArrayMethod_StridedLoop *)&metadata_to_float64like_strided; 145 | } 146 | else { 147 | *out_loop = (PyArrayMethod_StridedLoop 148 | *)&metadata_to_float64like_unaligned; 149 | } 150 | 151 | *flags = 0; 152 | return 0; 153 | } 154 | 155 | static int 156 | metadata_to_float64_get_loop(PyArrayMethod_Context *NPY_UNUSED(context), 157 | int aligned, int NPY_UNUSED(move_references), 158 | const npy_intp *strides, 159 | PyArrayMethod_StridedLoop **out_loop, 160 | NpyAuxData **NPY_UNUSED(out_transferdata), 161 | NPY_ARRAYMETHOD_FLAGS *flags) 162 | { 163 | int contig = 164 | (strides[0] == sizeof(double) && strides[1] == sizeof(double)); 165 | 166 | if (aligned && contig) { 167 | *out_loop = (PyArrayMethod_StridedLoop 168 | *)&metadata_to_float64like_contiguous; 169 | } 170 | else if (aligned) { 171 | *out_loop = 172 | (PyArrayMethod_StridedLoop *)&metadata_to_float64like_strided; 173 | } 174 | else { 175 | *out_loop = (PyArrayMethod_StridedLoop 176 | *)&metadata_to_float64like_unaligned; 177 | } 178 | 179 | *flags = 0; 180 | return 0; 181 | } 182 | 183 | /* 184 | * NumPy currently allows NULL for the own DType/"cls". For other DTypes 185 | * we would have to fill it in here: 186 | */ 187 | static PyArray_DTypeMeta *m2m_dtypes[2] = {NULL, NULL}; 188 | 189 | static PyType_Slot m2m_slots[] = { 190 | {NPY_METH_resolve_descriptors, 191 | &metadata_to_metadata_resolve_descriptors}, 192 | {NPY_METH_get_loop, &metadata_to_metadata_get_loop}, 193 | {0, NULL}}; 194 | 195 | PyArrayMethod_Spec MetadataToMetadataCastSpec = { 196 | .name = "cast_MetadataDType_to_MetadataDType", 197 | .nin = 1, 198 | .nout = 1, 199 | .flags = NPY_METH_SUPPORTS_UNALIGNED, 200 | .casting = NPY_SAME_KIND_CASTING, 201 | .dtypes = m2m_dtypes, 202 | .slots = m2m_slots, 203 | }; 204 | 205 | static PyType_Slot m2f_slots[] = { 206 | {NPY_METH_get_loop, &metadata_to_float64_get_loop}, {0, NULL}}; 207 | 208 | static char *m2f_name = "cast_MetadataDType_to_Float64"; 209 | 210 | PyArrayMethod_Spec ** 211 | get_casts(void) 212 | { 213 | PyArray_DTypeMeta **m2f_dtypes = malloc(2 * sizeof(PyArray_DTypeMeta *)); 214 | m2f_dtypes[0] = NULL; 215 | m2f_dtypes[1] = &PyArray_DoubleDType; 216 | 217 | PyArrayMethod_Spec *MetadataToFloat64CastSpec = 218 | malloc(sizeof(PyArrayMethod_Spec)); 219 | MetadataToFloat64CastSpec->name = m2f_name; 220 | MetadataToFloat64CastSpec->nin = 1; 221 | MetadataToFloat64CastSpec->nout = 1; 222 | MetadataToFloat64CastSpec->flags = NPY_METH_SUPPORTS_UNALIGNED; 223 | MetadataToFloat64CastSpec->casting = NPY_SAFE_CASTING; 224 | MetadataToFloat64CastSpec->dtypes = m2f_dtypes; 225 | MetadataToFloat64CastSpec->slots = m2f_slots; 226 | 227 | PyArrayMethod_Spec **casts = malloc(3 * sizeof(PyArrayMethod_Spec *)); 228 | casts[0] = &MetadataToMetadataCastSpec; 229 | casts[1] = MetadataToFloat64CastSpec; 230 | casts[2] = NULL; 231 | 232 | return casts; 233 | } 234 | -------------------------------------------------------------------------------- /metadatadtype/metadatadtype/src/casts.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_CASTS_H 2 | #define _NPY_CASTS_H 3 | 4 | PyArrayMethod_Spec ** 5 | get_casts(void); 6 | 7 | #endif /* _NPY_CASTS_H */ 8 | -------------------------------------------------------------------------------- /metadatadtype/metadatadtype/src/dtype.c: -------------------------------------------------------------------------------- 1 | // clang-format off 2 | #include 3 | #include "structmember.h" 4 | // clang-format on 5 | 6 | #define PY_ARRAY_UNIQUE_SYMBOL metadatadtype_ARRAY_API 7 | #define PY_UFUNC_UNIQUE_SYMBOL metadatadtype_UFUNC_API 8 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 9 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 10 | #define NO_IMPORT_ARRAY 11 | #define NO_IMPORT_UFUNC 12 | #include "numpy/arrayobject.h" 13 | #include "numpy/dtype_api.h" 14 | #include "numpy/ndarraytypes.h" 15 | 16 | #include "dtype.h" 17 | 18 | #include "casts.h" 19 | 20 | PyTypeObject *MetadataScalar_Type = NULL; 21 | 22 | /* 23 | * `get_value` and `get_unit` are small helpers to deal with the scalar. 24 | */ 25 | 26 | static double 27 | get_value(PyObject *scalar) 28 | { 29 | PyTypeObject *scalar_type = Py_TYPE(scalar); 30 | if (scalar_type != MetadataScalar_Type) { 31 | double res = PyFloat_AsDouble(scalar); 32 | if (res == -1 && PyErr_Occurred()) { 33 | PyErr_SetString( 34 | PyExc_TypeError, 35 | "Can only store MetadataScalar in a MetadataDType array."); 36 | return -1; 37 | } 38 | return res; 39 | } 40 | 41 | PyObject *value = PyObject_GetAttrString(scalar, "value"); 42 | if (value == NULL) { 43 | return -1; 44 | } 45 | double res = PyFloat_AsDouble(value); 46 | if (res == -1 && PyErr_Occurred()) { 47 | return -1; 48 | } 49 | Py_DECREF(value); 50 | return res; 51 | } 52 | 53 | static PyObject * 54 | get_metadata(PyObject *scalar) 55 | { 56 | if (Py_TYPE(scalar) != MetadataScalar_Type) { 57 | PyErr_SetString( 58 | PyExc_TypeError, 59 | "Can only store MetadataScalar in a MetadataDType array."); 60 | return NULL; 61 | } 62 | 63 | MetadataDTypeObject *dtype = 64 | (MetadataDTypeObject *)PyObject_GetAttrString(scalar, "dtype"); 65 | if (dtype == NULL) { 66 | return NULL; 67 | } 68 | 69 | PyObject *metadata = dtype->metadata; 70 | Py_DECREF(dtype); 71 | if (metadata == NULL) { 72 | return NULL; 73 | } 74 | Py_INCREF(metadata); 75 | return metadata; 76 | } 77 | 78 | /* 79 | * Internal helper to create new instances 80 | */ 81 | MetadataDTypeObject * 82 | new_metadatadtype_instance(PyObject *metadata) 83 | { 84 | MetadataDTypeObject *new = (MetadataDTypeObject *)PyArrayDescr_Type.tp_new( 85 | /* TODO: Using NULL for args here works, but seems not clean? */ 86 | (PyTypeObject *)&MetadataDType, NULL, NULL); 87 | if (new == NULL) { 88 | return NULL; 89 | } 90 | Py_INCREF(metadata); 91 | new->metadata = metadata; 92 | PyArray_Descr *base = (PyArray_Descr *)new; 93 | base->elsize = sizeof(double); 94 | base->alignment = _Alignof(double); /* is there a better spelling? */ 95 | /* do not support byte-order for now */ 96 | 97 | return new; 98 | } 99 | 100 | PyArray_Descr * 101 | common_instance(MetadataDTypeObject *dtype1, 102 | MetadataDTypeObject *NPY_UNUSED(dtype2)) 103 | { 104 | Py_INCREF(dtype1); 105 | return (PyArray_Descr *)dtype1; 106 | } 107 | 108 | static PyArray_DTypeMeta * 109 | common_dtype(PyArray_DTypeMeta *cls, PyArray_DTypeMeta *other) 110 | { 111 | /* 112 | * Typenum is useful for NumPy, but there it can still be convenient. 113 | * (New-style user dtypes will probably get -1 as type number...) 114 | */ 115 | if (other->type_num >= 0 && PyTypeNum_ISNUMBER(other->type_num) && 116 | !PyTypeNum_ISCOMPLEX(other->type_num) && 117 | other != &PyArray_LongDoubleDType) { 118 | /* 119 | * A (simple) builtin numeric type that is not a complex or longdouble 120 | * will always promote to double (cls). 121 | */ 122 | Py_INCREF(cls); 123 | return cls; 124 | } 125 | Py_INCREF(Py_NotImplemented); 126 | return (PyArray_DTypeMeta *)Py_NotImplemented; 127 | } 128 | 129 | static PyArray_Descr * 130 | metadata_discover_descriptor_from_pyobject(PyArray_DTypeMeta *NPY_UNUSED(cls), 131 | PyObject *obj) 132 | { 133 | if (Py_TYPE(obj) != MetadataScalar_Type) { 134 | PyErr_SetString( 135 | PyExc_TypeError, 136 | "Can only store MetadataScalar in a MetadataDType array."); 137 | return NULL; 138 | } 139 | 140 | PyObject *metadata = get_metadata(obj); 141 | if (metadata == NULL) { 142 | return NULL; 143 | } 144 | PyArray_Descr *ret = (PyArray_Descr *)PyObject_GetAttrString(obj, "dtype"); 145 | if (ret == NULL) { 146 | return NULL; 147 | } 148 | return ret; 149 | } 150 | 151 | static int 152 | metadatadtype_setitem(MetadataDTypeObject *descr, PyObject *obj, char *dataptr) 153 | { 154 | double value = get_value(obj); 155 | if (value == -1 && PyErr_Occurred()) { 156 | return -1; 157 | } 158 | 159 | memcpy(dataptr, &value, sizeof(double)); // NOLINT 160 | 161 | return 0; 162 | } 163 | 164 | static PyObject * 165 | metadatadtype_getitem(MetadataDTypeObject *descr, char *dataptr) 166 | { 167 | double val; 168 | /* get the value */ 169 | memcpy(&val, dataptr, sizeof(double)); // NOLINT 170 | 171 | PyObject *val_obj = PyFloat_FromDouble(val); 172 | if (val_obj == NULL) { 173 | return NULL; 174 | } 175 | 176 | PyObject *res = PyObject_CallFunctionObjArgs( 177 | (PyObject *)MetadataScalar_Type, val_obj, descr, NULL); 178 | if (res == NULL) { 179 | return NULL; 180 | } 181 | Py_DECREF(val_obj); 182 | 183 | return res; 184 | } 185 | 186 | static MetadataDTypeObject * 187 | metadatadtype_ensure_canonical(MetadataDTypeObject *self) 188 | { 189 | Py_INCREF(self); 190 | return self; 191 | } 192 | 193 | static PyType_Slot MetadataDType_Slots[] = { 194 | {NPY_DT_common_instance, &common_instance}, 195 | {NPY_DT_common_dtype, &common_dtype}, 196 | {NPY_DT_discover_descr_from_pyobject, 197 | &metadata_discover_descriptor_from_pyobject}, 198 | /* The header is wrong on main :(, so we add 1 */ 199 | {NPY_DT_setitem, &metadatadtype_setitem}, 200 | {NPY_DT_getitem, &metadatadtype_getitem}, 201 | {NPY_DT_ensure_canonical, &metadatadtype_ensure_canonical}, 202 | {0, NULL}}; 203 | 204 | static PyObject * 205 | metadatadtype_new(PyTypeObject *NPY_UNUSED(cls), PyObject *args, 206 | PyObject *kwds) 207 | { 208 | static char *kwargs_strs[] = {"metadata", NULL}; 209 | 210 | PyObject *metadata = NULL; 211 | 212 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O:MetadataDType", 213 | kwargs_strs, &metadata)) { 214 | return NULL; 215 | } 216 | if (metadata == NULL) { 217 | metadata = Py_None; 218 | } 219 | 220 | return (PyObject *)new_metadatadtype_instance(metadata); 221 | } 222 | 223 | static void 224 | metadatadtype_dealloc(MetadataDTypeObject *self) 225 | { 226 | Py_CLEAR(self->metadata); 227 | PyArrayDescr_Type.tp_dealloc((PyObject *)self); 228 | } 229 | 230 | static PyObject * 231 | metadatadtype_repr(MetadataDTypeObject *self) 232 | { 233 | PyObject *res = PyUnicode_FromFormat("MetadataDType(%R)", self->metadata); 234 | return res; 235 | } 236 | 237 | static PyMemberDef MetadataDType_members[] = { 238 | {"_metadata", T_OBJECT_EX, offsetof(MetadataDTypeObject, metadata), 239 | READONLY, "some metadata"}, 240 | {NULL}, 241 | }; 242 | 243 | /* 244 | * This is the basic things that you need to create a Python Type/Class in C. 245 | * However, there is a slight difference here because we create a 246 | * PyArray_DTypeMeta, which is a larger struct than a typical type. 247 | * (This should get a bit nicer eventually with Python >3.11.) 248 | */ 249 | PyArray_DTypeMeta MetadataDType = { 250 | {{ 251 | PyVarObject_HEAD_INIT(NULL, 0).tp_name = 252 | "metadatadtype.MetadataDType", 253 | .tp_basicsize = sizeof(MetadataDTypeObject), 254 | .tp_new = metadatadtype_new, 255 | .tp_dealloc = (destructor)metadatadtype_dealloc, 256 | .tp_repr = (reprfunc)metadatadtype_repr, 257 | .tp_str = (reprfunc)metadatadtype_repr, 258 | .tp_members = MetadataDType_members, 259 | }}, 260 | /* rest, filled in during DTypeMeta initialization */ 261 | }; 262 | 263 | int 264 | init_metadata_dtype(void) 265 | { 266 | /* 267 | * To create our DType, we have to use a "Spec" that tells NumPy how to 268 | * do it. You first have to create a static type, but see the note there! 269 | */ 270 | PyArrayMethod_Spec **casts = get_casts(); 271 | 272 | PyArrayDTypeMeta_Spec MetadataDType_DTypeSpec = { 273 | .flags = NPY_DT_PARAMETRIC | NPY_DT_NUMERIC, 274 | .casts = casts, 275 | .typeobj = MetadataScalar_Type, 276 | .slots = MetadataDType_Slots, 277 | }; 278 | /* Loaded dynamically, so may need to be set here: */ 279 | ((PyObject *)&MetadataDType)->ob_type = &PyArrayDTypeMeta_Type; 280 | ((PyTypeObject *)&MetadataDType)->tp_base = &PyArrayDescr_Type; 281 | if (PyType_Ready((PyTypeObject *)&MetadataDType) < 0) { 282 | return -1; 283 | } 284 | 285 | if (PyArrayInitDTypeMeta_FromSpec(&MetadataDType, 286 | &MetadataDType_DTypeSpec) < 0) { 287 | return -1; 288 | } 289 | 290 | MetadataDType.singleton = PyArray_GetDefaultDescr(&MetadataDType); 291 | 292 | free(MetadataDType_DTypeSpec.casts[1]->dtypes); 293 | free(MetadataDType_DTypeSpec.casts[1]); 294 | free(MetadataDType_DTypeSpec.casts); 295 | 296 | return 0; 297 | } 298 | -------------------------------------------------------------------------------- /metadatadtype/metadatadtype/src/dtype.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_DTYPE_H 2 | #define _NPY_DTYPE_H 3 | 4 | typedef struct { 5 | PyArray_Descr base; 6 | PyObject *metadata; 7 | } MetadataDTypeObject; 8 | 9 | extern PyArray_DTypeMeta MetadataDType; 10 | extern PyTypeObject *MetadataScalar_Type; 11 | 12 | MetadataDTypeObject * 13 | new_metadatadtype_instance(PyObject *metadata); 14 | 15 | int 16 | init_metadata_dtype(void); 17 | 18 | PyArray_Descr * 19 | common_instance(MetadataDTypeObject *dtype1, 20 | MetadataDTypeObject *NPY_UNUSED(dtype2)); 21 | 22 | // from numpy's dtypemeta.h, not publicly available 23 | #define NPY_DTYPE(descr) ((PyArray_DTypeMeta *)Py_TYPE(descr)) 24 | 25 | #endif /*_NPY_DTYPE_H*/ 26 | -------------------------------------------------------------------------------- /metadatadtype/metadatadtype/src/metadatadtype_main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL metadatadtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL metadatadtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #include "numpy/arrayobject.h" 8 | #include "numpy/ufuncobject.h" 9 | #include "numpy/dtype_api.h" 10 | 11 | #include "umath.h" 12 | #include "dtype.h" 13 | 14 | static struct PyModuleDef moduledef = { 15 | PyModuleDef_HEAD_INIT, 16 | .m_name = "metadatadtype_main", 17 | .m_size = -1, 18 | }; 19 | 20 | /* Module initialization function */ 21 | PyMODINIT_FUNC 22 | PyInit__metadatadtype_main(void) 23 | { 24 | import_array(); 25 | import_umath(); 26 | 27 | PyObject *m = PyModule_Create(&moduledef); 28 | if (m == NULL) { 29 | return NULL; 30 | } 31 | 32 | PyObject *mod = PyImport_ImportModule("metadatadtype"); 33 | if (mod == NULL) { 34 | goto error; 35 | } 36 | MetadataScalar_Type = 37 | (PyTypeObject *)PyObject_GetAttrString(mod, "MetadataScalar"); 38 | Py_DECREF(mod); 39 | 40 | if (MetadataScalar_Type == NULL) { 41 | goto error; 42 | } 43 | 44 | if (init_metadata_dtype() < 0) { 45 | goto error; 46 | } 47 | 48 | if (PyModule_AddObject(m, "MetadataDType", (PyObject *)&MetadataDType) < 49 | 0) { 50 | goto error; 51 | } 52 | 53 | if (init_ufuncs() == -1) { 54 | goto error; 55 | } 56 | 57 | return m; 58 | 59 | error: 60 | Py_DECREF(m); 61 | return NULL; 62 | } 63 | -------------------------------------------------------------------------------- /metadatadtype/metadatadtype/src/umath.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL metadatadtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL metadatadtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #define NO_IMPORT_ARRAY 8 | #define NO_IMPORT_UFUNC 9 | #include "numpy/ndarraytypes.h" 10 | #include "numpy/arrayobject.h" 11 | #include "numpy/ufuncobject.h" 12 | #include "numpy/dtype_api.h" 13 | 14 | #include "dtype.h" 15 | #include "umath.h" 16 | 17 | static int 18 | translate_given_descrs(int nin, int nout, 19 | PyArray_DTypeMeta *const NPY_UNUSED(wrapped_dtypes[]), 20 | PyArray_Descr *const given_descrs[], 21 | PyArray_Descr *new_descrs[]) 22 | { 23 | for (int i = 0; i < nin + nout; i++) { 24 | if (given_descrs[i] == NULL) { 25 | new_descrs[i] = NULL; 26 | } 27 | else { 28 | if (NPY_DTYPE(given_descrs[i]) == &PyArray_BoolDType) { 29 | new_descrs[i] = PyArray_DescrFromType(NPY_BOOL); 30 | } 31 | else { 32 | new_descrs[i] = PyArray_DescrFromType(NPY_DOUBLE); 33 | } 34 | } 35 | } 36 | return 0; 37 | } 38 | 39 | static int 40 | translate_loop_descrs(int nin, int NPY_UNUSED(nout), 41 | PyArray_DTypeMeta *const NPY_UNUSED(new_dtypes[]), 42 | PyArray_Descr *const given_descrs[], 43 | PyArray_Descr *original_descrs[], 44 | PyArray_Descr *loop_descrs[]) 45 | { 46 | if (nin == 2) { 47 | loop_descrs[0] = 48 | common_instance((MetadataDTypeObject *)given_descrs[0], 49 | (MetadataDTypeObject *)given_descrs[1]); 50 | if (loop_descrs[0] == NULL) { 51 | return -1; 52 | } 53 | Py_INCREF(loop_descrs[0]); 54 | loop_descrs[1] = loop_descrs[0]; 55 | Py_INCREF(loop_descrs[1]); 56 | if (NPY_DTYPE(original_descrs[2]) == &PyArray_BoolDType) { 57 | loop_descrs[2] = PyArray_DescrFromType(NPY_BOOL); 58 | } 59 | else { 60 | loop_descrs[2] = loop_descrs[0]; 61 | } 62 | Py_INCREF(loop_descrs[2]); 63 | } 64 | else if (nin == 1) { 65 | loop_descrs[0] = given_descrs[0]; 66 | Py_INCREF(loop_descrs[0]); 67 | if (NPY_DTYPE(original_descrs[1]) == &PyArray_BoolDType) { 68 | loop_descrs[1] = PyArray_DescrFromType(NPY_BOOL); 69 | } 70 | else { 71 | loop_descrs[1] = loop_descrs[0]; 72 | } 73 | Py_INCREF(loop_descrs[1]); 74 | } 75 | else { 76 | return -1; 77 | } 78 | return 0; 79 | } 80 | 81 | static PyObject * 82 | get_ufunc(const char *ufunc_name) 83 | { 84 | PyObject *mod = PyImport_ImportModule("numpy"); 85 | if (mod == NULL) { 86 | return NULL; 87 | } 88 | PyObject *ufunc = PyObject_GetAttrString(mod, ufunc_name); 89 | Py_DECREF(mod); 90 | 91 | return ufunc; 92 | } 93 | 94 | static int 95 | add_wrapping_loop(const char *ufunc_name, PyArray_DTypeMeta **dtypes, 96 | PyArray_DTypeMeta **wrapped_dtypes) 97 | { 98 | PyObject *ufunc = get_ufunc(ufunc_name); 99 | if (ufunc == NULL) { 100 | return -1; 101 | } 102 | int res = PyUFunc_AddWrappingLoop(ufunc, dtypes, wrapped_dtypes, 103 | &translate_given_descrs, 104 | &translate_loop_descrs); 105 | return res; 106 | } 107 | 108 | int 109 | init_ufuncs(void) 110 | { 111 | PyArray_DTypeMeta *binary_orig_dtypes[3] = {&MetadataDType, &MetadataDType, 112 | &MetadataDType}; 113 | PyArray_DTypeMeta *binary_wrapped_dtypes[3] = { 114 | &PyArray_DoubleDType, &PyArray_DoubleDType, &PyArray_DoubleDType}; 115 | if (add_wrapping_loop("multiply", binary_orig_dtypes, 116 | binary_wrapped_dtypes) == -1) { 117 | goto error; 118 | } 119 | 120 | PyArray_DTypeMeta *unary_boolean_dtypes[2] = {&MetadataDType, 121 | &PyArray_BoolDType}; 122 | PyArray_DTypeMeta *unary_boolean_wrapped_dtypes[2] = {&PyArray_DoubleDType, 123 | &PyArray_BoolDType}; 124 | if (add_wrapping_loop("isnan", unary_boolean_dtypes, 125 | unary_boolean_wrapped_dtypes) == -1) { 126 | goto error; 127 | } 128 | 129 | return 0; 130 | error: 131 | 132 | return -1; 133 | } 134 | -------------------------------------------------------------------------------- /metadatadtype/metadatadtype/src/umath.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_UFUNC_H 2 | #define _NPY_UFUNC_H 3 | 4 | int 5 | init_ufuncs(void); 6 | 7 | #endif /*_NPY_UFUNC_H */ 8 | -------------------------------------------------------------------------------- /metadatadtype/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "meson>=0.63.0", 4 | "meson-python", 5 | "patchelf", 6 | "wheel", 7 | "numpy", 8 | ] 9 | build-backend = "mesonpy" 10 | 11 | [project] 12 | name = "metadatadtype" 13 | description = "A dtype that holds a piece of metadata" 14 | version = "0.0.1" 15 | readme = 'README.md' 16 | author = "Nathan Goldbaum" 17 | requires-python = ">=3.9.0" 18 | dependencies = [ 19 | "numpy", 20 | ] 21 | 22 | [tool.meson-python.args] 23 | dist = [] 24 | setup = ["-Ddebug=true", "-Doptimization=0"] 25 | compile = [] 26 | install = [] 27 | -------------------------------------------------------------------------------- /metadatadtype/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | IFS=$'\n\t' 4 | 5 | if [ -d "build/" ] 6 | then 7 | rm -r build 8 | fi 9 | 10 | #meson setup build -Db_sanitize=address,undefined 11 | meson setup build 12 | python -m pip uninstall -y metadatadtype 13 | python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -Csetup-args="-Dbuildtype=debug" 14 | #python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' 15 | -------------------------------------------------------------------------------- /metadatadtype/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["NUMPY_EXPERIMENTAL_DTYPE_API"] = "1" 4 | -------------------------------------------------------------------------------- /metadatadtype/tests/test_metadatadtype.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from metadatadtype import MetadataDType, MetadataScalar 4 | 5 | 6 | def test_dtype_creation(): 7 | dtype = MetadataDType("some metadata") 8 | assert str(dtype) == "MetadataDType('some metadata')" 9 | 10 | 11 | def test_creation_from_zeros(): 12 | dtype = MetadataDType("test") 13 | arr = np.zeros(3, dtype=dtype) 14 | assert str(arr) == "[0.0 test 0.0 test 0.0 test]" 15 | 16 | 17 | def test_creation_from_list(): 18 | dtype = MetadataDType("test") 19 | arr = np.array([0, 0, 0], dtype=dtype) 20 | assert str(arr) == "[0.0 test 0.0 test 0.0 test]" 21 | 22 | 23 | def test_creation_from_scalar(): 24 | dtype = MetadataDType("test") 25 | scalar = MetadataScalar(1, dtype) 26 | arr = np.array([scalar, scalar, scalar]) 27 | assert str(arr) == "[1.0 test 1.0 test 1.0 test]" 28 | 29 | 30 | def test_multiplication(): 31 | dtype = MetadataDType("test") 32 | scalar = MetadataScalar(1, dtype) 33 | arr = np.array([scalar, scalar, scalar]) 34 | scalar = MetadataScalar(2, dtype) 35 | arr2 = np.array([scalar, scalar, scalar]) 36 | res = arr * arr2 37 | assert str(res) == "[2.0 test 2.0 test 2.0 test]" 38 | 39 | 40 | def test_isnan(): 41 | dtype = MetadataDType("test") 42 | num_scalar = MetadataScalar(1, dtype) 43 | nan_scalar = MetadataScalar(np.nan, dtype) 44 | arr = np.array([num_scalar, nan_scalar, nan_scalar]) 45 | np.testing.assert_array_equal(np.isnan(arr), np.array([False, True, True])) 46 | 47 | 48 | def test_cast_to_different_metadata(): 49 | dtype = MetadataDType("test") 50 | scalar = MetadataScalar(1, dtype) 51 | arr = np.array([scalar, scalar, scalar]) 52 | dtype2 = MetadataDType("test2") 53 | conv = arr.astype(dtype2) 54 | assert str(conv) == "[1.0 test2 1.0 test2 1.0 test2]" 55 | 56 | 57 | def test_cast_to_float64(): 58 | dtype = MetadataDType("test") 59 | scalar = MetadataScalar(1, dtype) 60 | arr = np.array([scalar, scalar, scalar]) 61 | conv = arr.astype("float64") 62 | assert str(conv) == "[1. 1. 1.]" 63 | 64 | 65 | def test_is_numeric(): 66 | assert MetadataDType._is_numeric 67 | -------------------------------------------------------------------------------- /mpfdtype/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .mesonpy*.ini 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /mpfdtype/README.md: -------------------------------------------------------------------------------- 1 | # A multi precision DType for NumPy 2 | 3 | A DType and scalar which uses [MPFR](https://www.mpfr.org/) for multi precision floating point math. MPFR itself has an LGPL license. 4 | 5 | A very basic example:: 6 | 7 | import numpy as np 8 | from mpfdtype import MPFDType, MPFloat 9 | 10 | # create an array with 200 bits precision: 11 | arr = np.arange(3).astype(MPFDType(200)) 12 | print(repr(arr)) 13 | # array(['0E+00', '1.0E+00', '2.0E+00'], dtype=MPFDType(200)) 14 | 15 | # Math uses the input precision (wraps almost all math functions): 16 | res = arr**2 + np.sqrt(arr) 17 | print(repr(res)) 18 | # array(['0E+00', '2.0E+00', 19 | # '5.4142135623730950488016887242096980785696718753769480731766784E+00'], 20 | # dtype=MPFDType(200)) 21 | 22 | # cast to a different precision: 23 | arr2 = arr.astype(MPFDType(500)) 24 | print(repr(arr2)) 25 | # array(['0E+00', '1.0E+00', '2.0E+00'], dtype=MPFDType(500)) 26 | 27 | res = arr + arr2 28 | print(repr(res)) # uses the larger precision now 29 | # array(['0E+00', '2.0E+00', '4.0E+00'], dtype=MPFDType(500)) 30 | 31 | There also is an `mpf.MPFloat(value, prec=None)`. There is no "context" 32 | as most libraries like this (including mpfr itself) typically have. 33 | The rounding mode is always the normal one. 34 | -------------------------------------------------------------------------------- /mpfdtype/meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'mpfdtype', 3 | 'c', 4 | 'cpp', 5 | ) 6 | 7 | py_mod = import('python') 8 | py = py_mod.find_installation() 9 | 10 | c = meson.get_compiler('c') 11 | mpfr = c.find_library('mpfr') 12 | 13 | incdir_numpy = run_command(py, 14 | [ 15 | '-c', 16 | 'import numpy; print(numpy.get_include())' 17 | ], 18 | check: true 19 | ).stdout().strip() 20 | 21 | includes = include_directories( 22 | [ 23 | incdir_numpy, 24 | 'mpfdtype/src', 25 | ], 26 | ) 27 | 28 | srcs = [ 29 | 'mpfdtype/src/casts.cpp', 30 | 'mpfdtype/src/casts.h', 31 | 'mpfdtype/src/dtype.c', 32 | 'mpfdtype/src/dtype.h', 33 | 'mpfdtype/src/mpfdtype_main.c', 34 | 'mpfdtype/src/numbers.cpp', 35 | 'mpfdtype/src/numbers.h', 36 | 'mpfdtype/src/ops.hpp', 37 | 'mpfdtype/src/scalar.c', 38 | 'mpfdtype/src/scalar.h', 39 | 'mpfdtype/src/terrible_hacks.c', 40 | 'mpfdtype/src/terrible_hacks.h', 41 | 'mpfdtype/src/umath.cpp', 42 | 'mpfdtype/src/umath.h', 43 | ] 44 | 45 | py.install_sources( 46 | [ 47 | 'mpfdtype/__init__.py', 48 | ], 49 | subdir: 'mpfdtype', 50 | pure: false 51 | ) 52 | 53 | py.extension_module( 54 | '_mpfdtype_main', 55 | srcs, 56 | install: true, 57 | subdir: 'mpfdtype', 58 | include_directories: includes, 59 | dependencies: [mpfr], 60 | ) 61 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from ._mpfdtype_main import MPFDType, MPFloat 4 | 5 | 6 | 7 | # Lets add some uglier hacks: 8 | 9 | # NumPy uses repr as a fallback (as of writing this code), we want to 10 | # customize the printing of MPFloats though... 11 | def mystr(obj): 12 | if isinstance(obj, MPFloat): 13 | return f"'{obj}'" 14 | return repr(obj) 15 | 16 | np.set_printoptions(formatter={"numpystr": mystr}) 17 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/casts.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_CASTS_H 2 | #define _NPY_CASTS_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | extern PyArrayMethod_Spec MPFToMPFCastSpec; 9 | 10 | PyArrayMethod_Spec ** 11 | init_casts(void); 12 | 13 | void 14 | free_casts(void); 15 | 16 | #ifdef __cplusplus 17 | } 18 | #endif 19 | 20 | #endif /* _NPY_CASTS_H */ 21 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/dtype.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL MPFDType_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL MPFDType_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #define NO_IMPORT_ARRAY 8 | #define NO_IMPORT_UFUNC 9 | #include "numpy/arrayobject.h" 10 | #include "numpy/ndarraytypes.h" 11 | #include "numpy/dtype_api.h" 12 | 13 | #include "mpfr.h" 14 | 15 | #include "scalar.h" 16 | #include "casts.h" 17 | #include "dtype.h" 18 | 19 | /* 20 | * Internal helper to create new instances. 21 | */ 22 | MPFDTypeObject * 23 | new_MPFDType_instance(mpfr_prec_t precision) 24 | { 25 | /* 26 | * The docs warn that temporary ops may need an increased max prec, so 27 | * there could a point in reducing it. But lets assume errors will get 28 | * set in that case. 29 | */ 30 | if (precision < MPFR_PREC_MIN || precision > MPFR_PREC_MAX) { 31 | PyErr_Format(PyExc_ValueError, "precision must be between %d and %d.", MPFR_PREC_MIN, 32 | MPFR_PREC_MAX); 33 | return NULL; 34 | } 35 | 36 | MPFDTypeObject *new = (MPFDTypeObject *)PyArrayDescr_Type.tp_new( 37 | /* TODO: Using NULL for args here works, but seems not clean? */ 38 | (PyTypeObject *)&MPFDType, NULL, NULL); 39 | if (new == NULL) { 40 | return NULL; 41 | } 42 | new->precision = precision; 43 | size_t size = mpfr_custom_get_size(precision); 44 | if (size > NPY_MAX_INT - sizeof(mpf_field)) { 45 | PyErr_SetString(PyExc_TypeError, 46 | "storage of single float would be too large for precision."); 47 | } 48 | new->base.elsize = sizeof(mpf_storage) + size; 49 | new->base.alignment = _Alignof(mpf_field); 50 | new->base.flags |= NPY_NEEDS_INIT; 51 | 52 | return new; 53 | } 54 | 55 | static MPFDTypeObject * 56 | ensure_canonical(MPFDTypeObject *self) 57 | { 58 | Py_INCREF(self); 59 | return self; 60 | } 61 | 62 | static MPFDTypeObject * 63 | common_instance(MPFDTypeObject *dtype1, MPFDTypeObject *dtype2) 64 | { 65 | if (dtype1->precision > dtype2->precision) { 66 | Py_INCREF(dtype1); 67 | return dtype1; 68 | } 69 | else { 70 | Py_INCREF(dtype2); 71 | return dtype2; 72 | } 73 | } 74 | 75 | static PyArray_DTypeMeta * 76 | common_dtype(PyArray_DTypeMeta *cls, PyArray_DTypeMeta *other) 77 | { 78 | /* 79 | * Typenum is useful for NumPy, but there it can still be convenient. 80 | * (New-style user dtypes will probably get -1 as type number...) 81 | */ 82 | if (other->type_num >= 0 && PyTypeNum_ISNUMBER(other->type_num) && 83 | !PyTypeNum_ISCOMPLEX(other->type_num)) { 84 | /* 85 | * A (simple) builtin numeric type (not complex) promotes to fixed 86 | * precision. 87 | */ 88 | Py_INCREF(cls); 89 | return cls; 90 | } 91 | Py_INCREF(Py_NotImplemented); 92 | return (PyArray_DTypeMeta *)Py_NotImplemented; 93 | } 94 | 95 | /* 96 | * Functions dealing with scalar logic 97 | */ 98 | 99 | static PyArray_Descr * 100 | mpf_discover_descriptor_from_pyobject(PyArray_DTypeMeta *NPY_UNUSED(cls), PyObject *obj) 101 | { 102 | if (Py_TYPE(obj) != &MPFloat_Type) { 103 | PyErr_SetString(PyExc_TypeError, "Can only store MPFloat in a MPFDType array."); 104 | return NULL; 105 | } 106 | mpfr_prec_t prec = get_prec_from_object(obj); 107 | if (prec < 0) { 108 | return NULL; 109 | } 110 | return (PyArray_Descr *)new_MPFDType_instance(prec); 111 | } 112 | 113 | static int 114 | mpf_setitem(MPFDTypeObject *descr, PyObject *obj, char *dataptr) 115 | { 116 | MPFloatObject *value; 117 | if (PyObject_TypeCheck(obj, &MPFloat_Type)) { 118 | Py_INCREF(obj); 119 | value = (MPFloatObject *)obj; 120 | } 121 | else { 122 | value = MPFloat_from_object(obj, -1); 123 | if (value == NULL) { 124 | return -1; 125 | } 126 | } 127 | // TODO: This doesn't support unaligned access, maybe we should just 128 | // allow DTypes to say that they cannot be unaligned?! 129 | 130 | mpfr_ptr res; 131 | mpf_load(res, dataptr, descr->precision); 132 | mpfr_set(res, value->mpf.x, MPFR_RNDN); 133 | mpf_store(dataptr, res); 134 | 135 | Py_DECREF(value); 136 | return 0; 137 | } 138 | 139 | /* 140 | * Note, same as above (but more). For correct support in HPy we likely need 141 | * to pass an `owner` here. But, we probably also need to pass a "base", 142 | * because that is how structured scalars work (they return a view...). 143 | * Those two might have subtly different logic, though? 144 | * (Realistically, maybe we can special case void to not pass the base, I do 145 | * not think that a scalar should ever be a view, such a scalar should not 146 | * exist. E.g. in that case, better not have a scalar at all to begin with.) 147 | */ 148 | static PyObject * 149 | mpf_getitem(MPFDTypeObject *descr, char *dataptr) 150 | { 151 | MPFloatObject *new = MPFLoat_raw_new(descr->precision); 152 | if (new == NULL) { 153 | return NULL; 154 | } 155 | mpfr_ptr val; 156 | mpf_load(val, dataptr, descr->precision); 157 | mpfr_set(new->mpf.x, val, MPFR_RNDN); 158 | 159 | return (PyObject *)new; 160 | } 161 | 162 | static PyType_Slot MPFDType_Slots[] = { 163 | {NPY_DT_ensure_canonical, &ensure_canonical}, 164 | {NPY_DT_common_instance, &common_instance}, 165 | {NPY_DT_common_dtype, &common_dtype}, 166 | {NPY_DT_discover_descr_from_pyobject, &mpf_discover_descriptor_from_pyobject}, 167 | {NPY_DT_setitem, &mpf_setitem}, 168 | {NPY_DT_getitem, &mpf_getitem}, 169 | {0, NULL}}; 170 | 171 | /* 172 | * The following defines everything type object related (i.e. not NumPy 173 | * specific). 174 | * 175 | * Note that this function is by default called without any arguments to fetch 176 | * a default version of the descriptor (in principle at least). During init 177 | * we fill in `cls->singleton` though for the dimensionless unit. 178 | */ 179 | static PyObject * 180 | MPFDType_new(PyTypeObject *NPY_UNUSED(cls), PyObject *args, PyObject *kwds) 181 | { 182 | static char *kwargs_strs[] = {"precision", NULL}; 183 | 184 | Py_ssize_t precision; 185 | 186 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "n:MPFDType", kwargs_strs, &precision)) { 187 | return NULL; 188 | } 189 | 190 | return (PyObject *)new_MPFDType_instance(precision); 191 | } 192 | 193 | static PyObject * 194 | MPFDType_repr(MPFDTypeObject *self) 195 | { 196 | PyObject *res = PyUnicode_FromFormat("MPFDType(%ld)", (long)self->precision); 197 | return res; 198 | } 199 | 200 | PyObject * 201 | MPFDType_get_prec(MPFDTypeObject *self) 202 | { 203 | return PyLong_FromLong(self->precision); 204 | } 205 | 206 | NPY_NO_EXPORT PyGetSetDef mpfdtype_getsetlist[] = { 207 | {"prec", (getter)MPFDType_get_prec, NULL, NULL, NULL}, 208 | {NULL, NULL, NULL, NULL, NULL}, /* Sentinel */ 209 | }; 210 | 211 | /* 212 | * This is the basic things that you need to create a Python Type/Class in C. 213 | * However, there is a slight difference here because we create a 214 | * PyArray_DTypeMeta, which is a larger struct than a typical type. 215 | * (This should get a bit nicer eventually with Python >3.11.) 216 | */ 217 | PyArray_DTypeMeta MPFDType = { 218 | {{ 219 | PyVarObject_HEAD_INIT(NULL, 0).tp_name = "MPFDType.MPFDType", 220 | .tp_basicsize = sizeof(MPFDTypeObject), 221 | .tp_new = MPFDType_new, 222 | .tp_repr = (reprfunc)MPFDType_repr, 223 | .tp_str = (reprfunc)MPFDType_repr, 224 | .tp_getset = mpfdtype_getsetlist, 225 | }}, 226 | /* rest, filled in during DTypeMeta initialization */ 227 | }; 228 | 229 | int 230 | init_mpf_dtype(void) 231 | { 232 | /* 233 | * To create our DType, we have to use a "Spec" that tells NumPy how to 234 | * do it. You first have to create a static type, but see the note there! 235 | */ 236 | PyArrayMethod_Spec **casts = init_casts(); 237 | 238 | PyArrayDTypeMeta_Spec MPFDType_DTypeSpec = { 239 | .flags = NPY_DT_PARAMETRIC | NPY_DT_NUMERIC, 240 | .casts = casts, 241 | .typeobj = &MPFloat_Type, 242 | .slots = MPFDType_Slots, 243 | }; 244 | /* Loaded dynamically, so may need to be set here: */ 245 | ((PyObject *)&MPFDType)->ob_type = &PyArrayDTypeMeta_Type; 246 | ((PyTypeObject *)&MPFDType)->tp_base = &PyArrayDescr_Type; 247 | if (PyType_Ready((PyTypeObject *)&MPFDType) < 0) { 248 | free_casts(); 249 | return -1; 250 | } 251 | 252 | if (PyArrayInitDTypeMeta_FromSpec(&MPFDType, &MPFDType_DTypeSpec) < 0) { 253 | free_casts(); 254 | return -1; 255 | } 256 | 257 | free_casts(); 258 | return 0; 259 | } 260 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/dtype.h: -------------------------------------------------------------------------------- 1 | #ifndef _MPRFDTYPE_DTYPE_H 2 | #define _MPRFDTYPE_DTYPE_H 3 | 4 | #include "mpfr.h" 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | #include "scalar.h" 11 | 12 | 13 | typedef struct { 14 | PyArray_Descr base; 15 | mpfr_prec_t precision; 16 | } MPFDTypeObject; 17 | 18 | 19 | /* 20 | * It would be more compat to just store the kind, exponent and signfificand, 21 | * however. For in-place operations mpfr needs cannot share the same 22 | * significand for multiple ops (but can have an op repeat). 23 | * So not storeing only those (saving 16 bytes of 48 for a 128 bit number) 24 | * removes the need to worry about this. 25 | */ 26 | typedef mpfr_t mpf_storage; 27 | 28 | extern PyArray_DTypeMeta MPFDType; 29 | 30 | 31 | /* 32 | * Load into an mpfr_ptr, use a macro which may allow easier changing back 33 | * to a compact storage scheme. 34 | */ 35 | static inline void 36 | _mpf_load(mpfr_ptr *x, char *data_ptr, mpfr_prec_t precision) { 37 | x[0] = (mpfr_ptr)data_ptr; 38 | /* 39 | * We must ensure the signficand is initialized, but NumPy only ensures 40 | * everything is NULL'ed. 41 | */ 42 | if (mpfr_custom_get_significand(x[0]) == NULL) { 43 | void *signficand = data_ptr + sizeof(mpf_storage); 44 | mpfr_custom_init(signficand, precision); 45 | mpfr_custom_init_set(x[0], MPFR_NAN_KIND, 0, precision, signficand); 46 | } 47 | } 48 | #define mpf_load(x, data_ptr, precision) _mpf_load(&x, data_ptr, precision) 49 | 50 | 51 | 52 | /* 53 | * Not actually required in the current scheme, but keep for now. 54 | * (I had a more compat storage scheme at some point.) 55 | */ 56 | static inline void 57 | mpf_store(char *data_ptr, mpfr_t x) { 58 | assert(data_ptr == (char *)x); 59 | } 60 | 61 | 62 | MPFDTypeObject * 63 | new_MPFDType_instance(mpfr_prec_t precision); 64 | 65 | int 66 | init_mpf_dtype(void); 67 | 68 | #ifdef __cplusplus 69 | } 70 | #endif 71 | 72 | #endif /*_MPRFDTYPE_DTYPE_H*/ 73 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/mpfdtype_main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL MPFDType_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL MPFDType_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #include "numpy/arrayobject.h" 7 | #include "numpy/ufuncobject.h" 8 | #include "numpy/dtype_api.h" 9 | 10 | #include "dtype.h" 11 | #include "umath.h" 12 | #include "terrible_hacks.h" 13 | 14 | static struct PyModuleDef moduledef = { 15 | PyModuleDef_HEAD_INIT, 16 | .m_name = "mpfdtype_main", 17 | .m_size = -1, 18 | }; 19 | 20 | /* Module initialization function */ 21 | PyMODINIT_FUNC 22 | PyInit__mpfdtype_main(void) 23 | { 24 | import_array(); 25 | import_umath(); 26 | 27 | PyObject *m = PyModule_Create(&moduledef); 28 | if (m == NULL) { 29 | return NULL; 30 | } 31 | 32 | if (init_mpf_scalar() < 0) { 33 | goto error; 34 | } 35 | 36 | if (PyModule_AddObject(m, "MPFloat", (PyObject *)&MPFloat_Type) < 0) { 37 | goto error; 38 | } 39 | 40 | if (init_mpf_dtype() < 0) { 41 | goto error; 42 | } 43 | 44 | if (PyModule_AddObject(m, "MPFDType", (PyObject *)&MPFDType) < 0) { 45 | goto error; 46 | } 47 | 48 | if (init_mpf_umath() == -1) { 49 | goto error; 50 | } 51 | 52 | if (init_terrible_hacks() < 0) { 53 | goto error; 54 | } 55 | 56 | return m; 57 | 58 | error: 59 | Py_DECREF(m); 60 | return NULL; 61 | } 62 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/numbers.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file defines scalar numeric operations. 3 | */ 4 | 5 | #define PY_ARRAY_UNIQUE_SYMBOL MPFDType_ARRAY_API 6 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 7 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 8 | #define NO_IMPORT_ARRAY 9 | 10 | extern "C" { 11 | #include 12 | 13 | #include "numpy/arrayobject.h" 14 | #include "numpy/ndarraytypes.h" 15 | #include "numpy/ufuncobject.h" 16 | 17 | #include "numpy/dtype_api.h" 18 | } 19 | 20 | #include 21 | 22 | #include "scalar.h" 23 | #include "ops.hpp" 24 | 25 | #include "numbers.h" 26 | 27 | 28 | template 29 | static PyObject * 30 | unary_func(MPFloatObject *self) 31 | { 32 | MPFloatObject *res = MPFLoat_raw_new(mpfr_get_prec(self->mpf.x)); 33 | if (res == NULL) { 34 | return NULL; 35 | } 36 | 37 | unary_op(self->mpf.x, res->mpf.x); 38 | return (PyObject *)res; 39 | } 40 | 41 | PyObject * 42 | nonzero(MPFloatObject *mpf) 43 | { 44 | return PyBool_FromLong(mpfr_zero_p(mpf->mpf.x)); 45 | } 46 | 47 | 48 | template 49 | static PyObject * 50 | binary_func(PyObject *op1, PyObject *op2) 51 | { 52 | MPFloatObject *self; 53 | PyObject *other; 54 | MPFloatObject *other_mpf = NULL; 55 | mpfr_prec_t precision; 56 | 57 | int is_forward; /* We may be a forward operation (so can defer) */ 58 | if (PyObject_TypeCheck(op1, &MPFloat_Type)) { 59 | is_forward = 1; 60 | self = (MPFloatObject *)op1; 61 | other = Py_NewRef(op2); 62 | } 63 | else { 64 | is_forward = 0; 65 | self = (MPFloatObject *)op2; 66 | other = Py_NewRef(op1); 67 | } 68 | 69 | precision = mpfr_get_prec(self->mpf.x); 70 | 71 | if (PyObject_TypeCheck(other, &MPFloat_Type)) { 72 | /* All good, we can continue, both are MPFloats */ 73 | Py_INCREF(other); 74 | other_mpf = (MPFloatObject *)other; 75 | precision = std::max(precision, mpfr_get_prec(other_mpf->mpf.x)); 76 | } 77 | else if (PyLong_CheckExact(other) || PyFloat_CheckExact(other) || 78 | (!is_forward && 79 | (PyLong_Check(other) || PyFloat_Check(other)))) { 80 | // TODO: We want weak handling, so truncate precision. But is it 81 | // correct to do it here? (not that it matters much...) 82 | other_mpf = MPFloat_from_object(other, precision); 83 | if (other_mpf == NULL) { 84 | return NULL; 85 | } 86 | } 87 | else { 88 | /* Defer to the other (NumPy types are handled through array path) */ 89 | Py_RETURN_NOTIMPLEMENTED; 90 | } 91 | 92 | MPFloatObject *res = MPFLoat_raw_new(precision); 93 | if (res == NULL) { 94 | Py_DECREF(other_mpf); 95 | return NULL; 96 | } 97 | if (is_forward) { 98 | binary_op(res->mpf.x, self->mpf.x, other_mpf->mpf.x); 99 | } 100 | else{ 101 | binary_op(res->mpf.x, other_mpf->mpf.x, self->mpf.x); 102 | } 103 | 104 | Py_DECREF(other_mpf); 105 | return (PyObject *)res; 106 | } 107 | 108 | 109 | PyObject * 110 | mpf_richcompare(MPFloatObject *self, PyObject *other, int cmp_op) 111 | { 112 | MPFloatObject *other_mpf = NULL; 113 | mpfr_prec_t precision; 114 | precision = mpfr_get_prec(self->mpf.x); 115 | 116 | /* Only accept fully known objects, is that correct? */ 117 | if (PyObject_TypeCheck(other, &MPFloat_Type)) { 118 | /* All good, we can continue, both are MPFloats */ 119 | Py_INCREF(other); 120 | other_mpf = (MPFloatObject *)other; 121 | precision = std::max(precision, mpfr_get_prec(other_mpf->mpf.x)); 122 | } 123 | else if (PyLong_CheckExact(other) || PyFloat_CheckExact(other)) { 124 | // TODO: Should we use full precision for comparison ops?! 125 | other_mpf = MPFloat_from_object(other, precision); 126 | if (other_mpf == NULL) { 127 | return NULL; 128 | } 129 | } 130 | else { 131 | /* Defer to the other (NumPy types are handled through array path) */ 132 | Py_RETURN_NOTIMPLEMENTED; 133 | } 134 | 135 | npy_bool cmp; 136 | 137 | switch (cmp_op) { 138 | case Py_LT: 139 | cmp = mpf_less(self->mpf.x, other_mpf->mpf.x); 140 | break; 141 | case Py_LE: 142 | cmp = mpf_lessequal(self->mpf.x, other_mpf->mpf.x); 143 | break; 144 | case Py_EQ: 145 | cmp = mpf_equal(self->mpf.x, other_mpf->mpf.x); 146 | break; 147 | case Py_NE: 148 | cmp = mpf_notequal(self->mpf.x, other_mpf->mpf.x); 149 | break; 150 | case Py_GT: 151 | cmp = mpf_greater(self->mpf.x, other_mpf->mpf.x); 152 | break; 153 | case Py_GE: 154 | cmp = mpf_greaterequal(self->mpf.x, other_mpf->mpf.x); 155 | break; 156 | default: 157 | Py_DECREF(other_mpf); 158 | Py_RETURN_NOTIMPLEMENTED; 159 | } 160 | Py_DECREF(other_mpf); 161 | 162 | return PyBool_FromLong(cmp); 163 | } 164 | 165 | 166 | PyNumberMethods mpf_as_number = { 167 | .nb_add = (binaryfunc)binary_func, 168 | .nb_subtract = (binaryfunc)binary_func, 169 | .nb_multiply = (binaryfunc)binary_func, 170 | //.nb_remainder = (binaryfunc)gentype_remainder, 171 | //.nb_divmod = (binaryfunc)gentype_divmod, 172 | .nb_power = (ternaryfunc)binary_func, 173 | .nb_negative = (unaryfunc)unary_func, 174 | .nb_positive = (unaryfunc)unary_func, 175 | .nb_absolute = (unaryfunc)unary_func, 176 | .nb_bool = (inquiry)nonzero, 177 | //.nb_int = (unaryfunc)gentype_int, 178 | //.nb_float = (unaryfunc)gentype_float, 179 | //.nb_floor_divide = (binaryfunc)gentype_floor_divide, 180 | .nb_true_divide = (binaryfunc)binary_func
, 181 | }; 182 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/numbers.h: -------------------------------------------------------------------------------- 1 | #ifndef _MPF_NUMBERS_H 2 | #define _MPF_NUMBERS_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | PyObject * 9 | mpf_richcompare(MPFloatObject *self, PyObject *other, int cmp_op); 10 | 11 | extern PyNumberMethods mpf_as_number; 12 | 13 | #ifdef __cplusplus 14 | } 15 | #endif 16 | 17 | #endif /* _MPF_NUMBERS_H */ 18 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/ops.hpp: -------------------------------------------------------------------------------- 1 | #include "mpfr.h" 2 | 3 | typedef int unary_op_def(mpfr_t, mpfr_t); 4 | typedef int binop_def(mpfr_t, mpfr_t, mpfr_t); 5 | typedef npy_bool cmp_def(mpfr_t, mpfr_t); 6 | 7 | 8 | /* 9 | * Unary operations 10 | */ 11 | static inline int 12 | negative(mpfr_t op, mpfr_t out) 13 | { 14 | return mpfr_neg(out, op, MPFR_RNDN); 15 | } 16 | 17 | static inline int 18 | positive(mpfr_t op, mpfr_t out) 19 | { 20 | if (out == op) { 21 | return 0; 22 | } 23 | return mpfr_set(out, op, MPFR_RNDN); 24 | } 25 | 26 | static inline int 27 | trunc(mpfr_t op, mpfr_t out) 28 | { 29 | return mpfr_trunc(out, op); 30 | } 31 | 32 | static inline int 33 | floor(mpfr_t op, mpfr_t out) 34 | { 35 | return mpfr_floor(out, op); 36 | } 37 | 38 | static inline int 39 | ceil(mpfr_t op, mpfr_t out) 40 | { 41 | return mpfr_ceil(out, op); 42 | } 43 | 44 | static inline int 45 | rint(mpfr_t op, mpfr_t out) 46 | { 47 | return mpfr_rint(out, op, MPFR_RNDN); 48 | } 49 | 50 | static inline int 51 | absolute(mpfr_t op, mpfr_t out) 52 | { 53 | return mpfr_abs(out, op, MPFR_RNDN); 54 | } 55 | 56 | static inline int 57 | sqrt(mpfr_t op, mpfr_t out) 58 | { 59 | return mpfr_pow_si(out, op, 2, MPFR_RNDN); 60 | } 61 | 62 | static inline int 63 | square(mpfr_t op, mpfr_t out) 64 | { 65 | return mpfr_sqrt(out, op, MPFR_RNDN); 66 | } 67 | 68 | static inline int 69 | log(mpfr_t op, mpfr_t out) 70 | { 71 | return mpfr_log(out, op, MPFR_RNDN); 72 | } 73 | 74 | static inline int 75 | log2(mpfr_t op, mpfr_t out) 76 | { 77 | return mpfr_log2(out, op, MPFR_RNDN); 78 | } 79 | 80 | static inline int 81 | log10(mpfr_t op, mpfr_t out) 82 | { 83 | return mpfr_log10(out, op, MPFR_RNDN); 84 | } 85 | 86 | static inline int 87 | log1p(mpfr_t op, mpfr_t out) 88 | { 89 | return mpfr_log1p(out, op, MPFR_RNDN); 90 | } 91 | 92 | static inline int 93 | exp(mpfr_t op, mpfr_t out) 94 | { 95 | return mpfr_exp(out, op, MPFR_RNDN); 96 | } 97 | 98 | static inline int 99 | exp2(mpfr_t op, mpfr_t out) 100 | { 101 | return mpfr_exp2(out, op, MPFR_RNDN); 102 | } 103 | 104 | static inline int 105 | expm1(mpfr_t op, mpfr_t out) 106 | { 107 | return mpfr_expm1(out, op, MPFR_RNDN); 108 | } 109 | 110 | static inline int 111 | sin(mpfr_t op, mpfr_t out) 112 | { 113 | return mpfr_sin(out, op, MPFR_RNDN); 114 | } 115 | 116 | static inline int 117 | cos(mpfr_t op, mpfr_t out) 118 | { 119 | return mpfr_cos(out, op, MPFR_RNDN); 120 | } 121 | 122 | static inline int 123 | tan(mpfr_t op, mpfr_t out) 124 | { 125 | return mpfr_tan(out, op, MPFR_RNDN); 126 | } 127 | 128 | static inline int 129 | arcsin(mpfr_t op, mpfr_t out) 130 | { 131 | return mpfr_asin(out, op, MPFR_RNDN); 132 | } 133 | 134 | static inline int 135 | arccos(mpfr_t op, mpfr_t out) 136 | { 137 | return mpfr_acos(out, op, MPFR_RNDN); 138 | } 139 | 140 | static inline int 141 | arctan(mpfr_t op, mpfr_t out) 142 | { 143 | return mpfr_tan(out, op, MPFR_RNDN); 144 | } 145 | 146 | 147 | /* 148 | * Binary operations 149 | */ 150 | static inline int 151 | add(mpfr_t out, mpfr_t op1, mpfr_t op2) 152 | { 153 | return mpfr_add(out, op1, op2, MPFR_RNDN); 154 | } 155 | 156 | static inline int 157 | sub(mpfr_t out, mpfr_t op1, mpfr_t op2) 158 | { 159 | return mpfr_sub(out, op1, op2, MPFR_RNDN); 160 | } 161 | 162 | static inline int 163 | mul(mpfr_t out, mpfr_t op1, mpfr_t op2) 164 | { 165 | return mpfr_mul(out, op1, op2, MPFR_RNDN); 166 | } 167 | 168 | static inline int 169 | div(mpfr_t out, mpfr_t op1, mpfr_t op2) 170 | { 171 | return mpfr_div(out, op1, op2, MPFR_RNDN); 172 | } 173 | 174 | static inline int 175 | hypot(mpfr_t out, mpfr_t op1, mpfr_t op2) 176 | { 177 | return mpfr_hypot(out, op1, op2, MPFR_RNDN); 178 | } 179 | 180 | static inline int 181 | pow(mpfr_t out, mpfr_t op1, mpfr_t op2) 182 | { 183 | return mpfr_pow(out, op1, op2, MPFR_RNDN); 184 | } 185 | 186 | static inline int 187 | arctan2(mpfr_t out, mpfr_t op1, mpfr_t op2) 188 | { 189 | return mpfr_atan2(out, op1, op2, MPFR_RNDN); 190 | } 191 | 192 | static inline int 193 | nextafter(mpfr_t out, mpfr_t op1, mpfr_t op2) 194 | { 195 | /* 196 | * Not ideal at all, but we should operate on the input, not output prec. 197 | * Plus, we actually know if this is the case or not, so could at least 198 | * short-cut (or special case both paths). 199 | */ 200 | mpfr_prec_t prec = mpfr_get_prec(op1); 201 | if (prec == mpfr_get_prec(out)) { 202 | mpfr_set(out, op1, MPFR_RNDN); 203 | mpfr_nexttoward(out, op2); 204 | return 0; 205 | } 206 | mpfr_t tmp; 207 | mpfr_init2(tmp, prec); // TODO: This could fail, mabye manual? 208 | mpfr_set(tmp, op1, MPFR_RNDN); 209 | mpfr_nexttoward(tmp, op2); 210 | int res = mpfr_set(out, tmp, MPFR_RNDN); 211 | mpfr_clear(tmp); 212 | 213 | return res; 214 | } 215 | 216 | 217 | /* 218 | * Comparisons 219 | */ 220 | static inline npy_bool 221 | mpf_equal(mpfr_t in1, mpfr_t in2) { 222 | return mpfr_equal_p(in1, in2) != 0; 223 | } 224 | 225 | static inline npy_bool 226 | mpf_notequal(mpfr_t in1, mpfr_t in2) { 227 | return mpfr_equal_p(in1, in2) == 0; 228 | } 229 | 230 | static inline npy_bool 231 | mpf_less(mpfr_t in1, mpfr_t in2) { 232 | return mpfr_less_p(in1, in2) != 0; 233 | } 234 | 235 | static inline npy_bool 236 | mpf_lessequal(mpfr_t in1, mpfr_t in2) { 237 | return mpfr_lessequal_p(in1, in2) != 0; 238 | 239 | } 240 | static inline npy_bool 241 | mpf_greater(mpfr_t in1, mpfr_t in2) { 242 | return mpfr_greater_p(in1, in2) != 0; 243 | } 244 | 245 | static inline npy_bool 246 | mpf_greaterequal(mpfr_t in1, mpfr_t in2) { 247 | return mpfr_greaterequal_p(in1, in2) != 0; 248 | } 249 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/scalar.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL MPFDType_ARRAY_API 4 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 5 | #define NO_IMPORT_ARRAY 6 | #include "numpy/arrayobject.h" 7 | #include "numpy/ndarraytypes.h" 8 | #include "numpy/dtype_api.h" 9 | 10 | #include "scalar.h" 11 | #include "numbers.h" 12 | 13 | 14 | mpfr_prec_t 15 | get_prec_from_object(PyObject *value) { 16 | Py_ssize_t prec; 17 | if (PyFloat_Check(value)) { 18 | prec = 53; 19 | } 20 | else if (PyLong_Check(value)) { 21 | prec = 8 * sizeof(Py_ssize_t); 22 | } 23 | else if (PyObject_TypeCheck(value, &MPFloat_Type)) { 24 | prec = mpfr_get_prec(((MPFloatObject *)value)->mpf.x); 25 | } 26 | else if (PyUnicode_CheckExact(value)) { 27 | PyErr_Format(PyExc_TypeError, 28 | "MPFloat: precsion must currently be given to parse string."); 29 | return -1; 30 | } 31 | else { 32 | PyErr_SetString(PyExc_TypeError, 33 | "MPFloat value must be an MPF, float, or string"); 34 | return -1; 35 | } 36 | return prec; 37 | } 38 | 39 | 40 | /* 41 | * Get a 0 initialized new scalar, the value may be changed immediately after 42 | * getting the new scalar. (otherwise scalars are immutable) 43 | */ 44 | MPFloatObject * 45 | MPFLoat_raw_new(mpfr_prec_t prec) 46 | { 47 | size_t n_limb = mpfr_custom_get_size(prec) / sizeof(mp_limb_t); 48 | MPFloatObject *new = PyObject_NewVar(MPFloatObject, &MPFloat_Type, n_limb); 49 | if (new == NULL) { 50 | return NULL; 51 | } 52 | mpfr_custom_init_set( 53 | new->mpf.x, MPFR_ZERO_KIND, 0, prec, new->mpf.significand); 54 | 55 | return new; 56 | } 57 | 58 | 59 | MPFloatObject * 60 | MPFloat_from_object(PyObject *value, Py_ssize_t prec) 61 | { 62 | if (prec != -1) { 63 | if (prec < MPFR_PREC_MIN || prec > MPFR_PREC_MAX) { 64 | PyErr_Format(PyExc_ValueError, 65 | "precision must be between %d and %d.", 66 | MPFR_PREC_MIN, MPFR_PREC_MAX); 67 | return NULL; 68 | } 69 | } 70 | else { 71 | prec = get_prec_from_object(value); 72 | if (prec < 0) { 73 | return NULL; 74 | } 75 | } 76 | 77 | MPFloatObject *self = MPFLoat_raw_new(prec); 78 | if (self == NULL) { 79 | return NULL; 80 | } 81 | 82 | if (PyFloat_Check(value)) { 83 | mpfr_set_d(self->mpf.x, PyFloat_AsDouble(value), MPFR_RNDN); 84 | } 85 | else if (PyLong_Check(value)) { 86 | Py_ssize_t val = PyLong_AsSsize_t(value); 87 | if (val == -1 && PyErr_Occurred()) { 88 | return NULL; 89 | } 90 | int ternary = mpfr_set_sj(self->mpf.x, val, MPFR_RNDN); 91 | if (ternary != 0) { 92 | // TODO: Not sure this should always raise, since a float will 93 | // still be close to correct. 94 | PyErr_Format(PyExc_ValueError, 95 | "%zd could not be converted to MPFloat with precision %zd", 96 | val, (Py_ssize_t)prec); 97 | Py_DECREF(self); 98 | return NULL; 99 | } 100 | } 101 | else if (PyObject_TypeCheck(value, &MPFloat_Type)) { 102 | mpfr_set(self->mpf.x, ((MPFloatObject *)value)->mpf.x, MPFR_RNDN); 103 | } 104 | else if (PyUnicode_CheckExact(value)) { 105 | // TODO: Might be better to use mpfr_strtofr 106 | Py_ssize_t s_length; 107 | const char *s = PyUnicode_AsUTF8AndSize(value, &s_length); 108 | char *end; 109 | mpfr_strtofr(self->mpf.x, s, &end, 10, MPFR_RNDN); 110 | if (s + s_length != end) { 111 | PyErr_SetString(PyExc_ValueError, 112 | "unable to parse string to MPFloat."); 113 | Py_DECREF(self); 114 | return NULL; 115 | } 116 | } 117 | else { 118 | PyErr_SetString(PyExc_TypeError, 119 | "MPFloat value must be an MPF, float, or string"); 120 | Py_DECREF(self); 121 | return NULL; 122 | } 123 | 124 | return self; 125 | } 126 | 127 | 128 | static PyObject * 129 | MPFloat_new(PyTypeObject *cls, PyObject *args, PyObject *kwargs) 130 | { 131 | char *keywords[] = {"", "prec", NULL}; 132 | PyObject *value; 133 | Py_ssize_t prec = -1; /* default precision may be discovered */ 134 | 135 | if (!PyArg_ParseTupleAndKeywords( 136 | args, kwargs, "O|$n", keywords, &value, &prec)) { 137 | return NULL; 138 | } 139 | 140 | return (PyObject *)MPFloat_from_object(value, prec); 141 | } 142 | 143 | 144 | static PyObject * 145 | MPFloat_str(MPFloatObject* self) 146 | { 147 | char *val_repr; 148 | char format[100]; 149 | 150 | mpfr_prec_t precision = mpfr_min_prec(self->mpf.x); 151 | 152 | // TODO: I am not 100% sure this ensures round-tripping. 153 | size_t ndigits = mpfr_get_str_ndigits(10, precision); 154 | ndigits -= 1; 155 | if (ndigits < 0) { 156 | ndigits = 0; 157 | } 158 | snprintf(format, 99, "%%.%zdRE", ndigits); 159 | 160 | /* Note: For format characters other than "e" precision is needed */ 161 | if (mpfr_asprintf(&val_repr, format, self->mpf.x) < 0) { 162 | /* Lets assume errors must be memory errors. */ 163 | PyErr_NoMemory(); 164 | return NULL; 165 | } 166 | PyObject *str = PyUnicode_FromString(val_repr); 167 | mpfr_free_str(val_repr); 168 | return str; 169 | } 170 | 171 | 172 | static PyObject * 173 | MPFloat_repr(MPFloatObject* self) 174 | { 175 | PyObject *val_repr = MPFloat_str(self); 176 | if (val_repr == NULL) { 177 | return NULL; 178 | } 179 | 180 | PyObject *res = PyUnicode_FromFormat( 181 | "MPFloat('%S', prec=%zd)", val_repr, mpfr_get_prec(self->mpf.x)); 182 | Py_DECREF(val_repr); 183 | return res; 184 | } 185 | 186 | 187 | static PyObject * 188 | MPFloat_get_prec(MPFloatObject *self) 189 | { 190 | return PyLong_FromLong(mpfr_get_prec(self->mpf.x)); 191 | } 192 | 193 | 194 | NPY_NO_EXPORT PyGetSetDef mpfloat_getsetlist[] = { 195 | {"prec", 196 | (getter)MPFloat_get_prec, 197 | NULL, 198 | NULL, NULL}, 199 | {NULL, NULL, NULL, NULL, NULL}, /* Sentinel */ 200 | }; 201 | 202 | 203 | PyTypeObject MPFloat_Type = { 204 | PyVarObject_HEAD_INIT(NULL, 0) 205 | .tp_name = "MPFDType.MPFDType", 206 | .tp_basicsize = sizeof(MPFloatObject), 207 | .tp_itemsize = sizeof(mp_limb_t), 208 | .tp_new = MPFloat_new, 209 | .tp_repr = (reprfunc)MPFloat_repr, 210 | .tp_str = (reprfunc)MPFloat_str, 211 | .tp_as_number = &mpf_as_number, 212 | .tp_richcompare = (richcmpfunc)mpf_richcompare, 213 | .tp_getset = mpfloat_getsetlist, 214 | }; 215 | 216 | 217 | int 218 | init_mpf_scalar(void) 219 | { 220 | return PyType_Ready(&MPFloat_Type); 221 | } 222 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/scalar.h: -------------------------------------------------------------------------------- 1 | #ifndef _MPRFDTYPE_SCALAR_H 2 | #define _MPRFDTYPE_SCALAR_H 3 | 4 | #include "mpfr.h" 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | #include 11 | 12 | 13 | typedef struct { 14 | mpfr_t x; 15 | mp_limb_t significand[]; 16 | } mpf_field; 17 | 18 | 19 | typedef struct { 20 | PyObject_VAR_HEAD; 21 | mpf_field mpf; 22 | } MPFloatObject; 23 | 24 | 25 | extern PyTypeObject MPFloat_Type; 26 | 27 | MPFloatObject * 28 | MPFLoat_raw_new(mpfr_prec_t prec); 29 | 30 | mpfr_prec_t 31 | get_prec_from_object(PyObject *value); 32 | 33 | MPFloatObject * 34 | MPFloat_from_object(PyObject *value, Py_ssize_t prec); 35 | 36 | int 37 | init_mpf_scalar(void); 38 | 39 | #ifdef __cplusplus 40 | } 41 | #endif 42 | 43 | #endif /* _MPRFDTYPE_SCALAR_H */ -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/terrible_hacks.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL MPFDType_ARRAY_API 4 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 5 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 6 | #define NO_IMPORT_ARRAY 7 | #include "numpy/arrayobject.h" 8 | #include "numpy/ndarraytypes.h" 9 | #include "numpy/dtype_api.h" 10 | 11 | #include "mpfr.h" 12 | 13 | #include "dtype.h" 14 | #include "scalar.h" 15 | /* 16 | * Some terrible hacks to make things work that need to be improved in NumPy. 17 | */ 18 | 19 | 20 | /* 21 | * A previous verion had a more tricky copyswap, but really we can just 22 | * copy the itemsize, needed because NumPy still uses it occasionally 23 | * (for larger itemsizes at least). 24 | */ 25 | static void 26 | copyswap_mpf(char *dst, char *src, int swap, PyArrayObject *ap) 27 | { 28 | /* Note that it is probably better to only get the descr from `ap` */ 29 | PyArray_Descr *descr = PyArray_DESCR(ap); 30 | 31 | /* copy data and then fix significand (could also do same as cast...) */ 32 | memcpy(dst, src, descr->elsize); 33 | // TODO: To support unaligned data, only need to do this if it is aligned: 34 | mpfr_custom_move((mpfr_ptr)dst, ((mpf_field *)dst)->significand); 35 | } 36 | 37 | 38 | /* Should only be used for sorting, so more complex than necessary, probably */ 39 | int compare_mpf(char *in1_ptr, char *in2_ptr, int swap, PyArrayObject *ap) 40 | { 41 | /* Note that it is probably better to only get the descr from `ap` */ 42 | mpfr_prec_t precision = ((MPFDTypeObject *)PyArray_DESCR(ap))->precision; 43 | 44 | mpfr_ptr in1, in2; 45 | 46 | mpf_load(in1, in1_ptr, precision); 47 | mpf_load(in2, in2_ptr, precision); 48 | 49 | if (!mpfr_total_order_p(in1, in2)) { 50 | return 1; 51 | } 52 | if (!mpfr_total_order_p(in2, in1)) { 53 | return -1; 54 | } 55 | return 0; 56 | } 57 | 58 | 59 | int 60 | init_terrible_hacks(void) { 61 | /* Defaults to -1 byt ISNUMBER misfires for it, so use MAX */ 62 | MPFDType.type_num = NPY_MAX_INT; 63 | 64 | /* 65 | * Add a some ->f slots the terrible way: 66 | */ 67 | MPFDTypeObject *descr = new_MPFDType_instance(10); 68 | if (descr == NULL) { 69 | return -1; 70 | } 71 | /* ->f slots are the same for all instances (currently). */ 72 | PyDataType_GetArrFuncs(&descr->base)->copyswap = (PyArray_CopySwapFunc *)©swap_mpf; 73 | PyDataType_GetArrFuncs(&descr->base)->compare = (PyArray_CompareFunc *)&compare_mpf; 74 | Py_DECREF(descr); 75 | 76 | return 0; 77 | } 78 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/terrible_hacks.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | int 5 | init_terrible_hacks(void); 6 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/src/umath.h: -------------------------------------------------------------------------------- 1 | #ifndef _MPRFDTYPE_UMATH_H 2 | #define _MPRFDTYPE_UMATH_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | int 9 | init_mpf_umath(void); 10 | 11 | #ifdef __cplusplus 12 | } 13 | #endif 14 | 15 | #endif /*_MPRFDTYPE_UMATH_H */ 16 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["NUMPY_EXPERIMENTAL_DTYPE_API"] = "1" 4 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/tests/test_array.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_array_equal 3 | 4 | from mpfdtype import MPFDType 5 | 6 | 7 | def test_advanced_indexing(): 8 | # As of writing the test, this relies on copyswap 9 | arr = np.arange(100).astype(MPFDType(100)) 10 | orig = np.arange(100).astype(MPFDType(100)) # second one, not a copy 11 | 12 | b = arr[[1, 2, 3, 4]] 13 | b[...] = 5 # does not mutate arr (internal references not broken) 14 | assert_array_equal(arr, orig) 15 | 16 | 17 | def test_is_numeric(): 18 | assert MPFDType._is_numeric 19 | -------------------------------------------------------------------------------- /mpfdtype/mpfdtype/tests/test_scalar.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import sys 4 | import numpy as np 5 | import operator 6 | 7 | from mpfdtype import MPFDType, MPFloat 8 | 9 | 10 | def test_create_scalar_simple(): 11 | # currently inferring 53bit precision from float: 12 | assert MPFloat(12.).prec == 53 13 | # currently infers 64bit or 32bit depending on system: 14 | assert MPFloat(1).prec == sys.maxsize.bit_count() + 1 15 | 16 | assert MPFloat(MPFloat(12.)).prec == 53 17 | assert MPFloat(MPFloat(1)).prec == sys.maxsize.bit_count() + 1 18 | 19 | 20 | def test_create_scalar_prec(): 21 | assert MPFloat(1, prec=100).prec == 100 22 | assert MPFloat(12., prec=123).prec == 123 23 | assert MPFloat("12.234", prec=1000).prec == 1000 24 | 25 | mpf1 = MPFloat("12.4325", prec=120) 26 | mpf2 = MPFloat(mpf1, prec=150) 27 | assert mpf1 == mpf2 28 | assert mpf2.prec == 150 29 | 30 | 31 | def test_basic_equality(): 32 | assert MPFloat(12) == MPFloat(12.) == MPFloat("12.00", prec=10) 33 | 34 | 35 | @pytest.mark.parametrize("val", [123532.543, 12893283.5]) 36 | def test_scalar_repr(val): 37 | # For non exponentials at least, the repr matches: 38 | val_repr = f"{val:e}".upper() 39 | expected = f"MPFloat('{val_repr}', prec=20)" 40 | assert repr(MPFloat(val, prec=20)) == expected 41 | 42 | @pytest.mark.parametrize("op", 43 | ["add", "sub", "mul", "pow"]) 44 | @pytest.mark.parametrize("other", [3., 12.5, 100., np.nan, np.inf]) 45 | def test_binary_ops(op, other): 46 | # Generally, the math ops should behave the same as double math if they 47 | # use double precision (which they currently do). 48 | # (double could have errors, but not for these simple ops) 49 | op = getattr(operator, op) 50 | try: 51 | expected = op(12.5, other) 52 | except Exception as e: 53 | with pytest.raises(type(e)): 54 | op(MPFloat(12.5), other) 55 | with pytest.raises(type(e)): 56 | op(12.5, MPFloat(other)) 57 | with pytest.raises(type(e)): 58 | op(MPFloat(12.5), MPFloat(other)) 59 | else: 60 | if np.isnan(expected): 61 | # Avoiding isnan (which was also not implemented when written) 62 | res = op(MPFloat(12.5), other) 63 | assert res != res 64 | res = op(12.5, MPFloat(other)) 65 | assert res != res 66 | res = op(MPFloat(12.5), MPFloat(other)) 67 | assert res != res 68 | else: 69 | assert op(MPFloat(12.5), other) == expected 70 | assert op(12.5, MPFloat(other)) == expected 71 | assert op(MPFloat(12.5), MPFloat(other)) == expected 72 | 73 | 74 | @pytest.mark.parametrize("op", 75 | ["eq", "ne", "le", "lt", "ge", "gt"]) 76 | @pytest.mark.parametrize("other", [3., 12.5, 100., np.nan, np.inf]) 77 | def test_comparisons(op, other): 78 | op = getattr(operator, op) 79 | expected = op(12.5, other) 80 | assert op(MPFloat(12.5), other) is expected 81 | assert op(12.5, MPFloat(other)) is expected 82 | assert op(MPFloat(12.5), MPFloat(other)) is expected 83 | 84 | 85 | @pytest.mark.parametrize("op", 86 | ["neg", "pos", "abs"]) 87 | @pytest.mark.parametrize("val", [3., 12.5, 100., np.nan, np.inf]) 88 | def test_comparisons(op, val): 89 | op = getattr(operator, op) 90 | expected = op(val) 91 | if np.isnan(expected): 92 | assert op(MPFloat(val)) != op(MPFloat(val)) 93 | else: 94 | assert op(MPFloat(val)) == expected 95 | -------------------------------------------------------------------------------- /mpfdtype/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "meson>=0.63.0", 4 | "meson-python", 5 | "patchelf", 6 | "wheel", 7 | "numpy", 8 | ] 9 | build-backend = "mesonpy" 10 | 11 | [project] 12 | name = "mpfdtype" 13 | description = "A dtype backing MPFR multi-precision floats" 14 | version = "0.0.1" 15 | readme = 'README.md' 16 | author = "Sebastian Berg and NumPy Developers" 17 | requires-python = ">=3.9.0" 18 | dependencies = [ 19 | "numpy" 20 | ] 21 | -------------------------------------------------------------------------------- /mpfdtype/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | IFS=$'\n\t' 4 | 5 | if [ -d "build/" ] 6 | then 7 | rm -r build 8 | fi 9 | 10 | #meson setup build -Db_sanitize=address,undefined 11 | meson setup build 12 | python -m pip uninstall -y mpfdtype 13 | python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -Csetup-args="-Dbuildtype=debug" 14 | #python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' 15 | -------------------------------------------------------------------------------- /quaddtype/README.md: -------------------------------------------------------------------------------- 1 | # Numpy-QuadDType 2 | 3 | ## Installation 4 | 5 | ``` 6 | pip install numpy==2.1.0 7 | pip install -i https://test.pypi.org/simple/ quaddtype 8 | ``` 9 | 10 | ## Usage 11 | 12 | ```python 13 | import numpy as np 14 | from numpy_quaddtype import QuadPrecDType, QuadPrecision 15 | 16 | # using sleef backend (default) 17 | np.array([1,2,3], dtype=QuadPrecDType()) 18 | np.array([1,2,3], dtype=QuadPrecDType("sleef")) 19 | 20 | # using longdouble backend 21 | np.array([1,2,3], dtype=QuadPrecDType("longdouble")) 22 | ``` 23 | 24 | ## Install from source 25 | 26 | The code needs the quad precision pieces of the sleef library, which 27 | is not available on most systems by default, so we have to generate 28 | that first. The below assumes one has the required pieces to build 29 | sleef (cmake and libmpfr-dev), and that one is in the package 30 | directory locally. 31 | 32 | ``` 33 | git clone https://github.com/shibatch/sleef.git 34 | cd sleef 35 | cmake -S . -B build -DSLEEF_BUILD_QUAD:BOOL=ON -DSLEEF_BUILD_SHARED_LIBS:BOOL=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON 36 | cmake --build build/ --clean-first -j 37 | cd .. 38 | ``` 39 | 40 | In principle, one can now install this system-wide, but easier would 41 | seem to use the version that was just created, as follows: 42 | ``` 43 | export SLEEF_DIR=$PWD/sleef/build 44 | export LIBRARY_PATH=$SLEEF_DIR/lib 45 | export C_INCLUDE_PATH=$SLEEF_DIR/include 46 | export CPLUS_INCLUDE_PATH=$SLEEF_DIR/include 47 | python3 -m venv temp 48 | source temp/bin/activate 49 | pip install meson-python numpy pytest 50 | pip install -e . -v --no-build-isolation 51 | export LD_LIBRARY_PATH=$SLEEF_DIR/lib 52 | ``` 53 | 54 | Here, we created an editable install on purpose, so one can just work 55 | from the package directory if needed, e.g., to run the tests with, 56 | ``` 57 | python -m pytest 58 | ``` 59 | -------------------------------------------------------------------------------- /quaddtype/meson.build: -------------------------------------------------------------------------------- 1 | project('numpy_quaddtype', 'c', 'cpp', default_options : ['cpp_std=c++17', 'b_pie=true']) 2 | 3 | py_mod = import('python') 4 | py = py_mod.find_installation() 5 | 6 | c = meson.get_compiler('c') 7 | 8 | sleef_dep = c.find_library('sleef') 9 | sleefquad_dep = c.find_library('sleefquad') 10 | 11 | incdir_numpy = run_command(py, 12 | [ 13 | '-c', 14 | 'import numpy; import os; print(os.path.relpath(numpy.get_include()))' 15 | ], 16 | check: true 17 | ).stdout().strip() 18 | 19 | includes = include_directories( 20 | [ 21 | incdir_numpy, 22 | 'numpy_quaddtype/src', 23 | ] 24 | ) 25 | 26 | srcs = [ 27 | 'numpy_quaddtype/src/quad_common.h', 28 | 'numpy_quaddtype/src/casts.h', 29 | 'numpy_quaddtype/src/casts.cpp', 30 | 'numpy_quaddtype/src/scalar.h', 31 | 'numpy_quaddtype/src/scalar.c', 32 | 'numpy_quaddtype/src/dtype.h', 33 | 'numpy_quaddtype/src/dtype.c', 34 | 'numpy_quaddtype/src/quaddtype_main.c', 35 | 'numpy_quaddtype/src/scalar_ops.h', 36 | 'numpy_quaddtype/src/scalar_ops.cpp', 37 | 'numpy_quaddtype/src/ops.hpp', 38 | 'numpy_quaddtype/src/umath.h', 39 | 'numpy_quaddtype/src/umath.cpp', 40 | 'numpy_quaddtype/src/dragon4.h', 41 | 'numpy_quaddtype/src/dragon4.c' 42 | ] 43 | 44 | py.install_sources( 45 | [ 46 | 'numpy_quaddtype/__init__.py', 47 | ], 48 | subdir: 'numpy_quaddtype', 49 | pure: false 50 | ) 51 | 52 | py.extension_module('_quaddtype_main', 53 | srcs, 54 | c_args: ['-g', '-O0', '-lsleef', '-lsleefquad'], 55 | dependencies: [sleef_dep, sleefquad_dep], 56 | install: true, 57 | subdir: 'numpy_quaddtype', 58 | include_directories: includes 59 | ) -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/__init__.py: -------------------------------------------------------------------------------- 1 | from ._quaddtype_main import ( 2 | QuadPrecision, 3 | QuadPrecDType, 4 | is_longdouble_128, 5 | get_sleef_constant 6 | ) 7 | 8 | __all__ = [ 9 | 'QuadPrecision', 'QuadPrecDType', 'SleefQuadPrecision', 'LongDoubleQuadPrecision', 10 | 'SleefQuadPrecDType', 'LongDoubleQuadPrecDType', 'is_longdouble_128', 'pi', 'e', 11 | 'log2e', 'log10e', 'ln2', 'ln10', 'max_value', 'min_value', 'epsilon' 12 | ] 13 | 14 | def SleefQuadPrecision(value): 15 | return QuadPrecision(value, backend='sleef') 16 | 17 | def LongDoubleQuadPrecision(value): 18 | return QuadPrecision(value, backend='longdouble') 19 | 20 | def SleefQuadPrecDType(): 21 | return QuadPrecDType(backend='sleef') 22 | 23 | def LongDoubleQuadPrecDType(): 24 | return QuadPrecDType(backend='longdouble') 25 | 26 | pi = get_sleef_constant("pi") 27 | e = get_sleef_constant("e") 28 | log2e = get_sleef_constant("log2e") 29 | log10e = get_sleef_constant("log10e") 30 | ln2 = get_sleef_constant("ln2") 31 | ln10 = get_sleef_constant("ln10") 32 | max_value = get_sleef_constant("quad_max") 33 | min_value = get_sleef_constant("quad_min") 34 | epsilon = get_sleef_constant("epsilon") -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/casts.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUADDTYPE_CASTS_H 2 | #define _QUADDTYPE_CASTS_H 3 | 4 | #include 5 | #include "numpy/dtype_api.h" 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | PyArrayMethod_Spec ** 12 | init_casts(void); 13 | 14 | void 15 | free_casts(void); 16 | 17 | #ifdef __cplusplus 18 | } 19 | #endif 20 | 21 | #endif -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/dragon4.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUADDTYPE_DRAGON4_H 2 | #define _QUADDTYPE_DRAGON4_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include "numpy/arrayobject.h" 10 | #include 11 | #include "quad_common.h" 12 | 13 | typedef enum DigitMode 14 | { 15 | /* Round digits to print shortest uniquely identifiable number. */ 16 | DigitMode_Unique, 17 | /* Output the digits of the number as if with infinite precision */ 18 | DigitMode_Exact, 19 | } DigitMode; 20 | 21 | typedef enum CutoffMode 22 | { 23 | /* up to cutoffNumber significant digits */ 24 | CutoffMode_TotalLength, 25 | /* up to cutoffNumber significant digits past the decimal point */ 26 | CutoffMode_FractionLength, 27 | } CutoffMode; 28 | 29 | typedef enum TrimMode 30 | { 31 | TrimMode_None, /* don't trim zeros, always leave a decimal point */ 32 | TrimMode_LeaveOneZero, /* trim all but the zero before the decimal point */ 33 | TrimMode_Zeros, /* trim all trailing zeros, leave decimal point */ 34 | TrimMode_DptZeros, /* trim trailing zeros & trailing decimal point */ 35 | } TrimMode; 36 | 37 | typedef struct { 38 | int scientific; 39 | DigitMode digit_mode; 40 | CutoffMode cutoff_mode; 41 | int precision; 42 | int min_digits; 43 | int sign; 44 | TrimMode trim_mode; 45 | int digits_left; 46 | int digits_right; 47 | int exp_digits; 48 | } Dragon4_Options; 49 | 50 | PyObject *Dragon4_Positional_QuadDType(Sleef_quad *val, DigitMode digit_mode, 51 | CutoffMode cutoff_mode, int precision, int min_digits, 52 | int sign, TrimMode trim, int pad_left, int pad_right); 53 | 54 | PyObject *Dragon4_Scientific_QuadDType(Sleef_quad *val, DigitMode digit_mode, 55 | int precision, int min_digits, int sign, TrimMode trim, 56 | int pad_left, int exp_digits); 57 | 58 | PyObject *Dragon4_Positional(PyObject *obj, DigitMode digit_mode, 59 | CutoffMode cutoff_mode, int precision, int min_digits, 60 | int sign, TrimMode trim, int pad_left, int pad_right); 61 | 62 | PyObject *Dragon4_Scientific(PyObject *obj, DigitMode digit_mode, int precision, 63 | int min_digits, int sign, TrimMode trim, int pad_left, 64 | int exp_digits); 65 | 66 | #ifdef __cplusplus 67 | } 68 | #endif 69 | 70 | #endif /* _QUADDTYPE_DRAGON4_H */ -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/dtype.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API 6 | #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API 7 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 8 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 9 | #define NO_IMPORT_ARRAY 10 | #define NO_IMPORT_UFUNC 11 | #include "numpy/arrayobject.h" 12 | #include "numpy/ndarraytypes.h" 13 | #include "numpy/dtype_api.h" 14 | 15 | #include "scalar.h" 16 | #include "casts.h" 17 | #include "dtype.h" 18 | #include "dragon4.h" 19 | 20 | static inline int 21 | quad_load(void *x, char *data_ptr, QuadBackendType backend) 22 | { 23 | if (data_ptr == NULL || x == NULL) { 24 | return -1; 25 | } 26 | if (backend == BACKEND_SLEEF) { 27 | *(Sleef_quad *)x = *(Sleef_quad *)data_ptr; 28 | } 29 | else { 30 | *(long double *)x = *(long double *)data_ptr; 31 | } 32 | return 0; 33 | } 34 | 35 | static inline int 36 | quad_store(char *data_ptr, void *x, QuadBackendType backend) 37 | { 38 | if (data_ptr == NULL || x == NULL) { 39 | return -1; 40 | } 41 | if (backend == BACKEND_SLEEF) { 42 | *(Sleef_quad *)data_ptr = *(Sleef_quad *)x; 43 | } 44 | else { 45 | *(long double *)data_ptr = *(long double *)x; 46 | } 47 | return 0; 48 | } 49 | 50 | QuadPrecDTypeObject * 51 | new_quaddtype_instance(QuadBackendType backend) 52 | { 53 | QuadBackendType target_backend = backend; 54 | if (backend != BACKEND_SLEEF && backend != BACKEND_LONGDOUBLE) { 55 | PyErr_SetString(PyExc_TypeError, "Backend must be sleef or longdouble"); 56 | return NULL; 57 | // target_backend = BACKEND_SLEEF; 58 | } 59 | 60 | QuadPrecDTypeObject *new = (QuadPrecDTypeObject *)PyArrayDescr_Type.tp_new( 61 | (PyTypeObject *)&QuadPrecDType, NULL, NULL); 62 | if (new == NULL) { 63 | return NULL; 64 | } 65 | new->base.elsize = (target_backend == BACKEND_SLEEF) ? sizeof(Sleef_quad) : sizeof(long double); 66 | new->base.alignment = 67 | (target_backend == BACKEND_SLEEF) ? _Alignof(Sleef_quad) : _Alignof(long double); 68 | new->backend = target_backend; 69 | return new; 70 | } 71 | 72 | static QuadPrecDTypeObject * 73 | ensure_canonical(QuadPrecDTypeObject *self) 74 | { 75 | Py_INCREF(self); 76 | return self; 77 | } 78 | 79 | static QuadPrecDTypeObject * 80 | common_instance(QuadPrecDTypeObject *dtype1, QuadPrecDTypeObject *dtype2) 81 | { 82 | // if backend mismatch then return SLEEF one (safe to cast ld to quad) 83 | if (dtype1->backend != dtype2->backend) { 84 | if (dtype1->backend == BACKEND_SLEEF) { 85 | Py_INCREF(dtype1); 86 | return dtype1; 87 | } 88 | 89 | Py_INCREF(dtype2); 90 | return dtype2; 91 | } 92 | Py_INCREF(dtype1); 93 | return dtype1; 94 | } 95 | 96 | static PyArray_DTypeMeta * 97 | common_dtype(PyArray_DTypeMeta *cls, PyArray_DTypeMeta *other) 98 | { 99 | // Promote integer and floating-point types to QuadPrecDType 100 | if (other->type_num >= 0 && 101 | (PyTypeNum_ISINTEGER(other->type_num) || PyTypeNum_ISFLOAT(other->type_num))) { 102 | Py_INCREF(cls); 103 | return cls; 104 | } 105 | // Don't promote complex types 106 | if (PyTypeNum_ISCOMPLEX(other->type_num)) { 107 | Py_INCREF(Py_NotImplemented); 108 | return (PyArray_DTypeMeta *)Py_NotImplemented; 109 | } 110 | 111 | Py_INCREF(Py_NotImplemented); 112 | return (PyArray_DTypeMeta *)Py_NotImplemented; 113 | } 114 | 115 | static PyArray_Descr * 116 | quadprec_discover_descriptor_from_pyobject(PyArray_DTypeMeta *NPY_UNUSED(cls), PyObject *obj) 117 | { 118 | if (Py_TYPE(obj) != &QuadPrecision_Type) { 119 | PyErr_SetString(PyExc_TypeError, "Can only store QuadPrecision in a QuadPrecDType array."); 120 | return NULL; 121 | } 122 | 123 | QuadPrecisionObject *quad_obj = (QuadPrecisionObject *)obj; 124 | 125 | return (PyArray_Descr *)new_quaddtype_instance(quad_obj->backend); 126 | } 127 | 128 | static int 129 | quadprec_setitem(QuadPrecDTypeObject *descr, PyObject *obj, char *dataptr) 130 | { 131 | QuadPrecisionObject *value; 132 | if (PyObject_TypeCheck(obj, &QuadPrecision_Type)) { 133 | Py_INCREF(obj); 134 | value = (QuadPrecisionObject *)obj; 135 | } 136 | else { 137 | value = QuadPrecision_from_object(obj, descr->backend); 138 | if (value == NULL) { 139 | return -1; 140 | } 141 | } 142 | 143 | if (quad_store(dataptr, &value->value, descr->backend) < 0) { 144 | Py_DECREF(value); 145 | char error_msg[100]; 146 | snprintf(error_msg, sizeof(error_msg), "Invalid memory location %p", (void *)dataptr); 147 | PyErr_SetString(PyExc_ValueError, error_msg); 148 | return -1; 149 | } 150 | 151 | Py_DECREF(value); 152 | return 0; 153 | } 154 | 155 | static PyObject * 156 | quadprec_getitem(QuadPrecDTypeObject *descr, char *dataptr) 157 | { 158 | QuadPrecisionObject *new = QuadPrecision_raw_new(descr->backend); 159 | if (!new) { 160 | return NULL; 161 | } 162 | if (quad_load(&new->value, dataptr, descr->backend) < 0) { 163 | Py_DECREF(new); 164 | char error_msg[100]; 165 | snprintf(error_msg, sizeof(error_msg), "Invalid memory location %p", (void *)dataptr); 166 | PyErr_SetString(PyExc_ValueError, error_msg); 167 | return NULL; 168 | } 169 | return (PyObject *)new; 170 | } 171 | 172 | static PyArray_Descr * 173 | quadprec_default_descr(PyArray_DTypeMeta *cls) 174 | { 175 | QuadPrecDTypeObject *temp = new_quaddtype_instance(BACKEND_SLEEF); 176 | return (PyArray_Descr *)temp; 177 | } 178 | 179 | static PyType_Slot QuadPrecDType_Slots[] = { 180 | {NPY_DT_ensure_canonical, &ensure_canonical}, 181 | {NPY_DT_common_instance, &common_instance}, 182 | {NPY_DT_common_dtype, &common_dtype}, 183 | {NPY_DT_discover_descr_from_pyobject, &quadprec_discover_descriptor_from_pyobject}, 184 | {NPY_DT_setitem, &quadprec_setitem}, 185 | {NPY_DT_getitem, &quadprec_getitem}, 186 | {NPY_DT_default_descr, &quadprec_default_descr}, 187 | {NPY_DT_PyArray_ArrFuncs_dotfunc, NULL}, 188 | {0, NULL}}; 189 | 190 | static PyObject * 191 | QuadPrecDType_new(PyTypeObject *NPY_UNUSED(cls), PyObject *args, PyObject *kwds) 192 | { 193 | static char *kwlist[] = {"backend", NULL}; 194 | const char *backend_str = "sleef"; 195 | 196 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s", kwlist, &backend_str)) { 197 | return NULL; 198 | } 199 | 200 | QuadBackendType backend = BACKEND_SLEEF; 201 | if (strcmp(backend_str, "longdouble") == 0) { 202 | backend = BACKEND_LONGDOUBLE; 203 | } 204 | else if (strcmp(backend_str, "sleef") != 0) { 205 | PyErr_SetString(PyExc_ValueError, "Invalid backend. Use 'sleef' or 'longdouble'."); 206 | return NULL; 207 | } 208 | 209 | return (PyObject *)quadprec_discover_descriptor_from_pyobject( 210 | &QuadPrecDType, (PyObject *)QuadPrecision_raw_new(backend)); 211 | } 212 | 213 | static PyObject * 214 | QuadPrecDType_repr(QuadPrecDTypeObject *self) 215 | { 216 | const char *backend_str = (self->backend == BACKEND_SLEEF) ? "sleef" : "longdouble"; 217 | return PyUnicode_FromFormat("QuadPrecDType(backend='%s')", backend_str); 218 | } 219 | 220 | static PyObject * 221 | QuadPrecDType_str(QuadPrecDTypeObject *self) 222 | { 223 | const char *backend_str = (self->backend == BACKEND_SLEEF) ? "sleef" : "longdouble"; 224 | return PyUnicode_FromFormat("QuadPrecDType(backend='%s')", backend_str); 225 | } 226 | 227 | PyArray_DTypeMeta QuadPrecDType = { 228 | {{ 229 | PyVarObject_HEAD_INIT(NULL, 0).tp_name = "numpy_quaddtype.QuadPrecDType", 230 | .tp_basicsize = sizeof(QuadPrecDTypeObject), 231 | .tp_new = QuadPrecDType_new, 232 | .tp_repr = (reprfunc)QuadPrecDType_repr, 233 | .tp_str = (reprfunc)QuadPrecDType_str, 234 | }}, 235 | }; 236 | 237 | int 238 | init_quadprec_dtype(void) 239 | { 240 | PyArrayMethod_Spec **casts = init_casts(); 241 | if (!casts) 242 | return -1; 243 | 244 | PyArrayDTypeMeta_Spec QuadPrecDType_DTypeSpec = { 245 | .flags = NPY_DT_PARAMETRIC | NPY_DT_NUMERIC, 246 | .casts = casts, 247 | .typeobj = &QuadPrecision_Type, 248 | .slots = QuadPrecDType_Slots, 249 | }; 250 | 251 | ((PyObject *)&QuadPrecDType)->ob_type = &PyArrayDTypeMeta_Type; 252 | 253 | ((PyTypeObject *)&QuadPrecDType)->tp_base = &PyArrayDescr_Type; 254 | 255 | if (PyType_Ready((PyTypeObject *)&QuadPrecDType) < 0) { 256 | return -1; 257 | } 258 | 259 | if (PyArrayInitDTypeMeta_FromSpec(&QuadPrecDType, &QuadPrecDType_DTypeSpec) < 0) { 260 | return -1; 261 | } 262 | 263 | free_casts(); 264 | 265 | return 0; 266 | } -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/dtype.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUADDTYPE_DTYPE_H 2 | #define _QUADDTYPE_DTYPE_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | #include "quad_common.h" 12 | 13 | typedef struct { 14 | PyArray_Descr base; 15 | QuadBackendType backend; 16 | } QuadPrecDTypeObject; 17 | 18 | extern PyArray_DTypeMeta QuadPrecDType; 19 | 20 | QuadPrecDTypeObject * 21 | new_quaddtype_instance(QuadBackendType backend); 22 | 23 | int 24 | init_quadprec_dtype(void); 25 | 26 | #ifdef __cplusplus 27 | } 28 | #endif 29 | 30 | #endif -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/ops.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // Unary Quad Operations 6 | typedef Sleef_quad (*unary_op_quad_def)(Sleef_quad *); 7 | 8 | static Sleef_quad 9 | quad_negative(Sleef_quad *op) 10 | { 11 | return Sleef_negq1(*op); 12 | } 13 | 14 | static Sleef_quad 15 | quad_positive(Sleef_quad *op) 16 | { 17 | return *op; 18 | } 19 | 20 | static inline Sleef_quad 21 | quad_absolute(Sleef_quad *op) 22 | { 23 | return Sleef_fabsq1(*op); 24 | } 25 | 26 | static inline Sleef_quad 27 | quad_rint(Sleef_quad *op) 28 | { 29 | return Sleef_rintq1(*op); 30 | } 31 | 32 | static inline Sleef_quad 33 | quad_trunc(Sleef_quad *op) 34 | { 35 | return Sleef_truncq1(*op); 36 | } 37 | 38 | static inline Sleef_quad 39 | quad_floor(Sleef_quad *op) 40 | { 41 | return Sleef_floorq1(*op); 42 | } 43 | 44 | static inline Sleef_quad 45 | quad_ceil(Sleef_quad *op) 46 | { 47 | return Sleef_ceilq1(*op); 48 | } 49 | 50 | static inline Sleef_quad 51 | quad_sqrt(Sleef_quad *op) 52 | { 53 | return Sleef_sqrtq1_u05(*op); 54 | } 55 | 56 | static inline Sleef_quad 57 | quad_square(Sleef_quad *op) 58 | { 59 | return Sleef_mulq1_u05(*op, *op); 60 | } 61 | 62 | static inline Sleef_quad 63 | quad_log(Sleef_quad *op) 64 | { 65 | return Sleef_logq1_u10(*op); 66 | } 67 | 68 | static inline Sleef_quad 69 | quad_log2(Sleef_quad *op) 70 | { 71 | return Sleef_log2q1_u10(*op); 72 | } 73 | 74 | static inline Sleef_quad 75 | quad_log10(Sleef_quad *op) 76 | { 77 | return Sleef_log10q1_u10(*op); 78 | } 79 | 80 | static inline Sleef_quad 81 | quad_log1p(Sleef_quad *op) 82 | { 83 | return Sleef_log1pq1_u10(*op); 84 | } 85 | 86 | static inline Sleef_quad 87 | quad_exp(Sleef_quad *op) 88 | { 89 | return Sleef_expq1_u10(*op); 90 | } 91 | 92 | static inline Sleef_quad 93 | quad_exp2(Sleef_quad *op) 94 | { 95 | return Sleef_exp2q1_u10(*op); 96 | } 97 | 98 | static inline Sleef_quad 99 | quad_sin(Sleef_quad *op) 100 | { 101 | return Sleef_sinq1_u10(*op); 102 | } 103 | 104 | static inline Sleef_quad 105 | quad_cos(Sleef_quad *op) 106 | { 107 | return Sleef_cosq1_u10(*op); 108 | } 109 | 110 | static inline Sleef_quad 111 | quad_tan(Sleef_quad *op) 112 | { 113 | return Sleef_tanq1_u10(*op); 114 | } 115 | 116 | static inline Sleef_quad 117 | quad_asin(Sleef_quad *op) 118 | { 119 | return Sleef_asinq1_u10(*op); 120 | } 121 | 122 | static inline Sleef_quad 123 | quad_acos(Sleef_quad *op) 124 | { 125 | return Sleef_acosq1_u10(*op); 126 | } 127 | 128 | static inline Sleef_quad 129 | quad_atan(Sleef_quad *op) 130 | { 131 | return Sleef_atanq1_u10(*op); 132 | } 133 | 134 | // Unary long double operations 135 | typedef long double (*unary_op_longdouble_def)(long double *); 136 | 137 | static inline long double 138 | ld_negative(long double *op) 139 | { 140 | return -(*op); 141 | } 142 | 143 | static inline long double 144 | ld_positive(long double *op) 145 | { 146 | return *op; 147 | } 148 | 149 | static inline long double 150 | ld_absolute(long double *op) 151 | { 152 | return fabsl(*op); 153 | } 154 | 155 | static inline long double 156 | ld_rint(long double *op) 157 | { 158 | return rintl(*op); 159 | } 160 | 161 | static inline long double 162 | ld_trunc(long double *op) 163 | { 164 | return truncl(*op); 165 | } 166 | 167 | static inline long double 168 | ld_floor(long double *op) 169 | { 170 | return floorl(*op); 171 | } 172 | 173 | static inline long double 174 | ld_ceil(long double *op) 175 | { 176 | return ceill(*op); 177 | } 178 | 179 | static inline long double 180 | ld_sqrt(long double *op) 181 | { 182 | return sqrtl(*op); 183 | } 184 | 185 | static inline long double 186 | ld_square(long double *op) 187 | { 188 | return (*op) * (*op); 189 | } 190 | 191 | static inline long double 192 | ld_log(long double *op) 193 | { 194 | return logl(*op); 195 | } 196 | 197 | static inline long double 198 | ld_log2(long double *op) 199 | { 200 | return log2l(*op); 201 | } 202 | 203 | static inline long double 204 | ld_log10(long double *op) 205 | { 206 | return log10l(*op); 207 | } 208 | 209 | static inline long double 210 | ld_log1p(long double *op) 211 | { 212 | return log1pl(*op); 213 | } 214 | 215 | static inline long double 216 | ld_exp(long double *op) 217 | { 218 | return expl(*op); 219 | } 220 | 221 | static inline long double 222 | ld_exp2(long double *op) 223 | { 224 | return exp2l(*op); 225 | } 226 | 227 | static inline long double 228 | ld_sin(long double *op) 229 | { 230 | return sinl(*op); 231 | } 232 | 233 | static inline long double 234 | ld_cos(long double *op) 235 | { 236 | return cosl(*op); 237 | } 238 | 239 | static inline long double 240 | ld_tan(long double *op) 241 | { 242 | return tanl(*op); 243 | } 244 | 245 | static inline long double 246 | ld_asin(long double *op) 247 | { 248 | return asinl(*op); 249 | } 250 | 251 | static inline long double 252 | ld_acos(long double *op) 253 | { 254 | return acosl(*op); 255 | } 256 | 257 | static inline long double 258 | ld_atan(long double *op) 259 | { 260 | return atanl(*op); 261 | } 262 | 263 | // Binary Quad operations 264 | typedef Sleef_quad (*binary_op_quad_def)(Sleef_quad *, Sleef_quad *); 265 | 266 | static inline Sleef_quad 267 | quad_add(Sleef_quad *in1, Sleef_quad *in2) 268 | { 269 | return Sleef_addq1_u05(*in1, *in2); 270 | } 271 | 272 | static inline Sleef_quad 273 | quad_sub(Sleef_quad *in1, Sleef_quad *in2) 274 | { 275 | return Sleef_subq1_u05(*in1, *in2); 276 | } 277 | 278 | static inline Sleef_quad 279 | quad_mul(Sleef_quad *a, Sleef_quad *b) 280 | { 281 | return Sleef_mulq1_u05(*a, *b); 282 | } 283 | 284 | static inline Sleef_quad 285 | quad_div(Sleef_quad *a, Sleef_quad *b) 286 | { 287 | return Sleef_divq1_u05(*a, *b); 288 | } 289 | 290 | static inline Sleef_quad 291 | quad_pow(Sleef_quad *a, Sleef_quad *b) 292 | { 293 | return Sleef_powq1_u10(*a, *b); 294 | } 295 | 296 | static inline Sleef_quad 297 | quad_mod(Sleef_quad *a, Sleef_quad *b) 298 | { 299 | return Sleef_fmodq1(*a, *b); 300 | } 301 | 302 | static inline Sleef_quad 303 | quad_minimum(Sleef_quad *in1, Sleef_quad *in2) 304 | { 305 | return Sleef_icmpleq1(*in1, *in2) ? *in1 : *in2; 306 | } 307 | 308 | static inline Sleef_quad 309 | quad_maximum(Sleef_quad *in1, Sleef_quad *in2) 310 | { 311 | return Sleef_icmpgeq1(*in1, *in2) ? *in1 : *in2; 312 | } 313 | 314 | static inline Sleef_quad 315 | quad_atan2(Sleef_quad *in1, Sleef_quad *in2) 316 | { 317 | return Sleef_atan2q1_u10(*in1, *in2); 318 | } 319 | 320 | // Binary long double operations 321 | typedef long double (*binary_op_longdouble_def)(long double *, long double *); 322 | 323 | static inline long double 324 | ld_add(long double *in1, long double *in2) 325 | { 326 | return (*in1) + (*in2); 327 | } 328 | 329 | static inline long double 330 | ld_sub(long double *in1, long double *in2) 331 | { 332 | return (*in1) - (*in2); 333 | } 334 | 335 | static inline long double 336 | ld_mul(long double *a, long double *b) 337 | { 338 | return (*a) * (*b); 339 | } 340 | 341 | static inline long double 342 | ld_div(long double *a, long double *b) 343 | { 344 | return (*a) / (*b); 345 | } 346 | 347 | static inline long double 348 | ld_pow(long double *a, long double *b) 349 | { 350 | return powl(*a, *b); 351 | } 352 | 353 | static inline long double 354 | ld_mod(long double *a, long double *b) 355 | { 356 | return fmodl(*a, *b); 357 | } 358 | 359 | static inline long double 360 | ld_minimum(long double *in1, long double *in2) 361 | { 362 | return (*in1 < *in2) ? *in1 : *in2; 363 | } 364 | 365 | static inline long double 366 | ld_maximum(long double *in1, long double *in2) 367 | { 368 | return (*in1 > *in2) ? *in1 : *in2; 369 | } 370 | 371 | static inline long double 372 | ld_atan2(long double *in1, long double *in2) 373 | { 374 | return atan2l(*in1, *in2); 375 | } 376 | 377 | // comparison quad functions 378 | typedef npy_bool (*cmp_quad_def)(const Sleef_quad *, const Sleef_quad *); 379 | 380 | static inline npy_bool 381 | quad_equal(const Sleef_quad *a, const Sleef_quad *b) 382 | { 383 | return Sleef_icmpeqq1(*a, *b); 384 | } 385 | 386 | static inline npy_bool 387 | quad_notequal(const Sleef_quad *a, const Sleef_quad *b) 388 | { 389 | return Sleef_icmpneq1(*a, *b); 390 | } 391 | 392 | static inline npy_bool 393 | quad_less(const Sleef_quad *a, const Sleef_quad *b) 394 | { 395 | return Sleef_icmpltq1(*a, *b); 396 | } 397 | 398 | static inline npy_bool 399 | quad_lessequal(const Sleef_quad *a, const Sleef_quad *b) 400 | { 401 | return Sleef_icmpleq1(*a, *b); 402 | } 403 | 404 | static inline npy_bool 405 | quad_greater(const Sleef_quad *a, const Sleef_quad *b) 406 | { 407 | return Sleef_icmpgtq1(*a, *b); 408 | } 409 | 410 | static inline npy_bool 411 | quad_greaterequal(const Sleef_quad *a, const Sleef_quad *b) 412 | { 413 | return Sleef_icmpgeq1(*a, *b); 414 | } 415 | 416 | // comparison quad functions 417 | typedef npy_bool (*cmp_londouble_def)(const long double *, const long double *); 418 | 419 | static inline npy_bool 420 | ld_equal(const long double *a, const long double *b) 421 | { 422 | return *a == *b; 423 | } 424 | 425 | static inline npy_bool 426 | ld_notequal(const long double *a, const long double *b) 427 | { 428 | return *a != *b; 429 | } 430 | 431 | static inline npy_bool 432 | ld_less(const long double *a, const long double *b) 433 | { 434 | return *a < *b; 435 | } 436 | 437 | static inline npy_bool 438 | ld_lessequal(const long double *a, const long double *b) 439 | { 440 | return *a <= *b; 441 | } 442 | 443 | static inline npy_bool 444 | ld_greater(const long double *a, const long double *b) 445 | { 446 | return *a > *b; 447 | } 448 | 449 | static inline npy_bool 450 | ld_greaterequal(const long double *a, const long double *b) 451 | { 452 | return *a >= *b; 453 | } -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/quad_common.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUADDTYPE_COMMON_H 2 | #define _QUADDTYPE_COMMON_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | typedef enum { 9 | BACKEND_INVALID = -1, 10 | BACKEND_SLEEF, 11 | BACKEND_LONGDOUBLE 12 | } QuadBackendType; 13 | 14 | #ifdef __cplusplus 15 | } 16 | #endif 17 | 18 | #endif -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/quaddtype_main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API 7 | #define PY_UFUNC_UNIQUE_SYMBOL QuadPrecType_UFUNC_API 8 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 9 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 10 | 11 | #include "numpy/arrayobject.h" 12 | #include "numpy/dtype_api.h" 13 | #include "numpy/ufuncobject.h" 14 | 15 | #include "scalar.h" 16 | #include "dtype.h" 17 | #include "umath.h" 18 | #include "quad_common.h" 19 | #include "float.h" 20 | 21 | 22 | static PyObject* py_is_longdouble_128(PyObject* self, PyObject* args) { 23 | if(sizeof(long double) == 16 && 24 | LDBL_MANT_DIG == 113 && 25 | LDBL_MAX_EXP == 16384) { 26 | Py_RETURN_TRUE; 27 | } else { 28 | Py_RETURN_FALSE; 29 | } 30 | } 31 | 32 | static PyObject* get_sleef_constant(PyObject* self, PyObject* args) { 33 | const char* constant_name; 34 | if (!PyArg_ParseTuple(args, "s", &constant_name)) { 35 | return NULL; 36 | } 37 | 38 | QuadPrecisionObject* result = QuadPrecision_raw_new(BACKEND_SLEEF); 39 | if (result == NULL) { 40 | return NULL; 41 | } 42 | 43 | if (strcmp(constant_name, "pi") == 0) { 44 | result->value.sleef_value = SLEEF_M_PIq; 45 | } else if (strcmp(constant_name, "e") == 0) { 46 | result->value.sleef_value = SLEEF_M_Eq; 47 | } else if (strcmp(constant_name, "log2e") == 0) { 48 | result->value.sleef_value = SLEEF_M_LOG2Eq; 49 | } else if (strcmp(constant_name, "log10e") == 0) { 50 | result->value.sleef_value = SLEEF_M_LOG10Eq; 51 | } else if (strcmp(constant_name, "ln2") == 0) { 52 | result->value.sleef_value = SLEEF_M_LN2q; 53 | } else if (strcmp(constant_name, "ln10") == 0) { 54 | result->value.sleef_value = SLEEF_M_LN10q; 55 | } else if (strcmp(constant_name, "quad_max") == 0) { 56 | result->value.sleef_value = SLEEF_QUAD_MAX; 57 | } else if (strcmp(constant_name, "quad_min") == 0) { 58 | result->value.sleef_value = SLEEF_QUAD_MIN; 59 | } else if (strcmp(constant_name, "epsilon") == 0) { 60 | result->value.sleef_value = SLEEF_QUAD_EPSILON; 61 | } 62 | else { 63 | PyErr_SetString(PyExc_ValueError, "Unknown constant name"); 64 | Py_DECREF(result); 65 | return NULL; 66 | } 67 | 68 | return (PyObject*)result; 69 | } 70 | 71 | static PyMethodDef module_methods[] = { 72 | {"is_longdouble_128", py_is_longdouble_128, METH_NOARGS, "Check if long double is 128-bit"}, 73 | {"get_sleef_constant", get_sleef_constant, METH_VARARGS, "Get Sleef constant by name"}, 74 | {NULL, NULL, 0, NULL} 75 | }; 76 | 77 | static struct PyModuleDef moduledef = { 78 | PyModuleDef_HEAD_INIT, 79 | .m_name = "_quaddtype_main", 80 | .m_doc = "Quad (128-bit) floating point Data Type for NumPy with multiple backends", 81 | .m_size = -1, 82 | .m_methods = module_methods 83 | }; 84 | 85 | PyMODINIT_FUNC 86 | PyInit__quaddtype_main(void) 87 | { 88 | import_array(); 89 | import_umath(); 90 | PyObject *m = PyModule_Create(&moduledef); 91 | if (!m) { 92 | return NULL; 93 | } 94 | 95 | if (init_quadprecision_scalar() < 0) 96 | goto error; 97 | 98 | if (PyModule_AddObject(m, "QuadPrecision", (PyObject *)&QuadPrecision_Type) < 0) 99 | goto error; 100 | 101 | if (init_quadprec_dtype() < 0) 102 | goto error; 103 | 104 | if (PyModule_AddObject(m, "QuadPrecDType", (PyObject *)&QuadPrecDType) < 0) 105 | goto error; 106 | 107 | if (init_quad_umath() < 0) { 108 | goto error; 109 | } 110 | 111 | return m; 112 | 113 | error: 114 | Py_XDECREF(m); 115 | return NULL; 116 | } -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/scalar.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API 7 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 8 | #define NO_IMPORT_ARRAY 9 | 10 | #include "numpy/arrayobject.h" 11 | #include "numpy/ndarraytypes.h" 12 | #include "numpy/dtype_api.h" 13 | 14 | #include "scalar.h" 15 | #include "scalar_ops.h" 16 | #include "dragon4.h" 17 | 18 | QuadPrecisionObject * 19 | QuadPrecision_raw_new(QuadBackendType backend) 20 | { 21 | QuadPrecisionObject *new = PyObject_New(QuadPrecisionObject, &QuadPrecision_Type); 22 | if (!new) 23 | return NULL; 24 | new->backend = backend; 25 | if (backend == BACKEND_SLEEF) { 26 | new->value.sleef_value = Sleef_cast_from_doubleq1(0.0); 27 | } 28 | else { 29 | new->value.longdouble_value = 0.0L; 30 | } 31 | return new; 32 | } 33 | 34 | QuadPrecisionObject * 35 | QuadPrecision_from_object(PyObject *value, QuadBackendType backend) 36 | { 37 | QuadPrecisionObject *self = QuadPrecision_raw_new(backend); 38 | if (!self) 39 | return NULL; 40 | 41 | if (PyFloat_Check(value)) { 42 | double dval = PyFloat_AsDouble(value); 43 | if (backend == BACKEND_SLEEF) { 44 | self->value.sleef_value = Sleef_cast_from_doubleq1(dval); 45 | } 46 | else { 47 | self->value.longdouble_value = (long double)dval; 48 | } 49 | } 50 | else if (PyUnicode_CheckExact(value)) { 51 | const char *s = PyUnicode_AsUTF8(value); 52 | char *endptr = NULL; 53 | if (backend == BACKEND_SLEEF) { 54 | self->value.sleef_value = Sleef_strtoq(s, &endptr); 55 | } 56 | else { 57 | self->value.longdouble_value = strtold(s, &endptr); 58 | } 59 | if (*endptr != '\0' || endptr == s) { 60 | PyErr_SetString(PyExc_ValueError, "Unable to parse string to QuadPrecision"); 61 | Py_DECREF(self); 62 | return NULL; 63 | } 64 | } 65 | else if (PyLong_Check(value)) { 66 | long long val = PyLong_AsLongLong(value); 67 | if (val == -1 && PyErr_Occurred()) { 68 | PyErr_SetString(PyExc_OverflowError, "Overflow Error, value out of range"); 69 | Py_DECREF(self); 70 | return NULL; 71 | } 72 | if (backend == BACKEND_SLEEF) { 73 | self->value.sleef_value = Sleef_cast_from_int64q1(val); 74 | } 75 | else { 76 | self->value.longdouble_value = (long double)val; 77 | } 78 | } 79 | else if (Py_TYPE(value) == &QuadPrecision_Type) { 80 | Py_DECREF(self); // discard the default one 81 | QuadPrecisionObject *quad_obj = (QuadPrecisionObject *)value; 82 | // create a new one with the same backend 83 | QuadPrecisionObject *self = QuadPrecision_raw_new(quad_obj->backend); 84 | if (quad_obj->backend == BACKEND_SLEEF) { 85 | self->value.sleef_value = quad_obj->value.sleef_value; 86 | } 87 | else { 88 | self->value.longdouble_value = quad_obj->value.longdouble_value; 89 | } 90 | 91 | return self; 92 | } 93 | else { 94 | PyObject *type_str = PyObject_Str((PyObject *)Py_TYPE(value)); 95 | if (type_str != NULL) { 96 | const char *type_cstr = PyUnicode_AsUTF8(type_str); 97 | if (type_cstr != NULL) { 98 | PyErr_Format(PyExc_TypeError, 99 | "QuadPrecision value must be a quad, float, int or string, but got %s " 100 | "instead", 101 | type_cstr); 102 | } 103 | else { 104 | PyErr_SetString( 105 | PyExc_TypeError, 106 | "QuadPrecision value must be a quad, float, int or string, but got an " 107 | "unknown type instead"); 108 | } 109 | Py_DECREF(type_str); 110 | } 111 | else { 112 | PyErr_SetString(PyExc_TypeError, 113 | "QuadPrecision value must be a quad, float, int or string, but got an " 114 | "unknown type instead"); 115 | } 116 | Py_DECREF(self); 117 | return NULL; 118 | } 119 | 120 | return self; 121 | } 122 | 123 | static PyObject * 124 | QuadPrecision_new(PyTypeObject *cls, PyObject *args, PyObject *kwargs) 125 | { 126 | PyObject *value; 127 | const char *backend_str = "sleef"; 128 | static char *kwlist[] = {"value", "backend", NULL}; 129 | 130 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|s", kwlist, &value, &backend_str)) { 131 | return NULL; 132 | } 133 | 134 | QuadBackendType backend = BACKEND_SLEEF; 135 | if (strcmp(backend_str, "longdouble") == 0) { 136 | backend = BACKEND_LONGDOUBLE; 137 | } 138 | else if (strcmp(backend_str, "sleef") != 0) { 139 | PyErr_SetString(PyExc_ValueError, "Invalid backend. Use 'sleef' or 'longdouble'."); 140 | return NULL; 141 | } 142 | 143 | return (PyObject *)QuadPrecision_from_object(value, backend); 144 | } 145 | 146 | static PyObject * 147 | QuadPrecision_str_dragon4(QuadPrecisionObject *self) 148 | { 149 | Dragon4_Options opt = {.scientific = 0, 150 | .digit_mode = DigitMode_Unique, 151 | .cutoff_mode = CutoffMode_TotalLength, 152 | .precision = SLEEF_QUAD_DIG, 153 | .sign = 1, 154 | .trim_mode = TrimMode_LeaveOneZero, 155 | .digits_left = 1, 156 | .digits_right = SLEEF_QUAD_DIG}; 157 | 158 | if (self->backend == BACKEND_SLEEF) { 159 | return Dragon4_Positional_QuadDType( 160 | &self->value.sleef_value, opt.digit_mode, opt.cutoff_mode, opt.precision, 161 | opt.min_digits, opt.sign, opt.trim_mode, opt.digits_left, opt.digits_right); 162 | } 163 | else { 164 | Sleef_quad sleef_val = Sleef_cast_from_doubleq1(self->value.longdouble_value); 165 | return Dragon4_Positional_QuadDType(&sleef_val, opt.digit_mode, opt.cutoff_mode, 166 | opt.precision, opt.min_digits, opt.sign, opt.trim_mode, 167 | opt.digits_left, opt.digits_right); 168 | } 169 | } 170 | 171 | static PyObject * 172 | QuadPrecision_str(QuadPrecisionObject *self) 173 | { 174 | char buffer[128]; 175 | if (self->backend == BACKEND_SLEEF) { 176 | Sleef_snprintf(buffer, sizeof(buffer), "%.*Qe", SLEEF_QUAD_DIG, self->value.sleef_value); 177 | } 178 | else { 179 | snprintf(buffer, sizeof(buffer), "%.35Le", self->value.longdouble_value); 180 | } 181 | return PyUnicode_FromString(buffer); 182 | } 183 | 184 | static PyObject * 185 | QuadPrecision_repr(QuadPrecisionObject *self) 186 | { 187 | PyObject *str = QuadPrecision_str(self); 188 | if (str == NULL) { 189 | return NULL; 190 | } 191 | const char *backend_str = (self->backend == BACKEND_SLEEF) ? "sleef" : "longdouble"; 192 | PyObject *res = PyUnicode_FromFormat("QuadPrecision('%S', backend='%s')", str, backend_str); 193 | Py_DECREF(str); 194 | return res; 195 | } 196 | 197 | static PyObject * 198 | QuadPrecision_repr_dragon4(QuadPrecisionObject *self) 199 | { 200 | Dragon4_Options opt = {.scientific = 1, 201 | .digit_mode = DigitMode_Unique, 202 | .cutoff_mode = CutoffMode_TotalLength, 203 | .precision = SLEEF_QUAD_DIG, 204 | .sign = 1, 205 | .trim_mode = TrimMode_LeaveOneZero, 206 | .digits_left = 1, 207 | .exp_digits = 3}; 208 | 209 | PyObject *str; 210 | if (self->backend == BACKEND_SLEEF) { 211 | str = Dragon4_Scientific_QuadDType(&self->value.sleef_value, opt.digit_mode, opt.precision, 212 | opt.min_digits, opt.sign, opt.trim_mode, opt.digits_left, 213 | opt.exp_digits); 214 | } 215 | else { 216 | Sleef_quad sleef_val = Sleef_cast_from_doubleq1(self->value.longdouble_value); 217 | str = Dragon4_Scientific_QuadDType(&sleef_val, opt.digit_mode, opt.precision, 218 | opt.min_digits, opt.sign, opt.trim_mode, opt.digits_left, 219 | opt.exp_digits); 220 | } 221 | 222 | if (str == NULL) { 223 | return NULL; 224 | } 225 | 226 | const char *backend_str = (self->backend == BACKEND_SLEEF) ? "sleef" : "longdouble"; 227 | PyObject *res = PyUnicode_FromFormat("QuadPrecision('%S', backend='%s')", str, backend_str); 228 | Py_DECREF(str); 229 | return res; 230 | } 231 | 232 | static void 233 | QuadPrecision_dealloc(QuadPrecisionObject *self) 234 | { 235 | Py_TYPE(self)->tp_free((PyObject *)self); 236 | } 237 | 238 | PyTypeObject QuadPrecision_Type = { 239 | PyVarObject_HEAD_INIT(NULL, 0).tp_name = "numpy_quaddtype.QuadPrecision", 240 | .tp_basicsize = sizeof(QuadPrecisionObject), 241 | .tp_itemsize = 0, 242 | .tp_new = QuadPrecision_new, 243 | .tp_dealloc = (destructor)QuadPrecision_dealloc, 244 | .tp_repr = (reprfunc)QuadPrecision_repr_dragon4, 245 | .tp_str = (reprfunc)QuadPrecision_str_dragon4, 246 | .tp_as_number = &quad_as_scalar, 247 | .tp_richcompare = (richcmpfunc)quad_richcompare, 248 | }; 249 | 250 | int 251 | init_quadprecision_scalar(void) 252 | { 253 | return PyType_Ready(&QuadPrecision_Type); 254 | } -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/scalar.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUADDTYPE_SCALAR_H 2 | #define _QUADDTYPE_SCALAR_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include "quad_common.h" 11 | 12 | typedef union { 13 | Sleef_quad sleef_value; 14 | long double longdouble_value; 15 | } quad_value; 16 | 17 | typedef struct { 18 | PyObject_HEAD 19 | quad_value value; 20 | QuadBackendType backend; 21 | } QuadPrecisionObject; 22 | 23 | extern PyTypeObject QuadPrecision_Type; 24 | 25 | QuadPrecisionObject * 26 | QuadPrecision_raw_new(QuadBackendType backend); 27 | 28 | QuadPrecisionObject * 29 | QuadPrecision_from_object(PyObject *value, QuadBackendType backend); 30 | 31 | int 32 | init_quadprecision_scalar(void); 33 | 34 | #define PyArray_IsScalar(obj, QuadPrecDType) PyObject_TypeCheck(obj, &QuadPrecision_Type) 35 | #define PyArrayScalar_VAL(obj, QuadPrecDType) (((QuadPrecisionObject *)obj)->value) 36 | 37 | #ifdef __cplusplus 38 | } 39 | #endif 40 | 41 | #endif -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/scalar_ops.cpp: -------------------------------------------------------------------------------- 1 | #define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API 2 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 3 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 4 | #define NO_IMPORT_ARRAY 5 | 6 | extern "C" { 7 | #include 8 | 9 | #include "numpy/arrayobject.h" 10 | #include "numpy/ndarraytypes.h" 11 | #include "numpy/ufuncobject.h" 12 | 13 | #include "numpy/dtype_api.h" 14 | } 15 | 16 | #include "scalar.h" 17 | #include "ops.hpp" 18 | #include "scalar_ops.h" 19 | #include "quad_common.h" 20 | 21 | template 22 | static PyObject * 23 | quad_unary_func(QuadPrecisionObject *self) 24 | { 25 | QuadPrecisionObject *res = QuadPrecision_raw_new(self->backend); 26 | if (!res) { 27 | return NULL; 28 | } 29 | 30 | if (self->backend == BACKEND_SLEEF) { 31 | res->value.sleef_value = sleef_op(&self->value.sleef_value); 32 | } 33 | else { 34 | res->value.longdouble_value = longdouble_op(&self->value.longdouble_value); 35 | } 36 | return (PyObject *)res; 37 | } 38 | 39 | PyObject * 40 | quad_nonzero(QuadPrecisionObject *self) 41 | { 42 | if (self->backend == BACKEND_SLEEF) { 43 | return PyBool_FromLong(Sleef_icmpneq1(self->value.sleef_value, Sleef_cast_from_int64q1(0))); 44 | } 45 | else { 46 | return PyBool_FromLong(self->value.longdouble_value != 0.0L); 47 | } 48 | } 49 | 50 | template 51 | static PyObject * 52 | quad_binary_func(PyObject *op1, PyObject *op2) 53 | { 54 | QuadPrecisionObject *self; 55 | PyObject *other; 56 | QuadPrecisionObject *other_quad = NULL; 57 | int is_forward; 58 | QuadBackendType backend; 59 | 60 | if (PyObject_TypeCheck(op1, &QuadPrecision_Type)) { 61 | is_forward = 1; 62 | self = (QuadPrecisionObject *)op1; 63 | other = Py_NewRef(op2); 64 | backend = self->backend; 65 | } 66 | else { 67 | is_forward = 0; 68 | self = (QuadPrecisionObject *)op2; 69 | other = Py_NewRef(op1); 70 | backend = self->backend; 71 | } 72 | 73 | if (PyObject_TypeCheck(other, &QuadPrecision_Type)) { 74 | Py_INCREF(other); 75 | other_quad = (QuadPrecisionObject *)other; 76 | if (other_quad->backend != backend) { 77 | PyErr_SetString(PyExc_TypeError, "Cannot mix QuadPrecision backends"); 78 | Py_DECREF(other); 79 | return NULL; 80 | } 81 | } 82 | else if (PyLong_Check(other) || PyFloat_Check(other)) { 83 | other_quad = QuadPrecision_from_object(other, backend); 84 | if (!other_quad) { 85 | Py_DECREF(other); 86 | return NULL; 87 | } 88 | } 89 | else { 90 | Py_DECREF(other); 91 | Py_RETURN_NOTIMPLEMENTED; 92 | } 93 | 94 | QuadPrecisionObject *res = QuadPrecision_raw_new(backend); 95 | if (!res) { 96 | Py_DECREF(other_quad); 97 | Py_DECREF(other); 98 | return NULL; 99 | } 100 | 101 | if (backend == BACKEND_SLEEF) { 102 | if (is_forward) { 103 | res->value.sleef_value = 104 | sleef_op(&self->value.sleef_value, &other_quad->value.sleef_value); 105 | } 106 | else { 107 | res->value.sleef_value = 108 | sleef_op(&other_quad->value.sleef_value, &self->value.sleef_value); 109 | } 110 | } 111 | else { 112 | if (is_forward) { 113 | res->value.longdouble_value = longdouble_op(&self->value.longdouble_value, 114 | &other_quad->value.longdouble_value); 115 | } 116 | else { 117 | res->value.longdouble_value = longdouble_op(&other_quad->value.longdouble_value, 118 | &self->value.longdouble_value); 119 | } 120 | } 121 | 122 | Py_DECREF(other_quad); 123 | Py_DECREF(other); 124 | return (PyObject *)res; 125 | } 126 | 127 | PyObject * 128 | quad_richcompare(QuadPrecisionObject *self, PyObject *other, int cmp_op) 129 | { 130 | QuadPrecisionObject *other_quad = NULL; 131 | QuadBackendType backend = self->backend; 132 | 133 | if (PyObject_TypeCheck(other, &QuadPrecision_Type)) { 134 | Py_INCREF(other); 135 | other_quad = (QuadPrecisionObject *)other; 136 | if (other_quad->backend != backend) { 137 | PyErr_SetString(PyExc_TypeError, 138 | "Cannot compare QuadPrecision objects with different backends"); 139 | Py_DECREF(other_quad); 140 | return NULL; 141 | } 142 | } 143 | else if (PyLong_CheckExact(other) || PyFloat_CheckExact(other)) { 144 | other_quad = QuadPrecision_from_object(other, backend); 145 | if (other_quad == NULL) { 146 | return NULL; 147 | } 148 | } 149 | else { 150 | Py_RETURN_NOTIMPLEMENTED; 151 | } 152 | 153 | int cmp; 154 | if (backend == BACKEND_SLEEF) { 155 | switch (cmp_op) { 156 | case Py_LT: 157 | cmp = Sleef_icmpltq1(self->value.sleef_value, other_quad->value.sleef_value); 158 | break; 159 | case Py_LE: 160 | cmp = Sleef_icmpleq1(self->value.sleef_value, other_quad->value.sleef_value); 161 | break; 162 | case Py_EQ: 163 | cmp = Sleef_icmpeqq1(self->value.sleef_value, other_quad->value.sleef_value); 164 | break; 165 | case Py_NE: 166 | cmp = Sleef_icmpneq1(self->value.sleef_value, other_quad->value.sleef_value); 167 | break; 168 | case Py_GT: 169 | cmp = Sleef_icmpgtq1(self->value.sleef_value, other_quad->value.sleef_value); 170 | break; 171 | case Py_GE: 172 | cmp = Sleef_icmpgeq1(self->value.sleef_value, other_quad->value.sleef_value); 173 | break; 174 | default: 175 | Py_DECREF(other_quad); 176 | Py_RETURN_NOTIMPLEMENTED; 177 | } 178 | } 179 | else { 180 | switch (cmp_op) { 181 | case Py_LT: 182 | cmp = self->value.longdouble_value < other_quad->value.longdouble_value; 183 | break; 184 | case Py_LE: 185 | cmp = self->value.longdouble_value <= other_quad->value.longdouble_value; 186 | break; 187 | case Py_EQ: 188 | cmp = self->value.longdouble_value == other_quad->value.longdouble_value; 189 | break; 190 | case Py_NE: 191 | cmp = self->value.longdouble_value != other_quad->value.longdouble_value; 192 | break; 193 | case Py_GT: 194 | cmp = self->value.longdouble_value > other_quad->value.longdouble_value; 195 | break; 196 | case Py_GE: 197 | cmp = self->value.longdouble_value >= other_quad->value.longdouble_value; 198 | break; 199 | default: 200 | Py_DECREF(other_quad); 201 | Py_RETURN_NOTIMPLEMENTED; 202 | } 203 | } 204 | Py_DECREF(other_quad); 205 | 206 | return PyBool_FromLong(cmp); 207 | } 208 | 209 | static PyObject * 210 | QuadPrecision_float(QuadPrecisionObject *self) 211 | { 212 | if (self->backend == BACKEND_SLEEF) { 213 | return PyFloat_FromDouble(Sleef_cast_to_doubleq1(self->value.sleef_value)); 214 | } 215 | else { 216 | return PyFloat_FromDouble((double)self->value.longdouble_value); 217 | } 218 | } 219 | 220 | static PyObject * 221 | QuadPrecision_int(QuadPrecisionObject *self) 222 | { 223 | if (self->backend == BACKEND_SLEEF) { 224 | return PyLong_FromLongLong(Sleef_cast_to_int64q1(self->value.sleef_value)); 225 | } 226 | else { 227 | return PyLong_FromLongLong((long long)self->value.longdouble_value); 228 | } 229 | } 230 | 231 | PyNumberMethods quad_as_scalar = { 232 | .nb_add = (binaryfunc)quad_binary_func, 233 | .nb_subtract = (binaryfunc)quad_binary_func, 234 | .nb_multiply = (binaryfunc)quad_binary_func, 235 | .nb_power = (ternaryfunc)quad_binary_func, 236 | .nb_negative = (unaryfunc)quad_unary_func, 237 | .nb_positive = (unaryfunc)quad_unary_func, 238 | .nb_absolute = (unaryfunc)quad_unary_func, 239 | .nb_bool = (inquiry)quad_nonzero, 240 | .nb_int = (unaryfunc)QuadPrecision_int, 241 | .nb_float = (unaryfunc)QuadPrecision_float, 242 | .nb_true_divide = (binaryfunc)quad_binary_func, 243 | }; -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/scalar_ops.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUADDTYPE_SCALAR_OPS_H 2 | #define _QUADDTYPE_SCALAR_OPS_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include "dtype.h" 10 | 11 | PyObject * 12 | quad_richcompare(QuadPrecisionObject *self, PyObject *other, int cmp_op); 13 | 14 | extern PyNumberMethods quad_as_scalar; 15 | 16 | #ifdef __cplusplus 17 | } 18 | #endif 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /quaddtype/numpy_quaddtype/src/umath.h: -------------------------------------------------------------------------------- 1 | #ifndef _QUADDTYPE_UMATH_H 2 | #define _QUADDTYPE_UMATH_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | int 9 | init_quad_umath(void); 10 | 11 | #ifdef __cplusplus 12 | } 13 | #endif 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /quaddtype/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "meson>=1.3.2", 4 | "meson-python", 5 | "patchelf", 6 | "wheel", 7 | "numpy" 8 | ] 9 | build-backend = "mesonpy" 10 | 11 | [project] 12 | name = "numpy_quaddtype" 13 | description = "Quad (128-bit) float dtype for numpy" 14 | version = "0.0.1" 15 | readme = 'README.md' 16 | author = "Swayam Singh" 17 | requires-python = ">=3.9.0" 18 | dependencies = [ 19 | "numpy" 20 | ] 21 | 22 | [project.optional-dependencies] 23 | test = [ 24 | "pytest", 25 | ] -------------------------------------------------------------------------------- /quaddtype/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | IFS=$'\n\t' 4 | 5 | if [ -d "build/" ] 6 | then 7 | rm -r build 8 | fi 9 | 10 | #meson setup build -Db_sanitize=address,undefined 11 | python -m pip uninstall -y numpy_quaddtype 12 | # python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -Csetup-args="-Dbuildtype=debug" 13 | python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -------------------------------------------------------------------------------- /quaddtype/tests/test_quaddtype.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | import numpy as np 4 | import operator 5 | 6 | from numpy_quaddtype import QuadPrecDType, QuadPrecision 7 | 8 | 9 | def test_create_scalar_simple(): 10 | assert isinstance(QuadPrecision("12.0"), QuadPrecision) 11 | assert isinstance(QuadPrecision(1.63), QuadPrecision) 12 | assert isinstance(QuadPrecision(1), QuadPrecision) 13 | 14 | 15 | def test_basic_equality(): 16 | assert QuadPrecision("12") == QuadPrecision( 17 | "12.0") == QuadPrecision("12.00") 18 | 19 | 20 | @pytest.mark.parametrize("op", ["add", "sub", "mul", "truediv", "pow"]) 21 | @pytest.mark.parametrize("other", ["3.0", "12.5", "100.0"]) 22 | def test_binary_ops(op, other): 23 | op_func = getattr(operator, op) 24 | quad_a = QuadPrecision("12.5") 25 | quad_b = QuadPrecision(other) 26 | float_a = 12.5 27 | float_b = float(other) 28 | 29 | quad_result = op_func(quad_a, quad_b) 30 | float_result = op_func(float_a, float_b) 31 | 32 | assert np.abs(np.float64(quad_result) - float_result) < 1e-10 33 | 34 | 35 | @pytest.mark.parametrize("op", ["eq", "ne", "le", "lt", "ge", "gt"]) 36 | @pytest.mark.parametrize("other", ["3.0", "12.5", "100.0"]) 37 | def test_comparisons(op, other): 38 | op_func = getattr(operator, op) 39 | quad_a = QuadPrecision("12.5") 40 | quad_b = QuadPrecision(other) 41 | float_a = 12.5 42 | float_b = float(other) 43 | 44 | assert op_func(quad_a, quad_b) == op_func(float_a, float_b) 45 | 46 | 47 | @pytest.mark.parametrize("op, val, expected", [ 48 | ("neg", "3.0", "-3.0"), 49 | ("neg", "-3.0", "3.0"), 50 | ("pos", "3.0", "3.0"), 51 | ("pos", "-3.0", "-3.0"), 52 | ("abs", "3.0", "3.0"), 53 | ("abs", "-3.0", "3.0"), 54 | ("neg", "12.5", "-12.5"), 55 | ("pos", "100.0", "100.0"), 56 | ("abs", "-25.5", "25.5"), 57 | ]) 58 | def test_unary_ops(op, val, expected): 59 | quad_val = QuadPrecision(val) 60 | expected_val = QuadPrecision(expected) 61 | 62 | if op == "neg": 63 | result = -quad_val 64 | elif op == "pos": 65 | result = +quad_val 66 | elif op == "abs": 67 | result = abs(quad_val) 68 | else: 69 | raise ValueError(f"Unsupported operation: {op}") 70 | 71 | assert result == expected_val, f"{op}({val}) should be {expected}, but got {result}" 72 | 73 | 74 | def test_nan_and_inf(): 75 | # NaN should not equal itself 76 | assert QuadPrecision("nan") != QuadPrecision("nan") 77 | 78 | # Test infinity comparisons 79 | assert QuadPrecision("inf") > QuadPrecision("1e1000") 80 | assert QuadPrecision("-inf") < QuadPrecision("-1e1000") 81 | 82 | 83 | def test_dtype_creation(): 84 | dtype = QuadPrecDType() 85 | assert isinstance(dtype, np.dtype) 86 | assert dtype.name == 'QuadPrecDType128' 87 | 88 | 89 | def test_array_creation(): 90 | arr = np.array([1, 2, 3], dtype=QuadPrecDType()) 91 | assert arr.dtype.name == 'QuadPrecDType128' 92 | assert all(isinstance(x, QuadPrecision) for x in arr) 93 | 94 | 95 | def test_array_operations(): 96 | arr1 = np.array( 97 | [QuadPrecision("1.5"), QuadPrecision("2.5"), QuadPrecision("3.5")]) 98 | arr2 = np.array( 99 | [QuadPrecision("0.5"), QuadPrecision("1.0"), QuadPrecision("1.5")]) 100 | 101 | result = arr1 + arr2 102 | expected = np.array( 103 | [QuadPrecision("2.0"), QuadPrecision("3.5"), QuadPrecision("5.0")]) 104 | assert np.all(result == expected) 105 | -------------------------------------------------------------------------------- /stringdtype/.clang-format: -------------------------------------------------------------------------------- 1 | # A clang-format style that approximates Python's PEP 7 2 | # Useful for IDE integration 3 | # 4 | # Based on Paul Ganssle's version at 5 | # https://gist.github.com/pganssle/0e3a5f828b4d07d79447f6ced8e7e4db 6 | # and modified for NumPy 7 | BasedOnStyle: Google 8 | AlignAfterOpenBracket: Align 9 | AllowShortEnumsOnASingleLine: false 10 | AllowShortIfStatementsOnASingleLine: false 11 | AlwaysBreakAfterReturnType: TopLevel 12 | BreakBeforeBraces: Stroustrup 13 | ColumnLimit: 79 14 | ContinuationIndentWidth: 8 15 | DerivePointerAlignment: false 16 | IndentWidth: 4 17 | IncludeBlocks: Regroup 18 | IncludeCategories: 19 | - Regex: '^[<"](Python|structmember|pymem)\.h' 20 | Priority: -3 21 | CaseSensitive: true 22 | - Regex: '^"numpy/' 23 | Priority: -2 24 | - Regex: '^"(npy_pycompat|npy_config)' 25 | Priority: -1 26 | - Regex: '^"[[:alnum:]_.]+"' 27 | Priority: 1 28 | - Regex: '^<[[:alnum:]_.]+"' 29 | Priority: 2 30 | Language: Cpp 31 | PointerAlignment: Right 32 | ReflowComments: true 33 | SpaceBeforeParens: ControlStatements 34 | SpacesInParentheses: false 35 | StatementMacros: [PyObject_HEAD, PyObject_VAR_HEAD, PyObject_HEAD_EXTRA] 36 | TabWidth: 4 37 | UseTab: Never 38 | -------------------------------------------------------------------------------- /stringdtype/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .mesonpy*.ini 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /stringdtype/README.md: -------------------------------------------------------------------------------- 1 | # A dtype that stores pointers to strings 2 | 3 | This is the prototype implementation of the variable-width UTF-8 string DType 4 | described in [NEP 55](https://numpy.org/neps/nep-0055-string_dtype.html). 5 | 6 | See the NEP for implementation details and usage examples. See 7 | `numpy.dtypes.StringDType` for the version that made it into NumPy. 8 | 9 | ## Building 10 | 11 | Ensure Meson and NumPy are installed in the python environment you would like to use: 12 | 13 | ``` 14 | $ python3 -m pip install meson meson-python 15 | ``` 16 | 17 | It is important to have the latest development version of numpy installed. 18 | Nightly wheels work well for this purpose, and can be installed easily: 19 | 20 | ```bash 21 | $ pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy 22 | ``` 23 | 24 | You can install with `pip` directly, taking care to disable build isolation so 25 | the numpy nightly gets picked up at build time: 26 | 27 | ```bash 28 | $ pip install -v . --no-build-isolation 29 | ``` 30 | 31 | If you want to work on the `stringdtype` code, you can build with meson, 32 | create a wheel, and install it. 33 | 34 | ```bash 35 | $ rm -r dist/ 36 | $ meson build 37 | $ python -m build --wheel -Cbuilddir=build 38 | $ python -m pip install dist/path-to-wheel-file.whl 39 | ``` 40 | -------------------------------------------------------------------------------- /stringdtype/meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'stringdtype', 3 | 'c', 4 | ) 5 | 6 | py_mod = import('python') 7 | py = py_mod.find_installation() 8 | 9 | incdir_numpy = run_command(py, 10 | [ 11 | '-c', 12 | 'import numpy; print(numpy.get_include())' 13 | ], 14 | check: true 15 | ).stdout().strip() 16 | 17 | cc = meson.get_compiler('c') 18 | 19 | npymath_path = incdir_numpy / '..' / 'lib' 20 | npymath_lib = cc.find_library('npymath', dirs: npymath_path) 21 | inc_np = include_directories(incdir_numpy) 22 | np_dep = declare_dependency(include_directories: inc_np) 23 | 24 | includes = include_directories( 25 | [ 26 | incdir_numpy, 27 | 'stringdtype/src' 28 | ] 29 | ) 30 | 31 | srcs = [ 32 | 'stringdtype/src/casts.c', 33 | 'stringdtype/src/casts.h', 34 | 'stringdtype/src/dtype.c', 35 | 'stringdtype/src/main.c', 36 | 'stringdtype/src/static_string.c', 37 | 'stringdtype/src/static_string.h', 38 | 'stringdtype/src/umath.c', 39 | 'stringdtype/src/umath.h', 40 | ] 41 | 42 | py.install_sources( 43 | [ 44 | 'stringdtype/__init__.py', 45 | 'stringdtype/scalar.py', 46 | ], 47 | subdir: 'stringdtype', 48 | pure: false 49 | ) 50 | 51 | py.extension_module( 52 | '_main', 53 | srcs, 54 | install: true, 55 | subdir: 'stringdtype', 56 | include_directories: includes, 57 | dependencies: [np_dep, npymath_lib] 58 | ) 59 | -------------------------------------------------------------------------------- /stringdtype/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "meson>=0.63.0", 4 | "meson-python", 5 | "patchelf", 6 | "wheel", 7 | "numpy", 8 | ] 9 | build-backend = "mesonpy" 10 | 11 | [tool.black] 12 | line-length = 79 13 | 14 | [tool.isort] 15 | profile = "black" 16 | line_length = 79 17 | 18 | [project] 19 | name = "stringdtype" 20 | description = "A dtype for storing UTF-8 strings" 21 | version = "0.0.1" 22 | readme = 'README.md' 23 | authors = [ 24 | { name = "Nathan Goldbaum" }, 25 | { name = "Peyton Murray" } 26 | ] 27 | requires-python = ">=3.9.0" 28 | dependencies = [ 29 | "numpy", 30 | ] 31 | 32 | [tool.ruff] 33 | line-length = 79 34 | per-file-ignores = {"__init__.py" = ["F401"]} 35 | 36 | [tool.meson-python.args] 37 | dist = [] 38 | setup = [] 39 | compile = [] 40 | install = [] 41 | -------------------------------------------------------------------------------- /stringdtype/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | IFS=$'\n\t' 4 | 5 | if [ -d "build/" ] 6 | then 7 | rm -r build 8 | fi 9 | 10 | #meson setup build -Db_sanitize=address,undefined 11 | meson setup build 12 | python -m pip uninstall -y stringdtype 13 | python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -Csetup-args="-Dbuildtype=debug" 14 | #python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' 15 | -------------------------------------------------------------------------------- /stringdtype/stringdtype/__init__.py: -------------------------------------------------------------------------------- 1 | """A dtype for working with variable-length string data 2 | 3 | """ 4 | 5 | from .scalar import StringScalar # isort: skip 6 | from ._main import StringDType, _memory_usage 7 | 8 | __all__ = [ 9 | "NA", 10 | "StringDType", 11 | "StringScalar", 12 | "_memory_usage", 13 | ] 14 | -------------------------------------------------------------------------------- /stringdtype/stringdtype/scalar.py: -------------------------------------------------------------------------------- 1 | """Scalar types needed by the dtype machinery.""" 2 | 3 | 4 | class StringScalar(str): 5 | pass 6 | -------------------------------------------------------------------------------- /stringdtype/stringdtype/src/casts.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_CASTS_H 2 | #define _NPY_CASTS_H 3 | 4 | PyArrayMethod_Spec ** 5 | get_casts(); 6 | 7 | #endif /* _NPY_CASTS_H */ 8 | -------------------------------------------------------------------------------- /stringdtype/stringdtype/src/dtype.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_DTYPE_H 2 | #define _NPY_DTYPE_H 3 | 4 | // clang-format off 5 | #include 6 | #include "structmember.h" 7 | // clang-format on 8 | 9 | #include "static_string.h" 10 | 11 | // not publicly exposed by the static string library so we need to define 12 | // this here so we can define `elsize` and `alignment` on the descr 13 | // 14 | // if the layout of npy_packed_static_string ever changes in the future 15 | // this may need to be updated. 16 | #define SIZEOF_NPY_PACKED_STATIC_STRING 2 * sizeof(size_t) 17 | #define ALIGNOF_NPY_PACKED_STATIC_STRING _Alignof(size_t) 18 | 19 | typedef struct { 20 | PyArray_Descr base; 21 | PyObject *na_object; 22 | int coerce; 23 | int has_nan_na; 24 | int has_string_na; 25 | int array_owned; 26 | _npy_static_string default_string; 27 | _npy_static_string na_name; 28 | PyThread_type_lock *allocator_lock; 29 | // the allocator should only be directly accessed after 30 | // acquiring the allocator_lock and the lock should 31 | // be released immediately after the allocator is 32 | // no longer needed 33 | npy_string_allocator *allocator; 34 | } StringDTypeObject; 35 | 36 | typedef struct { 37 | PyArray_DTypeMeta base; 38 | } StringDType_type; 39 | 40 | extern StringDType_type StringDType; 41 | extern PyTypeObject *StringScalar_Type; 42 | 43 | static inline npy_string_allocator * 44 | _NpyString_acquire_allocator(StringDTypeObject *descr) 45 | { 46 | if (!PyThread_acquire_lock(descr->allocator_lock, NOWAIT_LOCK)) { 47 | PyThread_acquire_lock(descr->allocator_lock, WAIT_LOCK); 48 | } 49 | return descr->allocator; 50 | } 51 | 52 | static inline void 53 | _NpyString_acquire_allocator2(StringDTypeObject *descr1, 54 | StringDTypeObject *descr2, 55 | npy_string_allocator **allocator1, 56 | npy_string_allocator **allocator2) 57 | { 58 | *allocator1 = _NpyString_acquire_allocator(descr1); 59 | if (descr1 != descr2) { 60 | *allocator2 = _NpyString_acquire_allocator(descr2); 61 | } 62 | else { 63 | *allocator2 = *allocator1; 64 | } 65 | } 66 | 67 | static inline void 68 | _NpyString_acquire_allocator3(StringDTypeObject *descr1, 69 | StringDTypeObject *descr2, 70 | StringDTypeObject *descr3, 71 | npy_string_allocator **allocator1, 72 | npy_string_allocator **allocator2, 73 | npy_string_allocator **allocator3) 74 | { 75 | _NpyString_acquire_allocator2(descr1, descr2, allocator1, allocator2); 76 | if (descr1 != descr3 && descr2 != descr3) { 77 | *allocator3 = _NpyString_acquire_allocator(descr3); 78 | } 79 | else { 80 | *allocator3 = descr3->allocator; 81 | } 82 | } 83 | 84 | static inline void 85 | _NpyString_release_allocator(StringDTypeObject *descr) 86 | { 87 | PyThread_release_lock(descr->allocator_lock); 88 | } 89 | 90 | static inline void 91 | _NpyString_release_allocator2(StringDTypeObject *descr1, 92 | StringDTypeObject *descr2) 93 | { 94 | _NpyString_release_allocator(descr1); 95 | if (descr1 != descr2) { 96 | _NpyString_release_allocator(descr2); 97 | } 98 | } 99 | 100 | static inline void 101 | _NpyString_release_allocator3(StringDTypeObject *descr1, 102 | StringDTypeObject *descr2, 103 | StringDTypeObject *descr3) 104 | { 105 | _NpyString_release_allocator2(descr1, descr2); 106 | if (descr1 != descr3 && descr2 != descr3) { 107 | _NpyString_release_allocator(descr3); 108 | } 109 | } 110 | 111 | PyObject * 112 | new_stringdtype_instance(PyObject *na_object, int coerce); 113 | 114 | int 115 | init_string_dtype(void); 116 | 117 | // Assumes that the caller has already acquired the allocator locks for both 118 | // descriptors 119 | int 120 | _compare(void *a, void *b, StringDTypeObject *descr_a, 121 | StringDTypeObject *descr_b); 122 | 123 | int 124 | init_string_na_object(PyObject *mod); 125 | 126 | int 127 | stringdtype_setitem(StringDTypeObject *descr, PyObject *obj, char **dataptr); 128 | 129 | // set the python error indicator when the gil is released 130 | void 131 | gil_error(PyObject *type, const char *msg); 132 | 133 | // the locks on both allocators must be acquired before calling this function 134 | int 135 | free_and_copy(npy_string_allocator *in_allocator, 136 | npy_string_allocator *out_allocator, 137 | const npy_packed_static_string *in, 138 | npy_packed_static_string *out, const char *location); 139 | 140 | PyArray_Descr * 141 | stringdtype_finalize_descr(PyArray_Descr *dtype); 142 | 143 | int 144 | _eq_comparison(int scoerce, int ocoerce, PyObject *sna, PyObject *ona); 145 | 146 | #endif /*_NPY_DTYPE_H*/ 147 | -------------------------------------------------------------------------------- /stringdtype/stringdtype/src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL stringdtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL stringdtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #include "numpy/ndarraytypes.h" 8 | #include "numpy/arrayobject.h" 9 | #include "numpy/ufuncobject.h" 10 | #include "numpy/dtype_api.h" 11 | 12 | #include "dtype.h" 13 | #include "static_string.h" 14 | #include "umath.h" 15 | 16 | static PyObject * 17 | _memory_usage(PyObject *NPY_UNUSED(self), PyObject *obj) 18 | { 19 | if (!PyArray_Check(obj)) { 20 | PyErr_SetString(PyExc_TypeError, 21 | "can only be called with ndarray object"); 22 | return NULL; 23 | } 24 | 25 | PyArrayObject *arr = (PyArrayObject *)obj; 26 | 27 | PyArray_Descr *descr = PyArray_DESCR(arr); 28 | PyArray_DTypeMeta *dtype = NPY_DTYPE(descr); 29 | 30 | if (dtype != (PyArray_DTypeMeta *)&StringDType) { 31 | PyErr_SetString(PyExc_TypeError, 32 | "can only be called with a StringDType array"); 33 | return NULL; 34 | } 35 | 36 | NpyIter *iter = NpyIter_New( 37 | arr, NPY_ITER_READONLY | NPY_ITER_EXTERNAL_LOOP | NPY_ITER_REFS_OK, 38 | NPY_KEEPORDER, NPY_NO_CASTING, NULL); 39 | 40 | if (iter == NULL) { 41 | return NULL; 42 | } 43 | 44 | NpyIter_IterNextFunc *iternext = NpyIter_GetIterNext(iter, NULL); 45 | 46 | if (iternext == NULL) { 47 | NpyIter_Deallocate(iter); 48 | return NULL; 49 | } 50 | 51 | char **dataptr = NpyIter_GetDataPtrArray(iter); 52 | npy_intp *strideptr = NpyIter_GetInnerStrideArray(iter); 53 | npy_intp *innersizeptr = NpyIter_GetInnerLoopSizePtr(iter); 54 | 55 | // initialize with the size of the internal buffer 56 | size_t memory_usage = PyArray_NBYTES(arr); 57 | 58 | do { 59 | char *in = dataptr[0]; 60 | npy_intp stride = *strideptr; 61 | npy_intp count = *innersizeptr; 62 | 63 | while (count--) { 64 | size_t size = _NpyString_size(((npy_packed_static_string *)in)); 65 | // FIXME: add a way for a string to report its heap size usage 66 | if (size > (sizeof(_npy_static_string) - 1)) { 67 | memory_usage += size; 68 | } 69 | in += stride; 70 | } 71 | 72 | } while (iternext(iter)); 73 | 74 | NpyIter_Deallocate(iter); 75 | 76 | PyObject *ret = PyLong_FromSize_t(memory_usage); 77 | 78 | return ret; 79 | } 80 | 81 | static PyMethodDef string_methods[] = { 82 | {"_memory_usage", _memory_usage, METH_O, 83 | "get memory usage for an array"}, 84 | {NULL, NULL, 0, NULL}, 85 | }; 86 | 87 | static struct PyModuleDef moduledef = { 88 | PyModuleDef_HEAD_INIT, 89 | .m_name = "stringdtype_main", 90 | .m_size = -1, 91 | .m_methods = string_methods, 92 | }; 93 | 94 | /* Module initialization function */ 95 | PyMODINIT_FUNC 96 | PyInit__main(void) 97 | { 98 | import_array(); 99 | import_umath(); 100 | 101 | PyObject *m = PyModule_Create(&moduledef); 102 | if (m == NULL) { 103 | return NULL; 104 | } 105 | 106 | PyObject *mod = PyImport_ImportModule("stringdtype"); 107 | if (mod == NULL) { 108 | goto error; 109 | } 110 | 111 | StringScalar_Type = 112 | (PyTypeObject *)PyObject_GetAttrString(mod, "StringScalar"); 113 | 114 | if (StringScalar_Type == NULL) { 115 | goto error; 116 | } 117 | 118 | Py_DECREF(mod); 119 | 120 | if (init_string_dtype() < 0) { 121 | goto error; 122 | } 123 | 124 | Py_INCREF((PyObject *)&StringDType); 125 | if (PyModule_AddObject(m, "StringDType", (PyObject *)&StringDType) < 0) { 126 | Py_DECREF((PyObject *)&StringDType); 127 | goto error; 128 | } 129 | 130 | if (init_ufuncs() == -1) { 131 | goto error; 132 | } 133 | 134 | return m; 135 | 136 | error: 137 | Py_DECREF(m); 138 | return NULL; 139 | } 140 | -------------------------------------------------------------------------------- /stringdtype/stringdtype/src/static_string.h: -------------------------------------------------------------------------------- 1 | #ifndef __NPY_STATIC_STRING_H 2 | #define __NPY_STATIC_STRING_H 3 | 4 | #include "stdint.h" 5 | #include "stdlib.h" 6 | 7 | typedef struct npy_packed_static_string npy_packed_static_string; 8 | 9 | typedef struct _npy_unpacked_static_string { 10 | size_t size; 11 | const char *buf; 12 | } _npy_static_string; 13 | 14 | // one byte in size is reserved for flags and small string optimization 15 | #define NPY_MAX_STRING_SIZE ((int64_t)1 << 8 * (sizeof(size_t) - 1)) - 1 16 | 17 | // Handles heap allocations for static strings. 18 | typedef struct npy_string_allocator npy_string_allocator; 19 | 20 | // Typedefs for allocator functions 21 | typedef void *(*npy_string_malloc_func)(size_t size); 22 | typedef void (*npy_string_free_func)(void *ptr); 23 | typedef void *(*npy_string_realloc_func)(void *ptr, size_t size); 24 | 25 | // Use these functions to create and destroy string allocators. Normally 26 | // users won't use these directly and will use an allocator already 27 | // attached to a dtype instance 28 | npy_string_allocator * 29 | _NpyString_new_allocator(npy_string_malloc_func m, npy_string_free_func f, 30 | npy_string_realloc_func r); 31 | 32 | // Deallocates the internal buffer and the allocator itself. 33 | void 34 | _NpyString_free_allocator(npy_string_allocator *allocator); 35 | 36 | // Allocates a new buffer for *to_init*, which must be set to NULL before 37 | // calling this function, filling the newly allocated buffer with the copied 38 | // contents of the first *size* entries in *init*, which must be valid and 39 | // initialized beforehand. Calling _NpyString_free on *to_init* before calling 40 | // this function on an existing string or copying the contents of 41 | // NPY_EMPTY_STRING into *to_init* is sufficient to initialize it. Does not 42 | // check if *to_init* is NULL or if the internal buffer is non-NULL, undefined 43 | // behavior or memory leaks are possible if this function is passed a pointer 44 | // to a an unintialized struct, a NULL pointer, or an existing heap-allocated 45 | // string. Returns -1 if allocating the string would exceed the maximum 46 | // allowed string size or exhaust available memory. Returns 0 on success. 47 | int 48 | _NpyString_newsize(const char *init, size_t size, 49 | npy_packed_static_string *to_init, 50 | npy_string_allocator *allocator); 51 | 52 | // Zeroes out the packed string and frees any heap allocated data. For 53 | // arena-allocated data, checks if the data are inside the arena and 54 | // will return -1 if not. Returns 0 on success. 55 | int 56 | _NpyString_free(npy_packed_static_string *str, npy_string_allocator *allocator); 57 | 58 | // Copies the contents of *in* into *out*. Allocates a new string buffer for 59 | // *out*, _NpyString_free *must* be called before this is called if *out* 60 | // points to an existing string. Returns -1 if malloc fails. Returns 0 on 61 | // success. 62 | int 63 | _NpyString_dup(const npy_packed_static_string *in, 64 | npy_packed_static_string *out, 65 | npy_string_allocator *in_allocator, 66 | npy_string_allocator *out_allocator); 67 | 68 | // Allocates a new string buffer for *out* with enough capacity to store 69 | // *size* bytes of text. Does not do any initialization, the caller must 70 | // initialize the string buffer after this function returns. Calling 71 | // _NpyString_free on *to_init* before calling this function on an existing 72 | // string or initializing a new string with the contents of NPY_EMPTY_STRING 73 | // is sufficient to initialize it. Does not check if *to_init* has already 74 | // been initialized or if the internal buffer is non-NULL, undefined behavior 75 | // or memory leaks are possible if this function is passed a NULL pointer or 76 | // an existing heap-allocated string. Returns 0 on success. Returns -1 if 77 | // allocating the string would exceed the maximum allowed string size or 78 | // exhaust available memory. Returns 0 on success. 79 | int 80 | _NpyString_newemptysize(size_t size, npy_packed_static_string *out, 81 | npy_string_allocator *allocator); 82 | 83 | // Determine if *in* corresponds to a null string (e.g. an NA object). Returns 84 | // -1 if *in* cannot be unpacked. Returns 1 if *in* is a null string and 85 | // zero otherwise. 86 | int 87 | _NpyString_isnull(const npy_packed_static_string *in); 88 | 89 | // Compare two strings. Has the same semantics as if strcmp were passed 90 | // null-terminated C strings with the contents of *s1* and *s2*. 91 | int 92 | _NpyString_cmp(const _npy_static_string *s1, const _npy_static_string *s2); 93 | 94 | // Copy and pack the first *size* entries of the buffer pointed to by *buf* 95 | // into the *packed_string*. Returns 0 on success and -1 on failure. 96 | int 97 | _NpyString_pack(npy_string_allocator *allocator, 98 | npy_packed_static_string *packed_string, const char *buf, 99 | size_t size); 100 | 101 | // Pack the null string into the *packed_string*. Returns 0 on success and -1 102 | // on failure. 103 | int 104 | _NpyString_pack_null(npy_string_allocator *allocator, 105 | npy_packed_static_string *packed_string); 106 | 107 | // Extract the packed contents of *packed_string* into *unpacked_string*. A 108 | // useful pattern is to define a stack-allocated _npy_static_string instance 109 | // initialized to {0, NULL} and pass a pointer to the stack-allocated unpacked 110 | // string to this function to unpack the contents of a packed string. The 111 | // *unpacked_string* is a read-only view onto the *packed_string* data and 112 | // should not be used to modify the string data. If *packed_string* is the 113 | // null string, sets *unpacked_string* to the NULL pointer. Returns -1 if 114 | // unpacking the string fails, returns 1 if *packed_string* is the null 115 | // string, and returns 0 otherwise. This function can be used to 116 | // simultaneously unpack a string and determine if it is a null string. 117 | int 118 | _NpyString_load(npy_string_allocator *allocator, 119 | const npy_packed_static_string *packed_string, 120 | _npy_static_string *unpacked_string); 121 | 122 | // Returns the size of the string data in the packed string. Useful in 123 | // situations where only the string size is needed and determining if it is a 124 | // null or unpacking the string is unnecessary. 125 | size_t 126 | _NpyString_size(const npy_packed_static_string *packed_string); 127 | 128 | #endif /*__NPY_STATIC_STRING_H */ 129 | -------------------------------------------------------------------------------- /stringdtype/stringdtype/src/umath.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_UFUNC_H 2 | #define _NPY_UFUNC_H 3 | 4 | int 5 | init_ufuncs(void); 6 | 7 | #endif /*_NPY_UFUNC_H */ 8 | -------------------------------------------------------------------------------- /stringdtype/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["NUMPY_EXPERIMENTAL_DTYPE_API"] = "1" 4 | -------------------------------------------------------------------------------- /unytdtype/.clang-format: -------------------------------------------------------------------------------- 1 | # A clang-format style that approximates Python's PEP 7 2 | # Useful for IDE integration 3 | # 4 | # Based on Paul Ganssle's version at 5 | # https://gist.github.com/pganssle/0e3a5f828b4d07d79447f6ced8e7e4db 6 | # and modified for NumPy 7 | BasedOnStyle: Google 8 | AlignAfterOpenBracket: Align 9 | AllowShortEnumsOnASingleLine: false 10 | AllowShortIfStatementsOnASingleLine: false 11 | AlwaysBreakAfterReturnType: TopLevel 12 | BreakBeforeBraces: Stroustrup 13 | ColumnLimit: 79 14 | ContinuationIndentWidth: 8 15 | DerivePointerAlignment: false 16 | IndentWidth: 4 17 | IncludeBlocks: Regroup 18 | IncludeCategories: 19 | - Regex: '^[<"](Python|structmember|pymem)\.h' 20 | Priority: -3 21 | CaseSensitive: true 22 | - Regex: '^"numpy/' 23 | Priority: -2 24 | - Regex: '^"(npy_pycompat|npy_config)' 25 | Priority: -1 26 | - Regex: '^"[[:alnum:]_.]+"' 27 | Priority: 1 28 | - Regex: '^<[[:alnum:]_.]+"' 29 | Priority: 2 30 | Language: Cpp 31 | PointerAlignment: Right 32 | ReflowComments: true 33 | SpaceBeforeParens: ControlStatements 34 | SpacesInParentheses: false 35 | StatementMacros: [PyObject_HEAD, PyObject_VAR_HEAD, PyObject_HEAD_EXTRA] 36 | TabWidth: 4 37 | UseTab: Never 38 | -------------------------------------------------------------------------------- /unytdtype/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .mesonpy*.ini 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /unytdtype/README.md: -------------------------------------------------------------------------------- 1 | # A dtype that stores unit metadata 2 | 3 | This is a simple proof-of-concept dtype using the (as of late 2022) experimental 4 | [new dtype 5 | implementation](https://numpy.org/neps/nep-0041-improved-dtype-support.html) in 6 | NumPy. It leverages the [unyt](https://unyt.readthedocs.org) library's `Unit` 7 | type to store unit metadata, but relies on the dtype machinery to implement 8 | mathematical operations rather than an ndarray subclass like `unyt_array`. This 9 | is currently mostly for experimenting with the dtype API and is not useful for 10 | real work. 11 | 12 | ## Building 13 | 14 | Ensure Meson and NumPy are installed in the python environment you would like to use: 15 | 16 | ``` 17 | $ python3 -m pip install meson meson-python numpy build patchelf 18 | ``` 19 | 20 | Build with meson, create a wheel, and install it 21 | 22 | ``` 23 | $ rm -r dist/ 24 | $ meson build 25 | $ python -m build --wheel -Cbuilddir=build 26 | $ python -m pip install --force-reinstall dist/unytdtype*.whl 27 | ``` 28 | 29 | The `mesonpy` build backend for pip [does not currently support editable 30 | installs](https://github.com/mesonbuild/meson-python/issues/47), so `pip install 31 | -e .` will not work. 32 | -------------------------------------------------------------------------------- /unytdtype/meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'unytdtype', 3 | 'c', 4 | ) 5 | 6 | py_mod = import('python') 7 | py = py_mod.find_installation() 8 | 9 | incdir_numpy = run_command(py, 10 | [ 11 | '-c', 12 | 'import numpy; print(numpy.get_include())' 13 | ], 14 | check: true 15 | ).stdout().strip() 16 | 17 | includes = include_directories( 18 | [ 19 | incdir_numpy, 20 | 'unytdtype/src' 21 | ] 22 | ) 23 | 24 | srcs = [ 25 | 'unytdtype/src/casts.c', 26 | 'unytdtype/src/casts.h', 27 | 'unytdtype/src/dtype.c', 28 | 'unytdtype/src/unytdtype_main.c', 29 | 'unytdtype/src/umath.c', 30 | 'unytdtype/src/umath.h', 31 | ] 32 | 33 | py.install_sources( 34 | [ 35 | 'unytdtype/__init__.py', 36 | 'unytdtype/scalar.py' 37 | ], 38 | subdir: 'unytdtype', 39 | pure: false 40 | ) 41 | 42 | py.extension_module( 43 | '_unytdtype_main', 44 | srcs, 45 | c_args: ['-g', '-O0'], 46 | install: true, 47 | subdir: 'unytdtype', 48 | include_directories: includes 49 | ) 50 | -------------------------------------------------------------------------------- /unytdtype/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "meson>=0.63.0", 4 | "meson-python", 5 | "patchelf", 6 | "wheel", 7 | "numpy", 8 | ] 9 | build-backend = "mesonpy" 10 | 11 | [project] 12 | name = "unytdtype" 13 | description = "A unit dtype backed by unyt" 14 | version = "0.0.1" 15 | readme = 'README.md' 16 | author = "Nathan Goldbaum" 17 | requires-python = ">=3.9.0" 18 | dependencies = [ 19 | "numpy" 20 | ] 21 | -------------------------------------------------------------------------------- /unytdtype/reinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | IFS=$'\n\t' 4 | 5 | if [ -d "build/" ] 6 | then 7 | rm -r build 8 | fi 9 | 10 | #meson setup build -Db_sanitize=address,undefined 11 | meson setup build 12 | python -m pip uninstall -y unytdtype 13 | python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' -Csetup-args="-Dbuildtype=debug" 14 | #python -m pip install . -v --no-build-isolation -Cbuilddir=build -C'compile-args=-v' 15 | -------------------------------------------------------------------------------- /unytdtype/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["NUMPY_EXPERIMENTAL_DTYPE_API"] = "1" 4 | -------------------------------------------------------------------------------- /unytdtype/tests/test_unytdtype.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unyt 3 | 4 | from unytdtype import UnytDType, UnytScalar 5 | 6 | 7 | def test_dtype_creation(): 8 | dtype = UnytDType("m") 9 | assert str(dtype) == "UnytDType('m')" 10 | 11 | dtype2 = UnytDType(unyt.Unit("m")) 12 | assert str(dtype2) == "UnytDType('m')" 13 | assert dtype == dtype2 14 | 15 | 16 | def test_scalar_creation(): 17 | dtype = UnytDType("m") 18 | unit = unyt.Unit("m") 19 | unit_s = "m" 20 | 21 | s_1 = UnytScalar(1, dtype) 22 | s_2 = UnytScalar(1, unit) 23 | s_3 = UnytScalar(1, unit_s) 24 | 25 | assert s_1 == s_2 == s_3 26 | 27 | 28 | def test_creation_from_zeros(): 29 | dtype = UnytDType("m") 30 | arr = np.zeros(3, dtype=dtype) 31 | assert str(arr) == "[0.0 m 0.0 m 0.0 m]" 32 | 33 | 34 | def test_creation_from_list(): 35 | dtype = UnytDType("m") 36 | arr = np.array([0, 0, 0], dtype=dtype) 37 | assert str(arr) == "[0.0 m 0.0 m 0.0 m]" 38 | 39 | 40 | def test_creation_from_scalar(): 41 | meter = UnytScalar(1, unyt.m) 42 | arr = np.array([meter, meter, meter]) 43 | assert str(arr) == "[1.0 m 1.0 m 1.0 m]" 44 | 45 | 46 | def test_multiplication(): 47 | meter = UnytScalar(1, unyt.m) 48 | arr = np.array([meter, meter, meter]) 49 | arr2 = np.array([2 * meter, 2 * meter, 2 * meter]) 50 | res = arr * arr2 51 | assert str(res) == "[2.0 m**2 2.0 m**2 2.0 m**2]" 52 | 53 | 54 | def test_cast_to_different_unit(): 55 | meter = UnytScalar(1, unyt.m) 56 | arr = np.array([meter, meter, meter]) 57 | conv = arr.astype(UnytDType("cm")) 58 | assert str(conv) == "[100.0 cm 100.0 cm 100.0 cm]" 59 | 60 | 61 | def test_insert_with_different_unit(): 62 | meter = UnytScalar(1, unyt.m) 63 | cm = UnytScalar(1, unyt.cm) 64 | arr = np.array([meter, meter, meter]) 65 | arr[0] = cm 66 | assert str(arr) == "[0.01 m 1.0 m 1.0 m]" 67 | 68 | 69 | def test_cast_to_float64(): 70 | meter = UnytScalar(1, unyt.m) 71 | arr = np.array([meter, meter, meter]) 72 | conv = arr.astype("float64") 73 | assert str(conv) == "[1. 1. 1.]" 74 | 75 | 76 | def test_is_numeric(): 77 | assert UnytDType._is_numeric 78 | -------------------------------------------------------------------------------- /unytdtype/unytdtype/__init__.py: -------------------------------------------------------------------------------- 1 | """A dtype that carries around unit metadata. 2 | 3 | This is an example usage of the experimental new dtype API 4 | in Numpy and is not yet intended for any real purpose. 5 | """ 6 | 7 | from .scalar import UnytScalar # isort: skip 8 | from ._unytdtype_main import UnytDType 9 | 10 | __all__ = ["UnytDType", "UnytScalar"] 11 | -------------------------------------------------------------------------------- /unytdtype/unytdtype/scalar.py: -------------------------------------------------------------------------------- 1 | """A scalar type needed by the dtype machinery.""" 2 | 3 | from unyt import Unit 4 | 5 | 6 | class UnytScalar: 7 | def __init__(self, value, unit): 8 | from . import UnytDType 9 | self.value = value 10 | if isinstance(unit, (str, Unit)): 11 | self.dtype = UnytDType(unit) 12 | elif isinstance(unit, UnytDType): 13 | self.dtype = unit 14 | else: 15 | raise RuntimeError 16 | 17 | @property 18 | def unit(self): 19 | return self.dtype.unit 20 | 21 | def __repr__(self): 22 | return f"{self.value} {self.dtype.unit}" 23 | 24 | def __rmul__(self, other): 25 | return UnytScalar(self.value * other, self.dtype.unit) 26 | 27 | def __eq__(self, other): 28 | return self.value == other.value and self.dtype == other.dtype 29 | -------------------------------------------------------------------------------- /unytdtype/unytdtype/src/casts.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_CASTS_H 2 | #define _NPY_CASTS_H 3 | 4 | /* Gets the conversion between two units: */ 5 | int 6 | get_conversion_factor(PyObject *from_unit, PyObject *to_unit, double *factor, 7 | double *offset); 8 | 9 | PyArrayMethod_Spec ** 10 | get_casts(void); 11 | 12 | int 13 | UnitConverter(PyObject *obj, PyObject **unit); 14 | 15 | #endif /* _NPY_CASTS_H */ 16 | -------------------------------------------------------------------------------- /unytdtype/unytdtype/src/dtype.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_DTYPE_H 2 | #define _NPY_DTYPE_H 3 | 4 | typedef struct { 5 | PyArray_Descr base; 6 | PyObject *unit; 7 | } UnytDTypeObject; 8 | 9 | extern PyArray_DTypeMeta UnytDType; 10 | extern PyTypeObject *UnytScalar_Type; 11 | 12 | UnytDTypeObject * 13 | new_unytdtype_instance(PyObject *unit); 14 | 15 | int 16 | init_unyt_dtype(void); 17 | 18 | #endif /*_NPY_DTYPE_H*/ 19 | -------------------------------------------------------------------------------- /unytdtype/unytdtype/src/umath.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL unytdtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL unytdtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #define NPY_TARGET_VERSION NPY_2_0_API_VERSION 7 | #define NO_IMPORT_ARRAY 8 | #define NO_IMPORT_UFUNC 9 | #include "numpy/arrayobject.h" 10 | #include "numpy/dtype_api.h" 11 | #include "numpy/ndarraytypes.h" 12 | #include "numpy/ufuncobject.h" 13 | 14 | #include "dtype.h" 15 | #include "umath.h" 16 | 17 | static int 18 | unit_multiply_strided_loop(PyArrayMethod_Context *context, char *const data[], 19 | npy_intp const dimensions[], 20 | npy_intp const strides[], NpyAuxData *auxdata) 21 | { 22 | npy_intp N = dimensions[0]; 23 | char *in1 = data[0], *in2 = data[1]; 24 | char *out = data[2]; 25 | npy_intp in1_stride = strides[0]; 26 | npy_intp in2_stride = strides[1]; 27 | npy_intp out_stride = strides[2]; 28 | 29 | while (N--) { 30 | *(double *)out = *(double *)in1 * *(double *)in2; 31 | in1 += in1_stride; 32 | in2 += in2_stride; 33 | out += out_stride; 34 | } 35 | return 0; 36 | } 37 | 38 | static NPY_CASTING 39 | unit_multiply_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *dtypes[], 40 | PyArray_Descr *given_descrs[], 41 | PyArray_Descr *loop_descrs[], 42 | npy_intp *unused) 43 | { 44 | /* Fetch the unyt based units: */ 45 | PyObject *unit1 = ((UnytDTypeObject *)given_descrs[0])->unit; 46 | PyObject *unit2 = ((UnytDTypeObject *)given_descrs[1])->unit; 47 | /* Find the correct result unit: */ 48 | PyObject *new_unit = PyNumber_Multiply(unit1, unit2); 49 | if (new_unit == NULL) { 50 | return -1; 51 | } 52 | 53 | /* Create new DType from the new unit: */ 54 | loop_descrs[2] = (PyArray_Descr *)new_unytdtype_instance(new_unit); 55 | if (loop_descrs[2] == NULL) { 56 | return -1; 57 | } 58 | /* The other operand units can be used as-is: */ 59 | Py_INCREF(given_descrs[0]); 60 | loop_descrs[0] = given_descrs[0]; 61 | Py_INCREF(given_descrs[1]); 62 | loop_descrs[1] = given_descrs[1]; 63 | 64 | return NPY_NO_CASTING; 65 | } 66 | 67 | /* 68 | * Function that adds our multiply loop to NumPy's multiply ufunc. 69 | */ 70 | int 71 | init_multiply_ufunc(void) 72 | { 73 | /* 74 | * Get the multiply ufunc: 75 | */ 76 | PyObject *numpy = PyImport_ImportModule("numpy"); 77 | if (numpy == NULL) { 78 | return -1; 79 | } 80 | PyObject *multiply = PyObject_GetAttrString(numpy, "multiply"); 81 | Py_DECREF(numpy); 82 | if (multiply == NULL) { 83 | return -1; 84 | } 85 | 86 | /* 87 | * The initializing "wrap up" code from the slides (plus one error check) 88 | */ 89 | static PyArray_DTypeMeta *dtypes[3] = {&UnytDType, &UnytDType, &UnytDType}; 90 | 91 | static PyType_Slot slots[] = { 92 | {NPY_METH_resolve_descriptors, &unit_multiply_resolve_descriptors}, 93 | {NPY_METH_strided_loop, &unit_multiply_strided_loop}, 94 | {0, NULL}}; 95 | 96 | PyArrayMethod_Spec MultiplySpec = { 97 | .name = "unyt_multiply", 98 | .nin = 2, 99 | .nout = 1, 100 | .dtypes = dtypes, 101 | .slots = slots, 102 | .flags = 0, 103 | .casting = NPY_NO_CASTING, 104 | }; 105 | 106 | /* Register */ 107 | if (PyUFunc_AddLoopFromSpec(multiply, &MultiplySpec) < 0) { 108 | Py_DECREF(multiply); 109 | return -1; 110 | } 111 | Py_DECREF(multiply); 112 | return 0; 113 | } 114 | -------------------------------------------------------------------------------- /unytdtype/unytdtype/src/umath.h: -------------------------------------------------------------------------------- 1 | #ifndef _NPY_UFUNC_H 2 | #define _NPY_UFUNC_H 3 | 4 | int 5 | init_multiply_ufunc(void); 6 | 7 | #endif /*_NPY_UFUNC_H */ 8 | -------------------------------------------------------------------------------- /unytdtype/unytdtype/src/unytdtype_main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define PY_ARRAY_UNIQUE_SYMBOL unytdtype_ARRAY_API 4 | #define PY_UFUNC_UNIQUE_SYMBOL unytdtype_UFUNC_API 5 | #define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION 6 | #include "numpy/arrayobject.h" 7 | #include "numpy/ufuncobject.h" 8 | #include "numpy/dtype_api.h" 9 | 10 | #include "dtype.h" 11 | #include "umath.h" 12 | 13 | static struct PyModuleDef moduledef = { 14 | PyModuleDef_HEAD_INIT, 15 | .m_name = "unytdtype_main", 16 | .m_size = -1, 17 | }; 18 | 19 | /* Module initialization function */ 20 | PyMODINIT_FUNC 21 | PyInit__unytdtype_main(void) 22 | { 23 | import_array(); 24 | import_umath(); 25 | 26 | PyObject *m = PyModule_Create(&moduledef); 27 | if (m == NULL) { 28 | return NULL; 29 | } 30 | 31 | PyObject *mod = PyImport_ImportModule("unytdtype"); 32 | if (mod == NULL) { 33 | goto error; 34 | } 35 | UnytScalar_Type = 36 | (PyTypeObject *)PyObject_GetAttrString(mod, "UnytScalar"); 37 | Py_DECREF(mod); 38 | 39 | if (UnytScalar_Type == NULL) { 40 | goto error; 41 | } 42 | 43 | if (init_unyt_dtype() < 0) { 44 | goto error; 45 | } 46 | 47 | if (PyModule_AddObject(m, "UnytDType", (PyObject *)&UnytDType) < 0) { 48 | goto error; 49 | } 50 | 51 | if (init_multiply_ufunc() == -1) { 52 | goto error; 53 | } 54 | 55 | return m; 56 | 57 | error: 58 | Py_DECREF(m); 59 | return NULL; 60 | } 61 | --------------------------------------------------------------------------------