Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/scipy/_lib/_util.py: 20%
347 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-14 06:37 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-14 06:37 +0000
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
18from scipy._lib._array_api import array_namespace
21AxisError: type[Exception]
22ComplexWarning: type[Warning]
23VisibleDeprecationWarning: type[Warning]
25if np.lib.NumpyVersion(np.__version__) >= '1.25.0':
26 from numpy.exceptions import (
27 AxisError, ComplexWarning, VisibleDeprecationWarning,
28 DTypePromotionError
29 )
30else:
31 from numpy import (
32 AxisError, ComplexWarning, VisibleDeprecationWarning # noqa: F401
33 )
34 DTypePromotionError = TypeError # type: ignore
36np_long: type
37np_ulong: type
39if np.lib.NumpyVersion(np.__version__) >= "2.0.0.dev0":
40 try:
41 with warnings.catch_warnings():
42 warnings.filterwarnings(
43 "ignore",
44 r".*In the future `np\.long` will be defined as.*",
45 FutureWarning,
46 )
47 np_long = np.long # type: ignore[attr-defined]
48 np_ulong = np.ulong # type: ignore[attr-defined]
49 except AttributeError:
50 np_long = np.int_
51 np_ulong = np.uint
52else:
53 np_long = np.int_
54 np_ulong = np.uint
56IntNumber = Union[int, np.integer]
57DecimalNumber = Union[float, np.floating, np.integer]
59# Since Generator was introduced in numpy 1.17, the following condition is needed for
60# backward compatibility
61if TYPE_CHECKING:
62 SeedType = Optional[Union[IntNumber, np.random.Generator,
63 np.random.RandomState]]
64 GeneratorType = TypeVar("GeneratorType", bound=Union[np.random.Generator,
65 np.random.RandomState])
67try:
68 from numpy.random import Generator as Generator
69except ImportError:
70 class Generator: # type: ignore[no-redef]
71 pass
74def _lazywhere(cond, arrays, f, fillvalue=None, f2=None):
75 """Return elements chosen from two possibilities depending on a condition
77 Equivalent to ``f(*arrays) if cond else fillvalue`` performed elementwise.
79 Parameters
80 ----------
81 cond : array
82 The condition (expressed as a boolean array).
83 arrays : tuple of array
84 Arguments to `f` (and `f2`). Must be broadcastable with `cond`.
85 f : callable
86 Where `cond` is True, output will be ``f(arr1[cond], arr2[cond], ...)``
87 fillvalue : object
88 If provided, value with which to fill output array where `cond` is
89 not True.
90 f2 : callable
91 If provided, output will be ``f2(arr1[cond], arr2[cond], ...)`` where
92 `cond` is not True.
94 Returns
95 -------
96 out : array
97 An array with elements from the output of `f` where `cond` is True
98 and `fillvalue` (or elements from the output of `f2`) elsewhere. The
99 returned array has data type determined by Type Promotion Rules
100 with the output of `f` and `fillvalue` (or the output of `f2`).
102 Notes
103 -----
104 ``xp.where(cond, x, fillvalue)`` requires explicitly forming `x` even where
105 `cond` is False. This function evaluates ``f(arr1[cond], arr2[cond], ...)``
106 onle where `cond` ``is True.
108 Examples
109 --------
110 >>> import numpy as np
111 >>> a, b = np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8])
112 >>> def f(a, b):
113 ... return a*b
114 >>> _lazywhere(a > 2, (a, b), f, np.nan)
115 array([ nan, nan, 21., 32.])
117 """
118 xp = array_namespace(cond, *arrays)
120 if (f2 is fillvalue is None) or (f2 is not None and fillvalue is not None):
121 raise ValueError("Exactly one of `fillvalue` or `f2` must be given.")
123 args = xp.broadcast_arrays(cond, *arrays)
124 bool_dtype = xp.asarray([True]).dtype # numpy 1.xx doesn't have `bool`
125 cond, arrays = xp.astype(args[0], bool_dtype, copy=False), args[1:]
127 temp1 = xp.asarray(f(*(arr[cond] for arr in arrays)))
129 if f2 is None:
130 fillvalue = xp.asarray(fillvalue)
131 dtype = xp.result_type(temp1.dtype, fillvalue.dtype)
132 out = xp.full(cond.shape, fill_value=fillvalue, dtype=dtype)
133 else:
134 ncond = ~cond
135 temp2 = xp.asarray(f2(*(arr[ncond] for arr in arrays)))
136 dtype = xp.result_type(temp1, temp2)
137 out = xp.empty(cond.shape, dtype=dtype)
138 out[ncond] = temp2
140 out[cond] = temp1
142 return out
145def _lazyselect(condlist, choicelist, arrays, default=0):
146 """
147 Mimic `np.select(condlist, choicelist)`.
149 Notice, it assumes that all `arrays` are of the same shape or can be
150 broadcasted together.
152 All functions in `choicelist` must accept array arguments in the order
153 given in `arrays` and must return an array of the same shape as broadcasted
154 `arrays`.
156 Examples
157 --------
158 >>> import numpy as np
159 >>> x = np.arange(6)
160 >>> np.select([x <3, x > 3], [x**2, x**3], default=0)
161 array([ 0, 1, 4, 0, 64, 125])
163 >>> _lazyselect([x < 3, x > 3], [lambda x: x**2, lambda x: x**3], (x,))
164 array([ 0., 1., 4., 0., 64., 125.])
166 >>> a = -np.ones_like(x)
167 >>> _lazyselect([x < 3, x > 3],
168 ... [lambda x, a: x**2, lambda x, a: a * x**3],
169 ... (x, a), default=np.nan)
170 array([ 0., 1., 4., nan, -64., -125.])
172 """
173 arrays = np.broadcast_arrays(*arrays)
174 tcode = np.mintypecode([a.dtype.char for a in arrays])
175 out = np.full(np.shape(arrays[0]), fill_value=default, dtype=tcode)
176 for func, cond in zip(choicelist, condlist):
177 if np.all(cond is False):
178 continue
179 cond, _ = np.broadcast_arrays(cond, arrays[0])
180 temp = tuple(np.extract(cond, arr) for arr in arrays)
181 np.place(out, cond, func(*temp))
182 return out
185def _aligned_zeros(shape, dtype=float, order="C", align=None):
186 """Allocate a new ndarray with aligned memory.
188 Primary use case for this currently is working around a f2py issue
189 in NumPy 1.9.1, where dtype.alignment is such that np.zeros() does
190 not necessarily create arrays aligned up to it.
192 """
193 dtype = np.dtype(dtype)
194 if align is None:
195 align = dtype.alignment
196 if not hasattr(shape, '__len__'):
197 shape = (shape,)
198 size = functools.reduce(operator.mul, shape) * dtype.itemsize
199 buf = np.empty(size + align + 1, np.uint8)
200 offset = buf.__array_interface__['data'][0] % align
201 if offset != 0:
202 offset = align - offset
203 # Note: slices producing 0-size arrays do not necessarily change
204 # data pointer --- so we use and allocate size+1
205 buf = buf[offset:offset+size+1][:-1]
206 data = np.ndarray(shape, dtype, buf, order=order)
207 data.fill(0)
208 return data
211def _prune_array(array):
212 """Return an array equivalent to the input array. If the input
213 array is a view of a much larger array, copy its contents to a
214 newly allocated array. Otherwise, return the input unchanged.
215 """
216 if array.base is not None and array.size < array.base.size // 2:
217 return array.copy()
218 return array
221def float_factorial(n: int) -> float:
222 """Compute the factorial and return as a float
224 Returns infinity when result is too large for a double
225 """
226 return float(math.factorial(n)) if n < 171 else np.inf
229# copy-pasted from scikit-learn utils/validation.py
230# change this to scipy.stats._qmc.check_random_state once numpy 1.16 is dropped
231def check_random_state(seed):
232 """Turn `seed` into a `np.random.RandomState` instance.
234 Parameters
235 ----------
236 seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
237 If `seed` is None (or `np.random`), the `numpy.random.RandomState`
238 singleton is used.
239 If `seed` is an int, a new ``RandomState`` instance is used,
240 seeded with `seed`.
241 If `seed` is already a ``Generator`` or ``RandomState`` instance then
242 that instance is used.
244 Returns
245 -------
246 seed : {`numpy.random.Generator`, `numpy.random.RandomState`}
247 Random number generator.
249 """
250 if seed is None or seed is np.random:
251 return np.random.mtrand._rand
252 if isinstance(seed, (numbers.Integral, np.integer)):
253 return np.random.RandomState(seed)
254 if isinstance(seed, (np.random.RandomState, np.random.Generator)):
255 return seed
257 raise ValueError('%r cannot be used to seed a numpy.random.RandomState'
258 ' instance' % seed)
261def _asarray_validated(a, check_finite=True,
262 sparse_ok=False, objects_ok=False, mask_ok=False,
263 as_inexact=False):
264 """
265 Helper function for SciPy argument validation.
267 Many SciPy linear algebra functions do support arbitrary array-like
268 input arguments. Examples of commonly unsupported inputs include
269 matrices containing inf/nan, sparse matrix representations, and
270 matrices with complicated elements.
272 Parameters
273 ----------
274 a : array_like
275 The array-like input.
276 check_finite : bool, optional
277 Whether to check that the input matrices contain only finite numbers.
278 Disabling may give a performance gain, but may result in problems
279 (crashes, non-termination) if the inputs do contain infinities or NaNs.
280 Default: True
281 sparse_ok : bool, optional
282 True if scipy sparse matrices are allowed.
283 objects_ok : bool, optional
284 True if arrays with dype('O') are allowed.
285 mask_ok : bool, optional
286 True if masked arrays are allowed.
287 as_inexact : bool, optional
288 True to convert the input array to a np.inexact dtype.
290 Returns
291 -------
292 ret : ndarray
293 The converted validated array.
295 """
296 if not sparse_ok:
297 import scipy.sparse
298 if scipy.sparse.issparse(a):
299 msg = ('Sparse matrices are not supported by this function. '
300 'Perhaps one of the scipy.sparse.linalg functions '
301 'would work instead.')
302 raise ValueError(msg)
303 if not mask_ok:
304 if np.ma.isMaskedArray(a):
305 raise ValueError('masked arrays are not supported')
306 toarray = np.asarray_chkfinite if check_finite else np.asarray
307 a = toarray(a)
308 if not objects_ok:
309 if a.dtype is np.dtype('O'):
310 raise ValueError('object arrays are not supported')
311 if as_inexact:
312 if not np.issubdtype(a.dtype, np.inexact):
313 a = toarray(a, dtype=np.float64)
314 return a
317def _validate_int(k, name, minimum=None):
318 """
319 Validate a scalar integer.
321 This function can be used to validate an argument to a function
322 that expects the value to be an integer. It uses `operator.index`
323 to validate the value (so, for example, k=2.0 results in a
324 TypeError).
326 Parameters
327 ----------
328 k : int
329 The value to be validated.
330 name : str
331 The name of the parameter.
332 minimum : int, optional
333 An optional lower bound.
334 """
335 try:
336 k = operator.index(k)
337 except TypeError:
338 raise TypeError(f'{name} must be an integer.') from None
339 if minimum is not None and k < minimum:
340 raise ValueError(f'{name} must be an integer not less '
341 f'than {minimum}') from None
342 return k
345# Add a replacement for inspect.getfullargspec()/
346# The version below is borrowed from Django,
347# https://github.com/django/django/pull/4846.
349# Note an inconsistency between inspect.getfullargspec(func) and
350# inspect.signature(func). If `func` is a bound method, the latter does *not*
351# list `self` as a first argument, while the former *does*.
352# Hence, cook up a common ground replacement: `getfullargspec_no_self` which
353# mimics `inspect.getfullargspec` but does not list `self`.
354#
355# This way, the caller code does not need to know whether it uses a legacy
356# .getfullargspec or a bright and shiny .signature.
358FullArgSpec = namedtuple('FullArgSpec',
359 ['args', 'varargs', 'varkw', 'defaults',
360 'kwonlyargs', 'kwonlydefaults', 'annotations'])
363def getfullargspec_no_self(func):
364 """inspect.getfullargspec replacement using inspect.signature.
366 If func is a bound method, do not list the 'self' parameter.
368 Parameters
369 ----------
370 func : callable
371 A callable to inspect
373 Returns
374 -------
375 fullargspec : FullArgSpec(args, varargs, varkw, defaults, kwonlyargs,
376 kwonlydefaults, annotations)
378 NOTE: if the first argument of `func` is self, it is *not*, I repeat
379 *not*, included in fullargspec.args.
380 This is done for consistency between inspect.getargspec() under
381 Python 2.x, and inspect.signature() under Python 3.x.
383 """
384 sig = inspect.signature(func)
385 args = [
386 p.name for p in sig.parameters.values()
387 if p.kind in [inspect.Parameter.POSITIONAL_OR_KEYWORD,
388 inspect.Parameter.POSITIONAL_ONLY]
389 ]
390 varargs = [
391 p.name for p in sig.parameters.values()
392 if p.kind == inspect.Parameter.VAR_POSITIONAL
393 ]
394 varargs = varargs[0] if varargs else None
395 varkw = [
396 p.name for p in sig.parameters.values()
397 if p.kind == inspect.Parameter.VAR_KEYWORD
398 ]
399 varkw = varkw[0] if varkw else None
400 defaults = tuple(
401 p.default for p in sig.parameters.values()
402 if (p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD and
403 p.default is not p.empty)
404 ) or None
405 kwonlyargs = [
406 p.name for p in sig.parameters.values()
407 if p.kind == inspect.Parameter.KEYWORD_ONLY
408 ]
409 kwdefaults = {p.name: p.default for p in sig.parameters.values()
410 if p.kind == inspect.Parameter.KEYWORD_ONLY and
411 p.default is not p.empty}
412 annotations = {p.name: p.annotation for p in sig.parameters.values()
413 if p.annotation is not p.empty}
414 return FullArgSpec(args, varargs, varkw, defaults, kwonlyargs,
415 kwdefaults or None, annotations)
418class _FunctionWrapper:
419 """
420 Object to wrap user's function, allowing picklability
421 """
422 def __init__(self, f, args):
423 self.f = f
424 self.args = [] if args is None else args
426 def __call__(self, x):
427 return self.f(x, *self.args)
430class MapWrapper:
431 """
432 Parallelisation wrapper for working with map-like callables, such as
433 `multiprocessing.Pool.map`.
435 Parameters
436 ----------
437 pool : int or map-like callable
438 If `pool` is an integer, then it specifies the number of threads to
439 use for parallelization. If ``int(pool) == 1``, then no parallel
440 processing is used and the map builtin is used.
441 If ``pool == -1``, then the pool will utilize all available CPUs.
442 If `pool` is a map-like callable that follows the same
443 calling sequence as the built-in map function, then this callable is
444 used for parallelization.
445 """
446 def __init__(self, pool=1):
447 self.pool = None
448 self._mapfunc = map
449 self._own_pool = False
451 if callable(pool):
452 self.pool = pool
453 self._mapfunc = self.pool
454 else:
455 from multiprocessing import Pool
456 # user supplies a number
457 if int(pool) == -1:
458 # use as many processors as possible
459 self.pool = Pool()
460 self._mapfunc = self.pool.map
461 self._own_pool = True
462 elif int(pool) == 1:
463 pass
464 elif int(pool) > 1:
465 # use the number of processors requested
466 self.pool = Pool(processes=int(pool))
467 self._mapfunc = self.pool.map
468 self._own_pool = True
469 else:
470 raise RuntimeError("Number of workers specified must be -1,"
471 " an int >= 1, or an object with a 'map' "
472 "method")
474 def __enter__(self):
475 return self
477 def terminate(self):
478 if self._own_pool:
479 self.pool.terminate()
481 def join(self):
482 if self._own_pool:
483 self.pool.join()
485 def close(self):
486 if self._own_pool:
487 self.pool.close()
489 def __exit__(self, exc_type, exc_value, traceback):
490 if self._own_pool:
491 self.pool.close()
492 self.pool.terminate()
494 def __call__(self, func, iterable):
495 # only accept one iterable because that's all Pool.map accepts
496 try:
497 return self._mapfunc(func, iterable)
498 except TypeError as e:
499 # wrong number of arguments
500 raise TypeError("The map-like callable must be of the"
501 " form f(func, iterable)") from e
504def rng_integers(gen, low, high=None, size=None, dtype='int64',
505 endpoint=False):
506 """
507 Return random integers from low (inclusive) to high (exclusive), or if
508 endpoint=True, low (inclusive) to high (inclusive). Replaces
509 `RandomState.randint` (with endpoint=False) and
510 `RandomState.random_integers` (with endpoint=True).
512 Return random integers from the "discrete uniform" distribution of the
513 specified dtype. If high is None (the default), then results are from
514 0 to low.
516 Parameters
517 ----------
518 gen : {None, np.random.RandomState, np.random.Generator}
519 Random number generator. If None, then the np.random.RandomState
520 singleton is used.
521 low : int or array-like of ints
522 Lowest (signed) integers to be drawn from the distribution (unless
523 high=None, in which case this parameter is 0 and this value is used
524 for high).
525 high : int or array-like of ints
526 If provided, one above the largest (signed) integer to be drawn from
527 the distribution (see above for behavior if high=None). If array-like,
528 must contain integer values.
529 size : array-like of ints, optional
530 Output shape. If the given shape is, e.g., (m, n, k), then m * n * k
531 samples are drawn. Default is None, in which case a single value is
532 returned.
533 dtype : {str, dtype}, optional
534 Desired dtype of the result. All dtypes are determined by their name,
535 i.e., 'int64', 'int', etc, so byteorder is not available and a specific
536 precision may have different C types depending on the platform.
537 The default value is 'int64'.
538 endpoint : bool, optional
539 If True, sample from the interval [low, high] instead of the default
540 [low, high) Defaults to False.
542 Returns
543 -------
544 out: int or ndarray of ints
545 size-shaped array of random integers from the appropriate distribution,
546 or a single such random int if size not provided.
547 """
548 if isinstance(gen, Generator):
549 return gen.integers(low, high=high, size=size, dtype=dtype,
550 endpoint=endpoint)
551 else:
552 if gen is None:
553 # default is RandomState singleton used by np.random.
554 gen = np.random.mtrand._rand
555 if endpoint:
556 # inclusive of endpoint
557 # remember that low and high can be arrays, so don't modify in
558 # place
559 if high is None:
560 return gen.randint(low + 1, size=size, dtype=dtype)
561 if high is not None:
562 return gen.randint(low, high=high + 1, size=size, dtype=dtype)
564 # exclusive
565 return gen.randint(low, high=high, size=size, dtype=dtype)
568@contextmanager
569def _fixed_default_rng(seed=1638083107694713882823079058616272161):
570 """Context with a fixed np.random.default_rng seed."""
571 orig_fun = np.random.default_rng
572 np.random.default_rng = lambda seed=seed: orig_fun(seed)
573 try:
574 yield
575 finally:
576 np.random.default_rng = orig_fun
579def _rng_html_rewrite(func):
580 """Rewrite the HTML rendering of ``np.random.default_rng``.
582 This is intended to decorate
583 ``numpydoc.docscrape_sphinx.SphinxDocString._str_examples``.
585 Examples are only run by Sphinx when there are plot involved. Even so,
586 it does not change the result values getting printed.
587 """
588 # hexadecimal or number seed, case-insensitive
589 pattern = re.compile(r'np.random.default_rng\((0x[0-9A-F]+|\d+)\)', re.I)
591 def _wrapped(*args, **kwargs):
592 res = func(*args, **kwargs)
593 lines = [
594 re.sub(pattern, 'np.random.default_rng()', line)
595 for line in res
596 ]
597 return lines
599 return _wrapped
602def _argmin(a, keepdims=False, axis=None):
603 """
604 argmin with a `keepdims` parameter.
606 See https://github.com/numpy/numpy/issues/8710
608 If axis is not None, a.shape[axis] must be greater than 0.
609 """
610 res = np.argmin(a, axis=axis)
611 if keepdims and axis is not None:
612 res = np.expand_dims(res, axis=axis)
613 return res
616def _first_nonnan(a, axis):
617 """
618 Return the first non-nan value along the given axis.
620 If a slice is all nan, nan is returned for that slice.
622 The shape of the return value corresponds to ``keepdims=True``.
624 Examples
625 --------
626 >>> import numpy as np
627 >>> nan = np.nan
628 >>> a = np.array([[ 3., 3., nan, 3.],
629 [ 1., nan, 2., 4.],
630 [nan, nan, 9., -1.],
631 [nan, 5., 4., 3.],
632 [ 2., 2., 2., 2.],
633 [nan, nan, nan, nan]])
634 >>> _first_nonnan(a, axis=0)
635 array([[3., 3., 2., 3.]])
636 >>> _first_nonnan(a, axis=1)
637 array([[ 3.],
638 [ 1.],
639 [ 9.],
640 [ 5.],
641 [ 2.],
642 [nan]])
643 """
644 k = _argmin(np.isnan(a), axis=axis, keepdims=True)
645 return np.take_along_axis(a, k, axis=axis)
648def _nan_allsame(a, axis, keepdims=False):
649 """
650 Determine if the values along an axis are all the same.
652 nan values are ignored.
654 `a` must be a numpy array.
656 `axis` is assumed to be normalized; that is, 0 <= axis < a.ndim.
658 For an axis of length 0, the result is True. That is, we adopt the
659 convention that ``allsame([])`` is True. (There are no values in the
660 input that are different.)
662 `True` is returned for slices that are all nan--not because all the
663 values are the same, but because this is equivalent to ``allsame([])``.
665 Examples
666 --------
667 >>> from numpy import nan, array
668 >>> a = array([[ 3., 3., nan, 3.],
669 ... [ 1., nan, 2., 4.],
670 ... [nan, nan, 9., -1.],
671 ... [nan, 5., 4., 3.],
672 ... [ 2., 2., 2., 2.],
673 ... [nan, nan, nan, nan]])
674 >>> _nan_allsame(a, axis=1, keepdims=True)
675 array([[ True],
676 [False],
677 [False],
678 [False],
679 [ True],
680 [ True]])
681 """
682 if axis is None:
683 if a.size == 0:
684 return True
685 a = a.ravel()
686 axis = 0
687 else:
688 shp = a.shape
689 if shp[axis] == 0:
690 shp = shp[:axis] + (1,)*keepdims + shp[axis + 1:]
691 return np.full(shp, fill_value=True, dtype=bool)
692 a0 = _first_nonnan(a, axis=axis)
693 return ((a0 == a) | np.isnan(a)).all(axis=axis, keepdims=keepdims)
696def _contains_nan(a, nan_policy='propagate', use_summation=True,
697 policies=None):
698 if not isinstance(a, np.ndarray):
699 use_summation = False # some array_likes ignore nans (e.g. pandas)
700 if policies is None:
701 policies = ['propagate', 'raise', 'omit']
702 if nan_policy not in policies:
703 raise ValueError("nan_policy must be one of {%s}" %
704 ', '.join("'%s'" % s for s in policies))
706 if np.issubdtype(a.dtype, np.inexact):
707 # The summation method avoids creating a (potentially huge) array.
708 if use_summation:
709 with np.errstate(invalid='ignore', over='ignore'):
710 contains_nan = np.isnan(np.sum(a))
711 else:
712 contains_nan = np.isnan(a).any()
713 elif np.issubdtype(a.dtype, object):
714 contains_nan = False
715 for el in a.ravel():
716 # isnan doesn't work on non-numeric elements
717 if np.issubdtype(type(el), np.number) and np.isnan(el):
718 contains_nan = True
719 break
720 else:
721 # Only `object` and `inexact` arrays can have NaNs
722 contains_nan = False
724 if contains_nan and nan_policy == 'raise':
725 raise ValueError("The input contains nan values")
727 return contains_nan, nan_policy
730def _rename_parameter(old_name, new_name, dep_version=None):
731 """
732 Generate decorator for backward-compatible keyword renaming.
734 Apply the decorator generated by `_rename_parameter` to functions with a
735 recently renamed parameter to maintain backward-compatibility.
737 After decoration, the function behaves as follows:
738 If only the new parameter is passed into the function, behave as usual.
739 If only the old parameter is passed into the function (as a keyword), raise
740 a DeprecationWarning if `dep_version` is provided, and behave as usual
741 otherwise.
742 If both old and new parameters are passed into the function, raise a
743 DeprecationWarning if `dep_version` is provided, and raise the appropriate
744 TypeError (function got multiple values for argument).
746 Parameters
747 ----------
748 old_name : str
749 Old name of parameter
750 new_name : str
751 New name of parameter
752 dep_version : str, optional
753 Version of SciPy in which old parameter was deprecated in the format
754 'X.Y.Z'. If supplied, the deprecation message will indicate that
755 support for the old parameter will be removed in version 'X.Y+2.Z'
757 Notes
758 -----
759 Untested with functions that accept *args. Probably won't work as written.
761 """
762 def decorator(fun):
763 @functools.wraps(fun)
764 def wrapper(*args, **kwargs):
765 if old_name in kwargs:
766 if dep_version:
767 end_version = dep_version.split('.')
768 end_version[1] = str(int(end_version[1]) + 2)
769 end_version = '.'.join(end_version)
770 message = (f"Use of keyword argument `{old_name}` is "
771 f"deprecated and replaced by `{new_name}`. "
772 f"Support for `{old_name}` will be removed "
773 f"in SciPy {end_version}.")
774 warnings.warn(message, DeprecationWarning, stacklevel=2)
775 if new_name in kwargs:
776 message = (f"{fun.__name__}() got multiple values for "
777 f"argument now known as `{new_name}`")
778 raise TypeError(message)
779 kwargs[new_name] = kwargs.pop(old_name)
780 return fun(*args, **kwargs)
781 return wrapper
782 return decorator
785def _rng_spawn(rng, n_children):
786 # spawns independent RNGs from a parent RNG
787 bg = rng._bit_generator
788 ss = bg._seed_seq
789 child_rngs = [np.random.Generator(type(bg)(child_ss))
790 for child_ss in ss.spawn(n_children)]
791 return child_rngs
794def _get_nan(*data):
795 # Get NaN of appropriate dtype for data
796 data = [np.asarray(item) for item in data]
797 try:
798 dtype = np.result_type(*data, np.half) # must be a float16 at least
799 except DTypePromotionError:
800 # fallback to float64
801 return np.array(np.nan, dtype=np.float64)[()]
802 return np.array(np.nan, dtype=dtype)[()]
805def normalize_axis_index(axis, ndim):
806 # Check if `axis` is in the correct range and normalize it
807 if axis < -ndim or axis >= ndim:
808 msg = f"axis {axis} is out of bounds for array of dimension {ndim}"
809 raise AxisError(msg)
811 if axis < 0:
812 axis = axis + ndim
813 return axis
816def _call_callback_maybe_halt(callback, res):
817 """Call wrapped callback; return True if algorithm should stop.
819 Parameters
820 ----------
821 callback : callable or None
822 A user-provided callback wrapped with `_wrap_callback`
823 res : OptimizeResult
824 Information about the current iterate
826 Returns
827 -------
828 halt : bool
829 True if minimization should stop
831 """
832 if callback is None:
833 return False
834 try:
835 callback(res)
836 return False
837 except StopIteration:
838 callback.stop_iteration = True
839 return True
842class _RichResult(dict):
843 """ Container for multiple outputs with pretty-printing """
844 def __getattr__(self, name):
845 try:
846 return self[name]
847 except KeyError as e:
848 raise AttributeError(name) from e
850 __setattr__ = dict.__setitem__
851 __delattr__ = dict.__delitem__
853 def __repr__(self):
854 order_keys = ['message', 'success', 'status', 'fun', 'funl', 'x', 'xl',
855 'col_ind', 'nit', 'lower', 'upper', 'eqlin', 'ineqlin',
856 'converged', 'flag', 'function_calls', 'iterations',
857 'root']
858 order_keys = getattr(self, '_order_keys', order_keys)
859 # 'slack', 'con' are redundant with residuals
860 # 'crossover_nit' is probably not interesting to most users
861 omit_keys = {'slack', 'con', 'crossover_nit', '_order_keys'}
863 def key(item):
864 try:
865 return order_keys.index(item[0].lower())
866 except ValueError: # item not in list
867 return np.inf
869 def omit_redundant(items):
870 for item in items:
871 if item[0] in omit_keys:
872 continue
873 yield item
875 def item_sorter(d):
876 return sorted(omit_redundant(d.items()), key=key)
878 if self.keys():
879 return _dict_formatter(self, sorter=item_sorter)
880 else:
881 return self.__class__.__name__ + "()"
883 def __dir__(self):
884 return list(self.keys())
887def _indenter(s, n=0):
888 """
889 Ensures that lines after the first are indented by the specified amount
890 """
891 split = s.split("\n")
892 indent = " "*n
893 return ("\n" + indent).join(split)
896def _float_formatter_10(x):
897 """
898 Returns a string representation of a float with exactly ten characters
899 """
900 if np.isposinf(x):
901 return " inf"
902 elif np.isneginf(x):
903 return " -inf"
904 elif np.isnan(x):
905 return " nan"
906 return np.format_float_scientific(x, precision=3, pad_left=2, unique=False)
909def _dict_formatter(d, n=0, mplus=1, sorter=None):
910 """
911 Pretty printer for dictionaries
913 `n` keeps track of the starting indentation;
914 lines are indented by this much after a line break.
915 `mplus` is additional left padding applied to keys
916 """
917 if isinstance(d, dict):
918 m = max(map(len, list(d.keys()))) + mplus # width to print keys
919 s = '\n'.join([k.rjust(m) + ': ' + # right justified, width m
920 _indenter(_dict_formatter(v, m+n+2, 0, sorter), m+2)
921 for k, v in sorter(d)]) # +2 for ': '
922 else:
923 # By default, NumPy arrays print with linewidth=76. `n` is
924 # the indent at which a line begins printing, so it is subtracted
925 # from the default to avoid exceeding 76 characters total.
926 # `edgeitems` is the number of elements to include before and after
927 # ellipses when arrays are not shown in full.
928 # `threshold` is the maximum number of elements for which an
929 # array is shown in full.
930 # These values tend to work well for use with OptimizeResult.
931 with np.printoptions(linewidth=76-n, edgeitems=2, threshold=12,
932 formatter={'float_kind': _float_formatter_10}):
933 s = str(d)
934 return s