diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index fdd3f10..7f4114b 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -19,14 +19,14 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: ">=3.11.0" + python-version: ">=3.12" - name: Install cibuildwheel run: pip install cibuildwheel==3.1.4 - name: Build wheels env: - CIBW_BUILD: "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64 cp314t-manylinux_x86_64" + CIBW_BUILD: "cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64 cp314t-manylinux_x86_64" CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_BUILD_VERBOSITY: "3" @@ -68,7 +68,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: ">=3.11.0" + python-version: ">=3.12" - name: Install dependencies run: | @@ -88,7 +88,7 @@ jobs: - name: Build wheels env: - CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp314t-*" + CIBW_BUILD: "cp312-* cp313-* cp314-* cp314t-*" CIBW_ENABLE: cpython-prerelease cpython-freethreading # CIBW_ARCHS_MACOS: ${{ matrix.os == 'macos-13' && 'x86_64' || 'arm64' }} CIBW_BUILD_VERBOSITY: "3" @@ -135,7 +135,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: ">=3.11.0" + python-version: ">=3.12" architecture: ${{ matrix.architecture }} - name: Install CMake @@ -149,7 +149,7 @@ jobs: - name: Build wheels env: - CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp314t-*" + CIBW_BUILD: "cp312-* cp313-* cp314-* cp314t-*" CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_ARCHS_WINDOWS: ${{ matrix.architecture == 'x86' && 'x86' || 'AMD64' }} CIBW_BUILD_VERBOSITY: "3" @@ -177,7 +177,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: ">=3.11.0" + python-version: ">=3.12" - name: Install system dependencies run: | diff --git a/README.md b/README.md index 151fc1a..6ff6c50 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ A cross-platform Quad (128-bit) float Data-Type for NumPy. ## Installation ```bash -pip install "numpy>=2.4" +pip install "numpy>=2.5.3" pip install numpy-quaddtype ``` @@ -70,7 +70,7 @@ np.array([1,2,3], dtype=QuadPrecDType("longdouble")) ### Linux/Unix/macOS -**Prerequisites:** gcc/clang, CMake (≥3.15), Python 3.11+, Git, NumPy ≥ 2.4 +**Prerequisites:** gcc/clang, CMake (≥3.15), Python 3.12+, Git, NumPy ≥ 2.5.3 ```bash # setup the virtual env @@ -78,7 +78,7 @@ python3 -m venv temp source temp/bin/activate # Install build and test dependencies -pip install pytest meson meson-python "numpy>=2.4" +pip install pytest meson meson-python "numpy>=2.5.3" # To build without QBLAS (default for MSVC) # export CFLAGS="-DDISABLE_QUADBLAS" @@ -92,7 +92,7 @@ python -m pytest tests ### Windows -**Prerequisites:** Visual Studio 2017+ (with MSVC), CMake (≥3.15), Python 3.11+, Git +**Prerequisites:** Visual Studio 2017+ (with MSVC), CMake (≥3.15), Python 3.12+, Git 1. **Setup Development Environment** diff --git a/docs/index.md b/docs/index.md index d7183e6..f238bfd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ :alt: PyPI version ``` -```{image} https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue.svg +```{image} https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue.svg :alt: Python versions ``` diff --git a/pyproject.toml b/pyproject.toml index 697216e..4d52df2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = [ "meson-python>=0.18.0", "wheel", "cmake", - "numpy>=2.4" + "numpy>=2.5.3" ] build-backend = "mesonpy" @@ -20,16 +20,15 @@ maintainers = [{name = "NumPy Developers", email = "numpy-discussion@python.org" classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Free Threading", "Typing :: Typed", ] -requires-python = ">=3.11.0" +requires-python = ">=3.12" dependencies = [ - "numpy>=2.4" + "numpy>=2.5.3" ] [project.optional-dependencies] diff --git a/src/csrc/umath/binary_ops.cpp b/src/csrc/umath/binary_ops.cpp index 56aaf55..85c355d 100644 --- a/src/csrc/umath/binary_ops.cpp +++ b/src/csrc/umath/binary_ops.cpp @@ -492,48 +492,10 @@ create_quad_binary_2out_ufunc(PyObject *numpy, const char *ufunc_name) return -1; } - PyObject *promoter_capsule = - PyCapsule_New((void *)&quad_ufunc_promoter, "numpy._ufunc_promoter", NULL); - if (promoter_capsule == NULL) { + if (quad_add_promoters(ufunc) < 0) { Py_DECREF(ufunc); return -1; } - - // Register promoter for (QuadPrecDType, Any, Any, Any) - PyObject *DTypes = PyTuple_Pack(4, &QuadPrecDType, &PyArrayDescr_Type, - &PyArrayDescr_Type, &PyArrayDescr_Type); - if (DTypes == NULL) { - Py_DECREF(promoter_capsule); - Py_DECREF(ufunc); - return -1; - } - - if (PyUFunc_AddPromoter(ufunc, DTypes, promoter_capsule) < 0) { - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); - Py_DECREF(ufunc); - return -1; - } - Py_DECREF(DTypes); - - // Register promoter for (Any, QuadPrecDType, Any, Any) - DTypes = PyTuple_Pack(4, &PyArrayDescr_Type, &QuadPrecDType, - &PyArrayDescr_Type, &PyArrayDescr_Type); - if (DTypes == NULL) { - Py_DECREF(promoter_capsule); - Py_DECREF(ufunc); - return -1; - } - - if (PyUFunc_AddPromoter(ufunc, DTypes, promoter_capsule) < 0) { - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); - Py_DECREF(ufunc); - return -1; - } - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); - Py_DECREF(ufunc); return 0; } @@ -572,46 +534,10 @@ create_quad_binary_ufunc(PyObject *numpy, const char *ufunc_name) return -1; } - PyObject *promoter_capsule = - PyCapsule_New((void *)&quad_ufunc_promoter, "numpy._ufunc_promoter", NULL); - if (promoter_capsule == NULL) { - Py_DECREF(ufunc); - return -1; - } - - // Register promoter for (QuadPrecDType, Any, Any) - PyObject *DTypes = PyTuple_Pack(3, &QuadPrecDType, &PyArrayDescr_Type, &PyArrayDescr_Type); - if (DTypes == NULL) { - Py_DECREF(promoter_capsule); - Py_DECREF(ufunc); - return -1; - } - - if (PyUFunc_AddPromoter(ufunc, DTypes, promoter_capsule) < 0) { - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); - Py_DECREF(ufunc); - return -1; - } - Py_DECREF(DTypes); - - // Register promoter for (Any, QuadPrecDType, Any) - DTypes = PyTuple_Pack(3, &PyArrayDescr_Type, &QuadPrecDType, &PyArrayDescr_Type); - if (DTypes == NULL) { - Py_DECREF(promoter_capsule); - Py_DECREF(ufunc); - return -1; - } - - if (PyUFunc_AddPromoter(ufunc, DTypes, promoter_capsule) < 0) { - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); + if (quad_add_promoters(ufunc) < 0) { Py_DECREF(ufunc); return -1; } - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); - Py_DECREF(ufunc); return 0; } diff --git a/src/csrc/umath/comparison_ops.cpp b/src/csrc/umath/comparison_ops.cpp index f9a0539..5004dcc 100644 --- a/src/csrc/umath/comparison_ops.cpp +++ b/src/csrc/umath/comparison_ops.cpp @@ -269,21 +269,6 @@ comparison_object_output_is_bool(PyUFuncObject *ufunc) strcmp(ufunc->name, "logical_xor") != 0; } -// Registers `promoter` for a single (in1, in2, out) DType pattern. -static int -add_comparison_promoter(PyObject *ufunc, PyObject *promoter, PyArray_DTypeMeta *in1, - PyArray_DTypeMeta *in2, PyArray_DTypeMeta *out) -{ - PyObject *DTypes = PyTuple_Pack(3, (PyObject *)in1, (PyObject *)in2, (PyObject *)out); - if (DTypes == NULL) { - return -1; - } - - int res = PyUFunc_AddPromoter(ufunc, DTypes, promoter); - Py_DECREF(DTypes); - return res; -} - NPY_NO_EXPORT int comparison_ufunc_promoter(PyObject *ufunc_obj, PyArray_DTypeMeta *const op_dtypes[], PyArray_DTypeMeta *const signature[], PyArray_DTypeMeta *new_op_dtypes[]) @@ -401,8 +386,7 @@ create_quad_comparison_ufunc(PyObject *numpy, const char *ufunc_name) }; for (size_t i = 0; i < sizeof(promoter_patterns) / sizeof(promoter_patterns[0]); i++) { - if (add_comparison_promoter(ufunc, promoter_capsule, promoter_patterns[i][0], - promoter_patterns[i][1], promoter_patterns[i][2]) < 0) { + if (quad_add_promoter(ufunc, promoter_capsule, promoter_patterns[i]) < 0) { Py_DECREF(promoter_capsule); Py_DECREF(ufunc); return -1; diff --git a/src/csrc/umath/matmul.cpp b/src/csrc/umath/matmul.cpp index 821c288..3944915 100644 --- a/src/csrc/umath/matmul.cpp +++ b/src/csrc/umath/matmul.cpp @@ -518,48 +518,10 @@ init_matmul_ops(PyObject *numpy) return -1; } - PyObject *promoter_capsule = - PyCapsule_New((void *)&quad_ufunc_promoter, "numpy._ufunc_promoter", NULL); - if (promoter_capsule == NULL) { + if (quad_add_promoters(ufunc) < 0) { Py_DECREF(ufunc); return -1; } - - // Register promoter for (QuadPrecDType, Any, Any) - PyObject *DTypes = PyTuple_Pack(3, &QuadPrecDType, &PyArrayDescr_Type, &PyArrayDescr_Type); - if (DTypes == NULL) { - Py_DECREF(promoter_capsule); - Py_DECREF(ufunc); - return -1; - } - - if (PyUFunc_AddPromoter(ufunc, DTypes, promoter_capsule) < 0) { - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); - Py_DECREF(ufunc); - return -1; - } - Py_DECREF(DTypes); - - // Register promoter for (Any, QuadPrecDType, Any) - DTypes = PyTuple_Pack(3, &PyArrayDescr_Type, &QuadPrecDType, &PyArrayDescr_Type); - if (DTypes == NULL) { - Py_DECREF(promoter_capsule); - Py_DECREF(ufunc); - return -1; - } - - if (PyUFunc_AddPromoter(ufunc, DTypes, promoter_capsule) < 0) { - Py_DECREF(promoter_capsule); - Py_DECREF(DTypes); - Py_DECREF(ufunc); - return -1; - } - Py_DECREF(DTypes); - - Py_DECREF(promoter_capsule); - Py_DECREF(ufunc); - return 0; -} \ No newline at end of file +} diff --git a/src/csrc/umath/unary_ops.cpp b/src/csrc/umath/unary_ops.cpp index 7061294..168201a 100644 --- a/src/csrc/umath/unary_ops.cpp +++ b/src/csrc/umath/unary_ops.cpp @@ -19,6 +19,7 @@ extern "C" { #include "scalar.h" #include "dtype.h" #include "ops.hpp" +#include "umath/promoters.hpp" static NPY_CASTING quad_unary_op_resolve_descriptors(PyObject *self, PyArray_DTypeMeta *const dtypes[], @@ -140,6 +141,11 @@ create_quad_unary_ufunc(PyObject *numpy, const char *ufunc_name) return -1; } + if (quad_add_promoters(ufunc) < 0) { + Py_DECREF(ufunc); + return -1; + } + Py_DECREF(ufunc); return 0; } @@ -409,6 +415,11 @@ create_quad_unary_2out_ufunc(PyObject *numpy, const char *ufunc_name) return -1; } + if (quad_add_promoters(ufunc) < 0) { + Py_DECREF(ufunc); + return -1; + } + Py_DECREF(ufunc); return 0; } diff --git a/src/include/umath/promoters.hpp b/src/include/umath/promoters.hpp index 2f41bb5..659cbb0 100644 --- a/src/include/umath/promoters.hpp +++ b/src/include/umath/promoters.hpp @@ -22,6 +22,24 @@ quad_ufunc_has_object_input(PyUFuncObject *ufunc, PyArray_DTypeMeta *const op_dt return false; } +inline int +quad_add_promoter(PyObject *ufunc, PyObject *promoter, PyArray_DTypeMeta *const dtypes[]) +{ + int nargs = ((PyUFuncObject *)ufunc)->nargs; + PyObject *dtype_tuple = PyTuple_New(nargs); + if (dtype_tuple == NULL) { + return -1; + } + for (int i = 0; i < nargs; i++) { + Py_INCREF(dtypes[i]); + PyTuple_SET_ITEM(dtype_tuple, i, (PyObject *)dtypes[i]); + } + + int res = PyUFunc_AddPromoter(ufunc, dtype_tuple, promoter); + Py_DECREF(dtype_tuple); + return res; +} + inline void quad_set_promoted_dtype(PyArray_DTypeMeta *signature_dtype, PyArray_DTypeMeta *fallback_dtype, @@ -49,22 +67,51 @@ quad_ufunc_promoter(PyObject *ufunc_obj, PyArray_DTypeMeta *const op_dtypes[], return 0; } - if (quad_ufunc_has_object_input(ufunc, op_dtypes)) { - for (int i = 0; i < nargs; i++) { - quad_set_promoted_dtype(signature[i], &PyArray_ObjectDType, &new_op_dtypes[i]); + PyArray_DTypeMeta *common_dtype = signature[ufunc->nin]; + for (int i = ufunc->nin + 1; i < nargs; i++) { + if (signature[i] != common_dtype) { + common_dtype = NULL; + break; } - return 0; } - - // This promoter is only registered for patterns where at least one - // input is QuadPrecDType, so we always promote all args to QuadPrecDType. + if (common_dtype == NULL) { + common_dtype = quad_ufunc_has_object_input(ufunc, op_dtypes) + ? &PyArray_ObjectDType : &QuadPrecDType; + } for (int i = 0; i < nargs; i++) { - quad_set_promoted_dtype(signature[i], &QuadPrecDType, &new_op_dtypes[i]); + quad_set_promoted_dtype(signature[i], common_dtype, &new_op_dtypes[i]); } - return 0; } +inline int +quad_add_promoters(PyObject *ufunc_obj) +{ + PyUFuncObject *ufunc = (PyUFuncObject *)ufunc_obj; + assert(ufunc->nin >= 1 && ufunc->nin <= 2 && ufunc->nargs <= 4); + PyObject *capsule = PyCapsule_New((void *)&quad_ufunc_promoter, + "numpy._ufunc_promoter", NULL); + if (capsule == NULL) { + return -1; + } + PyArray_DTypeMeta *any_dtype = (PyArray_DTypeMeta *)&PyArrayDescr_Type; + PyArray_DTypeMeta *pattern[4] = {any_dtype, any_dtype, any_dtype, any_dtype}; + for (int i = 0; i < ufunc->nin; i++) { + pattern[i] = &QuadPrecDType; + } + + // All-Quad inputs must precede mixed inputs to avoid ambiguous promotion + // when an explicit output dtype excludes the Quad loop. + int res = quad_add_promoter(ufunc_obj, capsule, pattern); + for (int quad_slot = 0; res == 0 && ufunc->nin == 2 && quad_slot < 2; quad_slot++) { + pattern[quad_slot] = &QuadPrecDType; + pattern[1 - quad_slot] = any_dtype; + res = quad_add_promoter(ufunc_obj, capsule, pattern); + } + Py_DECREF(capsule); + return res; +} + inline int quad_ldexp_promoter(PyObject *ufunc_obj, PyArray_DTypeMeta *const op_dtypes[], @@ -89,4 +136,4 @@ quad_ldexp_promoter(PyObject *ufunc_obj, PyArray_DTypeMeta *const op_dtypes[], return 0; } -#endif \ No newline at end of file +#endif diff --git a/src/numpy_quaddtype/_quaddtype_main.pyi b/src/numpy_quaddtype/_quaddtype_main.pyi index eb22ce4..ca25b53 100644 --- a/src/numpy_quaddtype/_quaddtype_main.pyi +++ b/src/numpy_quaddtype/_quaddtype_main.pyi @@ -1,4 +1,5 @@ -from typing import Any, Literal, TypeAlias, final, overload +from collections.abc import Buffer +from typing import Any, Literal, SupportsIndex, TypeAlias, final, overload import builtins import numpy as np from numpy._typing import _128Bit # pyright: ignore[reportPrivateUsage] @@ -192,6 +193,10 @@ def get_sleef_constant( /, ) -> QuadPrecision: ... +def from_raw_bytes( + data: Buffer, backend: _Backend = "sleef", ld_format: SupportsIndex = -1, / +) -> QuadPrecision: ... + def set_num_threads(num_threads: int, /) -> None: ... def get_num_threads() -> int: ... def get_quadblas_version() -> str: ... diff --git a/tests/test_promoters.py b/tests/test_promoters.py new file mode 100644 index 0000000..237eb32 --- /dev/null +++ b/tests/test_promoters.py @@ -0,0 +1,200 @@ +import numpy as np +import pytest + +from numpy_quaddtype import QuadPrecDType + +SAME_DTYPE_UFUNCS = [ + np.add, np.subtract, np.multiply, np.divide, np.floor_divide, np.power, + np.remainder, np.minimum, np.maximum, np.fmin, np.fmax, np.matmul, np.fmod, + np.arctan2, np.hypot, np.float_power, np.copysign, np.nextafter, np.logaddexp, + np.logaddexp2, np.heaviside, np.divmod, np.absolute, np.negative, np.positive, + np.sign, np.sqrt, np.square, np.cbrt, np.reciprocal, np.exp, np.exp2, np.expm1, + np.log, np.log2, np.log10, np.log1p, np.sin, np.cos, np.tan, np.arcsin, + np.arccos, np.arctan, np.sinh, np.cosh, np.tanh, np.arcsinh, np.arccosh, + np.arctanh, np.floor, np.ceil, np.trunc, np.rint, np.fabs, np.spacing, + np.conjugate, np.degrees, np.radians, np.deg2rad, np.rad2deg, np.modf, +] + + +@pytest.fixture(params=["sleef", "longdouble"]) +def quad(request): + return np.array( + [[1.5, 2.5], [3.5, 4.5]], dtype=QuadPrecDType(backend=request.param) + ) + + +def assert_outputs_equal(ufunc, actual, expected): + if ufunc.nout == 1: + actual, expected = (actual,), (expected,) + for result, reference in zip(actual, expected, strict=True): + np.testing.assert_array_equal(result, reference, strict=True) + + +@pytest.mark.parametrize("ufunc", SAME_DTYPE_UFUNCS) +@pytest.mark.parametrize("keyword", ["dtype", "signature"]) +def test_explicit_float64_matches_numpy(quad, ufunc, keyword): + kwargs = {"dtype": np.float64} + if keyword == "signature": + kwargs = {"signature": (None,) * ufunc.nin + (np.float64,) * ufunc.nout} + with np.errstate(all="ignore"): + actual = ufunc(*(quad,) * ufunc.nin, **kwargs) + expected = ufunc(*(quad.astype(np.float64),) * ufunc.nin, **kwargs) + + assert_outputs_equal(ufunc, actual, expected) + + +@pytest.mark.parametrize("reverse", [False, True]) +def test_mixed_precision_matches_float32_float64(quad, reverse): + higher = np.array([2**24 + 1], dtype=quad.dtype) + lower = np.array([-2**24], dtype=np.float32) + args = (lower, higher) if reverse else (higher, lower) + reference_args = tuple(a.astype(np.float64) if a is higher else a for a in args) + + result = np.add(*args) + assert result.dtype == quad.dtype + np.testing.assert_array_equal( + result.astype(np.float64), np.add(*reference_args), strict=True + ) + + rounded = np.add(*args, dtype=np.float32) + np.testing.assert_array_equal( + rounded, np.add(*reference_args, dtype=np.float32), strict=True + ) + np.testing.assert_array_equal(rounded, [0]) + + out = np.empty(1, dtype=np.float32) + reference_out = np.empty_like(out) + assert np.add(*args, out=out) is out + np.add(*reference_args, out=reference_out) + np.testing.assert_array_equal(out, reference_out, strict=True) + np.testing.assert_array_equal(out, [1]) + + +@pytest.mark.parametrize("dtype, casting", [ + (np.float32, "same_kind"), (np.float64, "unsafe"), + (np.int64, "unsafe"), (np.bool_, "unsafe"), (object, "safe"), +]) +def test_explicit_dtype_allows_casts(quad, dtype, casting): + reference = quad.astype(object if dtype is object else np.float64) + actual = np.add(quad, quad, dtype=dtype, casting=casting) + expected = np.add(reference, reference, dtype=dtype, casting=casting) + np.testing.assert_array_equal(actual, expected, strict=True) + + +@pytest.mark.parametrize("dtype, casting", [ + (np.float32, "no"), (np.float32, "equiv"), (np.float32, "safe"), + (np.int64, "same_kind"), (np.bool_, "same_kind"), +]) +def test_explicit_dtype_rejects_casts(quad, dtype, casting): + for operand in [quad, quad.astype(np.float64)]: + with pytest.raises(TypeError): + np.add(operand, operand, dtype=dtype, casting=casting) + + +def test_explicit_dtype_requires_registered_cast(quad): + with pytest.raises(TypeError): + np.add(quad, quad, dtype=np.complex128, casting="unsafe") + + +@pytest.mark.parametrize("ufunc", [np.add, np.negative, np.matmul]) +def test_explicit_object_dtype(quad, ufunc): + actual = ufunc(*(quad,) * ufunc.nin, dtype=object) + expected = ufunc(*(quad.astype(object),) * ufunc.nin) + np.testing.assert_array_equal(actual, expected, strict=True) + + +@pytest.mark.parametrize("ufunc, error", [ + (np.sqrt, TypeError), (np.fmod, AttributeError), + (np.modf, TypeError), (np.divmod, TypeError), +]) +def test_explicit_object_dtype_without_supported_loop(quad, ufunc, error): + for operand in [quad, quad.astype(object)]: + with pytest.raises(error): + ufunc(*(operand,) * ufunc.nin, dtype=object) + + +@pytest.mark.parametrize("ufunc", [np.add, np.divmod]) +@pytest.mark.parametrize("reverse", [False, True]) +@pytest.mark.parametrize("keyword", ["dtype", "signature"]) +def test_explicit_float_dtype_overrides_object_promotion(quad, ufunc, reverse, keyword): + objects = quad.astype(object) + args = (objects, quad) if reverse else (quad, objects) + reference_args = tuple(a.astype(np.float64) if a is quad else a for a in args) + kwargs = {"dtype": np.float64} + if keyword == "signature": + kwargs = {"signature": (None, None) + (np.float64,) * ufunc.nout} + for operands in [args, reference_args]: + with pytest.raises(TypeError): + ufunc(*operands, **kwargs) + actual = ufunc(*args, **kwargs, casting="unsafe") + expected = ufunc(*reference_args, **kwargs, casting="unsafe") + assert_outputs_equal(ufunc, actual, expected) + + +@pytest.mark.parametrize("ufunc", [np.modf, np.divmod]) +@pytest.mark.parametrize("outputs", [ + (np.float32, None), (None, np.float32), (np.float32, np.float64), +]) +def test_partial_or_conflicting_outputs_match_numpy(quad, ufunc, outputs): + signature = (None,) * ufunc.nin + outputs + for operand in [quad, quad.astype(np.float64)]: + with pytest.raises(TypeError): + ufunc(*(operand,) * ufunc.nin, signature=signature) + + +@pytest.mark.parametrize("fixed_input", [0, 1]) +def test_fixed_input_is_not_overridden(quad, fixed_input): + for operand, input_dtype in [ + (quad, QuadPrecDType), (quad.astype(np.float64), np.float64) + ]: + signature = [None, None, np.float32] + signature[fixed_input] = input_dtype + with pytest.raises(TypeError): + np.add(operand, operand, signature=tuple(signature)) + + +def test_object_conjugate_preserves_values_and_backend(quad): + actual = np.conjugate(quad, dtype=object) + np.testing.assert_array_equal(actual, quad.astype(object), strict=True) + for scalar in actual.flat: + assert scalar.dtype == quad.dtype + + +class TestPromoterNoInterference: + def test_timedelta_modulus_raises_typeerror(self): + with pytest.raises(TypeError, match="remainder"): + np.remainder(np.timedelta64(7, "Y"), 15) + + def test_timedelta_divide_preserves_dtype(self): + values = np.arange(1000, dtype="m8[s]") + result = values.sum() / len(values) + assert result.dtype.kind == "m" + + def test_timedelta_mean_correct(self): + values = np.arange(1000, dtype="m8[s]") + np.testing.assert_array_equal(values.mean(), values.sum() / len(values)) + + def test_matmul_float64_preserves_values(self): + identity = np.eye(3) + values = np.ones((3, 2)) + np.testing.assert_array_equal(np.matmul(identity, values), values, strict=True) + + @pytest.mark.parametrize("ufunc", SAME_DTYPE_UFUNCS) + def test_builtin_inputs_preserve_dtype(self, ufunc): + operand = np.array([[1.5, 2.5]], dtype=np.float64) + args = (operand, operand.T) if ufunc is np.matmul else (operand,) * ufunc.nin + with np.errstate(all="ignore"): + result = ufunc(*args) + for output in result if ufunc.nout == 2 else (result,): + assert output.dtype == np.float64 + + +@pytest.mark.parametrize("method", ["reduce", "accumulate", "reduceat"]) +def test_explicit_float64_reductions(quad, method): + operand = quad.ravel() + indices = ([0, 2],) if method == "reduceat" else () + + actual = getattr(np.add, method)(operand, *indices, dtype=np.float64) + expected = getattr(np.add, method)(operand.astype(np.float64), *indices) + + np.testing.assert_array_equal(actual, expected, strict=True) diff --git a/tests/test_quaddtype.py b/tests/test_quaddtype.py index 0fb597e..254f77b 100644 --- a/tests/test_quaddtype.py +++ b/tests/test_quaddtype.py @@ -3697,6 +3697,57 @@ def test_array_conjugate_method(): np.testing.assert_array_equal(result.astype(float), [1.5, -2.5, 0.0]) +@pytest.mark.parametrize("backend", ["sleef", "longdouble"]) +def test_object_conjugate_preserves_values_and_backend(backend): + dtype = QuadPrecDType(backend=backend) + objects = np.array([1.5, 2.5], dtype=dtype).astype(object) + + result = np.conjugate(objects) + + np.testing.assert_array_equal(result, objects, strict=True) + for scalar in result: + assert scalar.dtype == dtype + + +@pytest.mark.parametrize("backend", ["sleef", "longdouble"]) +@pytest.mark.parametrize("method", ["conj", "conjugate"]) +@pytest.mark.parametrize("value", [1.5, -0.0, np.inf, np.nan]) +@pytest.mark.parametrize("args", [(), (None,)]) +def test_scalar_conjugate_preserves_values_and_backend(backend, method, value, args): + scalar = QuadPrecision(value, backend=backend) + + result = getattr(scalar, method)(*args) + + assert result.dtype == scalar.dtype + np.testing.assert_array_equal(float(result), value) + assert np.signbit(float(result)) == np.signbit(value) + + +@pytest.mark.parametrize("backend", ["sleef", "longdouble"]) +@pytest.mark.parametrize("method", ["conj", "conjugate"]) +@pytest.mark.parametrize("dtype", [np.float32, np.float64, object, QuadPrecDType]) +def test_scalar_conjugate_with_out(backend, method, dtype): + scalar = QuadPrecision(1.5, backend=backend) + out = np.empty((), dtype=scalar.dtype if dtype is QuadPrecDType else dtype) + reference_out = np.empty((), dtype=np.float64 if dtype is QuadPrecDType else dtype) + + result = getattr(scalar, method)(out) + expected = getattr(np.float64(1.5), method)(reference_out) + + np.testing.assert_array_equal(float(result), float(expected)) + np.testing.assert_array_equal(out.astype(np.float64), reference_out.astype(np.float64)) + if dtype is object or dtype is QuadPrecDType: + assert result.dtype == scalar.dtype + + +@pytest.mark.parametrize("backend", ["sleef", "longdouble"]) +@pytest.mark.parametrize("method", ["conj", "conjugate"]) +@pytest.mark.parametrize("args", [(1,), (None, None), (np.empty((), dtype=np.int64),)]) +def test_scalar_conjugate_rejects_invalid_out(backend, method, args): + for scalar in [QuadPrecision(1.5, backend=backend), np.float64(1.5)]: + with pytest.raises(TypeError): + getattr(scalar, method)(*args) + @pytest.mark.parametrize("x1,x2,expected", [ # Basic Pythagorean triples (3.0, 4.0, 5.0), @@ -6455,62 +6506,6 @@ def test_logical_reduce_on_non_quad_arrays(): assert result == True -class TestPromoterNoInterference: - """Regression tests for overly broad promoter registration. - - Prior to the fix, promoters were registered with PyArrayDescr_Type in - all slots, matching ANY dtype combination. This caused the quaddtype - promoter to intercept operations on unrelated NumPy types (timedelta64, - float64, etc.), breaking normal NumPy functionality. - - See https://github.com/numpy/numpy-quaddtype/issues/76 - """ - - def test_timedelta_modulus_raises_typeerror(self): - """timedelta64 % int must raise TypeError, not be silently handled.""" - with pytest.raises(TypeError, match="remainder"): - np.remainder(np.timedelta64(7, 'Y'), 15) - - def test_timedelta_divide_preserves_dtype(self): - """timedelta64 / int must return timedelta64, not float64.""" - a = np.arange(1000, dtype="m8[s]") - result = a.sum() / len(a) - assert result.dtype.kind == 'm', ( - f"Expected timedelta64 dtype, got {result.dtype}") - - def test_timedelta_mean_correct(self): - """timedelta mean must use timedelta division, not float promotion.""" - a = np.arange(1000, dtype="m8[s]") - mean_val = a.mean() - sum_div = a.sum() / len(a) - np.testing.assert_array_equal(mean_val, sum_div) - - @pytest.mark.parametrize("op", [ - np.add, np.subtract, np.multiply, np.divide, - np.floor_divide, np.power, np.mod, - ]) - def test_binary_ufunc_float64_preserves_dtype(self, op): - """Builtin float64 ops must not be affected by quad promoters.""" - a = np.array([1.0, 2.0, 3.0], dtype=np.float64) - b = np.array([4.0, 5.0, 6.0], dtype=np.float64) - result = op(a, b) - assert result.dtype == np.float64 - - def test_matmul_float64_preserves_dtype(self): - a = np.eye(3, dtype=np.float64) - b = np.ones((3, 2), dtype=np.float64) - result = np.matmul(a, b) - assert result.dtype == np.float64 - np.testing.assert_array_equal(result, b) - - def test_divmod_float64_preserves_dtype(self): - a = np.array([7.0, 8.0], dtype=np.float64) - b = np.array([3.0, 3.0], dtype=np.float64) - q, r = np.divmod(a, b) - assert q.dtype == np.float64 - assert r.dtype == np.float64 - - class TestObjectPromotion: @pytest.fixture def backend(self, request):