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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions scipy/_lib/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@
IntNumber = Union[int, np.integer]
DecimalNumber = Union[float, np.floating, np.integer]

copy_if_needed: Optional[bool]

if np.lib.NumpyVersion(np.__version__) >= "2.0.0":
copy_if_needed = None
elif np.lib.NumpyVersion(np.__version__) < "1.28.0":
copy_if_needed = False
else:
# 2.0.0 dev versions, handle cases where copy may or may not exist
try:
np.array([1]).__array__(copy=None) # type: ignore[call-overload]
copy_if_needed = None
except TypeError:
copy_if_needed = False

# Since Generator was introduced in numpy 1.17, the following condition is needed for
# backward compatibility
if TYPE_CHECKING:
Expand Down
6 changes: 5 additions & 1 deletion scipy/fft/_pocketfft/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
import contextlib

import numpy as np

from scipy._lib._util import copy_if_needed

# good_size is exposed (and used) from this import
from .pypocketfft import good_size


__all__ = ['good_size', 'set_workers', 'get_workers']

_config = threading.local()
Expand Down Expand Up @@ -95,7 +99,7 @@ def _asfarray(x):
# Require native byte order
dtype = x.dtype.newbyteorder('=')
# Always align input
copy = not x.flags['ALIGNED']
copy = True if not x.flags['ALIGNED'] else copy_if_needed
return np.array(x, dtype=dtype, copy=copy)

def _datacopied(arr, original):
Expand Down
2 changes: 1 addition & 1 deletion scipy/fft/_pocketfft/tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ class FakeArray2:
def __init__(self, data):
self._data = data

def __array__(self):
def __array__(self, dtype=None, copy=None):
return self._data

# TODO: Is this test actually valuable? The behavior it's testing shouldn't be
Expand Down
2 changes: 1 addition & 1 deletion scipy/fftpack/tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ class FakeArray2:
def __init__(self, data):
self._data = data

def __array__(self):
def __array__(self, dtype=None, copy=None):
return self._data


Expand Down
5 changes: 4 additions & 1 deletion scipy/interpolate/_interpolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ravel, poly1d, asarray, intp)

import scipy.special as spec
from scipy._lib._util import copy_if_needed
from scipy.special import comb

from . import _fitpack_py
Expand Down Expand Up @@ -502,7 +503,9 @@ def __init__(self, x, y, kind='linear', axis=-1,

# `copy` keyword semantics changed in NumPy 2.0, once that is
# the minimum version this can use `copy=None`.
self.copy = np._CopyMode.ALWAYS if copy else np._CopyMode.IF_NEEDED
self.copy = copy
if not copy:
self.copy = copy_if_needed

if kind in ['zero', 'slinear', 'quadratic', 'cubic']:
order = {'zero': 0, 'slinear': 1,
Expand Down
2 changes: 1 addition & 1 deletion scipy/interpolate/tests/test_rgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ def __getitem__(self, idx):
def __array_interface__(self):
return None

def __array__(self):
def __array__(self, dtype=None, copy=None):
raise RuntimeError("No array representation")


Expand Down
2 changes: 1 addition & 1 deletion scipy/io/matlab/tests/test_mio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,7 +1239,7 @@ def test_save_unicode_field(tmpdir):

def test_save_custom_array_type(tmpdir):
class CustomArray:
def __array__(self):
def __array__(self, dtype=None, copy=None):
return np.arange(6.0).reshape(2, 3)
a = CustomArray()
filename = os.path.join(str(tmpdir), 'test.mat')
Expand Down
2 changes: 1 addition & 1 deletion scipy/linalg/_testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class _FakeMatrix2:
def __init__(self, data):
self._data = data

def __array__(self):
def __array__(self, dtype=None, copy=None):
return self._data


Expand Down
2 changes: 1 addition & 1 deletion scipy/linalg/tests/test_decomp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2361,7 +2361,7 @@ def test_datacopied(self):
M2 = M.copy()

class Fake1:
def __array__(self):
def __array__(self, dtype=None, copy=None):
return A

class Fake2:
Expand Down
25 changes: 20 additions & 5 deletions scipy/optimize/_nonlin.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Copyright (C) 2009, Pauli Virtanen <pav@iki.fi>
# Distributed under the same license as SciPy.

import inspect
import sys
import warnings

import numpy as np
from scipy.linalg import norm, solve, inv, qr, svd, LinAlgError
from numpy import asarray, dot, vdot

from scipy.linalg import norm, solve, inv, qr, svd, LinAlgError
import scipy.sparse.linalg
import scipy.sparse
from scipy.linalg import get_blas_funcs
import inspect
from scipy._lib._util import getfullargspec_no_self as _getfullargspec
from ._linesearch import scalar_search_wolfe1, scalar_search_armijo

Expand Down Expand Up @@ -421,8 +424,12 @@ def __init__(self, **kw):
if value is not None:
setattr(self, name, kw[name])

if hasattr(self, 'todense'):
self.__array__ = lambda: self.todense()

if hasattr(self, "todense"):
def __array__(self, dtype=None, copy=None):
if dtype is not None:
raise ValueError(f"`dtype` must be None, was {dtype}")
return self.todense()

def aspreconditioner(self):
return InverseJacobian(self)
Expand Down Expand Up @@ -675,7 +682,15 @@ def append(self, c, d):
if len(self.cs) > c.size:
self.collapse()

def __array__(self):
def __array__(self, dtype=None, copy=None):
if dtype is not None:
warnings.warn("LowRankMatrix is scipy-internal code, `dtype` "
f"should only be None but was {dtype} (not handled)",
stacklevel=3)
if copy is not None:
warnings.warn("LowRankMatrix is scipy-internal code, `copy` "
f"should only be None but was {copy} (not handled)",
stacklevel=3)
if self.collapsed is not None:
return self.collapsed

Expand Down
3 changes: 3 additions & 0 deletions scipy/sparse/_bsr.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import numpy as np

from scipy._lib._util import copy_if_needed
from ._matrix import spmatrix
from ._data import _data_matrix, _minmax_mixin
from ._compressed import _cs_matrix
Expand Down Expand Up @@ -78,6 +79,8 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False, blocksize=None):
maxval = max(maxval, max(blocksize))
idx_dtype = self._get_index_dtype((indices, indptr), maxval=maxval,
check_contents=True)
if not copy:
copy = copy_if_needed
self.indices = np.array(indices, copy=copy, dtype=idx_dtype)
self.indptr = np.array(indptr, copy=copy, dtype=idx_dtype)
self.data = getdata(data, copy=copy, dtype=dtype)
Expand Down
4 changes: 3 additions & 1 deletion scipy/sparse/_compressed.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import operator

import numpy as np
from scipy._lib._util import _prune_array
from scipy._lib._util import _prune_array, copy_if_needed

from ._base import _spbase, issparse, SparseEfficiencyWarning
from ._data import _data_matrix, _minmax_mixin
Expand Down Expand Up @@ -67,6 +67,8 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False):
maxval=maxval,
check_contents=True)

if not copy:
copy = copy_if_needed
self.indices = np.array(indices, copy=copy,
dtype=idx_dtype)
self.indptr = np.array(indptr, copy=copy, dtype=idx_dtype)
Expand Down
3 changes: 3 additions & 0 deletions scipy/sparse/_coo.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import numpy as np

from .._lib._util import copy_if_needed
from ._matrix import spmatrix
from ._sparsetools import coo_tocsr, coo_todense, coo_matvec
from ._base import issparse, SparseEfficiencyWarning, _spbase, sparray
Expand All @@ -26,6 +27,8 @@ class _coo_base(_data_matrix, _minmax_mixin):
def __init__(self, arg1, shape=None, dtype=None, copy=False):
_data_matrix.__init__(self)
is_array = isinstance(self, sparray)
if not copy:
copy = copy_if_needed

if isinstance(arg1, tuple):
if isshape(arg1, allow_1d=is_array):
Expand Down
3 changes: 3 additions & 0 deletions scipy/sparse/_dia.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import numpy as np

from .._lib._util import copy_if_needed
from ._matrix import spmatrix
from ._base import issparse, _formats, _spbase, sparray
from ._data import _data_matrix
Expand Down Expand Up @@ -54,6 +55,8 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False):
else:
if shape is None:
raise ValueError('expected a shape argument')
if not copy:
copy = copy_if_needed
self.data = np.atleast_2d(np.array(arg1[0], dtype=dtype, copy=copy))
offsets = np.array(arg1[1],
dtype=self._get_index_dtype(maxval=max(shape)),
Expand Down
3 changes: 3 additions & 0 deletions scipy/spatial/_ckdtree.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import numpy as np
import scipy.sparse
from scipy._lib._util import copy_if_needed

cimport numpy as np

Expand Down Expand Up @@ -551,6 +552,8 @@ cdef class cKDTree:

self._python_tree = None

if not copy_data:
copy_data = copy_if_needed
data = np.array(data, order='C', copy=copy_data, dtype=np.float64)

if data.ndim != 2:
Expand Down
16 changes: 7 additions & 9 deletions scipy/stats/tests/test_axis_nan_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1168,23 +1168,21 @@ def test_raise_invalid_args_g17713():
with pytest.raises(TypeError, match=message):
stats.gmean([1, 2, 3], 0, float, [1, 1, 1], 10)

@pytest.mark.parametrize(
'dtype',
(list(np.typecodes['Float']
+ np.typecodes['Integer']
+ np.typecodes['Complex'])))

@pytest.mark.parametrize('dtype', [np.int16, np.float32, np.complex128])
def test_array_like_input(dtype):
# Check that `_axis_nan_policy`-decorated functions work with custom
# containers that are coercible to numeric arrays

class ArrLike:
def __init__(self, x):
def __init__(self, x, dtype):
self._x = x
self._dtype = dtype

def __array__(self):
return np.asarray(x, dtype=dtype)
def __array__(self, dtype=None, copy=None):
return np.asarray(x, dtype=self._dtype)

x = [1]*2 + [3, 4, 5]
res = stats.mode(ArrLike(x))
res = stats.mode(ArrLike(x, dtype=dtype))
assert res.mode == 1
assert res.count == 2
2 changes: 1 addition & 1 deletion scipy/stats/tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2478,7 +2478,7 @@ class ArrLike:
def __init__(self, x):
self._x = x

def __array__(self):
def __array__(self, dtype=None, copy=None):
return self._x.astype(object)

with pytest.raises(TypeError, match=message):
Expand Down