Tutorial 03 - Computation of Sobol Indices
This tutorial covers the approximation of the Sobol indices of a target function, which are used to infer information about the global parameter sensitivity of the model. To verify the results we compute with PyThia, we use the Sobol function as the object of interest, as the Sobol indices are explicitly known for this function. The Sobol function is given by
Note
The larger \(a_j\), the less is the influence, i.e. the sensitivity, of parameter \(x_j\).
The mean of the Sobol function is \(\mathbb{E}[f]=1\) and the variance reads
With this, we can easily compute the Sobol indices by
An implementation of the Sobol function method sobol_function()
and the analytical Sobol indices method sobol_sc()
is included in the complete script at the end of this tutorial.
First, we choose some values for the coefficients \(a_1,\dots,a_M\) and with this the target function.
import numpy as np
import pythia as pt
a = np.array([1, 2, 3])
def target_function(x: np.ndarray) -> np.ndarray:
return sobol_function(x, a=a)
Additionally, we compute the analytical Sobol indices.
sobol_dict = sobol_sc(a=a, dim=len(a))[0]
sobol_coefficients = np.array(list(sobol_dict.values())).reshape(-1, 1)
Next we define the necessary quantities for the PC expansion. For more information see Tutorial 01 - Approximation of Functions with Polynomial Chaos. As stochastic parameters we need to choose uniformly distributed variables \(x_j\) on \([0,1]\) according to the number of coefficients \(a_1,\dots,a_M\), i.e.,
params = [
pt.parameter.Parameter(name=f"x_{j+1}", domain=[0, 1], distribution="uniform")
for j in range(a.size)
]
As expansion terms we choose multivariate Legendre polynomials of total polynomial degree less then \(11\)
max_dim = 11
# limit total polynomial degree of expansion terms to 10
indices = pt.index.simplex_set(len(params), max_dim - 1)
index_set = pt.index.IndexSet(indices)
The last thing we need are training data. We generate the training pairs again by an optimal weighted distribution, see Tutorial 02 - Approximation of n-D functions with Polynomial Chaos for more detail.
N = 10_000
s = pt.sampler.WLSTensorSampler(params, [max_dim - 1] * len(params))
x_train = s.sample(N)
w_train = s.weight(x_train)
y_train = target_function(x_train)
With this, we compute the PC expansion of the target_function
with PyThia
surrogate = pt.chaos.PolynomialChaos(params, index_set, x_train, w_train, y_train)
As the approximative Sobol indices can be easily derived from the PC expansion coefficients, the PolynomialChaos
class computes them automatically upon initialization.
They can be accessed via surrogate.sobol
, which is an array ordered according to index_set.sobol_tuples
.
Hence it is easy to verify, if the approximation of the Sobol indices was done correctly.
print("Comparison of Sobol indices")
print(f" {'sobol_tuple':<12} {'exact':<8} {'approx':<8} {'abs error':<9}")
print("-" * 44)
for j, sdx in enumerate(sobol_dict.keys()):
print(
f" {str(sdx):<11} ", # Sobol index subscripts
f"{sobol_coefficients[j, 0]:<4.2e} ",
f"{surrogate.sobol[j, 0]:<4.2e} ",
f"{np.abs(sobol_coefficients[j, 0] - surrogate.sobol[j, 0]):<4.2e}",
)
This concludes this tutorial.
Complete Script
The complete source code listed below is located in the tutorials/
subdirectory of the repository root.
1"""
2File: tutorials/tutorial_03.py
3Author: Nando Hegemann
4Gitlab: https://gitlab.com/Nando-Hegemann
5Description: Tutorial 03 - Computation of Sobol Indices.
6SPDX-License-Identifier: LGPL-3.0-or-later OR Hippocratic-3.0-ECO-MEDIA-MIL
7"""
8import numpy as np
9import pythia as pt
10
11
12def sobol_function(x: np.ndarray, a: np.ndarray | None = None, **kwargs) -> np.ndarray:
13 """Sobol function.
14
15 Parameters
16 ----------
17 x : np.ndarray
18 Input values.
19 a : np.ndarray | None, optional
20 Coefficients, by default None
21
22 Returns
23 -------
24 :
25 Sobol function values.
26
27 Raises
28 ------
29 ValueError
30 Wrong dimension for input `x`.
31 ValueError
32 Wrong shape of Coefficients `a`.
33 """
34 if not 0 < x.ndim < 3:
35 raise ValueError("Wrong ndim: {}".format(x.ndim))
36 if x.ndim == 1:
37 x.shape = 1, -1
38 if a is None:
39 a = np.zeros(x.shape[1])
40 elif not a.shape == (x.shape[1],):
41 raise ValueError("Wrong shape: {}".format(a.shape))
42 return np.prod((abs(4.0 * x - 2.0) + a) / (1.0 + a), axis=1).reshape(-1, 1)
43
44
45def sobol_sc(a: np.ndarray, dim: int = 1, **kwargs) -> tuple | np.ndarray:
46 """Sobol function Sobol indices.
47
48 Parameters
49 ----------
50 a : np.ndarray
51 Coefficients.
52 dim : int, optional
53 Parameter dimension, by default 1.
54
55 Returns
56 -------
57 :
58 Sobol indices of Sobol function.
59 """
60 sobol = {}
61 beta = (1.0 + a) ** (-2) / 3
62 var = np.prod(1.0 + beta) - 1.0
63 sobol_tuples = pt.index.IndexSet(pt.index.tensor_set([1, 1, 1])).sobol_tuples
64 for sdx in sobol_tuples:
65 sobol[sdx] = 1.0 / var
66 for k in sdx:
67 sobol[sdx] *= beta[k - 1]
68 if dim > 1:
69 return np.array([sobol for _ in range(dim)])
70 else:
71 return sobol
72
73
74print("Tutorial 03 - 2D approximation with PC")
75
76# target function definition
77a = np.array([1, 2, 3])
78
79
80def target_function(x: np.ndarray) -> np.ndarray:
81 """Target function.
82
83 Parameters
84 ----------
85 x : np.ndarray
86 """
87 return sobol_function(x, a=a)
88
89
90# analytical sobol coefficients
91sobol_dict = sobol_sc(a=a, dim=len(a))[0]
92sobol_coefficients = np.array(list(sobol_dict.values())).reshape(-1, 1)
93
94# setup pc surrogate
95params = [
96 pt.parameter.Parameter(name=f"x_{j+1}", domain=[0, 1], distribution="uniform")
97 for j in range(a.size)
98]
99
100max_dim = 11
101# limit total polynomial degree of expansion terms to 10
102indices = pt.index.simplex_set(len(params), max_dim - 1)
103index_set = pt.index.IndexSet(indices)
104print("multiindex information:")
105print(f" number of indices: {index_set.shape[0]}")
106print(f" dimension: {index_set.shape[1]}")
107print(f" number of sobol indices: {len(index_set.sobol_tuples)}")
108
109N = 10_000
110print(f"generate training data ({N})")
111s = pt.sampler.WLSTensorSampler(params, [max_dim - 1] * len(params))
112x_train = s.sample(N)
113w_train = s.weight(x_train)
114y_train = target_function(x_train)
115
116print("compute pc expansion")
117surrogate = pt.chaos.PolynomialChaos(params, index_set, x_train, w_train, y_train)
118
119# test PC approximation
120N = 1000
121print(f"generate test data ({N})")
122s_test = pt.sampler.ParameterSampler(params)
123x_test = s_test.sample(N)
124y_test = target_function(x_test)
125y_approx = surrogate.eval(x_test)
126
127error_L2 = np.sqrt(np.sum((y_test - y_approx) ** 2) / N)
128error_L2_rel = error_L2 / np.sqrt(np.sum((y_test) ** 2) / N)
129error_max = np.max(np.abs(y_test - y_approx))
130error_max_rel = np.max(np.abs(y_test - y_approx) / np.abs(y_test))
131
132print(f"test error L2 (abs/rel): {error_L2:4.2e} / {error_L2_rel:4.2e}")
133print(f"test error max (abs/rel): {error_max:4.2e} / {error_max_rel:4.2e}")
134
135# compare Sobol indices
136print("Comparison of Sobol indices")
137print(f" {'sobol_tuple':<12} {'exact':<8} {'approx':<8} {'abs error':<9}")
138print("-" * 44)
139for j, sdx in enumerate(sobol_dict.keys()):
140 print(
141 f" {str(sdx):<11} ", # Sobol index subscripts
142 f"{sobol_coefficients[j, 0]:<4.2e} ",
143 f"{surrogate.sobol[j, 0]:<4.2e} ",
144 f"{np.abs(sobol_coefficients[j, 0] - surrogate.sobol[j, 0]):<4.2e}",
145 )