| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 |
- import inspect
- from dataclasses import dataclass
- from typing import TYPE_CHECKING, Any
- from collections.abc import Callable
- import numpy as np
- from scipy.stats._common import ConfidenceInterval
- from scipy.stats._qmc import check_random_state
- from scipy.stats._resampling import BootstrapResult
- from scipy.stats import qmc, bootstrap
- from scipy._lib._array_api import xp_capabilities
- from scipy._lib._util import _transition_to_rng
- if TYPE_CHECKING:
- import numpy.typing as npt
- from scipy._lib._util import DecimalNumber, IntNumber
- __all__ = [
- 'sobol_indices'
- ]
- def f_ishigami(x: "npt.ArrayLike") -> "npt.NDArray[np.inexact[Any]]":
- r"""Ishigami function.
- .. math::
- Y(\mathbf{x}) = \sin x_1 + 7 \sin^2 x_2 + 0.1 x_3^4 \sin x_1
- with :math:`\mathbf{x} \in [-\pi, \pi]^3`.
- Parameters
- ----------
- x : array_like ([x1, x2, x3], n)
- Returns
- -------
- f : array_like (n,)
- Function evaluation.
- References
- ----------
- .. [1] Ishigami, T. and T. Homma. "An importance quantification technique
- in uncertainty analysis for computer models." IEEE,
- :doi:`10.1109/ISUMA.1990.151285`, 1990.
- """
- x = np.atleast_2d(x)
- f_eval = (
- np.sin(x[0])
- + 7 * np.sin(x[1])**2
- + 0.1 * (x[2]**4) * np.sin(x[0])
- )
- return f_eval
- def sample_A_B(
- n,
- dists,
- rng=None
- ):
- """Sample two matrices A and B.
- Uses a Sobol' sequence with 2`d` columns to have 2 uncorrelated matrices.
- This is more efficient than using 2 random draw of Sobol'.
- See sec. 5 from [1]_.
- Output shape is (d, n).
- References
- ----------
- .. [1] Saltelli, A., P. Annoni, I. Azzini, F. Campolongo, M. Ratto, and
- S. Tarantola. "Variance based sensitivity analysis of model
- output. Design and estimator for the total sensitivity index."
- Computer Physics Communications, 181(2):259-270,
- :doi:`10.1016/j.cpc.2009.09.018`, 2010.
- """
- d = len(dists)
- A_B = qmc.Sobol(d=2*d, seed=rng, bits=64).random(n).T
- A_B = A_B.reshape(2, d, -1)
- try:
- for d_, dist in enumerate(dists):
- A_B[:, d_] = dist.ppf(A_B[:, d_])
- except AttributeError as exc:
- message = "Each distribution in `dists` must have method `ppf`."
- raise ValueError(message) from exc
- return A_B
- def sample_AB(A: np.ndarray, B: np.ndarray) -> np.ndarray:
- """AB matrix.
- AB: rows of B into A. Shape (d, d, n).
- - Copy A into d "pages"
- - In the first page, replace 1st rows of A with 1st row of B.
- ...
- - In the dth page, replace dth row of A with dth row of B.
- - return the stack of pages
- """
- d, n = A.shape
- AB = np.tile(A, (d, 1, 1))
- i = np.arange(d)
- AB[i, i] = B[i]
- return AB
- def saltelli_2010(
- f_A: np.ndarray, f_B: np.ndarray, f_AB: np.ndarray
- ) -> tuple[np.ndarray, np.ndarray]:
- r"""Saltelli2010 formulation.
- .. math::
- S_i = \frac{1}{N} \sum_{j=1}^N
- f(\mathbf{B})_j (f(\mathbf{AB}^{(i)})_j - f(\mathbf{A})_j)
- .. math::
- S_{T_i} = \frac{1}{N} \sum_{j=1}^N
- (f(\mathbf{A})_j - f(\mathbf{AB}^{(i)})_j)^2
- Parameters
- ----------
- f_A, f_B : array_like (s, n)
- Function values at A and B, respectively
- f_AB : array_like (d, s, n)
- Function values at each of the AB pages
- Returns
- -------
- s, st : array_like (s, d)
- First order and total order Sobol' indices.
- References
- ----------
- .. [1] Saltelli, A., P. Annoni, I. Azzini, F. Campolongo, M. Ratto, and
- S. Tarantola. "Variance based sensitivity analysis of model
- output. Design and estimator for the total sensitivity index."
- Computer Physics Communications, 181(2):259-270,
- :doi:`10.1016/j.cpc.2009.09.018`, 2010.
- """
- # Empirical variance calculated using output from A and B which are
- # independent. Output of AB is not independent and cannot be used
- var = np.var([f_A, f_B], axis=(0, -1))
- # We divide by the variance to have a ratio of variance
- # this leads to eq. 2
- s = np.mean(f_B * (f_AB - f_A), axis=-1) / var # Table 2 (b)
- st = 0.5 * np.mean((f_A - f_AB) ** 2, axis=-1) / var # Table 2 (f)
- return s.T, st.T
- @dataclass
- class BootstrapSobolResult:
- first_order: BootstrapResult
- total_order: BootstrapResult
- @dataclass
- class SobolResult:
- first_order: np.ndarray
- total_order: np.ndarray
- _indices_method: Callable
- _f_A: np.ndarray
- _f_B: np.ndarray
- _f_AB: np.ndarray
- _A: np.ndarray | None = None
- _B: np.ndarray | None = None
- _AB: np.ndarray | None = None
- _bootstrap_result: BootstrapResult | None = None
- def bootstrap(
- self,
- confidence_level: "DecimalNumber" = 0.95,
- n_resamples: "IntNumber" = 999
- ) -> BootstrapSobolResult:
- """Bootstrap Sobol' indices to provide confidence intervals.
- Parameters
- ----------
- confidence_level : float, default: ``0.95``
- The confidence level of the confidence intervals.
- n_resamples : int, default: ``999``
- The number of resamples performed to form the bootstrap
- distribution of the indices.
- Returns
- -------
- res : BootstrapSobolResult
- Bootstrap result containing the confidence intervals and the
- bootstrap distribution of the indices.
- An object with attributes:
- first_order : BootstrapResult
- Bootstrap result of the first order indices.
- total_order : BootstrapResult
- Bootstrap result of the total order indices.
- See `BootstrapResult` for more details.
- """
- def statistic(idx):
- f_A_ = self._f_A[:, idx]
- f_B_ = self._f_B[:, idx]
- f_AB_ = self._f_AB[..., idx]
- return self._indices_method(f_A_, f_B_, f_AB_)
- n = self._f_A.shape[1]
- res = bootstrap(
- [np.arange(n)], statistic=statistic, method="BCa",
- n_resamples=n_resamples,
- confidence_level=confidence_level,
- bootstrap_result=self._bootstrap_result
- )
- self._bootstrap_result = res
- first_order = BootstrapResult(
- confidence_interval=ConfidenceInterval(
- res.confidence_interval.low[0], res.confidence_interval.high[0]
- ),
- bootstrap_distribution=res.bootstrap_distribution[0],
- standard_error=res.standard_error[0],
- )
- total_order = BootstrapResult(
- confidence_interval=ConfidenceInterval(
- res.confidence_interval.low[1], res.confidence_interval.high[1]
- ),
- bootstrap_distribution=res.bootstrap_distribution[1],
- standard_error=res.standard_error[1],
- )
- return BootstrapSobolResult(
- first_order=first_order, total_order=total_order
- )
- @xp_capabilities(np_only=True)
- @_transition_to_rng('random_state', replace_doc=False)
- def sobol_indices(
- *,
- func,
- n,
- dists=None,
- method='saltelli_2010',
- rng=None
- ):
- r"""Global sensitivity indices of Sobol'.
- Parameters
- ----------
- func : callable or dict(str, array_like)
- If `func` is a callable, function to compute the Sobol' indices from.
- Its signature must be::
- func(x: ArrayLike) -> ArrayLike
- with ``x`` of shape ``(d, n)`` and output of shape ``(s, n)`` where:
- - ``d`` is the input dimensionality of `func`
- (number of input variables),
- - ``s`` is the output dimensionality of `func`
- (number of output variables), and
- - ``n`` is the number of samples (see `n` below).
- Function evaluation values must be finite.
- If `func` is a dictionary, contains the function evaluations from three
- different arrays. Keys must be: ``f_A``, ``f_B`` and ``f_AB``.
- ``f_A`` and ``f_B`` should have a shape ``(s, n)`` and ``f_AB``
- should have a shape ``(d, s, n)``.
- This is an advanced feature and misuse can lead to wrong analysis.
- n : int
- Number of samples used to generate the matrices ``A`` and ``B``.
- Must be a power of 2. The total number of points at which `func` is
- evaluated will be ``n*(d+2)``.
- dists : list(distributions), optional
- List of each parameter's distribution. The distribution of parameters
- depends on the application and should be carefully chosen.
- Parameters are assumed to be independently distributed, meaning there
- is no constraint nor relationship between their values.
- Distributions must be an instance of a class with a ``ppf``
- method.
- Must be specified if `func` is a callable, and ignored otherwise.
- method : Callable or str, default: 'saltelli_2010'
- Method used to compute the first and total Sobol' indices.
- If a callable, its signature must be::
- func(f_A: np.ndarray, f_B: np.ndarray, f_AB: np.ndarray)
- -> Tuple[np.ndarray, np.ndarray]
- with ``f_A, f_B`` of shape ``(s, n)`` and ``f_AB`` of shape
- ``(d, s, n)``.
- These arrays contain the function evaluations from three different sets
- of samples.
- The output is a tuple of the first and total indices with
- shape ``(s, d)``.
- This is an advanced feature and misuse can lead to wrong analysis.
- rng : `numpy.random.Generator`, optional
- Pseudorandom number generator state. When `rng` is None, a new
- `numpy.random.Generator` is created using entropy from the
- operating system. Types other than `numpy.random.Generator` are
- passed to `numpy.random.default_rng` to instantiate a ``Generator``.
- .. versionchanged:: 1.15.0
- As part of the `SPEC-007 <https://scientific-python.org/specs/spec-0007/>`_
- transition from use of `numpy.random.RandomState` to
- `numpy.random.Generator`, this keyword was changed from `random_state` to
- `rng`. For an interim period, both keywords will continue to work, although
- only one may be specified at a time. After the interim period, function
- calls using the `random_state` keyword will emit warnings. Following a
- deprecation period, the `random_state` keyword will be removed.
- Returns
- -------
- res : SobolResult
- An object with attributes:
- first_order : ndarray of shape (s, d)
- First order Sobol' indices.
- total_order : ndarray of shape (s, d)
- Total order Sobol' indices.
- And method:
- bootstrap(confidence_level: float, n_resamples: int)
- -> BootstrapSobolResult
- A method providing confidence intervals on the indices.
- See `scipy.stats.bootstrap` for more details.
- The bootstrapping is done on both first and total order indices,
- and they are available in `BootstrapSobolResult` as attributes
- ``first_order`` and ``total_order``.
- Notes
- -----
- The Sobol' method [1]_, [2]_ is a variance-based Sensitivity Analysis which
- obtains the contribution of each parameter to the variance of the
- quantities of interest (QoIs; i.e., the outputs of `func`).
- Respective contributions can be used to rank the parameters and
- also gauge the complexity of the model by computing the
- model's effective (or mean) dimension.
- .. note::
- Parameters are assumed to be independently distributed. Each
- parameter can still follow any distribution. In fact, the distribution
- is very important and should match the real distribution of the
- parameters.
- It uses a functional decomposition of the variance of the function to
- explore
- .. math::
- \mathbb{V}(Y) = \sum_{i}^{d} \mathbb{V}_i (Y) + \sum_{i<j}^{d}
- \mathbb{V}_{ij}(Y) + ... + \mathbb{V}_{1,2,...,d}(Y),
- introducing conditional variances:
- .. math::
- \mathbb{V}_i(Y) = \mathbb{\mathbb{V}}[\mathbb{E}(Y|x_i)]
- \qquad
- \mathbb{V}_{ij}(Y) = \mathbb{\mathbb{V}}[\mathbb{E}(Y|x_i x_j)]
- - \mathbb{V}_i(Y) - \mathbb{V}_j(Y),
- Sobol' indices are expressed as
- .. math::
- S_i = \frac{\mathbb{V}_i(Y)}{\mathbb{V}[Y]}
- \qquad
- S_{ij} =\frac{\mathbb{V}_{ij}(Y)}{\mathbb{V}[Y]}.
- :math:`S_{i}` corresponds to the first-order term which apprises the
- contribution of the i-th parameter, while :math:`S_{ij}` corresponds to the
- second-order term which informs about the contribution of interactions
- between the i-th and the j-th parameters. These equations can be
- generalized to compute higher order terms; however, they are expensive to
- compute and their interpretation is complex.
- This is why only first order indices are provided.
- Total order indices represent the global contribution of the parameters
- to the variance of the QoI and are defined as:
- .. math::
- S_{T_i} = S_i + \sum_j S_{ij} + \sum_{j,k} S_{ijk} + ...
- = 1 - \frac{\mathbb{V}[\mathbb{E}(Y|x_{\sim i})]}{\mathbb{V}[Y]}.
- First order indices sum to at most 1, while total order indices sum to at
- least 1. If there are no interactions, then first and total order indices
- are equal, and both first and total order indices sum to 1.
- .. warning::
- Negative Sobol' values are due to numerical errors. Increasing the
- number of points `n` should help.
- The number of sample required to have a good analysis increases with
- the dimensionality of the problem. e.g. for a 3 dimension problem,
- consider at minima ``n >= 2**12``. The more complex the model is,
- the more samples will be needed.
- Even for a purely additive model, the indices may not sum to 1 due
- to numerical noise.
- References
- ----------
- .. [1] Sobol, I. M.. "Sensitivity analysis for nonlinear mathematical
- models." Mathematical Modeling and Computational Experiment, 1:407-414,
- 1993.
- .. [2] Sobol, I. M. (2001). "Global sensitivity indices for nonlinear
- mathematical models and their Monte Carlo estimates." Mathematics
- and Computers in Simulation, 55(1-3):271-280,
- :doi:`10.1016/S0378-4754(00)00270-6`, 2001.
- .. [3] Saltelli, A. "Making best use of model evaluations to
- compute sensitivity indices." Computer Physics Communications,
- 145(2):280-297, :doi:`10.1016/S0010-4655(02)00280-1`, 2002.
- .. [4] Saltelli, A., M. Ratto, T. Andres, F. Campolongo, J. Cariboni,
- D. Gatelli, M. Saisana, and S. Tarantola. "Global Sensitivity Analysis.
- The Primer." 2007.
- .. [5] Saltelli, A., P. Annoni, I. Azzini, F. Campolongo, M. Ratto, and
- S. Tarantola. "Variance based sensitivity analysis of model
- output. Design and estimator for the total sensitivity index."
- Computer Physics Communications, 181(2):259-270,
- :doi:`10.1016/j.cpc.2009.09.018`, 2010.
- .. [6] Ishigami, T. and T. Homma. "An importance quantification technique
- in uncertainty analysis for computer models." IEEE,
- :doi:`10.1109/ISUMA.1990.151285`, 1990.
- Examples
- --------
- The following is an example with the Ishigami function [6]_
- .. math::
- Y(\mathbf{x}) = \sin x_1 + 7 \sin^2 x_2 + 0.1 x_3^4 \sin x_1,
- with :math:`\mathbf{x} \in [-\pi, \pi]^3`. This function exhibits strong
- non-linearity and non-monotonicity.
- Remember, Sobol' indices assumes that samples are independently
- distributed. In this case we use a uniform distribution on each marginals.
- >>> import numpy as np
- >>> from scipy.stats import sobol_indices, uniform
- >>> rng = np.random.default_rng()
- >>> def f_ishigami(x):
- ... f_eval = (
- ... np.sin(x[0])
- ... + 7 * np.sin(x[1])**2
- ... + 0.1 * (x[2]**4) * np.sin(x[0])
- ... )
- ... return f_eval
- >>> indices = sobol_indices(
- ... func=f_ishigami, n=1024,
- ... dists=[
- ... uniform(loc=-np.pi, scale=2*np.pi),
- ... uniform(loc=-np.pi, scale=2*np.pi),
- ... uniform(loc=-np.pi, scale=2*np.pi)
- ... ],
- ... rng=rng
- ... )
- >>> indices.first_order
- array([0.31637954, 0.43781162, 0.00318825])
- >>> indices.total_order
- array([0.56122127, 0.44287857, 0.24229595])
- Confidence interval can be obtained using bootstrapping.
- >>> boot = indices.bootstrap()
- Then, this information can be easily visualized.
- >>> import matplotlib.pyplot as plt
- >>> fig, axs = plt.subplots(1, 2, figsize=(9, 4))
- >>> _ = axs[0].errorbar(
- ... [1, 2, 3], indices.first_order, fmt='o',
- ... yerr=[
- ... indices.first_order - boot.first_order.confidence_interval.low,
- ... boot.first_order.confidence_interval.high - indices.first_order
- ... ],
- ... )
- >>> axs[0].set_ylabel("First order Sobol' indices")
- >>> axs[0].set_xlabel('Input parameters')
- >>> axs[0].set_xticks([1, 2, 3])
- >>> _ = axs[1].errorbar(
- ... [1, 2, 3], indices.total_order, fmt='o',
- ... yerr=[
- ... indices.total_order - boot.total_order.confidence_interval.low,
- ... boot.total_order.confidence_interval.high - indices.total_order
- ... ],
- ... )
- >>> axs[1].set_ylabel("Total order Sobol' indices")
- >>> axs[1].set_xlabel('Input parameters')
- >>> axs[1].set_xticks([1, 2, 3])
- >>> plt.tight_layout()
- >>> plt.show()
- .. note::
- By default, `scipy.stats.uniform` has support ``[0, 1]``.
- Using the parameters ``loc`` and ``scale``, one obtains the uniform
- distribution on ``[loc, loc + scale]``.
- This result is particularly interesting because the first order index
- :math:`S_{x_3} = 0` whereas its total order is :math:`S_{T_{x_3}} = 0.244`.
- This means that higher order interactions with :math:`x_3` are responsible
- for the difference. Almost 25% of the observed variance
- on the QoI is due to the correlations between :math:`x_3` and :math:`x_1`,
- although :math:`x_3` by itself has no impact on the QoI.
- The following gives a visual explanation of Sobol' indices on this
- function. Let's generate 1024 samples in :math:`[-\pi, \pi]^3` and
- calculate the value of the output.
- >>> from scipy.stats import qmc
- >>> n_dim = 3
- >>> p_labels = ['$x_1$', '$x_2$', '$x_3$']
- >>> sample = qmc.Sobol(d=n_dim, seed=rng).random(1024)
- >>> sample = qmc.scale(
- ... sample=sample,
- ... l_bounds=[-np.pi, -np.pi, -np.pi],
- ... u_bounds=[np.pi, np.pi, np.pi]
- ... )
- >>> output = f_ishigami(sample.T)
- Now we can do scatter plots of the output with respect to each parameter.
- This gives a visual way to understand how each parameter impacts the
- output of the function.
- >>> fig, ax = plt.subplots(1, n_dim, figsize=(12, 4))
- >>> for i in range(n_dim):
- ... xi = sample[:, i]
- ... ax[i].scatter(xi, output, marker='+')
- ... ax[i].set_xlabel(p_labels[i])
- >>> ax[0].set_ylabel('Y')
- >>> plt.tight_layout()
- >>> plt.show()
- Now Sobol' goes a step further:
- by conditioning the output value by given values of the parameter
- (black lines), the conditional output mean is computed. It corresponds to
- the term :math:`\mathbb{E}(Y|x_i)`. Taking the variance of this term gives
- the numerator of the Sobol' indices.
- >>> mini = np.min(output)
- >>> maxi = np.max(output)
- >>> n_bins = 10
- >>> bins = np.linspace(-np.pi, np.pi, num=n_bins, endpoint=False)
- >>> dx = bins[1] - bins[0]
- >>> fig, ax = plt.subplots(1, n_dim, figsize=(12, 4))
- >>> for i in range(n_dim):
- ... xi = sample[:, i]
- ... ax[i].scatter(xi, output, marker='+')
- ... ax[i].set_xlabel(p_labels[i])
- ... for bin_ in bins:
- ... idx = np.where((bin_ <= xi) & (xi <= bin_ + dx))
- ... xi_ = xi[idx]
- ... y_ = output[idx]
- ... ave_y_ = np.mean(y_)
- ... ax[i].plot([bin_ + dx/2] * 2, [mini, maxi], c='k')
- ... ax[i].scatter(bin_ + dx/2, ave_y_, c='r')
- >>> ax[0].set_ylabel('Y')
- >>> plt.tight_layout()
- >>> plt.show()
- Looking at :math:`x_3`, the variance
- of the mean is zero leading to :math:`S_{x_3} = 0`. But we can further
- observe that the variance of the output is not constant along the parameter
- values of :math:`x_3`. This heteroscedasticity is explained by higher order
- interactions. Moreover, an heteroscedasticity is also noticeable on
- :math:`x_1` leading to an interaction between :math:`x_3` and :math:`x_1`.
- On :math:`x_2`, the variance seems to be constant and thus null interaction
- with this parameter can be supposed.
- This case is fairly simple to analyse visually---although it is only a
- qualitative analysis. Nevertheless, when the number of input parameters
- increases such analysis becomes unrealistic as it would be difficult to
- conclude on high-order terms. Hence the benefit of using Sobol' indices.
- """
- rng = check_random_state(rng)
- n_ = int(n)
- if not (n_ & (n_ - 1) == 0) or n != n_:
- raise ValueError(
- "The balance properties of Sobol' points require 'n' "
- "to be a power of 2."
- )
- n = n_
- if not callable(method):
- indices_methods = {
- "saltelli_2010": saltelli_2010,
- }
- try:
- method = method.lower() # type: ignore[assignment]
- indices_method_ = indices_methods[method]
- except KeyError as exc:
- message = (
- f"{method!r} is not a valid 'method'. It must be one of"
- f" {set(indices_methods)!r} or a callable."
- )
- raise ValueError(message) from exc
- else:
- indices_method_ = method
- sig = inspect.signature(indices_method_)
- if set(sig.parameters) != {'f_A', 'f_B', 'f_AB'}:
- message = (
- "If 'method' is a callable, it must have the following"
- f" signature: {inspect.signature(saltelli_2010)}"
- )
- raise ValueError(message)
- def indices_method(f_A, f_B, f_AB):
- """Wrap indices method to ensure proper output dimension.
- 1D when single output, 2D otherwise.
- """
- return np.squeeze(indices_method_(f_A=f_A, f_B=f_B, f_AB=f_AB))
- if callable(func):
- if dists is None:
- raise ValueError(
- "'dists' must be defined when 'func' is a callable."
- )
- def wrapped_func(x):
- return np.atleast_2d(func(x))
- A, B = sample_A_B(n=n, dists=dists, rng=rng)
- AB = sample_AB(A=A, B=B)
- f_A = wrapped_func(A)
- if f_A.shape[1] != n:
- raise ValueError(
- "'func' output should have a shape ``(s, -1)`` with ``s`` "
- "the number of output."
- )
- def funcAB(AB):
- d, d, n = AB.shape
- AB = np.moveaxis(AB, 0, -1).reshape(d, n*d)
- f_AB = wrapped_func(AB)
- return np.moveaxis(f_AB.reshape((-1, n, d)), -1, 0)
- f_B = wrapped_func(B)
- f_AB = funcAB(AB)
- else:
- message = (
- "When 'func' is a dictionary, it must contain the following "
- "keys: 'f_A', 'f_B' and 'f_AB'."
- "'f_A' and 'f_B' should have a shape ``(s, n)`` and 'f_AB' "
- "should have a shape ``(d, s, n)``."
- )
- try:
- f_A, f_B, f_AB = map(lambda arr: arr.copy(), np.atleast_2d(
- func['f_A'], func['f_B'], func['f_AB']
- ))
- except KeyError as exc:
- raise ValueError(message) from exc
- if f_A.shape[1] != n or f_A.shape != f_B.shape or \
- f_AB.shape == f_A.shape or f_AB.shape[-1] % n != 0:
- raise ValueError(message)
- # Normalization by mean
- # Sobol', I. and Levitan, Y. L. (1999). On the use of variance reducing
- # multipliers in monte carlo computations of a global sensitivity index.
- # Computer Physics Communications, 117(1) :52-61.
- mean = np.mean([f_A, f_B], axis=(0, -1)).reshape(-1, 1)
- f_A -= mean
- f_B -= mean
- f_AB -= mean
- # Compute indices
- # Filter warnings for constant output as var = 0
- with np.errstate(divide='ignore', invalid='ignore'):
- first_order, total_order = indices_method(f_A=f_A, f_B=f_B, f_AB=f_AB)
- # null variance means null indices
- first_order[~np.isfinite(first_order)] = 0
- total_order[~np.isfinite(total_order)] = 0
- res = dict(
- first_order=first_order,
- total_order=total_order,
- _indices_method=indices_method,
- _f_A=f_A,
- _f_B=f_B,
- _f_AB=f_AB
- )
- if callable(func):
- res.update(
- dict(
- _A=A,
- _B=B,
- _AB=AB,
- )
- )
- return SobolResult(**res)
|