123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- import sys
- import copy
- import heapq
- import collections
- import functools
- import numpy as np
- from scipy._lib._util import MapWrapper, _FunctionWrapper
- class LRUDict(collections.OrderedDict):
- def __init__(self, max_size):
- self.__max_size = max_size
- def __setitem__(self, key, value):
- existing_key = (key in self)
- super().__setitem__(key, value)
- if existing_key:
- self.move_to_end(key)
- elif len(self) > self.__max_size:
- self.popitem(last=False)
- def update(self, other):
- # Not needed below
- raise NotImplementedError()
- class SemiInfiniteFunc:
- """
- Argument transform from (start, +-oo) to (0, 1)
- """
- def __init__(self, func, start, infty):
- self._func = func
- self._start = start
- self._sgn = -1 if infty < 0 else 1
- # Overflow threshold for the 1/t**2 factor
- self._tmin = sys.float_info.min**0.5
- def get_t(self, x):
- z = self._sgn * (x - self._start) + 1
- if z == 0:
- # Can happen only if point not in range
- return np.inf
- return 1 / z
- def __call__(self, t):
- if t < self._tmin:
- return 0.0
- else:
- x = self._start + self._sgn * (1 - t) / t
- f = self._func(x)
- return self._sgn * (f / t) / t
- class DoubleInfiniteFunc:
- """
- Argument transform from (-oo, oo) to (-1, 1)
- """
- def __init__(self, func):
- self._func = func
- # Overflow threshold for the 1/t**2 factor
- self._tmin = sys.float_info.min**0.5
- def get_t(self, x):
- s = -1 if x < 0 else 1
- return s / (abs(x) + 1)
- def __call__(self, t):
- if abs(t) < self._tmin:
- return 0.0
- else:
- x = (1 - abs(t)) / t
- f = self._func(x)
- return (f / t) / t
- def _max_norm(x):
- return np.amax(abs(x))
- def _get_sizeof(obj):
- try:
- return sys.getsizeof(obj)
- except TypeError:
- # occurs on pypy
- if hasattr(obj, '__sizeof__'):
- return int(obj.__sizeof__())
- return 64
- class _Bunch:
- def __init__(self, **kwargs):
- self.__keys = kwargs.keys()
- self.__dict__.update(**kwargs)
- def __repr__(self):
- return "_Bunch({})".format(", ".join("{}={}".format(k, repr(self.__dict__[k]))
- for k in self.__keys))
- def quad_vec(f, a, b, epsabs=1e-200, epsrel=1e-8, norm='2', cache_size=100e6, limit=10000,
- workers=1, points=None, quadrature=None, full_output=False,
- *, args=()):
- r"""Adaptive integration of a vector-valued function.
- Parameters
- ----------
- f : callable
- Vector-valued function f(x) to integrate.
- a : float
- Initial point.
- b : float
- Final point.
- epsabs : float, optional
- Absolute tolerance.
- epsrel : float, optional
- Relative tolerance.
- norm : {'max', '2'}, optional
- Vector norm to use for error estimation.
- cache_size : int, optional
- Number of bytes to use for memoization.
- limit : float or int, optional
- An upper bound on the number of subintervals used in the adaptive
- algorithm.
- workers : int or map-like callable, optional
- If `workers` is an integer, part of the computation is done in
- parallel subdivided to this many tasks (using
- :class:`python:multiprocessing.pool.Pool`).
- Supply `-1` to use all cores available to the Process.
- Alternatively, supply a map-like callable, such as
- :meth:`python:multiprocessing.pool.Pool.map` for evaluating the
- population in parallel.
- This evaluation is carried out as ``workers(func, iterable)``.
- points : list, optional
- List of additional breakpoints.
- quadrature : {'gk21', 'gk15', 'trapezoid'}, optional
- Quadrature rule to use on subintervals.
- Options: 'gk21' (Gauss-Kronrod 21-point rule),
- 'gk15' (Gauss-Kronrod 15-point rule),
- 'trapezoid' (composite trapezoid rule).
- Default: 'gk21' for finite intervals and 'gk15' for (semi-)infinite
- full_output : bool, optional
- Return an additional ``info`` dictionary.
- args : tuple, optional
- Extra arguments to pass to function, if any.
- .. versionadded:: 1.8.0
- Returns
- -------
- res : {float, array-like}
- Estimate for the result
- err : float
- Error estimate for the result in the given norm
- info : dict
- Returned only when ``full_output=True``.
- Info dictionary. Is an object with the attributes:
- success : bool
- Whether integration reached target precision.
- status : int
- Indicator for convergence, success (0),
- failure (1), and failure due to rounding error (2).
- neval : int
- Number of function evaluations.
- intervals : ndarray, shape (num_intervals, 2)
- Start and end points of subdivision intervals.
- integrals : ndarray, shape (num_intervals, ...)
- Integral for each interval.
- Note that at most ``cache_size`` values are recorded,
- and the array may contains *nan* for missing items.
- errors : ndarray, shape (num_intervals,)
- Estimated integration error for each interval.
- Notes
- -----
- The algorithm mainly follows the implementation of QUADPACK's
- DQAG* algorithms, implementing global error control and adaptive
- subdivision.
- The algorithm here has some differences to the QUADPACK approach:
- Instead of subdividing one interval at a time, the algorithm
- subdivides N intervals with largest errors at once. This enables
- (partial) parallelization of the integration.
- The logic of subdividing "next largest" intervals first is then
- not implemented, and we rely on the above extension to avoid
- concentrating on "small" intervals only.
- The Wynn epsilon table extrapolation is not used (QUADPACK uses it
- for infinite intervals). This is because the algorithm here is
- supposed to work on vector-valued functions, in an user-specified
- norm, and the extension of the epsilon algorithm to this case does
- not appear to be widely agreed. For max-norm, using elementwise
- Wynn epsilon could be possible, but we do not do this here with
- the hope that the epsilon extrapolation is mainly useful in
- special cases.
- References
- ----------
- [1] R. Piessens, E. de Doncker, QUADPACK (1983).
- Examples
- --------
- We can compute integrations of a vector-valued function:
- >>> from scipy.integrate import quad_vec
- >>> import numpy as np
- >>> import matplotlib.pyplot as plt
- >>> alpha = np.linspace(0.0, 2.0, num=30)
- >>> f = lambda x: x**alpha
- >>> x0, x1 = 0, 2
- >>> y, err = quad_vec(f, x0, x1)
- >>> plt.plot(alpha, y)
- >>> plt.xlabel(r"$\alpha$")
- >>> plt.ylabel(r"$\int_{0}^{2} x^\alpha dx$")
- >>> plt.show()
- """
- a = float(a)
- b = float(b)
- if args:
- if not isinstance(args, tuple):
- args = (args,)
- # create a wrapped function to allow the use of map and Pool.map
- f = _FunctionWrapper(f, args)
- # Use simple transformations to deal with integrals over infinite
- # intervals.
- kwargs = dict(epsabs=epsabs,
- epsrel=epsrel,
- norm=norm,
- cache_size=cache_size,
- limit=limit,
- workers=workers,
- points=points,
- quadrature='gk15' if quadrature is None else quadrature,
- full_output=full_output)
- if np.isfinite(a) and np.isinf(b):
- f2 = SemiInfiniteFunc(f, start=a, infty=b)
- if points is not None:
- kwargs['points'] = tuple(f2.get_t(xp) for xp in points)
- return quad_vec(f2, 0, 1, **kwargs)
- elif np.isfinite(b) and np.isinf(a):
- f2 = SemiInfiniteFunc(f, start=b, infty=a)
- if points is not None:
- kwargs['points'] = tuple(f2.get_t(xp) for xp in points)
- res = quad_vec(f2, 0, 1, **kwargs)
- return (-res[0],) + res[1:]
- elif np.isinf(a) and np.isinf(b):
- sgn = -1 if b < a else 1
- # NB. explicitly split integral at t=0, which separates
- # the positive and negative sides
- f2 = DoubleInfiniteFunc(f)
- if points is not None:
- kwargs['points'] = (0,) + tuple(f2.get_t(xp) for xp in points)
- else:
- kwargs['points'] = (0,)
- if a != b:
- res = quad_vec(f2, -1, 1, **kwargs)
- else:
- res = quad_vec(f2, 1, 1, **kwargs)
- return (res[0]*sgn,) + res[1:]
- elif not (np.isfinite(a) and np.isfinite(b)):
- raise ValueError("invalid integration bounds a={}, b={}".format(a, b))
- norm_funcs = {
- None: _max_norm,
- 'max': _max_norm,
- '2': np.linalg.norm
- }
- if callable(norm):
- norm_func = norm
- else:
- norm_func = norm_funcs[norm]
- parallel_count = 128
- min_intervals = 2
- try:
- _quadrature = {None: _quadrature_gk21,
- 'gk21': _quadrature_gk21,
- 'gk15': _quadrature_gk15,
- 'trapz': _quadrature_trapezoid, # alias for backcompat
- 'trapezoid': _quadrature_trapezoid}[quadrature]
- except KeyError as e:
- raise ValueError("unknown quadrature {!r}".format(quadrature)) from e
- # Initial interval set
- if points is None:
- initial_intervals = [(a, b)]
- else:
- prev = a
- initial_intervals = []
- for p in sorted(points):
- p = float(p)
- if not (a < p < b) or p == prev:
- continue
- initial_intervals.append((prev, p))
- prev = p
- initial_intervals.append((prev, b))
- global_integral = None
- global_error = None
- rounding_error = None
- interval_cache = None
- intervals = []
- neval = 0
- for x1, x2 in initial_intervals:
- ig, err, rnd = _quadrature(x1, x2, f, norm_func)
- neval += _quadrature.num_eval
- if global_integral is None:
- if isinstance(ig, (float, complex)):
- # Specialize for scalars
- if norm_func in (_max_norm, np.linalg.norm):
- norm_func = abs
- global_integral = ig
- global_error = float(err)
- rounding_error = float(rnd)
- cache_count = cache_size // _get_sizeof(ig)
- interval_cache = LRUDict(cache_count)
- else:
- global_integral += ig
- global_error += err
- rounding_error += rnd
- interval_cache[(x1, x2)] = copy.copy(ig)
- intervals.append((-err, x1, x2))
- heapq.heapify(intervals)
- CONVERGED = 0
- NOT_CONVERGED = 1
- ROUNDING_ERROR = 2
- NOT_A_NUMBER = 3
- status_msg = {
- CONVERGED: "Target precision reached.",
- NOT_CONVERGED: "Target precision not reached.",
- ROUNDING_ERROR: "Target precision could not be reached due to rounding error.",
- NOT_A_NUMBER: "Non-finite values encountered."
- }
- # Process intervals
- with MapWrapper(workers) as mapwrapper:
- ier = NOT_CONVERGED
- while intervals and len(intervals) < limit:
- # Select intervals with largest errors for subdivision
- tol = max(epsabs, epsrel*norm_func(global_integral))
- to_process = []
- err_sum = 0
- for j in range(parallel_count):
- if not intervals:
- break
- if j > 0 and err_sum > global_error - tol/8:
- # avoid unnecessary parallel splitting
- break
- interval = heapq.heappop(intervals)
- neg_old_err, a, b = interval
- old_int = interval_cache.pop((a, b), None)
- to_process.append(((-neg_old_err, a, b, old_int), f, norm_func, _quadrature))
- err_sum += -neg_old_err
- # Subdivide intervals
- for dint, derr, dround_err, subint, dneval in mapwrapper(_subdivide_interval, to_process):
- neval += dneval
- global_integral += dint
- global_error += derr
- rounding_error += dround_err
- for x in subint:
- x1, x2, ig, err = x
- interval_cache[(x1, x2)] = ig
- heapq.heappush(intervals, (-err, x1, x2))
- # Termination check
- if len(intervals) >= min_intervals:
- tol = max(epsabs, epsrel*norm_func(global_integral))
- if global_error < tol/8:
- ier = CONVERGED
- break
- if global_error < rounding_error:
- ier = ROUNDING_ERROR
- break
- if not (np.isfinite(global_error) and np.isfinite(rounding_error)):
- ier = NOT_A_NUMBER
- break
- res = global_integral
- err = global_error + rounding_error
- if full_output:
- res_arr = np.asarray(res)
- dummy = np.full(res_arr.shape, np.nan, dtype=res_arr.dtype)
- integrals = np.array([interval_cache.get((z[1], z[2]), dummy)
- for z in intervals], dtype=res_arr.dtype)
- errors = np.array([-z[0] for z in intervals])
- intervals = np.array([[z[1], z[2]] for z in intervals])
- info = _Bunch(neval=neval,
- success=(ier == CONVERGED),
- status=ier,
- message=status_msg[ier],
- intervals=intervals,
- integrals=integrals,
- errors=errors)
- return (res, err, info)
- else:
- return (res, err)
- def _subdivide_interval(args):
- interval, f, norm_func, _quadrature = args
- old_err, a, b, old_int = interval
- c = 0.5 * (a + b)
- # Left-hand side
- if getattr(_quadrature, 'cache_size', 0) > 0:
- f = functools.lru_cache(_quadrature.cache_size)(f)
- s1, err1, round1 = _quadrature(a, c, f, norm_func)
- dneval = _quadrature.num_eval
- s2, err2, round2 = _quadrature(c, b, f, norm_func)
- dneval += _quadrature.num_eval
- if old_int is None:
- old_int, _, _ = _quadrature(a, b, f, norm_func)
- dneval += _quadrature.num_eval
- if getattr(_quadrature, 'cache_size', 0) > 0:
- dneval = f.cache_info().misses
- dint = s1 + s2 - old_int
- derr = err1 + err2 - old_err
- dround_err = round1 + round2
- subintervals = ((a, c, s1, err1), (c, b, s2, err2))
- return dint, derr, dround_err, subintervals, dneval
- def _quadrature_trapezoid(x1, x2, f, norm_func):
- """
- Composite trapezoid quadrature
- """
- x3 = 0.5*(x1 + x2)
- f1 = f(x1)
- f2 = f(x2)
- f3 = f(x3)
- s2 = 0.25 * (x2 - x1) * (f1 + 2*f3 + f2)
- round_err = 0.25 * abs(x2 - x1) * (float(norm_func(f1))
- + 2*float(norm_func(f3))
- + float(norm_func(f2))) * 2e-16
- s1 = 0.5 * (x2 - x1) * (f1 + f2)
- err = 1/3 * float(norm_func(s1 - s2))
- return s2, err, round_err
- _quadrature_trapezoid.cache_size = 3 * 3
- _quadrature_trapezoid.num_eval = 3
- def _quadrature_gk(a, b, f, norm_func, x, w, v):
- """
- Generic Gauss-Kronrod quadrature
- """
- fv = [0.0]*len(x)
- c = 0.5 * (a + b)
- h = 0.5 * (b - a)
- # Gauss-Kronrod
- s_k = 0.0
- s_k_abs = 0.0
- for i in range(len(x)):
- ff = f(c + h*x[i])
- fv[i] = ff
- vv = v[i]
- # \int f(x)
- s_k += vv * ff
- # \int |f(x)|
- s_k_abs += vv * abs(ff)
- # Gauss
- s_g = 0.0
- for i in range(len(w)):
- s_g += w[i] * fv[2*i + 1]
- # Quadrature of abs-deviation from average
- s_k_dabs = 0.0
- y0 = s_k / 2.0
- for i in range(len(x)):
- # \int |f(x) - y0|
- s_k_dabs += v[i] * abs(fv[i] - y0)
- # Use similar error estimation as quadpack
- err = float(norm_func((s_k - s_g) * h))
- dabs = float(norm_func(s_k_dabs * h))
- if dabs != 0 and err != 0:
- err = dabs * min(1.0, (200 * err / dabs)**1.5)
- eps = sys.float_info.epsilon
- round_err = float(norm_func(50 * eps * h * s_k_abs))
- if round_err > sys.float_info.min:
- err = max(err, round_err)
- return h * s_k, err, round_err
- def _quadrature_gk21(a, b, f, norm_func):
- """
- Gauss-Kronrod 21 quadrature with error estimate
- """
- # Gauss-Kronrod points
- x = (0.995657163025808080735527280689003,
- 0.973906528517171720077964012084452,
- 0.930157491355708226001207180059508,
- 0.865063366688984510732096688423493,
- 0.780817726586416897063717578345042,
- 0.679409568299024406234327365114874,
- 0.562757134668604683339000099272694,
- 0.433395394129247190799265943165784,
- 0.294392862701460198131126603103866,
- 0.148874338981631210884826001129720,
- 0,
- -0.148874338981631210884826001129720,
- -0.294392862701460198131126603103866,
- -0.433395394129247190799265943165784,
- -0.562757134668604683339000099272694,
- -0.679409568299024406234327365114874,
- -0.780817726586416897063717578345042,
- -0.865063366688984510732096688423493,
- -0.930157491355708226001207180059508,
- -0.973906528517171720077964012084452,
- -0.995657163025808080735527280689003)
- # 10-point weights
- w = (0.066671344308688137593568809893332,
- 0.149451349150580593145776339657697,
- 0.219086362515982043995534934228163,
- 0.269266719309996355091226921569469,
- 0.295524224714752870173892994651338,
- 0.295524224714752870173892994651338,
- 0.269266719309996355091226921569469,
- 0.219086362515982043995534934228163,
- 0.149451349150580593145776339657697,
- 0.066671344308688137593568809893332)
- # 21-point weights
- v = (0.011694638867371874278064396062192,
- 0.032558162307964727478818972459390,
- 0.054755896574351996031381300244580,
- 0.075039674810919952767043140916190,
- 0.093125454583697605535065465083366,
- 0.109387158802297641899210590325805,
- 0.123491976262065851077958109831074,
- 0.134709217311473325928054001771707,
- 0.142775938577060080797094273138717,
- 0.147739104901338491374841515972068,
- 0.149445554002916905664936468389821,
- 0.147739104901338491374841515972068,
- 0.142775938577060080797094273138717,
- 0.134709217311473325928054001771707,
- 0.123491976262065851077958109831074,
- 0.109387158802297641899210590325805,
- 0.093125454583697605535065465083366,
- 0.075039674810919952767043140916190,
- 0.054755896574351996031381300244580,
- 0.032558162307964727478818972459390,
- 0.011694638867371874278064396062192)
- return _quadrature_gk(a, b, f, norm_func, x, w, v)
- _quadrature_gk21.num_eval = 21
- def _quadrature_gk15(a, b, f, norm_func):
- """
- Gauss-Kronrod 15 quadrature with error estimate
- """
- # Gauss-Kronrod points
- x = (0.991455371120812639206854697526329,
- 0.949107912342758524526189684047851,
- 0.864864423359769072789712788640926,
- 0.741531185599394439863864773280788,
- 0.586087235467691130294144838258730,
- 0.405845151377397166906606412076961,
- 0.207784955007898467600689403773245,
- 0.000000000000000000000000000000000,
- -0.207784955007898467600689403773245,
- -0.405845151377397166906606412076961,
- -0.586087235467691130294144838258730,
- -0.741531185599394439863864773280788,
- -0.864864423359769072789712788640926,
- -0.949107912342758524526189684047851,
- -0.991455371120812639206854697526329)
- # 7-point weights
- w = (0.129484966168869693270611432679082,
- 0.279705391489276667901467771423780,
- 0.381830050505118944950369775488975,
- 0.417959183673469387755102040816327,
- 0.381830050505118944950369775488975,
- 0.279705391489276667901467771423780,
- 0.129484966168869693270611432679082)
- # 15-point weights
- v = (0.022935322010529224963732008058970,
- 0.063092092629978553290700663189204,
- 0.104790010322250183839876322541518,
- 0.140653259715525918745189590510238,
- 0.169004726639267902826583426598550,
- 0.190350578064785409913256402421014,
- 0.204432940075298892414161999234649,
- 0.209482141084727828012999174891714,
- 0.204432940075298892414161999234649,
- 0.190350578064785409913256402421014,
- 0.169004726639267902826583426598550,
- 0.140653259715525918745189590510238,
- 0.104790010322250183839876322541518,
- 0.063092092629978553290700663189204,
- 0.022935322010529224963732008058970)
- return _quadrature_gk(a, b, f, norm_func, x, w, v)
- _quadrature_gk15.num_eval = 15
|