diff --git a/docs/examples/sensitivity/plot_sobol_analyze.py b/docs/examples/sensitivity/plot_sobol_analyze.py new file mode 100644 index 0000000000000000000000000000000000000000..87bbad8e0828257c19bd75f31b31fd7040f4694a --- /dev/null +++ b/docs/examples/sensitivity/plot_sobol_analyze.py @@ -0,0 +1,213 @@ +""" + +Sobol' analysis +=============== +""" + +# %% md +# +# This example shows how to compute Sobol' sensitivity indices using +# the :class:`.SobolAnalyze` class. +# +# Let us consider the Ishigami function :cite:p:`LeGratiet2014` +# +# :math:`y=f\left(x_1, x_2, x_3\right)=\sin \left(x_1\right)+7 \sin \left(\mathrm{x}_2\right)^2+0.1 x_3^4 \sin \left(x_1\right)` +# +# where :math:`x_i` is uniformly distributed on :math:`[-\pi, \pi]` for :math:`i \in [1,2,3]`. We define the function as follows: + +import numpy as np + +def f(x1,x2,x3): + return np.sin(x1) + 7*np.sin(x2)**2 + 0.1*x3**4*np.sin(x1) + + +# %% md +# +# The variation of each :math:`x_i` leads to the variation of :math:`y`. Sobol' analysis is used to quantify the contribution +# of each :math:`x_i` and their interactions to the variation of :math:`y`. Steps of a Sobol' analysis are: +# +# #. Draw samples of input. +# #. Evaluate the model at each input sample to obtain model output. +# #. Compute Sobol' indices based on model outputs. +# + +from psimpy.sampler import Saltelli +from psimpy.sensitivity import SobolAnalyze + +ndim = 3 +bounds = np.array([[-np.pi, np.pi], [-np.pi, np.pi], [-np.pi, np.pi]]) +calc_second_order = False +nbase = 1024 + +# draw Saltelli samples +saltelli_sampler = Saltelli(ndim, bounds, calc_second_order) +saltelli_samples = saltelli_sampler.sample(nbase) + +# Evaluate the model to obtain model outputs +Y = f(saltelli_samples[:,0], saltelli_samples[:,1], saltelli_samples[:, 2]) + +# Compute Sobol' indices +sobol_analyzer = SobolAnalyze(ndim, Y, calc_second_order, seed=2) +S_res = sobol_analyzer.run() + +print("Estimated first-order Sobol' index: ", S_res['S1'][:,0]) +print("Analytical solution: [0.314, 0.442, 0]") + + +# %% md +# +# Figure below shows the results. The bar plots represent estimated first-order +# and total-effect Sobol' indices, as well as their 95% confidence interval. The +# red stars represent true values of the first-order Sobol' indices. +# + +import matplotlib.pyplot as plt + +name_vars = [r'$x_1$',r'$x_2$',r'$x_3$'] +x = {'S1':[1,4,7], 'ST':[2,5,8]} +color = {'S1':'#69b3a2', 'ST':'#3399e6'} +width=0.8 +capsize=3 + +fig, ax1 = plt.subplots(figsize=(4,3)) + +# estimated first-order Sobol' indices and their confidence interval +ax1.bar(x['S1'], S_res['S1'][:,0], width=width, yerr=S_res['S1'][:,2], color=color['S1'], capsize=capsize) + +ax1.axis([0,9,0,0.7]) +ax1.set_xticks([1.5,4.5,7.5]) +ax1.set_xticklabels(name_vars) +ax1.tick_params(axis='both',bottom=False) +ax1.tick_params(axis="y", labelcolor=color['S1']) +ax1.yaxis.grid(True, linestyle='--', which='major', color='grey', alpha=.25) +ax1.set_ylabel('First-order index', color=color['S1']) + +# analytical solution for first-order indices +ax1.scatter(x['S1'], [0.314, 0.442, 0], s=30, c='red', marker='*') + + +# estimated total-effect Sobol' indices and their confidence interval +ax2 = ax1.twinx() +ax2.bar(x['ST'], S_res['ST'][:,0], width=width, yerr=S_res['ST'][:,2], color=color['ST'], capsize=capsize) + +ax2.axis([0,9,0,0.7]) +ax2.set_xticks([1.5,4.5,7.5]) +ax2.tick_params(axis='both',bottom=False) +ax2.tick_params(axis="y", labelcolor=color['ST']) +ax2.yaxis.grid(True, linestyle='--', which='major', color='grey', alpha=.25) +ax2.set_ylabel('Total-effect index', color=color['ST']) + +plt.tight_layout() + + +# %% md +# .. note:: The interaction between one parameter and all other parameters can be +# quantified by the difference between its first-order and total-effet Sobol' +# indices. If detailed information on interaction between any two parameters +# is needed, one can set ``calc_second_order`` to `True`. +# + + +# %% md +# +# **Many models of interest in real-world applications are very computationally +# expensive. In that case, we resort to emulation techniques in order to efficiently +# conduct Sobol' analysis.** See :cite:t:`Zhao2021a` for the detailed theory. +# +# Steps of such an analysis are: +# +# #. Draw input samples for emulator training. +# #. Evaluate the model at each training input point to obtain training data. +# #. Train an emulator for the computationally expensive model of interest. +# #. Draw samples of input for Sobol' analysis. +# #. Evaluate the emulator at each input sample at step 3 to obtain approximated model outputs. +# #. Compute Sobol' indices based on approximated model outputs. +# +# Assume that the above Ishigami function is computationally expensive, an emulator-based +# Sobol' analysis can be conducted as follows: +# + +import numpy as np +from psimpy.sampler import LHS +from psimpy.emulator import ScalarGaSP +from psimpy.sampler import Saltelli +from psimpy.sensitivity import SobolAnalyze + +# define the model +def f(x1,x2,x3): + return np.sin(x1) + 7*np.sin(x2)**2 + 0.1*x3**4*np.sin(x1) + +ndim = 3 +bounds = np.array([[-np.pi, np.pi], [-np.pi, np.pi], [-np.pi, np.pi]]) +seed = 2 + +# draw 100 input samples for emulator training using Latin hypercube sampling +lhs_sampler = LHS(ndim, bounds, seed, criterion='maximin', iteration=200) +design = lhs_sampler.sample(nsamples=100) + +# evaluate the model at each training input point +response = f(design[:,0], design[:,1], design[:,2]) + +# train an emulator +scalar_gasp = ScalarGaSP(ndim) +scalar_gasp.train(design, response) + +# the trained emulator needs to be validated before moving on +# here we leave out this step for simplicity... + +# draw samples of input for Sobol' analysis +calc_second_order = False +nbase = 1024 + +saltelli_sampler = Saltelli(ndim, bounds, calc_second_order) +saltelli_samples = saltelli_sampler.sample(nbase) + +# evaluate the emulator to obtain approximated model output. +# in order to take emulator-induced uncertainty into account, we draw 50 realizations +# from the emulator +Y = scalar_gasp.sample(saltelli_samples, 50) + +# Compute Sobol' indices based on approximated model outputs +sobol_analyzer = SobolAnalyze(ndim, Y, calc_second_order, seed=seed) +S_res = sobol_analyzer.run(mode='parallel', max_workers=5) + +print("\n") +print("Emulator-based first-order Sobol' index: ", S_res['S1'][:,0]) +print("Analytical solution: [0.314, 0.442, 0]") + +# %% md +# The following figure shows how the results of emulator-based Sobol' analysis +# look like. +# + +import matplotlib.pyplot as plt + +name_vars = [r'$x_1$',r'$x_2$',r'$x_3$'] +x = {'S1':[1,4,7], 'ST':[2,5,8]} +color = {'S1':'#69b3a2', 'ST':'#3399e6'} +width=0.8 +capsize=3 + +fig, ax1 = plt.subplots(figsize=(4,3)) + +ax1.bar(x['S1'], S_res['S1'][:,0], width=width, yerr=S_res['S1'][:,2], color=color['S1'], capsize=capsize) +ax1.axis([0,9,0,0.7]) +ax1.set_xticks([1.5,4.5,7.5]) +ax1.set_xticklabels(name_vars) +ax1.tick_params(axis='both',bottom=False) +ax1.tick_params(axis="y", labelcolor=color['S1']) +ax1.yaxis.grid(True, linestyle='--', which='major', color='grey', alpha=.25) +ax1.set_ylabel('First-order index', color=color['S1']) + +ax1.scatter(x['S1'], [0.314, 0.442, 0], s=30, c='red', marker='*') + +ax2 = ax1.twinx() +ax2.bar(x['ST'], S_res['ST'][:,0], width=width, yerr=S_res['ST'][:,2], color=color['ST'], capsize=capsize) +ax2.axis([0,9,0,0.7]) +ax2.set_xticks([1.5,4.5,7.5]) +ax2.tick_params(axis='both',bottom=False) +ax2.tick_params(axis="y", labelcolor=color['ST']) +ax2.yaxis.grid(True, linestyle='--', which='major', color='grey', alpha=.25) +ax2.set_ylabel('Total-effect index', color=color['ST']) + +plt.tight_layout() \ No newline at end of file