Coverage for /usr/lib/python3/dist-packages/scipy/_lib/_util.py: 19%
258 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1import re
2from contextlib import contextmanager
3import functools
4import operator
5import warnings
6import numbers
7from collections import namedtuple
8import inspect
9import math
10from typing import (
11 Optional,
12 Union,
13 TYPE_CHECKING,
14 TypeVar,
15)
17import numpy as np
19IntNumber = Union[int, np.integer]
20DecimalNumber = Union[float, np.floating, np.integer]
22# Since Generator was introduced in numpy 1.17, the following condition is needed for
23# backward compatibility
24if TYPE_CHECKING:
25 SeedType = Optional[Union[IntNumber, np.random.Generator,
26 np.random.RandomState]]
27 GeneratorType = TypeVar("GeneratorType", bound=Union[np.random.Generator,
28 np.random.RandomState])
30try:
31 from numpy.random import Generator as Generator
32except ImportError:
33 class Generator(): # type: ignore[no-redef]
34 pass
37def _lazywhere(cond, arrays, f, fillvalue=None, f2=None):
38 """
39 np.where(cond, x, fillvalue) always evaluates x even where cond is False.
40 This one only evaluates f(arr1[cond], arr2[cond], ...).
42 Examples
43 --------
44 >>> import numpy as np
45 >>> a, b = np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8])
46 >>> def f(a, b):
47 ... return a*b
48 >>> _lazywhere(a > 2, (a, b), f, np.nan)
49 array([ nan, nan, 21., 32.])
51 Notice, it assumes that all `arrays` are of the same shape, or can be
52 broadcasted together.
54 """
55 cond = np.asarray(cond)
56 if fillvalue is None:
57 if f2 is None:
58 raise ValueError("One of (fillvalue, f2) must be given.")
59 else:
60 fillvalue = np.nan
61 else:
62 if f2 is not None:
63 raise ValueError("Only one of (fillvalue, f2) can be given.")
65 args = np.broadcast_arrays(cond, *arrays)
66 cond, arrays = args[0], args[1:]
67 temp = tuple(np.extract(cond, arr) for arr in arrays)
68 tcode = np.mintypecode([a.dtype.char for a in arrays])
69 out = np.full(np.shape(arrays[0]), fill_value=fillvalue, dtype=tcode)
70 np.place(out, cond, f(*temp))
71 if f2 is not None:
72 temp = tuple(np.extract(~cond, arr) for arr in arrays)
73 np.place(out, ~cond, f2(*temp))
75 return out
78def _lazyselect(condlist, choicelist, arrays, default=0):
79 """
80 Mimic `np.select(condlist, choicelist)`.
82 Notice, it assumes that all `arrays` are of the same shape or can be
83 broadcasted together.
85 All functions in `choicelist` must accept array arguments in the order
86 given in `arrays` and must return an array of the same shape as broadcasted
87 `arrays`.
89 Examples
90 --------
91 >>> import numpy as np
92 >>> x = np.arange(6)
93 >>> np.select([x <3, x > 3], [x**2, x**3], default=0)
94 array([ 0, 1, 4, 0, 64, 125])
96 >>> _lazyselect([x < 3, x > 3], [lambda x: x**2, lambda x: x**3], (x,))
97 array([ 0., 1., 4., 0., 64., 125.])
99 >>> a = -np.ones_like(x)
100 >>> _lazyselect([x < 3, x > 3],
101 ... [lambda x, a: x**2, lambda x, a: a * x**3],
102 ... (x, a), default=np.nan)
103 array([ 0., 1., 4., nan, -64., -125.])
105 """
106 arrays = np.broadcast_arrays(*arrays)
107 tcode = np.mintypecode([a.dtype.char for a in arrays])
108 out = np.full(np.shape(arrays[0]), fill_value=default, dtype=tcode)
109 for func, cond in zip(choicelist, condlist):
110 if np.all(cond is False):
111 continue
112 cond, _ = np.broadcast_arrays(cond, arrays[0])
113 temp = tuple(np.extract(cond, arr) for arr in arrays)
114 np.place(out, cond, func(*temp))
115 return out
118def _aligned_zeros(shape, dtype=float, order="C", align=None):
119 """Allocate a new ndarray with aligned memory.
121 Primary use case for this currently is working around a f2py issue
122 in NumPy 1.9.1, where dtype.alignment is such that np.zeros() does
123 not necessarily create arrays aligned up to it.
125 """
126 dtype = np.dtype(dtype)
127 if align is None:
128 align = dtype.alignment
129 if not hasattr(shape, '__len__'):
130 shape = (shape,)
131 size = functools.reduce(operator.mul, shape) * dtype.itemsize
132 buf = np.empty(size + align + 1, np.uint8)
133 offset = buf.__array_interface__['data'][0] % align
134 if offset != 0:
135 offset = align - offset
136 # Note: slices producing 0-size arrays do not necessarily change
137 # data pointer --- so we use and allocate size+1
138 buf = buf[offset:offset+size+1][:-1]
139 data = np.ndarray(shape, dtype, buf, order=order)
140 data.fill(0)
141 return data
144def _prune_array(array):
145 """Return an array equivalent to the input array. If the input
146 array is a view of a much larger array, copy its contents to a
147 newly allocated array. Otherwise, return the input unchanged.
148 """
149 if array.base is not None and array.size < array.base.size // 2:
150 return array.copy()
151 return array
154def float_factorial(n: int) -> float:
155 """Compute the factorial and return as a float
157 Returns infinity when result is too large for a double
158 """
159 return float(math.factorial(n)) if n < 171 else np.inf
162# copy-pasted from scikit-learn utils/validation.py
163# change this to scipy.stats._qmc.check_random_state once numpy 1.16 is dropped
164def check_random_state(seed):
165 """Turn `seed` into a `np.random.RandomState` instance.
167 Parameters
168 ----------
169 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
170 If `seed` is None (or `np.random`), the `numpy.random.RandomState`
171 singleton is used.
172 If `seed` is an int, a new ``RandomState`` instance is used,
173 seeded with `seed`.
174 If `seed` is already a ``Generator`` or ``RandomState`` instance then
175 that instance is used.
177 Returns
178 -------
179 seed : {`numpy.random.Generator`, `numpy.random.RandomState`}
180 Random number generator.
182 """
183 if seed is None or seed is np.random:
184 return np.random.mtrand._rand
185 if isinstance(seed, (numbers.Integral, np.integer)):
186 return np.random.RandomState(seed)
187 if isinstance(seed, (np.random.RandomState, np.random.Generator)):
188 return seed
190 raise ValueError('%r cannot be used to seed a numpy.random.RandomState'
191 ' instance' % seed)
194def _asarray_validated(a, check_finite=True,
195 sparse_ok=False, objects_ok=False, mask_ok=False,
196 as_inexact=False):
197 """
198 Helper function for SciPy argument validation.
200 Many SciPy linear algebra functions do support arbitrary array-like
201 input arguments. Examples of commonly unsupported inputs include
202 matrices containing inf/nan, sparse matrix representations, and
203 matrices with complicated elements.
205 Parameters
206 ----------
207 a : array_like
208 The array-like input.
209 check_finite : bool, optional
210 Whether to check that the input matrices contain only finite numbers.
211 Disabling may give a performance gain, but may result in problems
212 (crashes, non-termination) if the inputs do contain infinities or NaNs.
213 Default: True
214 sparse_ok : bool, optional
215 True if scipy sparse matrices are allowed.
216 objects_ok : bool, optional
217 True if arrays with dype('O') are allowed.
218 mask_ok : bool, optional
219 True if masked arrays are allowed.
220 as_inexact : bool, optional
221 True to convert the input array to a np.inexact dtype.
223 Returns
224 -------
225 ret : ndarray
226 The converted validated array.
228 """
229 if not sparse_ok:
230 import scipy.sparse
231 if scipy.sparse.issparse(a):
232 msg = ('Sparse matrices are not supported by this function. '
233 'Perhaps one of the scipy.sparse.linalg functions '
234 'would work instead.')
235 raise ValueError(msg)
236 if not mask_ok:
237 if np.ma.isMaskedArray(a):
238 raise ValueError('masked arrays are not supported')
239 toarray = np.asarray_chkfinite if check_finite else np.asarray
240 a = toarray(a)
241 if not objects_ok:
242 if a.dtype is np.dtype('O'):
243 raise ValueError('object arrays are not supported')
244 if as_inexact:
245 if not np.issubdtype(a.dtype, np.inexact):
246 a = toarray(a, dtype=np.float_)
247 return a
250def _validate_int(k, name, minimum=None):
251 """
252 Validate a scalar integer.
254 This functon can be used to validate an argument to a function
255 that expects the value to be an integer. It uses `operator.index`
256 to validate the value (so, for example, k=2.0 results in a
257 TypeError).
259 Parameters
260 ----------
261 k : int
262 The value to be validated.
263 name : str
264 The name of the parameter.
265 minimum : int, optional
266 An optional lower bound.
267 """
268 try:
269 k = operator.index(k)
270 except TypeError:
271 raise TypeError(f'{name} must be an integer.') from None
272 if minimum is not None and k < minimum:
273 raise ValueError(f'{name} must be an integer not less '
274 f'than {minimum}') from None
275 return k
278# Add a replacement for inspect.getfullargspec()/
279# The version below is borrowed from Django,
280# https://github.com/django/django/pull/4846.
282# Note an inconsistency between inspect.getfullargspec(func) and
283# inspect.signature(func). If `func` is a bound method, the latter does *not*
284# list `self` as a first argument, while the former *does*.
285# Hence, cook up a common ground replacement: `getfullargspec_no_self` which
286# mimics `inspect.getfullargspec` but does not list `self`.
287#
288# This way, the caller code does not need to know whether it uses a legacy
289# .getfullargspec or a bright and shiny .signature.
291FullArgSpec = namedtuple('FullArgSpec',
292 ['args', 'varargs', 'varkw', 'defaults',
293 'kwonlyargs', 'kwonlydefaults', 'annotations'])
296def getfullargspec_no_self(func):
297 """inspect.getfullargspec replacement using inspect.signature.
299 If func is a bound method, do not list the 'self' parameter.
301 Parameters
302 ----------
303 func : callable
304 A callable to inspect
306 Returns
307 -------
308 fullargspec : FullArgSpec(args, varargs, varkw, defaults, kwonlyargs,
309 kwonlydefaults, annotations)
311 NOTE: if the first argument of `func` is self, it is *not*, I repeat
312 *not*, included in fullargspec.args.
313 This is done for consistency between inspect.getargspec() under
314 Python 2.x, and inspect.signature() under Python 3.x.
316 """
317 sig = inspect.signature(func)
318 args = [
319 p.name for p in sig.parameters.values()
320 if p.kind in [inspect.Parameter.POSITIONAL_OR_KEYWORD,
321 inspect.Parameter.POSITIONAL_ONLY]
322 ]
323 varargs = [
324 p.name for p in sig.parameters.values()
325 if p.kind == inspect.Parameter.VAR_POSITIONAL
326 ]
327 varargs = varargs[0] if varargs else None
328 varkw = [
329 p.name for p in sig.parameters.values()
330 if p.kind == inspect.Parameter.VAR_KEYWORD
331 ]
332 varkw = varkw[0] if varkw else None
333 defaults = tuple(
334 p.default for p in sig.parameters.values()
335 if (p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD and
336 p.default is not p.empty)
337 ) or None
338 kwonlyargs = [
339 p.name for p in sig.parameters.values()
340 if p.kind == inspect.Parameter.KEYWORD_ONLY
341 ]
342 kwdefaults = {p.name: p.default for p in sig.parameters.values()
343 if p.kind == inspect.Parameter.KEYWORD_ONLY and
344 p.default is not p.empty}
345 annotations = {p.name: p.annotation for p in sig.parameters.values()
346 if p.annotation is not p.empty}
347 return FullArgSpec(args, varargs, varkw, defaults, kwonlyargs,
348 kwdefaults or None, annotations)
351class _FunctionWrapper:
352 """
353 Object to wrap user's function, allowing picklability
354 """
355 def __init__(self, f, args):
356 self.f = f
357 self.args = [] if args is None else args
359 def __call__(self, x):
360 return self.f(x, *self.args)
363class MapWrapper:
364 """
365 Parallelisation wrapper for working with map-like callables, such as
366 `multiprocessing.Pool.map`.
368 Parameters
369 ----------
370 pool : int or map-like callable
371 If `pool` is an integer, then it specifies the number of threads to
372 use for parallelization. If ``int(pool) == 1``, then no parallel
373 processing is used and the map builtin is used.
374 If ``pool == -1``, then the pool will utilize all available CPUs.
375 If `pool` is a map-like callable that follows the same
376 calling sequence as the built-in map function, then this callable is
377 used for parallelization.
378 """
379 def __init__(self, pool=1):
380 self.pool = None
381 self._mapfunc = map
382 self._own_pool = False
384 if callable(pool):
385 self.pool = pool
386 self._mapfunc = self.pool
387 else:
388 from multiprocessing import Pool
389 # user supplies a number
390 if int(pool) == -1:
391 # use as many processors as possible
392 self.pool = Pool()
393 self._mapfunc = self.pool.map
394 self._own_pool = True
395 elif int(pool) == 1:
396 pass
397 elif int(pool) > 1:
398 # use the number of processors requested
399 self.pool = Pool(processes=int(pool))
400 self._mapfunc = self.pool.map
401 self._own_pool = True
402 else:
403 raise RuntimeError("Number of workers specified must be -1,"
404 " an int >= 1, or an object with a 'map' "
405 "method")
407 def __enter__(self):
408 return self
410 def terminate(self):
411 if self._own_pool:
412 self.pool.terminate()
414 def join(self):
415 if self._own_pool:
416 self.pool.join()
418 def close(self):
419 if self._own_pool:
420 self.pool.close()
422 def __exit__(self, exc_type, exc_value, traceback):
423 if self._own_pool:
424 self.pool.close()
425 self.pool.terminate()
427 def __call__(self, func, iterable):
428 # only accept one iterable because that's all Pool.map accepts
429 try:
430 return self._mapfunc(func, iterable)
431 except TypeError as e:
432 # wrong number of arguments
433 raise TypeError("The map-like callable must be of the"
434 " form f(func, iterable)") from e
437def rng_integers(gen, low, high=None, size=None, dtype='int64',
438 endpoint=False):
439 """
440 Return random integers from low (inclusive) to high (exclusive), or if
441 endpoint=True, low (inclusive) to high (inclusive). Replaces
442 `RandomState.randint` (with endpoint=False) and
443 `RandomState.random_integers` (with endpoint=True).
445 Return random integers from the "discrete uniform" distribution of the
446 specified dtype. If high is None (the default), then results are from
447 0 to low.
449 Parameters
450 ----------
451 gen : {None, np.random.RandomState, np.random.Generator}
452 Random number generator. If None, then the np.random.RandomState
453 singleton is used.
454 low : int or array-like of ints
455 Lowest (signed) integers to be drawn from the distribution (unless
456 high=None, in which case this parameter is 0 and this value is used
457 for high).
458 high : int or array-like of ints
459 If provided, one above the largest (signed) integer to be drawn from
460 the distribution (see above for behavior if high=None). If array-like,
461 must contain integer values.
462 size : array-like of ints, optional
463 Output shape. If the given shape is, e.g., (m, n, k), then m * n * k
464 samples are drawn. Default is None, in which case a single value is
465 returned.
466 dtype : {str, dtype}, optional
467 Desired dtype of the result. All dtypes are determined by their name,
468 i.e., 'int64', 'int', etc, so byteorder is not available and a specific
469 precision may have different C types depending on the platform.
470 The default value is np.int_.
471 endpoint : bool, optional
472 If True, sample from the interval [low, high] instead of the default
473 [low, high) Defaults to False.
475 Returns
476 -------
477 out: int or ndarray of ints
478 size-shaped array of random integers from the appropriate distribution,
479 or a single such random int if size not provided.
480 """
481 if isinstance(gen, Generator):
482 return gen.integers(low, high=high, size=size, dtype=dtype,
483 endpoint=endpoint)
484 else:
485 if gen is None:
486 # default is RandomState singleton used by np.random.
487 gen = np.random.mtrand._rand
488 if endpoint:
489 # inclusive of endpoint
490 # remember that low and high can be arrays, so don't modify in
491 # place
492 if high is None:
493 return gen.randint(low + 1, size=size, dtype=dtype)
494 if high is not None:
495 return gen.randint(low, high=high + 1, size=size, dtype=dtype)
497 # exclusive
498 return gen.randint(low, high=high, size=size, dtype=dtype)
501@contextmanager
502def _fixed_default_rng(seed=1638083107694713882823079058616272161):
503 """Context with a fixed np.random.default_rng seed."""
504 orig_fun = np.random.default_rng
505 np.random.default_rng = lambda seed=seed: orig_fun(seed)
506 try:
507 yield
508 finally:
509 np.random.default_rng = orig_fun
512def _rng_html_rewrite(func):
513 """Rewrite the HTML rendering of ``np.random.default_rng``.
515 This is intended to decorate
516 ``numpydoc.docscrape_sphinx.SphinxDocString._str_examples``.
518 Examples are only run by Sphinx when there are plot involved. Even so,
519 it does not change the result values getting printed.
520 """
521 # hexadecimal or number seed, case-insensitive
522 pattern = re.compile(r'np.random.default_rng\((0x[0-9A-F]+|\d+)\)', re.I)
524 def _wrapped(*args, **kwargs):
525 res = func(*args, **kwargs)
526 lines = [
527 re.sub(pattern, 'np.random.default_rng()', line)
528 for line in res
529 ]
530 return lines
532 return _wrapped
535def _argmin(a, keepdims=False, axis=None):
536 """
537 argmin with a `keepdims` parameter.
539 See https://github.com/numpy/numpy/issues/8710
541 If axis is not None, a.shape[axis] must be greater than 0.
542 """
543 res = np.argmin(a, axis=axis)
544 if keepdims and axis is not None:
545 res = np.expand_dims(res, axis=axis)
546 return res
549def _first_nonnan(a, axis):
550 """
551 Return the first non-nan value along the given axis.
553 If a slice is all nan, nan is returned for that slice.
555 The shape of the return value corresponds to ``keepdims=True``.
557 Examples
558 --------
559 >>> import numpy as np
560 >>> nan = np.nan
561 >>> a = np.array([[ 3., 3., nan, 3.],
562 [ 1., nan, 2., 4.],
563 [nan, nan, 9., -1.],
564 [nan, 5., 4., 3.],
565 [ 2., 2., 2., 2.],
566 [nan, nan, nan, nan]])
567 >>> _first_nonnan(a, axis=0)
568 array([[3., 3., 2., 3.]])
569 >>> _first_nonnan(a, axis=1)
570 array([[ 3.],
571 [ 1.],
572 [ 9.],
573 [ 5.],
574 [ 2.],
575 [nan]])
576 """
577 k = _argmin(np.isnan(a), axis=axis, keepdims=True)
578 return np.take_along_axis(a, k, axis=axis)
581def _nan_allsame(a, axis, keepdims=False):
582 """
583 Determine if the values along an axis are all the same.
585 nan values are ignored.
587 `a` must be a numpy array.
589 `axis` is assumed to be normalized; that is, 0 <= axis < a.ndim.
591 For an axis of length 0, the result is True. That is, we adopt the
592 convention that ``allsame([])`` is True. (There are no values in the
593 input that are different.)
595 `True` is returned for slices that are all nan--not because all the
596 values are the same, but because this is equivalent to ``allsame([])``.
598 Examples
599 --------
600 >>> import numpy as np
601 >>> a = np.array([[ 3., 3., nan, 3.],
602 [ 1., nan, 2., 4.],
603 [nan, nan, 9., -1.],
604 [nan, 5., 4., 3.],
605 [ 2., 2., 2., 2.],
606 [nan, nan, nan, nan]])
607 >>> _nan_allsame(a, axis=1, keepdims=True)
608 array([[ True],
609 [False],
610 [False],
611 [False],
612 [ True],
613 [ True]])
614 """
615 if axis is None:
616 if a.size == 0:
617 return True
618 a = a.ravel()
619 axis = 0
620 else:
621 shp = a.shape
622 if shp[axis] == 0:
623 shp = shp[:axis] + (1,)*keepdims + shp[axis + 1:]
624 return np.full(shp, fill_value=True, dtype=bool)
625 a0 = _first_nonnan(a, axis=axis)
626 return ((a0 == a) | np.isnan(a)).all(axis=axis, keepdims=keepdims)
629def _contains_nan(a, nan_policy='propagate', use_summation=True,
630 policies=None):
631 if not isinstance(a, np.ndarray):
632 use_summation = False # some array_likes ignore nans (e.g. pandas)
633 if policies is None:
634 policies = ['propagate', 'raise', 'omit']
635 if nan_policy not in policies:
636 raise ValueError("nan_policy must be one of {%s}" %
637 ', '.join("'%s'" % s for s in policies))
639 if np.issubdtype(a.dtype, np.inexact):
640 # The summation method avoids creating a (potentially huge) array.
641 if use_summation:
642 with np.errstate(invalid='ignore', over='ignore'):
643 contains_nan = np.isnan(np.sum(a))
644 else:
645 contains_nan = np.isnan(a).any()
646 elif np.issubdtype(a.dtype, object):
647 contains_nan = False
648 for el in a.ravel():
649 # isnan doesn't work on non-numeric elements
650 if np.issubdtype(type(el), np.number) and np.isnan(el):
651 contains_nan = True
652 break
653 else:
654 # Only `object` and `inexact` arrays can have NaNs
655 contains_nan = False
657 if contains_nan and nan_policy == 'raise':
658 raise ValueError("The input contains nan values")
660 return contains_nan, nan_policy
663def _rename_parameter(old_name, new_name, dep_version=None):
664 """
665 Generate decorator for backward-compatible keyword renaming.
667 Apply the decorator generated by `_rename_parameter` to functions with a
668 recently renamed parameter to maintain backward-compatibility.
670 After decoration, the function behaves as follows:
671 If only the new parameter is passed into the function, behave as usual.
672 If only the old parameter is passed into the function (as a keyword), raise
673 a DeprecationWarning if `dep_version` is provided, and behave as usual
674 otherwise.
675 If both old and new parameters are passed into the function, raise a
676 DeprecationWarning if `dep_version` is provided, and raise the appropriate
677 TypeError (function got multiple values for argument).
679 Parameters
680 ----------
681 old_name : str
682 Old name of parameter
683 new_name : str
684 New name of parameter
685 dep_version : str, optional
686 Version of SciPy in which old parameter was deprecated in the format
687 'X.Y.Z'. If supplied, the deprecation message will indicate that
688 support for the old parameter will be removed in version 'X.Y+2.Z'
690 Notes
691 -----
692 Untested with functions that accept *args. Probably won't work as written.
694 """
695 def decorator(fun):
696 @functools.wraps(fun)
697 def wrapper(*args, **kwargs):
698 if old_name in kwargs:
699 if dep_version:
700 end_version = dep_version.split('.')
701 end_version[1] = str(int(end_version[1]) + 2)
702 end_version = '.'.join(end_version)
703 message = (f"Use of keyword argument `{old_name}` is "
704 f"deprecated and replaced by `{new_name}`. "
705 f"Support for `{old_name}` will be removed "
706 f"in SciPy {end_version}.")
707 warnings.warn(message, DeprecationWarning, stacklevel=2)
708 if new_name in kwargs:
709 message = (f"{fun.__name__}() got multiple values for "
710 f"argument now known as `{new_name}`")
711 raise TypeError(message)
712 kwargs[new_name] = kwargs.pop(old_name)
713 return fun(*args, **kwargs)
714 return wrapper
715 return decorator
718def _rng_spawn(rng, n_children):
719 # spawns independent RNGs from a parent RNG
720 bg = rng._bit_generator
721 ss = bg._seed_seq
722 child_rngs = [np.random.Generator(type(bg)(child_ss))
723 for child_ss in ss.spawn(n_children)]
724 return child_rngs
727def _get_nan(*data):
728 # Get NaN of appropriate dtype for data
729 data = [np.asarray(item) for item in data]
730 dtype = np.result_type(*data, np.half) # must be a float16 at least
731 return np.array(np.nan, dtype=dtype)[()]