|
import math |
|
import heapq |
|
import itertools |
|
|
|
from dataclasses import dataclass, field |
|
from types import ModuleType |
|
from typing import Any, TypeAlias |
|
|
|
from scipy._lib._array_api import ( |
|
array_namespace, |
|
xp_size, |
|
xp_copy, |
|
xp_broadcast_promote |
|
) |
|
from scipy._lib._util import MapWrapper |
|
|
|
from scipy.integrate._rules import ( |
|
ProductNestedFixed, |
|
GaussKronrodQuadrature, |
|
GenzMalikCubature, |
|
) |
|
from scipy.integrate._rules._base import _split_subregion |
|
|
|
__all__ = ['cubature'] |
|
|
|
Array: TypeAlias = Any |
|
|
|
|
|
@dataclass |
|
class CubatureRegion: |
|
estimate: Array |
|
error: Array |
|
a: Array |
|
b: Array |
|
_xp: ModuleType = field(repr=False) |
|
|
|
def __lt__(self, other): |
|
|
|
|
|
|
|
|
|
this_err = self._xp.max(self._xp.abs(self.error)) |
|
other_err = self._xp.max(self._xp.abs(other.error)) |
|
|
|
return this_err > other_err |
|
|
|
|
|
@dataclass |
|
class CubatureResult: |
|
estimate: Array |
|
error: Array |
|
status: str |
|
regions: list[CubatureRegion] |
|
subdivisions: int |
|
atol: float |
|
rtol: float |
|
|
|
|
|
def cubature(f, a, b, *, rule="gk21", rtol=1e-8, atol=0, max_subdivisions=10000, |
|
args=(), workers=1, points=None): |
|
r""" |
|
Adaptive cubature of multidimensional array-valued function. |
|
|
|
Given an arbitrary integration rule, this function returns an estimate of the |
|
integral to the requested tolerance over the region defined by the arrays `a` and |
|
`b` specifying the corners of a hypercube. |
|
|
|
Convergence is not guaranteed for all integrals. |
|
|
|
Parameters |
|
---------- |
|
f : callable |
|
Function to integrate. `f` must have the signature:: |
|
|
|
f(x : ndarray, *args) -> ndarray |
|
|
|
`f` should accept arrays ``x`` of shape:: |
|
|
|
(npoints, ndim) |
|
|
|
and output arrays of shape:: |
|
|
|
(npoints, output_dim_1, ..., output_dim_n) |
|
|
|
In this case, `cubature` will return arrays of shape:: |
|
|
|
(output_dim_1, ..., output_dim_n) |
|
a, b : array_like |
|
Lower and upper limits of integration as 1D arrays specifying the left and right |
|
endpoints of the intervals being integrated over. Limits can be infinite. |
|
rule : str, optional |
|
Rule used to estimate the integral. If passing a string, the options are |
|
"gauss-kronrod" (21 node), or "genz-malik" (degree 7). If a rule like |
|
"gauss-kronrod" is specified for an ``n``-dim integrand, the corresponding |
|
Cartesian product rule is used. "gk21", "gk15" are also supported for |
|
compatibility with `quad_vec`. See Notes. |
|
rtol, atol : float, optional |
|
Relative and absolute tolerances. Iterations are performed until the error is |
|
estimated to be less than ``atol + rtol * abs(est)``. Here `rtol` controls |
|
relative accuracy (number of correct digits), while `atol` controls absolute |
|
accuracy (number of correct decimal places). To achieve the desired `rtol`, set |
|
`atol` to be smaller than the smallest value that can be expected from |
|
``rtol * abs(y)`` so that `rtol` dominates the allowable error. If `atol` is |
|
larger than ``rtol * abs(y)`` the number of correct digits is not guaranteed. |
|
Conversely, to achieve the desired `atol`, set `rtol` such that |
|
``rtol * abs(y)`` is always smaller than `atol`. Default values are 1e-8 for |
|
`rtol` and 0 for `atol`. |
|
max_subdivisions : int, optional |
|
Upper bound on the number of subdivisions to perform. Default is 10,000. |
|
args : tuple, optional |
|
Additional positional args passed to `f`, if any. |
|
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 of array_like, optional |
|
List of points to avoid evaluating `f` at, under the condition that the rule |
|
being used does not evaluate `f` on the boundary of a region (which is the |
|
case for all Genz-Malik and Gauss-Kronrod rules). This can be useful if `f` has |
|
a singularity at the specified point. This should be a list of array-likes where |
|
each element has length ``ndim``. Default is empty. See Examples. |
|
|
|
Returns |
|
------- |
|
res : object |
|
Object containing the results of the estimation. It has the following |
|
attributes: |
|
|
|
estimate : ndarray |
|
Estimate of the value of the integral over the overall region specified. |
|
error : ndarray |
|
Estimate of the error of the approximation over the overall region |
|
specified. |
|
status : str |
|
Whether the estimation was successful. Can be either: "converged", |
|
"not_converged". |
|
subdivisions : int |
|
Number of subdivisions performed. |
|
atol, rtol : float |
|
Requested tolerances for the approximation. |
|
regions: list of object |
|
List of objects containing the estimates of the integral over smaller |
|
regions of the domain. |
|
|
|
Each object in ``regions`` has the following attributes: |
|
|
|
a, b : ndarray |
|
Points describing the corners of the region. If the original integral |
|
contained infinite limits or was over a region described by `region`, |
|
then `a` and `b` are in the transformed coordinates. |
|
estimate : ndarray |
|
Estimate of the value of the integral over this region. |
|
error : ndarray |
|
Estimate of the error of the approximation over this region. |
|
|
|
Notes |
|
----- |
|
The algorithm uses a similar algorithm to `quad_vec`, which itself is based on the |
|
implementation of QUADPACK's DQAG* algorithms, implementing global error control and |
|
adaptive subdivision. |
|
|
|
The source of the nodes and weights used for Gauss-Kronrod quadrature can be found |
|
in [1]_, and the algorithm for calculating the nodes and weights in Genz-Malik |
|
cubature can be found in [2]_. |
|
|
|
The rules currently supported via the `rule` argument are: |
|
|
|
- ``"gauss-kronrod"``, 21-node Gauss-Kronrod |
|
- ``"genz-malik"``, n-node Genz-Malik |
|
|
|
If using Gauss-Kronrod for an ``n``-dim integrand where ``n > 2``, then the |
|
corresponding Cartesian product rule will be found by taking the Cartesian product |
|
of the nodes in the 1D case. This means that the number of nodes scales |
|
exponentially as ``21^n`` in the Gauss-Kronrod case, which may be problematic in a |
|
moderate number of dimensions. |
|
|
|
Genz-Malik is typically less accurate than Gauss-Kronrod but has much fewer nodes, |
|
so in this situation using "genz-malik" might be preferable. |
|
|
|
Infinite limits are handled with an appropriate variable transformation. Assuming |
|
``a = [a_1, ..., a_n]`` and ``b = [b_1, ..., b_n]``: |
|
|
|
If :math:`a_i = -\infty` and :math:`b_i = \infty`, the i-th integration variable |
|
will use the transformation :math:`x = \frac{1-|t|}{t}` and :math:`t \in (-1, 1)`. |
|
|
|
If :math:`a_i \ne \pm\infty` and :math:`b_i = \infty`, the i-th integration variable |
|
will use the transformation :math:`x = a_i + \frac{1-t}{t}` and |
|
:math:`t \in (0, 1)`. |
|
|
|
If :math:`a_i = -\infty` and :math:`b_i \ne \pm\infty`, the i-th integration |
|
variable will use the transformation :math:`x = b_i - \frac{1-t}{t}` and |
|
:math:`t \in (0, 1)`. |
|
|
|
References |
|
---------- |
|
.. [1] R. Piessens, E. de Doncker, Quadpack: A Subroutine Package for Automatic |
|
Integration, files: dqk21.f, dqk15.f (1983). |
|
|
|
.. [2] A.C. Genz, A.A. Malik, Remarks on algorithm 006: An adaptive algorithm for |
|
numerical integration over an N-dimensional rectangular region, Journal of |
|
Computational and Applied Mathematics, Volume 6, Issue 4, 1980, Pages 295-302, |
|
ISSN 0377-0427 |
|
:doi:`10.1016/0771-050X(80)90039-X` |
|
|
|
Examples |
|
-------- |
|
**1D integral with vector output**: |
|
|
|
.. math:: |
|
|
|
\int^1_0 \mathbf f(x) \text dx |
|
|
|
Where ``f(x) = x^n`` and ``n = np.arange(10)`` is a vector. Since no rule is |
|
specified, the default "gk21" is used, which corresponds to Gauss-Kronrod |
|
integration with 21 nodes. |
|
|
|
>>> import numpy as np |
|
>>> from scipy.integrate import cubature |
|
>>> def f(x, n): |
|
... # Make sure x and n are broadcastable |
|
... return x[:, np.newaxis]**n[np.newaxis, :] |
|
>>> res = cubature( |
|
... f, |
|
... a=[0], |
|
... b=[1], |
|
... args=(np.arange(10),), |
|
... ) |
|
>>> res.estimate |
|
array([1. , 0.5 , 0.33333333, 0.25 , 0.2 , |
|
0.16666667, 0.14285714, 0.125 , 0.11111111, 0.1 ]) |
|
|
|
**7D integral with arbitrary-shaped array output**:: |
|
|
|
f(x) = cos(2*pi*r + alphas @ x) |
|
|
|
for some ``r`` and ``alphas``, and the integral is performed over the unit |
|
hybercube, :math:`[0, 1]^7`. Since the integral is in a moderate number of |
|
dimensions, "genz-malik" is used rather than the default "gauss-kronrod" to |
|
avoid constructing a product rule with :math:`21^7 \approx 2 \times 10^9` nodes. |
|
|
|
>>> import numpy as np |
|
>>> from scipy.integrate import cubature |
|
>>> def f(x, r, alphas): |
|
... # f(x) = cos(2*pi*r + alphas @ x) |
|
... # Need to allow r and alphas to be arbitrary shape |
|
... npoints, ndim = x.shape[0], x.shape[-1] |
|
... alphas = alphas[np.newaxis, ...] |
|
... x = x.reshape(npoints, *([1]*(len(alphas.shape) - 1)), ndim) |
|
... return np.cos(2*np.pi*r + np.sum(alphas * x, axis=-1)) |
|
>>> rng = np.random.default_rng() |
|
>>> r, alphas = rng.random((2, 3)), rng.random((2, 3, 7)) |
|
>>> res = cubature( |
|
... f=f, |
|
... a=np.array([0, 0, 0, 0, 0, 0, 0]), |
|
... b=np.array([1, 1, 1, 1, 1, 1, 1]), |
|
... rtol=1e-5, |
|
... rule="genz-malik", |
|
... args=(r, alphas), |
|
... ) |
|
>>> res.estimate |
|
array([[-0.79812452, 0.35246913, -0.52273628], |
|
[ 0.88392779, 0.59139899, 0.41895111]]) |
|
|
|
**Parallel computation with** `workers`: |
|
|
|
>>> from concurrent.futures import ThreadPoolExecutor |
|
>>> with ThreadPoolExecutor() as executor: |
|
... res = cubature( |
|
... f=f, |
|
... a=np.array([0, 0, 0, 0, 0, 0, 0]), |
|
... b=np.array([1, 1, 1, 1, 1, 1, 1]), |
|
... rtol=1e-5, |
|
... rule="genz-malik", |
|
... args=(r, alphas), |
|
... workers=executor.map, |
|
... ) |
|
>>> res.estimate |
|
array([[-0.79812452, 0.35246913, -0.52273628], |
|
[ 0.88392779, 0.59139899, 0.41895111]]) |
|
|
|
**2D integral with infinite limits**: |
|
|
|
.. math:: |
|
|
|
\int^{ \infty }_{ -\infty } |
|
\int^{ \infty }_{ -\infty } |
|
e^{-x^2-y^2} |
|
\text dy |
|
\text dx |
|
|
|
>>> def gaussian(x): |
|
... return np.exp(-np.sum(x**2, axis=-1)) |
|
>>> res = cubature(gaussian, [-np.inf, -np.inf], [np.inf, np.inf]) |
|
>>> res.estimate |
|
3.1415926 |
|
|
|
**1D integral with singularities avoided using** `points`: |
|
|
|
.. math:: |
|
|
|
\int^{ 1 }_{ -1 } |
|
\frac{\sin(x)}{x} |
|
\text dx |
|
|
|
It is necessary to use the `points` parameter to avoid evaluating `f` at the origin. |
|
|
|
>>> def sinc(x): |
|
... return np.sin(x)/x |
|
>>> res = cubature(sinc, [-1], [1], points=[[0]]) |
|
>>> res.estimate |
|
1.8921661 |
|
""" |
|
|
|
|
|
|
|
|
|
xp = array_namespace(a, b) |
|
max_subdivisions = float("inf") if max_subdivisions is None else max_subdivisions |
|
points = [] if points is None else points |
|
|
|
|
|
|
|
a, b, *points = xp_broadcast_promote(a, b, *points, force_floating=True) |
|
result_dtype = a.dtype |
|
|
|
if xp_size(a) == 0 or xp_size(b) == 0: |
|
raise ValueError("`a` and `b` must be nonempty") |
|
|
|
if a.ndim != 1 or b.ndim != 1: |
|
raise ValueError("`a` and `b` must be 1D arrays") |
|
|
|
|
|
if isinstance(rule, str): |
|
ndim = xp_size(a) |
|
|
|
if rule == "genz-malik": |
|
rule = GenzMalikCubature(ndim, xp=xp) |
|
else: |
|
quadratues = { |
|
"gauss-kronrod": GaussKronrodQuadrature(21, xp=xp), |
|
|
|
|
|
"gk21": GaussKronrodQuadrature(21, xp=xp), |
|
"gk15": GaussKronrodQuadrature(15, xp=xp), |
|
} |
|
|
|
base_rule = quadratues.get(rule) |
|
|
|
if base_rule is None: |
|
raise ValueError(f"unknown rule {rule}") |
|
|
|
rule = ProductNestedFixed([base_rule] * ndim) |
|
|
|
|
|
|
|
sign = (-1) ** xp.sum(xp.astype(a > b, xp.int8), dtype=result_dtype) |
|
|
|
a_flipped = xp.min(xp.stack([a, b]), axis=0) |
|
b_flipped = xp.max(xp.stack([a, b]), axis=0) |
|
|
|
a, b = a_flipped, b_flipped |
|
|
|
|
|
if xp.any(xp.isinf(a)) or xp.any(xp.isinf(b)): |
|
f = _InfiniteLimitsTransform(f, a, b, xp=xp) |
|
a, b = f.transformed_limits |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
points = [xp.reshape(point, (1, -1)) for point in points] |
|
points = [f.inv(point) for point in points] |
|
points = [xp.reshape(point, (-1,)) for point in points] |
|
|
|
|
|
points.extend(f.points) |
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(points) == 0: |
|
initial_regions = [(a, b)] |
|
else: |
|
initial_regions = _split_region_at_points(a, b, points, xp) |
|
|
|
regions = [] |
|
est = 0.0 |
|
err = 0.0 |
|
|
|
for a_k, b_k in initial_regions: |
|
est_k = rule.estimate(f, a_k, b_k, args) |
|
err_k = rule.estimate_error(f, a_k, b_k, args) |
|
regions.append(CubatureRegion(est_k, err_k, a_k, b_k, xp)) |
|
|
|
est += est_k |
|
err += err_k |
|
|
|
subdivisions = 0 |
|
success = True |
|
|
|
with MapWrapper(workers) as mapwrapper: |
|
while xp.any(err > atol + rtol * xp.abs(est)): |
|
|
|
region_k = heapq.heappop(regions) |
|
|
|
est_k = region_k.estimate |
|
err_k = region_k.error |
|
|
|
a_k, b_k = region_k.a, region_k.b |
|
|
|
|
|
|
|
|
|
est -= est_k |
|
err -= err_k |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
executor_args = zip( |
|
itertools.repeat(f), |
|
itertools.repeat(rule), |
|
itertools.repeat(args), |
|
_split_subregion(a_k, b_k, xp), |
|
) |
|
|
|
for subdivision_result in mapwrapper(_process_subregion, executor_args): |
|
a_k_sub, b_k_sub, est_sub, err_sub = subdivision_result |
|
|
|
est += est_sub |
|
err += err_sub |
|
|
|
new_region = CubatureRegion(est_sub, err_sub, a_k_sub, b_k_sub, xp) |
|
|
|
heapq.heappush(regions, new_region) |
|
|
|
subdivisions += 1 |
|
|
|
if subdivisions >= max_subdivisions: |
|
success = False |
|
break |
|
|
|
status = "converged" if success else "not_converged" |
|
|
|
|
|
est = sign * est |
|
|
|
return CubatureResult( |
|
estimate=est, |
|
error=err, |
|
status=status, |
|
subdivisions=subdivisions, |
|
regions=regions, |
|
atol=atol, |
|
rtol=rtol, |
|
) |
|
|
|
|
|
def _process_subregion(data): |
|
f, rule, args, coord = data |
|
a_k_sub, b_k_sub = coord |
|
|
|
est_sub = rule.estimate(f, a_k_sub, b_k_sub, args) |
|
err_sub = rule.estimate_error(f, a_k_sub, b_k_sub, args) |
|
|
|
return a_k_sub, b_k_sub, est_sub, err_sub |
|
|
|
|
|
def _is_strictly_in_region(a, b, point, xp): |
|
if xp.all(point == a) or xp.all(point == b): |
|
return False |
|
|
|
return xp.all(a <= point) and xp.all(point <= b) |
|
|
|
|
|
def _split_region_at_points(a, b, points, xp): |
|
""" |
|
Given the integration limits `a` and `b` describing a rectangular region and a list |
|
of `points`, find the list of ``[(a_1, b_1), ..., (a_l, b_l)]`` which breaks up the |
|
initial region into smaller subregion such that no `points` lie strictly inside |
|
any of the subregions. |
|
""" |
|
|
|
regions = [(a, b)] |
|
|
|
for point in points: |
|
if xp.any(xp.isinf(point)): |
|
|
|
|
|
|
|
|
|
continue |
|
|
|
new_subregions = [] |
|
|
|
for a_k, b_k in regions: |
|
if _is_strictly_in_region(a_k, b_k, point, xp): |
|
subregions = _split_subregion(a_k, b_k, xp, point) |
|
|
|
for left, right in subregions: |
|
|
|
if xp.any(left == right): |
|
continue |
|
else: |
|
new_subregions.append((left, right)) |
|
|
|
new_subregions.extend(subregions) |
|
|
|
else: |
|
new_subregions.append((a_k, b_k)) |
|
|
|
regions = new_subregions |
|
|
|
return regions |
|
|
|
|
|
class _VariableTransform: |
|
""" |
|
A transformation that can be applied to an integral. |
|
""" |
|
|
|
@property |
|
def transformed_limits(self): |
|
""" |
|
New limits of integration after applying the transformation. |
|
""" |
|
|
|
raise NotImplementedError |
|
|
|
@property |
|
def points(self): |
|
""" |
|
Any problematic points introduced by the transformation. |
|
|
|
These should be specified as points where ``_VariableTransform(f)(self, point)`` |
|
would be problematic. |
|
|
|
For example, if the transformation ``x = 1/((1-t)(1+t))`` is applied to a |
|
univariate integral, then points should return ``[ [1], [-1] ]``. |
|
""" |
|
|
|
return [] |
|
|
|
def inv(self, x): |
|
""" |
|
Map points ``x`` to ``t`` such that if ``f`` is the original function and ``g`` |
|
is the function after the transformation is applied, then:: |
|
|
|
f(x) = g(self.inv(x)) |
|
""" |
|
|
|
raise NotImplementedError |
|
|
|
def __call__(self, t, *args, **kwargs): |
|
""" |
|
Apply the transformation to ``f`` and multiply by the Jacobian determinant. |
|
This should be the new integrand after the transformation has been applied so |
|
that the following is satisfied:: |
|
|
|
f_transformed = _VariableTransform(f) |
|
|
|
cubature(f, a, b) == cubature( |
|
f_transformed, |
|
*f_transformed.transformed_limits(a, b), |
|
) |
|
""" |
|
|
|
raise NotImplementedError |
|
|
|
|
|
class _InfiniteLimitsTransform(_VariableTransform): |
|
r""" |
|
Transformation for handling infinite limits. |
|
|
|
Assuming ``a = [a_1, ..., a_n]`` and ``b = [b_1, ..., b_n]``: |
|
|
|
If :math:`a_i = -\infty` and :math:`b_i = \infty`, the i-th integration variable |
|
will use the transformation :math:`x = \frac{1-|t|}{t}` and :math:`t \in (-1, 1)`. |
|
|
|
If :math:`a_i \ne \pm\infty` and :math:`b_i = \infty`, the i-th integration variable |
|
will use the transformation :math:`x = a_i + \frac{1-t}{t}` and |
|
:math:`t \in (0, 1)`. |
|
|
|
If :math:`a_i = -\infty` and :math:`b_i \ne \pm\infty`, the i-th integration |
|
variable will use the transformation :math:`x = b_i - \frac{1-t}{t}` and |
|
:math:`t \in (0, 1)`. |
|
""" |
|
|
|
def __init__(self, f, a, b, xp): |
|
self._xp = xp |
|
|
|
self._f = f |
|
self._orig_a = a |
|
self._orig_b = b |
|
|
|
|
|
self._double_inf_pos = (a == -math.inf) & (b == math.inf) |
|
|
|
|
|
start_inf_mask = (a != -math.inf) & (b == math.inf) |
|
|
|
|
|
inf_end_mask = (a == -math.inf) & (b != math.inf) |
|
|
|
|
|
|
|
self._semi_inf_pos = start_inf_mask | inf_end_mask |
|
|
|
|
|
|
|
self._orig_a[inf_end_mask] = -b[inf_end_mask] |
|
self._orig_b[inf_end_mask] = -a[inf_end_mask] |
|
|
|
self._num_inf = self._xp.sum( |
|
self._xp.astype(self._double_inf_pos | self._semi_inf_pos, self._xp.int64), |
|
).__int__() |
|
|
|
@property |
|
def transformed_limits(self): |
|
a = xp_copy(self._orig_a) |
|
b = xp_copy(self._orig_b) |
|
|
|
a[self._double_inf_pos] = -1 |
|
b[self._double_inf_pos] = 1 |
|
|
|
a[self._semi_inf_pos] = 0 |
|
b[self._semi_inf_pos] = 1 |
|
|
|
return a, b |
|
|
|
@property |
|
def points(self): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self._num_inf != 0: |
|
return [self._xp.zeros(self._orig_a.shape)] |
|
else: |
|
return [] |
|
|
|
def inv(self, x): |
|
t = xp_copy(x) |
|
npoints = x.shape[0] |
|
|
|
double_inf_mask = self._xp.tile( |
|
self._double_inf_pos[self._xp.newaxis, :], |
|
(npoints, 1), |
|
) |
|
|
|
semi_inf_mask = self._xp.tile( |
|
self._semi_inf_pos[self._xp.newaxis, :], |
|
(npoints, 1), |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
zero_mask = x[double_inf_mask] == 0 |
|
non_zero_mask = double_inf_mask & ~zero_mask |
|
t[zero_mask] = math.inf |
|
t[non_zero_mask] = 1/(x[non_zero_mask] + self._xp.sign(x[non_zero_mask])) |
|
|
|
start = self._xp.tile(self._orig_a[self._semi_inf_pos], (npoints,)) |
|
t[semi_inf_mask] = 1/(x[semi_inf_mask] - start + 1) |
|
|
|
return t |
|
|
|
def __call__(self, t, *args, **kwargs): |
|
x = xp_copy(t) |
|
npoints = t.shape[0] |
|
|
|
double_inf_mask = self._xp.tile( |
|
self._double_inf_pos[self._xp.newaxis, :], |
|
(npoints, 1), |
|
) |
|
|
|
semi_inf_mask = self._xp.tile( |
|
self._semi_inf_pos[self._xp.newaxis, :], |
|
(npoints, 1), |
|
) |
|
|
|
|
|
x[double_inf_mask] = ( |
|
(1 - self._xp.abs(t[double_inf_mask])) / t[double_inf_mask] |
|
) |
|
|
|
start = self._xp.tile(self._orig_a[self._semi_inf_pos], (npoints,)) |
|
|
|
|
|
x[semi_inf_mask] = start + (1 - t[semi_inf_mask]) / t[semi_inf_mask] |
|
|
|
jacobian_det = 1/self._xp.prod( |
|
self._xp.reshape( |
|
t[semi_inf_mask | double_inf_mask]**2, |
|
(-1, self._num_inf), |
|
), |
|
axis=-1, |
|
) |
|
|
|
f_x = self._f(x, *args, **kwargs) |
|
jacobian_det = self._xp.reshape(jacobian_det, (-1, *([1]*(len(f_x.shape) - 1)))) |
|
|
|
return f_x * jacobian_det |
|
|