diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 3f9d6bd482..5f826fc820 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -198,6 +198,11 @@ jobs: path: dist merge-multiple: true + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + - name: Check SDist run: | mkdir -p test-sdist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 812a4e97be..bf5a9924d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: if: ${{ needs.changes.outputs.changes == 'true' }} strategy: matrix: - python-version: ["3.10", "3.13"] + python-version: ["3.11", "3.13"] steps: - uses: actions/checkout@v5 with: @@ -65,7 +65,7 @@ jobs: - uses: pre-commit/action@v3.0.1 test: - name: "${{ matrix.os }} test py${{ matrix.python-version }} numpy${{ matrix.numpy-version }} : fast-compile ${{ matrix.fast-compile }} : float32 ${{ matrix.float32 }} : ${{ matrix.part }}" + name: "${{ matrix.os }} test py${{ matrix.python-version }} : fast-compile ${{ matrix.fast-compile }} : float32 ${{ matrix.float32 }} : ${{ matrix.part }}" needs: - changes - style @@ -75,8 +75,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] - python-version: ["3.10", "3.13"] - numpy-version: ["~=1.26.0", ">=2.0"] + python-version: ["3.11", "3.13"] fast-compile: [0, 1] float32: [0, 1] install-numba: [0] @@ -92,23 +91,16 @@ jobs: - "tests/tensor/test_basic.py tests/tensor/test_inplace.py tests/tensor/conv" - "tests/tensor/test_blas.py tests/tensor/test_elemwise.py tests/tensor/test_math_scipy.py" exclude: - - python-version: "3.10" + - python-version: "3.11" fast-compile: 1 - - python-version: "3.10" + - python-version: "3.11" float32: 1 - fast-compile: 1 float32: 1 - - numpy-version: "~=1.26.0" - fast-compile: 1 - - numpy-version: "~=1.26.0" - float32: 1 - - numpy-version: "~=1.26.0" - python-version: "3.13" include: - os: "ubuntu-latest" part: "--doctest-modules pytensor --ignore=pytensor/misc/check_duplicate_key.py --ignore=pytensor/link --ignore=pytensor/ipython.py" python-version: "3.12" - numpy-version: ">=2.0" fast-compile: 0 float32: 0 install-numba: 0 @@ -117,56 +109,48 @@ jobs: install-xarray: 0 - install-numba: 1 os: "ubuntu-latest" - python-version: "3.10" - numpy-version: "~=2.1.0" + python-version: "3.11" fast-compile: 0 float32: 0 part: "tests/link/numba --ignore=tests/link/numba/test_slinalg.py" - install-numba: 1 os: "ubuntu-latest" python-version: "3.13" - numpy-version: "~=2.1.0" fast-compile: 0 float32: 0 part: "tests/link/numba --ignore=tests/link/numba/test_slinalg.py" - install-numba: 1 os: "ubuntu-latest" python-version: "3.13" - numpy-version: "~=2.1.0" fast-compile: 0 float32: 0 part: "tests/link/numba/test_slinalg.py" - install-jax: 1 os: "ubuntu-latest" - python-version: "3.10" - numpy-version: ">=2.0" + python-version: "3.11" fast-compile: 0 float32: 0 part: "tests/link/jax" - install-jax: 1 os: "ubuntu-latest" python-version: "3.13" - numpy-version: ">=2.0" fast-compile: 0 float32: 0 part: "tests/link/jax" - install-torch: 1 os: "ubuntu-latest" - python-version: "3.10" - numpy-version: ">=2.0" + python-version: "3.11" fast-compile: 0 float32: 0 part: "tests/link/pytorch" - install-xarray: 1 os: "ubuntu-latest" python-version: "3.13" - numpy-version: ">=2.0" fast-compile: 0 float32: 0 part: "tests/xtensor" - os: macos-15 python-version: "3.13" - numpy-version: ">=2.0" fast-compile: 0 float32: 0 install-numba: 0 @@ -222,7 +206,6 @@ jobs: fi env: PYTHON_VERSION: ${{ matrix.python-version }} - NUMPY_VERSION: ${{ matrix.numpy-version }} INSTALL_NUMBA: ${{ matrix.install-numba }} INSTALL_JAX: ${{ matrix.install-jax }} INSTALL_TORCH: ${{ matrix.install-torch}} @@ -265,7 +248,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: mamba-org/setup-micromamba@v2 with: environment-name: pytensor-test @@ -281,7 +264,7 @@ jobs: python -c 'import pytensor; print(pytensor.config.__str__(print_doc=False))' python -c 'import pytensor; assert pytensor.config.blas__ldflags != "", "Blas flags are empty"' env: - PYTHON_VERSION: 3.10 + PYTHON_VERSION: 3.11 - name: Download previous benchmark data uses: actions/cache@v4 with: diff --git a/doc/environment.yml b/doc/environment.yml index 5b1f8790dc..50dd65df6b 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -3,7 +3,7 @@ channels: - conda-forge - nodefaults dependencies: - - python=3.10 + - python=3.11 - gcc_linux-64 - gxx_linux-64 - numpy diff --git a/environment-osx-arm64.yml b/environment-osx-arm64.yml index 6e51d99724..0064fe0330 100644 --- a/environment-osx-arm64.yml +++ b/environment-osx-arm64.yml @@ -7,9 +7,9 @@ name: pytensor-dev channels: - conda-forge dependencies: - - python>=3.10 + - python>=3.11 - compilers - - numpy>=1.17.0 + - numpy>=2.0.0 - scipy>=1,<2 - filelock>=3.15 - etuples diff --git a/environment.yml b/environment.yml index 11b415b453..5a883752ce 100644 --- a/environment.yml +++ b/environment.yml @@ -7,9 +7,9 @@ name: pytensor-dev channels: - conda-forge dependencies: - - python>=3.10 + - python>=3.11 - compilers - - numpy>=1.17.0 + - numpy>=2.0.0 - scipy>=1,<2 - filelock>=3.15 - etuples diff --git a/pyproject.toml b/pyproject.toml index 6dae7f0c80..da56fdfe11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ requires = [ "setuptools>=59.0.0", "cython", - "numpy>=1.17.0", + "numpy>=2.0", "versioneer[toml]==0.29", ] build-backend = "setuptools.build_meta" @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [project] name = "pytensor" dynamic = ['version'] -requires-python = ">=3.10,<3.14" +requires-python = ">=3.11,<3.14" authors = [{ name = "pymc-devs", email = "pymc.devs@gmail.com" }] description = "Optimizing compiler for evaluating mathematical expressions on CPUs and GPUs." readme = "README.rst" @@ -30,7 +30,6 @@ classifiers = [ "Operating System :: Unix", "Operating System :: MacOS", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -49,7 +48,7 @@ keywords = [ dependencies = [ "setuptools>=59.0.0", "scipy>=1,<2", - "numpy>=1.17.0", + "numpy>=2.0", "filelock>=3.15", "etuples", "logical-unification", @@ -169,7 +168,7 @@ lines-after-imports = 2 [tool.mypy] -python_version = "3.10" +python_version = "3.11" ignore_missing_imports = true strict_equality = true warn_redundant_casts = true diff --git a/pytensor/link/c/basic.py b/pytensor/link/c/basic.py index a45179bbe1..0fe46c8a9f 100644 --- a/pytensor/link/c/basic.py +++ b/pytensor/link/c/basic.py @@ -29,8 +29,7 @@ from pytensor.link.c.cmodule import get_module_cache as _get_module_cache from pytensor.link.c.interface import CLinkerObject, CLinkerOp, CLinkerType from pytensor.link.utils import gc_helper, map_storage, raise_with_op, streamline -from pytensor.npy_2_compat import ndarray_c_version -from pytensor.utils import difference, uniq +from pytensor.utils import NDARRAY_C_VERSION, difference, uniq NoParams = object() @@ -1367,7 +1366,7 @@ def cmodule_key_( # We must always add the numpy ABI version here as # DynamicModule always add the include - sig.append(f"NPY_ABI_VERSION=0x{ndarray_c_version:X}") + sig.append(f"NPY_ABI_VERSION=0x{NDARRAY_C_VERSION:X}") if c_compiler: sig.append("c_compiler_str=" + c_compiler.version_str()) diff --git a/pytensor/link/numba/dispatch/elemwise.py b/pytensor/link/numba/dispatch/elemwise.py index 7244762b93..5ee056f43f 100644 --- a/pytensor/link/numba/dispatch/elemwise.py +++ b/pytensor/link/numba/dispatch/elemwise.py @@ -4,6 +4,7 @@ import numba import numpy as np from numba.core.extending import overload +from numpy.lib.array_utils import normalize_axis_index, normalize_axis_tuple from numpy.lib.stride_tricks import as_strided from pytensor.graph.op import Op @@ -19,7 +20,6 @@ store_core_outputs, ) from pytensor.link.utils import compile_function_src -from pytensor.npy_2_compat import normalize_axis_index, normalize_axis_tuple from pytensor.scalar.basic import ( AND, OR, diff --git a/pytensor/npy_2_compat.py b/pytensor/npy_2_compat.py index 207316c08f..29aa805422 100644 --- a/pytensor/npy_2_compat.py +++ b/pytensor/npy_2_compat.py @@ -1,74 +1,12 @@ -from textwrap import dedent - import numpy as np -# Conditional numpy imports for numpy 1.26 and 2.x compatibility -try: - from numpy.lib.array_utils import normalize_axis_index, normalize_axis_tuple -except ModuleNotFoundError: - # numpy < 2.0 - from numpy.core.multiarray import normalize_axis_index # type: ignore[no-redef] - from numpy.core.numeric import normalize_axis_tuple # type: ignore[no-redef] - - -try: - from numpy._core.einsumfunc import ( # type: ignore[attr-defined] - _find_contraction, - _parse_einsum_input, - ) -except ModuleNotFoundError: - from numpy.core.einsumfunc import ( # type: ignore[no-redef] - _find_contraction, - _parse_einsum_input, - ) - - -# suppress linting warning by "using" the imports here: -__all__ = [ - "_find_contraction", - "_parse_einsum_input", - "normalize_axis_index", - "normalize_axis_tuple", -] - - -numpy_version_tuple = tuple(int(n) for n in np.__version__.split(".")[:2]) -numpy_version = np.lib.NumpyVersion( - np.__version__ -) # used to compare with version strings, e.g. numpy_version < "1.16.0" -using_numpy_2 = numpy_version >= "2.0.0rc1" - - -if using_numpy_2: - ndarray_c_version = np._core._multiarray_umath._get_ndarray_c_version() # type: ignore[attr-defined] -else: - ndarray_c_version = np.core._multiarray_umath._get_ndarray_c_version() # type: ignore[attr-defined] - - -# used in tests: the type of error thrown if a value is too large for the specified -# numpy data type is different in numpy 2.x -UintOverflowError = OverflowError if using_numpy_2 else TypeError - - -# to patch up some of the C code, we need to use these special values... -if using_numpy_2: - numpy_axis_is_none_flag = np.iinfo(np.int32).min # the value of "NPY_RAVEL_AXIS" -else: - # 32 is the value used to mark axis = None in Numpy C-API prior to version 2.0 - numpy_axis_is_none_flag = 32 - - -# max number of dims is 64 in numpy 2.x; 32 in older versions -numpy_maxdims = 64 if using_numpy_2 else 32 - - # function that replicates np.unique from numpy < 2.0 def old_np_unique( arr, return_index=False, return_inverse=False, return_counts=False, axis=None ): """Replicate np.unique from numpy versions < 2.0""" - if not return_inverse or not using_numpy_2: + if not return_inverse: return np.unique(arr, return_index, return_inverse, return_counts, axis) outs = list(np.unique(arr, return_index, return_inverse, return_counts, axis)) @@ -82,227 +20,3 @@ def old_np_unique( outs[inv_idx] = outs[inv_idx].reshape(inv_shape) return tuple(outs) - - -# compatibility header for C code -def npy_2_compat_header() -> str: - """Compatibility header that Numpy suggests is vendored with code that uses Numpy < 2.0 and Numpy 2.x""" - return dedent(""" - #ifndef NUMPY_CORE_INCLUDE_NUMPY_NPY_2_COMPAT_H_ - #define NUMPY_CORE_INCLUDE_NUMPY_NPY_2_COMPAT_H_ - - - /* - * This header is meant to be included by downstream directly for 1.x compat. - * In that case we need to ensure that users first included the full headers - * and not just `ndarraytypes.h`. - */ - - #ifndef NPY_FEATURE_VERSION - #error "The NumPy 2 compat header requires `import_array()` for which " \\ - "the `ndarraytypes.h` header include is not sufficient. Please " \\ - "include it after `numpy/ndarrayobject.h` or similar." \\ - "" \\ - "To simplify inclusion, you may use `PyArray_ImportNumPy()` " \\ - "which is defined in the compat header and is lightweight (can be)." - #endif - - #if NPY_ABI_VERSION < 0x02000000 - /* - * Define 2.0 feature version as it is needed below to decide whether we - * compile for both 1.x and 2.x (defining it gaurantees 1.x only). - */ - #define NPY_2_0_API_VERSION 0x00000012 - /* - * If we are compiling with NumPy 1.x, PyArray_RUNTIME_VERSION so we - * pretend the `PyArray_RUNTIME_VERSION` is `NPY_FEATURE_VERSION`. - * This allows downstream to use `PyArray_RUNTIME_VERSION` if they need to. - */ - #define PyArray_RUNTIME_VERSION NPY_FEATURE_VERSION - /* Compiling on NumPy 1.x where these are the same: */ - #define PyArray_DescrProto PyArray_Descr - #endif - - - /* - * Define a better way to call `_import_array()` to simplify backporting as - * we now require imports more often (necessary to make ABI flexible). - */ - #ifdef import_array1 - - static inline int - PyArray_ImportNumPyAPI() - { - if (NPY_UNLIKELY(PyArray_API == NULL)) { - import_array1(-1); - } - return 0; - } - - #endif /* import_array1 */ - - - /* - * NPY_DEFAULT_INT - * - * The default integer has changed, `NPY_DEFAULT_INT` is available at runtime - * for use as type number, e.g. `PyArray_DescrFromType(NPY_DEFAULT_INT)`. - * - * NPY_RAVEL_AXIS - * - * This was introduced in NumPy 2.0 to allow indicating that an axis should be - * raveled in an operation. Before NumPy 2.0, NPY_MAXDIMS was used for this purpose. - * - * NPY_MAXDIMS - * - * A constant indicating the maximum number dimensions allowed when creating - * an ndarray. - * - * NPY_NTYPES_LEGACY - * - * The number of built-in NumPy dtypes. - */ - #if NPY_FEATURE_VERSION >= NPY_2_0_API_VERSION - #define NPY_DEFAULT_INT NPY_INTP - #define NPY_RAVEL_AXIS NPY_MIN_INT - #define NPY_MAXARGS 64 - - #elif NPY_ABI_VERSION < 0x02000000 - #define NPY_DEFAULT_INT NPY_LONG - #define NPY_RAVEL_AXIS 32 - #define NPY_MAXARGS 32 - - /* Aliases of 2.x names to 1.x only equivalent names */ - #define NPY_NTYPES NPY_NTYPES_LEGACY - #define PyArray_DescrProto PyArray_Descr - #define _PyArray_LegacyDescr PyArray_Descr - /* NumPy 2 definition always works, but add it for 1.x only */ - #define PyDataType_ISLEGACY(dtype) (1) - #else - #define NPY_DEFAULT_INT \\ - (PyArray_RUNTIME_VERSION >= NPY_2_0_API_VERSION ? NPY_INTP : NPY_LONG) - #define NPY_RAVEL_AXIS \\ - (PyArray_RUNTIME_VERSION >= NPY_2_0_API_VERSION ? -1 : 32) - #define NPY_MAXARGS \\ - (PyArray_RUNTIME_VERSION >= NPY_2_0_API_VERSION ? 64 : 32) - #endif - - - /* - * Access inline functions for descriptor fields. Except for the first - * few fields, these needed to be moved (elsize, alignment) for - * additional space. Or they are descriptor specific and are not generally - * available anymore (metadata, c_metadata, subarray, names, fields). - * - * Most of these are defined via the `DESCR_ACCESSOR` macro helper. - */ - #if NPY_FEATURE_VERSION >= NPY_2_0_API_VERSION || NPY_ABI_VERSION < 0x02000000 - /* Compiling for 1.x or 2.x only, direct field access is OK: */ - - static inline void - PyDataType_SET_ELSIZE(PyArray_Descr *dtype, npy_intp size) - { - dtype->elsize = size; - } - - static inline npy_uint64 - PyDataType_FLAGS(const PyArray_Descr *dtype) - { - #if NPY_FEATURE_VERSION >= NPY_2_0_API_VERSION - return dtype->flags; - #else - return (unsigned char)dtype->flags; /* Need unsigned cast on 1.x */ - #endif - } - - #define DESCR_ACCESSOR(FIELD, field, type, legacy_only) \\ - static inline type \\ - PyDataType_##FIELD(const PyArray_Descr *dtype) { \\ - if (legacy_only && !PyDataType_ISLEGACY(dtype)) { \\ - return (type)0; \\ - } \\ - return ((_PyArray_LegacyDescr *)dtype)->field; \\ - } - #else /* compiling for both 1.x and 2.x */ - - static inline void - PyDataType_SET_ELSIZE(PyArray_Descr *dtype, npy_intp size) - { - if (PyArray_RUNTIME_VERSION >= NPY_2_0_API_VERSION) { - ((_PyArray_DescrNumPy2 *)dtype)->elsize = size; - } - else { - ((PyArray_DescrProto *)dtype)->elsize = (int)size; - } - } - - static inline npy_uint64 - PyDataType_FLAGS(const PyArray_Descr *dtype) - { - if (PyArray_RUNTIME_VERSION >= NPY_2_0_API_VERSION) { - return ((_PyArray_DescrNumPy2 *)dtype)->flags; - } - else { - return (unsigned char)((PyArray_DescrProto *)dtype)->flags; - } - } - - /* Cast to LegacyDescr always fine but needed when `legacy_only` */ - #define DESCR_ACCESSOR(FIELD, field, type, legacy_only) \\ - static inline type \\ - PyDataType_##FIELD(const PyArray_Descr *dtype) { \\ - if (legacy_only && !PyDataType_ISLEGACY(dtype)) { \\ - return (type)0; \\ - } \\ - if (PyArray_RUNTIME_VERSION >= NPY_2_0_API_VERSION) { \\ - return ((_PyArray_LegacyDescr *)dtype)->field; \\ - } \\ - else { \\ - return ((PyArray_DescrProto *)dtype)->field; \\ - } \\ - } - #endif - - DESCR_ACCESSOR(ELSIZE, elsize, npy_intp, 0) - DESCR_ACCESSOR(ALIGNMENT, alignment, npy_intp, 0) - DESCR_ACCESSOR(METADATA, metadata, PyObject *, 1) - DESCR_ACCESSOR(SUBARRAY, subarray, PyArray_ArrayDescr *, 1) - DESCR_ACCESSOR(NAMES, names, PyObject *, 1) - DESCR_ACCESSOR(FIELDS, fields, PyObject *, 1) - DESCR_ACCESSOR(C_METADATA, c_metadata, NpyAuxData *, 1) - - #undef DESCR_ACCESSOR - - - #if !(defined(NPY_INTERNAL_BUILD) && NPY_INTERNAL_BUILD) - #if NPY_FEATURE_VERSION >= NPY_2_0_API_VERSION - static inline PyArray_ArrFuncs * - PyDataType_GetArrFuncs(const PyArray_Descr *descr) - { - return _PyDataType_GetArrFuncs(descr); - } - #elif NPY_ABI_VERSION < 0x02000000 - static inline PyArray_ArrFuncs * - PyDataType_GetArrFuncs(const PyArray_Descr *descr) - { - return descr->f; - } - #else - static inline PyArray_ArrFuncs * - PyDataType_GetArrFuncs(const PyArray_Descr *descr) - { - if (PyArray_RUNTIME_VERSION >= NPY_2_0_API_VERSION) { - return _PyDataType_GetArrFuncs(descr); - } - else { - return ((PyArray_DescrProto *)descr)->f; - } - } - #endif - - - #endif /* not internal build */ - - #endif /* NUMPY_CORE_INCLUDE_NUMPY_NPY_2_COMPAT_H_ */ - - """) diff --git a/pytensor/tensor/basic.py b/pytensor/tensor/basic.py index bf9638c473..2ecce07647 100644 --- a/pytensor/tensor/basic.py +++ b/pytensor/tensor/basic.py @@ -15,6 +15,7 @@ import numpy as np from numpy.exceptions import AxisError +from numpy.lib.array_utils import normalize_axis_index, normalize_axis_tuple import pytensor import pytensor.scalar.sharedvar @@ -31,7 +32,6 @@ from pytensor.graph.type import HasShape, Type from pytensor.link.c.op import COp from pytensor.link.c.params_type import ParamsType -from pytensor.npy_2_compat import normalize_axis_index, normalize_axis_tuple from pytensor.printing import Printer, min_informative_str, pprint, set_precedence from pytensor.raise_op import CheckAndRaise from pytensor.scalar import int32 diff --git a/pytensor/tensor/blas.py b/pytensor/tensor/blas.py index d175f50219..1b1459e55e 100644 --- a/pytensor/tensor/blas.py +++ b/pytensor/tensor/blas.py @@ -83,10 +83,10 @@ from pathlib import Path import numpy as np +from numpy.lib.array_utils import normalize_axis_tuple from scipy.linalg import get_blas_funcs from pytensor.graph import Variable, vectorize_graph -from pytensor.npy_2_compat import normalize_axis_tuple try: diff --git a/pytensor/tensor/einsum.py b/pytensor/tensor/einsum.py index 3b1e68463f..b1517ebb56 100644 --- a/pytensor/tensor/einsum.py +++ b/pytensor/tensor/einsum.py @@ -6,14 +6,13 @@ from typing import cast import numpy as np - -from pytensor.compile.builders import OpFromGraph -from pytensor.npy_2_compat import ( +from numpy._core.einsumfunc import ( # type: ignore[attr-defined] _find_contraction, _parse_einsum_input, - normalize_axis_index, - normalize_axis_tuple, ) +from numpy.lib.array_utils import normalize_axis_index, normalize_axis_tuple + +from pytensor.compile.builders import OpFromGraph from pytensor.tensor import TensorLike from pytensor.tensor.basic import ( arange, diff --git a/pytensor/tensor/elemwise.py b/pytensor/tensor/elemwise.py index 4dd29dc37d..5c98a2e023 100644 --- a/pytensor/tensor/elemwise.py +++ b/pytensor/tensor/elemwise.py @@ -4,6 +4,7 @@ from typing import Literal import numpy as np +from numpy.lib.array_utils import normalize_axis_tuple import pytensor.tensor.basic from pytensor.configdefaults import config @@ -16,7 +17,6 @@ from pytensor.link.c.op import COp, ExternalCOp, OpenMPOp from pytensor.link.c.params_type import ParamsType from pytensor.misc.frozendict import frozendict -from pytensor.npy_2_compat import normalize_axis_tuple from pytensor.printing import Printer, pprint from pytensor.scalar import get_scalar_type from pytensor.scalar.basic import identity as scalar_identity diff --git a/pytensor/tensor/extra_ops.py b/pytensor/tensor/extra_ops.py index a6eafcf485..6dafe646e8 100644 --- a/pytensor/tensor/extra_ops.py +++ b/pytensor/tensor/extra_ops.py @@ -2,6 +2,7 @@ from collections.abc import Collection, Iterable import numpy as np +from numpy.lib.array_utils import normalize_axis_index import pytensor import pytensor.scalar.basic as ps @@ -17,12 +18,7 @@ from pytensor.link.c.op import COp from pytensor.link.c.params_type import ParamsType from pytensor.link.c.type import EnumList, Generic -from pytensor.npy_2_compat import ( - normalize_axis_index, - npy_2_compat_header, - numpy_axis_is_none_flag, - old_np_unique, -) +from pytensor.npy_2_compat import old_np_unique from pytensor.raise_op import Assert from pytensor.scalar import int64 as int_t from pytensor.scalar import upcast @@ -51,7 +47,7 @@ from pytensor.tensor.type import TensorType, dvector, int_dtypes, integer_dtypes, vector from pytensor.tensor.utils import normalize_reduce_axis from pytensor.tensor.variable import TensorVariable -from pytensor.utils import LOCAL_BITWIDTH, PYTHON_INT_BITWIDTH +from pytensor.utils import LOCAL_BITWIDTH, NPY_RAVEL_AXIS, PYTHON_INT_BITWIDTH class CpuContiguous(COp): @@ -308,7 +304,7 @@ def __init__(self, axis: int | None = None, mode="add"): @property def c_axis(self) -> int: if self.axis is None: - return numpy_axis_is_none_flag + return NPY_RAVEL_AXIS return self.axis def make_node(self, x): @@ -366,10 +362,6 @@ def infer_shape(self, fgraph, node, shapes): return shapes - def c_support_code_apply(self, node: Apply, name: str) -> str: - """Needed to define NPY_RAVEL_AXIS""" - return npy_2_compat_header() - def c_code(self, node, name, inames, onames, sub): (x,) = inames (z,) = onames @@ -428,7 +420,7 @@ def c_code(self, node, name, inames, onames, sub): return code def c_code_cache_version(self): - return (9,) + return (10,) def __str__(self): return f"{self.__class__.__name__}{{{self.axis}, {self.mode}}}" diff --git a/pytensor/tensor/math.py b/pytensor/tensor/math.py index c3fa6cb0c8..8119b8a80e 100644 --- a/pytensor/tensor/math.py +++ b/pytensor/tensor/math.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Optional import numpy as np +from numpy.lib.array_utils import normalize_axis_tuple from pytensor import config, printing from pytensor import scalar as ps @@ -13,11 +14,6 @@ from pytensor.graph.replace import _vectorize_node from pytensor.link.c.op import COp from pytensor.link.c.params_type import ParamsType -from pytensor.npy_2_compat import ( - normalize_axis_tuple, - npy_2_compat_header, - numpy_axis_is_none_flag, -) from pytensor.printing import pprint from pytensor.raise_op import Assert from pytensor.scalar.basic import BinaryScalarOp @@ -165,7 +161,7 @@ def get_params(self, node): c_axis = np.int64(self.axis[0]) else: # The value here doesn't matter, it won't be used - c_axis = numpy_axis_is_none_flag + c_axis = 0 return self.params_type.get_params(c_axis=c_axis) def make_node(self, x): @@ -208,10 +204,6 @@ def perform(self, node, inp, outs): max_idx[0] = np.asarray(np.argmax(reshaped_x, axis=-1), dtype="int64") - def c_support_code_apply(self, node: Apply, name: str) -> str: - """Needed to define NPY_RAVEL_AXIS""" - return npy_2_compat_header() - def c_code(self, node, name, inp, out, sub): (x,) = inp (argmax,) = out @@ -258,7 +250,7 @@ def c_code(self, node, name, inp, out, sub): """ def c_code_cache_version(self): - return (2,) + return (3,) def infer_shape(self, fgraph, node, shapes): (ishape,) = shapes diff --git a/pytensor/tensor/nlinalg.py b/pytensor/tensor/nlinalg.py index 74c985e1e6..6c9e2f6708 100644 --- a/pytensor/tensor/nlinalg.py +++ b/pytensor/tensor/nlinalg.py @@ -4,13 +4,13 @@ from typing import Literal, cast import numpy as np +from numpy.lib.array_utils import normalize_axis_tuple from pytensor import scalar as ps from pytensor.compile.builders import OpFromGraph from pytensor.gradient import DisconnectedType from pytensor.graph.basic import Apply from pytensor.graph.op import Op -from pytensor.npy_2_compat import normalize_axis_tuple from pytensor.tensor import TensorLike from pytensor.tensor import basic as ptb from pytensor.tensor import math as ptm diff --git a/pytensor/tensor/rewriting/basic.py b/pytensor/tensor/rewriting/basic.py index 514296e76b..696d89a366 100644 --- a/pytensor/tensor/rewriting/basic.py +++ b/pytensor/tensor/rewriting/basic.py @@ -25,6 +25,7 @@ import logging import numpy as np +from numpy.lib.array_utils import normalize_axis_index from pytensor import compile, config from pytensor.compile.ops import ViewOp @@ -41,7 +42,6 @@ ) from pytensor.graph.rewriting.db import RewriteDatabase from pytensor.graph.rewriting.unify import OpPattern, OpPatternOpTypeType -from pytensor.npy_2_compat import normalize_axis_index from pytensor.raise_op import Assert, CheckAndRaise, assert_op from pytensor.scalar import ( AND, diff --git a/pytensor/tensor/rewriting/subtensor_lift.py b/pytensor/tensor/rewriting/subtensor_lift.py index bfb78a98e5..4d0a8cd5cb 100644 --- a/pytensor/tensor/rewriting/subtensor_lift.py +++ b/pytensor/tensor/rewriting/subtensor_lift.py @@ -2,12 +2,12 @@ from typing import cast import numpy as np +from numpy.lib.array_utils import normalize_axis_index, normalize_axis_tuple from pytensor import Variable from pytensor.compile import optdb from pytensor.graph import Constant, FunctionGraph, node_rewriter, vectorize_graph from pytensor.graph.rewriting.basic import NodeRewriter, copy_stack_trace -from pytensor.npy_2_compat import normalize_axis_index, normalize_axis_tuple from pytensor.scalar import basic as ps from pytensor.tensor.basic import ( Alloc, diff --git a/pytensor/tensor/shape.py b/pytensor/tensor/shape.py index 348d356f98..34b141d454 100644 --- a/pytensor/tensor/shape.py +++ b/pytensor/tensor/shape.py @@ -6,6 +6,7 @@ from typing import cast as typing_cast import numpy as np +from numpy.lib.array_utils import normalize_axis_tuple import pytensor from pytensor.gradient import DisconnectedType @@ -15,7 +16,6 @@ from pytensor.graph.type import HasShape from pytensor.link.c.op import COp from pytensor.link.c.params_type import ParamsType -from pytensor.npy_2_compat import normalize_axis_tuple from pytensor.tensor import _get_vector_length, as_tensor_variable, get_vector_length from pytensor.tensor import basic as ptb from pytensor.tensor.exceptions import NotScalarConstantError diff --git a/pytensor/tensor/special.py b/pytensor/tensor/special.py index df7edf05ad..3464294a14 100644 --- a/pytensor/tensor/special.py +++ b/pytensor/tensor/special.py @@ -6,7 +6,6 @@ from pytensor.graph.basic import Apply from pytensor.graph.replace import _vectorize_node from pytensor.link.c.op import COp -from pytensor.npy_2_compat import npy_2_compat_header from pytensor.tensor.basic import as_tensor_variable from pytensor.tensor.elemwise import get_normalized_batch_axes from pytensor.tensor.math import gamma, gammaln, log, neg, sum @@ -61,11 +60,7 @@ def infer_shape(self, fgraph, node, shape): return [shape[1]] def c_code_cache_version(self): - return (5,) - - def c_support_code_apply(self, node: Apply, name: str) -> str: - # return super().c_support_code_apply(node, name) - return npy_2_compat_header() + return (6,) def c_code(self, node, name, inp, out, sub): dy, sm = inp @@ -296,10 +291,6 @@ def infer_shape(self, fgraph, node, shape): def c_headers(self, **kwargs): return [""] - def c_support_code_apply(self, node: Apply, name: str) -> str: - """Needed to define NPY_RAVEL_AXIS""" - return npy_2_compat_header() - def c_code(self, node, name, inp, out, sub): (x,) = inp (sm,) = out @@ -495,7 +486,7 @@ def c_code(self, node, name, inp, out, sub): @staticmethod def c_code_cache_version(): - return (5,) + return (6,) def softmax(c, axis=None): @@ -555,10 +546,6 @@ def infer_shape(self, fgraph, node, shape): def c_headers(self, **kwargs): return [""] - def c_support_code_apply(self, node: Apply, name: str) -> str: - """Needed to define NPY_RAVEL_AXIS""" - return npy_2_compat_header() - def c_code(self, node, name, inp, out, sub): (x,) = inp (sm,) = out @@ -750,7 +737,7 @@ def c_code(self, node, name, inp, out, sub): @staticmethod def c_code_cache_version(): - return (2,) + return (3,) def log_softmax(c, axis=None): diff --git a/pytensor/tensor/subtensor.py b/pytensor/tensor/subtensor.py index 1bf9ad65c1..ccc706c7f2 100644 --- a/pytensor/tensor/subtensor.py +++ b/pytensor/tensor/subtensor.py @@ -6,6 +6,7 @@ from typing import cast, overload import numpy as np +from numpy.lib.array_utils import normalize_axis_tuple import pytensor from pytensor import scalar as ps @@ -18,7 +19,6 @@ from pytensor.graph.utils import MethodNotDefined from pytensor.link.c.op import COp from pytensor.link.c.params_type import ParamsType -from pytensor.npy_2_compat import normalize_axis_tuple, numpy_version, using_numpy_2 from pytensor.printing import Printer, pprint, set_precedence from pytensor.scalar.basic import ScalarConstant, ScalarVariable from pytensor.tensor import ( @@ -2330,199 +2330,6 @@ def copy_of_x(self, x): return f"""(PyArrayObject*)PyArray_FromAny(py_{x}, NULL, 0, 0, NPY_ARRAY_ENSURECOPY, NULL)""" - def c_support_code(self, **kwargs): - if numpy_version < "1.8.0" or using_numpy_2: - return None - - types = [ - "npy_" + t - for t in [ - "int8", - "int16", - "int32", - "int64", - "uint8", - "uint16", - "uint32", - "uint64", - "float16", - "float32", - "float64", - ] - ] - - complex_types = ["npy_" + t for t in ("complex32", "complex64", "complex128")] - - inplace_map_template = """ - #if defined(%(typen)s) - static void %(type)s_inplace_add(PyArrayMapIterObject *mit, - PyArrayIterObject *it, int inc_or_set) - { - int index = mit->size; - while (index--) { - %(op)s - - PyArray_MapIterNext(mit); - PyArray_ITER_NEXT(it); - } - } - #endif - """ - - floatadd = ( - "((%(type)s*)mit->dataptr)[0] = " - "(inc_or_set ? ((%(type)s*)mit->dataptr)[0] : 0)" - " + ((%(type)s*)it->dataptr)[0];" - ) - complexadd = """ - ((%(type)s*)mit->dataptr)[0].real = - (inc_or_set ? ((%(type)s*)mit->dataptr)[0].real : 0) - + ((%(type)s*)it->dataptr)[0].real; - ((%(type)s*)mit->dataptr)[0].imag = - (inc_or_set ? ((%(type)s*)mit->dataptr)[0].imag : 0) - + ((%(type)s*)it->dataptr)[0].imag; - """ - - fns = "".join( - [ - inplace_map_template - % {"type": t, "typen": t.upper(), "op": floatadd % {"type": t}} - for t in types - ] - + [ - inplace_map_template - % {"type": t, "typen": t.upper(), "op": complexadd % {"type": t}} - for t in complex_types - ] - ) - - def gen_binop(type, typen): - return f""" - #if defined({typen}) - {type}_inplace_add, - #endif - """ - - fn_array = ( - "static inplace_map_binop addition_funcs[] = {" - + "".join(gen_binop(type=t, typen=t.upper()) for t in types + complex_types) - + "NULL};\n" - ) - - def gen_num(typen): - return f""" - #if defined({typen}) - {typen}, - #endif - """ - - type_number_array = ( - "static int type_numbers[] = {" - + "".join(gen_num(typen=t.upper()) for t in types + complex_types) - + "-1000};" - ) - - code = ( - """ - typedef void (*inplace_map_binop)(PyArrayMapIterObject *, - PyArrayIterObject *, int inc_or_set); - """ - + fns - + fn_array - + type_number_array - + """ - static int - map_increment(PyArrayMapIterObject *mit, PyArrayObject *op, - inplace_map_binop add_inplace, int inc_or_set) - { - PyArrayObject *arr = NULL; - PyArrayIterObject *it; - PyArray_Descr *descr; - if (mit->ait == NULL) { - return -1; - } - descr = PyArray_DESCR(mit->ait->ao); - Py_INCREF(descr); - arr = (PyArrayObject *)PyArray_FromAny((PyObject *)op, descr, - 0, 0, NPY_ARRAY_FORCECAST, NULL); - if (arr == NULL) { - return -1; - } - if ((mit->subspace != NULL) && (mit->consec)) { - PyArray_MapIterSwapAxes(mit, (PyArrayObject **)&arr, 0); - if (arr == NULL) { - return -1; - } - } - it = (PyArrayIterObject*) - PyArray_BroadcastToShape((PyObject*)arr, mit->dimensions, mit->nd); - if (it == NULL) { - Py_DECREF(arr); - return -1; - } - - (*add_inplace)(mit, it, inc_or_set); - - Py_DECREF(arr); - Py_DECREF(it); - return 0; - } - - - static int - inplace_increment(PyArrayObject *a, PyObject *index, PyArrayObject *inc, - int inc_or_set) - { - inplace_map_binop add_inplace = NULL; - int type_number = -1; - int i = 0; - PyArrayMapIterObject * mit; - - if (PyArray_FailUnlessWriteable(a, "input/output array") < 0) { - return -1; - } - - if (PyArray_NDIM(a) == 0) { - PyErr_SetString(PyExc_IndexError, "0-d arrays can't be indexed."); - return -1; - } - type_number = PyArray_TYPE(a); - - while (type_numbers[i] >= 0 && addition_funcs[i] != NULL){ - if (type_number == type_numbers[i]) { - add_inplace = addition_funcs[i]; - break; - } - i++ ; - } - - if (add_inplace == NULL) { - PyErr_SetString(PyExc_TypeError, "unsupported type for a"); - return -1; - } - mit = (PyArrayMapIterObject *) PyArray_MapIterArray(a, index); - if (mit == NULL) { - goto fail; - } - if (map_increment(mit, inc, add_inplace, inc_or_set) != 0) { - goto fail; - } - - Py_DECREF(mit); - - Py_INCREF(Py_None); - return 0; - - fail: - Py_XDECREF(mit); - - return -1; - } - """ - ) - - return code - def c_code(self, node, name, input_names, output_names, sub): x, y, idx = input_names [out] = output_names @@ -2636,34 +2443,7 @@ def c_code(self, node, name, input_names, output_names, sub): """ return code - if numpy_version < "1.8.0" or using_numpy_2: - raise NotImplementedError - - return f""" - PyObject* rval = NULL; - if ({params}->inplace) - {{ - if ({x} != {out}) - {{ - Py_XDECREF({out}); - Py_INCREF({x}); - {out} = {x}; - }} - }} - else - {{ - Py_XDECREF({out}); - {out} = {copy_of_x}; - if (!{out}) {{ - // Exception already set - {fail} - }} - }} - if (inplace_increment({out}, (PyObject *){idx}, {y}, (1 - {params}->set_instead_of_inc))) {{ - {fail}; - }} - Py_XDECREF(rval); - """ + raise NotImplementedError def c_code_cache_version(self): return (10,) diff --git a/pytensor/tensor/utils.py b/pytensor/tensor/utils.py index 0f41cfb3ae..1f12e47cc1 100644 --- a/pytensor/tensor/utils.py +++ b/pytensor/tensor/utils.py @@ -5,10 +5,10 @@ import numpy as np from numpy import nditer +from numpy.lib.array_utils import normalize_axis_tuple import pytensor from pytensor.graph import FunctionGraph, Variable -from pytensor.npy_2_compat import normalize_axis_tuple from pytensor.tensor.exceptions import NotScalarConstantError from pytensor.utils import hash_from_code diff --git a/pytensor/utils.py b/pytensor/utils.py index c81fb74f56..46c53b0759 100644 --- a/pytensor/utils.py +++ b/pytensor/utils.py @@ -10,6 +10,8 @@ from functools import partial from pathlib import Path +import numpy as np + __all__ = [ "get_unbound_function", @@ -19,6 +21,8 @@ "output_subprocess_Popen", "LOCAL_BITWIDTH", "PYTHON_INT_BITWIDTH", + "NPY_RAVEL_AXIS", + "NDARRAY_C_VERSION", "NoDuplicateOptWarningFilter", ] @@ -46,6 +50,13 @@ 'l' denotes a C long int, and the size is expressed in bytes. """ +NPY_RAVEL_AXIS = np.iinfo(np.int32).min +""" +The value of the numpy C API NPY_RAVEL_AXIS. +""" + +NDARRAY_C_VERSION = np._core._multiarray_umath._get_ndarray_c_version() # type: ignore[attr-defined] + def __call_excepthooks(type, value, trace): """ diff --git a/scripts/slowest_tests/update-slowest-times-issue.sh b/scripts/slowest_tests/update-slowest-times-issue.sh index a87ce19ec3..690e6b72f6 100644 --- a/scripts/slowest_tests/update-slowest-times-issue.sh +++ b/scripts/slowest_tests/update-slowest-times-issue.sh @@ -21,10 +21,10 @@ jobs=$(gh api /repos/$owner/$repo/actions/runs/$latest_id/jobs --jq ' | map({name: .name, run_id: .run_id, id: .id, started_at: .started_at, completed_at: .completed_at}) ') -# Skip 3.10, float32, and Benchmark tests +# Skip oldest supported Python version, float32, and Benchmark tests function skip_job() { name=$1 - if [[ $name == *"py3.10"* ]]; then + if [[ $name == *"py3.11"* ]]; then return 0 fi diff --git a/tests/compile/function/test_function.py b/tests/compile/function/test_function.py index b4748e78c5..d36743192f 100644 --- a/tests/compile/function/test_function.py +++ b/tests/compile/function/test_function.py @@ -10,7 +10,6 @@ from pytensor.compile.function import function, function_dump from pytensor.compile.io import In from pytensor.configdefaults import config -from pytensor.npy_2_compat import UintOverflowError from pytensor.tensor.type import ( bscalar, bvector, @@ -176,11 +175,11 @@ def test_in_allow_downcast_int(self): assert np.array_equal(f([2**20], np.ones(1, dtype="int8"), 1), [2]) # Value too big for b, raises OverflowError (in numpy >= 2.0... TypeError in numpy < 2.0) - with pytest.raises(UintOverflowError): + with pytest.raises(OverflowError): f([3], [312], 1) # Value too big for c, raises OverflowError - with pytest.raises(UintOverflowError): + with pytest.raises(OverflowError): f([3], [6], 806) def test_in_allow_downcast_floatX(self): diff --git a/tests/compile/function/test_pfunc.py b/tests/compile/function/test_pfunc.py index 3e23b12f74..564b1bca47 100644 --- a/tests/compile/function/test_pfunc.py +++ b/tests/compile/function/test_pfunc.py @@ -9,7 +9,6 @@ from pytensor.compile.sharedvalue import shared from pytensor.configdefaults import config from pytensor.graph.utils import MissingInputError -from pytensor.npy_2_compat import UintOverflowError from pytensor.tensor.math import sum as pt_sum from pytensor.tensor.type import ( bscalar, @@ -239,11 +238,11 @@ def test_param_allow_downcast_int(self): assert np.all(f([2**20], np.ones(1, dtype="int8"), 1) == 2) # Value too big for b, raises OverflowError in numpy >= 2.0, TypeError in numpy <2.0 - with pytest.raises(UintOverflowError): + with pytest.raises(OverflowError): f([3], [312], 1) # Value too big for c, raises OverflowError in numpy >= 2.0, TypeError in numpy <2.0 - with pytest.raises(UintOverflowError): + with pytest.raises(OverflowError): f([3], [6], 806) def test_param_allow_downcast_floatX(self): @@ -329,7 +328,7 @@ def test_allow_input_downcast_int(self): g([3], np.array([6], dtype="int16"), 0) # Value too big for b, raises OverflowError in numpy >= 2.0, TypeError in numpy <2.0 - with pytest.raises(UintOverflowError): + with pytest.raises(OverflowError): g([3], [312], 0) h = pfunc([a, b, c], (a + b + c)) # Default: allow_input_downcast=None @@ -340,7 +339,7 @@ def test_allow_input_downcast_int(self): h([3], np.array([6], dtype="int16"), 0) # Value too big for b, raises OverflowError in numpy >= 2.0, TypeError in numpy <2.0 - with pytest.raises(UintOverflowError): + with pytest.raises(OverflowError): h([3], [312], 0) def test_allow_downcast_floatX(self): diff --git a/tests/tensor/test_elemwise.py b/tests/tensor/test_elemwise.py index e89a70d0f1..d01dba4b4c 100644 --- a/tests/tensor/test_elemwise.py +++ b/tests/tensor/test_elemwise.py @@ -19,7 +19,6 @@ from pytensor.graph.replace import vectorize_node from pytensor.link.basic import PerformLinker from pytensor.link.c.basic import CLinker, OpWiseCLinker -from pytensor.npy_2_compat import numpy_maxdims from pytensor.scalar import ScalarOp, float32, float64, int32, int64 from pytensor.tensor import as_tensor_variable from pytensor.tensor.basic import get_scalar_constant_value, second @@ -168,6 +167,7 @@ def test_infer_shape(self): ) def test_too_big_rank(self): + numpy_maxdims = 64 x = self.type(self.dtype, shape=())() y = x.dimshuffle(("x",) * (numpy_maxdims + 1)) diff --git a/tests/tensor/test_math.py b/tests/tensor/test_math.py index c68b8a3159..9215693f79 100644 --- a/tests/tensor/test_math.py +++ b/tests/tensor/test_math.py @@ -24,7 +24,6 @@ from pytensor.graph.replace import vectorize_node from pytensor.graph.traversal import ancestors, applys_between from pytensor.link.c.basic import DualLinker -from pytensor.npy_2_compat import using_numpy_2 from pytensor.printing import pprint from pytensor.raise_op import Assert from pytensor.tensor import blas, blas_c @@ -399,10 +398,7 @@ def test_maximum_minimum_grad(): # in numpy >= 2.0, negating a uint raises an error neg_good = _good_broadcast_unary_normal.copy() -if using_numpy_2: - neg_bad = {"uint8": neg_good.pop("uint8"), "uint16": neg_good.pop("uint16")} -else: - neg_bad = None +neg_bad = {"uint8": neg_good.pop("uint8"), "uint16": neg_good.pop("uint16")} TestNegBroadcast = makeBroadcastTester( op=neg,