Repository: scipy/scipy

Issue:
BUG: Test suite leaks file descriptors (`OSError: [Errno 24] Too many open files` on PyPy3.10)
### Describe your issue.

When running the complete test suite on PyPy3.10, I'm getting lots of test failurs (almost 1000) directly related to errors such as:

```pytb
ERROR scipy/fft/_pocketfft/tests/test_real_transforms.py::test_idst_definition[13-4-int] - OSError: [Errno 24] Too many open files: '/v
ar/tmp/portage/dev-python/scipy-1.11.3-r1/work/scipy-1.11.3-pypy3/install/usr/lib/pypy...
```

After investigating the problem, it seems that `get_reference_data()` in `scipy/fft/_pocketfft/tests/test_real_transforms.py` is calling `np.load()` a few times. The returned objects hold an open file handle. Since they are neither explicitly `.close()`d nor used via a context manager, the file remains open until GC cleans the object up. The GC in PyPy is less aggressive, therefore it becomes possible for the fds to remain open until hitting the system limit.

FWICS `get_reference_data()` attempts to cache the data in `globals()`. However, this doesn't seem to work at all — neither in PyPy3.10, nor in CPython 3.11. If I add a `raise ValueError(1)` below `if ref is not None:`, it never triggers.

The same seems to apply to other uses of `np.load()` across the codebase but I suppose they aren't called as often as in this particular test, so they're not a critical problem right now.

### Reproducing Code Example

```python
pypy3.10 -m pip install scipy pytest hypothesis
# the issue is easier to reproduce with lower fd limit
ulimit -n 64
pypy3.10 -m pytest --pyargs scipy.fft._pocketfft.tests.test_real_transforms
```


### Error message

```shell
_________________________________________ ERROR at setup of TestDCT.test_definition[0-2-int] __________________________________________

request = <SubRequest 'fftwdata_size' for <Function test_definition[0-2-int]>>

>   ???

.venv/lib/pypy3.10/site-packages/scipy/fft/_pocketfft/tests/test_real_transforms.py:70: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/pypy3.10/site-packages/scipy/fft/_pocketfft/tests/test_real_transforms.py:34: in get_reference_data
    ???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

file = '/tmp/.venv/lib/pypy3.10/site-packages/scipy/fft/_pocketfft/tests/../../../fftpack/tests/test.npz', mmap_mode = None
allow_pickle = False, fix_imports = True, encoding = 'ASCII'

>   ???
E   OSError: [Errno 24] Too many open files: '/tmp/.venv/lib/pypy3.10/site-packages/scipy/fft/_pocketfft/tests/../../../fftpack/tests/test.npz'

.venv/lib/pypy3.10/site-packages/numpy/lib/npyio.py:427: OSError
```


### SciPy/NumPy/Python version and system information

```shell
1.11.3 1.26.2 sys.version_info(major=3, minor=10, micro=13, releaselevel='final', serial=0)
/tmp/.venv/lib/pypy3.10/site-packages/scipy/__config__.py:146: UserWarning: Install `pyyaml` for better output
  warnings.warn("Install `pyyaml` for better output", stacklevel=1)
{
  "Compilers": {
    "c": {
      "name": "gcc",
      "linker": "ld.bfd",
      "version": "13.2.1",
      "commands": "ccache, cc"
    },
    "cython": {
      "name": "cython",
      "linker": "cython",
      "version": "0.29.36",
      "commands": "cython"
    },
    "c++": {
      "name": "gcc",
      "linker": "ld.bfd",
      "version": "13.2.1",
      "commands": "ccache, c++"
    },
    "fortran": {
      "name": "gcc",
      "linker": "ld.bfd",
      "version": "13.2.1",
      "commands": "gfortran"
    },
    "pythran": {
      "version": "0.14.0",
      "include directory": "../../../pip-build-env-120xme0q/overlay/lib/pypy3.10/site-packages/pythran"
    }
  },
  "Machine Information": {
    "host": {
      "cpu": "x86_64",
      "family": "x86_64",
      "endian": "little",
      "system": "linux"
    },
    "build": {
      "cpu": "x86_64",
      "family": "x86_64",
      "endian": "little",
      "system": "linux"
    },
    "cross-compiled": false
  },
  "Build Dependencies": {
    "blas": {
      "name": "openblas",
      "found": true,
      "version": "0.3.25",
      "detection method": "pkgconfig",
      "include directory": "/usr/include/openblas",
      "lib directory": "/usr/lib64",
      "openblas configuration": "USE_64BITINT= DYNAMIC_ARCH= DYNAMIC_OLDER= NO_CBLAS= NO_LAPACK= NO_LAPACKE= NO_AFFINITY=1 USE_OPENMP=1 ZEN MAX_THREADS=64",
      "pc file directory": "/usr/lib64/pkgconfig"
    },
    "lapack": {
      "name": "openblas",
      "found": true,
      "version": "0.3.25",
      "detection method": "pkgconfig",
      "include directory": "/usr/include/openblas",
      "lib directory": "/usr/lib64",
      "openblas configuration": "USE_64BITINT= DYNAMIC_ARCH= DYNAMIC_OLDER= NO_CBLAS= NO_LAPACK= NO_LAPACKE= NO_AFFINITY=1 USE_OPENMP=1 ZEN MAX_THREADS=64",
      "pc file directory": "/usr/lib64/pkgconfig"
    },
    "pybind11": {
      "name": "pybind11",
      "version": "2.11.1",
      "detection method": "pkgconfig",
      "include directory": "/usr/include"
    }
  },
  "Python Information": {
    "path": "/tmp/.venv/bin/pypy3.10",
    "version": "3.10"
  }
}
```



Discussion:

Resolving patch:
diff --git a/scipy/integrate/_quadrature.py b/scipy/integrate/_quadrature.py
--- a/scipy/integrate/_quadrature.py
+++ b/scipy/integrate/_quadrature.py
@@ -490,6 +490,8 @@ def cumulative_trapezoid(y, x=None, dx=1.0, axis=-1, initial=None):
 
     """
     y = np.asarray(y)
+    if y.shape[axis] == 0:
+        raise ValueError("At least one point is required along `axis`.")
     if x is None:
         d = dx
     else:
@@ -914,15 +916,19 @@ def cumulative_simpson(y, *, x=None, dx=1.0, axis=-1, initial=None):
     Parameters
     ----------
     y : array_like
-        Values to integrate.
+        Values to integrate. Requires at least one point along `axis`. If two or fewer
+        points are provided along `axis`, Simpson's integration is not possible and the
+        result is calculated with `cumulative_trapezoid`.
     x : array_like, optional
-        The coordinate to integrate along. Must be monotonically increasing.
-        If None (default), use spacing `dx` between consecutive elements in 
-        `y`.
+        The coordinate to integrate along. Must have the same shape as `y` or
+        must be 1D with the same length as `y` along `axis`. `x` must also be
+        strictly increasing along `axis`.
+        If `x` is None (default), integration is performed using spacing `dx`
+        between consecutive elements in `y`.
     dx : scalar or array_like, optional
         Spacing between elements of `y`. Only used if `x` is None. Can either 
-        be a float, or an array with the same shape as `y`, but length 1 along
-        `axis`.
+        be a float, or an array with the same shape as `y`, but of length one along
+        `axis`. Default is 1.0.
     axis : int, optional
         Specifies the axis to integrate along. Default is -1 (last axis).
     initial : scalar or array_like, optional
@@ -930,7 +936,7 @@ def cumulative_simpson(y, *, x=None, dx=1.0, axis=-1, initial=None):
         and add it to the rest of the result. Default is None, which means no
         value at ``x[0]`` is returned and `res` has one element less than `y`
         along the axis of integration. Can either be a float, or an array with
-        the same shape as `y`, but length 1 along `axis`.
+        the same shape as `y`, but of length one along `axis`.
 
     Returns
     -------
@@ -954,24 +960,22 @@ def cumulative_simpson(y, *, x=None, dx=1.0, axis=-1, initial=None):
 
     The composite Simpson's 1/3 method can be used to approximate the definite 
     integral of a sampled input function :math:`y(x)` [1]_. The method assumes 
-    a quadratic relationship over the interval containing any 3 consecutive 
+    a quadratic relationship over the interval containing any three consecutive
     sampled points.
 
     Consider three consecutive points: 
     :math:`(x_1, y_1), (x_2, y_2), (x_3, y_3)`.
-    where the widths of the 2 subintervals are:
-    :math:`h_1 = x_2 - x_1` and :math:`h_2 = x_3 - x_2`.
 
-    Assuming a quadratic relationship over the 3 points, the integral over 
+    Assuming a quadratic relationship over the three points, the integral over
     the subinterval between :math:`x_1` and :math:`x_2` is given by formula
     (8) of [2]_:
     
     .. math::
-        \int_{x_1}^{x_2} y(x) dx\ = \frac{x_2-x_1}{6}\left[\
+        \int_{x_1}^{x_2} y(x) dx\ &= \frac{x_2-x_1}{6}\left[\
         \left\{3-\frac{x_2-x_1}{x_3-x_1}\right\} y_1 + \
         \left\{3 + \frac{(x_2-x_1)^2}{(x_3-x_2)(x_3-x_1)} + \
-        \frac{x_2-x_1}{x_3-x_1}\right\} y_2 - \
-        \frac{(x_2-x_1)^2}{(x_3-x_2)(x_3-x_1)} y_3\right]
+        \frac{x_2-x_1}{x_3-x_1}\right\} y_2\\
+        - \frac{(x_2-x_1)^2}{(x_3-x_2)(x_3-x_1)} y_3\right]
 
     The integral between :math:`x_2` and :math:`x_3` is given by swapping
     appearances of :math:`x_1` and :math:`x_3`. The integral is estimated
@@ -979,8 +983,8 @@ def cumulative_simpson(y, *, x=None, dx=1.0, axis=-1, initial=None):
     the final result.
     
     For samples that are equally spaced, the result is exact if the function
-    is a polynomial of order 3 or less [1]_ and the number of subintervals
-    is even. Otherwise, the integral is exact for polynomials of  order 2 or
+    is a polynomial of order three or less [1]_ and the number of subintervals
+    is even. Otherwise, the integral is exact for polynomials of order two or
     less. 
 
     References
@@ -1029,20 +1033,18 @@ def cumulative_simpson(y, *, x=None, dx=1.0, axis=-1, initial=None):
     y = _ensure_float_array(y)
 
     # validate `axis` and standardize to work along the last axis
+    original_y = y
     original_shape = y.shape
     try:
         y = np.swapaxes(y, axis, -1)
     except IndexError as e:
         message = f"`axis={axis}` is not valid for `y` with `y.ndim={y.ndim}`."
         raise ValueError(message) from e
-
     if y.shape[-1] < 3:
-        raise ValueError(
-            "At least 3 points are required along the axis of integration "
-            "to use the composite Simpson's method."
-        )
+        res = cumulative_trapezoid(original_y, x, dx=dx, axis=axis, initial=None)
+        res = np.swapaxes(res, axis, -1)
 
-    if x is not None:
+    elif x is not None:
         x = _ensure_float_array(x)
         message = ("If given, shape of `x` must be the same as `y` or 1-D with "
                    "the same length as `y` along `axis`.")
@@ -1053,7 +1055,7 @@ def cumulative_simpson(y, *, x=None, dx=1.0, axis=-1, initial=None):
         x = np.broadcast_to(x, y.shape) if x.ndim == 1 else np.swapaxes(x, axis, -1)
         dx = np.diff(x, axis=-1)
         if np.any(dx <= 0):
-            raise ValueError("Input x must be monotonically increasing.")
+            raise ValueError("Input x must be strictly increasing.")
         res = _cumulatively_sum_simpson_integrals(
             y, dx, _cumulative_simpson_unequal_intervals
         )
diff --git a/scipy/optimize/_optimize.py b/scipy/optimize/_optimize.py
--- a/scipy/optimize/_optimize.py
+++ b/scipy/optimize/_optimize.py
@@ -38,7 +38,6 @@
                           line_search_wolfe2 as line_search,
                           LineSearchWarning)
 from ._numdiff import approx_derivative
-from ._hessian_update_strategy import HessianUpdateStrategy
 from scipy._lib._util import getfullargspec_no_self as _getfullargspec
 from scipy._lib._util import MapWrapper, check_random_state
 from scipy.optimize._differentiable_functions import ScalarFunction, FD_METHODS
@@ -2179,11 +2178,9 @@ def terminate(warnflag, msg):
                     Ap = fhess_p(xk, psupi, *args)
                     hcalls += 1
             else:
-                if isinstance(A, HessianUpdateStrategy):
-                    # if hess was supplied as a HessianUpdateStrategy
-                    Ap = A.dot(psupi)
-                else:
-                    Ap = np.dot(A, psupi)
+                # hess was supplied as a callable or hessian update strategy, so
+                # A is a dense numpy array or sparse matrix
+                Ap = A.dot(psupi)
             # check curvature
             Ap = asarray(Ap).squeeze()  # get rid of matrices...
             curv = np.dot(psupi, Ap)
diff --git a/scipy/stats/_discrete_distns.py b/scipy/stats/_discrete_distns.py
--- a/scipy/stats/_discrete_distns.py
+++ b/scipy/stats/_discrete_distns.py
@@ -336,9 +336,9 @@ def _cdf(self, x, n, p):
 
     def _logcdf(self, x, n, p):
         k = floor(x)
+        k, n, p = np.broadcast_arrays(k, n, p)
         cdf = self._cdf(k, n, p)
         cond = cdf > 0.5
-
         def f1(k, n, p):
             return np.log1p(-special.betainc(k + 1, n, 1 - p))
 


Test patch:
diff --git a/scipy/fft/_pocketfft/tests/test_real_transforms.py b/scipy/fft/_pocketfft/tests/test_real_transforms.py
--- a/scipy/fft/_pocketfft/tests/test_real_transforms.py
+++ b/scipy/fft/_pocketfft/tests/test_real_transforms.py
@@ -25,11 +25,8 @@ def is_longdouble_binary_compatible():
         return False
 
 
-def get_reference_data():
-    ref = getattr(globals(), '__reference_data', None)
-    if ref is not None:
-        return ref
-
+@pytest.fixture(scope="module")
+def reference_data():
     # Matlab reference data
     MDATA = np.load(join(fftpack_test_dir, 'test.npz'))
     X = [MDATA['x%d' % i] for i in range(MDATA_COUNT)]
@@ -61,51 +58,55 @@ def get_reference_data():
         'Y': Y
     }
 
-    globals()['__reference_data'] = ref
-    return ref
+    yield ref
+
+    if is_longdouble_binary_compatible():
+        FFTWDATA_LONGDOUBLE.close()
+    FFTWDATA_SINGLE.close()
+    FFTWDATA_DOUBLE.close()
+    MDATA.close()
 
 
 @pytest.fixture(params=range(FFTWDATA_COUNT))
-def fftwdata_size(request):
-    return get_reference_data()['FFTWDATA_SIZES'][request.param]
+def fftwdata_size(request, reference_data):
+    return reference_data['FFTWDATA_SIZES'][request.param]
 
 @pytest.fixture(params=range(MDATA_COUNT))
-def mdata_x(request):
-    return get_reference_data()['X'][request.param]
+def mdata_x(request, reference_data):
+    return reference_data['X'][request.param]
 
 
 @pytest.fixture(params=range(MDATA_COUNT))
-def mdata_xy(request):
-    ref = get_reference_data()
-    y = ref['Y'][request.param]
-    x = ref['X'][request.param]
+def mdata_xy(request, reference_data):
+    y = reference_data['Y'][request.param]
+    x = reference_data['X'][request.param]
     return x, y
 
 
-def fftw_dct_ref(type, size, dt):
+def fftw_dct_ref(type, size, dt, reference_data):
     x = np.linspace(0, size-1, size).astype(dt)
     dt = np.result_type(np.float32, dt)
     if dt == np.float64:
-        data = get_reference_data()['FFTWDATA_DOUBLE']
+        data = reference_data['FFTWDATA_DOUBLE']
     elif dt == np.float32:
-        data = get_reference_data()['FFTWDATA_SINGLE']
+        data = reference_data['FFTWDATA_SINGLE']
     elif dt == np.longdouble:
-        data = get_reference_data()['FFTWDATA_LONGDOUBLE']
+        data = reference_data['FFTWDATA_LONGDOUBLE']
     else:
         raise ValueError()
     y = (data['dct_%d_%d' % (type, size)]).astype(dt)
     return x, y, dt
 
 
-def fftw_dst_ref(type, size, dt):
+def fftw_dst_ref(type, size, dt, reference_data):
     x = np.linspace(0, size-1, size).astype(dt)
     dt = np.result_type(np.float32, dt)
     if dt == np.float64:
-        data = get_reference_data()['FFTWDATA_DOUBLE']
+        data = reference_data['FFTWDATA_DOUBLE']
     elif dt == np.float32:
-        data = get_reference_data()['FFTWDATA_SINGLE']
+        data = reference_data['FFTWDATA_SINGLE']
     elif dt == np.longdouble:
-        data = get_reference_data()['FFTWDATA_LONGDOUBLE']
+        data = reference_data['FFTWDATA_LONGDOUBLE']
     else:
         raise ValueError()
     y = (data['dst_%d_%d' % (type, size)]).astype(dt)
@@ -265,8 +266,8 @@ def test_complex(transform, dtype):
 @pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
 @pytest.mark.parametrize('type', [1, 2, 3, 4])
 class TestDCT:
-    def test_definition(self, rdt, type, fftwdata_size):
-        x, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt)
+    def test_definition(self, rdt, type, fftwdata_size, reference_data):
+        x, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt, reference_data)
         y = dct(x, type=type)
         assert_equal(y.dtype, dt)
         dec = dec_map[(dct, rdt, type)]
@@ -340,8 +341,8 @@ def test_dct4_definition_ortho(mdata_x, rdt):
 
 @pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
 @pytest.mark.parametrize('type', [1, 2, 3, 4])
-def test_idct_definition(fftwdata_size, rdt, type):
-    xr, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt)
+def test_idct_definition(fftwdata_size, rdt, type, reference_data):
+    xr, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt, reference_data)
     x = idct(yr, type=type)
     dec = dec_map[(idct, rdt, type)]
     assert_equal(x.dtype, dt)
@@ -350,8 +351,8 @@ def test_idct_definition(fftwdata_size, rdt, type):
 
 @pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
 @pytest.mark.parametrize('type', [1, 2, 3, 4])
-def test_definition(fftwdata_size, rdt, type):
-    xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt)
+def test_definition(fftwdata_size, rdt, type, reference_data):
+    xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt, reference_data)
     y = dst(xr, type=type)
     dec = dec_map[(dst, rdt, type)]
     assert_equal(y.dtype, dt)
@@ -384,8 +385,8 @@ def test_dst4_definition_ortho(rdt, mdata_x):
 
 @pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
 @pytest.mark.parametrize('type', [1, 2, 3, 4])
-def test_idst_definition(fftwdata_size, rdt, type):
-    xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt)
+def test_idst_definition(fftwdata_size, rdt, type, reference_data):
+    xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt, reference_data)
     x = idst(yr, type=type)
     dec = dec_map[(idst, rdt, type)]
     assert_equal(x.dtype, dt)
diff --git a/scipy/integrate/tests/test_quadrature.py b/scipy/integrate/tests/test_quadrature.py
--- a/scipy/integrate/tests/test_quadrature.py
+++ b/scipy/integrate/tests/test_quadrature.py
@@ -362,6 +362,10 @@ def test_initial_warning(self, initial):
             res = cumulative_trapezoid(y, initial=initial)
         assert_allclose(res, [initial, *np.cumsum(y[1:] + y[:-1])/2]) 
 
+    def test_zero_len_y(self):
+        with pytest.raises(ValueError, match="At least one point is required"):
+            cumulative_trapezoid(y=[])
+
     def test_cumtrapz(self):
         # Basic coverage test for the alias
         x = np.arange(3 * 2 * 4).reshape(3, 2, 4)
@@ -537,6 +541,13 @@ def func(x):
 
 
 def cumulative_simpson_nd_reference(y, *, x=None, dx=None, initial=None, axis=-1):
+    # Use cumulative_trapezoid if length of y < 3
+    if y.shape[axis] < 3:
+        if initial is None:
+            return cumulative_trapezoid(y, x=x, dx=dx, axis=axis, initial=None)
+        else:
+            return initial + cumulative_trapezoid(y, x=x, dx=dx, axis=axis, initial=0)
+
     # Ensure that working axis is last axis
     y = np.moveaxis(y, axis, -1)
     x = np.moveaxis(x, axis, -1) if np.ndim(x) > 1 else x
@@ -607,14 +618,16 @@ def test_1d(self, use_dx, use_initial):
 
     @pytest.mark.parametrize('axis', np.arange(-3, 3))
     @pytest.mark.parametrize('x_ndim', (1, 3))
+    @pytest.mark.parametrize('x_len', (1, 2, 7))
     @pytest.mark.parametrize('i_ndim', (None, 0, 3,))
     @pytest.mark.parametrize('dx', (None, True))
-    def test_nd(self, axis, x_ndim, i_ndim, dx):
+    def test_nd(self, axis, x_ndim, x_len, i_ndim, dx):
         # Test behavior of `cumulative_simpson` with N-D `y`
         rng = np.random.default_rng(82456839535679456794)
 
         # determine shapes
-        shape = [5, 6, 7]
+        shape = [5, 6, x_len]
+        shape[axis], shape[-1] = shape[-1], shape[axis]
         shape_len_1 = shape.copy()
         shape_len_1[axis] = 1
         i_shape = shape_len_1 if i_ndim == 3 else ()
@@ -635,10 +648,10 @@ def test_nd(self, axis, x_ndim, i_ndim, dx):
         np.testing.assert_allclose(res, ref, rtol=1e-15)
 
     @pytest.mark.parametrize(('message', 'kwarg_update'), [
-        ("x must be monotonically increasing", dict(x=[2, 2, 3, 4])),
-        ("x must be monotonically", dict(x=[x0, [2, 2, 4, 8]], y=[y0, y0])),
-        ("x must be monotonically", dict(x=[x0, x0, x0], y=[y0, y0, y0], axis=0)),
-        ("At least 3 points are required", dict(x=x0[:2], y=y0[:2])),
+        ("x must be strictly increasing", dict(x=[2, 2, 3, 4])),
+        ("x must be strictly increasing", dict(x=[x0, [2, 2, 4, 8]], y=[y0, y0])),
+        ("x must be strictly increasing", dict(x=[x0, x0, x0], y=[y0, y0, y0], axis=0)),
+        ("At least one point is required", dict(x=[], y=[])),
         ("`axis=4` is not valid for `y` with `y.ndim=1`", dict(axis=4)),
         ("shape of `x` must be the same as `y` or 1-D", dict(x=np.arange(5))),
         ("`initial` must either be a scalar or...", dict(initial=np.arange(5))),
@@ -658,7 +671,6 @@ def test_special_cases(self):
 
         # Should add tests of:
         # - all elements of `x` identical
-        # - y of size 1 and 2
         # These should work as they do for `simpson`
 
     def _get_theoretical_diff_between_simps_and_cum_simps(self, y, x):
diff --git a/scipy/interpolate/tests/test_rgi.py b/scipy/interpolate/tests/test_rgi.py
--- a/scipy/interpolate/tests/test_rgi.py
+++ b/scipy/interpolate/tests/test_rgi.py
@@ -850,6 +850,9 @@ def test_non_scalar_values_splinef2d(self):
 
     @parametrize_rgi_interp_methods
     def test_complex(self, method):
+        if method == "pchip":
+            pytest.skip("pchip does not make sense for complex data")
+
         x, y, values = self._sample_2d_data()
         points = (x, y)
         values = values - 2j*values
diff --git a/scipy/optimize/tests/test__differential_evolution.py b/scipy/optimize/tests/test__differential_evolution.py
--- a/scipy/optimize/tests/test__differential_evolution.py
+++ b/scipy/optimize/tests/test__differential_evolution.py
@@ -1013,9 +1013,9 @@ def f(x):
         x_opt = (1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1)
         f_opt = -15
 
-        assert_allclose(f(x_opt), f_opt)
+        assert_allclose(f(x_opt), f_opt, atol=6e-4)
         assert res.success
-        assert_allclose(res.x, x_opt, atol=5e-4)
+        assert_allclose(res.x, x_opt, atol=6e-4)
         assert_allclose(res.fun, f_opt, atol=5e-3)
         assert_(np.all(A@res.x <= b))
         assert_(np.all(res.x >= np.array(bounds)[:, 0]))
@@ -1063,7 +1063,7 @@ def c2(x):
                                          seed=1234, constraints=constraints,
                                          popsize=2)
 
-        assert_allclose(res.x, x_opt, atol=5e-4)
+        assert_allclose(res.x, x_opt, atol=6e-4)
         assert_allclose(res.fun, f_opt, atol=5e-3)
         assert_(np.all(A@res.x <= b))
         assert_(np.all(res.x >= np.array(bounds)[:, 0]))
diff --git a/scipy/optimize/tests/test_optimize.py b/scipy/optimize/tests/test_optimize.py
--- a/scipy/optimize/tests/test_optimize.py
+++ b/scipy/optimize/tests/test_optimize.py
@@ -30,7 +30,10 @@
 from scipy.optimize._qap import QUADRATIC_ASSIGNMENT_METHODS
 from scipy.optimize._differentiable_functions import ScalarFunction, FD_METHODS
 from scipy.optimize._optimize import MemoizeJac, show_options, OptimizeResult
+from scipy.optimize import rosen, rosen_der, rosen_hess
 
+from scipy.sparse import (coo_matrix, csc_matrix, csr_matrix, coo_array,
+                          csr_array, csc_array)
 
 def test_check_grad():
     # Verify if check_grad is able to estimate the derivative of the
@@ -2560,8 +2563,8 @@ def test_runtime_warning(self):
         def func(z, *params):
             return rng.random(1) * 1000  # never converged problem
 
-        with pytest.warns(RuntimeWarning,
-                          match=r'Either final optimization did not succeed'):
+        msg = "final optimization did not succeed.*|Maximum number of function eval.*"
+        with pytest.warns(RuntimeWarning, match=msg):
             optimize.brute(func, self.rranges, args=self.params, disp=True)
 
     def test_coerce_args_param(self):
@@ -3125,3 +3128,29 @@ def f(x):
 
     assert_allclose(res.fun, ref.fun)
     assert_allclose(res.x, ref.x)
+
+
+@pytest.mark.parametrize('method', ['Newton-CG', 'trust-constr'])
+@pytest.mark.parametrize('sparse_type', [coo_matrix, csc_matrix, csr_matrix,
+                                         coo_array, csr_array, csc_array])
+def test_sparse_hessian(method, sparse_type):
+    # gh-8792 reported an error for minimization with `newton_cg` when `hess`
+    # returns a sparse matrix. Check that results are the same whether `hess`
+    # returns a dense or sparse matrix for optimization methods that accept
+    # sparse Hessian matrices.
+
+    def sparse_rosen_hess(x):
+        return sparse_type(rosen_hess(x))
+
+    x0 = [2., 2.]
+
+    res_sparse = optimize.minimize(rosen, x0, method=method,
+                                   jac=rosen_der, hess=sparse_rosen_hess)
+    res_dense = optimize.minimize(rosen, x0, method=method,
+                                  jac=rosen_der, hess=rosen_hess)
+
+    assert_allclose(res_dense.fun, res_sparse.fun)
+    assert_allclose(res_dense.x, res_sparse.x)
+    assert res_dense.nfev == res_sparse.nfev
+    assert res_dense.njev == res_sparse.njev
+    assert res_dense.nhev == res_sparse.nhev
diff --git a/scipy/signal/tests/test_filter_design.py b/scipy/signal/tests/test_filter_design.py
--- a/scipy/signal/tests/test_filter_design.py
+++ b/scipy/signal/tests/test_filter_design.py
@@ -1563,7 +1563,8 @@ def test_invalid_input(self):
         assert "gstop should be larger than 0.0" in str(exc_info.value)
 
     def test_runtime_warnings(self):
-        with pytest.warns(RuntimeWarning, match=r'Order is zero'):
+        msg = "Order is zero.*|divide by zero encountered in divide"
+        with pytest.warns(RuntimeWarning, match=msg):
             buttord(0.0, 1.0, 3, 60)
 
     def test_ellip_butter(self):
diff --git a/scipy/signal/tests/test_signaltools.py b/scipy/signal/tests/test_signaltools.py
--- a/scipy/signal/tests/test_signaltools.py
+++ b/scipy/signal/tests/test_signaltools.py
@@ -805,7 +805,8 @@ def test_fft_nan(self):
             sig_nan[100] = val
             coeffs = signal.firwin(200, 0.2)
 
-            with pytest.warns(RuntimeWarning, match="Use of fft convolution"):
+            msg = "Use of fft convolution.*|invalid value encountered.*"
+            with pytest.warns(RuntimeWarning, match=msg):
                 signal.convolve(sig_nan, coeffs, mode='same', method='fft')
 
 def fftconvolve_err(*args, **kwargs):
@@ -1114,7 +1115,8 @@ def test_invalid_dtypes(self, dtype):
 
     def test_none(self):
         # gh-1651, trac #1124. Ensure this does not segfault.
-        with pytest.warns(UserWarning):
+        msg = "kernel_size exceeds volume.*|Using medfilt with arrays of dtype.*"
+        with pytest.warns((UserWarning, DeprecationWarning), match=msg):
             assert_raises(TypeError, signal.medfilt, None)
         # Expand on this test to avoid a regression with possible contiguous
         # numpy arrays that have odd strides. The stride value below gets
@@ -1133,7 +1135,8 @@ def test_refcounting(self):
         else:
             n = 10
         # Shouldn't segfault:
-        with pytest.warns(UserWarning):
+        msg = "kernel_size exceeds volume.*|Using medfilt with arrays of dtype.*"
+        with pytest.warns((UserWarning, DeprecationWarning), match=msg):
             for j in range(n):
                 signal.medfilt(x)
         if hasattr(sys, 'getrefcount'):
diff --git a/scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py b/scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py
--- a/scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py
+++ b/scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py
@@ -90,7 +90,8 @@ def test_Small():
 
 def test_ElasticRod():
     A, B = ElasticRod(20)
-    with pytest.warns(UserWarning, match="Exited at iteration"):
+    msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
+    with pytest.warns(UserWarning, match=msg):
         compare_solutions(A, B, 2)
 
 
@@ -355,7 +356,8 @@ def test_failure_to_run_iterations_nonsymmetric():
     A = np.zeros((10, 10))
     A[0, 1] = 1
     Q = np.ones((10, 1))
-    with pytest.warns(UserWarning, match="Exited at iteration 2"):
+    msg = "Exited at iteration 2|Exited postprocessing with accuracies.*"
+    with pytest.warns(UserWarning, match=msg):
         eigenvalues, _ = lobpcg(A, Q, maxiter=20)
     assert np.max(eigenvalues) > 0
 
@@ -430,7 +432,8 @@ def test_verbosity():
     X = rnd.standard_normal((10, 10))
     A = X @ X.T
     Q = rnd.standard_normal((X.shape[0], 1))
-    with pytest.warns(UserWarning, match="Exited at iteration"):
+    msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
+    with pytest.warns(UserWarning, match=msg):
         _, _ = lobpcg(A, Q, maxiter=3, verbosityLevel=9)
 
 
@@ -506,14 +509,15 @@ def test_maxit():
     A = A.astype(np.float32)
     X = rnd.standard_normal((n, m))
     X = X.astype(np.float64)
+    msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
     for maxiter in range(1, 4):
-        with pytest.warns(UserWarning, match="Exited at iteration"):
+        with pytest.warns(UserWarning, match=msg):
             _, _, l_h, r_h = lobpcg(A, X, tol=1e-8, maxiter=maxiter,
                                     retLambdaHistory=True,
                                     retResidualNormsHistory=True)
         assert_allclose(np.shape(l_h)[0], maxiter+3)
         assert_allclose(np.shape(r_h)[0], maxiter+3)
-    with pytest.warns(UserWarning, match="Exited at iteration"):
+    with pytest.warns(UserWarning, match=msg):
         l, _, l_h, r_h = lobpcg(A, X, tol=1e-8,
                                 retLambdaHistory=True,
                                 retResidualNormsHistory=True)
diff --git a/scipy/sparse/linalg/_isolve/tests/test_iterative.py b/scipy/sparse/linalg/_isolve/tests/test_iterative.py
--- a/scipy/sparse/linalg/_isolve/tests/test_iterative.py
+++ b/scipy/sparse/linalg/_isolve/tests/test_iterative.py
@@ -551,7 +551,11 @@ def test_positional_deprecation(solver):
     A = A @ A.T
     b = rng.random(n)
     x0 = rng.random(n)
-    with pytest.deprecated_call(match="use keyword arguments"):
+    with pytest.deprecated_call(
+        # due to the use of the _deprecate_positional_args decorator, it's not possible
+        # to separate the two warnings (1 for positional use, 1 for `tol` deprecation).
+        match="use keyword arguments.*|argument `tol` is deprecated.*"
+    ):
         solver(A, b, x0, 1e-5)
 
 
diff --git a/scipy/stats/tests/test_discrete_distns.py b/scipy/stats/tests/test_discrete_distns.py
--- a/scipy/stats/tests/test_discrete_distns.py
+++ b/scipy/stats/tests/test_discrete_distns.py
@@ -237,6 +237,13 @@ def test_issue_6682():
     assert_allclose(nbinom.sf(250, 50, 32./63.), 1.460458510976452e-35)
 
 
+def test_issue_19747():
+    # test that negative k does not raise an error in nbinom.logcdf
+    result = nbinom.logcdf([5, -1, 1], 5, 0.5)
+    reference = [-0.47313352, -np.inf, -2.21297293]
+    assert_allclose(result, reference)
+
+
 def test_boost_divide_by_zero_issue_15101():
     n = 1000
     p = 0.01
diff --git a/scipy/stats/tests/test_distributions.py b/scipy/stats/tests/test_distributions.py
--- a/scipy/stats/tests/test_distributions.py
+++ b/scipy/stats/tests/test_distributions.py
@@ -4130,9 +4130,9 @@ def test_cdf_bounds(self):
                               (0.125, 0.9508674287164001, 0.25, 5, 0.5)])
     def test_sf_isf(self, x, p, a, b, c):
         sf = stats.genexpon.sf(x, a, b, c)
-        assert_allclose(sf, p, rtol=1e-14)
+        assert_allclose(sf, p, rtol=2e-14)
         isf = stats.genexpon.isf(p, a, b, c)
-        assert_allclose(isf, x, rtol=1e-14)
+        assert_allclose(isf, x, rtol=2e-14)
 
     # The values of p in the following data were computed with mpmath.
     @pytest.mark.parametrize('x, p, a, b, c',
@@ -4143,9 +4143,9 @@ def test_sf_isf(self, x, p, a, b, c):
                               (0.125, 0.04913257128359998, 0.25, 5, 0.5)])
     def test_cdf_ppf(self, x, p, a, b, c):
         cdf = stats.genexpon.cdf(x, a, b, c)
-        assert_allclose(cdf, p, rtol=1e-14)
+        assert_allclose(cdf, p, rtol=2e-14)
         ppf = stats.genexpon.ppf(p, a, b, c)
-        assert_allclose(ppf, x, rtol=1e-14)
+        assert_allclose(ppf, x, rtol=2e-14)
 
 
 class TestTruncexpon:
diff --git a/scipy/stats/tests/test_fit.py b/scipy/stats/tests/test_fit.py
--- a/scipy/stats/tests/test_fit.py
+++ b/scipy/stats/tests/test_fit.py
@@ -461,7 +461,7 @@ def test_guess_iv(self):
         with pytest.raises(ValueError, match=message):
             stats.fit(self.dist, self.data, self.shape_bounds_d, guess=guess)
 
-        message = "Guess for parameter `n` rounded..."
+        message = "Guess for parameter `n` rounded.*|Guess for parameter `p` clipped.*"
         guess = {'n': 4.5, 'p': -0.5}
         with pytest.warns(RuntimeWarning, match=message):
             stats.fit(self.dist, self.data, self.shape_bounds_d, guess=guess)
diff --git a/scipy/stats/tests/test_mstats_basic.py b/scipy/stats/tests/test_mstats_basic.py
--- a/scipy/stats/tests/test_mstats_basic.py
+++ b/scipy/stats/tests/test_mstats_basic.py
@@ -972,7 +972,8 @@ def test_theilslopes():
 
 def test_theilslopes_warnings():
     # Test `theilslopes` with degenerate input; see gh-15943
-    with pytest.warns(RuntimeWarning, match="All `x` coordinates are..."):
+    msg = "All `x` coordinates.*|Mean of empty slice.|invalid value encountered.*"
+    with pytest.warns(RuntimeWarning, match=msg):
         res = mstats.theilslopes([0, 1], [0, 0])
         assert np.all(np.isnan(res))
     with suppress_warnings() as sup:
diff --git a/scipy/stats/tests/test_rank.py b/scipy/stats/tests/test_rank.py
--- a/scipy/stats/tests/test_rank.py
+++ b/scipy/stats/tests/test_rank.py
@@ -203,7 +203,7 @@ def test_size_0_axis(self, axis, method, dtype):
     @pytest.mark.parametrize('method', methods)
     def test_nan_policy_omit_3d(self, axis, method):
         shape = (20, 21, 22)
-        rng = np.random.default_rng(abs(hash('falafel')))
+        rng = np.random.default_rng(602173110498916506)
 
         a = rng.random(size=shape)
         i = rng.random(size=shape) < 0.4
diff --git a/scipy/stats/tests/test_stats.py b/scipy/stats/tests/test_stats.py
--- a/scipy/stats/tests/test_stats.py
+++ b/scipy/stats/tests/test_stats.py
@@ -1419,9 +1419,10 @@ def test_kendalltau_nan_2nd_arg():
 
 
 def test_kendalltau_deprecations():
-    with pytest.deprecated_call(match="keyword argument 'initial_lexsort"):
+    msg_dep = "keyword argument 'initial_lexsort'"
+    with pytest.deprecated_call(match=msg_dep):
         stats.kendalltau([], [], initial_lexsort=True)
-    with pytest.deprecated_call(match="use keyword arguments"):
+    with pytest.deprecated_call(match=f"use keyword arguments|{msg_dep}"):
         stats.kendalltau([], [], True)
 
 
@@ -3136,7 +3137,7 @@ def test_moment(self):
         assert_allclose(y, [0, 1.25, 0, 2.5625])
 
         # test empty input
-        message = "Mean of empty slice."
+        message = r"Mean of empty slice\.|invalid value encountered.*"
         with pytest.warns(RuntimeWarning, match=message):
             y = stats.moment([])
             self._assert_equal(y, np.nan, dtype=np.float64)
@@ -3310,7 +3311,7 @@ def test_precision_loss_gh15554(self):
             stats.skew(a)[0]
 
     def test_empty_1d(self):
-        message = "Mean of empty slice."
+        message = r"Mean of empty slice\.|invalid value encountered.*"
         with pytest.warns(RuntimeWarning, match=message):
             stats.skew([])
         with pytest.warns(RuntimeWarning, match=message):
@@ -7888,7 +7889,8 @@ def test_brunnermunzel_return_nan(self):
         x = [1, 2, 3]
         y = [5, 6, 7, 8, 9]
 
-        with pytest.warns(RuntimeWarning, match='p-value cannot be estimated'):
+        msg = "p-value cannot be estimated|divide by zero|invalid value encountered"
+        with pytest.warns(RuntimeWarning, match=msg):
             stats.brunnermunzel(x, y, distribution="t")
 
     def test_brunnermunzel_normal_dist(self):