diff --git a/docs/logo/BVRLogoV03.png b/docs/logo/BVRLogoV03.png new file mode 100644 index 0000000000000000000000000000000000000000..b68ce44489d73ee4251a723dfcf0e9e5c8c36d64 Binary files /dev/null and b/docs/logo/BVRLogoV03.png differ diff --git a/docs/logo/BVRLogoV03_longtext.png b/docs/logo/BVRLogoV03_longtext.png new file mode 100644 index 0000000000000000000000000000000000000000..9cc47ce15481d9e16d1661682e27c97034755d4d Binary files /dev/null and b/docs/logo/BVRLogoV03_longtext.png differ diff --git a/docs/logo/BVRLogoV03_shorttext.png b/docs/logo/BVRLogoV03_shorttext.png new file mode 100644 index 0000000000000000000000000000000000000000..491a6ecb1d38aa38442e05c725968bcec6d911b7 Binary files /dev/null and b/docs/logo/BVRLogoV03_shorttext.png differ diff --git a/examples/model-comparison/example_model_comparison.py b/examples/model-comparison/example_model_comparison.py index ebd80fea82a3caf3ff204b89da96c59737ba502b..249a0f0c344632ed1302d5d7725ef7ea345d4302 100644 --- a/examples/model-comparison/example_model_comparison.py +++ b/examples/model-comparison/example_model_comparison.py @@ -22,7 +22,12 @@ import scipy.io as io import pandas as pd import joblib import sys +<<<<<<< HEAD sys.path.append("../../src/bayesvalidrox/") +======= +#sys.path.append("../../src/bayesvalidrox/") +sys.path.append("../../src/") +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) from bayesvalidrox.pylink.pylink import PyLinkForwardModel from bayesvalidrox.surrogate_models.inputs import Input diff --git a/examples/only-model/L2_model.py b/examples/only-model/L2_model.py new file mode 100644 index 0000000000000000000000000000000000000000..6b28c818101e25859bdb222b82cfd9bee741d381 --- /dev/null +++ b/examples/only-model/L2_model.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This is a simple linear model. + +The code for this numerical experiments is available at +https://github.com/MichaelSinsbeck/paper_sequential-design-model-selection. + +Author: Farid Mohammadi, M.Sc. +E-Mail: farid.mohammadi@iws.uni-stuttgart.de +Department of Hydromechanics and Modelling of Hydrosystems (LH2) +Institute for Modelling Hydraulic and Environmental Systems (IWS), +University of Stuttgart, www.iws.uni-stuttgart.de/lh2/ +Pfaffenwaldring 61 +70569 Stuttgart + +Created on Fri Oct 8 2021 + +""" +import numpy as np + + +def L2_model(xx): + """ + Linear model y = a*x+b + + Models adapted from Anneli Guthke's paper: + ch€oniger, A., T. W€ohling, L. Samaniego,and W. Nowak (2014), Model + selection on solid ground: Rigorous comparison ofnine ways to evaluate + Bayesian modelevidence,Water Resour. Res.,50,9484–9513, + doi:10.1002/2014WR016062 + + Parameters + ---------- + xx : array + Parameters a and b. + + Returns + ------- + 2D-array + The first row contains the measurement locations. + The second row contains the model outputs. + + """ + n_output = 15 + meas_loc = np.linspace(0.25, 4.75, n_output) + + # L2_model + L2_model = xx[:, 0] * meas_loc + xx[:, 1] + + # Output + output = { + 'x_values': meas_loc, + 'Z': L2_model + } + + return output diff --git a/examples/only-model/NL2_model.py b/examples/only-model/NL2_model.py new file mode 100644 index 0000000000000000000000000000000000000000..5fd4820e76a9756b85b891b0d8272404e81d3361 --- /dev/null +++ b/examples/only-model/NL2_model.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This is a nonlinear cosine model. + +The code for this numerical experiments is available at +https://github.com/MichaelSinsbeck/paper_sequential-design-model-selection. + +Author: Farid Mohammadi, M.Sc. +E-Mail: farid.mohammadi@iws.uni-stuttgart.de +Department of Hydromechanics and Modelling of Hydrosystems (LH2) +Institute for Modelling Hydraulic and Environmental Systems (IWS), +University of Stuttgart, www.iws.uni-stuttgart.de/lh2/ +Pfaffenwaldring 61 +70569 Stuttgart + +Created on Fri Oct 8 2021 + +""" +import numpy as np + + +def NL2_model(xx): + """ + Nonlinear model y = exp(a*x) + b + + Models adapted from Anneli Guthke's paper: + ch€oniger, A., T. W€ohling, L. Samaniego,and W. Nowak (2014), Model + selection on solid ground: Rigorous comparison ofnine ways to evaluate + Bayesian modelevidence,Water Resour. Res.,50,9484–9513, + doi:10.1002/2014WR016062 + + Parameters + ---------- + xx : array + Parameters a and b. + + Returns + ------- + 2D-array + The first row contains the measurement locations. + The second row contains the model outputs. + + """ + n_output = 15 + meas_loc = np.linspace(0.25, 4.75, n_output) + + # NL2_model + NL2_model = np.exp(xx[:, 0] * meas_loc) + xx[:, 1] + + # Output + output = { + 'x_values': meas_loc, + 'Z': NL2_model + } + + return output diff --git a/examples/only-model/NL4_model.py b/examples/only-model/NL4_model.py new file mode 100644 index 0000000000000000000000000000000000000000..5ca495306d9a6d277ec654a3efbdfb84bfc28ce1 --- /dev/null +++ b/examples/only-model/NL4_model.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This is a nonlinear cosine model. + +The code for this numerical experiments is available at +https://github.com/MichaelSinsbeck/paper_sequential-design-model-selection. + +Author: Farid Mohammadi, M.Sc. +E-Mail: farid.mohammadi@iws.uni-stuttgart.de +Department of Hydromechanics and Modelling of Hydrosystems (LH2) +Institute for Modelling Hydraulic and Environmental Systems (IWS), +University of Stuttgart, www.iws.uni-stuttgart.de/lh2/ +Pfaffenwaldring 61 +70569 Stuttgart + +Created on Fri Oct 8 2021 + +""" +import numpy as np + + +def NL4_model(xx): + """ + Nonlinear model y = a*cos(b*x+c)+d + + Models adapted from Anneli Guthke's paper: + ch€oniger, A., T. W€ohling, L. Samaniego,and W. Nowak (2014), Model + selection on solid ground: Rigorous comparison ofnine ways to evaluate + Bayesian modelevidence,Water Resour. Res.,50,9484–9513, + doi:10.1002/2014WR016062 + + Parameters + ---------- + xx : array + Parameters a and b. + + Returns + ------- + 2D-array + The first row contains the measurement locations. + The second row contains the model outputs. + + """ + n_output = 15 + meas_loc = np.linspace(0.25, 4.75, n_output) + + # NL4_model + NL4_model = xx[:, 0] * np.cos(xx[:, 1] * meas_loc + xx[:, 2]) + xx[:, 3] + + # Output + output = { + 'x_values': meas_loc, + 'Z': NL4_model + } + + return output diff --git a/examples/only-model/bayesvalidrox/__init__.py b/examples/only-model/bayesvalidrox/__init__.py deleted file mode 100644 index 55c14687472fe6ff00a2438f31b9ba9ecd2992cd..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -__version__ = "0.0.5" - -from .pylink.pylink import PyLinkForwardModel -from .surrogate_models.surrogate_models import MetaModel -from .surrogate_models.meta_model_engine import MetaModelEngine -from .surrogate_models.inputs import Input -from .post_processing.post_processing import PostProcessing -from .bayes_inference.bayes_inference import BayesInference -from .bayes_inference.bayes_model_comparison import BayesModelComparison -from .bayes_inference.discrepancy import Discrepancy - -__all__ = [ - "__version__", - "PyLinkForwardModel", - "Input", - "Discrepancy", - "MetaModel", - "MetaModelEngine", - "PostProcessing", - "BayesInference", - "BayesModelComparison" - ] diff --git a/examples/only-model/bayesvalidrox/__pycache__/__init__.cpython-311.pyc b/examples/only-model/bayesvalidrox/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 278aab3ea132ca1f0dbb5897698da7ba6551a21c..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/bayes_inference/__init__.py b/examples/only-model/bayesvalidrox/bayes_inference/__init__.py deleted file mode 100644 index df8d935680f96ab487cf087866e8bfd504762945..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/bayes_inference/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- - -from .bayes_inference import BayesInference -from .mcmc import MCMC - -__all__ = [ - "BayesInference", - "MCMC" - ] diff --git a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc b/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index cb05638998140eeacbfda327b1cbbcff8e50b2e7..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc b/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc deleted file mode 100644 index 7f5bbe1146a0ace3a1e790b0274a9167756c5c79..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc b/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc deleted file mode 100644 index 919edf1404e4ac63d005a49146a39ded9defe066..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc b/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc deleted file mode 100644 index 277082acf177169ba1aeb1049190f4e2821cf92f..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc b/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc deleted file mode 100644 index 28789e25211ee767120c61154597194c2f92b447..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/bayes_inference/bayes_inference.py b/examples/only-model/bayesvalidrox/bayes_inference/bayes_inference.py deleted file mode 100644 index d566503a5718387be88925915229157bd8125a1a..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/bayes_inference/bayes_inference.py +++ /dev/null @@ -1,1537 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import numpy as np -import os -import copy -import pandas as pd -from tqdm import tqdm -from scipy import stats -import scipy.linalg as spla -import joblib -import seaborn as sns -import corner -import h5py -import multiprocessing -import gc -from sklearn.metrics import mean_squared_error, r2_score -from sklearn import preprocessing -from matplotlib.patches import Patch -import matplotlib.lines as mlines -from matplotlib.backends.backend_pdf import PdfPages -import matplotlib.pylab as plt - -from .mcmc import MCMC - -# Load the mplstyle -#plt.style.use(os.path.join(os.path.split(__file__)[0], -# '../', 'bayesvalidrox.mplstyle')) - - -class BayesInference: - """ - A class to perform Bayesian Analysis. - - - Attributes - ---------- - MetaModel : obj - Meta model object. - discrepancy : obj - The discrepancy object for the sigma2s, i.e. the diagonal entries - of the variance matrix for a multivariate normal likelihood. - name : str, optional - The type of analysis, either calibration (`Calib`) or validation - (`Valid`). The default is `'Calib'`. - emulator : bool, optional - Analysis with emulator (MetaModel). The default is `True`. - bootstrap : bool, optional - Bootstrap the analysis. The default is `False`. - req_outputs : list, optional - The list of requested output to be used for the analysis. - The default is `None`. If None, all the defined outputs for the model - object is used. - selected_indices : dict, optional - A dictionary with the selected indices of each model output. The - default is `None`. If `None`, all measurement points are used in the - analysis. - samples : array of shape (n_samples, n_params), optional - The samples to be used in the analysis. The default is `None`. If - None the samples are drawn from the probablistic input parameter - object of the MetaModel object. - n_samples : int, optional - Number of samples to be used in the analysis. The default is `500000`. - If samples is not `None`, this argument will be assigned based on the - number of samples given. - measured_data : dict, optional - A dictionary containing the observation data. The default is `None`. - if `None`, the observation defined in the Model object of the - MetaModel is used. - inference_method : str, optional - A method for approximating the posterior distribution in the Bayesian - inference step. The default is `'rejection'`, which stands for - rejection sampling. A Markov Chain Monte Carlo sampler can be simply - selected by passing `'MCMC'`. - mcmc_params : dict, optional - A dictionary with args required for the Bayesian inference with - `MCMC`. The default is `None`. - - Pass the mcmc_params like the following: - - >>> mcmc_params:{ - 'init_samples': None, # initial samples - 'n_walkers': 100, # number of walkers (chain) - 'n_steps': 100000, # number of maximum steps - 'n_burn': 200, # number of burn-in steps - 'moves': None, # Moves for the emcee sampler - 'multiprocessing': False, # multiprocessing - 'verbose': False # verbosity - } - The items shown above are the default values. If any parmeter is - not defined, the default value will be assigned to it. - bayes_loocv : bool, optional - Bayesian Leave-one-out Cross Validation. The default is `False`. If - `True`, the LOOCV procedure is used to estimate the bayesian Model - Evidence (BME). - n_bootstrap_itrs : int, optional - Number of bootstrap iteration. The default is `1`. If bayes_loocv is - `True`, this is qualt to the total length of the observation data - set. - perturbed_data : array of shape (n_bootstrap_itrs, n_obs), optional - User defined perturbed data. The default is `[]`. - bootstrap_noise : float, optional - A noise level to perturb the data set. The default is `0.05`. - just_analysis : bool, optional - Justifiability analysis. The default is False. - valid_metrics : list, optional - List of the validation metrics. The following metrics are supported: - - 1. log_BME : logarithm of the Bayesian model evidence - 2. KLD : Kullback-Leibler Divergence - 3. inf_entropy: Information entropy - The default is `['log_BME']`. - plot_post_pred : bool, optional - Plot posterior predictive plots. The default is `True`. - plot_map_pred : bool, optional - Plot the model outputs vs the metamodel predictions for the maximum - a posteriori (defined as `max_a_posteriori`) parameter set. The - default is `False`. - max_a_posteriori : str, optional - Maximum a posteriori. `'mean'` and `'mode'` are available. The default - is `'mean'`. - corner_title_fmt : str, optional - Title format for the posterior distribution plot with python - package `corner`. The default is `'.2e'`. - - """ - - def __init__(self, MetaModel, discrepancy=None, emulator=True, - name='Calib', bootstrap=False, req_outputs=None, - selected_indices=None, samples=None, n_samples=100000, - measured_data=None, inference_method='rejection', - mcmc_params=None, bayes_loocv=False, n_bootstrap_itrs=1, - perturbed_data=[], bootstrap_noise=0.05, just_analysis=False, - valid_metrics=['BME'], plot_post_pred=True, - plot_map_pred=False, max_a_posteriori='mean', - corner_title_fmt='.2e'): - - self.MetaModel = MetaModel - self.Discrepancy = discrepancy - self.emulator = emulator - self.name = name - self.bootstrap = bootstrap - self.req_outputs = req_outputs - self.selected_indices = selected_indices - self.samples = samples - self.n_samples = n_samples - self.measured_data = measured_data - self.inference_method = inference_method - self.mcmc_params = mcmc_params - self.perturbed_data = perturbed_data - self.bayes_loocv = bayes_loocv - self.n_bootstrap_itrs = n_bootstrap_itrs - self.bootstrap_noise = bootstrap_noise - self.just_analysis = just_analysis - self.valid_metrics = valid_metrics - self.plot_post_pred = plot_post_pred - self.plot_map_pred = plot_map_pred - self.max_a_posteriori = max_a_posteriori - self.corner_title_fmt = corner_title_fmt - - # ------------------------------------------------------------------------- - def create_inference(self): - """ - Starts the inference. - - Returns - ------- - BayesInference : obj - The Bayes inference object. - - """ - - # Set some variables - if self.MetaModel is not None: - MetaModel = self.MetaModel - Model = MetaModel.ModelObj - n_params = MetaModel.n_params - output_names = Model.Output.names - par_names = MetaModel.ExpDesign.par_names - else: - - - # If the prior is set by the user, take it. - if self.samples is None: - self.samples = MetaModel.ExpDesign.generate_samples( - self.n_samples, 'random') - else: - try: - samples = self.samples.values - except AttributeError: - samples = self.samples - - # Take care of an additional Sigma2s - self.samples = samples[:, :n_params] - - # Update number of samples - self.n_samples = self.samples.shape[0] - - # ---------- Preparation of observation data ---------- - # Read observation data and perturb it if requested. - if self.measured_data is None: - self.measured_data = Model.read_observation(case=self.name) - # Convert measured_data to a data frame - if not isinstance(self.measured_data, pd.DataFrame): - self.measured_data = pd.DataFrame(self.measured_data) - - # Extract the total number of measurement points - if self.name.lower() == 'calib': - self.n_tot_measurement = Model.n_obs - else: - self.n_tot_measurement = Model.n_obs_valid - - # Find measurement error (if not given) for post predictive plot - if not hasattr(self, 'measurement_error'): - if isinstance(self.Discrepancy, dict): - Disc = self.Discrepancy['known'] - else: - Disc = self.Discrepancy - if isinstance(Disc.parameters, dict): - self.measurement_error = {k: np.sqrt(Disc.parameters[k]) for k - in Disc.parameters.keys()} - else: - try: - self.measurement_error = np.sqrt(Disc.parameters) - except TypeError: - pass - - # ---------- Preparation of variance for covariance matrix ---------- - # Independent and identically distributed - total_sigma2 = dict() - opt_sigma_flag = isinstance(self.Discrepancy, dict) - opt_sigma = None - for key_idx, key in enumerate(output_names): - - # Find opt_sigma - if opt_sigma_flag and opt_sigma is None: - # Option A: known error with unknown bias term - opt_sigma = 'A' - known_discrepancy = self.Discrepancy['known'] - self.Discrepancy = self.Discrepancy['infer'] - sigma2 = np.array(known_discrepancy.parameters[key]) - - elif opt_sigma == 'A' or self.Discrepancy.parameters is not None: - # Option B: The sigma2 is known (no bias term) - if opt_sigma == 'A': - sigma2 = np.array(known_discrepancy.parameters[key]) - else: - opt_sigma = 'B' - sigma2 = np.array(self.Discrepancy.parameters[key]) - - elif not isinstance(self.Discrepancy.InputDisc, str): - # Option C: The sigma2 is unknown (bias term including error) - opt_sigma = 'C' - self.Discrepancy.opt_sigma = opt_sigma - n_measurement = self.measured_data[key].values.shape - sigma2 = np.zeros((n_measurement[0])) - - total_sigma2[key] = sigma2 - - self.Discrepancy.opt_sigma = opt_sigma - self.Discrepancy.total_sigma2 = total_sigma2 - - # If inferred sigma2s obtained from e.g. calibration are given - try: - self.sigma2s = self.Discrepancy.get_sample(self.n_samples) - except: - pass - - # ---------------- Bootstrap & TOM -------------------- - if self.bootstrap or self.bayes_loocv or self.just_analysis: - if len(self.perturbed_data) == 0: - # zero mean noise Adding some noise to the observation function - self.perturbed_data = self._perturb_data( - self.measured_data, output_names - ) - else: - self.n_bootstrap_itrs = len(self.perturbed_data) - - # -------- Model Discrepancy ----------- - if hasattr(self, 'error_model') and self.error_model \ - and self.name.lower() != 'calib': - # Select posterior mean as MAP - MAP_theta = self.samples.mean(axis=0).reshape((1, n_params)) - # MAP_theta = stats.mode(self.samples,axis=0)[0] - - # Evaluate the (meta-)model at the MAP - y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=MAP_theta) - - # Train a GPR meta-model using MAP - print('Create error meta model') - self.error_MetaModel = MetaModel.create_model_error( - self.bias_inputs, y_MAP, Name=self.name - ) - - # ----------------------------------------------------- - # ----- Loop over the perturbed observation data ------ - # ----------------------------------------------------- - # Initilize arrays - logLikelihoods = np.zeros((self.n_samples, self.n_bootstrap_itrs), - dtype=np.float16) - BME_Corr = np.zeros((self.n_bootstrap_itrs)) - log_BME = np.zeros((self.n_bootstrap_itrs)) - KLD = np.zeros((self.n_bootstrap_itrs)) - inf_entropy = np.zeros((self.n_bootstrap_itrs)) - - # Compute the prior predtions - # Evaluate the MetaModel - if self.emulator: - y_hat, y_std = MetaModel.eval_metamodel(samples=self.samples) - self.__mean_pce_prior_pred = y_hat - self._std_pce_prior_pred = y_std - - # Correct the predictions with Model discrepancy - if hasattr(self, 'error_model') and self.error_model: - y_hat_corr, y_std = self.error_MetaModel.eval_model_error( - self.bias_inputs, self.__mean_pce_prior_pred - ) - self.__mean_pce_prior_pred = y_hat_corr - self._std_pce_prior_pred = y_std - - # Surrogate model's error using RMSE of test data - if hasattr(MetaModel, 'rmse'): - surrError = MetaModel.rmse - else: - surrError = None - - else: - # Evaluate the original model - self.__model_prior_pred = self._eval_model( - samples=self.samples, key='PriorPred' - ) - surrError = None - - # Start the likelihood-BME computations for the perturbed data - for itr_idx, data in tqdm( - enumerate(self.perturbed_data), - total=self.n_bootstrap_itrs, - desc="Boostraping the BME calculations", ascii=True - ): - - # ---------------- Likelihood calculation ---------------- - if self.emulator: - model_evals = self.__mean_pce_prior_pred - else: - model_evals = self.__model_prior_pred - - # Leave one out - if self.bayes_loocv or self.just_analysis: - self.selected_indices = np.nonzero(data)[0] - - # Prepare data dataframe - nobs = list(self.measured_data.count().values[1:]) - numbers = list(np.cumsum(nobs)) - indices = list(zip([0] + numbers, numbers)) - data_dict = { - output_names[i]: data[j:k] for i, (j, k) in - enumerate(indices) - } - - # Unknown sigma2 - if opt_sigma == 'C' or hasattr(self, 'sigma2s'): - logLikelihoods[:, itr_idx] = self.normpdf( - model_evals, data_dict, total_sigma2, - sigma2=self.sigma2s, std=surrError - ) - else: - # known sigma2 - logLikelihoods[:, itr_idx] = self.normpdf( - model_evals, data_dict, total_sigma2, - std=surrError - ) - - # ---------------- BME Calculations ---------------- - # BME (log) - log_BME[itr_idx] = np.log( - np.nanmean(np.exp(logLikelihoods[:, itr_idx], - dtype=np.longdouble)) # changed for windows - ) - - # BME correction when using Emulator - if self.emulator: - BME_Corr[itr_idx] = self.__corr_factor_BME( - data_dict, total_sigma2, log_BME[itr_idx] - ) - - # Rejection Step - if 'kld' in list(map(str.lower, self.valid_metrics)) and\ - 'inf_entropy' in list(map(str.lower, self.valid_metrics)): - # Random numbers between 0 and 1 - unif = np.random.rand(1, self.n_samples)[0] - - # Reject the poorly performed prior - Likelihoods = np.exp(logLikelihoods[:, itr_idx], - dtype=np.float64) - accepted = (Likelihoods/np.max(Likelihoods)) >= unif - posterior = self.samples[accepted] - - # Posterior-based expectation of likelihoods - postExpLikelihoods = np.mean( - logLikelihoods[:, itr_idx][accepted] - ) - - # Calculate Kullback-Leibler Divergence - KLD[itr_idx] = postExpLikelihoods - log_BME[itr_idx] - - # Posterior-based expectation of prior densities - if 'inf_entropy' in list(map(str.lower, self.valid_metrics)): - n_thread = int(0.875 * multiprocessing.cpu_count()) - with multiprocessing.Pool(n_thread) as p: - postExpPrior = np.mean(np.concatenate( - p.map( - MetaModel.ExpDesign.JDist.pdf, - np.array_split(posterior.T, n_thread, axis=1)) - ) - ) - # Information Entropy based on Entropy paper Eq. 38 - inf_entropy[itr_idx] = log_BME[itr_idx] - postExpPrior - \ - postExpLikelihoods - - # Clear memory - gc.collect(generation=2) - - # ---------- Store metrics for perturbed data set ---------------- - # Likelihoods (Size: n_samples, n_bootstrap_itr) - self.log_likes = logLikelihoods - - # BME (log), KLD, infEntropy (Size: 1,n_bootstrap_itr) - self.log_BME = log_BME - - # BMECorrFactor (log) (Size: 1,n_bootstrap_itr) - if self.emulator: - self.log_BME_corr_factor = BME_Corr - - if 'kld' in list(map(str.lower, self.valid_metrics)): - self.KLD = KLD - if 'inf_entropy' in list(map(str.lower, self.valid_metrics)): - self.inf_entropy = inf_entropy - - # BME = BME + BMECorrFactor - if self.emulator: - self.log_BME += self.log_BME_corr_factor - - # ---------------- Parameter Bayesian inference ---------------- - if self.inference_method.lower() == 'mcmc': - # Instantiate the MCMC object - MCMC_Obj = MCMC(self) - self.MCMC_Obj = MCMC_Obj - self.posterior_df = MCMC_Obj.run_sampler( - self.measured_data, total_sigma2 - ) - - elif self.name.lower() == 'valid': - # Convert to a dataframe if samples are provided after calibration. - self.posterior_df = pd.DataFrame(self.samples, columns=par_names) - - else: - # Rejection sampling - self.posterior_df = self._rejection_sampling() - # Provide posterior's summary - print('\n') - print('-'*15 + 'Posterior summary' + '-'*15) - pd.options.display.max_columns = None - pd.options.display.max_rows = None - print(self.posterior_df.describe()) - print('-'*50) - - # -------- Model Discrepancy ----------- - if hasattr(self, 'error_model') and self.error_model \ - and self.name.lower() == 'calib': - if self.inference_method.lower() == 'mcmc': - self.error_MetaModel = MCMC_Obj.error_MetaModel - else: - # Select posterior mean as MAP - if opt_sigma == "B": - posterior_df = self.posterior_df.values - else: - posterior_df = self.posterior_df.values[:, :-Model.n_outputs] - - # Select posterior mean as Maximum a posteriori - map_theta = posterior_df.mean(axis=0).reshape((1, n_params)) - # map_theta = stats.mode(Posterior_df,axis=0)[0] - - # Evaluate the (meta-)model at the MAP - y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=map_theta) - - # Train a GPR meta-model using MAP - self.error_MetaModel = MetaModel.create_model_error( - self.bias_inputs, y_MAP, Name=self.name - ) - - # -------- Posterior perdictive ----------- - self._posterior_predictive() - - # ----------------------------------------------------- - # ------------------ Visualization -------------------- - # ----------------------------------------------------- - # Create Output directory, if it doesn't exist already. - out_dir = f'Outputs_Bayes_{Model.name}_{self.name}' - os.makedirs(out_dir, exist_ok=True) - - # -------- Posteior parameters -------- - if opt_sigma != "B": - par_names.extend( - [self.Discrepancy.InputDisc.Marginals[i].name for i - in range(len(self.Discrepancy.InputDisc.Marginals))] - ) - # Pot with corner - figPosterior = corner.corner(self.posterior_df.to_numpy(), - labels=par_names, - quantiles=[0.15, 0.5, 0.85], - show_titles=True, - title_fmt=self.corner_title_fmt, - labelpad=0.2, - use_math_text=True, - title_kwargs={"fontsize": 28}, - plot_datapoints=False, - plot_density=False, - fill_contours=True, - smooth=0.5, - smooth1d=0.5) - - # Loop over axes and set x limits - if opt_sigma == "B": - axes = np.array(figPosterior.axes).reshape( - (len(par_names), len(par_names)) - ) - for yi in range(len(par_names)): - ax = axes[yi, yi] - ax.set_xlim(MetaModel.bound_tuples[yi]) - for xi in range(yi): - ax = axes[yi, xi] - ax.set_xlim(MetaModel.bound_tuples[xi]) - plt.close() - - # Turn off gridlines - for ax in figPosterior.axes: - ax.grid(False) - - if self.emulator: - plotname = f'/Posterior_Dist_{Model.name}_emulator' - else: - plotname = f'/Posterior_Dist_{Model.name}' - - figPosterior.set_size_inches((24, 16)) - figPosterior.savefig(f'./{out_dir}{plotname}.pdf', - bbox_inches='tight') - - # -------- Plot MAP -------- - if self.plot_map_pred: - self._plot_max_a_posteriori() - - # -------- Plot log_BME dist -------- - if self.bootstrap: - - # Computing the TOM performance - self.log_BME_tom = stats.chi2.rvs( - self.n_tot_measurement, size=self.log_BME.shape[0] - ) - - fig, ax = plt.subplots() - sns.kdeplot(self.log_BME_tom, ax=ax, color="green", shade=True) - sns.kdeplot( - self.log_BME, ax=ax, color="blue", shade=True, - label='Model BME') - - ax.set_xlabel('log$_{10}$(BME)') - ax.set_ylabel('Probability density') - - legend_elements = [ - Patch(facecolor='green', edgecolor='green', label='TOM BME'), - Patch(facecolor='blue', edgecolor='blue', label='Model BME') - ] - ax.legend(handles=legend_elements) - - if self.emulator: - plotname = f'/BME_hist_{Model.name}_emulator' - else: - plotname = f'/BME_hist_{Model.name}' - - plt.savefig(f'./{out_dir}{plotname}.pdf', bbox_inches='tight') - plt.show() - plt.close() - - # -------- Posteior perdictives -------- - if self.plot_post_pred: - # Plot the posterior predictive - self._plot_post_predictive() - - return self - - # ------------------------------------------------------------------------- - def _perturb_data(self, data, output_names): - """ - Returns an array with n_bootstrap_itrs rowsof perturbed data. - The first row includes the original observation data. - If `self.bayes_loocv` is True, a 2d-array will be returned with - repeated rows and zero diagonal entries. - - Parameters - ---------- - data : pandas DataFrame - Observation data. - output_names : list - List of the output names. - - Returns - ------- - final_data : array - Perturbed data set. - - """ - noise_level = self.bootstrap_noise - obs_data = data[output_names].values - n_measurement, n_outs = obs_data.shape - self.n_tot_measurement = obs_data[~np.isnan(obs_data)].shape[0] - # Number of bootstrap iterations - if self.bayes_loocv: - self.n_bootstrap_itrs = self.n_tot_measurement - - # Pass loocv dataset - if self.bayes_loocv: - obs = obs_data.T[~np.isnan(obs_data.T)] - final_data = np.repeat(np.atleast_2d(obs), self.n_bootstrap_itrs, - axis=0) - np.fill_diagonal(final_data, 0) - return final_data - - else: - final_data = np.zeros( - (self.n_bootstrap_itrs, self.n_tot_measurement) - ) - final_data[0] = obs_data.T[~np.isnan(obs_data.T)] - for itrIdx in range(1, self.n_bootstrap_itrs): - data = np.zeros((n_measurement, n_outs)) - for idx in range(len(output_names)): - std = np.nanstd(obs_data[:, idx]) - if std == 0: - std = 0.001 - noise = std * noise_level - data[:, idx] = np.add( - obs_data[:, idx], - np.random.normal(0, 1, obs_data.shape[0]) * noise, - ) - - final_data[itrIdx] = data.T[~np.isnan(data.T)] - - return final_data - - # ------------------------------------------------------------------------- - def _logpdf(self, x, mean, cov): - """ - computes the likelihood based on a multivariate normal distribution. - - Parameters - ---------- - x : TYPE - DESCRIPTION. - mean : array_like - Observation data. - cov : 2d array - Covariance matrix of the distribution. - - Returns - ------- - log_lik : float - Log likelihood. - - """ - n = len(mean) - L = spla.cholesky(cov, lower=True) - beta = np.sum(np.log(np.diag(L))) - dev = x - mean - alpha = dev.dot(spla.cho_solve((L, True), dev)) - log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi) - return log_lik - - # ------------------------------------------------------------------------- - def _eval_model(self, samples=None, key='MAP'): - """ - Evaluates Forward Model. - - Parameters - ---------- - samples : array of shape (n_samples, n_params), optional - Parameter sets. The default is None. - key : str, optional - Key string to be passed to the run_model_parallel method. - The default is 'MAP'. - - Returns - ------- - model_outputs : dict - Model outputs. - - """ - MetaModel = self.MetaModel - Model = MetaModel.ModelObj - - if samples is None: - self.samples = MetaModel.ExpDesign.generate_samples( - self.n_samples, 'random') - else: - self.samples = samples - self.n_samples = len(samples) - - model_outputs, _ = Model.run_model_parallel( - self.samples, key_str=key+self.name) - - # Clean up - # Zip the subdirectories - try: - dir_name = f'{Model.name}MAP{self.name}' - key = dir_name + '_' - Model.zip_subdirs(dir_name, key) - except: - pass - - return model_outputs - - # ------------------------------------------------------------------------- - def _kernel_rbf(self, X, hyperparameters): - """ - Isotropic squared exponential kernel. - - Higher l values lead to smoother functions and therefore to coarser - approximations of the training data. Lower l values make functions - more wiggly with wide uncertainty regions between training data points. - - sigma_f controls the marginal variance of b(x) - - Parameters - ---------- - X : ndarray of shape (n_samples_X, n_features) - - hyperparameters : Dict - Lambda characteristic length - sigma_f controls the marginal variance of b(x) - sigma_0 unresolvable error nugget term, interpreted as random - error that cannot be attributed to measurement error. - Returns - ------- - var_cov_matrix : ndarray of shape (n_samples_X,n_samples_X) - Kernel k(X, X). - - """ - from sklearn.gaussian_process.kernels import RBF - min_max_scaler = preprocessing.MinMaxScaler() - X_minmax = min_max_scaler.fit_transform(X) - - nparams = len(hyperparameters) - # characteristic length (0,1] - Lambda = hyperparameters[0] - # sigma_f controls the marginal variance of b(x) - sigma2_f = hyperparameters[1] - - # cov_matrix = sigma2_f*rbf_kernel(X_minmax, gamma = 1/Lambda**2) - - rbf = RBF(length_scale=Lambda) - cov_matrix = sigma2_f * rbf(X_minmax) - if nparams > 2: - # (unresolvable error) nugget term that is interpreted as random - # error that cannot be attributed to measurement error. - sigma2_0 = hyperparameters[2:] - for i, j in np.ndindex(cov_matrix.shape): - cov_matrix[i, j] += np.sum(sigma2_0) if i == j else 0 - - return cov_matrix - - # ------------------------------------------------------------------------- - def normpdf(self, outputs, obs_data, total_sigma2s, sigma2=None, std=None): - """ - Calculates the likelihood of simulation outputs compared with - observation data. - - Parameters - ---------- - outputs : dict - A dictionary containing the simulation outputs as array of shape - (n_samples, n_measurement) for each model output. - obs_data : dict - A dictionary/dataframe containing the observation data. - total_sigma2s : dict - A dictionary with known values of the covariance diagonal entries, - a.k.a sigma^2. - sigma2 : array, optional - An array of the sigma^2 samples, when the covariance diagonal - entries are unknown and are being jointly inferred. The default is - None. - std : dict, optional - A dictionary containing the root mean squared error as array of - shape (n_samples, n_measurement) for each model output. The default - is None. - - Returns - ------- - logLik : array of shape (n_samples) - Likelihoods. - - """ - Model = self.MetaModel.ModelObj - logLik = 0.0 - - # Extract the requested model outputs for likelihood calulation - if self.req_outputs is None: - req_outputs = Model.Output.names - else: - req_outputs = list(self.req_outputs) - - # Loop over the outputs - for idx, out in enumerate(req_outputs): - - # (Meta)Model Output - nsamples, nout = outputs[out].shape - - # Prepare data and remove NaN - try: - data = obs_data[out].values[~np.isnan(obs_data[out])] - except AttributeError: - data = obs_data[out][~np.isnan(obs_data[out])] - - # Prepare sigma2s - non_nan_indices = ~np.isnan(total_sigma2s[out]) - tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout] - - # Add the std of the PCE is chosen as emulator. - if self.emulator: - if std is not None: - tot_sigma2s += std[out]**2 - - # Covariance Matrix - covMatrix = np.diag(tot_sigma2s) - - # Select the data points to compare - try: - indices = self.selected_indices[out] - except: - indices = list(range(nout)) - covMatrix = np.diag(covMatrix[indices, indices]) - - # If sigma2 is not given, use given total_sigma2s - if sigma2 is None: - logLik += stats.multivariate_normal.logpdf( - outputs[out][:, indices], data[indices], covMatrix) - continue - - # Loop over each run/sample and calculate logLikelihood - logliks = np.zeros(nsamples) - for s_idx in range(nsamples): - - # Simulation run - tot_outputs = outputs[out] - - # Covariance Matrix - covMatrix = np.diag(tot_sigma2s) - - if sigma2 is not None: - # Check the type error term - if hasattr(self, 'bias_inputs') and \ - not hasattr(self, 'error_model'): - # Infer a Bias model usig Gaussian Process Regression - bias_inputs = np.hstack( - (self.bias_inputs[out], - tot_outputs[s_idx].reshape(-1, 1))) - - params = sigma2[s_idx, idx*3:(idx+1)*3] - covMatrix = self._kernel_rbf(bias_inputs, params) - else: - # Infer equal sigma2s - try: - sigma_2 = sigma2[s_idx, idx] - except TypeError: - sigma_2 = 0.0 - - covMatrix += sigma_2 * np.eye(nout) - # covMatrix = np.diag(sigma2 * total_sigma2s) - - # Select the data points to compare - try: - indices = self.selected_indices[out] - except: - indices = list(range(nout)) - covMatrix = np.diag(covMatrix[indices, indices]) - - # Compute loglikelihood - logliks[s_idx] = self._logpdf( - tot_outputs[s_idx, indices], data[indices], covMatrix - ) - - logLik += logliks - return logLik - - # ------------------------------------------------------------------------- - def _corr_factor_BME_old(self, Data, total_sigma2s, posterior): - """ - Calculates the correction factor for BMEs. - """ - MetaModel = self.MetaModel - OrigModelOutput = MetaModel.ExpDesign.Y - Model = MetaModel.ModelObj - - # Posterior with guassian-likelihood - postDist = stats.gaussian_kde(posterior.T) - - # Remove NaN - Data = Data[~np.isnan(Data)] - total_sigma2s = total_sigma2s[~np.isnan(total_sigma2s)] - - # Covariance Matrix - covMatrix = np.diag(total_sigma2s[:self.n_tot_measurement]) - - # Extract the requested model outputs for likelihood calulation - if self.req_outputs is None: - OutputType = Model.Output.names - else: - OutputType = list(self.req_outputs) - - # SampleSize = OrigModelOutput[OutputType[0]].shape[0] - - - # Flatten the OutputType for OrigModel - TotalOutputs = np.concatenate([OrigModelOutput[x] for x in OutputType], 1) - - NrofBayesSamples = self.n_samples - # Evaluate MetaModel on the experimental design - Samples = MetaModel.ExpDesign.X - OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=Samples) - - # Reset the NrofSamples to NrofBayesSamples - self.n_samples = NrofBayesSamples - - # Flatten the OutputType for MetaModel - TotalPCEOutputs = np.concatenate([OutputRS[x] for x in OutputRS], 1) - TotalPCEstdOutputRS= np.concatenate([stdOutputRS[x] for x in stdOutputRS], 1) - - logweight = 0 - for i, sample in enumerate(Samples): - # Compute likelilhood output vs RS - covMatrix = np.diag(TotalPCEstdOutputRS[i]**2) - logLik = self._logpdf(TotalOutputs[i], TotalPCEOutputs[i], covMatrix) - # Compute posterior likelihood of the collocation points - logpostLik = np.log(postDist.pdf(sample[:, None]))[0] - if logpostLik != -np.inf: - logweight += logLik + logpostLik - return logweight - - # ------------------------------------------------------------------------- - def __corr_factor_BME(self, obs_data, total_sigma2s, logBME): - """ - Calculates the correction factor for BMEs. - """ - MetaModel = self.MetaModel - samples = MetaModel.ExpDesign.X - model_outputs = MetaModel.ExpDesign.Y - Model = MetaModel.ModelObj - n_samples = samples.shape[0] - - # Extract the requested model outputs for likelihood calulation - output_names = Model.Output.names - - # Evaluate MetaModel on the experimental design and ValidSet - OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples) - - logLik_data = np.zeros((n_samples)) - logLik_model = np.zeros((n_samples)) - # Loop over the outputs - for idx, out in enumerate(output_names): - - # (Meta)Model Output - nsamples, nout = model_outputs[out].shape - - # Prepare data and remove NaN - try: - data = obs_data[out].values[~np.isnan(obs_data[out])] - except AttributeError: - data = obs_data[out][~np.isnan(obs_data[out])] - - # Prepare sigma2s - non_nan_indices = ~np.isnan(total_sigma2s[out]) - tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout] - - # Covariance Matrix - covMatrix_data = np.diag(tot_sigma2s) - - for i, sample in enumerate(samples): - - # Simulation run - y_m = model_outputs[out][i] - - # Surrogate prediction - y_m_hat = OutputRS[out][i] - - # CovMatrix with the surrogate error - covMatrix = np.eye(len(y_m)) * 1/(2*np.pi) - - # Select the data points to compare - try: - indices = self.selected_indices[out] - except: - indices = list(range(nout)) - covMatrix = np.diag(covMatrix[indices, indices]) - covMatrix_data = np.diag(covMatrix_data[indices, indices]) - - # Compute likelilhood output vs data - logLik_data[i] += self._logpdf( - y_m_hat[indices], data[indices], - covMatrix_data - ) - - # Compute likelilhood output vs surrogate - logLik_model[i] += self._logpdf( - y_m_hat[indices], y_m[indices], - covMatrix - ) - - # Weight - logLik_data -= logBME - weights = np.mean(np.exp(logLik_model+logLik_data)) - - return np.log(weights) - - # ------------------------------------------------------------------------- - def _rejection_sampling(self): - """ - Performs rejection sampling to update the prior distribution on the - input parameters. - - Returns - ------- - posterior : pandas.dataframe - Posterior samples of the input parameters. - - """ - - MetaModel = self.MetaModel - try: - sigma2_prior = self.Discrepancy.sigma2_prior - except: - sigma2_prior = None - - # Check if the discrepancy is defined as a distribution: - samples = self.samples - - if sigma2_prior is not None: - samples = np.hstack((samples, sigma2_prior)) - - # Take the first column of Likelihoods (Observation data without noise) - if self.just_analysis or self.bayes_loocv: - index = self.n_tot_measurement-1 - # account for numerical overflow, especially for windows - scale = 0 - if np.max(self.log_likes[:, index])>709: - scale = np.max(self.log_likes[:, index])-709 - - self.log_likes[:, 0]-=scale - likelihoods = np.exp(self.log_likes[:, index], dtype=np.longdouble) - else: - # account for numerical overflow, especially for windows - scale = 0 - if np.max(self.log_likes[:, 0])>709: - scale = np.max(self.log_likes[:, 0])-709 - - self.log_likes[:, 0]-=scale - likelihoods = np.exp(self.log_likes[:, 0], dtype=np.longdouble) - - n_samples = len(likelihoods) - norm_ikelihoods = likelihoods / np.max(likelihoods) - - # Normalize based on min if all Likelihoods are zero - if all(likelihoods == 0.0): - likelihoods = self.log_likes[:, 0] - norm_ikelihoods = likelihoods / np.min(likelihoods) - - # Random numbers between 0 and 1 - unif = np.random.rand(1, n_samples)[0] - - # Reject the poorly performed prior - accepted_samples = samples[norm_ikelihoods >= unif] - - # Output the Posterior - par_names = MetaModel.ExpDesign.par_names - if sigma2_prior is not None: - for name in self.Discrepancy.name: - par_names.append(name) - - return pd.DataFrame(accepted_samples, columns=sigma2_prior) - - # ------------------------------------------------------------------------- - def _posterior_predictive(self): - """ - Stores the prior- and posterior predictive samples, i.e. model - evaluations using the samples, into hdf5 files. - - priorPredictive.hdf5 : Prior predictive samples. - postPredictive_wo_noise.hdf5 : Posterior predictive samples without - the additive noise. - postPredictive.hdf5 : Posterior predictive samples with the additive - noise. - - Returns - ------- - None. - - """ - - MetaModel = self.MetaModel - Model = MetaModel.ModelObj - - # Make a directory to save the prior/posterior predictive - out_dir = f'Outputs_Bayes_{Model.name}_{self.name}' - os.makedirs(out_dir, exist_ok=True) - - # Read observation data and perturb it if requested - if self.measured_data is None: - self.measured_data = Model.read_observation(case=self.name) - - if not isinstance(self.measured_data, pd.DataFrame): - self.measured_data = pd.DataFrame(self.measured_data) - - # X_values - x_values = MetaModel.ExpDesign.x_values - - try: - sigma2_prior = self.Discrepancy.sigma2_prior - except: - sigma2_prior = None - - # Extract posterior samples - posterior_df = self.posterior_df - - # Take care of the sigma2 - if sigma2_prior is not None: - try: - sigma2s = posterior_df[self.Discrepancy.name].values - posterior_df = posterior_df.drop( - labels=self.Discrepancy.name, axis=1 - ) - except: - sigma2s = self.sigma2s - - # Posterior predictive - if self.emulator: - if self.inference_method == 'rejection': - prior_pred = self.__mean_pce_prior_pred - if self.name.lower() != 'calib': - post_pred = self.__mean_pce_prior_pred - post_pred_std = self._std_pce_prior_pred - else: - post_pred, post_pred_std = MetaModel.eval_metamodel( - samples=posterior_df.values - ) - - else: - if self.inference_method == 'rejection': - prior_pred = self.__model_prior_pred - if self.name.lower() != 'calib': - post_pred = self.__mean_pce_prior_pred, - post_pred_std = self._std_pce_prior_pred - else: - post_pred = self._eval_model( - samples=posterior_df.values, key='PostPred' - ) - # Correct the predictions with Model discrepancy - if hasattr(self, 'error_model') and self.error_model: - y_hat, y_std = self.error_MetaModel.eval_model_error( - self.bias_inputs, post_pred - ) - post_pred, post_pred_std = y_hat, y_std - - # Add discrepancy from likelihood Sample to the current posterior runs - total_sigma2 = self.Discrepancy.total_sigma2 - post_pred_withnoise = copy.deepcopy(post_pred) - for varIdx, var in enumerate(Model.Output.names): - for i in range(len(post_pred[var])): - pred = post_pred[var][i] - - # Known sigma2s - clean_sigma2 = total_sigma2[var][~np.isnan(total_sigma2[var])] - tot_sigma2 = clean_sigma2[:len(pred)] - cov = np.diag(tot_sigma2) - - # Check the type error term - if sigma2_prior is not None: - # Inferred sigma2s - if hasattr(self, 'bias_inputs') and \ - not hasattr(self, 'error_model'): - # TODO: Infer a Bias model usig GPR - bias_inputs = np.hstack(( - self.bias_inputs[var], pred.reshape(-1, 1))) - params = sigma2s[i, varIdx*3:(varIdx+1)*3] - cov = self._kernel_rbf(bias_inputs, params) - else: - # Infer equal sigma2s - try: - sigma2 = sigma2s[i, varIdx] - except TypeError: - sigma2 = 0.0 - - # Convert biasSigma2s to a covMatrix - cov += sigma2 * np.eye(len(pred)) - - if self.emulator: - if hasattr(MetaModel, 'rmse') and \ - MetaModel.rmse is not None: - stdPCE = MetaModel.rmse[var] - else: - stdPCE = post_pred_std[var][i] - # Expected value of variance (Assump: i.i.d stds) - cov += np.diag(stdPCE**2) - - # Sample a multivariate normal distribution with mean of - # prediction and variance of cov - post_pred_withnoise[var][i] = np.random.multivariate_normal( - pred, cov, 1 - ) - - # ----- Prior Predictive ----- - if self.inference_method.lower() == 'rejection': - # Create hdf5 metadata - hdf5file = f'{out_dir}/priorPredictive.hdf5' - hdf5_exist = os.path.exists(hdf5file) - if hdf5_exist: - os.remove(hdf5file) - file = h5py.File(hdf5file, 'a') - - # Store x_values - if type(x_values) is dict: - grp_x_values = file.create_group("x_values/") - for varIdx, var in enumerate(Model.Output.names): - grp_x_values.create_dataset(var, data=x_values[var]) - else: - file.create_dataset("x_values", data=x_values) - - # Store posterior predictive - grpY = file.create_group("EDY/") - for varIdx, var in enumerate(Model.Output.names): - grpY.create_dataset(var, data=prior_pred[var]) - - # ----- Posterior Predictive only model evaluations ----- - # Create hdf5 metadata - hdf5file = out_dir+'/postPredictive_wo_noise.hdf5' - hdf5_exist = os.path.exists(hdf5file) - if hdf5_exist: - os.remove(hdf5file) - file = h5py.File(hdf5file, 'a') - - # Store x_values - if type(x_values) is dict: - grp_x_values = file.create_group("x_values/") - for varIdx, var in enumerate(Model.Output.names): - grp_x_values.create_dataset(var, data=x_values[var]) - else: - file.create_dataset("x_values", data=x_values) - - # Store posterior predictive - grpY = file.create_group("EDY/") - for varIdx, var in enumerate(Model.Output.names): - grpY.create_dataset(var, data=post_pred[var]) - - # ----- Posterior Predictive with noise ----- - # Create hdf5 metadata - hdf5file = out_dir+'/postPredictive.hdf5' - hdf5_exist = os.path.exists(hdf5file) - if hdf5_exist: - os.remove(hdf5file) - file = h5py.File(hdf5file, 'a') - - # Store x_values - if type(x_values) is dict: - grp_x_values = file.create_group("x_values/") - for varIdx, var in enumerate(Model.Output.names): - grp_x_values.create_dataset(var, data=x_values[var]) - else: - file.create_dataset("x_values", data=x_values) - - # Store posterior predictive - grpY = file.create_group("EDY/") - for varIdx, var in enumerate(Model.Output.names): - grpY.create_dataset(var, data=post_pred_withnoise[var]) - - return - - # ------------------------------------------------------------------------- - def _plot_max_a_posteriori(self): - """ - Plots the response of the model output against that of the metamodel at - the maximum a posteriori sample (mean or mode of posterior.) - - Returns - ------- - None. - - """ - - MetaModel = self.MetaModel - Model = MetaModel.ModelObj - out_dir = f'Outputs_Bayes_{Model.name}_{self.name}' - opt_sigma = self.Discrepancy.opt_sigma - - # -------- Find MAP and run MetaModel and origModel -------- - # Compute the MAP - if self.max_a_posteriori.lower() == 'mean': - if opt_sigma == "B": - Posterior_df = self.posterior_df.values - else: - Posterior_df = self.posterior_df.values[:, :-Model.n_outputs] - map_theta = Posterior_df.mean(axis=0).reshape( - (1, MetaModel.n_params)) - else: - map_theta = stats.mode(Posterior_df.values, axis=0)[0] - # Prin report - print("\nPoint estimator:\n", map_theta[0]) - - # Run the models for MAP - # MetaModel - map_metamodel_mean, map_metamodel_std = MetaModel.eval_metamodel( - samples=map_theta) - self.map_metamodel_mean = map_metamodel_mean - self.map_metamodel_std = map_metamodel_std - - # origModel - map_orig_model = self._eval_model(samples=map_theta) - self.map_orig_model = map_orig_model - - # Extract slicing index - x_values = map_orig_model['x_values'] - - # List of markers and colors - Color = ['k', 'b', 'g', 'r'] - Marker = 'x' - - # Create a PdfPages object - pdf = PdfPages(f'./{out_dir}MAP_PCE_vs_Model_{self.name}.pdf') - fig = plt.figure() - for i, key in enumerate(Model.Output.names): - - y_val = map_orig_model[key] - y_pce_val = map_metamodel_mean[key] - y_pce_val_std = map_metamodel_std[key] - - plt.plot(x_values, y_val, color=Color[i], marker=Marker, - lw=2.0, label='$Y_{MAP}^{M}$') - - plt.plot( - x_values, y_pce_val[i], color=Color[i], lw=2.0, - marker=Marker, linestyle='--', label='$Y_{MAP}^{PCE}$' - ) - # plot the confidence interval - plt.fill_between( - x_values, y_pce_val[i] - 1.96*y_pce_val_std[i], - y_pce_val[i] + 1.96*y_pce_val_std[i], - color=Color[i], alpha=0.15 - ) - - # Calculate the adjusted R_squared and RMSE - R2 = r2_score(y_pce_val.reshape(-1, 1), y_val.reshape(-1, 1)) - rmse = np.sqrt(mean_squared_error(y_pce_val, y_val)) - - plt.ylabel(key) - plt.xlabel("Time [s]") - plt.title(f'Model vs MetaModel {key}') - - ax = fig.axes[0] - leg = ax.legend(loc='best', frameon=True) - fig.canvas.draw() - p = leg.get_window_extent().inverse_transformed(ax.transAxes) - ax.text( - p.p0[1]-0.05, p.p1[1]-0.25, - f'RMSE = {rmse:.3f}\n$R^2$ = {R2:.3f}', - transform=ax.transAxes, color='black', - bbox=dict(facecolor='none', edgecolor='black', - boxstyle='round,pad=1')) - - plt.show() - - # save the current figure - pdf.savefig(fig, bbox_inches='tight') - - # Destroy the current plot - plt.clf() - - pdf.close() - - # ------------------------------------------------------------------------- - def _plot_post_predictive(self): - """ - Plots the posterior predictives against the observation data. - - Returns - ------- - None. - - """ - - Model = self.MetaModel.ModelObj - out_dir = f'Outputs_Bayes_{Model.name}_{self.name}' - # Plot the posterior predictive - for out_idx, out_name in enumerate(Model.Output.names): - fig, ax = plt.subplots() - with sns.axes_style("ticks"): - x_key = list(self.measured_data)[0] - - # --- Read prior and posterior predictive --- - if self.inference_method == 'rejection' and \ - self.name.lower() != 'valid': - # --- Prior --- - # Load posterior predictive - f = h5py.File( - f'{out_dir}/priorPredictive.hdf5', 'r+') - - try: - x_coords = np.array(f[f"x_values/{out_name}"]) - except: - x_coords = np.array(f["x_values"]) - - X_values = np.repeat(x_coords, 10000) - - prior_pred_df = {} - prior_pred_df[x_key] = X_values - prior_pred_df[out_name] = np.array( - f[f"EDY/{out_name}"])[:10000].flatten('F') - prior_pred_df = pd.DataFrame(prior_pred_df) - - tags_post = ['prior'] * len(prior_pred_df) - prior_pred_df.insert( - len(prior_pred_df.columns), "Tags", tags_post, - True) - f.close() - - # --- Posterior --- - f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+') - - X_values = np.repeat( - x_coords, np.array(f[f"EDY/{out_name}"]).shape[0]) - - post_pred_df = {} - post_pred_df[x_key] = X_values - post_pred_df[out_name] = np.array( - f[f"EDY/{out_name}"]).flatten('F') - - post_pred_df = pd.DataFrame(post_pred_df) - - tags_post = ['posterior'] * len(post_pred_df) - post_pred_df.insert( - len(post_pred_df.columns), "Tags", tags_post, True) - f.close() - # Concatenate two dataframes based on x_values - frames = [prior_pred_df, post_pred_df] - all_pred_df = pd.concat(frames) - - # --- Plot posterior predictive --- - sns.violinplot( - x_key, y=out_name, data=all_pred_df, hue="Tags", - legend=False, ax=ax, split=True, inner=None, - color=".8") - - # --- Plot Data --- - # Find the x,y coordinates for each point - x_coords = np.arange(x_coords.shape[0]) - first_header = list(self.measured_data)[0] - obs_data = self.measured_data.round({first_header: 6}) - sns.pointplot( - x=first_header, y=out_name, color='g', markers='x', - linestyles='', capsize=16, data=obs_data, ax=ax) - - ax.errorbar( - x_coords, obs_data[out_name].values, - yerr=1.96*self.measurement_error[out_name], - ecolor='g', fmt=' ', zorder=-1) - - # Add labels to the legend - handles, labels = ax.get_legend_handles_labels() - labels.append('Data') - - data_marker = mlines.Line2D( - [], [], color='lime', marker='+', linestyle='None', - markersize=10) - handles.append(data_marker) - - # Add legend - ax.legend(handles=handles, labels=labels, loc='best', - fontsize='large', frameon=True) - - else: - # Load posterior predictive - f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+') - - try: - x_coords = np.array(f[f"x_values/{out_name}"]) - except: - x_coords = np.array(f["x_values"]) - - mu = np.mean(np.array(f[f"EDY/{out_name}"]), axis=0) - std = np.std(np.array(f[f"EDY/{out_name}"]), axis=0) - - # --- Plot posterior predictive --- - plt.plot( - x_coords, mu, marker='o', color='b', - label='Mean Post. Predictive') - plt.fill_between( - x_coords, mu-1.96*std, mu+1.96*std, color='b', - alpha=0.15) - - # --- Plot Data --- - ax.plot( - x_coords, self.measured_data[out_name].values, - 'ko', label='data', markeredgecolor='w') - - # --- Plot ExpDesign --- - orig_ED_Y = self.MetaModel.ExpDesign.Y[out_name] - for output in orig_ED_Y: - plt.plot( - x_coords, output, color='grey', alpha=0.15 - ) - - # Add labels for axes - plt.xlabel('Time [s]') - plt.ylabel(out_name) - - # Add labels to the legend - handles, labels = ax.get_legend_handles_labels() - - patch = Patch(color='b', alpha=0.15) - handles.insert(1, patch) - labels.insert(1, '95 $\\%$ CI') - - # Add legend - ax.legend(handles=handles, labels=labels, loc='best', - frameon=True) - - # Save figure in pdf format - if self.emulator: - plotname = f'/Post_Prior_Perd_{Model.name}_emulator' - else: - plotname = f'/Post_Prior_Perd_{Model.name}' - - fig.savefig(f'./{out_dir}{plotname}_{out_name}.pdf', - bbox_inches='tight') diff --git a/examples/only-model/bayesvalidrox/bayes_inference/bayes_model_comparison.py b/examples/only-model/bayesvalidrox/bayes_inference/bayes_model_comparison.py deleted file mode 100644 index 718abb8bdac3fd653bb3751a31cc0667c5032e95..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/bayes_inference/bayes_model_comparison.py +++ /dev/null @@ -1,714 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Sat Aug 24 16:04:06 2019 - -@author: farid -""" -import numpy as np -import os -from scipy import stats -import seaborn as sns -import matplotlib.patches as patches -import matplotlib.colors as mcolors -import matplotlib.pylab as plt -from .bayes_inference import BayesInference - -# Load the mplstyle -plt.style.use(os.path.join(os.path.split(__file__)[0], - '../', 'bayesvalidrox.mplstyle')) - - -class BayesModelComparison: - """ - A class to perform Bayesian Analysis. - - - Attributes - ---------- - justifiability : bool, optional - Whether to perform the justifiability analysis. The default is - `True`. - perturbed_data : array of shape (n_bootstrap_itrs, n_obs), optional - User defined perturbed data. The default is `None`. - n_bootstarp : int - Number of bootstrap iteration. The default is `1000`. - data_noise_level : float - A noise level to perturb the data set. The default is `0.01`. - just_n_meas : int - Number of measurements considered for visualization of the - justifiability results. - - """ - - def __init__(self, justifiability=True, perturbed_data=None, - n_bootstarp=1000, data_noise_level=0.01, just_n_meas=2, - emulator=True): - - self.justifiability = justifiability - self.perturbed_data = perturbed_data - self.n_bootstarp = n_bootstarp - self.data_noise_level = data_noise_level - self.just_n_meas = just_n_meas - self.emulator = emulator - - # -------------------------------------------------------------------------- - def create_model_comparison(self, model_dict, opts_dict): - """ - Starts the two-stage model comparison. - Stage I: Compare models using Bayes factors. - Stage II: Compare models via justifiability analysis. - - Parameters - ---------- - model_dict : dict - A dictionary including the metamodels. - opts_dict : dict - A dictionary given the `BayesInference` options. - - Example: - - >>> opts_bootstrap = { - "bootstrap": True, - "n_samples": 10000, - "Discrepancy": DiscrepancyOpts, - "emulator": True, - "plot_post_pred": True - } - - Returns - ------- - output : dict - A dictionary containing the objects and the model weights for the - comparison using Bayes factors and justifiability analysis. - - """ - - # Bayes factor - bayes_dict_bf, model_weights_dict_bf = self.compare_models( - model_dict, opts_dict - ) - - output = { - 'Bayes objects BF': bayes_dict_bf, - 'Model weights BF': model_weights_dict_bf - } - - # Justifiability analysis - if self.justifiability: - bayes_dict_ja, model_weights_dict_ja = self.compare_models( - model_dict, opts_dict, justifiability=True - ) - - output['Bayes objects JA'] = bayes_dict_ja - output['Model weights JA'] = model_weights_dict_ja - - return output - - # -------------------------------------------------------------------------- - def compare_models(self, model_dict, opts_dict, justifiability=False): - """ - Passes the options to instantiates the BayesInference class for each - model and passes the options from `opts_dict`. Then, it starts the - computations. - It also creates a folder and saves the diagrams, e.g., Bayes factor - plot, confusion matrix, etc. - - Parameters - ---------- - model_dict : dict - A dictionary including the metamodels. - opts_dict : dict - A dictionary given the `BayesInference` options. - justifiability : bool, optional - Whether to perform the justifiability analysis. The default is - `False`. - - Returns - ------- - bayes_dict : dict - A dictionary with `BayesInference` objects. - model_weights_dict : dict - A dictionary containing the model weights. - - """ - - if not isinstance(model_dict, dict): - raise Exception("To run model comparsion, you need to pass a " - "dictionary of models.") - - # Extract model names - self.model_names = [*model_dict] - - # Compute total number of the measurement points - MetaModel = list(model_dict.items())[0][1] - MetaModel.ModelObj.read_observation() - self.n_meas = MetaModel.ModelObj.n_obs - - # ----- Generate data ----- - # Find n_bootstrap - if self.perturbed_data is None: - n_bootstarp = self.n_bootstarp - else: - n_bootstarp = self.perturbed_data.shape[0] - - # Create dataset - justData = self.generate_dataset( - model_dict, justifiability, n_bootstarp=n_bootstarp) - - # Run create Interface for each model - bayes_dict = {} - for model in model_dict.keys(): - print("-"*20) - print("Bayesian inference of {}.\n".format(model)) - - BayesOpts = BayesInference(model_dict[model]) - - # Set BayesInference options - for key, value in opts_dict.items(): - if key in BayesOpts.__dict__.keys(): - if key == "Discrepancy" and isinstance(value, dict): - setattr(BayesOpts, key, value[model]) - else: - setattr(BayesOpts, key, value) - - # Pass justifiability data as perturbed data - BayesOpts.perturbed_data = justData - BayesOpts.just_analysis = justifiability - - bayes_dict[model] = BayesOpts.create_inference() - print("-"*20) - - # Compute model weights - BME_Dict = dict() - for modelName, bayesObj in bayes_dict.items(): - BME_Dict[modelName] = np.exp(bayesObj.log_BME, dtype=np.longdouble) - - # BME correction in BayesInference class - model_weights = self.cal_model_weight( - BME_Dict, justifiability, n_bootstarp=n_bootstarp) - - # Plot model weights - if justifiability: - model_names = self.model_names - model_names.insert(0, 'Observation') - - # Split the model weights and save in a dict - list_ModelWeights = np.split( - model_weights, model_weights.shape[1]/self.n_meas, axis=1) - model_weights_dict = {key: weights for key, weights in - zip(model_names, list_ModelWeights)} - - self.plot_just_analysis(model_weights_dict) - else: - # Create box plot for model weights - self.plot_model_weights(model_weights, 'model_weights') - - # Create kde plot for bayes factors - self.plot_bayes_factor(BME_Dict, 'kde_plot') - - # Store model weights in a dict - model_weights_dict = {key: weights for key, weights in - zip(self.model_names, model_weights)} - - return bayes_dict, model_weights_dict - - # ------------------------------------------------------------------------- - def generate_dataset(self, model_dict, justifiability=False, - n_bootstarp=1): - """ - Generates the perturbed data set for the Bayes factor calculations and - the data set for the justifiability analysis. - - Parameters - ---------- - model_dict : dict - A dictionary including the metamodels. - bool, optional - Whether to perform the justifiability analysis. The default is - `False`. - n_bootstarp : int, optional - Number of bootstrap iterations. The default is `1`. - - Returns - ------- - all_just_data: array - Created data set. - - """ - # Compute some variables - all_just_data = [] - metaModel = list(model_dict.items())[0][1] - out_names = metaModel.ModelObj.Output.names - - # Perturb observations for Bayes Factor - if self.perturbed_data is None: - self.perturbed_data = self.__perturb_data( - metaModel.ModelObj.observations, out_names, n_bootstarp, - noise_level=self.data_noise_level) - - # Only for Bayes Factor - if not justifiability: - return self.perturbed_data - - # Evaluate metamodel - runs = {} - for key, metaModel in model_dict.items(): - if self.emulator: - y_hat, _ = metaModel.eval_metamodel(nsamples=n_bootstarp) - runs[key] = y_hat - if not self.emulator: - # y_hat_ = metaModel.model. # TODO: run the model instead of the surrogate - samples = metaModel.ExpDesign.generate_samples( - n_bootstarp, - sampling_method = 'random' - ) - y_hat = self._eval_model(metaModel, - samples=samples, key='PriorPred' - ) - #print(y_hat) - runs[key] = y_hat - # Generate data - for i in range(n_bootstarp): - y_data = self.perturbed_data[i].reshape(1, -1) - justData = np.tril(np.repeat(y_data, y_data.shape[1], axis=0)) - # Use surrogate runs for data-generating process - for key, metaModel in model_dict.items(): - model_data = np.array( - [runs[key][out][i] for out in out_names]).reshape(y_data.shape) - justData = np.vstack(( - justData, - np.tril(np.repeat(model_data, model_data.shape[1], axis=0)) - )) - # Save in a list - all_just_data.append(justData) - - # Squeeze the array - all_just_data = np.array(all_just_data).transpose(1, 0, 2).reshape( - -1, np.array(all_just_data).shape[2] - ) - - return all_just_data - - # ------------------------------------------------------------------------- - def __perturb_data(self, data, output_names, n_bootstrap, noise_level): - """ - Returns an array with n_bootstrap_itrs rowsof perturbed data. - The first row includes the original observation data. - If `self.bayes_loocv` is True, a 2d-array will be returned with - repeated rows and zero diagonal entries. - - Parameters - ---------- - data : pandas DataFrame - Observation data. - output_names : list - List of the output names. - - Returns - ------- - final_data : array - Perturbed data set. - - """ - obs_data = data[output_names].values - n_measurement, n_outs = obs_data.shape - n_tot_measurement = obs_data[~np.isnan(obs_data)].shape[0] - final_data = np.zeros( - (n_bootstrap, n_tot_measurement) - ) - final_data[0] = obs_data.T[~np.isnan(obs_data.T)] - for itrIdx in range(1, n_bootstrap): - data = np.zeros((n_measurement, n_outs)) - for idx in range(len(output_names)): - std = np.nanstd(obs_data[:, idx]) - if std == 0: - std = 0.001 - noise = std * noise_level - data[:, idx] = np.add( - obs_data[:, idx], - np.random.normal(0, 1, obs_data.shape[0]) * noise, - ) - - final_data[itrIdx] = data.T[~np.isnan(data.T)] - - return final_data - - # ------------------------------------------------------------------------- - def cal_model_weight(self, BME_Dict, justifiability=False, n_bootstarp=1): - """ - Normalize the BME (Asumption: Model Prior weights are equal for models) - - Parameters - ---------- - BME_Dict : dict - A dictionary containing the BME values. - - Returns - ------- - model_weights : array - Model weights. - - """ - # Stack the BME values for all models - all_BME = np.vstack(list(BME_Dict.values())) - - if justifiability: - # Compute expected log_BME for justifiabiliy analysis - all_BME = all_BME.reshape( - all_BME.shape[0], -1, n_bootstarp).mean(axis=2) - - # Model weights - model_weights = np.divide(all_BME, np.nansum(all_BME, axis=0)) - - return model_weights - - # ------------------------------------------------------------------------- - def plot_just_analysis(self, model_weights_dict): - """ - Visualizes the confusion matrix and the model wights for the - justifiability analysis. - - Parameters - ---------- - model_weights_dict : dict - Model weights. - - Returns - ------- - None. - - """ - - directory = 'Outputs_Comparison/' - os.makedirs(directory, exist_ok=True) - Color = [*mcolors.TABLEAU_COLORS] - names = [*model_weights_dict] - - model_names = [model.replace('_', '$-$') for model in self.model_names] - for name in names: - fig, ax = plt.subplots() - for i, model in enumerate(model_names[1:]): - plt.plot(list(range(1, self.n_meas+1)), - model_weights_dict[name][i], - color=Color[i], marker='o', - ms=10, linewidth=2, label=model - ) - - plt.title(f"Data generated by: {name.replace('_', '$-$')}") - plt.ylabel("Weights") - plt.xlabel("No. of measurement points") - ax.set_xticks(list(range(1, self.n_meas+1))) - plt.legend(loc="best") - fig.savefig( - f'{directory}modelWeights_{name}.svg', bbox_inches='tight' - ) - plt.close() - - # Confusion matrix for some measurement points - epsilon = 1 if self.just_n_meas != 1 else 0 - for index in range(0, self.n_meas+epsilon, self.just_n_meas): - weights = np.array( - [model_weights_dict[key][:, index] for key in model_weights_dict] - ) - g = sns.heatmap( - weights.T, annot=True, cmap='Blues', xticklabels=model_names, - yticklabels=model_names[1:], annot_kws={"size": 24} - ) - - # x axis on top - g.xaxis.tick_top() - g.xaxis.set_label_position('top') - g.set_xlabel(r"\textbf{Data generated by:}", labelpad=15) - g.set_ylabel(r"\textbf{Model weight for:}", labelpad=15) - g.figure.savefig( - f"{directory}confusionMatrix_ND_{index+1}.pdf", - bbox_inches='tight' - ) - plt.close() - - # ------------------------------------------------------------------------- - def plot_model_weights(self, model_weights, plot_name): - """ - Visualizes the model weights resulting from BMS via the observation - data. - - Parameters - ---------- - model_weights : array - Model weights. - plot_name : str - Plot name. - - Returns - ------- - None. - - """ - font_size = 40 - # mkdir for plots - directory = 'Outputs_Comparison/' - os.makedirs(directory, exist_ok=True) - - # Create figure - fig, ax = plt.subplots() - - # Filter data using np.isnan - mask = ~np.isnan(model_weights.T) - filtered_data = [d[m] for d, m in zip(model_weights, mask.T)] - - # Create the boxplot - bp = ax.boxplot(filtered_data, patch_artist=True, showfliers=False) - - # change outline color, fill color and linewidth of the boxes - for box in bp['boxes']: - # change outline color - box.set(color='#7570b3', linewidth=4) - # change fill color - box.set(facecolor='#1b9e77') - - # change color and linewidth of the whiskers - for whisker in bp['whiskers']: - whisker.set(color='#7570b3', linewidth=2) - - # change color and linewidth of the caps - for cap in bp['caps']: - cap.set(color='#7570b3', linewidth=2) - - # change color and linewidth of the medians - for median in bp['medians']: - median.set(color='#b2df8a', linewidth=2) - - # change the style of fliers and their fill - # for flier in bp['fliers']: - # flier.set(marker='o', color='#e7298a', alpha=0.75) - - # Custom x-axis labels - model_names = [model.replace('_', '$-$') for model in self.model_names] - ax.set_xticklabels(model_names) - - ax.set_ylabel('Weight', fontsize=font_size) - - # Title - plt.title('Posterior Model Weights') - - # Set y lim - ax.set_ylim((-0.05, 1.05)) - - # Set size of the ticks - for t in ax.get_xticklabels(): - t.set_fontsize(font_size) - for t in ax.get_yticklabels(): - t.set_fontsize(font_size) - - # Save the figure - fig.savefig( - f'./{directory}{plot_name}.pdf', bbox_inches='tight' - ) - - plt.close() - - # ------------------------------------------------------------------------- - def plot_bayes_factor(self, BME_Dict, plot_name=''): - """ - Plots the Bayes factor distibutions in a :math:`N_m \\times N_m` - matrix, where :math:`N_m` is the number of the models. - - Parameters - ---------- - BME_Dict : dict - A dictionary containing the BME values of the models. - plot_name : str, optional - Plot name. The default is ''. - - Returns - ------- - None. - - """ - - font_size = 40 - - # mkdir for plots - directory = 'Outputs_Comparison/' - os.makedirs(directory, exist_ok=True) - - Colors = ["blue", "green", "gray", "brown"] - - model_names = list(BME_Dict.keys()) - nModels = len(model_names) - - # Plots - fig, axes = plt.subplots( - nrows=nModels, ncols=nModels, sharex=True, sharey=True - ) - - for i, key_i in enumerate(model_names): - - for j, key_j in enumerate(model_names): - ax = axes[i, j] - # Set size of the ticks - for t in ax.get_xticklabels(): - t.set_fontsize(font_size) - for t in ax.get_yticklabels(): - t.set_fontsize(font_size) - - if j != i: - - # Null hypothesis: key_j is the better model - BayesFactor = np.log10( - np.divide(BME_Dict[key_i], BME_Dict[key_j]) - ) - - # sns.kdeplot(BayesFactor, ax=ax, color=Colors[i], shade=True) - # sns.histplot(BayesFactor, ax=ax, stat="probability", - # kde=True, element='step', - # color=Colors[j]) - - # taken from seaborn's source code (utils.py and - # distributions.py) - def seaborn_kde_support(data, bw, gridsize, cut, clip): - if clip is None: - clip = (-np.inf, np.inf) - support_min = max(data.min() - bw * cut, clip[0]) - support_max = min(data.max() + bw * cut, clip[1]) - return np.linspace(support_min, support_max, gridsize) - - kde_estim = stats.gaussian_kde( - BayesFactor, bw_method='scott' - ) - - # manual linearization of data - # linearized = np.linspace( - # quotient.min(), quotient.max(), num=500) - - # or better: mimic seaborn's internal stuff - bw = kde_estim.scotts_factor() * np.std(BayesFactor) - linearized = seaborn_kde_support( - BayesFactor, bw, 100, 3, None) - - # computes values of the estimated function on the - # estimated linearized inputs - Z = kde_estim.evaluate(linearized) - - # https://stackoverflow.com/questions/29661574/normalize- - # numpy-array-columns-in-python - def normalize(x): - return (x - x.min(0)) / x.ptp(0) - - # normalize so it is between 0;1 - Z2 = normalize(Z) - ax.plot(linearized, Z2, "-", color=Colors[i], linewidth=4) - ax.fill_between( - linearized, 0, Z2, color=Colors[i], alpha=0.25 - ) - - # Draw BF significant levels according to Jeffreys 1961 - # Strong evidence for both models - ax.axvline( - x=np.log10(3), ymin=0, linewidth=4, color='dimgrey' - ) - # Strong evidence for one model - ax.axvline( - x=np.log10(10), ymin=0, linewidth=4, color='orange' - ) - # Decisive evidence for one model - ax.axvline( - x=np.log10(100), ymin=0, linewidth=4, color='r' - ) - - # legend - BF_label = key_i.replace('_', '$-$') + \ - '/' + key_j.replace('_', '$-$') - legend_elements = [ - patches.Patch(facecolor=Colors[i], edgecolor=Colors[i], - label=f'BF({BF_label})') - ] - ax.legend( - loc='upper left', handles=legend_elements, - fontsize=font_size-(nModels+1)*5 - ) - - elif j == i: - # build a rectangle in axes coords - left, width = 0, 1 - bottom, height = 0, 1 - - # axes coordinates are 0,0 is bottom left and 1,1 is upper - # right - p = patches.Rectangle( - (left, bottom), width, height, color='white', - fill=True, transform=ax.transAxes, clip_on=False - ) - ax.grid(False) - ax.add_patch(p) - # ax.text(0.5*(left+right), 0.5*(bottom+top), key_i, - fsize = font_size+20 if nModels < 4 else font_size - ax.text(0.5, 0.5, key_i.replace('_', '$-$'), - horizontalalignment='center', - verticalalignment='center', - fontsize=fsize, color=Colors[i], - transform=ax.transAxes) - - # Defining custom 'ylim' values. - custom_ylim = (0, 1.05) - - # Setting the values for all axes. - plt.setp(axes, ylim=custom_ylim) - - # set labels - for i in range(nModels): - axes[-1, i].set_xlabel('log$_{10}$(BF)', fontsize=font_size) - axes[i, 0].set_ylabel('Probability', fontsize=font_size) - - # Adjust subplots - plt.subplots_adjust(wspace=0.2, hspace=0.1) - - plt.savefig( - f'./{directory}Bayes_Factor{plot_name}.pdf', bbox_inches='tight' - ) - - plt.close() - - def _eval_model(self, MetaModel, samples=None, key='MAP'): - """ - Evaluates Forward Model. - mostly copy paste from BayesInference - - Parameters - ---------- - samples : array of shape (n_samples, n_params), optional - Parameter sets. The default is None. - key : str, optional - Key string to be passed to the run_model_parallel method. - The default is 'MAP'. - - Returns - ------- - model_outputs : dict - Model outputs. - - """ - #MetaModel = self.MetaModel - Model = MetaModel.ModelObj - - if samples is None: - self.samples = MetaModel.ExpDesign.generate_samples( - self.n_samples, 'random') - else: - self.samples = samples - self.n_samples = len(samples) - - self.name = 'ModelComp' - model_outputs, _ = Model.run_model_parallel( - self.samples, key_str=key+self.name) - - # Clean up - # Zip the subdirectories - try: - dir_name = f'{Model.name}MAP{self.name}' - key = dir_name + '_' - Model.zip_subdirs(dir_name, key) - except: - pass - - return model_outputs \ No newline at end of file diff --git a/examples/only-model/bayesvalidrox/bayes_inference/discrepancy.py b/examples/only-model/bayesvalidrox/bayes_inference/discrepancy.py deleted file mode 100644 index 0f52c4f0d0afe314fdc73c388f6da5a8aa93ca06..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/bayes_inference/discrepancy.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import scipy.stats as stats -from bayesvalidrox.surrogate_models.exp_designs import ExpDesigns - - -class Discrepancy: - """ - Discrepancy class for Bayesian inference method. - We define the reference or reality to be equal to what we can model and a - descripancy term \\( \\epsilon \\). We consider the followin format: - - $$\\textbf{y}_{\\text{reality}} = \\mathcal{M}(\\theta) + \\epsilon,$$ - - where \\( \\epsilon \\in R^{N_{out}} \\) represents the the effects of - measurement error and model inaccuracy. For simplicity, it can be defined - as an additive Gaussian disrepancy with zeromean and given covariance - matrix \\( \\Sigma \\): - - $$\\epsilon \\sim \\mathcal{N}(\\epsilon|0, \\Sigma). $$ - - In the context of model inversion or calibration, an observation point - \\( \\textbf{y}_i \\in \\mathcal{y} \\) is a realization of a Gaussian - distribution with mean value of \\(\\mathcal{M}(\\theta) \\) and covariance - matrix of \\( \\Sigma \\). - - $$ p(\\textbf{y}|\\theta) = \\mathcal{N}(\\textbf{y}|\\mathcal{M} - (\\theta))$$ - - The following options are available: - - * Option A: With known redidual covariance matrix \\(\\Sigma\\) for - independent measurements. - - * Option B: With unknown redidual covariance matrix \\(\\Sigma\\), - paramethrized as \\(\\Sigma(\\theta_{\\epsilon})=\\sigma^2 \\textbf{I}_ - {N_{out}}\\) with unknown residual variances \\(\\sigma^2\\). - This term will be jointly infer with the uncertain input parameters. For - the inversion, you need to define a prior marginal via `Input` class. Note - that \\(\\sigma^2\\) is only a single scalar multiplier for the diagonal - entries of the covariance matrix \\(\\Sigma\\). - - Attributes - ---------- - InputDisc : obj - Input object. When the \\(\\sigma^2\\) is expected to be inferred - jointly with the parameters (`Option B`).If multiple output groups are - defined by `Model.Output.names`, each model output needs to have. - a prior marginal using the `Input` class. The default is `''`. - disc_type : str - Type of the noise definition. `'Gaussian'` is only supported so far. - parameters : dict or pandas.DataFrame - Known residual variance \\(\\sigma^2\\), i.e. diagonal entry of the - covariance matrix of the multivariate normal likelihood in case of - `Option A`. - - """ - - def __init__(self, InputDisc='', disc_type='Gaussian', parameters=None): - self.InputDisc = InputDisc - self.disc_type = disc_type - self.parameters = parameters - - # ------------------------------------------------------------------------- - def get_sample(self, n_samples): - """ - Generate samples for the \\(\\sigma^2\\), i.e. the diagonal entries of - the variance-covariance matrix in the multivariate normal distribution. - - Parameters - ---------- - n_samples : int - Number of samples (parameter sets). - - Returns - ------- - sigma2_prior: array of shape (n_samples, n_params) - \\(\\sigma^2\\) samples. - - """ - self.n_samples = n_samples - ExpDesign = ExpDesigns(self.InputDisc) - self.sigma2_prior = ExpDesign.generate_ED( - n_samples, sampling_method='random', max_pce_deg=1 - ) - # Store BoundTuples - self.ExpDesign = ExpDesign - - # Naive approach: Fit a gaussian kernel to the provided data - self.ExpDesign.JDist = stats.gaussian_kde(ExpDesign.raw_data) - - # Save the names of sigmas - if len(self.InputDisc.Marginals) != 0: - self.name = [] - for Marginalidx in range(len(self.InputDisc.Marginals)): - self.name.append(self.InputDisc.Marginals[Marginalidx].name) - - return self.sigma2_prior diff --git a/examples/only-model/bayesvalidrox/bayes_inference/mcmc.py b/examples/only-model/bayesvalidrox/bayes_inference/mcmc.py deleted file mode 100644 index 6b9ca94823fb7064baa2f0588d0f97fb4c9d1d44..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/bayes_inference/mcmc.py +++ /dev/null @@ -1,909 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import numpy as np -import emcee -import pandas as pd -import matplotlib.pyplot as plt -from matplotlib.backends.backend_pdf import PdfPages -import multiprocessing -import scipy.stats as st -from scipy.linalg import cholesky as chol -import warnings -import shutil -os.environ["OMP_NUM_THREADS"] = "1" - - -class MCMC: - """ - A class for bayesian inference via a Markov-Chain Monte-Carlo (MCMC) - Sampler to approximate the posterior distribution of the Bayes theorem: - $$p(\\theta|\\mathcal{y}) = \\frac{p(\\mathcal{y}|\\theta) p(\\theta)} - {p(\\mathcal{y})}.$$ - - This class make inference with emcee package [1] using an Affine Invariant - Ensemble sampler (AIES) [2]. - - [1] Foreman-Mackey, D., Hogg, D.W., Lang, D. and Goodman, J., 2013.emcee: - the MCMC hammer. Publications of the Astronomical Society of the - Pacific, 125(925), p.306. https://emcee.readthedocs.io/en/stable/ - - [2] Goodman, J. and Weare, J., 2010. Ensemble samplers with affine - invariance. Communications in applied mathematics and computational - science, 5(1), pp.65-80. - - - Attributes - ---------- - BayesOpts : obj - Bayes object. - """ - - def __init__(self, BayesOpts): - - self.BayesOpts = BayesOpts - - def run_sampler(self, observation, total_sigma2): - - BayesObj = self.BayesOpts - MetaModel = BayesObj.MetaModel - Model = MetaModel.ModelObj - Discrepancy = self.BayesOpts.Discrepancy - n_cpus = Model.n_cpus - priorDist = MetaModel.ExpDesign.JDist - ndim = MetaModel.n_params - self.counter = 0 - output_dir = f'Outputs_Bayes_{Model.name}_{self.BayesOpts.name}' - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - self.observation = observation - self.total_sigma2 = total_sigma2 - - # Unpack mcmc parameters given to BayesObj.mcmc_params - self.initsamples = None - self.nwalkers = 100 - self.nburn = 200 - self.nsteps = 100000 - self.moves = None - self.mp = False - self.verbose = False - - # Extract initial samples - if 'init_samples' in BayesObj.mcmc_params: - self.initsamples = BayesObj.mcmc_params['init_samples'] - if isinstance(self.initsamples, pd.DataFrame): - self.initsamples = self.initsamples.values - - # Extract number of steps per walker - if 'n_steps' in BayesObj.mcmc_params: - self.nsteps = int(BayesObj.mcmc_params['n_steps']) - # Extract number of walkers (chains) - if 'n_walkers' in BayesObj.mcmc_params: - self.nwalkers = int(BayesObj.mcmc_params['n_walkers']) - # Extract moves - if 'moves' in BayesObj.mcmc_params: - self.moves = BayesObj.mcmc_params['moves'] - # Extract multiprocessing - if 'multiprocessing' in BayesObj.mcmc_params: - self.mp = BayesObj.mcmc_params['multiprocessing'] - # Extract verbose - if 'verbose' in BayesObj.mcmc_params: - self.verbose = BayesObj.mcmc_params['verbose'] - - # Set initial samples - np.random.seed(0) - if self.initsamples is None: - try: - initsamples = priorDist.sample(self.nwalkers).T - except: - # when aPCE selected - gaussian kernel distribution - inputSamples = MetaModel.ExpDesign.raw_data.T - random_indices = np.random.choice( - len(inputSamples), size=self.nwalkers, replace=False - ) - initsamples = inputSamples[random_indices] - - else: - if self.initsamples.ndim == 1: - # When MAL is given. - theta = self.initsamples - initsamples = [theta + 1e-1*np.multiply( - np.random.randn(ndim), theta) for i in - range(self.nwalkers)] - else: - # Pick samples based on a uniform dist between min and max of - # each dim - initsamples = np.zeros((self.nwalkers, ndim)) - bound_tuples = [] - for idx_dim in range(ndim): - lower = np.min(self.initsamples[:, idx_dim]) - upper = np.max(self.initsamples[:, idx_dim]) - bound_tuples.append((lower, upper)) - dist = st.uniform(loc=lower, scale=upper-lower) - initsamples[:, idx_dim] = dist.rvs(size=self.nwalkers) - - # Update lower and upper - MetaModel.ExpDesign.bound_tuples = bound_tuples - - # Check if sigma^2 needs to be inferred - if Discrepancy.opt_sigma != 'B': - sigma2_samples = Discrepancy.get_sample(self.nwalkers) - - # Update initsamples - initsamples = np.hstack((initsamples, sigma2_samples)) - - # Update ndim - ndim = initsamples.shape[1] - - # Discrepancy bound - disc_bound_tuple = Discrepancy.ExpDesign.bound_tuples - - # Update bound_tuples - MetaModel.ExpDesign.bound_tuples += disc_bound_tuple - - print("\n>>>> Bayesian inference with MCMC for " - f"{self.BayesOpts.name} started. <<<<<<") - - # Set up the backend - filename = f"{output_dir}/emcee_sampler.h5" - backend = emcee.backends.HDFBackend(filename) - # Clear the backend in case the file already exists - backend.reset(self.nwalkers, ndim) - - # Define emcee sampler - # Here we'll set up the computation. emcee combines multiple "walkers", - # each of which is its own MCMC chain. The number of trace results will - # be nwalkers * nsteps. - if self.mp: - # Run in parallel - if n_cpus is None: - n_cpus = multiprocessing.cpu_count() - - with multiprocessing.Pool(n_cpus) as pool: - sampler = emcee.EnsembleSampler( - self.nwalkers, ndim, self.log_posterior, moves=self.moves, - pool=pool, backend=backend - ) - - # Check if a burn-in phase is needed! - if self.initsamples is None: - # Burn-in - print("\n Burn-in period is starting:") - pos = sampler.run_mcmc( - initsamples, self.nburn, progress=True - ) - - # Reset sampler - sampler.reset() - pos = pos.coords - else: - pos = initsamples - - # Production run - print("\n Production run is starting:") - pos, prob, state = sampler.run_mcmc( - pos, self.nsteps, progress=True - ) - - else: - # Run in series and monitor the convergence - sampler = emcee.EnsembleSampler( - self.nwalkers, ndim, self.log_posterior, moves=self.moves, - backend=backend, vectorize=True - ) - - # Check if a burn-in phase is needed! - if self.initsamples is None: - # Burn-in - print("\n Burn-in period is starting:") - pos = sampler.run_mcmc( - initsamples, self.nburn, progress=True - ) - - # Reset sampler - sampler.reset() - pos = pos.coords - else: - pos = initsamples - - # Production run - print("\n Production run is starting:") - - # Track how the average autocorrelation time estimate changes - autocorrIdx = 0 - autocorr = np.empty(self.nsteps) - tauold = np.inf - autocorreverynsteps = 50 - - # sample step by step using the generator sampler.sample - for sample in sampler.sample(pos, - iterations=self.nsteps, - tune=True, - progress=True): - - # only check convergence every autocorreverynsteps steps - if sampler.iteration % autocorreverynsteps: - continue - - # Train model discrepancy/error - if hasattr(BayesObj, 'errorModel') and BayesObj.errorModel \ - and not sampler.iteration % 3 * autocorreverynsteps: - try: - self.error_MetaModel = self.train_error_model(sampler) - except: - pass - - # Print the current mean acceptance fraction - if self.verbose: - print("\nStep: {}".format(sampler.iteration)) - acc_fr = np.mean(sampler.acceptance_fraction) - print(f"Mean acceptance fraction: {acc_fr:.3f}") - - # compute the autocorrelation time so far - # using tol=0 means that we'll always get an estimate even if - # it isn't trustworthy - tau = sampler.get_autocorr_time(tol=0) - # average over walkers - autocorr[autocorrIdx] = np.nanmean(tau) - autocorrIdx += 1 - - # output current autocorrelation estimate - if self.verbose: - print(f"Mean autocorr time estimate: {np.nanmean(tau):.3f}") - list_gr = np.round(self.gelman_rubin(sampler.chain), 3) - print("Gelman-Rubin Test*: ", list_gr) - - # check convergence - converged = np.all(tau*autocorreverynsteps < sampler.iteration) - converged &= np.all(np.abs(tauold - tau) / tau < 0.01) - converged &= np.all(self.gelman_rubin(sampler.chain) < 1.1) - - if converged: - break - tauold = tau - - # Posterior diagnostics - try: - tau = sampler.get_autocorr_time(tol=0) - except emcee.autocorr.AutocorrError: - tau = 5 - - if all(np.isnan(tau)): - tau = 5 - - burnin = int(2*np.nanmax(tau)) - thin = int(0.5*np.nanmin(tau)) if int(0.5*np.nanmin(tau)) != 0 else 1 - finalsamples = sampler.get_chain(discard=burnin, flat=True, thin=thin) - acc_fr = np.nanmean(sampler.acceptance_fraction) - list_gr = np.round(self.gelman_rubin(sampler.chain[:, burnin:]), 3) - - # Print summary - print('\n') - print('-'*15 + 'Posterior diagnostics' + '-'*15) - print(f"mean auto-correlation time: {np.nanmean(tau):.3f}") - print(f"thin: {thin}") - print(f"burn-in: {burnin}") - print(f"flat chain shape: {finalsamples.shape}") - print(f"Mean acceptance fraction: {acc_fr:.3f}") - print("Gelman-Rubin Test*: ", list_gr) - - print("\n* This value must lay between 0.234 and 0.5.") - print("* These values must be smaller than 1.1.") - print('-'*50) - - print(f"\n>>>> Bayesian inference with MCMC for {self.BayesOpts.name} " - "successfully completed. <<<<<<\n") - - # Extract parameter names and their prior ranges - par_names = MetaModel.ExpDesign.par_names - - if Discrepancy.opt_sigma != 'B': - for i in range(len(Discrepancy.InputDisc.Marginals)): - par_names.append(Discrepancy.InputDisc.Marginals[i].name) - - params_range = MetaModel.ExpDesign.bound_tuples - - # Plot traces - if self.verbose and self.nsteps < 10000: - pdf = PdfPages(output_dir+'/traceplots.pdf') - fig = plt.figure() - for parIdx in range(ndim): - # Set up the axes with gridspec - fig = plt.figure() - grid = plt.GridSpec(4, 4, hspace=0.2, wspace=0.2) - main_ax = fig.add_subplot(grid[:-1, :3]) - y_hist = fig.add_subplot(grid[:-1, -1], xticklabels=[], - sharey=main_ax) - - for i in range(self.nwalkers): - samples = sampler.chain[i, :, parIdx] - main_ax.plot(samples, '-') - - # histogram on the attached axes - y_hist.hist(samples[burnin:], 40, histtype='stepfilled', - orientation='horizontal', color='gray') - - main_ax.set_ylim(params_range[parIdx]) - main_ax.set_title('traceplot for ' + par_names[parIdx]) - main_ax.set_xlabel('step number') - - # save the current figure - pdf.savefig(fig, bbox_inches='tight') - - # Destroy the current plot - plt.clf() - - pdf.close() - - # plot development of autocorrelation estimate - if not self.mp: - fig1 = plt.figure() - steps = autocorreverynsteps*np.arange(1, autocorrIdx+1) - taus = autocorr[:autocorrIdx] - plt.plot(steps, steps / autocorreverynsteps, "--k") - plt.plot(steps, taus) - plt.xlim(0, steps.max()) - plt.ylim(0, np.nanmax(taus)+0.1*(np.nanmax(taus)-np.nanmin(taus))) - plt.xlabel("number of steps") - plt.ylabel(r"mean $\hat{\tau}$") - fig1.savefig(f"{output_dir}/autocorrelation_time.pdf", - bbox_inches='tight') - - # logml_dict = self.marginal_llk_emcee(sampler, self.nburn, logp=None, - # maxiter=5000) - # print('\nThe Bridge Sampling Estimation is " - # f"{logml_dict['logml']:.5f}.') - - # # Posterior-based expectation of posterior probablity - # postExpPostLikelihoods = np.mean(sampler.get_log_prob(flat=True) - # [self.nburn*self.nwalkers:]) - - # # Posterior-based expectation of prior densities - # postExpPrior = np.mean(self.log_prior(emcee_trace.T)) - - # # Posterior-based expectation of likelihoods - # postExpLikelihoods_emcee = postExpPostLikelihoods - postExpPrior - - # # Calculate Kullback-Leibler Divergence - # KLD_emcee = postExpLikelihoods_emcee - logml_dict['logml'] - # print("Kullback-Leibler divergence: %.5f"%KLD_emcee) - - # # Information Entropy based on Entropy paper Eq. 38 - # infEntropy_emcee = logml_dict['logml'] - postExpPrior - - # postExpLikelihoods_emcee - # print("Information Entropy: %.5f" %infEntropy_emcee) - - Posterior_df = pd.DataFrame(finalsamples, columns=par_names) - - return Posterior_df - - # ------------------------------------------------------------------------- - def log_prior(self, theta): - """ - Calculates the log prior likelihood \\( p(\\theta)\\) for the given - parameter set(s) \\( \\theta \\). - - Parameters - ---------- - theta : array of shape (n_samples, n_params) - Parameter sets, i.e. proposals of MCMC chains. - - Returns - ------- - logprior: float or array of shape n_samples - Log prior likelihood. If theta has only one row, a single value is - returned otherwise an array. - - """ - - MetaModel = self.BayesOpts.MetaModel - Discrepancy = self.BayesOpts.Discrepancy - - # Find the number of sigma2 parameters - if Discrepancy.opt_sigma != 'B': - disc_bound_tuples = Discrepancy.ExpDesign.bound_tuples - disc_marginals = Discrepancy.ExpDesign.InputObj.Marginals - disc_prior_space = Discrepancy.ExpDesign.prior_space - n_sigma2 = len(disc_bound_tuples) - else: - n_sigma2 = -len(theta) - prior_dist = MetaModel.ExpDesign.prior_space - params_range = MetaModel.ExpDesign.bound_tuples - theta = theta if theta.ndim != 1 else theta.reshape((1, -1)) - nsamples = theta.shape[0] - logprior = -np.inf*np.ones(nsamples) - - for i in range(nsamples): - # Check if the sample is within the parameters' range - if self._check_ranges(theta[i], params_range): - # Check if all dists are uniform, if yes priors are equal. - if all(MetaModel.input_obj.Marginals[i].dist_type == 'uniform' - for i in range(MetaModel.n_params)): - logprior[i] = 0.0 - else: - logprior[i] = np.log( - prior_dist.pdf(theta[i, :-n_sigma2].T) - ) - - # Check if bias term needs to be inferred - if Discrepancy.opt_sigma != 'B': - if self._check_ranges(theta[i, -n_sigma2:], - disc_bound_tuples): - if all('unif' in disc_marginals[i].dist_type for i in - range(Discrepancy.ExpDesign.ndim)): - logprior[i] = 0.0 - else: - logprior[i] += np.log( - disc_prior_space.pdf(theta[i, -n_sigma2:]) - ) - - if nsamples == 1: - return logprior[0] - else: - return logprior - - # ------------------------------------------------------------------------- - def log_likelihood(self, theta): - """ - Computes likelihood \\( p(\\mathcal{Y}|\\theta)\\) of the performance - of the (meta-)model in reproducing the observation data. - - Parameters - ---------- - theta : array of shape (n_samples, n_params) - Parameter set, i.e. proposals of the MCMC chains. - - Returns - ------- - log_like : array of shape (n_samples) - Log likelihood. - - """ - - BayesOpts = self.BayesOpts - MetaModel = BayesOpts.MetaModel - Discrepancy = self.BayesOpts.Discrepancy - - # Find the number of sigma2 parameters - if Discrepancy.opt_sigma != 'B': - disc_bound_tuples = Discrepancy.ExpDesign.bound_tuples - n_sigma2 = len(disc_bound_tuples) - else: - n_sigma2 = -len(theta) - # Check if bias term needs to be inferred - if Discrepancy.opt_sigma != 'B': - sigma2 = theta[:, -n_sigma2:] - theta = theta[:, :-n_sigma2] - else: - sigma2 = None - theta = theta if theta.ndim != 1 else theta.reshape((1, -1)) - - # Evaluate Model/MetaModel at theta - mean_pred, BayesOpts._std_pce_prior_pred = self.eval_model(theta) - - # Surrogate model's error using RMSE of test data - surrError = MetaModel.rmse if hasattr(MetaModel, 'rmse') else None - - # Likelihood - log_like = BayesOpts.normpdf( - mean_pred, self.observation, self.total_sigma2, sigma2, - std=surrError - ) - return log_like - - # ------------------------------------------------------------------------- - def log_posterior(self, theta): - """ - Computes the posterior likelihood \\(p(\\theta| \\mathcal{Y})\\) for - the given parameterset. - - Parameters - ---------- - theta : array of shape (n_samples, n_params) - Parameter set, i.e. proposals of the MCMC chains. - - Returns - ------- - log_like : array of shape (n_samples) - Log posterior likelihood. - - """ - - nsamples = 1 if theta.ndim == 1 else theta.shape[0] - - if nsamples == 1: - if self.log_prior(theta) == -np.inf: - return -np.inf - else: - # Compute log prior - log_prior = self.log_prior(theta) - # Compute log Likelihood - log_likelihood = self.log_likelihood(theta) - - return log_prior + log_likelihood - else: - # Compute log prior - log_prior = self.log_prior(theta) - - # Initialize log_likelihood - log_likelihood = -np.inf*np.ones(nsamples) - - # find the indices for -inf sets - non_inf_idx = np.where(log_prior != -np.inf)[0] - - # Compute loLikelihoods - if non_inf_idx.size != 0: - log_likelihood[non_inf_idx] = self.log_likelihood( - theta[non_inf_idx] - ) - - return log_prior + log_likelihood - - # ------------------------------------------------------------------------- - def eval_model(self, theta): - """ - Evaluates the (meta-) model at the given theta. - - Parameters - ---------- - theta : array of shape (n_samples, n_params) - Parameter set, i.e. proposals of the MCMC chains. - - Returns - ------- - mean_pred : dict - Mean model prediction. - std_pred : dict - Std of model prediction. - - """ - - BayesObj = self.BayesOpts - MetaModel = BayesObj.MetaModel - Model = MetaModel.ModelObj - - if BayesObj.emulator: - # Evaluate the MetaModel - mean_pred, std_pred = MetaModel.eval_metamodel(samples=theta) - else: - # Evaluate the origModel - mean_pred, std_pred = dict(), dict() - - model_outs, _ = Model.run_model_parallel( - theta, prevRun_No=self.counter, - key_str='_MCMC', mp=False, verbose=False) - - # Save outputs in respective dicts - for varIdx, var in enumerate(Model.Output.names): - mean_pred[var] = model_outs[var] - std_pred[var] = np.zeros((mean_pred[var].shape)) - - # Remove the folder - if Model.link_type.lower() != 'function': - shutil.rmtree(f"{Model.name}_MCMC_{self.counter+1}") - - # Add one to the counter - self.counter += 1 - - if hasattr(self, 'error_MetaModel') and BayesObj.error_model: - meanPred, stdPred = self.error_MetaModel.eval_model_error( - BayesObj.BiasInputs, mean_pred - ) - - return mean_pred, std_pred - - # ------------------------------------------------------------------------- - def train_error_model(self, sampler): - """ - Trains an error model using a Gaussian Process Regression. - - Parameters - ---------- - sampler : obj - emcee sampler. - - Returns - ------- - error_MetaModel : obj - A error model. - - """ - BayesObj = self.BayesOpts - MetaModel = BayesObj.MetaModel - - # Prepare the poster samples - try: - tau = sampler.get_autocorr_time(tol=0) - except emcee.autocorr.AutocorrError: - tau = 5 - - if all(np.isnan(tau)): - tau = 5 - - burnin = int(2*np.nanmax(tau)) - thin = int(0.5*np.nanmin(tau)) if int(0.5*np.nanmin(tau)) != 0 else 1 - finalsamples = sampler.get_chain(discard=burnin, flat=True, thin=thin) - posterior = finalsamples[:, :MetaModel.n_params] - - # Select posterior mean as MAP - map_theta = posterior.mean(axis=0).reshape((1, MetaModel.n_params)) - # MAP_theta = st.mode(Posterior_df,axis=0)[0] - - # Evaluate the (meta-)model at the MAP - y_map, y_std_map = MetaModel.eval_metamodel(samples=map_theta) - - # Train a GPR meta-model using MAP - error_MetaModel = MetaModel.create_model_error( - BayesObj.BiasInputs, y_map, name='Calib') - - return error_MetaModel - - # ------------------------------------------------------------------------- - def gelman_rubin(self, chain, return_var=False): - """ - The potential scale reduction factor (PSRF) defined by the variance - within one chain, W, with the variance between chains B. - Both variances are combined in a weighted sum to obtain an estimate of - the variance of a parameter \\( \\theta \\).The square root of the - ratio of this estimates variance to the within chain variance is called - the potential scale reduction. - For a well converged chain it should approach 1. Values greater than - 1.1 typically indicate that the chains have not yet fully converged. - - Source: http://joergdietrich.github.io/emcee-convergence.html - - https://github.com/jwalton3141/jwalton3141.github.io/blob/master/assets/posts/ESS/rwmh.py - - Parameters - ---------- - chain : array (n_walkers, n_steps, n_params) - The emcee ensamples. - - Returns - ------- - R_hat : float - The Gelman-Robin values. - - """ - m_chains, n_iters = chain.shape[:2] - - # Calculate between-chain variance - θb = np.mean(chain, axis=1) - θbb = np.mean(θb, axis=0) - B_over_n = ((θbb - θb)**2).sum(axis=0) - B_over_n /= (m_chains - 1) - - # Calculate within-chain variances - ssq = np.var(chain, axis=1, ddof=1) - W = np.mean(ssq, axis=0) - - # (over) estimate of variance - var_θ = W * (n_iters - 1) / n_iters + B_over_n - - if return_var: - return var_θ - else: - # The square root of the ratio of this estimates variance to the - # within chain variance - R_hat = np.sqrt(var_θ / W) - return R_hat - - # ------------------------------------------------------------------------- - def marginal_llk_emcee(self, sampler, nburn=None, logp=None, maxiter=1000): - """ - The Bridge Sampling Estimator of the Marginal Likelihood based on - https://gist.github.com/junpenglao/4d2669d69ddfe1d788318264cdcf0583 - - Parameters - ---------- - sampler : TYPE - MultiTrace, result of MCMC run. - nburn : int, optional - Number of burn-in step. The default is None. - logp : TYPE, optional - Model Log-probability function. The default is None. - maxiter : int, optional - Maximum number of iterations. The default is 1000. - - Returns - ------- - marg_llk : dict - Estimated Marginal log-Likelihood. - - """ - r0, tol1, tol2 = 0.5, 1e-10, 1e-4 - - if logp is None: - logp = sampler.log_prob_fn - - # Split the samples into two parts - # Use the first 50% for fiting the proposal distribution - # and the second 50% in the iterative scheme. - if nburn is None: - mtrace = sampler.chain - else: - mtrace = sampler.chain[:, nburn:, :] - - nchain, len_trace, nrofVars = mtrace.shape - - N1_ = len_trace // 2 - N1 = N1_*nchain - N2 = len_trace*nchain - N1 - - samples_4_fit = np.zeros((nrofVars, N1)) - samples_4_iter = np.zeros((nrofVars, N2)) - effective_n = np.zeros((nrofVars)) - - # matrix with already transformed samples - for var in range(nrofVars): - - # for fitting the proposal - x = mtrace[:, :N1_, var] - - samples_4_fit[var, :] = x.flatten() - # for the iterative scheme - x2 = mtrace[:, N1_:, var] - samples_4_iter[var, :] = x2.flatten() - - # effective sample size of samples_4_iter, scalar - effective_n[var] = self._my_ESS(x2) - - # median effective sample size (scalar) - neff = np.median(effective_n) - - # get mean & covariance matrix and generate samples from proposal - m = np.mean(samples_4_fit, axis=1) - V = np.cov(samples_4_fit) - L = chol(V, lower=True) - - # Draw N2 samples from the proposal distribution - gen_samples = m[:, None] + np.dot( - L, st.norm.rvs(0, 1, size=samples_4_iter.shape) - ) - - # Evaluate proposal distribution for posterior & generated samples - q12 = st.multivariate_normal.logpdf(samples_4_iter.T, m, V) - q22 = st.multivariate_normal.logpdf(gen_samples.T, m, V) - - # Evaluate unnormalized posterior for posterior & generated samples - q11 = logp(samples_4_iter.T) - q21 = logp(gen_samples.T) - - # Run iterative scheme: - tmp = self._iterative_scheme( - N1, N2, q11, q12, q21, q22, r0, neff, tol1, maxiter, 'r' - ) - if ~np.isfinite(tmp['logml']): - warnings.warn( - "logml could not be estimated within maxiter, rerunning with " - "adjusted starting value. Estimate might be more variable than" - " usual.") - # use geometric mean as starting value - r0_2 = np.sqrt(tmp['r_vals'][-2]*tmp['r_vals'][-1]) - tmp = self._iterative_scheme( - q11, q12, q21, q22, r0_2, neff, tol2, maxiter, 'logml' - ) - - marg_llk = dict( - logml=tmp['logml'], niter=tmp['niter'], method="normal", - q11=q11, q12=q12, q21=q21, q22=q22 - ) - return marg_llk - - # ------------------------------------------------------------------------- - def _iterative_scheme(self, N1, N2, q11, q12, q21, q22, r0, neff, tol, - maxiter, criterion): - """ - Iterative scheme as proposed in Meng and Wong (1996) to estimate the - marginal likelihood - - """ - l1 = q11 - q12 - l2 = q21 - q22 - # To increase numerical stability, - # subtracting the median of l1 from l1 & l2 later - lstar = np.median(l1) - s1 = neff/(neff + N2) - s2 = N2/(neff + N2) - r = r0 - r_vals = [r] - logml = np.log(r) + lstar - criterion_val = 1 + tol - - i = 0 - while (i <= maxiter) & (criterion_val > tol): - rold = r - logmlold = logml - numi = np.exp(l2 - lstar)/(s1 * np.exp(l2 - lstar) + s2 * r) - deni = 1/(s1 * np.exp(l1 - lstar) + s2 * r) - if np.sum(~np.isfinite(numi))+np.sum(~np.isfinite(deni)) > 0: - warnings.warn( - """Infinite value in iterative scheme, returning NaN. - Try rerunning with more samples.""") - r = (N1/N2) * np.sum(numi)/np.sum(deni) - r_vals.append(r) - logml = np.log(r) + lstar - i += 1 - if criterion == 'r': - criterion_val = np.abs((r - rold)/r) - elif criterion == 'logml': - criterion_val = np.abs((logml - logmlold)/logml) - - if i >= maxiter: - return dict(logml=np.NaN, niter=i, r_vals=np.asarray(r_vals)) - else: - return dict(logml=logml, niter=i) - - # ------------------------------------------------------------------------- - def _my_ESS(self, x): - """ - Compute the effective sample size of estimand of interest. - Vectorised implementation. - https://github.com/jwalton3141/jwalton3141.github.io/blob/master/assets/posts/ESS/rwmh.py - - - Parameters - ---------- - x : array of shape (n_walkers, n_steps) - MCMC Samples. - - Returns - ------- - int - Effective sample size. - - """ - m_chains, n_iters = x.shape - - def variogram(t): - variogram = ((x[:, t:] - x[:, :(n_iters - t)])**2).sum() - variogram /= (m_chains * (n_iters - t)) - return variogram - - post_var = self.gelman_rubin(x, return_var=True) - - t = 1 - rho = np.ones(n_iters) - negative_autocorr = False - - # Iterate until the sum of consecutive estimates of autocorrelation is - # negative - while not negative_autocorr and (t < n_iters): - rho[t] = 1 - variogram(t) / (2 * post_var) - - if not t % 2: - negative_autocorr = sum(rho[t-1:t+1]) < 0 - - t += 1 - - return int(m_chains*n_iters / (1 + 2*rho[1:t].sum())) - - # ------------------------------------------------------------------------- - def _check_ranges(self, theta, ranges): - """ - This function checks if theta lies in the given ranges. - - Parameters - ---------- - theta : array - Proposed parameter set. - ranges : nested list - List of the praremeter ranges. - - Returns - ------- - c : bool - If it lies in the given range, it return True else False. - - """ - c = True - # traverse in the list1 - for i, bounds in enumerate(ranges): - x = theta[i] - # condition check - if x < bounds[0] or x > bounds[1]: - c = False - return c - return c diff --git a/examples/only-model/bayesvalidrox/bayesvalidrox.mplstyle b/examples/only-model/bayesvalidrox/bayesvalidrox.mplstyle deleted file mode 100644 index 1f31c01f24597de0e0be741be4d3a706c4213a6c..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/bayesvalidrox.mplstyle +++ /dev/null @@ -1,16 +0,0 @@ -figure.titlesize : 30 -axes.titlesize : 30 -axes.labelsize : 30 -axes.linewidth : 3 -axes.grid : True -lines.linewidth : 3 -lines.markersize : 10 -xtick.labelsize : 30 -ytick.labelsize : 30 -legend.fontsize : 30 -font.family : serif -font.serif : Arial -font.size : 30 -text.usetex : True -grid.linestyle : - -figure.figsize : 24, 16 diff --git a/examples/only-model/bayesvalidrox/desktop.ini b/examples/only-model/bayesvalidrox/desktop.ini deleted file mode 100644 index 632de13ae6b61cecf0d9fdbf9c97cfb16bfb51a4..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/desktop.ini +++ /dev/null @@ -1,2 +0,0 @@ -[LocalizedFileNames] -exploration.py=@exploration.py,0 diff --git a/examples/only-model/bayesvalidrox/post_processing/__init__.py b/examples/only-model/bayesvalidrox/post_processing/__init__.py deleted file mode 100644 index 81c9825420b6ed3f027fb3c141be8af05a89f695..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/post_processing/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from .post_processing import PostProcessing - -__all__ = [ - "PostProcessing" - ] diff --git a/examples/only-model/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc b/examples/only-model/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index e9108fe43d767221eb456e5fec28ee23ba4afbe5..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc b/examples/only-model/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc deleted file mode 100644 index e253baea5f0711c774d08f539558c911df465f70..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/post_processing/post_processing.py b/examples/only-model/bayesvalidrox/post_processing/post_processing.py deleted file mode 100644 index 3a89cf739e9b2a221ec1c078947d3624c747af96..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/post_processing/post_processing.py +++ /dev/null @@ -1,1352 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import numpy as np -import math -import os -from itertools import combinations, cycle -import pandas as pd -import scipy.stats as stats -from sklearn.linear_model import LinearRegression -from sklearn.metrics import mean_squared_error, r2_score -import matplotlib.pyplot as plt -import matplotlib.ticker as ticker -from matplotlib.offsetbox import AnchoredText -from matplotlib.patches import Patch -# Load the mplstyle -#plt.style.use(os.path.join(os.path.split(__file__)[0], -# '../', 'bayesvalidrox.mplstyle')) - - -class PostProcessing: - """ - This class provides many helper functions to post-process the trained - meta-model. - - Attributes - ---------- - MetaModel : obj - MetaModel object to do postprocessing on. - name : str - Type of the anaylsis. The default is `'calib'`. If a validation is - expected to be performed change this to `'valid'`. - """ - - def __init__(self, MetaModel, name='calib'): - self.MetaModel = MetaModel - self.name = name - - # ------------------------------------------------------------------------- - def plot_moments(self, xlabel='Time [s]', plot_type=None): - """ - Plots the moments in a pdf format in the directory - `Outputs_PostProcessing`. - - Parameters - ---------- - xlabel : str, optional - String to be displayed as x-label. The default is `'Time [s]'`. - plot_type : str, optional - Options: bar or line. The default is `None`. - - Returns - ------- - pce_means: dict - Mean of the model outputs. - pce_means: dict - Standard deviation of the model outputs. - - """ - - bar_plot = True if plot_type == 'bar' else False - meta_model_type = self.MetaModel.meta_model_type - Model = self.MetaModel.ModelObj - - # Read Monte-Carlo reference - self.mc_reference = Model.read_mc_reference() - - # Set the x values - x_values_orig = self.MetaModel.ExpDesign.x_values - - # Compute the moments with the PCEModel object - self.pce_means, self.pce_stds = self.compute_pce_moments() - - # Get the variables - out_names = Model.Output.names - - # Open a pdf for the plots - newpath = (f'Outputs_PostProcessing_{self.name}/') - if not os.path.exists(newpath): - os.makedirs(newpath) - - # Plot the best fit line, set the linewidth (lw), color and - # transparency (alpha) of the line - for key in out_names: - fig, ax = plt.subplots(nrows=1, ncols=2) - - # Extract mean and std - mean_data = self.pce_means[key] - std_data = self.pce_stds[key] - - # Extract a list of x values - if type(x_values_orig) is dict: - x = x_values_orig[key] - else: - x = x_values_orig - - # Plot: bar plot or line plot - if bar_plot: - ax[0].bar(list(map(str, x)), mean_data, color='b', - width=0.25) - ax[1].bar(list(map(str, x)), std_data, color='b', - width=0.25) - ax[0].legend(labels=[meta_model_type]) - ax[1].legend(labels=[meta_model_type]) - else: - #print(x) - #print(mean_data) - ax[0].plot(x, mean_data, lw=3, color='k', marker='x', - label=meta_model_type) - ax[1].plot(x, std_data, lw=3, color='k', marker='x', - label=meta_model_type) - - if self.mc_reference is not None: - if bar_plot: - ax[0].bar(list(map(str, x)), self.mc_reference['mean'], - color='r', width=0.25) - ax[1].bar(list(map(str, x)), self.mc_reference['std'], - color='r', width=0.25) - ax[0].legend(labels=[meta_model_type]) - ax[1].legend(labels=[meta_model_type]) - else: - ax[0].plot(x, self.mc_reference['mean'], lw=3, marker='x', - color='r', label='Ref.') - ax[1].plot(x, self.mc_reference['std'], lw=3, marker='x', - color='r', label='Ref.') - - # Label the axes and provide a title - ax[0].set_xlabel(xlabel) - ax[1].set_xlabel(xlabel) - ax[0].set_ylabel(key) - ax[1].set_ylabel(key) - - # Provide a title - ax[0].set_title('Mean of ' + key) - ax[1].set_title('Std of ' + key) - - if not bar_plot: - ax[0].legend(loc='best') - ax[1].legend(loc='best') - - plt.tight_layout() - - # save the current figure - fig.savefig( - f'./{newpath}Mean_Std_PCE_{key}.pdf', - bbox_inches='tight' - ) - - return self.pce_means, self.pce_stds - - # ------------------------------------------------------------------------- - def valid_metamodel(self, n_samples=1, samples=None, model_out_dict=None, - x_axis='Time [s]'): - """ - Evaluates and plots the meta model and the PCEModel outputs for the - given number of samples or the given samples. - - Parameters - ---------- - n_samples : int, optional - Number of samples to be evaluated. The default is 1. - samples : array of shape (n_samples, n_params), optional - Samples to be evaluated. The default is None. - model_out_dict: dict - The model runs using the samples provided. - x_axis : str, optional - Label of x axis. The default is `'Time [s]'`. - - Returns - ------- - None. - - """ - MetaModel = self.MetaModel - Model = MetaModel.ModelObj - - if samples is None: - self.n_samples = n_samples - samples = self._get_sample() - else: - self.n_samples = samples.shape[0] - - # Extract x_values - x_values = MetaModel.ExpDesign.x_values - - if model_out_dict is not None: - self.model_out_dict = model_out_dict - else: - self.model_out_dict = self._eval_model(samples, key_str='valid') - self.pce_out_mean, self.pce_out_std = MetaModel.eval_metamodel(samples) - - try: - key = Model.Output.names[1] - except IndexError: - key = Model.Output.names[0] - - n_obs = self.model_out_dict[key].shape[1] - - if n_obs == 1: - self._plot_validation() - else: - print(x_values) - self._plot_validation_multi(x_values=x_values, x_axis=x_axis) - - # ------------------------------------------------------------------------- - def check_accuracy(self, n_samples=None, samples=None, outputs=None): - """ - Checks accuracy of the metamodel by computing the root mean square - error and validation error for all outputs. - - Parameters - ---------- - n_samples : int, optional - Number of samples. The default is None. - samples : array of shape (n_samples, n_params), optional - Parameter sets to be checked. The default is None. - outputs : dict, optional - Output dictionary with model outputs for all given output types in - `Model.Output.names`. The default is None. - - Raises - ------ - Exception - When neither n_samples nor samples are provided. - - Returns - ------- - rmse: dict - Root mean squared error for each output. - valid_error : dict - Validation error for each output. - - """ - MetaModel = self.MetaModel - Model = MetaModel.ModelObj - - # Set the number of samples - if n_samples: - self.n_samples = n_samples - elif samples is not None: - self.n_samples = samples.shape[0] - else: - raise Exception("Please provide either samples or pass number of " - "samples!") - - # Generate random samples if necessary - Samples = self._get_sample() if samples is None else samples - - # Run the original model with the generated samples - if outputs is None: - outputs = self._eval_model(Samples, key_str='validSet') - - # Run the PCE model with the generated samples - pce_outputs, _ = MetaModel.eval_metamodel(samples=Samples) - - self.rmse = {} - self.valid_error = {} - # Loop over the keys and compute RMSE error. - for key in Model.Output.names: - # Root mena square - self.rmse[key] = mean_squared_error(outputs[key], pce_outputs[key], - squared=False, - multioutput='raw_values') - # Validation error - self.valid_error[key] = (self.rmse[key]**2) / \ - np.var(outputs[key], ddof=1, axis=0) - - # Print a report table - print("\n>>>>> Errors of {} <<<<<".format(key)) - print("\nIndex | RMSE | Validation Error") - print('-'*35) - print('\n'.join(f'{i+1} | {k:.3e} | {j:.3e}' for i, (k, j) - in enumerate(zip(self.rmse[key], - self.valid_error[key])))) - # Save error dicts in PCEModel object - self.MetaModel.rmse = self.rmse - self.MetaModel.valid_error = self.valid_error - - return - - # ------------------------------------------------------------------------- - def plot_seq_design_diagnostics(self, ref_BME_KLD=None): - """ - Plots the Bayesian Model Evidence (BME) and Kullback-Leibler divergence - (KLD) for the sequential design. - - Parameters - ---------- - ref_BME_KLD : array, optional - Reference BME and KLD . The default is `None`. - - Returns - ------- - None. - - """ - PCEModel = self.MetaModel - n_init_samples = PCEModel.ExpDesign.n_init_samples - n_total_samples = PCEModel.ExpDesign.X.shape[0] - - newpath = f'Outputs_PostProcessing_{self.name}/seq_design_diagnostics/' - if not os.path.exists(newpath): - os.makedirs(newpath) - - plotList = ['Modified LOO error', 'Validation error', 'KLD', 'BME', - 'RMSEMean', 'RMSEStd', 'Hellinger distance'] - seqList = [PCEModel.SeqModifiedLOO, PCEModel.seqValidError, - PCEModel.SeqKLD, PCEModel.SeqBME, PCEModel.seqRMSEMean, - PCEModel.seqRMSEStd, PCEModel.SeqDistHellinger] - - markers = ('x', 'o', 'd', '*', '+') - colors = ('k', 'darkgreen', 'b', 'navy', 'darkred') - - # Plot the evolution of the diagnostic criteria of the - # Sequential Experimental Design. - for plotidx, plot in enumerate(plotList): - fig, ax = plt.subplots() - seq_dict = seqList[plotidx] - name_util = list(seq_dict.keys()) - - if len(name_util) == 0: - continue - - # Box plot when Replications have been detected. - if any(int(name.split("rep_", 1)[1]) > 1 for name in name_util): - # Extract the values from dict - sorted_seq_opt = {} - # Number of replications - n_reps = PCEModel.ExpDesign.n_replication - - # Get the list of utility function names - # Handle if only one UtilityFunction is provided - if not isinstance(PCEModel.ExpDesign.util_func, list): - util_funcs = [PCEModel.ExpDesign.util_func] - else: - util_funcs = PCEModel.ExpDesign.util_func - - for util in util_funcs: - sortedSeq = {} - # min number of runs available from reps - n_runs = min([seq_dict[f'{util}_rep_{i+1}'].shape[0] - for i in range(n_reps)]) - - for runIdx in range(n_runs): - values = [] - for key in seq_dict.keys(): - if util in key: - values.append(seq_dict[key][runIdx].mean()) - sortedSeq['SeqItr_'+str(runIdx)] = np.array(values) - sorted_seq_opt[util] = sortedSeq - - # BoxPlot - def draw_plot(data, labels, edge_color, fill_color, idx): - pos = labels - (idx-1) - bp = plt.boxplot(data, positions=pos, labels=labels, - patch_artist=True, sym='', widths=0.75) - elements = ['boxes', 'whiskers', 'fliers', 'means', - 'medians', 'caps'] - for element in elements: - plt.setp(bp[element], color=edge_color[idx]) - - for patch in bp['boxes']: - patch.set(facecolor=fill_color[idx]) - - if PCEModel.ExpDesign.n_new_samples != 1: - step1 = PCEModel.ExpDesign.n_new_samples - step2 = 1 - else: - step1 = 5 - step2 = 5 - edge_color = ['red', 'blue', 'green'] - fill_color = ['tan', 'cyan', 'lightgreen'] - plot_label = plot - # Plot for different Utility Functions - for idx, util in enumerate(util_funcs): - all_errors = np.empty((n_reps, 0)) - - for key in list(sorted_seq_opt[util].keys()): - errors = sorted_seq_opt.get(util, {}).get(key)[:, None] - all_errors = np.hstack((all_errors, errors)) - - # Special cases for BME and KLD - if plot == 'KLD' or plot == 'BME': - # BME convergence if refBME is provided - if ref_BME_KLD is not None: - if plot == 'BME': - refValue = ref_BME_KLD[0] - plot_label = r'BME/BME$^{Ref.}$' - if plot == 'KLD': - refValue = ref_BME_KLD[1] - plot_label = '$D_{KL}[p(\\theta|y_*),p(\\theta)]'\ - ' / D_{KL}^{Ref.}[p(\\theta|y_*), '\ - 'p(\\theta)]$' - - # Difference between BME/KLD and the ref. values - all_errors = np.divide(all_errors, - np.full((all_errors.shape), - refValue)) - - # Plot baseline for zero, i.e. no difference - plt.axhline(y=1.0, xmin=0, xmax=1, c='green', - ls='--', lw=2) - - # Plot each UtilFuncs - labels = np.arange(n_init_samples, n_total_samples+1, step1) - draw_plot(all_errors[:, ::step2], labels, edge_color, - fill_color, idx) - - plt.xticks(labels, labels) - # Set the major and minor locators - ax.xaxis.set_major_locator(ticker.AutoLocator()) - ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) - ax.xaxis.grid(True, which='major', linestyle='-') - ax.xaxis.grid(True, which='minor', linestyle='--') - - # Legend - legend_elements = [] - for idx, util in enumerate(util_funcs): - legend_elements.append(Patch(facecolor=fill_color[idx], - edgecolor=edge_color[idx], - label=util)) - plt.legend(handles=legend_elements[::-1], loc='best') - - if plot != 'BME' and plot != 'KLD': - plt.yscale('log') - plt.autoscale(True) - plt.xlabel('\\# of training samples') - plt.ylabel(plot_label) - plt.title(plot) - - # save the current figure - plot_name = plot.replace(' ', '_') - fig.savefig( - f'./{newpath}/seq_{plot_name}.pdf', - bbox_inches='tight' - ) - # Destroy the current plot - plt.clf() - # Save arrays into files - f = open(f'./{newpath}/seq_{plot_name}.txt', 'w') - f.write(str(sorted_seq_opt)) - f.close() - else: - for idx, name in enumerate(name_util): - seq_values = seq_dict[name] - if PCEModel.ExpDesign.n_new_samples != 1: - step = PCEModel.ExpDesign.n_new_samples - else: - step = 1 - x_idx = np.arange(n_init_samples, n_total_samples+1, step) - if n_total_samples not in x_idx: - x_idx = np.hstack((x_idx, n_total_samples)) - - if plot == 'KLD' or plot == 'BME': - # BME convergence if refBME is provided - if ref_BME_KLD is not None: - if plot == 'BME': - refValue = ref_BME_KLD[0] - plot_label = r'BME/BME$^{Ref.}$' - if plot == 'KLD': - refValue = ref_BME_KLD[1] - plot_label = '$D_{KL}[p(\\theta|y_*),p(\\theta)]'\ - ' / D_{KL}^{Ref.}[p(\\theta|y_*), '\ - 'p(\\theta)]$' - - # Difference between BME/KLD and the ref. values - values = np.divide(seq_values, - np.full((seq_values.shape), - refValue)) - - # Plot baseline for zero, i.e. no difference - plt.axhline(y=1.0, xmin=0, xmax=1, c='green', - ls='--', lw=2) - - # Set the limits - plt.ylim([1e-1, 1e1]) - - # Create the plots - plt.semilogy(x_idx, values, marker=markers[idx], - color=colors[idx], ls='--', lw=2, - label=name.split("_rep", 1)[0]) - else: - plot_label = plot - - # Create the plots - plt.plot(x_idx, seq_values, marker=markers[idx], - color=colors[idx], ls='--', lw=2, - label=name.split("_rep", 1)[0]) - - else: - plot_label = plot - seq_values = np.nan_to_num(seq_values) - - # Plot the error evolution for each output - plt.semilogy(x_idx, seq_values.mean(axis=1), - marker=markers[idx], ls='--', lw=2, - color=colors[idx], - label=name.split("_rep", 1)[0]) - - # Set the major and minor locators - ax.xaxis.set_major_locator(ticker.AutoLocator()) - ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) - ax.xaxis.grid(True, which='major', linestyle='-') - ax.xaxis.grid(True, which='minor', linestyle='--') - - ax.tick_params(axis='both', which='major', direction='in', - width=3, length=10) - ax.tick_params(axis='both', which='minor', direction='in', - width=2, length=8) - plt.xlabel('Number of runs') - plt.ylabel(plot_label) - plt.title(plot) - plt.legend(frameon=True) - - # save the current figure - plot_name = plot.replace(' ', '_') - fig.savefig( - f'./{newpath}/seq_{plot_name}.pdf', - bbox_inches='tight' - ) - # Destroy the current plot - plt.clf() - - # ---------------- Saving arrays into files --------------- - np.save(f'./{newpath}/seq_{plot_name}.npy', seq_values) - - return - - # ------------------------------------------------------------------------- - def sobol_indices(self, xlabel=None, plot_type=None): - """ - Provides Sobol indices as a sensitivity measure to infer the importance - of the input parameters. See Eq. 27 in [1] for more details. For the - case with Principal component analysis refer to [2]. - - [1] Global sensitivity analysis: A flexible and efficient framework - with an example from stochastic hydrogeology S. Oladyshkin, F.P. - de Barros, W. Nowak https://doi.org/10.1016/j.advwatres.2011.11.001 - - [2] Nagel, J.B., Rieckermann, J. and Sudret, B., 2020. Principal - component analysis and sparse polynomial chaos expansions for global - sensitivity analysis and model calibration: Application to urban - drainage simulation. Reliability Engineering & System Safety, 195, - p.106737. - - Parameters - ---------- - xlabel : str, optional - Label of the x-axis. The default is `'Time [s]'`. - plot_type : str, optional - Plot type. The default is `None`. This corresponds to line plot. - Bar chart can be selected by `bar`. - - Returns - ------- - sobol_cell: dict - Sobol indices. - total_sobol: dict - Total Sobol indices. - - """ - #print('arrived') - if not xlabel: - xlabel = 'Time [s]' # test workaround - - # Extract the necessary variables - PCEModel = self.MetaModel - basis_dict = PCEModel.basis_dict - coeffs_dict = PCEModel.coeffs_dict - n_params = PCEModel.n_params - max_order = np.max(PCEModel.pce_deg) - sobol_cell_b = {} - total_sobol_b = {} - cov_Z_p_q = np.zeros((n_params)) - #print('init done') - for b_i in range(PCEModel.n_bootstrap_itrs): - - sobol_cell_, total_sobol_ = {}, {} - - for output in PCEModel.ModelObj.Output.names: - #print(coeffs_dict[f'b_{b_i+1}'][output]) - n_meas_points = len(coeffs_dict[f'b_{b_i+1}'][output]) - #print(n_meas_points) - # Initialize the (cell) array containing the (total) Sobol indices. - sobol_array = dict.fromkeys(range(1, max_order+1), []) - sobol_cell_array = dict.fromkeys(range(1, max_order+1), []) - - for i_order in range(1, max_order+1): - n_comb = math.comb(n_params, i_order) - - sobol_cell_array[i_order] = np.zeros((n_comb, n_meas_points)) - - total_sobol_array = np.zeros((n_params, n_meas_points)) - - # Initialize the cell to store the names of the variables - TotalVariance = np.zeros((n_meas_points)) - #print('stop1') - # Loop over all measurement points and calculate sobol indices - for pIdx in range(n_meas_points): - - # Extract the basis indices (alpha) and coefficients - Basis = basis_dict[f'b_{b_i+1}'][output][f'y_{pIdx+1}'] - - try: - clf_poly = PCEModel.clf_poly[f'b_{b_i+1}'][output][f'y_{pIdx+1}'] - PCECoeffs = clf_poly.coef_ - except: - PCECoeffs = coeffs_dict[f'b_{b_i+1}'][output][f'y_{pIdx+1}'] - - # Compute total variance - TotalVariance[pIdx] = np.sum(np.square(PCECoeffs[1:])) - - nzidx = np.where(PCECoeffs != 0)[0] - # Set all the Sobol indices equal to zero in the presence of a - # null output. - if len(nzidx) == 0: - # This is buggy. - for i_order in range(1, max_order+1): - sobol_cell_array[i_order][:, pIdx] = 0 - - # Otherwise compute them by summing well-chosen coefficients - else: - nz_basis = Basis[nzidx] - for i_order in range(1, max_order+1): - idx = np.where(np.sum(nz_basis > 0, axis=1) == i_order) - subbasis = nz_basis[idx] - Z = np.array(list(combinations(range(n_params), i_order))) - - for q in range(Z.shape[0]): - Zq = Z[q] - subsubbasis = subbasis[:, Zq] - subidx = np.prod(subsubbasis, axis=1) > 0 - sum_ind = nzidx[idx[0][subidx]] - if TotalVariance[pIdx] == 0.0: - sobol_cell_array[i_order][q, pIdx] = 0.0 - else: - sobol = np.sum(np.square(PCECoeffs[sum_ind])) - sobol /= TotalVariance[pIdx] - sobol_cell_array[i_order][q, pIdx] = sobol - - # Compute the TOTAL Sobol indices. - for ParIdx in range(n_params): - idx = nz_basis[:, ParIdx] > 0 - sum_ind = nzidx[idx] - - if TotalVariance[pIdx] == 0.0: - total_sobol_array[ParIdx, pIdx] = 0.0 - else: - sobol = np.sum(np.square(PCECoeffs[sum_ind])) - sobol /= TotalVariance[pIdx] - total_sobol_array[ParIdx, pIdx] = sobol - - # ----- if PCA selected: Compute covariance ----- - if PCEModel.dim_red_method.lower() == 'pca': - # Extract the basis indices (alpha) and coefficients for - # next component - if pIdx < n_meas_points-1: - nextBasis = basis_dict[f'b_{b_i+1}'][output][f'y_{pIdx+2}'] - if PCEModel.bootstrap_method != 'fast' or b_i == 0: - clf_poly = PCEModel.clf_poly[f'b_{b_i+1}'][output][f'y_{pIdx+2}'] - nextPCECoeffs = clf_poly.coef_ - else: - nextPCECoeffs = coeffs_dict[f'b_{b_i+1}'][output][f'y_{pIdx+2}'] - - # Choose the common non-zero basis - mask = (Basis[:, None] == nextBasis).all(-1).any(-1) - n_mask = (nextBasis[:, None] == Basis).all(-1).any(-1) - - # Compute the covariance in Eq 17. - for ParIdx in range(n_params): - idx = (mask) & (Basis[:, ParIdx] > 0) - n_idx = (n_mask) & (nextBasis[:, ParIdx] > 0) - try: - cov_Z_p_q[ParIdx] += np.sum(np.dot( - PCECoeffs[idx], nextPCECoeffs[n_idx]) - ) - except: - pass - - # Compute the sobol indices according to Ref. 2 - if PCEModel.dim_red_method.lower() == 'pca': - n_c_points = PCEModel.ExpDesign.Y[output].shape[1] - PCA = PCEModel.pca[f'b_{b_i+1}'][output] - compPCA = PCA.components_ - nComp = compPCA.shape[0] - var_Z_p = PCA.explained_variance_ - - # Extract the sobol index of the components - for i_order in range(1, max_order+1): - n_comb = math.comb(n_params, i_order) - sobol_array[i_order] = np.zeros((n_comb, n_c_points)) - Z = np.array(list(combinations(range(n_params), i_order))) - - # Loop over parameters - for q in range(Z.shape[0]): - S_Z_i = sobol_cell_array[i_order][q] - - for tIdx in range(n_c_points): - var_Y_t = np.var( - PCEModel.ExpDesign.Y[output][:, tIdx]) - if var_Y_t == 0.0: - term1, term2 = 0.0, 0.0 - else: - # Eq. 17 - term1 = 0.0 - for i in range(nComp): - a = S_Z_i[i] * var_Z_p[i] - a *= compPCA[i, tIdx]**2 - term1 += a - - # TODO: Term 2 - # term2 = 0.0 - # for i in range(nComp-1): - # term2 += cov_Z_p_q[q] * compPCA[i, tIdx] - # term2 *= compPCA[i+1, tIdx] - # term2 *= 2 - - sobol_array[i_order][q, tIdx] = term1 #+ term2 - - # Devide over total output variance Eq. 18 - sobol_array[i_order][q, tIdx] /= var_Y_t - - # Compute the TOTAL Sobol indices. - total_sobol = np.zeros((n_params, n_c_points)) - for ParIdx in range(n_params): - S_Z_i = total_sobol_array[ParIdx] - - for tIdx in range(n_c_points): - var_Y_t = np.var(PCEModel.ExpDesign.Y[output][:, tIdx]) - if var_Y_t == 0.0: - term1, term2 = 0.0, 0.0 - else: - term1 = 0 - for i in range(nComp): - term1 += S_Z_i[i] * var_Z_p[i] * \ - (compPCA[i, tIdx]**2) - - # Term 2 - term2 = 0 - for i in range(nComp-1): - term2 += cov_Z_p_q[ParIdx] * compPCA[i, tIdx] \ - * compPCA[i+1, tIdx] - term2 *= 2 - - total_sobol[ParIdx, tIdx] = term1 #+ term2 - - # Devide over total output variance Eq. 18 - total_sobol[ParIdx, tIdx] /= var_Y_t - - sobol_cell_[output] = sobol_array - total_sobol_[output] = total_sobol - else: - sobol_cell_[output] = sobol_cell_array - total_sobol_[output] = total_sobol_array - - # Save for each bootsrtap iteration - sobol_cell_b[b_i] = sobol_cell_ - total_sobol_b[b_i] = total_sobol_ - - # Average total sobol indices - total_sobol_all = {} - for i in sorted(total_sobol_b): - for k, v in total_sobol_b[i].items(): - if k not in total_sobol_all: - total_sobol_all[k] = [None] * len(total_sobol_b) - total_sobol_all[k][i] = v - - self.total_sobol = {} - for output in PCEModel.ModelObj.Output.names: - self.total_sobol[output] = np.mean(total_sobol_all[output], axis=0) - - # ---------------- Plot ----------------------- - par_names = PCEModel.ExpDesign.par_names - x_values_orig = PCEModel.ExpDesign.x_values - - newpath = (f'Outputs_PostProcessing_{self.name}/') - if not os.path.exists(newpath): - os.makedirs(newpath) - - fig = plt.figure() - - # Do the plots and save sobol results - uses self.total_sobol - for outIdx, output in enumerate(PCEModel.ModelObj.Output.names): - - # Extract total Sobol indices - total_sobol = self.total_sobol[output] - - # Compute quantiles - q_5 = np.quantile(total_sobol_all[output], q=0.05, axis=0) - q_97_5 = np.quantile(total_sobol_all[output], q=0.975, axis=0) - - # Extract a list of x values - if type(x_values_orig) is dict: - x = x_values_orig[output] - else: - x = x_values_orig - - if plot_type == 'bar': - ax = fig.add_axes([0, 0, 1, 1]) - dict1 = {xlabel: x} - dict2 = {param: sobolIndices for param, sobolIndices - in zip(par_names, total_sobol)} - - df = pd.DataFrame({**dict1, **dict2}) - df.plot(x=xlabel, y=par_names, kind="bar", ax=ax, rot=0, - colormap='Dark2', yerr=q_97_5-q_5) - ax.set_ylabel('Total Sobol indices, $S^T$') - - else: - for i, sobolIndices in enumerate(total_sobol): - plt.plot(x, sobolIndices, label=par_names[i], - marker='x', lw=2.5) - plt.fill_between(x, q_5[i], q_97_5[i], alpha=0.15) - - plt.ylabel('Total Sobol indices, $S^T$') - plt.xlabel(xlabel) - - plt.title(f'Sensitivity analysis of {output}') - if plot_type != 'bar': - plt.legend(loc='best', frameon=True) - - # Save indices - np.savetxt(f'./{newpath}totalsobol_' + - output.replace('/', '_') + '.csv', - total_sobol.T, delimiter=',', - header=','.join(par_names), comments='') - - # save the current figure - fig.savefig( - f'./{newpath}Sobol_indices_{output}.pdf', - bbox_inches='tight' - ) - - # Destroy the current plot - plt.clf() - - return self.total_sobol - - # ------------------------------------------------------------------------- - def check_reg_quality(self, n_samples=1000, samples=None): - """ - Checks the quality of the metamodel for single output models based on: - https://towardsdatascience.com/how-do-you-check-the-quality-of-your-regression-model-in-python-fa61759ff685 - - - Parameters - ---------- - n_samples : int, optional - Number of parameter sets to use for the check. The default is 1000. - samples : array of shape (n_samples, n_params), optional - Parameter sets to use for the check. The default is None. - - Returns - ------- - None. - - """ - MetaModel = self.MetaModel - - if samples is None: - self.n_samples = n_samples - samples = self._get_sample() - else: - self.n_samples = samples.shape[0] - - # Evaluate the original and the surrogate model - y_val = self._eval_model(samples, key_str='valid') - y_pce_val, _ = MetaModel.eval_metamodel(samples=samples) - - # Open a pdf for the plots - newpath = f'Outputs_PostProcessing_{self.name}/' - if not os.path.exists(newpath): - os.makedirs(newpath) - - # Fit the data(train the model) - for key in y_pce_val.keys(): - print(key) - y_pce_val_ = y_pce_val[key] - y_val_ = y_val[key] - residuals = y_val_ - y_pce_val_ - if residuals.shape[1] != 0: - sum_residuals = np.mean(residuals, axis = 1) # TODO: mean here? or sum? - - # ------ Residuals vs. predicting variables ------ - # Check the assumptions of linearity and independence - fig1 = plt.figure() - for i, par in enumerate(MetaModel.ExpDesign.par_names): - plt.title(f"{key}: Residuals vs. {par}") - #print(samples[:, i].shape) - #print(residuals.shape) - #print(MetaModel.ExpDesign.par_names) - plt.scatter( - x=samples[:, i], y=sum_residuals, color='blue', edgecolor='k') # TODO: issues here with sizes for different times - plt.grid(True) - xmin, xmax = min(samples[:, i]), max(samples[:, i]) - plt.hlines(y=0, xmin=xmin*0.9, xmax=xmax*1.1, color='red', - lw=3, linestyle='--') - plt.xlabel(par) - plt.ylabel('Residuals') - plt.show() - - # save the current figure - fig1.savefig(f'./{newpath}/Residuals_vs_Par_{i+1}.pdf', - bbox_inches='tight') - # Destroy the current plot - plt.clf() - - # ------ Fitted vs. residuals ------ - # Check the assumptions of linearity and independence - fig2 = plt.figure() - plt.title(f"{key}: Residuals vs. fitted values") - plt.scatter(x=y_pce_val_, y=residuals, color='blue', edgecolor='k') - plt.grid(True) - xmin, xmax = np.min(y_val_), np.max(y_val_) # TODO: changed this here to np - plt.hlines(y=0, xmin=xmin*0.9, xmax=xmax*1.1, color='red', lw=3, - linestyle='--') - plt.xlabel(key) - plt.ylabel('Residuals') - plt.show() - - # save the current figure - fig2.savefig(f'./{newpath}/Fitted_vs_Residuals.pdf', - bbox_inches='tight') - # Destroy the current plot - plt.clf() - - # ------ Histogram of normalized residuals ------ - fig3 = plt.figure() - resid_pearson = residuals / (np.max(residuals)-np.min(residuals)) # TODO: changed this here to np - plt.hist(resid_pearson, bins=20, edgecolor='k') - plt.ylabel('Count') - plt.xlabel('Normalized residuals') - plt.title(f"{key}: Histogram of normalized residuals") - - # Normality (Shapiro-Wilk) test of the residuals - ax = plt.gca() - _, p = stats.shapiro(residuals) - if p < 0.01: - annText = "The residuals seem to come from Gaussian process." - else: - annText = "The normality assumption may not hold." - at = AnchoredText(annText, prop=dict(size=30), frameon=True, - loc='upper left') - at.patch.set_boxstyle("round,pad=0.,rounding_size=0.2") - ax.add_artist(at) - - plt.show() - - # save the current figure - fig3.savefig(f'./{newpath}/Hist_NormResiduals.pdf', - bbox_inches='tight') - # Destroy the current plot - plt.clf() - - # ------ Q-Q plot of the normalized residuals ------ - plt.figure() - stats.probplot(residuals[:, 0], plot=plt) - plt.xticks() - plt.yticks() - plt.xlabel("Theoretical quantiles") - plt.ylabel("Sample quantiles") - plt.title(f"{key}: Q-Q plot of normalized residuals") - plt.grid(True) - plt.show() - - # save the current figure - plt.savefig(f'./{newpath}/QQPlot_NormResiduals.pdf', - bbox_inches='tight') - # Destroy the current plot - plt.clf() - - # ------------------------------------------------------------------------- - def eval_pce_model_3d(self): - - self.n_samples = 1000 - - PCEModel = self.MetaModel - Model = self.MetaModel.ModelObj - n_samples = self.n_samples - - # Create 3D-Grid - # TODO: Make it general - x = np.linspace(-5, 10, n_samples) - y = np.linspace(0, 15, n_samples) - - X, Y = np.meshgrid(x, y) - PCE_Z = np.zeros((self.n_samples, self.n_samples)) - Model_Z = np.zeros((self.n_samples, self.n_samples)) - - for idxMesh in range(self.n_samples): - sample_mesh = np.vstack((X[:, idxMesh], Y[:, idxMesh])).T - - univ_p_val = PCEModel.univ_basis_vals(sample_mesh) - - for Outkey, ValuesDict in PCEModel.coeffs_dict.items(): - - pce_out_mean = np.zeros((len(sample_mesh), len(ValuesDict))) - pce_out_std = np.zeros((len(sample_mesh), len(ValuesDict))) - model_outs = np.zeros((len(sample_mesh), len(ValuesDict))) - - for Inkey, InIdxValues in ValuesDict.items(): - print(Inkey.split('_')) - idx = int(Inkey.split('_')[1]) - 1 - basis_deg_ind = PCEModel.basis_dict[Outkey][Inkey] - clf_poly = PCEModel.clf_poly[Outkey][Inkey] - - PSI_Val = PCEModel.create_psi(basis_deg_ind, univ_p_val) - - # Perdiction with error bar - y_mean, y_std = clf_poly.predict(PSI_Val, return_std=True) - - pce_out_mean[:, idx] = y_mean - pce_out_std[:, idx] = y_std - - # Model evaluation - model_out_dict, _ = Model.run_model_parallel(sample_mesh, - key_str='Valid3D') - model_outs[:, idx] = model_out_dict[Outkey].T - - PCE_Z[:, idxMesh] = y_mean - Model_Z[:, idxMesh] = model_outs[:, 0] - - # ---------------- 3D plot for PCEModel ----------------------- - fig_PCE = plt.figure() - ax = plt.axes(projection='3d') - ax.plot_surface(X, Y, PCE_Z, rstride=1, cstride=1, - cmap='viridis', edgecolor='none') - ax.set_title('PCEModel') - ax.set_xlabel('$x_1$') - ax.set_ylabel('$x_2$') - ax.set_zlabel('$f(x_1,x_2)$') - - plt.grid() - plt.show() - - # Saving the figure - newpath = f'Outputs_PostProcessing_{self.name}/' - if not os.path.exists(newpath): - os.makedirs(newpath) - - # save the figure to file - fig_PCE.savefig(f'./{newpath}/3DPlot_PCEModel.pdf', - bbox_inches='tight') - plt.close(fig_PCE) - - # ---------------- 3D plot for Model ----------------------- - fig_Model = plt.figure() - ax = plt.axes(projection='3d') - ax.plot_surface(X, Y, PCE_Z, rstride=1, cstride=1, - cmap='viridis', edgecolor='none') - ax.set_title('Model') - ax.set_xlabel('$x_1$') - ax.set_ylabel('$x_2$') - ax.set_zlabel('$f(x_1,x_2)$') - - plt.grid() - plt.show() - - # Save the figure - fig_Model.savefig(f'./{newpath}/3DPlot_Model.pdf', - bbox_inches='tight') - plt.close(fig_Model) - - return - - # ------------------------------------------------------------------------- - def compute_pce_moments(self): - """ - Computes the first two moments using the PCE-based meta-model. - - Returns - ------- - pce_means: dict - The first moments (mean) of outpust. - pce_means: dict - The first moments (mean) of outpust. - - """ - - MetaModel = self.MetaModel - outputs = MetaModel.ModelObj.Output.names - pce_means_b = {} - pce_stds_b = {} - - # Loop over bootstrap iterations - for b_i in range(MetaModel.n_bootstrap_itrs): - # Loop over the metamodels - coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items() - means = {} - stds = {} - for output, coef_dict in coeffs_dicts: - - pce_mean = np.zeros((len(coef_dict))) - pce_var = np.zeros((len(coef_dict))) - - for index, values in coef_dict.items(): - idx = int(index.split('_')[1]) - 1 - coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index] - - # Mean = c_0 - if coeffs[0] != 0: - pce_mean[idx] = coeffs[0] - else: - clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output] - pce_mean[idx] = clf_poly[index].intercept_ - # Var = sum(coeffs[1:]**2) - pce_var[idx] = np.sum(np.square(coeffs[1:])) - - # Save predictions for each output - if MetaModel.dim_red_method.lower() == 'pca': - PCA = MetaModel.pca[f'b_{b_i+1}'][output] - means[output] = PCA.inverse_transform(pce_mean) - stds[output] = np.sqrt(np.dot(pce_var, - PCA.components_**2)) - else: - means[output] = pce_mean - stds[output] = np.sqrt(pce_var) - - # Save predictions for each bootstrap iteration - pce_means_b[b_i] = means - pce_stds_b[b_i] = stds - - # Change the order of nesting - mean_all = {} - for i in sorted(pce_means_b): - for k, v in pce_means_b[i].items(): - if k not in mean_all: - mean_all[k] = [None] * len(pce_means_b) - mean_all[k][i] = v - std_all = {} - for i in sorted(pce_stds_b): - for k, v in pce_stds_b[i].items(): - if k not in std_all: - std_all[k] = [None] * len(pce_stds_b) - std_all[k][i] = v - - # Back transformation if PCA is selected. - pce_means, pce_stds = {}, {} - for output in outputs: - pce_means[output] = np.mean(mean_all[output], axis=0) - pce_stds[output] = np.mean(std_all[output], axis=0) - - # Print a report table - print("\n>>>>> Moments of {} <<<<<".format(output)) - print("\nIndex | Mean | Std. deviation") - print('-'*35) - print('\n'.join(f'{i+1} | {k:.3e} | {j:.3e}' for i, (k, j) - in enumerate(zip(pce_means[output], - pce_stds[output])))) - print('-'*40) - - return pce_means, pce_stds - - # ------------------------------------------------------------------------- - def _get_sample(self, n_samples=None): - """ - Generates random samples taken from the input parameter space. - - Returns - ------- - samples : array of shape (n_samples, n_params) - Generated samples. - - """ - if n_samples is None: - n_samples = self.n_samples - MetaModel = self.MetaModel - self.samples = MetaModel.ExpDesign.generate_samples( - n_samples, - sampling_method='random') - return self.samples - - # ------------------------------------------------------------------------- - def _eval_model(self, samples=None, key_str='Valid'): - """ - Evaluates Forward Model for the given number of self.samples or given - samples. - - Parameters - ---------- - samples : array of shape (n_samples, n_params), optional - Samples to evaluate the model at. The default is None. - key_str : str, optional - Key string pass to the model. The default is 'Valid'. - - Returns - ------- - model_outs : dict - Dictionary of results. - - """ - Model = self.MetaModel.ModelObj - - if samples is None: - samples = self._get_sample() - self.samples = samples - else: - self.n_samples = len(samples) - - model_outs, _ = Model.run_model_parallel(samples, key_str=key_str) - - return model_outs - - # ------------------------------------------------------------------------- - def _plot_validation(self): - """ - Plots outputs for visual comparison of metamodel outputs with that of - the (full) original model. - - Returns - ------- - None. - - """ - PCEModel = self.MetaModel - - # get the samples - x_val = self.samples - y_pce_val = self.pce_out_mean - y_val = self.model_out_dict - - # Open a pdf for the plots - newpath = f'Outputs_PostProcessing_{self.name}/' - if not os.path.exists(newpath): - os.makedirs(newpath) - - fig = plt.figure() - # Fit the data(train the model) - for key in y_pce_val.keys(): - - y_pce_val_ = y_pce_val[key] - y_val_ = y_val[key] - - regression_model = LinearRegression() - regression_model.fit(y_pce_val_, y_val_) - - # Predict - x_new = np.linspace(np.min(y_pce_val_), np.max(y_val_), 100) - y_predicted = regression_model.predict(x_new[:, np.newaxis]) - - plt.scatter(y_pce_val_, y_val_, color='gold', linewidth=2) - plt.plot(x_new, y_predicted, color='k') - - # Calculate the adjusted R_squared and RMSE - # the total number of explanatory variables in the model - # (not including the constant term) - length_list = [] - for key, value in PCEModel.coeffs_dict['b_1'][key].items(): - length_list.append(len(value)) - n_predictors = min(length_list) - n_samples = x_val.shape[0] - - R2 = r2_score(y_pce_val_, y_val_) - AdjR2 = 1 - (1 - R2) * (n_samples - 1) / \ - (n_samples - n_predictors - 1) - rmse = mean_squared_error(y_pce_val_, y_val_, squared=False) - - plt.annotate(f'RMSE = {rmse:.3f}\n Adjusted $R^2$ = {AdjR2:.3f}', - xy=(0.05, 0.85), xycoords='axes fraction') - - plt.ylabel("Original Model") - plt.xlabel("PCE Model") - plt.grid() - plt.show() - - # save the current figure - plot_name = key.replace(' ', '_') - fig.savefig(f'./{newpath}/Model_vs_PCEModel_{plot_name}.pdf', - bbox_inches='tight') - - # Destroy the current plot - plt.clf() - - # ------------------------------------------------------------------------- - def _plot_validation_multi(self, x_values=[], x_axis="x [m]"): - """ - Plots outputs for visual comparison of metamodel outputs with that of - the (full) multioutput original model - - Parameters - ---------- - x_values : list or array, optional - List of x values. The default is []. - x_axis : str, optional - Label of the x axis. The default is "x [m]". - - Returns - ------- - None. - - """ - Model = self.MetaModel.ModelObj - - newpath = f'Outputs_PostProcessing_{self.name}/' - if not os.path.exists(newpath): - os.makedirs(newpath) - - # List of markers and colors - color = cycle((['b', 'g', 'r', 'y', 'k'])) - marker = cycle(('x', 'd', '+', 'o', '*')) - - fig = plt.figure() - # Plot the model vs PCE model - for keyIdx, key in enumerate(Model.Output.names): - - y_pce_val = self.pce_out_mean[key] - y_pce_val_std = self.pce_out_std[key] - y_val = self.model_out_dict[key] - try: - x = self.model_out_dict['x_values'][key] - except (TypeError, IndexError): - x = x_values - - for idx in range(y_val.shape[0]): - Color = next(color) - Marker = next(marker) - - plt.plot(x, y_val[idx], color=Color, marker=Marker, - label='$Y_{%s}^M$'%(idx+1)) - - plt.plot(x, y_pce_val[idx], color=Color, marker=Marker, - linestyle='--', - label='$Y_{%s}^{PCE}$'%(idx+1)) - plt.fill_between(x, y_pce_val[idx]-1.96*y_pce_val_std[idx], - y_pce_val[idx]+1.96*y_pce_val_std[idx], - color=Color, alpha=0.15) - - # Calculate the RMSE - print(y_pce_val.shape) - print(y_val.shape) - rmse = mean_squared_error(y_pce_val, y_val, squared=False) - R2 = r2_score(y_pce_val[idx].reshape(-1, 1), - y_val[idx].reshape(-1, 1)) - - plt.annotate(f'RMSE = {rmse:.3f}\n $R^2$ = {R2:.3f}', - xy=(0.85, 0.1), xycoords='axes fraction') - - plt.ylabel(key) - plt.xlabel(x_axis) - plt.legend(loc='best') - plt.grid() - - # save the current figure - plot_name = key.replace(' ', '_') - fig.savefig(f'./{newpath}/Model_vs_PCEModel_{plot_name}.pdf', - bbox_inches='tight') - - # Destroy the current plot - plt.clf() - - # Zip the subdirectories - Model.zip_subdirs(f'{Model.name}valid', f'{Model.name}valid_') diff --git a/examples/only-model/bayesvalidrox/pylink/__init__.py b/examples/only-model/bayesvalidrox/pylink/__init__.py deleted file mode 100644 index 4bd81739faf43956324b30f6d8e5365b29d55677..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/pylink/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from .pylink import PyLinkForwardModel - -__all__ = [ - "PyLinkForwardModel" - ] diff --git a/examples/only-model/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc b/examples/only-model/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 0e142124f5bff161ebaf8f9c632cd82465101bbd..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc b/examples/only-model/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc deleted file mode 100644 index 006f78d91ff411531d6e30040c2f8ef05bf10ab2..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/pylink/pylink.py b/examples/only-model/bayesvalidrox/pylink/pylink.py deleted file mode 100644 index 2decb7da4b33565b0d1e4739f0351db66d0c55d6..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/pylink/pylink.py +++ /dev/null @@ -1,664 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from dataclasses import dataclass - -import os -import shutil -import h5py -import numpy as np -import time -import zipfile -import pandas as pd -import multiprocessing -from functools import partial -import tqdm - - -class PyLinkForwardModel(object): - """ - A forward model binder - - This calss serves as a code wrapper. This wrapper allows the execution of - a third-party software/solver within the scope of BayesValidRox. - - Attributes - ---------- - link_type : str - The type of the wrapper. The default is `'pylink'`. This runs the - third-party software or an executable using a sell command with given - input files. - Second option is `'function'` which assumed that model can be run using - a function written separately in a Python script. - name : str - Name of the model. - py_file : str - Python file name without `.py` extension to be run for the `'function'` - wrapper. Note that the name of the python file and that of the function - must be simillar. This function must recieve the parameters in an array - of shape `(n_samples, n_params)` and returns a dictionary with the - x_values and output arrays for given output names. - func_args : dict - Additional arguments for the python file. The default is `{}`. - shell_command : str - Shell command to be executed for the `'pylink'` wrapper. - input_file : str or list - The input file to be passed to the `'pylink'` wrapper. - input_template : str or list - A template input file to be passed to the `'pylink'` wrapper. This file - must be a copy of `input_file` with `<Xi>` place holder for the input - parameters defined using `inputs` class, with i being the number of - parameter. The file name ending should include `.tpl` before the actual - extension of the input file, for example, `params.tpl.input`. - aux_file : str or list - The list of auxiliary files needed for the `'pylink'` wrapper. - exe_path : str - Execution path if you wish to run the model for the `'pylink'` wrapper - in another directory. The default is `None`, which corresponds to the - currecnt working directory. - output_file_names : list of str - List of the name of the model output text files for the `'pylink'` - wrapper. - output_names : list of str - List of the model outputs to be used for the analysis. - output_parser : str - Name of the model parser file (without `.py` extension) that recieves - the `output_file_names` and returns a 2d-array with the first row being - the x_values, e.g. x coordinates or time and the rest of raws pass the - simulation output for each model output defined in `output_names`. Note - that again here the name of the file and that of the function must be - the same. - multi_process: bool - Whether the model runs to be executed in parallel for the `'pylink'` - wrapper. The default is `True`. - n_cpus: int - The number of cpus to be used for the parallel model execution for the - `'pylink'` wrapper. The default is `None`, which corresponds to all - available cpus. - meas_file : str - The name of the measurement text-based file. This file must contain - x_values as the first column and one column for each model output. The - default is `None`. Only needed for the Bayesian Inference. - meas_file_valid : str - The name of the measurement text-based file for the validation. The - default is `None`. Only needed for the validation with Bayesian - Inference. - mc_ref_file : str - The name of the text file for the Monte-Carlo reference (mean and - standard deviation) values. It must contain `x_values` as the first - column, `mean` as the second column and `std` as the third. It can be - used to compare the estimated moments using meta-model in the post- - processing step. This is only available for one output. - obs_dict : dict - A dictionary containing the measurement text-based file. It must - contain `x_values` as the first item and one item for each model output - . The default is `{}`. Only needed for the Bayesian Inference. - obs_dict_valid : dict - A dictionary containing the validation measurement text-based file. It - must contain `x_values` as the first item and one item for each model - output. The default is `{}`. - mc_ref_dict : dict - A dictionary containing the Monte-Carlo reference (mean and standard - deviation) values. It must contain `x_values` as the first item and - `mean` as the second item and `std` as the third. The default is `{}`. - This is only available for one output. - """ - - # Nested class - @dataclass - class OutputData(object): - parser: str = "" - names: list = None - file_names: list = None - - def __init__(self, link_type='pylink', name=None, py_file=None, - func_args={}, shell_command='', input_file=None, - input_template=None, aux_file=None, exe_path='', - output_file_names=[], output_names=[], output_parser='', - multi_process=True, n_cpus=None, meas_file=None, - meas_file_valid=None, mc_ref_file=None, obs_dict={}, - obs_dict_valid={}, mc_ref_dict={}): - self.link_type = link_type - self.name = name - self.shell_command = shell_command - self.py_file = py_file - self.func_args = func_args - self.input_file = input_file - self.input_template = input_template - self.aux_file = aux_file - self.exe_path = exe_path - self.multi_process = multi_process - self.n_cpus = n_cpus - self.Output = self.OutputData( - parser=output_parser, - names=output_names, - file_names=output_file_names, - ) - self.n_outputs = len(self.Output.names) - self.meas_file = meas_file - self.meas_file_valid = meas_file_valid - self.mc_ref_file = mc_ref_file - self.observations = obs_dict - self.observations_valid = obs_dict_valid - self.mc_reference = mc_ref_dict - - # added by Rebecca Kohlhaas - self.cellID = None - - # ------------------------------------------------------------------------- - def within_range(self, out, minout, maxout): - inside = False - if (out > minout).all() and (out < maxout).all(): - inside = True - return inside - - # ------------------------------------------------------------------------- - def read_observation(self, case='calib'): - """ - Reads/prepare the observation/measurement data for - calibration. - - Returns - ------- - DataFrame - A dataframe with the calibration data. - - """ - if case.lower() == 'calib': - if isinstance(self.observations, dict) and bool(self.observations): - obs = pd.DataFrame.from_dict(self.observations) - elif self.meas_file is not None: - file_path = os.path.join(os.getcwd(), self.meas_file) - obs = pd.read_csv(file_path, delimiter=',') - elif isinstance(self.observations, pd.DataFrame): - obs = self.observations - else: - raise Exception("Please provide the observation data as a " - "dictionary via observations attribute or pass" - " the csv-file path to MeasurementFile " - "attribute") - elif case.lower() == 'valid': - if isinstance(self.observations_valid, dict) and \ - bool(self.observations_valid): - obs = pd.DataFrame.from_dict(self.observations_valid) - elif self.meas_file_valid is not None: - file_path = os.path.join(os.getcwd(), self.meas_file_valid) - obs = pd.read_csv(file_path, delimiter=',') - elif isinstance(self.observations_valid, pd.DataFrame): - obs = self.observations_valid - else: - raise Exception("Please provide the observation data as a " - "dictionary via Observations attribute or pass" - " the csv-file path to MeasurementFile " - "attribute") - - # Compute the number of observation - n_obs = obs[self.Output.names].notnull().sum().values.sum() - - if case.lower() == 'calib': - self.observations = obs - self.n_obs = n_obs - return self.observations - elif case.lower() == 'valid': - self.observations_valid = obs - self.n_obs_valid = n_obs - return self.observations_valid - - # ------------------------------------------------------------------------- - def read_mc_reference(self): - """ - Is used, if a Monte-Carlo reference is available for - further in-depth post-processing after meta-model training. - - Returns - ------- - None - - """ - if self.mc_ref_file is None and \ - isinstance(self.mc_reference, pd.DataFrame): - return self.mc_reference - elif isinstance(self.mc_reference, dict) and bool(self.mc_reference): - self.mc_reference = pd.DataFrame.from_dict(self.mc_reference) - elif self.mc_ref_file is not None: - file_path = os.path.join(os.getcwd(), self.mc_ref_file) - self.mc_reference = pd.read_csv(file_path, delimiter=',') - else: - self.mc_reference = None - return self.mc_reference - - # ------------------------------------------------------------------------- - def read_output(self): - """ - Reads the the parser output file and returns it as an - executable function. It is required when the models returns the - simulation outputs in csv files. - - Returns - ------- - Output : func - Output parser function. - - """ - output_func_name = self.Output.parser - - output_func = getattr(__import__(output_func_name), output_func_name) - - file_names = [] - for File in self.Output.file_names: - file_names.append(os.path.join(self.exe_path, File)) - try: - output = output_func(self.name, file_names) - except TypeError: - output = output_func(file_names) - return output - - # ------------------------------------------------------------------------- - def update_input_params(self, new_input_file, param_set): - """ - Finds this pattern with <X1> in the new_input_file and replace it with - the new value from the array param_sets. - - Parameters - ---------- - new_input_file : list - List of the input files with the adapted names. - param_set : array of shape (n_params) - Parameter set. - - Returns - ------- - None. - - """ - NofPa = param_set.shape[0] - text_to_search_list = [f'<X{i+1}>' for i in range(NofPa)] - - for filename in new_input_file: - # Read in the file - with open(filename, 'r') as file: - filedata = file.read() - - # Replace the target string - for text_to_search, params in zip(text_to_search_list, param_set): - filedata = filedata.replace(text_to_search, f'{params:0.4e}') - - # Write the file out again - with open(filename, 'w') as file: - file.write(filedata) - - # ------------------------------------------------------------------------- - def run_command(self, command, output_file_names): - """ - Runs the execution command given by the user to run the given model. - It checks if the output files have been generated. If yes, the jobe is - done and it extracts and returns the requested output(s). Otherwise, - it executes the command again. - - Parameters - ---------- - command : str - The shell command to be executed. - output_file_names : list - Name of the output file names. - - Returns - ------- - simulation_outputs : array of shape (n_obs, n_outputs) - Simulation outputs. - - """ - - # Check if simulation is finished - while True: - time.sleep(3) - files = os.listdir(".") - if all(elem in files for elem in output_file_names): - break - else: - # Run command - Process = os.system(f'./../{command}') - if Process != 0: - print('\nMessage 1:') - print(f'\tIf value of \'{Process}\' is a non-zero value, ' - 'then compilation problems \n' % Process) - - os.chdir("..") - - # Read the output - simulation_outputs = self.read_output() - - return simulation_outputs - - # ------------------------------------------------------------------------- - def run_forwardmodel(self, xx): - """ - This function creates subdirectory for the current run and copies the - necessary files to this directory and renames them. Next, it executes - the given command. - - Parameters - ---------- - xx : tuple - A tuple including parameter set, simulation number and key string. - - Returns - ------- - output : array of shape (n_outputs+1, n_obs) - An array passed by the output paraser containing the x_values as - the first row and the simulations results stored in the the rest of - the array. - - """ - c_points, run_no, key_str = xx - - # Handle if only one imput file is provided - if not isinstance(self.input_template, list): - self.input_template = [self.input_template] - if not isinstance(self.input_file, list): - self.input_file = [self.input_file] - - new_input_file = [] - # Loop over the InputTemplates: - for in_temp in self.input_template: - if '/' in in_temp: - in_temp = in_temp.split('/')[-1] - new_input_file.append(in_temp.split('.tpl')[0] + key_str + - f"_{run_no+1}" + in_temp.split('.tpl')[1]) - - # Create directories - newpath = self.name + key_str + f'_{run_no+1}' - if not os.path.exists(newpath): - os.makedirs(newpath) - - # Copy the necessary files to the directories - for in_temp in self.input_template: - # Input file(s) of the model - shutil.copy2(in_temp, newpath) - # Auxiliary file - if self.aux_file is not None: - shutil.copy2(self.aux_file, newpath) # Auxiliary file - - # Rename the Inputfile and/or auxiliary file - os.chdir(newpath) - for input_tem, input_file in zip(self.input_template, new_input_file): - if '/' in input_tem: - input_tem = input_tem.split('/')[-1] - os.rename(input_tem, input_file) - - # Update the parametrs in Input file - self.update_input_params(new_input_file, c_points) - - # Update the user defined command and the execution path - try: - new_command = self.shell_command.replace(self.input_file[0], - new_input_file[0]) - new_command = new_command.replace(self.input_file[1], - new_input_file[1]) - except: - new_command = self.shell_command.replace(self.input_file[0], - new_input_file[0]) - # Set the exe path if not provided - if not bool(self.exe_path): - self.exe_path = os.getcwd() - - # Run the model - output = self.run_command(new_command, self.Output.file_names) - - return output - - # ------------------------------------------------------------------------- - def run_model_parallel(self, c_points, prevRun_No=0, key_str='', - mp=True, verbose=True): - """ - Runs model simulations. If mp is true (default), then the simulations - are started in parallel. - - Parameters - ---------- - c_points : array of shape (n_samples, n_params) - Collocation points (training set). - prevRun_No : int, optional - Previous run number, in case the sequential design is selected. - The default is `0`. - key_str : str, optional - A descriptive string for validation runs. The default is `''`. - mp : bool, optional - Multiprocessing. The default is `True`. - verbose: bool, optional - Verbosity. The default is `True`. - - Returns - ------- - all_outputs : dict - A dictionary with x values (time step or point id) and all outputs. - Each key contains an array of the shape `(n_samples, n_obs)`. - new_c_points : array - Updated collocation points (training set). If a simulation does not - executed successfully, the parameter set is removed. - - """ - - # Initilization - n_c_points = len(c_points) - all_outputs = {} - - # Extract the function - if self.link_type.lower() == 'function': - # Prepare the function - Function = getattr(__import__(self.py_file), self.py_file) - # --------------------------------------------------------------- - # -------------- Multiprocessing with Pool Class ---------------- - # --------------------------------------------------------------- - # Start a pool with the number of CPUs - if self.n_cpus is None: - n_cpus = multiprocessing.cpu_count() - else: - n_cpus = self.n_cpus - - # Run forward model - if n_c_points == 1 or not mp: - if self.link_type.lower() == 'function': - group_results = Function(c_points, **self.func_args) - else: - group_results = self.run_forwardmodel( - (c_points[0], prevRun_No, key_str) - ) - - elif self.multi_process or mp: - with multiprocessing.Pool(n_cpus) as p: - - if self.link_type.lower() == 'function': - imap_var = p.imap(partial(Function, **self.func_args), - c_points[:, np.newaxis]) - else: - args = zip(c_points, - [prevRun_No+i for i in range(n_c_points)], - [key_str]*n_c_points) - imap_var = p.imap(self.run_forwardmodel, args) - - if verbose: - desc = f'Running forward model {key_str}' - group_results = list(tqdm.tqdm(imap_var, total=n_c_points, - desc=desc)) - else: - group_results = list(imap_var) - - # Check for NaN - for var_i, var in enumerate(self.Output.names): - # If results are given as one dictionary - if isinstance(group_results, dict): - Outputs = np.asarray(group_results[var]) - # If results are given as list of dictionaries - elif isinstance(group_results, list): - Outputs = np.asarray([item[var] for item in group_results], - dtype=np.float64) - NaN_idx = np.unique(np.argwhere(np.isnan(Outputs))[:, 0]) - new_c_points = np.delete(c_points, NaN_idx, axis=0) - all_outputs[var] = np.atleast_2d( - np.delete(Outputs, NaN_idx, axis=0) - ) - - # Print the collocation points whose simulations crashed - if len(NaN_idx) != 0: - print('\n') - print('*'*20) - print("\nThe following parametersets have been removed:\n", - c_points[NaN_idx]) - print("\n") - print('*'*20) - - # Save time steps or x-values - if isinstance(group_results, dict): - all_outputs["x_values"] = group_results["x_values"] - elif any(isinstance(i, dict) for i in group_results): - all_outputs["x_values"] = group_results[0]["x_values"] - - # Store simulations in a hdf5 file - self._store_simulations( - c_points, all_outputs, NaN_idx, key_str, prevRun_No - ) - - return all_outputs, new_c_points - - # ------------------------------------------------------------------------- - def _store_simulations(self, c_points, all_outputs, NaN_idx, key_str, - prevRun_No): - - # Create hdf5 metadata - if key_str == '': - hdf5file = f'ExpDesign_{self.name}.hdf5' # added _{self.ModelObj.func_args} - else: - hdf5file = f'ValidSet_{self.name}.hdf5' - hdf5_exist = os.path.exists(hdf5file) - file = h5py.File(hdf5file, 'a') - - # ---------- Save time steps or x-values ---------- - if not hdf5_exist: - if type(all_outputs["x_values"]) is dict: - grp_x_values = file.create_group("x_values/") - for varIdx, var in enumerate(self.Output.names): - grp_x_values.create_dataset( - var, data=all_outputs["x_values"][var] - ) - else: - file.create_dataset("x_values", data=all_outputs["x_values"]) - - # ---------- Save outputs ---------- - for varIdx, var in enumerate(self.Output.names): - - if not hdf5_exist: - grpY = file.create_group("EDY/"+var) - else: - grpY = file.get("EDY/"+var) - - if prevRun_No == 0 and key_str == '': - grpY.create_dataset(f'init_{key_str}', data=all_outputs[var]) - else: - try: - oldEDY = np.array(file[f'EDY/{var}/adaptive_{key_str}']) - del file[f'EDY/{var}/adaptive_{key_str}'] - data = np.vstack((oldEDY, all_outputs[var])) - except KeyError: - data = all_outputs[var] - grpY.create_dataset('adaptive_'+key_str, data=data) - - if prevRun_No == 0 and key_str == '': - grpY.create_dataset(f"New_init_{key_str}", - data=all_outputs[var]) - else: - try: - name = f'EDY/{var}/New_adaptive_{key_str}' - oldEDY = np.array(file[name]) - del file[f'EDY/{var}/New_adaptive_{key_str}'] - data = np.vstack((oldEDY, all_outputs[var])) - except KeyError: - data = all_outputs[var] - grpY.create_dataset(f'New_adaptive_{key_str}', data=data) - - # ---------- Save CollocationPoints ---------- - new_c_points = np.delete(c_points, NaN_idx, axis=0) - grpX = file.create_group("EDX") if not hdf5_exist else file.get("EDX") - if prevRun_No == 0 and key_str == '': - grpX.create_dataset("init_"+key_str, data=c_points) - if len(NaN_idx) != 0: - grpX.create_dataset("New_init_"+key_str, data=new_c_points) - - else: - try: - name = f'EDX/adaptive_{key_str}' - oldCollocationPoints = np.array(file[name]) - del file[f'EDX/adaptive_{key_str}'] - data = np.vstack((oldCollocationPoints, new_c_points)) - except KeyError: - data = new_c_points - grpX.create_dataset('adaptive_'+key_str, data=data) - - if len(NaN_idx) != 0: - try: - name = f'EDX/New_adaptive_{key_str}' - oldCollocationPoints = np.array(file[name]) - del file[f'EDX/New_adaptive_{key_str}'] - data = np.vstack((oldCollocationPoints, new_c_points)) - except KeyError: - data = new_c_points - grpX.create_dataset('New_adaptive_'+key_str, data=data) - - # Close h5py file - file.close() - - # ------------------------------------------------------------------------- - def zip_subdirs(self, dir_name, key): - """ - Zips all the files containing the key(word). - - Parameters - ---------- - dir_name : str - Directory name. - key : str - Keyword to search for. - - Returns - ------- - None. - - """ - # setup file paths variable - dir_list = [] - file_paths = [] - - # Read all directory, subdirectories and file lists - dir_path = os.getcwd() - - for root, directories, files in os.walk(dir_path): - for directory in directories: - # Create the full filepath by using os module. - if key in directory: - folderPath = os.path.join(dir_path, directory) - dir_list.append(folderPath) - - # Loop over the identified directories to store the file paths - for direct_name in dir_list: - for root, directories, files in os.walk(direct_name): - for filename in files: - # Create the full filepath by using os module. - filePath = os.path.join(root, filename) - file_paths.append('.'+filePath.split(dir_path)[1]) - - # writing files to a zipfile - if len(file_paths) != 0: - zip_file = zipfile.ZipFile(dir_name+'.zip', 'w') - with zip_file: - # writing each file one by one - for file in file_paths: - zip_file.write(file) - - file_paths = [path for path in os.listdir('.') if key in path] - - for path in file_paths: - shutil.rmtree(path) - - print("\n") - print(f'{dir_name}.zip file has been created successfully!\n') - - return diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__init__.py b/examples/only-model/bayesvalidrox/surrogate_models/__init__.py deleted file mode 100644 index 70bfb20f570464c2907a0a4128f4ed99b6c13736..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from .surrogate_models import MetaModel - -__all__ = [ - "MetaModel" - ] diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 61571bd1e2bf0c8b5ec4faf4c27e71b0526bc80a..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc deleted file mode 100644 index 3820b6ebf286b68cd5bedabe083f98a933a04823..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc deleted file mode 100644 index 2111e7b45606ee6d23ec37bdc6af982cf6812b80..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc deleted file mode 100644 index fa3d7416568a694823e30bb7eaea6b06ebab9d3d..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc deleted file mode 100644 index af55ca5d94d223a3244bc9ce745720acc2da05dd..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc deleted file mode 100644 index 092cdd70d05ca0bbad7579aae82369478a7b8ae1..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc deleted file mode 100644 index a8a22b9f5698e86415d782ab9609abda6c9b4508..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc deleted file mode 100644 index 5f8c85996a9c8d48326d736211a004c9f09844fd..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc deleted file mode 100644 index ad054a2ed1f2ab163a883333d0bcc716526bc051..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc deleted file mode 100644 index def10cfc2a4359d25ec4f385e263aff0d27884ed..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc deleted file mode 100644 index 87e325f39519c87f046e35417898743d42e70b28..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc deleted file mode 100644 index 9ea9fec9d079257a40ae2d8117854c9516d47a2c..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc b/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc deleted file mode 100644 index 62a8185dbf2cc3a241d95a03e74c778ad96ec8c5..0000000000000000000000000000000000000000 Binary files a/examples/only-model/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc and /dev/null differ diff --git a/examples/only-model/bayesvalidrox/surrogate_models/adaptPlot.py b/examples/only-model/bayesvalidrox/surrogate_models/adaptPlot.py deleted file mode 100644 index 102f0373c1086ba4420ada2fb2fc723b78bbd53f..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/adaptPlot.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 13 13:46:24 2020 - -@author: farid -""" -import os -from sklearn.metrics import mean_squared_error, r2_score -from itertools import cycle -from matplotlib.backends.backend_pdf import PdfPages -import matplotlib.pyplot as plt - - -def adaptPlot(PCEModel, Y_Val, Y_PC_Val, Y_PC_Val_std, x_values=[], - plotED=False, SaveFig=True): - - NrofSamples = PCEModel.ExpDesign.n_new_samples - initNSamples = PCEModel.ExpDesign.n_init_samples - itrNr = 1 + (PCEModel.ExpDesign.X.shape[0] - initNSamples)//NrofSamples - - oldEDY = PCEModel.ExpDesign.Y - - if SaveFig: - newpath = 'adaptivePlots' - os.makedirs(newpath, exist_ok=True) - - # create a PdfPages object - pdf = PdfPages(f'./{newpath}/Model_vs_PCEModel_itr_{itrNr}.pdf') - - # List of markers and colors - color = cycle((['b', 'g', 'r', 'y', 'k'])) - marker = cycle(('x', 'd', '+', 'o', '*')) - - OutNames = list(Y_Val.keys()) - x_axis = 'Time [s]' - - if len(OutNames) == 1: - OutNames.insert(0, x_axis) - try: - x_values = Y_Val['x_values'] - except KeyError: - x_values = x_values - - fig = plt.figure(figsize=(24, 16)) - - # Plot the model vs PCE model - for keyIdx, key in enumerate(PCEModel.ModelObj.Output.names): - Y_PC_Val_ = Y_PC_Val[key] - Y_PC_Val_std_ = Y_PC_Val_std[key] - Y_Val_ = Y_Val[key] - if Y_Val_.ndim == 1: - Y_Val_ = Y_Val_.reshape(1, -1) - old_EDY = oldEDY[key] - if isinstance(x_values, dict): - x = x_values[key] - else: - x = x_values - - for idx, y in enumerate(Y_Val_): - Color = next(color) - Marker = next(marker) - - plt.plot( - x, y, color=Color, marker=Marker, - lw=2.0, label='$Y_{%s}^{M}$'%(idx+itrNr) - ) - - plt.plot( - x, Y_PC_Val_[idx], color=Color, marker=Marker, - lw=2.0, linestyle='--', label='$Y_{%s}^{PCE}$'%(idx+itrNr) - ) - plt.fill_between( - x, Y_PC_Val_[idx]-1.96*Y_PC_Val_std_[idx], - Y_PC_Val_[idx]+1.96*Y_PC_Val_std_[idx], color=Color, - alpha=0.15 - ) - - if plotED: - for output in old_EDY: - plt.plot(x, output, color='grey', alpha=0.1) - - # Calculate the RMSE - RMSE = mean_squared_error(Y_PC_Val_, Y_Val_, squared=False) - R2 = r2_score(Y_PC_Val_.reshape(-1, 1), Y_Val_.reshape(-1, 1)) - - plt.ylabel(key) - plt.xlabel(x_axis) - plt.title(key) - - ax = fig.axes[0] - ax.legend(loc='best', frameon=True) - fig.canvas.draw() - ax.text(0.65, 0.85, - f'RMSE = {round(RMSE, 3)}\n$R^2$ = {round(R2, 3)}', - transform=ax.transAxes, color='black', - bbox=dict(facecolor='none', - edgecolor='black', - boxstyle='round,pad=1') - ) - plt.grid() - - if SaveFig: - # save the current figure - pdf.savefig(fig, bbox_inches='tight') - - # Destroy the current plot - plt.clf() - pdf.close() diff --git a/examples/only-model/bayesvalidrox/surrogate_models/apoly_construction.py b/examples/only-model/bayesvalidrox/surrogate_models/apoly_construction.py deleted file mode 100644 index a7914c7deac51c2180aa6858207ccf0bac5c1f02..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/apoly_construction.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import numpy as np - - -def apoly_construction(Data, degree): - """ - Construction of Data-driven Orthonormal Polynomial Basis - Author: Dr.-Ing. habil. Sergey Oladyshkin - Department of Stochastic Simulation and Safety Research for Hydrosystems - Institute for Modelling Hydraulic and Environmental Systems - Universitaet Stuttgart, Pfaffenwaldring 5a, 70569 Stuttgart - E-mail: Sergey.Oladyshkin@iws.uni-stuttgart.de - http://www.iws-ls3.uni-stuttgart.de - The current script is based on definition of arbitrary polynomial chaos - expansion (aPC), which is presented in the following manuscript: - Oladyshkin, S. and W. Nowak. Data-driven uncertainty quantification using - the arbitrary polynomial chaos expansion. Reliability Engineering & System - Safety, Elsevier, V. 106, P. 179-190, 2012. - DOI: 10.1016/j.ress.2012.05.002. - - Parameters - ---------- - Data : array - Raw data. - degree : int - Maximum polynomial degree. - - Returns - ------- - Polynomial : array - The coefficients of the univariate orthonormal polynomials. - - """ - - # Initialization - dd = degree + 1 - nsamples = len(Data) - - # Forward linear transformation (Avoiding numerical issues) - MeanOfData = np.mean(Data) - Data = Data/MeanOfData - - # Compute raw moments of input data - raw_moments = [np.sum(np.power(Data, p))/nsamples for p in range(2*dd+2)] - - # Main Loop for Polynomial with degree up to dd - PolyCoeff_NonNorm = np.empty((0, 1)) - Polynomial = np.zeros((dd+1, dd+1)) - - for degree in range(dd+1): - Mm = np.zeros((degree+1, degree+1)) - Vc = np.zeros((degree+1)) - - # Define Moments Matrix Mm - for i in range(degree+1): - for j in range(degree+1): - if (i < degree): - Mm[i, j] = raw_moments[i+j] - - elif (i == degree) and (j == degree): - Mm[i, j] = 1 - - # Numerical Optimization for Matrix Solver - Mm[i] = Mm[i] / max(abs(Mm[i])) - - # Defenition of Right Hand side ortogonality conditions: Vc - for i in range(degree+1): - Vc[i] = 1 if i == degree else 0 - - # Solution: Coefficients of Non-Normal Orthogonal Polynomial: Vp Eq.(4) - try: - Vp = np.linalg.solve(Mm, Vc) - except: - inv_Mm = np.linalg.pinv(Mm) - Vp = np.dot(inv_Mm, Vc.T) - - if degree == 0: - PolyCoeff_NonNorm = np.append(PolyCoeff_NonNorm, Vp) - - if degree != 0: - if degree == 1: - zero = [0] - else: - zero = np.zeros((degree, 1)) - PolyCoeff_NonNorm = np.hstack((PolyCoeff_NonNorm, zero)) - - PolyCoeff_NonNorm = np.vstack((PolyCoeff_NonNorm, Vp)) - - if 100*abs(sum(abs(np.dot(Mm, Vp)) - abs(Vc))) > 0.5: - print('\n---> Attention: Computational Error too high !') - print('\n---> Problem: Convergence of Linear Solver') - - # Original Numerical Normalization of Coefficients with Norm and - # orthonormal Basis computation Matrix Storrage - # Note: Polynomial(i,j) correspont to coefficient number "j-1" - # of polynomial degree "i-1" - P_norm = 0 - for i in range(nsamples): - Poly = 0 - for k in range(degree+1): - if degree == 0: - Poly += PolyCoeff_NonNorm[k] * (Data[i]**k) - else: - Poly += PolyCoeff_NonNorm[degree, k] * (Data[i]**k) - - P_norm += Poly**2 / nsamples - - P_norm = np.sqrt(P_norm) - - for k in range(degree+1): - if degree == 0: - Polynomial[degree, k] = PolyCoeff_NonNorm[k]/P_norm - else: - Polynomial[degree, k] = PolyCoeff_NonNorm[degree, k]/P_norm - - # Backward linear transformation to the real data space - Data *= MeanOfData - for k in range(len(Polynomial)): - Polynomial[:, k] = Polynomial[:, k] / (MeanOfData**(k)) - - return Polynomial diff --git a/examples/only-model/bayesvalidrox/surrogate_models/bayes_linear.py b/examples/only-model/bayesvalidrox/surrogate_models/bayes_linear.py deleted file mode 100644 index a7d6b5929a83fc89d15d7ab8f369187d0542923c..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/bayes_linear.py +++ /dev/null @@ -1,523 +0,0 @@ -import numpy as np -from sklearn.base import RegressorMixin -from sklearn.linear_model._base import LinearModel -from sklearn.utils import check_X_y, check_array, as_float_array -from sklearn.utils.validation import check_is_fitted -from scipy.linalg import svd -import warnings -from sklearn.preprocessing import normalize as f_normalize - - - -class BayesianLinearRegression(RegressorMixin,LinearModel): - ''' - Superclass for Empirical Bayes and Variational Bayes implementations of - Bayesian Linear Regression Model - ''' - def __init__(self, n_iter, tol, fit_intercept,copy_X, verbose): - self.n_iter = n_iter - self.fit_intercept = fit_intercept - self.copy_X = copy_X - self.verbose = verbose - self.tol = tol - - - def _check_convergence(self, mu, mu_old): - ''' - Checks convergence of algorithm using changes in mean of posterior - distribution of weights - ''' - return np.sum(abs(mu-mu_old)>self.tol) == 0 - - - def _center_data(self,X,y): - ''' Centers data''' - X = as_float_array(X,self.copy_X) - # normalisation should be done in preprocessing! - X_std = np.ones(X.shape[1], dtype = X.dtype) - if self.fit_intercept: - X_mean = np.average(X,axis = 0) - y_mean = np.average(y,axis = 0) - X -= X_mean - y = y - y_mean - else: - X_mean = np.zeros(X.shape[1],dtype = X.dtype) - y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype) - return X,y, X_mean, y_mean, X_std - - - def predict_dist(self,X): - ''' - Calculates mean and variance of predictive distribution for each data - point of test set.(Note predictive distribution for each data point is - Gaussian, therefore it is uniquely determined by mean and variance) - - Parameters - ---------- - x: array-like of size (n_test_samples, n_features) - Set of features for which corresponding responses should be predicted - - Returns - ------- - :list of two numpy arrays [mu_pred, var_pred] - - mu_pred : numpy array of size (n_test_samples,) - Mean of predictive distribution - - var_pred: numpy array of size (n_test_samples,) - Variance of predictive distribution - ''' - # Note check_array and check_is_fitted are done within self._decision_function(X) - mu_pred = self._decision_function(X) - data_noise = 1./self.beta_ - model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1) - var_pred = data_noise + model_noise - return [mu_pred,var_pred] - - - - -class EBLinearRegression(BayesianLinearRegression): - ''' - Bayesian Regression with type II maximum likelihood (Empirical Bayes) - - Parameters: - ----------- - n_iter: int, optional (DEFAULT = 300) - Maximum number of iterations - - tol: float, optional (DEFAULT = 1e-3) - Threshold for convergence - - optimizer: str, optional (DEFAULT = 'fp') - Method for optimization , either Expectation Maximization or - Fixed Point Gull-MacKay {'em','fp'}. Fixed point iterations are - faster, but can be numerically unstable (especially in case of near perfect fit). - - fit_intercept: bool, optional (DEFAULT = True) - If True includes bias term in model - - perfect_fit_tol: float (DEAFAULT = 1e-5) - Prevents overflow of precision parameters (this is smallest value RSS can have). - ( !!! Note if using EM instead of fixed-point, try smaller values - of perfect_fit_tol, for better estimates of variance of predictive distribution ) - - alpha: float (DEFAULT = 1) - Initial value of precision paramter for coefficients ( by default we define - very broad distribution ) - - copy_X : boolean, optional (DEFAULT = True) - If True, X will be copied, otherwise will be - - verbose: bool, optional (Default = False) - If True at each iteration progress report is printed out - - Attributes - ---------- - coef_ : array, shape = (n_features) - Coefficients of the regression model (mean of posterior distribution) - - intercept_: float - Value of bias term (if fit_intercept is False, then intercept_ = 0) - - alpha_ : float - Estimated precision of coefficients - - beta_ : float - Estimated precision of noise - - eigvals_ : array, shape = (n_features, ) - Eigenvalues of covariance matrix (from posterior distribution of weights) - - eigvecs_ : array, shape = (n_features, n_featues) - Eigenvectors of covariance matrix (from posterior distribution of weights) - - ''' - - def __init__(self,n_iter = 300, tol = 1e-3, optimizer = 'fp', fit_intercept = True, - normalize=True, perfect_fit_tol = 1e-6, alpha = 1, copy_X = True, verbose = False): - super(EBLinearRegression,self).__init__(n_iter, tol, fit_intercept, copy_X, verbose) - if optimizer not in ['em','fp']: - raise ValueError('Optimizer can be either "em" of "fp" ') - self.optimizer = optimizer - self.alpha = alpha - self.perfect_fit = False - self.normalize = True - self.scores_ = [np.NINF] - self.perfect_fit_tol = perfect_fit_tol - - def _check_convergence(self, mu, mu_old): - ''' - Checks convergence of algorithm using changes in mean of posterior - distribution of weights - ''' - return np.sum(abs(mu-mu_old)>self.tol) == 0 - - - def _center_data(self,X,y): - ''' Centers data''' - X = as_float_array(X,self.copy_X) - # normalisation should be done in preprocessing! - X_std = np.ones(X.shape[1], dtype = X.dtype) - if self.fit_intercept: - X_mean = np.average(X, axis=0) - X -= X_mean - if self.normalize: - X, X_std = f_normalize(X, axis=0, copy=False, - return_norm=True) - else: - X_std = np.ones(X.shape[1], dtype=X.dtype) - y_mean = np.average(y, axis=0) - y = y - y_mean - else: - X_mean = np.zeros(X.shape[1],dtype = X.dtype) - y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype) - return X,y, X_mean, y_mean, X_std - - def fit(self, X, y): - ''' - Fits Bayesian Linear Regression using Empirical Bayes - - Parameters - ---------- - X: array-like of size [n_samples,n_features] - Matrix of explanatory variables (should not include bias term) - - y: array-like of size [n_features] - Vector of dependent variables. - - Returns - ------- - object: self - self - - ''' - # preprocess data - X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True) - n_samples, n_features = X.shape - X, y, X_mean, y_mean, X_std = self._center_data(X, y) - self._x_mean_ = X_mean - self._y_mean = y_mean - self._x_std = X_std - - # precision of noise & and coefficients - alpha = self.alpha - var_y = np.var(y) - # check that variance is non zero !!! - if var_y == 0 : - beta = 1e-2 - else: - beta = 1. / np.var(y) - - # to speed all further computations save svd decomposition and reuse it later - u,d,vt = svd(X, full_matrices = False) - Uy = np.dot(u.T,y) - dsq = d**2 - mu = 0 - - for i in range(self.n_iter): - - # find mean for posterior of w ( for EM this is E-step) - mu_old = mu - if n_samples > n_features: - mu = vt.T * d/(dsq+alpha/beta) - else: - # clever use of SVD here , faster for large n_features - mu = u * 1./(dsq + alpha/beta) - mu = np.dot(X.T,mu) - mu = np.dot(mu,Uy) - - # precompute errors, since both methods use it in estimation - error = y - np.dot(X,mu) - sqdErr = np.sum(error**2) - - if sqdErr / n_samples < self.perfect_fit_tol: - self.perfect_fit = True - warnings.warn( ('Almost perfect fit!!! Estimated values of variance ' - 'for predictive distribution are computed using only RSS')) - break - - if self.optimizer == "fp": - gamma = np.sum(beta*dsq/(beta*dsq + alpha)) - # use updated mu and gamma parameters to update alpha and beta - # !!! made computation numerically stable for perfect fit case - alpha = gamma / (np.sum(mu**2) + np.finfo(np.float32).eps ) - beta = ( n_samples - gamma ) / (sqdErr + np.finfo(np.float32).eps ) - else: - # M-step, update parameters alpha and beta to maximize ML TYPE II - eigvals = 1. / (beta * dsq + alpha) - alpha = n_features / ( np.sum(mu**2) + np.sum(1/eigvals) ) - beta = n_samples / ( sqdErr + np.sum(dsq/eigvals) ) - - # if converged or exceeded maximum number of iterations => terminate - converged = self._check_convergence(mu_old,mu) - if self.verbose: - print( "Iteration {0} completed".format(i) ) - if converged is True: - print("Algorithm converged after {0} iterations".format(i)) - if converged or i==self.n_iter -1: - break - eigvals = 1./(beta * dsq + alpha) - self.coef_ = beta*np.dot(vt.T*d*eigvals ,Uy) - self._set_intercept(X_mean,y_mean,X_std) - self.beta_ = beta - self.alpha_ = alpha - self.eigvals_ = eigvals - self.eigvecs_ = vt.T - - # set intercept_ - if self.fit_intercept: - self.coef_ = self.coef_ / X_std - self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T) - else: - self.intercept_ = 0. - - return self - - def predict(self,X, return_std=False): - ''' - Computes predictive distribution for test set. - Predictive distribution for each data point is one dimensional - Gaussian and therefore is characterised by mean and variance. - - Parameters - ----------- - X: {array-like, sparse} (n_samples_test, n_features) - Test data, matrix of explanatory variables - - Returns - ------- - : list of length two [y_hat, var_hat] - - y_hat: numpy array of size (n_samples_test,) - Estimated values of targets on test set (i.e. mean of predictive - distribution) - - var_hat: numpy array of size (n_samples_test,) - Variance of predictive distribution - ''' - y_hat = np.dot(X,self.coef_) + self.intercept_ - - if return_std: - if self.normalize: - X = (X - self._x_mean_) / self._x_std - data_noise = 1./self.beta_ - model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1) - var_pred = data_noise + model_noise - std_hat = np.sqrt(var_pred) - return y_hat, std_hat - else: - return y_hat - - -# ============================== VBLR ========================================= - -def gamma_mean(a,b): - ''' - Computes mean of gamma distribution - - Parameters - ---------- - a: float - Shape parameter of Gamma distribution - - b: float - Rate parameter of Gamma distribution - - Returns - ------- - : float - Mean of Gamma distribution - ''' - return float(a) / b - - - -class VBLinearRegression(BayesianLinearRegression): - ''' - Implements Bayesian Linear Regression using mean-field approximation. - Assumes gamma prior on precision parameters of coefficients and noise. - - Parameters: - ----------- - n_iter: int, optional (DEFAULT = 100) - Maximum number of iterations for KL minimization - - tol: float, optional (DEFAULT = 1e-3) - Convergence threshold - - fit_intercept: bool, optional (DEFAULT = True) - If True will use bias term in model fitting - - a: float, optional (Default = 1e-4) - Shape parameter of Gamma prior for precision of coefficients - - b: float, optional (Default = 1e-4) - Rate parameter of Gamma prior for precision coefficients - - c: float, optional (Default = 1e-4) - Shape parameter of Gamma prior for precision of noise - - d: float, optional (Default = 1e-4) - Rate parameter of Gamma prior for precision of noise - - verbose: bool, optional (Default = False) - If True at each iteration progress report is printed out - - Attributes - ---------- - coef_ : array, shape = (n_features) - Coefficients of the regression model (mean of posterior distribution) - - intercept_: float - Value of bias term (if fit_intercept is False, then intercept_ = 0) - - alpha_ : float - Mean of precision of coefficients - - beta_ : float - Mean of precision of noise - - eigvals_ : array, shape = (n_features, ) - Eigenvalues of covariance matrix (from posterior distribution of weights) - - eigvecs_ : array, shape = (n_features, n_featues) - Eigenvectors of covariance matrix (from posterior distribution of weights) - - ''' - - def __init__(self, n_iter = 100, tol =1e-4, fit_intercept = True, - a = 1e-4, b = 1e-4, c = 1e-4, d = 1e-4, copy_X = True, - verbose = False): - super(VBLinearRegression,self).__init__(n_iter, tol, fit_intercept, copy_X, - verbose) - self.a,self.b = a, b - self.c,self.d = c, d - - - def fit(self,X,y): - ''' - Fits Variational Bayesian Linear Regression Model - - Parameters - ---------- - X: array-like of size [n_samples,n_features] - Matrix of explanatory variables (should not include bias term) - - Y: array-like of size [n_features] - Vector of dependent variables. - - Returns - ------- - object: self - self - ''' - # preprocess data - X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True) - n_samples, n_features = X.shape - X, y, X_mean, y_mean, X_std = self._center_data(X, y) - self._x_mean_ = X_mean - self._y_mean = y_mean - self._x_std = X_std - - # SVD decomposition, done once , reused at each iteration - u,D,vt = svd(X, full_matrices = False) - dsq = D**2 - UY = np.dot(u.T,y) - - # some parameters of Gamma distribution have closed form solution - a = self.a + 0.5 * n_features - c = self.c + 0.5 * n_samples - b,d = self.b, self.d - - # initial mean of posterior for coefficients - mu = 0 - - for i in range(self.n_iter): - - # update parameters of distribution Q(weights) - e_beta = gamma_mean(c,d) - e_alpha = gamma_mean(a,b) - mu_old = np.copy(mu) - mu,eigvals = self._posterior_weights(e_beta,e_alpha,UY,dsq,u,vt,D,X) - - # update parameters of distribution Q(precision of weights) - b = self.b + 0.5*( np.sum(mu**2) + np.sum(eigvals)) - - # update parameters of distribution Q(precision of likelihood) - sqderr = np.sum((y - np.dot(X,mu))**2) - xsx = np.sum(dsq*eigvals) - d = self.d + 0.5*(sqderr + xsx) - - # check convergence - converged = self._check_convergence(mu,mu_old) - if self.verbose is True: - print("Iteration {0} is completed".format(i)) - if converged is True: - print("Algorithm converged after {0} iterations".format(i)) - - # terminate if convergence or maximum number of iterations are achieved - if converged or i==(self.n_iter-1): - break - - # save necessary parameters - self.beta_ = gamma_mean(c,d) - self.alpha_ = gamma_mean(a,b) - self.coef_, self.eigvals_ = self._posterior_weights(self.beta_, self.alpha_, UY, - dsq, u, vt, D, X) - self._set_intercept(X_mean,y_mean,X_std) - self.eigvecs_ = vt.T - return self - - - def _posterior_weights(self, e_beta, e_alpha, UY, dsq, u, vt, d, X): - ''' - Calculates parameters of approximate posterior distribution - of weights - ''' - # eigenvalues of covariance matrix - sigma = 1./ (e_beta*dsq + e_alpha) - - # mean of approximate posterior distribution - n_samples, n_features = X.shape - if n_samples > n_features: - mu = vt.T * d/(dsq + e_alpha/e_beta)# + np.finfo(np.float64).eps) - else: - mu = u * 1./(dsq + e_alpha/e_beta)# + np.finfo(np.float64).eps) - mu = np.dot(X.T,mu) - mu = np.dot(mu,UY) - return mu,sigma - - def predict(self,X, return_std=False): - ''' - Computes predictive distribution for test set. - Predictive distribution for each data point is one dimensional - Gaussian and therefore is characterised by mean and variance. - - Parameters - ----------- - X: {array-like, sparse} (n_samples_test, n_features) - Test data, matrix of explanatory variables - - Returns - ------- - : list of length two [y_hat, var_hat] - - y_hat: numpy array of size (n_samples_test,) - Estimated values of targets on test set (i.e. mean of predictive - distribution) - - var_hat: numpy array of size (n_samples_test,) - Variance of predictive distribution - ''' - x = (X - self._x_mean_) / self._x_std - y_hat = np.dot(x,self.coef_) + self._y_mean - - if return_std: - data_noise = 1./self.beta_ - model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1) - var_pred = data_noise + model_noise - std_hat = np.sqrt(var_pred) - return y_hat, std_hat - else: - return y_hat \ No newline at end of file diff --git a/examples/only-model/bayesvalidrox/surrogate_models/desktop.ini b/examples/only-model/bayesvalidrox/surrogate_models/desktop.ini deleted file mode 100644 index 632de13ae6b61cecf0d9fdbf9c97cfb16bfb51a4..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/desktop.ini +++ /dev/null @@ -1,2 +0,0 @@ -[LocalizedFileNames] -exploration.py=@exploration.py,0 diff --git a/examples/only-model/bayesvalidrox/surrogate_models/eval_rec_rule.py b/examples/only-model/bayesvalidrox/surrogate_models/eval_rec_rule.py deleted file mode 100644 index b583c7eb2ec58d55d19b34130812730d21a12368..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/eval_rec_rule.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" - - -Based on the implementation in UQLab [1]. - -References: -1. S. Marelli, and B. Sudret, UQLab: A framework for uncertainty quantification -in Matlab, Proc. 2nd Int. Conf. on Vulnerability, Risk Analysis and Management -(ICVRAM2014), Liverpool, United Kingdom, 2014, 2554-2563. - -2. S. Marelli, N. Lüthen, B. Sudret, UQLab user manual – Polynomial chaos -expansions, Report # UQLab-V1.4-104, Chair of Risk, Safety and Uncertainty -Quantification, ETH Zurich, Switzerland, 2021. - -Author: Farid Mohammadi, M.Sc. -E-Mail: farid.mohammadi@iws.uni-stuttgart.de -Department of Hydromechanics and Modelling of Hydrosystems (LH2) -Institute for Modelling Hydraulic and Environmental Systems (IWS), University -of Stuttgart, www.iws.uni-stuttgart.de/lh2/ -Pfaffenwaldring 61 -70569 Stuttgart - -Created on Fri Jan 14 2022 -""" -import numpy as np -from numpy.polynomial.polynomial import polyval - - -def poly_rec_coeffs(n_max, poly_type, params=None): - """ - Computes the recurrence coefficients for classical Wiener-Askey orthogonal - polynomials. - - Parameters - ---------- - n_max : int - Maximum polynomial degree. - poly_type : string - Polynomial type. - params : list, optional - Parameters required for `laguerre` poly type. The default is None. - - Returns - ------- - AB : dict - The 3 term recursive coefficients and the applicable ranges. - - """ - - if poly_type == 'legendre': - - def an(n): - return np.zeros((n+1, 1)) - - def sqrt_bn(n): - sq_bn = np.zeros((n+1, 1)) - sq_bn[0, 0] = 1 - for i in range(1, n+1): - sq_bn[i, 0] = np.sqrt(1./(4-i**-2)) - return sq_bn - - bounds = [-1, 1] - - elif poly_type == 'hermite': - - def an(n): - return np.zeros((n+1, 1)) - - def sqrt_bn(n): - sq_bn = np.zeros((n+1, 1)) - sq_bn[0, 0] = 1 - for i in range(1, n+1): - sq_bn[i, 0] = np.sqrt(i) - return sq_bn - - bounds = [-np.inf, np.inf] - - elif poly_type == 'laguerre': - - def an(n): - a = np.zeros((n+1, 1)) - for i in range(1, n+1): - a[i] = 2*n + params[1] - return a - - def sqrt_bn(n): - sq_bn = np.zeros((n+1, 1)) - sq_bn[0, 0] = 1 - for i in range(1, n+1): - sq_bn[i, 0] = -np.sqrt(i * (i+params[1]-1)) - return sq_bn - - bounds = [0, np.inf] - - AB = {'alpha_beta': np.concatenate((an(n_max), sqrt_bn(n_max)), axis=1), - 'bounds': bounds} - - return AB - - -def eval_rec_rule(x, max_deg, poly_type): - """ - Evaluates the polynomial that corresponds to the Jacobi matrix defined - from the AB. - - Parameters - ---------- - x : array (n_samples) - Points where the polynomials are evaluated. - max_deg : int - Maximum degree. - poly_type : string - Polynomial type. - - Returns - ------- - values : array of shape (n_samples, max_deg+1) - Polynomials corresponding to the Jacobi matrix. - - """ - AB = poly_rec_coeffs(max_deg, poly_type) - AB = AB['alpha_beta'] - - values = np.zeros((len(x), AB.shape[0]+1)) - values[:, 1] = 1 / AB[0, 1] - - for k in range(AB.shape[0]-1): - values[:, k+2] = np.multiply((x - AB[k, 0]), values[:, k+1]) - \ - np.multiply(values[:, k], AB[k, 1]) - values[:, k+2] = np.divide(values[:, k+2], AB[k+1, 1]) - return values[:, 1:] - - -def eval_rec_rule_arbitrary(x, max_deg, poly_coeffs): - """ - Evaluates the polynomial at sample array x. - - Parameters - ---------- - x : array (n_samples) - Points where the polynomials are evaluated. - max_deg : int - Maximum degree. - poly_coeffs : dict - Polynomial coefficients computed based on moments. - - Returns - ------- - values : array of shape (n_samples, max_deg+1) - Univariate Polynomials evaluated at samples. - - """ - values = np.zeros((len(x), max_deg+1)) - - for deg in range(max_deg+1): - values[:, deg] = polyval(x, poly_coeffs[deg]).T - - return values - - -def eval_univ_basis(x, max_deg, poly_types, apoly_coeffs=None): - """ - Evaluates univariate regressors along input directions. - - Parameters - ---------- - x : array of shape (n_samples, n_params) - Training samples. - max_deg : int - Maximum polynomial degree. - poly_types : list of strings - List of polynomial types for all parameters. - apoly_coeffs : dict , optional - Polynomial coefficients computed based on moments. The default is None. - - Returns - ------- - univ_vals : array of shape (n_samples, n_params, max_deg+1) - Univariate polynomials for all degrees and parameters evaluated at x. - - """ - # Initilize the output array - n_samples, n_params = x.shape - univ_vals = np.zeros((n_samples, n_params, max_deg+1)) - - for i in range(n_params): - - if poly_types[i] == 'arbitrary': - polycoeffs = apoly_coeffs[f'p_{i+1}'] - univ_vals[:, i] = eval_rec_rule_arbitrary(x[:, i], max_deg, - polycoeffs) - else: - univ_vals[:, i] = eval_rec_rule(x[:, i], max_deg, poly_types[i]) - - return univ_vals diff --git a/examples/only-model/bayesvalidrox/surrogate_models/exp_designs.py b/examples/only-model/bayesvalidrox/surrogate_models/exp_designs.py deleted file mode 100644 index a078aec9c19c5a85a637ba50d02c48459ceea6d3..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/exp_designs.py +++ /dev/null @@ -1,737 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import numpy as np -import math -import itertools -import chaospy -import scipy.stats as st -from tqdm import tqdm - -from .apoly_construction import apoly_construction - - -class ExpDesigns: - """ - This class generates samples from the prescribed marginals for the model - parameters using the `Input` object. - - Attributes - ---------- - Input : obj - Input object containing the parameter marginals, i.e. name, - distribution type and distribution parameters or available raw data. - method : str - Type of the experimental design. The default is `'normal'`. Other - option is `'sequential'`. - meta_Model : str - Type of the meta_model. - sampling_method : str - Name of the sampling method for the experimental design. The following - sampling method are supported: - - * random - * latin_hypercube - * sobol - * halton - * hammersley - * chebyshev(FT) - * grid(FT) - * user - hdf5_file : str - Name of the hdf5 file that contains the experimental design. - n_new_samples : int - Number of (initial) training points. - n_max_samples : int - Number of maximum training points. - mod_LOO_threshold : float - The modified leave-one-out cross validation threshold where the - sequential design stops. - tradeoff_scheme : str - Trade-off scheme to assign weights to the exploration and exploitation - scores in the sequential design. - n_canddidate : int - Number of candidate training sets to calculate the scores for. - explore_method : str - Type of the exploration method for the sequential design. The following - methods are supported: - - * Voronoi - * random - * latin_hypercube - * LOOCV - * dual annealing - exploit_method : str - Type of the exploitation method for the sequential design. The - following methods are supported: - - * BayesOptDesign - * BayesActDesign - * VarOptDesign - * alphabetic - * Space-filling - util_func : str or list - The utility function to be specified for the `exploit_method`. For the - available utility functions see Note section. - n_cand_groups : int - Number of candidate groups. Each group of candidate training sets will - be evaulated separately in parallel. - n_replication : int - Number of replications. Only for comparison. The default is 1. - post_snapshot : int - Whether to plot the posterior in the sequential design. The default is - `True`. - step_snapshot : int - The number of steps to plot the posterior in the sequential design. The - default is 1. - max_a_post : list or array - Maximum a posteriori of the posterior distribution, if known. The - default is `[]`. - adapt_verbose : bool - Whether to plot the model response vs that of metamodel for the new - trining point in the sequential design. - - Note - ---------- - The following utiliy functions for the **exploitation** methods are - supported: - - #### BayesOptDesign (when data is available) - - DKL (Kullback-Leibler Divergence) - - DPP (D-Posterior-percision) - - APP (A-Posterior-percision) - - #### VarBasedOptDesign -> when data is not available - - Entropy (Entropy/MMSE/active learning) - - EIGF (Expected Improvement for Global fit) - - LOOCV (Leave-one-out Cross Validation) - - #### alphabetic - - D-Opt (D-Optimality) - - A-Opt (A-Optimality) - - K-Opt (K-Optimality) - """ - - def __init__(self, Input, method='normal', meta_Model='pce', - sampling_method='random', hdf5_file=None, - n_new_samples=1, n_max_samples=None, mod_LOO_threshold=1e-16, - tradeoff_scheme=None, n_canddidate=1, explore_method='random', - exploit_method='Space-filling', util_func='Space-filling', - n_cand_groups=4, n_replication=1, post_snapshot=False, - step_snapshot=1, max_a_post=[], adapt_verbose=False): - - self.InputObj = Input - self.method = method - self.meta_Model = meta_Model - self.sampling_method = sampling_method - self.hdf5_file = hdf5_file - self.n_new_samples = n_new_samples - self.n_max_samples = n_max_samples - self.mod_LOO_threshold = mod_LOO_threshold - self.explore_method = explore_method - self.exploit_method = exploit_method - self.util_func = util_func - self.tradeoff_scheme = tradeoff_scheme - self.n_canddidate = n_canddidate - self.n_cand_groups = n_cand_groups - self.n_replication = n_replication - self.post_snapshot = post_snapshot - self.step_snapshot = step_snapshot - self.max_a_post = max_a_post - self.adapt_verbose = adapt_verbose - - # ------------------------------------------------------------------------- - def generate_samples(self, n_samples, sampling_method='random', - transform=False): - """ - Generates samples with given sampling method - - Parameters - ---------- - n_samples : int - Number of requested samples. - sampling_method : str, optional - Sampling method. The default is `'random'`. - transform : bool, optional - Transformation via an isoprobabilistic transformation method. The - default is `False`. - - Returns - ------- - samples: array of shape (n_samples, n_params) - Generated samples from defined model input object. - - """ - try: - samples = chaospy.generate_samples( - int(n_samples), domain=self.origJDist, rule=sampling_method - ) - except: - samples = self.random_sampler(int(n_samples)).T - - return samples.T - - # ------------------------------------------------------------------------- - def generate_ED(self, n_samples, sampling_method='random', transform=False, - max_pce_deg=None): - """ - Generates experimental designs (training set) with the given method. - - Parameters - ---------- - n_samples : int - Number of requested training points. - sampling_method : str, optional - Sampling method. The default is `'random'`. - transform : bool, optional - Isoprobabilistic transformation. The default is `False`. - max_pce_deg : int, optional - Maximum PCE polynomial degree. The default is `None`. - - Returns - ------- - samples : array of shape (n_samples, n_params) - Selected training samples. - - """ - Inputs = self.InputObj - self.ndim = len(Inputs.Marginals) - if not hasattr(self, 'n_init_samples'): - self.n_init_samples = self.ndim + 1 - n_samples = int(n_samples) - - # Check if PCE or aPCE metamodel is selected. - if self.meta_Model.lower() == 'apce': - self.apce = True - else: - self.apce = False - - # Check if input is given as dist or input_data. - if len(Inputs.Marginals[0].input_data): - self.input_data_given = True - else: - self.input_data_given = False - - # Get the bounds if input_data are directly defined by user: - if self.input_data_given: - for i in range(self.ndim): - low_bound = np.min(Inputs.Marginals[i].input_data) - up_bound = np.max(Inputs.Marginals[i].input_data) - Inputs.Marginals[i].parameters = [low_bound, up_bound] - - # Generate the samples based on requested method - self.raw_data, self.bound_tuples = self.init_param_space(max_pce_deg) - - # Pass user-defined samples as ED - if sampling_method == 'user': - samples = self.X - self.n_samples = len(samples) - - # Sample the distribution of parameters - elif self.input_data_given: - # Case II: Input values are directly given by the user. - - if sampling_method == 'random': - samples = self.random_sampler(n_samples) - - elif sampling_method == 'PCM' or \ - sampling_method == 'LSCM': - samples = self.pcm_sampler(max_pce_deg) - - else: - # Create ExpDesign in the actual space using chaospy - try: - samples = chaospy.generate_samples(n_samples, - domain=self.JDist, - rule=sampling_method).T - except: - samples = self.JDist.resample(n_samples).T - - elif not self.input_data_given: - # Case I = User passed known distributions - samples = chaospy.generate_samples(n_samples, domain=self.JDist, - rule=sampling_method).T - - # Transform samples to the original space - if transform: - tr_samples = self.transform( - samples, - method=sampling_method - ) - if sampling_method == 'user' or not self.apce: - return samples, tr_samples - else: - return tr_samples, samples - else: - return samples - - # ------------------------------------------------------------------------- - def init_param_space(self, max_deg=None): - """ - Initializes parameter space. - - Parameters - ---------- - max_deg : int, optional - Maximum degree. The default is `None`. - - Returns - ------- - raw_data : array of shape (n_params, n_samples) - Raw data. - bound_tuples : list of tuples - A list containing lower and upper bounds of parameters. - - """ - Inputs = self.InputObj - ndim = self.ndim - rosenblatt_flag = Inputs.Rosenblatt - mc_size = 50000 - - # Save parameter names - self.par_names = [] - for parIdx in range(ndim): - self.par_names.append(Inputs.Marginals[parIdx].name) - - # Create a multivariate probability distribution - if max_deg is not None: - JDist, poly_types = self.build_dist(rosenblatt=rosenblatt_flag) - self.JDist, self.poly_types = JDist, poly_types - - if self.input_data_given: - - self.MCSize = len(Inputs.Marginals[0].input_data) - self.raw_data = np.zeros((ndim, self.MCSize)) - - for parIdx in range(ndim): - # Save parameter names - try: - self.raw_data[parIdx] = np.array( - Inputs.Marginals[parIdx].input_data) - except: - self.raw_data[parIdx] = self.JDist[parIdx].sample(mc_size) - - else: - # Generate random samples based on parameter distributions - self.raw_data = chaospy.generate_samples(mc_size, - domain=self.JDist) - - # Create orthogonal polynomial coefficients if necessary - if self.apce and max_deg is not None and Inputs.poly_coeffs_flag: - self.polycoeffs = {} - for parIdx in tqdm(range(ndim), ascii=True, - desc="Computing orth. polynomial coeffs"): - poly_coeffs = apoly_construction( - self.raw_data[parIdx], - max_deg - ) - self.polycoeffs[f'p_{parIdx+1}'] = poly_coeffs - - # Extract moments - for parIdx in range(ndim): - mu = np.mean(self.raw_data[parIdx]) - std = np.std(self.raw_data[parIdx]) - self.InputObj.Marginals[parIdx].moments = [mu, std] - - # Generate the bounds based on given inputs for marginals - bound_tuples = [] - for i in range(ndim): - if Inputs.Marginals[i].dist_type == 'unif': - low_bound, up_bound = Inputs.Marginals[i].parameters - else: - low_bound = np.min(self.raw_data[i]) - up_bound = np.max(self.raw_data[i]) - - bound_tuples.append((low_bound, up_bound)) - - self.bound_tuples = tuple(bound_tuples) - - return self.raw_data, self.bound_tuples - - # ------------------------------------------------------------------------- - def build_dist(self, rosenblatt): - """ - Creates the polynomial types to be passed to univ_basis_vals method of - the MetaModel object. - - Parameters - ---------- - rosenblatt : bool - Rosenblatt transformation flag. - - Returns - ------- - orig_space_dist : object - A chaospy JDist object or a gaussian_kde object. - poly_types : list - List of polynomial types for the parameters. - - """ - Inputs = self.InputObj - all_data = [] - all_dist_types = [] - orig_joints = [] - poly_types = [] - - for parIdx in range(self.ndim): - - if Inputs.Marginals[parIdx].dist_type is None: - data = Inputs.Marginals[parIdx].input_data - all_data.append(data) - dist_type = None - else: - dist_type = Inputs.Marginals[parIdx].dist_type - params = Inputs.Marginals[parIdx].parameters - - if rosenblatt: - polytype = 'hermite' - dist = chaospy.Normal() - - elif dist_type is None: - polytype = 'arbitrary' - dist = None - - elif 'unif' in dist_type.lower(): - polytype = 'legendre' - dist = chaospy.Uniform(lower=params[0], upper=params[1]) - - elif 'norm' in dist_type.lower() and \ - 'log' not in dist_type.lower(): - polytype = 'hermite' - dist = chaospy.Normal(mu=params[0], sigma=params[1]) - - elif 'gamma' in dist_type.lower(): - polytype = 'laguerre' - dist = chaospy.Gamma(shape=params[0], - scale=params[1], - shift=params[2]) - - elif 'beta' in dist_type.lower(): - polytype = 'jacobi' - dist = chaospy.Beta(alpha=params[0], beta=params[1], - lower=params[2], upper=params[3]) - - elif 'lognorm' in dist_type.lower(): - polytype = 'hermite' - mu = np.log(params[0]**2/np.sqrt(params[0]**2 + params[1]**2)) - sigma = np.sqrt(np.log(1 + params[1]**2 / params[0]**2)) - dist = chaospy.LogNormal(mu, sigma) - # dist = chaospy.LogNormal(mu=params[0], sigma=params[1]) - - elif 'expon' in dist_type.lower(): - polytype = 'arbitrary' - dist = chaospy.Exponential(scale=params[0], shift=params[1]) - - elif 'weibull' in dist_type.lower(): - polytype = 'arbitrary' - dist = chaospy.Weibull(shape=params[0], scale=params[1], - shift=params[2]) - - else: - message = (f"DistType {dist_type} for parameter" - f"{parIdx+1} is not available.") - raise ValueError(message) - - if self.input_data_given or self.apce: - polytype = 'arbitrary' - - # Store dists and poly_types - orig_joints.append(dist) - poly_types.append(polytype) - all_dist_types.append(dist_type) - - # Prepare final output to return - if None in all_dist_types: - # Naive approach: Fit a gaussian kernel to the provided data - Data = np.asarray(all_data) - orig_space_dist = st.gaussian_kde(Data) - self.prior_space = orig_space_dist - else: - orig_space_dist = chaospy.J(*orig_joints) - self.prior_space = st.gaussian_kde(orig_space_dist.sample(10000)) - - return orig_space_dist, poly_types - - # ------------------------------------------------------------------------- - def random_sampler(self, n_samples): - """ - Samples the given raw data randomly. - - Parameters - ---------- - n_samples : int - Number of requested samples. - - Returns - ------- - samples: array of shape (n_samples, n_params) - The sampling locations in the input space. - - """ - samples = np.zeros((n_samples, self.ndim)) - sample_size = self.raw_data.shape[1] - - # Use a combination of raw data - if n_samples < sample_size: - for pa_idx in range(self.ndim): - # draw random indices - rand_idx = np.random.randint(0, sample_size, n_samples) - # store the raw data with given random indices - samples[:, pa_idx] = self.raw_data[pa_idx, rand_idx] - else: - try: - samples = self.JDist.resample(int(n_samples)).T - except AttributeError: - samples = self.JDist.sample(int(n_samples)).T - # Check if all samples are in the bound_tuples - for idx, param_set in enumerate(samples): - if not self._check_ranges(param_set, self.bound_tuples): - try: - proposed_sample = chaospy.generate_samples( - 1, domain=self.JDist, rule='random').T[0] - except: - proposed_sample = self.JDist.resample(1).T[0] - while not self._check_ranges(proposed_sample, - self.bound_tuples): - try: - proposed_sample = chaospy.generate_samples( - 1, domain=self.JDist, rule='random').T[0] - except: - proposed_sample = self.JDist.resample(1).T[0] - samples[idx] = proposed_sample - - return samples - - # ------------------------------------------------------------------------- - def pcm_sampler(self, max_deg): - """ - Generates collocation points based on the root of the polynomial - degrees. - - Parameters - ---------- - max_deg : int - Maximum degree defined by user. - - Returns - ------- - opt_col_points: array of shape (n_samples, n_params) - Collocation points. - - """ - - raw_data = self.raw_data - - # Guess the closest degree to self.n_samples - def M_uptoMax(deg): - result = [] - for d in range(1, deg+1): - result.append(math.factorial(self.ndim+d) // - (math.factorial(self.ndim) * math.factorial(d))) - return np.array(result) - - guess_Deg = np.where(M_uptoMax(max_deg) > self.n_samples)[0][0] - - c_points = np.zeros((guess_Deg+1, self.ndim)) - - def PolynomialPa(parIdx): - return apoly_construction(self.raw_data[parIdx], max_deg) - - for i in range(self.ndim): - poly_coeffs = PolynomialPa(i)[guess_Deg+1][::-1] - c_points[:, i] = np.trim_zeros(np.roots(poly_coeffs)) - - # Construction of optimal integration points - Prod = itertools.product(np.arange(1, guess_Deg+2), repeat=self.ndim) - sort_dig_unique_combos = np.array(list(filter(lambda x: x, Prod))) - - # Ranking relatively mean - Temp = np.empty(shape=[0, guess_Deg+1]) - for j in range(self.ndim): - s = abs(c_points[:, j]-np.mean(raw_data[j])) - Temp = np.append(Temp, [s], axis=0) - temp = Temp.T - - index_CP = np.sort(temp, axis=0) - sort_cpoints = np.empty((0, guess_Deg+1)) - - for j in range(self.ndim): - sort_cp = c_points[index_CP[:, j], j] - sort_cpoints = np.vstack((sort_cpoints, sort_cp)) - - # Mapping of Combination to Cpoint Combination - sort_unique_combos = np.empty(shape=[0, self.ndim]) - for i in range(len(sort_dig_unique_combos)): - sort_un_comb = [] - for j in range(self.ndim): - SortUC = sort_cpoints[j, sort_dig_unique_combos[i, j]-1] - sort_un_comb.append(SortUC) - sort_uni_comb = np.asarray(sort_un_comb) - sort_unique_combos = np.vstack((sort_unique_combos, sort_uni_comb)) - - # Output the collocation points - if self.sampling_method.lower() == 'lscm': - opt_col_points = sort_unique_combos - else: - opt_col_points = sort_unique_combos[0:self.n_samples] - - return opt_col_points - - # ------------------------------------------------------------------------- - def transform(self, X, params=None, method=None): - """ - Transform the samples via either a Rosenblatt or an isoprobabilistic - transformation. - - Parameters - ---------- - X : array of shape (n_samples,n_params) - Samples to be transformed. - method : string - If transformation method is 'user' transform X, else just pass X. - - Returns - ------- - tr_X: array of shape (n_samples,n_params) - Transformed samples. - - """ - if self.InputObj.Rosenblatt: - self.origJDist, _ = self.build_dist(False) - if method == 'user': - tr_X = self.JDist.inv(self.origJDist.fwd(X.T)).T - else: - # Inverse to original spcace -- generate sample ED - tr_X = self.origJDist.inv(self.JDist.fwd(X.T)).T - else: - # Transform samples via an isoprobabilistic transformation - n_samples, n_params = X.shape - Inputs = self.InputObj - origJDist = self.JDist - poly_types = self.poly_types - - disttypes = [] - for par_i in range(n_params): - disttypes.append(Inputs.Marginals[par_i].dist_type) - - # Pass non-transformed X, if arbitrary PCE is selected. - if None in disttypes or self.input_data_given or self.apce: - return X - - cdfx = np.zeros((X.shape)) - tr_X = np.zeros((X.shape)) - - for par_i in range(n_params): - - # Extract the parameters of the original space - disttype = disttypes[par_i] - if disttype is not None: - dist = origJDist[par_i] - else: - dist = None - polytype = poly_types[par_i] - cdf = np.vectorize(lambda x: dist.cdf(x)) - - # Extract the parameters of the transformation space based on - # polyType - if polytype == 'legendre' or disttype == 'uniform': - # Generate Y_Dists based - params_Y = [-1, 1] - dist_Y = st.uniform(loc=params_Y[0], - scale=params_Y[1]-params_Y[0]) - inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x)) - - elif polytype == 'hermite' or disttype == 'norm': - params_Y = [0, 1] - dist_Y = st.norm(loc=params_Y[0], scale=params_Y[1]) - inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x)) - - elif polytype == 'laguerre' or disttype == 'gamma': - params_Y = [1, params[1]] - dist_Y = st.gamma(loc=params_Y[0], scale=params_Y[1]) - inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x)) - - # Compute CDF_x(X) - cdfx[:, par_i] = cdf(X[:, par_i]) - - # Compute invCDF_y(cdfx) - tr_X[:, par_i] = inv_cdf(cdfx[:, par_i]) - - return tr_X - - # ------------------------------------------------------------------------- - def fit_dist(self, y): - """ - Fits the known distributions to the data. - - Parameters - ---------- - y : array of shape (n_samples) - Data to be fitted. - - Returns - ------- - sel_dist: string - Selected distribution type from `lognorm`, `norm`, `uniform` or - `expon`. - params : list - Parameters corresponding to the selected distibution type. - - """ - dist_results = [] - params = {} - dist_names = ['lognorm', 'norm', 'uniform', 'expon'] - for dist_name in dist_names: - dist = getattr(st, dist_name) - - try: - if dist_name != 'lognorm': - param = dist.fit(y) - else: - param = dist.fit(np.exp(y), floc=0) - except: - param = dist.fit(y) - - params[dist_name] = param - # Applying the Kolmogorov-Smirnov test - D, p = st.kstest(y, dist_name, args=param) - dist_results.append((dist_name, D)) - - # select the best fitted distribution - sel_dist, D = (min(dist_results, key=lambda item: item[1])) - - if sel_dist == 'uniform': - params[sel_dist] = [params[sel_dist][0], params[sel_dist][0] + - params[sel_dist][1]] - if D < 0.05: - return sel_dist, params[sel_dist] - else: - return None, None - - # ------------------------------------------------------------------------- - def _check_ranges(self, theta, ranges): - """ - This function checks if theta lies in the given ranges. - - Parameters - ---------- - theta : array - Proposed parameter set. - ranges : nested list - List of the praremeter ranges. - - Returns - ------- - c : bool - If it lies in the given range, it return True else False. - - """ - c = True - # traverse in the list1 - for i, bounds in enumerate(ranges): - x = theta[i] - # condition check - if x < bounds[0] or x > bounds[1]: - c = False - return c - return c diff --git a/examples/only-model/bayesvalidrox/surrogate_models/exploration.py b/examples/only-model/bayesvalidrox/surrogate_models/exploration.py deleted file mode 100644 index cb3ccfcd4a15e26b2292973167d01efedd5a9a62..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/exploration.py +++ /dev/null @@ -1,468 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import numpy as np -from scipy.spatial import distance - - -class Exploration: - """ - Created based on the Surrogate Modeling Toolbox (SUMO) [1]. - - [1] Gorissen, D., Couckuyt, I., Demeester, P., Dhaene, T. and Crombecq, K., - 2010. A surrogate modeling and adaptive sampling toolbox for computer - based design. Journal of machine learning research.-Cambridge, Mass., - 11, pp.2051-2055. sumo@sumo.intec.ugent.be - http://sumo.intec.ugent.be - - Attributes - ---------- - MetaModel : obj - MetaModel object. - n_candidate : int - Number of candidate samples. - mc_criterion : str - Selection crieterion. The default is `'mc-intersite-proj-th'`. Another - option is `'mc-intersite-proj'`. - w : int - Number of random points in the domain for each sample of the - training set. - """ - - def __init__(self, MetaModel, n_candidate, - mc_criterion='mc-intersite-proj-th'): - self.MetaModel = MetaModel - self.Marginals = [] - self.n_candidate = n_candidate - self.mc_criterion = mc_criterion - self.w = 100 - - def get_exploration_samples(self): - """ - This function generates candidates to be selected as new design and - their associated exploration scores. - - Returns - ------- - all_candidates : array of shape (n_candidate, n_params) - A list of samples. - exploration_scores: arrays of shape (n_candidate) - Exploration scores. - """ - MetaModel = self.MetaModel - explore_method = MetaModel.ExpDesign.explore_method - - print("\n") - print(f' The {explore_method}-Method is selected as the exploration ' - 'method.') - print("\n") - - if explore_method == 'Voronoi': - # Generate samples using the Voronoi method - all_candidates, exploration_scores = self.get_vornoi_samples() - else: - # Generate samples using the MC method - all_candidates, exploration_scores = self.get_mc_samples() - - return all_candidates, exploration_scores - - # ------------------------------------------------------------------------- - def get_vornoi_samples(self): - """ - This function generates samples based on voronoi cells and their - corresponding scores - - Returns - ------- - new_samples : array of shape (n_candidate, n_params) - A list of samples. - exploration_scores: arrays of shape (n_candidate) - Exploration scores. - """ - - mc_criterion = self.mc_criterion - n_candidate = self.n_candidate - # Get the Old ExpDesign #samples - old_ED_X = self.MetaModel.ExpDesign.X - ndim = old_ED_X.shape[1] - - # calculate error #averageErrors - error_voronoi, all_candidates = self.approximate_voronoi( - self.w, old_ED_X - ) - - # Pick the best candidate point in the voronoi cell - # for each best sample - selected_samples = np.empty((0, ndim)) - bad_samples = [] - - for index in range(len(error_voronoi)): - - # get candidate new samples from voronoi tesselation - candidates = self.closest_points[index] - - # get total number of candidates - n_new_samples = candidates.shape[0] - - # still no candidate samples around this one, skip it! - if n_new_samples == 0: - print('The following sample has been skipped because there ' - 'were no candidate samples around it...') - print(old_ED_X[index]) - bad_samples.append(index) - continue - - # find candidate that is farthest away from any existing sample - max_min_distance = 0 - best_candidate = 0 - min_intersite_dist = np.zeros((n_new_samples)) - min_projected_dist = np.zeros((n_new_samples)) - - for j in range(n_new_samples): - - new_samples = np.vstack((old_ED_X, selected_samples)) - - # find min distorted distance from all other samples - euclidean_dist = self._build_dist_matrix_point( - new_samples, candidates[j], do_sqrt=True) - min_euclidean_dist = np.min(euclidean_dist) - min_intersite_dist[j] = min_euclidean_dist - - # Check if this is the maximum minimum distance from all other - # samples - if min_euclidean_dist >= max_min_distance: - max_min_distance = min_euclidean_dist - best_candidate = j - - # Projected distance - projected_dist = distance.cdist( - new_samples, [candidates[j]], 'chebyshev') - min_projected_dist[j] = np.min(projected_dist) - - if mc_criterion == 'mc-intersite-proj': - weight_euclidean_dist = 0.5 * ((n_new_samples+1)**(1/ndim) - 1) - weight_projected_dist = 0.5 * (n_new_samples+1) - total_dist_scores = weight_euclidean_dist * min_intersite_dist - total_dist_scores += weight_projected_dist * min_projected_dist - - elif mc_criterion == 'mc-intersite-proj-th': - alpha = 0.5 # chosen (tradeoff) - d_min = 2 * alpha / n_new_samples - if any(min_projected_dist < d_min): - candidates = np.delete( - candidates, [min_projected_dist < d_min], axis=0 - ) - total_dist_scores = np.delete( - min_intersite_dist, [min_projected_dist < d_min], - axis=0 - ) - else: - total_dist_scores = min_intersite_dist - else: - raise NameError( - 'The MC-Criterion you requested is not available.' - ) - - # Add the best candidate to the list of new samples - best_candidate = np.argsort(total_dist_scores)[::-1][:n_candidate] - selected_samples = np.vstack( - (selected_samples, candidates[best_candidate]) - ) - - self.new_samples = selected_samples - self.exploration_scores = np.delete(error_voronoi, bad_samples, axis=0) - - return self.new_samples, self.exploration_scores - - # ------------------------------------------------------------------------- - def get_mc_samples(self, all_candidates=None): - """ - This function generates random samples based on Global Monte Carlo - methods and their corresponding scores, based on [1]. - - [1] Crombecq, K., Laermans, E. and Dhaene, T., 2011. Efficient - space-filling and non-collapsing sequential design strategies for - simulation-based modeling. European Journal of Operational Research - , 214(3), pp.683-696. - DOI: https://doi.org/10.1016/j.ejor.2011.05.032 - - Implemented methods to compute scores: - 1) mc-intersite-proj - 2) mc-intersite-proj-th - - Arguments - --------- - all_candidates : array, optional - Samples to compute the scores for. The default is `None`. In this - case, samples will be generated by defined model input marginals. - - Returns - ------- - new_samples : array of shape (n_candidate, n_params) - A list of samples. - exploration_scores: arrays of shape (n_candidate) - Exploration scores. - """ - MetaModel = self.MetaModel - explore_method = MetaModel.ExpDesign.explore_method - mc_criterion = self.mc_criterion - if all_candidates is None: - n_candidate = self.n_candidate - else: - n_candidate = all_candidates.shape[0] - - # Get the Old ExpDesign #samples - old_ED_X = MetaModel.ExpDesign.X - ndim = old_ED_X.shape[1] - - # ----- Compute the number of random points ----- - if all_candidates is None: - # Generate MC Samples - all_candidates = MetaModel.ExpDesign.generate_samples( - self.n_candidate, explore_method - ) - self.all_candidates = all_candidates - - # initialization - min_intersite_dist = np.zeros((n_candidate)) - min_projected_dist = np.zeros((n_candidate)) - - for i, candidate in enumerate(all_candidates): - - # find candidate that is farthest away from any existing sample - maxMinDistance = 0 - - # find min distorted distance from all other samples - euclidean_dist = self._build_dist_matrix_point( - old_ED_X, candidate, do_sqrt=True - ) - min_euclidean_dist = np.min(euclidean_dist) - min_intersite_dist[i] = min_euclidean_dist - - # Check if this is the maximum minimum distance from all other - # samples - if min_euclidean_dist >= maxMinDistance: - maxMinDistance = min_euclidean_dist - - # Projected distance - projected_dist = self._build_dist_matrix_point( - old_ED_X, candidate, 'chebyshev' - ) - min_projected_dist[i] = np.min(projected_dist) - - if mc_criterion == 'mc-intersite-proj': - weight_euclidean_dist = ((n_candidate+1)**(1/ndim) - 1) * 0.5 - weight_projected_dist = (n_candidate+1) * 0.5 - total_dist_scores = weight_euclidean_dist * min_intersite_dist - total_dist_scores += weight_projected_dist * min_projected_dist - - elif mc_criterion == 'mc-intersite-proj-th': - alpha = 0.5 # chosen (tradeoff) - d_min = 2 * alpha / n_candidate - if any(min_projected_dist < d_min): - all_candidates = np.delete( - all_candidates, [min_projected_dist < d_min], axis=0 - ) - total_dist_scores = np.delete( - min_intersite_dist, [min_projected_dist < d_min], axis=0 - ) - else: - total_dist_scores = min_intersite_dist - else: - raise NameError('The MC-Criterion you requested is not available.') - - self.new_samples = all_candidates - self.exploration_scores = total_dist_scores - self.exploration_scores /= np.nansum(total_dist_scores) - - return self.new_samples, self.exploration_scores - - # ------------------------------------------------------------------------- - def approximate_voronoi(self, w, samples): - """ - An approximate (monte carlo) version of Matlab's voronoi command. - - Arguments - --------- - samples : array - Old experimental design to be used as center points for voronoi - cells. - - Returns - ------- - areas : array - An approximation of the voronoi cells' areas. - all_candidates: list of arrays - A list of samples in each voronoi cell. - """ - MetaModel = self.MetaModel - - n_samples = samples.shape[0] - ndim = samples.shape[1] - - # Compute the number of random points - n_points = w * samples.shape[0] - # Generate w random points in the domain for each sample - points = MetaModel.ExpDesign.generate_samples(n_points, 'random') - self.all_candidates = points - - # Calculate the nearest sample to each point - self.areas = np.zeros((n_samples)) - self.closest_points = [np.empty((0, ndim)) for i in range(n_samples)] - - # Compute the minimum distance from all the samples of old_ED_X for - # each test point - for idx in range(n_points): - # calculate the minimum distance - distances = self._build_dist_matrix_point( - samples, points[idx], do_sqrt=True - ) - closest_sample = np.argmin(distances) - - # Add to the voronoi list of the closest sample - self.areas[closest_sample] = self.areas[closest_sample] + 1 - prev_closest_points = self.closest_points[closest_sample] - self.closest_points[closest_sample] = np.vstack( - (prev_closest_points, points[idx]) - ) - - # Divide by the amount of points to get the estimated volume of each - # voronoi cell - self.areas /= n_points - - self.perc = np.max(self.areas * 100) - - self.errors = self.areas - - return self.areas, self.all_candidates - - # ------------------------------------------------------------------------- - def _build_dist_matrix_point(self, samples, point, method='euclidean', - do_sqrt=False): - """ - Calculates the intersite distance of all points in samples from point. - - Parameters - ---------- - samples : array of shape (n_samples, n_params) - The old experimental design. - point : array - A candidate point. - method : str - Distance method. - do_sqrt : bool, optional - Whether to return distances or squared distances. The default is - `False`. - - Returns - ------- - distances : array - Distances. - - """ - distances = distance.cdist(samples, np.array([point]), method) - - # do square root? - if do_sqrt: - return distances - else: - return distances**2 - -#if __name__ == "__main__": -# import scipy.stats as stats -# import matplotlib.pyplot as plt -# import matplotlib as mpl -# import matplotlib.cm as cm -# plt.rc('font', family='sans-serif', serif='Arial') -# plt.rc('figure', figsize = (12, 8)) -# -# def plotter(old_ED_X, all_candidates, exploration_scores): -# global Bounds -# -# from scipy.spatial import Voronoi, voronoi_plot_2d -# vor = Voronoi(old_ED_X) -# -# fig = voronoi_plot_2d(vor) -# -# # find min/max values for normalization -## minima = min(exploration_scores) -## maxima = max(exploration_scores) -## -## # normalize chosen colormap -## norm = mpl.colors.Normalize(vmin=minima, vmax=maxima, clip=True) -## mapper = cm.ScalarMappable(norm=norm, cmap=cm.Blues_r) -## -## for r in range(len(vor.point_region)): -## region = vor.regions[vor.point_region[r]] -## if not -1 in region: -## polygon = [vor.vertices[i] for i in region] -## plt.fill(*zip(*polygon), color=mapper.to_rgba(exploration_scores[r])) -# -# -# ax1 = fig.add_subplot(111) -# -# ax1.scatter(old_ED_X[:,0], old_ED_X[:,1], s=10, c='r', marker="s", label='Old Design Points') -# for i in range(old_ED_X.shape[0]): -# txt = 'p'+str(i+1) -# ax1.annotate(txt, (old_ED_X[i,0],old_ED_X[i,1])) -# -## for i in range(NrofCandGroups): -## Candidates = all_candidates['group_'+str(i+1)] -## ax1.scatter(Candidates[:,0],Candidates[:,1], s=10, c='b', marker="o", label='Design candidates') -# ax1.scatter(all_candidates[:,0],all_candidates[:,1], s=10, c='b', marker="o", label='Design candidates') -# -# ax1.set_xlim(Bounds[0][0], Bounds[0][1]) -# ax1.set_ylim(Bounds[1][0], Bounds[1][1]) -# -# plt.legend(loc='best'); -# plt.show() -# -# def voronoi_volumes(points): -# from scipy.spatial import Voronoi, ConvexHull -# v = Voronoi(points) -# vol = np.zeros(v.npoints) -# -# for i, reg_num in enumerate(v.point_region): -# indices = v.regions[reg_num] -# if -1 in indices: # some regions can be opened -# vol[i] = np.inf -# else: -# -# #print("reg_num={0: 3.3f} X1={1: 3.3f} X2={2: 3.3f}".format(reg_num, v.points[reg_num-1, 0], v.points[reg_num-1, 1])) -# vol[i] = ConvexHull(v.vertices[indices]).volume -# -# print('-'*40) -# for i in range(nrofSamples): -# print("idx={0:d} X1={1: 3.3f} X2={2: 3.3f} Volume={3: 3.3f}".format(i+1, v.points[i, 0], v.points[i, 1], vol[i])) -# -# return vol -# -# NofPa = 2 -# -# Bounds = ((-5,10), (0,15)) -# -# nrofSamples = 10 -# old_ED_X = np.zeros((nrofSamples, NofPa)) -# for idx in range(NofPa): -# Loc = Bounds[idx][0] -# Scale = Bounds[idx][1] - Bounds[idx][0] -# old_ED_X[:,idx] = stats.uniform(loc=Loc, scale=Scale).rvs(size=nrofSamples) -# -# -# nNewCandidate = 40 -# -# # New Function -# volumes = voronoi_volumes(old_ED_X) -# -# -# # SUMO -# Exploration = Exploration(Bounds, old_ED_X, nNewCandidate) -# -# #all_candidates, Score = Exploration.get_vornoi_samples() -# all_candidates, Score = Exploration.get_mc_samples() -# -# print('-'*40) -## for i in range(nrofSamples): -## print("idx={0:d} X1={1: 3.3f} X2={2: 3.3f} Volume={3: 3.3f}".format(i+1, old_ED_X[i,0], old_ED_X[i,1], vornoi.areas[i])) -# -# plotter(old_ED_X, all_candidates, volumes) - diff --git a/examples/only-model/bayesvalidrox/surrogate_models/glexindex.py b/examples/only-model/bayesvalidrox/surrogate_models/glexindex.py deleted file mode 100644 index 6d9ba3c2f3c02be8e2ca04be6f95779ed0825ad8..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/glexindex.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Multi indices for monomial exponents. -Credit: Jonathan Feinberg -https://github.com/jonathf/numpoly/blob/master/numpoly/utils/glexindex.py -""" - -import numpy -import numpy.typing - - -def glexindex(start, stop=None, dimensions=1, cross_truncation=1., - graded=False, reverse=False): - """ - Generate graded lexicographical multi-indices for the monomial exponents. - Args: - start (Union[int, numpy.ndarray]): - The lower order of the indices. If array of int, counts as lower - bound for each axis. - stop (Union[int, numpy.ndarray, None]): - The maximum shape included. If omitted: stop <- start; start <- 0 - If int is provided, set as largest total order. If array of int, - set as upper bound for each axis. - dimensions (int): - The number of dimensions in the expansion. - cross_truncation (float, Tuple[float, float]): - Use hyperbolic cross truncation scheme to reduce the number of - terms in expansion. If two values are provided, first is low bound - truncation, while the latter upper bound. If only one value, upper - bound is assumed. - graded (bool): - Graded sorting, meaning the indices are always sorted by the index - sum. E.g. ``(2, 2, 2)`` has a sum of 6, and will therefore be - consider larger than both ``(3, 1, 1)`` and ``(1, 1, 3)``. - reverse (bool): - Reversed lexicographical sorting meaning that ``(1, 3)`` is - considered smaller than ``(3, 1)``, instead of the opposite. - Returns: - list: - Order list of indices. - Examples: - >>> numpoly.glexindex(4).tolist() - [[0], [1], [2], [3]] - >>> numpoly.glexindex(2, dimensions=2).tolist() - [[0, 0], [1, 0], [0, 1]] - >>> numpoly.glexindex(start=2, stop=3, dimensions=2).tolist() - [[2, 0], [1, 1], [0, 2]] - >>> numpoly.glexindex([1, 2, 3]).tolist() - [[0, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 2]] - >>> numpoly.glexindex([1, 2, 3], cross_truncation=numpy.inf).tolist() - [[0, 0, 0], [0, 1, 0], [0, 0, 1], [0, 1, 1], [0, 0, 2], [0, 1, 2]] - """ - if stop is None: - start, stop = 0, start - start = numpy.array(start, dtype=int).flatten() - stop = numpy.array(stop, dtype=int).flatten() - start, stop, _ = numpy.broadcast_arrays(start, stop, numpy.empty(dimensions)) - - cross_truncation = cross_truncation*numpy.ones(2) - indices = _glexindex(start, stop, cross_truncation) - if indices.size: - indices = indices[glexsort(indices.T, graded=graded, reverse=reverse)] - return indices - - -def _glexindex(start, stop, cross_truncation=1.): - """Backend for the glexindex function.""" - # At the beginning the current list of indices just ranges over the - # last dimension. - bound = stop.max() - dimensions = len(start) - start = numpy.clip(start, a_min=0, a_max=None) - dtype = numpy.uint8 if bound < 256 else numpy.uint16 - range_ = numpy.arange(bound, dtype=dtype) - indices = range_[:, numpy.newaxis] - - for idx in range(dimensions-1): - - # Truncate at each step to keep memory usage low - if idx: - indices = indices[cross_truncate(indices, bound-1, cross_truncation[1])] - - # Repeats the current set of indices. - # e.g. [0,1,2] -> [0,1,2,0,1,2,...,0,1,2] - indices = numpy.tile(indices, (bound, 1)) - - # Stretches ranges over the new dimension. - # e.g. [0,1,2] -> [0,0,...,0,1,1,...,1,2,2,...,2] - front = range_.repeat(len(indices)//bound)[:, numpy.newaxis] - - # Puts them two together. - indices = numpy.column_stack((front, indices)) - - # Complete the truncation scheme - if dimensions == 1: - indices = indices[(indices >= start) & (indices < bound)] - else: - lower = cross_truncate(indices, start-1, cross_truncation[0]) - upper = cross_truncate(indices, stop-1, cross_truncation[1]) - indices = indices[lower ^ upper] - - return numpy.array(indices, dtype=int).reshape(-1, dimensions) - - -def cross_truncate(indices, bound, norm): - r""" - Truncate of indices using L_p norm. - .. math: - L_p(x) = \sum_i |x_i/b_i|^p ^{1/p} \leq 1 - where :math:`b_i` are bounds that each :math:`x_i` should follow. - Args: - indices (Sequence[int]): - Indices to be truncated. - bound (int, Sequence[int]): - The bound function for witch the indices can not be larger than. - norm (float, Sequence[float]): - The `p` in the `L_p`-norm. Support includes both `L_0` and `L_inf`. - Returns: - Boolean indices to ``indices`` with True for each index where the - truncation criteria holds. - Examples: - >>> indices = numpy.array(numpy.mgrid[:10, :10]).reshape(2, -1).T - >>> indices[cross_truncate(indices, 2, norm=0)].T - array([[0, 0, 0, 1, 2], - [0, 1, 2, 0, 0]]) - >>> indices[cross_truncate(indices, 2, norm=1)].T - array([[0, 0, 0, 1, 1, 2], - [0, 1, 2, 0, 1, 0]]) - >>> indices[cross_truncate(indices, [0, 1], norm=1)].T - array([[0, 0], - [0, 1]]) - """ - assert norm >= 0, "negative L_p norm not allowed" - bound = numpy.asfarray(bound).flatten()*numpy.ones(indices.shape[1]) - - if numpy.any(bound < 0): - return numpy.zeros((len(indices),), dtype=bool) - - if numpy.any(bound == 0): - out = numpy.all(indices[:, bound == 0] == 0, axis=-1) - if numpy.any(bound): - out &= cross_truncate(indices[:, bound != 0], bound[bound != 0], norm=norm) - return out - - if norm == 0: - out = numpy.sum(indices > 0, axis=-1) <= 1 - out[numpy.any(indices > bound, axis=-1)] = False - elif norm == numpy.inf: - out = numpy.max(indices/bound, axis=-1) <= 1 - else: - out = numpy.sum((indices/bound)**norm, axis=-1)**(1./norm) <= 1 - - assert numpy.all(out[numpy.all(indices == 0, axis=-1)]) - - return out - - -def glexsort( - keys: numpy.typing.ArrayLike, - graded: bool = False, - reverse: bool = False, -) -> numpy.ndarray: - """ - Sort keys using graded lexicographical ordering. - Same as ``numpy.lexsort``, but also support graded and reverse - lexicographical ordering. - Args: - keys: - Values to sort. - graded: - Graded sorting, meaning the indices are always sorted by the index - sum. E.g. ``(2, 2, 2)`` has a sum of 6, and will therefore be - consider larger than both ``(3, 1, 1)`` and ``(1, 1, 3)``. - reverse: - Reverse lexicographical sorting meaning that ``(1, 3)`` is - considered smaller than ``(3, 1)``, instead of the opposite. - Returns: - Array of indices that sort the keys along the specified axis. - Examples: - >>> indices = numpy.array([[0, 0, 0, 1, 2, 1], - ... [1, 2, 0, 0, 0, 1]]) - >>> indices[:, numpy.lexsort(indices)] - array([[0, 1, 2, 0, 1, 0], - [0, 0, 0, 1, 1, 2]]) - >>> indices[:, numpoly.glexsort(indices)] - array([[0, 1, 2, 0, 1, 0], - [0, 0, 0, 1, 1, 2]]) - >>> indices[:, numpoly.glexsort(indices, reverse=True)] - array([[0, 0, 0, 1, 1, 2], - [0, 1, 2, 0, 1, 0]]) - >>> indices[:, numpoly.glexsort(indices, graded=True)] - array([[0, 1, 0, 2, 1, 0], - [0, 0, 1, 0, 1, 2]]) - >>> indices[:, numpoly.glexsort(indices, graded=True, reverse=True)] - array([[0, 0, 1, 0, 1, 2], - [0, 1, 0, 2, 1, 0]]) - >>> indices = numpy.array([4, 5, 6, 3, 2, 1]) - >>> indices[numpoly.glexsort(indices)] - array([1, 2, 3, 4, 5, 6]) - """ - keys_ = numpy.atleast_2d(keys) - if reverse: - keys_ = keys_[::-1] - - indices = numpy.array(numpy.lexsort(keys_)) - if graded: - indices = indices[numpy.argsort( - numpy.sum(keys_[:, indices], axis=0))].T - return indices diff --git a/examples/only-model/bayesvalidrox/surrogate_models/inputs.py b/examples/only-model/bayesvalidrox/surrogate_models/inputs.py deleted file mode 100644 index 783e82b053cc458be712b588b7fde3a0f3c8decb..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/inputs.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -class Input: - """ - A class to define the uncertain input parameters. - - Attributes - ---------- - Marginals : obj - Marginal objects. See `inputs.Marginal`. - Rosenblatt : bool - If Rossenblatt transformation is required for the dependent input - parameters. - - Examples - ------- - Marginals can be defined as following: - - >>> Inputs.add_marginals() - >>> Inputs.Marginals[0].name = 'X_1' - >>> Inputs.Marginals[0].dist_type = 'uniform' - >>> Inputs.Marginals[0].parameters = [-5, 5] - - If there is no common data is avaliable, the input data can be given - as following: - - >>> Inputs.add_marginals() - >>> Inputs.Marginals[0].name = 'X_1' - >>> Inputs.Marginals[0].input_data = input_data - """ - poly_coeffs_flag = True - - def __init__(self): - self.Marginals = [] - self.Rosenblatt = False - - def add_marginals(self): - """ - Adds a new Marginal object to the input object. - - Returns - ------- - None. - - """ - self.Marginals.append(Marginal()) - - -# Nested class -class Marginal: - """ - An object containing the specifications of the marginals for each uncertain - parameter. - - Attributes - ---------- - name : string - Name of the parameter. The default is `'$x_1$'`. - dist_type : string - Name of the distribution. The default is `None`. - parameters : list - List of the parameters corresponding to the distribution type. The - default is `None`. - input_data : array - Available input data. The default is `[]`. - moments : list - List of the moments. - """ - - def __init__(self): - self.name = '$x_1$' - self.dist_type = None - self.parameters = None - self.input_data = [] - self.moments = None diff --git a/examples/only-model/bayesvalidrox/surrogate_models/meta_model_engine.py b/examples/only-model/bayesvalidrox/surrogate_models/meta_model_engine.py deleted file mode 100644 index 7ca9e9cca220f2efa4c964a067a8f839cd188e1a..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/meta_model_engine.py +++ /dev/null @@ -1,2175 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Jan 28 09:21:18 2022 - -@author: farid -""" -import numpy as np -from scipy import stats, signal, linalg, sparse -from scipy.spatial import distance -from copy import deepcopy, copy -from tqdm import tqdm -import scipy.optimize as opt -from sklearn.metrics import mean_squared_error -import multiprocessing -import matplotlib.pyplot as plt -import sys -import os -import gc -import seaborn as sns -from joblib import Parallel, delayed - -import bayesvalidrox -from .exploration import Exploration -from bayesvalidrox.bayes_inference.bayes_inference import BayesInference -from bayesvalidrox.bayes_inference.discrepancy import Discrepancy -import pandas as pd - - -class MetaModelEngine(): - """ Sequential experimental design - This class provieds method for trainig the meta-model in an iterative - manners. - The main method to execute the task is `train_seq_design`, which - recieves a model object and returns the trained metamodel. - """ - - def __init__(self, meta_model_opts): - self.MetaModel = meta_model_opts - - # ------------------------------------------------------------------------- - def run(self): - - Model = self.MetaModel.ModelObj - self.MetaModel.n_params = len(self.MetaModel.input_obj.Marginals) - self.MetaModel.ExpDesignFlag = 'normal' - # --- Prepare pce degree --- - if self.MetaModel.meta_model_type.lower() == 'pce': - if type(self.MetaModel.pce_deg) is not np.ndarray: - self.MetaModel.pce_deg = np.array(self.MetaModel.pce_deg) - - if self.MetaModel.ExpDesign.method == 'normal': - self.MetaModel.ExpDesignFlag = 'normal' - self.MetaModel.train_norm_design(parallel = False) - - elif self.MetaModel.ExpDesign.method == 'sequential': - self.train_seq_design() - else: - raise Exception("The method for experimental design you requested" - " has not been implemented yet.") - - # Zip the model run directories - if self.MetaModel.ModelObj.link_type.lower() == 'pylink' and\ - self.MetaModel.ExpDesign.sampling_method.lower() != 'user': - Model.zip_subdirs(Model.name, f'{Model.name}_') - - # ------------------------------------------------------------------------- - def train_seq_design(self): - """ - Starts the adaptive sequential design for refining the surrogate model - by selecting training points in a sequential manner. - - Returns - ------- - MetaModel : object - Meta model object. - - """ - # Set model to have shorter call - Model = self.MetaModel.ModelObj - # MetaModel = self.MetaModel - self.Model = Model - - # Initialization - self.MetaModel.SeqModifiedLOO = {} - self.MetaModel.seqValidError = {} - self.MetaModel.SeqBME = {} - self.MetaModel.SeqKLD = {} - self.MetaModel.SeqDistHellinger = {} - self.MetaModel.seqRMSEMean = {} - self.MetaModel.seqRMSEStd = {} - self.MetaModel.seqMinDist = [] - - # Determine the metamodel type - if self.MetaModel.meta_model_type.lower() != 'gpe': - pce = True - else: - pce = False - # If given, use mc reference data - mc_ref = True if bool(Model.mc_reference) else False - if mc_ref: - Model.read_mc_reference() - - # if valid_samples not defined, do so now - if not hasattr(self.MetaModel, 'valid_samples'): - self.MetaModel.valid_samples = [] - self.MetaModel.valid_model_runs = [] - self.MetaModel.valid_likelihoods = [] - - # Get the parameters - max_n_samples = self.MetaModel.ExpDesign.n_max_samples - mod_LOO_threshold = self.MetaModel.ExpDesign.mod_LOO_threshold - n_canddidate = self.MetaModel.ExpDesign.n_canddidate - post_snapshot = self.MetaModel.ExpDesign.post_snapshot - n_replication = self.MetaModel.ExpDesign.n_replication - util_func = self.MetaModel.ExpDesign.util_func - output_name = Model.Output.names - validError = None - # Handle if only one UtilityFunctions is provided - if not isinstance(util_func, list): - util_func = [self.MetaModel.ExpDesign.util_func] - - # Read observations or MCReference - if len(Model.observations) != 0 or Model.meas_file is not None: - self.observations = Model.read_observation() - obs_data = self.observations - else: - obs_data = [] - TotalSigma2 = {} - - # TODO: ---------- Initial self.MetaModel ---------- - # First run MetaModel on non-sequential design - self.MetaModel.train_norm_design(parallel = False) - initMetaModel = deepcopy(self.MetaModel) - - # Validation error if validation set is provided. - use as initial errors - if self.MetaModel.valid_model_runs: - init_rmse, init_valid_error = self.__validError(initMetaModel) - init_valid_error = list(init_valid_error.values()) - else: - init_rmse = None - - # Check if discrepancy is provided - if len(obs_data) != 0 and hasattr(self.MetaModel, 'Discrepancy'): - TotalSigma2 = self.MetaModel.Discrepancy.parameters - - # Calculate the initial BME - out = self.__BME_Calculator( - initMetaModel, obs_data, TotalSigma2, init_rmse) - init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out - print(f"\nInitial BME: {init_BME:.2f}") - print(f"Initial KLD: {init_KLD:.2f}") - - # Posterior snapshot (initial) - if post_snapshot: - parNames = self.MetaModel.ExpDesign.par_names - print('Posterior snapshot (initial) is being plotted...') - self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init') - - # Check the convergence of the Mean & Std - if mc_ref and pce: - init_rmse_mean, init_rmse_std = self.__error_Mean_Std() - print(f"Initial Mean and Std error: {init_rmse_mean:.2f}," - f" {init_rmse_std:.2f}") - - # Read the initial experimental design - # TODO: this sequential, or the non-sequential samples?? - Xinit = initMetaModel.ExpDesign.X - init_n_samples = len(initMetaModel.ExpDesign.X) - initYprev = initMetaModel.ModelOutputDict - initLCerror = initMetaModel.LCerror - n_itrs = max_n_samples - init_n_samples - - # Read the initial ModifiedLOO - if pce: - Scores_all, varExpDesignY = [], [] - for out_name in output_name: - y = self.MetaModel.ExpDesign.Y[out_name] - Scores_all.append(list( - self.MetaModel.score_dict['b_1'][out_name].values())) - if self.MetaModel.dim_red_method.lower() == 'pca': - pca = self.MetaModel.pca['b_1'][out_name] - components = pca.transform(y) - varExpDesignY.append(np.var(components, axis=0)) - else: - varExpDesignY.append(np.var(y, axis=0)) - - Scores = [item for sublist in Scores_all for item in sublist] - weights = [item for sublist in varExpDesignY for item in sublist] - init_mod_LOO = [np.average([1-score for score in Scores], - weights=weights)] - - prevMetaModel_dict = {} - # Replicate the sequential design - for repIdx in range(n_replication): # TODO: what does this do? - print(f'\n>>>> Replication: {repIdx+1}<<<<') - - # To avoid changes ub original aPCE object - self.MetaModel.ExpDesign.X = Xinit - self.MetaModel.ExpDesign.Y = initYprev - self.MetaModel.LCerror = initLCerror - - for util_f in util_func: # TODO: recheck choices for this - print(f'\n>>>> Utility Function: {util_f} <<<<') - # To avoid changes ub original aPCE object - self.MetaModel.ExpDesign.X = Xinit - self.MetaModel.ExpDesign.Y = initYprev - self.MetaModel.LCerror = initLCerror - - # Set the experimental design - Xprev = Xinit - total_n_samples = init_n_samples - Yprev = initYprev - - Xfull = [] - Yfull = [] - - # Store the initial ModifiedLOO - if pce: - print("\nInitial ModifiedLOO:", init_mod_LOO) - SeqModifiedLOO = np.array(init_mod_LOO) - - if len(self.MetaModel.valid_model_runs) != 0: - SeqValidError = np.array(init_valid_error) - - # Check if data is provided - if len(obs_data) != 0: - SeqBME = np.array([init_BME]) - SeqKLD = np.array([init_KLD]) - SeqDistHellinger = np.array([init_dist_hellinger]) - - if mc_ref and pce: - seqRMSEMean = np.array([init_rmse_mean]) - seqRMSEStd = np.array([init_rmse_std]) - - # ------- Start Sequential Experimental Design ------- - postcnt = 1 - for itr_no in range(1, n_itrs+1): - print(f'\n>>>> Iteration number {itr_no} <<<<') - - # Save the metamodel prediction before updating - prevMetaModel_dict[itr_no] = deepcopy(self.MetaModel) # Write last MetaModel here - if itr_no > 1: - pc_model = prevMetaModel_dict[itr_no-1] - self._y_hat_prev, _ = pc_model.eval_metamodel( # What's the use of this here?? - samples=Xfull[-1].reshape(1, -1)) - del prevMetaModel_dict[itr_no-1] # Delete second to last metamodel here? - - # Optimal Bayesian Design - self.MetaModel.ExpDesignFlag = 'sequential' - Xnew, updatedPrior = self.opt_SeqDesign(TotalSigma2, # TODO: check in this!! - n_canddidate, - util_f) - S = np.min(distance.cdist(Xinit, Xnew, 'euclidean')) - self.MetaModel.seqMinDist.append(S) - print(f"\nmin Dist from OldExpDesign: {S:2f}") - print("\n") - - # Evaluate the full model response at the new sample - Ynew, _ = Model.run_model_parallel( - Xnew, prevRun_No=total_n_samples - ) - total_n_samples += Xnew.shape[0] - - # ------ Plot the surrogate model vs Origninal Model ------ - if hasattr(self.MetaModel, 'adapt_verbose') and \ - self.MetaModel.adapt_verbose: - from .adaptPlot import adaptPlot - y_hat, std_hat = self.MetaModel.eval_metamodel( - samples=Xnew - ) - adaptPlot( - self.MetaModel, Ynew, y_hat, std_hat, - plotED=False - ) - - # -------- Retrain the surrogate model ------- - # Extend new experimental design - Xfull = np.vstack((Xprev, Xnew)) - - # Updating experimental design Y - for out_name in output_name: - Yfull = np.vstack((Yprev[out_name], Ynew[out_name])) - self.MetaModel.ModelOutputDict[out_name] = Yfull - - # Pass new design to the metamodel object - self.MetaModel.ExpDesign.sampling_method = 'user' - self.MetaModel.ExpDesign.X = Xfull - self.MetaModel.ExpDesign.Y = self.MetaModel.ModelOutputDict - - # Save the Experimental Design for next iteration - Xprev = Xfull - Yprev = self.MetaModel.ModelOutputDict - - # Pass the new prior as the input - self.MetaModel.input_obj.poly_coeffs_flag = False - if updatedPrior is not None: - self.MetaModel.input_obj.poly_coeffs_flag = True - print("updatedPrior:", updatedPrior.shape) - # Arbitrary polynomial chaos - for i in range(updatedPrior.shape[1]): - self.MetaModel.input_obj.Marginals[i].dist_type = None - x = updatedPrior[:, i] - self.MetaModel.input_obj.Marginals[i].raw_data = x - - # Train the surrogate model for new ExpDesign - self.MetaModel.train_norm_design(parallel=False) - - # -------- Evaluate the retrained surrogate model ------- - # Extract Modified LOO from Output - if pce: - Scores_all, varExpDesignY = [], [] - for out_name in output_name: - y = self.MetaModel.ExpDesign.Y[out_name] - Scores_all.append(list( - self.MetaModel.score_dict['b_1'][out_name].values())) - if self.MetaModel.dim_red_method.lower() == 'pca': - pca = self.MetaModel.pca['b_1'][out_name] - components = pca.transform(y) - varExpDesignY.append(np.var(components, - axis=0)) - else: - varExpDesignY.append(np.var(y, axis=0)) - Scores = [item for sublist in Scores_all for item - in sublist] - weights = [item for sublist in varExpDesignY for item - in sublist] - ModifiedLOO = [np.average( - [1-score for score in Scores], weights=weights)] - - print('\n') - print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO) - print('\n') - - # Compute the validation error - if self.MetaModel.valid_model_runs: - rmse, validError = self.__validError(self.MetaModel) - ValidError = list(validError.values()) - else: - rmse = None - - # Store updated ModifiedLOO - if pce: - SeqModifiedLOO = np.vstack( - (SeqModifiedLOO, ModifiedLOO)) - if len(self.MetaModel.valid_model_runs) != 0: - SeqValidError = np.vstack( - (SeqValidError, ValidError)) - # -------- Caclulation of BME as accuracy metric ------- - # Check if data is provided - if len(obs_data) != 0: - # Calculate the initial BME - out = self.__BME_Calculator(self.MetaModel, obs_data, - TotalSigma2, rmse) - BME, KLD, Posterior, likes, DistHellinger = out - print('\n') - print(f"Updated BME: {BME:.2f}") - print(f"Updated KLD: {KLD:.2f}") - print('\n') - - # Plot some snapshots of the posterior - step_snapshot = self.MetaModel.ExpDesign.step_snapshot - if post_snapshot and postcnt % step_snapshot == 0: - parNames = self.MetaModel.ExpDesign.par_names - print('Posterior snapshot is being plotted...') - self.__posteriorPlot(Posterior, parNames, - f'SeqPosterior_{postcnt}') - postcnt += 1 - - # Check the convergence of the Mean&Std - if mc_ref and pce: - print('\n') - RMSE_Mean, RMSE_std = self.__error_Mean_Std() - print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, " - f"{RMSE_std:.2f}") - print('\n') - - # Store the updated BME & KLD - # Check if data is provided - if len(obs_data) != 0: - SeqBME = np.vstack((SeqBME, BME)) - SeqKLD = np.vstack((SeqKLD, KLD)) - SeqDistHellinger = np.vstack((SeqDistHellinger, - DistHellinger)) - if mc_ref and pce: - seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean)) - seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std)) - - if pce and any(LOO < mod_LOO_threshold - for LOO in ModifiedLOO): - break - - # Clean up - if len(obs_data) != 0: - del out - print() - print('-'*50) - print() - - # Store updated ModifiedLOO and BME in dictonary - strKey = f'{util_f}_rep_{repIdx+1}' - if pce: - self.MetaModel.SeqModifiedLOO[strKey] = SeqModifiedLOO - if len(self.MetaModel.valid_model_runs) != 0: - self.MetaModel.seqValidError[strKey] = SeqValidError - - # Check if data is provided - if len(obs_data) != 0: - self.MetaModel.SeqBME[strKey] = SeqBME - self.MetaModel.SeqKLD[strKey] = SeqKLD - if hasattr(self.MetaModel, 'valid_likelihoods') and \ - self.MetaModel.valid_likelihoods: - self.MetaModel.SeqDistHellinger[strKey] = SeqDistHellinger - if mc_ref and pce: - self.MetaModel.seqRMSEMean[strKey] = seqRMSEMean - self.MetaModel.seqRMSEStd[strKey] = seqRMSEStd - - # return self.MetaModel - - # ------------------------------------------------------------------------- - def util_VarBasedDesign(self, X_can, index, util_func='Entropy'): - """ - Computes the exploitation scores based on: - active learning MacKay(ALM) and active learning Cohn (ALC) - Paper: Sequential Design with Mutual Information for Computer - Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas - (2016) - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - index : int - Model output index. - UtilMethod : string, optional - Exploitation utility function. The default is 'Entropy'. - - Returns - ------- - float - Score. - - """ - MetaModel = self.MetaModel - ED_X = MetaModel.ExpDesign.X - out_dict_y = MetaModel.ExpDesign.Y - out_names = MetaModel.ModelObj.Output.names - - # Run the Metamodel for the candidate - X_can = X_can.reshape(1, -1) - Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can) - - if util_func.lower() == 'alm': - # ----- Entropy/MMSE/active learning MacKay(ALM) ----- - # Compute perdiction variance of the old model - canPredVar = {key: std_PC_can[key]**2 for key in out_names} - - varPCE = np.zeros((len(out_names), X_can.shape[0])) - for KeyIdx, key in enumerate(out_names): - varPCE[KeyIdx] = np.max(canPredVar[key], axis=1) - score = np.max(varPCE, axis=0) - - elif util_func.lower() == 'eigf': - # ----- Expected Improvement for Global fit ----- - # Find closest EDX to the candidate - distances = distance.cdist(ED_X, X_can, 'euclidean') - index = np.argmin(distances) - - # Compute perdiction error and variance of the old model - predError = {key: Y_PC_can[key] for key in out_names} - canPredVar = {key: std_PC_can[key]**2 for key in out_names} - - # Compute perdiction error and variance of the old model - # Eq (5) from Liu et al.(2018) - EIGF_PCE = np.zeros((len(out_names), X_can.shape[0])) - for KeyIdx, key in enumerate(out_names): - residual = predError[key] - out_dict_y[key][int(index)] - var = canPredVar[key] - EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1) - score = np.max(EIGF_PCE, axis=0) - - return -1 * score # -1 is for minimization instead of maximization - - # ------------------------------------------------------------------------- - def util_BayesianActiveDesign(self, y_hat, std, sigma2Dict, var='DKL'): - """ - Computes scores based on Bayesian active design criterion (var). - - It is based on the following paper: - Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak. - "Bayesian3 active learning for the gaussian process emulator using - information theory." Entropy 22, no. 8 (2020): 890. - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - var : string, optional - BAL design criterion. The default is 'DKL'. - - Returns - ------- - float - Score. - - """ - - # Get the data - obs_data = self.observations - n_obs = self.Model.n_obs - mc_size = 10000 - - # Sample a distribution for a normal dist - # with Y_mean_can as the mean and Y_std_can as std. - Y_MC, std_MC = {}, {} - logPriorLikelihoods = np.zeros((mc_size)) - for key in list(y_hat): - cov = np.diag(std[key]**2) - rv = stats.multivariate_normal(mean=y_hat[key], cov=cov) - Y_MC[key] = rv.rvs(size=mc_size) - logPriorLikelihoods += rv.logpdf(Y_MC[key]) - std_MC[key] = np.zeros((mc_size, y_hat[key].shape[0])) - - # Likelihood computation (Comparison of data and simulation - # results via PCE with candidate design) - likelihoods = self.__normpdf(Y_MC, std_MC, obs_data, sigma2Dict) - - # Rejection Step - # Random numbers between 0 and 1 - unif = np.random.rand(1, mc_size)[0] - - # Reject the poorly performed prior - accepted = (likelihoods/np.max(likelihoods)) >= unif - - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods), dtype=np.longdouble) - - # Posterior-based expectation of likelihoods - postLikelihoods = likelihoods[accepted] - postExpLikelihoods = np.mean(np.log(postLikelihoods)) - - # Posterior-based expectation of prior densities - postExpPrior = np.mean(logPriorLikelihoods[accepted]) - - # Utility function Eq.2 in Ref. (2) - # Posterior covariance matrix after observing data y - # Kullback-Leibler Divergence (Sergey's paper) - if var == 'DKL': - - # TODO: Calculate the correction factor for BME - # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can, - # ObservationData, sigma2Dict) - # BME += BMECorrFactor - # Haun et al implementation - # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME) - U_J_d = postExpLikelihoods - logBME - - # Marginal log likelihood - elif var == 'BME': - U_J_d = np.nanmean(likelihoods) - - # Entropy-based information gain - elif var == 'infEntropy': - logBME = np.log(np.nanmean(likelihoods)) - infEntropy = logBME - postExpPrior - postExpLikelihoods - U_J_d = infEntropy * -1 # -1 for minimization - - # Bayesian information criterion - elif var == 'BIC': - coeffs = self.MetaModel.coeffs_dict.values() - nModelParams = max(len(v) for val in coeffs for v in val.values()) - maxL = np.nanmax(likelihoods) - U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams - - # Akaike information criterion - elif var == 'AIC': - coeffs = self.MetaModel.coeffs_dict.values() - nModelParams = max(len(v) for val in coeffs for v in val.values()) - maxlogL = np.log(np.nanmax(likelihoods)) - AIC = -2 * maxlogL + 2 * nModelParams - # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1) - penTerm = 0 - U_J_d = 1*(AIC + penTerm) - - # Deviance information criterion - elif var == 'DIC': - # D_theta_bar = np.mean(-2 * Likelihoods) - N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0])) - Likelihoods_theta_mean = self.__normpdf( - y_hat, std, obs_data, sigma2Dict - ) - DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p - - U_J_d = DIC - - else: - print('The algorithm you requested has not been implemented yet!') - - # Handle inf and NaN (replace by zero) - if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf: - U_J_d = 0.0 - - # Clear memory - del likelihoods - del Y_MC - del std_MC - - return -1 * U_J_d # -1 is for minimization instead of maximization - - # ------------------------------------------------------------------------- - def update_metamodel(self, MetaModel, output, y_hat_can, univ_p_val, index, - new_pca=False): - BasisIndices = MetaModel.basis_dict[output]["y_"+str(index+1)] - clf_poly = MetaModel.clf_poly[output]["y_"+str(index+1)] - Mn = clf_poly.coef_ - Sn = clf_poly.sigma_ - beta = clf_poly.alpha_ - active = clf_poly.active_ - Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val) - - Sn_new_inv = np.linalg.inv(Sn) - Sn_new_inv += beta * np.dot(Psi[:, active].T, Psi[:, active]) - Sn_new = np.linalg.inv(Sn_new_inv) - - Mn_new = np.dot(Sn_new_inv, Mn[active]).reshape(-1, 1) - Mn_new += beta * np.dot(Psi[:, active].T, y_hat_can) - Mn_new = np.dot(Sn_new, Mn_new).flatten() - - # Compute the old and new moments of PCEs - mean_old = Mn[0] - mean_new = Mn_new[0] - std_old = np.sqrt(np.sum(np.square(Mn[1:]))) - std_new = np.sqrt(np.sum(np.square(Mn_new[1:]))) - - # Back transformation if PCA is selected. - if MetaModel.dim_red_method.lower() == 'pca': - old_pca = MetaModel.pca[output] - mean_old = old_pca.mean_[index] - mean_old += np.sum(mean_old * old_pca.components_[:, index]) - std_old = np.sqrt(np.sum(std_old**2 * - old_pca.components_[:, index]**2)) - mean_new = new_pca.mean_[index] - mean_new += np.sum(mean_new * new_pca.components_[:, index]) - std_new = np.sqrt(np.sum(std_new**2 * - new_pca.components_[:, index]**2)) - # print(f"mean_old: {mean_old:.2f} mean_new: {mean_new:.2f}") - # print(f"std_old: {std_old:.2f} std_new: {std_new:.2f}") - # Store the old and new moments of PCEs - results = { - 'mean_old': mean_old, - 'mean_new': mean_new, - 'std_old': std_old, - 'std_new': std_new - } - return results - - # ------------------------------------------------------------------------- - def util_BayesianDesign_old(self, X_can, X_MC, sigma2Dict, var='DKL'): - """ - Computes scores based on Bayesian sequential design criterion (var). - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - var : string, optional - Bayesian design criterion. The default is 'DKL'. - - Returns - ------- - float - Score. - - """ - - # To avoid changes ub original aPCE object - Model = self.Model - MetaModel = deepcopy(self.MetaModel) - old_EDY = MetaModel.ExpDesign.Y - - # Evaluate the PCE metamodels using the candidate design - Y_PC_can, Y_std_can = self.MetaModel.eval_metamodel( - samples=np.array([X_can]) - ) - - # Generate y from posterior predictive - m_size = 100 - y_hat_samples = {} - for idx, key in enumerate(Model.Output.names): - means, stds = Y_PC_can[key][0], Y_std_can[key][0] - y_hat_samples[key] = np.random.multivariate_normal( - means, np.diag(stds), m_size) - - # Create the SparseBayes-based PCE metamodel: - MetaModel.input_obj.poly_coeffs_flag = False - univ_p_val = self.MetaModel.univ_basis_vals(X_can) - G_n_m_all = np.zeros((m_size, len(Model.Output.names), Model.n_obs)) - - for i in range(m_size): - for idx, key in enumerate(Model.Output.names): - if MetaModel.dim_red_method.lower() == 'pca': - # Equal number of components - new_outputs = np.vstack( - (old_EDY[key], y_hat_samples[key][i]) - ) - new_pca, _ = MetaModel.pca_transformation(new_outputs) - target = new_pca.transform( - y_hat_samples[key][i].reshape(1, -1) - )[0] - else: - new_pca, target = False, y_hat_samples[key][i] - - for j in range(len(target)): - - # Update surrogate - result = self.update_metamodel( - MetaModel, key, target[j], univ_p_val, j, new_pca) - - # Compute Expected Information Gain (Eq. 39) - G_n_m = np.log(result['std_old']/result['std_new']) - 1./2 - G_n_m += result['std_new']**2 / (2*result['std_old']**2) - G_n_m += (result['mean_new'] - result['mean_old'])**2 /\ - (2*result['std_old']**2) - - G_n_m_all[i, idx, j] = G_n_m - - U_J_d = G_n_m_all.mean(axis=(1, 2)).mean() - return -1 * U_J_d - - # ------------------------------------------------------------------------- - def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'): - """ - Computes scores based on Bayesian sequential design criterion (var). - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - var : string, optional - Bayesian design criterion. The default is 'DKL'. - - Returns - ------- - float - Score. - - """ - - # To avoid changes ub original aPCE object - MetaModel = self.MetaModel - out_names = MetaModel.ModelObj.Output.names - if X_can.ndim == 1: - X_can = X_can.reshape(1, -1) - - # Compute the mean and std based on the MetaModel - # pce_means, pce_stds = self._compute_pce_moments(MetaModel) - if var == 'ALC': - Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC) - - # Old Experimental design - oldExpDesignX = MetaModel.ExpDesign.X - oldExpDesignY = MetaModel.ExpDesign.Y - - # Evaluate the PCE metamodels at that location ??? - Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can) - PCE_Model_can = deepcopy(MetaModel) - # Add the candidate to the ExpDesign - NewExpDesignX = np.vstack((oldExpDesignX, X_can)) - - NewExpDesignY = {} - for key in oldExpDesignY.keys(): - NewExpDesignY[key] = np.vstack( - (oldExpDesignY[key], Y_PC_can[key]) - ) - - PCE_Model_can.ExpDesign.sampling_method = 'user' - PCE_Model_can.ExpDesign.X = NewExpDesignX - PCE_Model_can.ModelOutputDict = NewExpDesignY - PCE_Model_can.ExpDesign.Y = NewExpDesignY - - # Train the model for the observed data using x_can - PCE_Model_can.input_obj.poly_coeffs_flag = False - PCE_Model_can.train_norm_design(parallel=False) - - # Set the ExpDesign to its original values - PCE_Model_can.ExpDesign.X = oldExpDesignX - PCE_Model_can.ModelOutputDict = oldExpDesignY - PCE_Model_can.ExpDesign.Y = oldExpDesignY - - if var.lower() == 'mi': - # Mutual information based on Krause et al - # Adapted from Beck & Guillas (MICE) paper - _, std_PC_can = PCE_Model_can.eval_metamodel(samples=X_can) - std_can = {key: std_PC_can[key] for key in out_names} - - std_old = {key: Y_std_can[key] for key in out_names} - - varPCE = np.zeros((len(out_names))) - for i, key in enumerate(out_names): - varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2) - score = np.mean(varPCE) - - return -1 * score - - elif var.lower() == 'alc': - # Active learning based on Gramyc and Lee - # Adaptive design and analysis of supercomputer experiments Techno- - # metrics, 51 (2009), pp. 130–145. - - # Evaluate the MetaModel at the given samples - Y_MC_can, Y_MC_std_can = PCE_Model_can.eval_metamodel(samples=X_MC) - - # Compute the score - score = [] - for i, key in enumerate(out_names): - pce_var = Y_MC_std_can[key]**2 - pce_var_can = Y_MC_std[key]**2 - score.append(np.mean(pce_var-pce_var_can, axis=0)) - score = np.mean(score) - - return -1 * score - - # ---------- Inner MC simulation for computing Utility Value ---------- - # Estimation of the integral via Monte Varlo integration - MCsize = X_MC.shape[0] - ESS = 0 - - while ((ESS > MCsize) or (ESS < 1)): - - # Enriching Monte Carlo samples if need be - if ESS != 0: - X_MC = self.MetaModel.ExpDesign.generate_samples( - MCsize, 'random' - ) - - # Evaluate the MetaModel at the given samples - Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC) - - # Likelihood computation (Comparison of data and simulation - # results via PCE with candidate design) - likelihoods = self.__normpdf( - Y_MC, std_MC, self.observations, sigma2Dict - ) - - # Check the Effective Sample Size (1<ESS<MCsize) - ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods))) - - # Enlarge sample size if it doesn't fulfill the criteria - if ((ESS > MCsize) or (ESS < 1)): - print("--- increasing MC size---") - MCsize *= 10 - ESS = 0 - - # Rejection Step - # Random numbers between 0 and 1 - unif = np.random.rand(1, MCsize)[0] - - # Reject the poorly performed prior - accepted = (likelihoods/np.max(likelihoods)) >= unif - - # -------------------- Utility functions -------------------- - # Utility function Eq.2 in Ref. (2) - # Kullback-Leibler Divergence (Sergey's paper) - if var == 'DKL': - - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods, dtype=np.longdouble)) - - # Posterior-based expectation of likelihoods - postLikelihoods = likelihoods[accepted] - postExpLikelihoods = np.mean(np.log(postLikelihoods)) - - # Haun et al implementation - U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME) - - # U_J_d = np.sum(G_n_m_all) - # Ryan et al (2014) implementation - # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0]) - # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME - - # U_J_d = postExpLikelihoods - logBME - - # Marginal likelihood - elif var == 'BME': - - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods)) - U_J_d = logBME - - # Bayes risk likelihood - elif var == 'BayesRisk': - - U_J_d = -1 * np.var(likelihoods) - - # Entropy-based information gain - elif var == 'infEntropy': - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods)) - - # Posterior-based expectation of likelihoods - postLikelihoods = likelihoods[accepted] - postLikelihoods /= np.nansum(likelihoods[accepted]) - postExpLikelihoods = np.mean(np.log(postLikelihoods)) - - # Posterior-based expectation of prior densities - postExpPrior = np.mean(logPriorLikelihoods[accepted]) - - infEntropy = logBME - postExpPrior - postExpLikelihoods - - U_J_d = infEntropy * -1 # -1 for minimization - - # D-Posterior-precision - elif var == 'DPP': - X_Posterior = X_MC[accepted] - # covariance of the posterior parameters - U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior))) - - # A-Posterior-precision - elif var == 'APP': - X_Posterior = X_MC[accepted] - # trace of the posterior parameters - U_J_d = -np.log(np.trace(np.cov(X_Posterior))) - - else: - print('The algorithm you requested has not been implemented yet!') - - # Clear memory - del likelihoods - del Y_MC - del std_MC - - return -1 * U_J_d # -1 is for minimization instead of maximization - - # ------------------------------------------------------------------------- - def subdomain(self, Bounds, n_new_samples): - """ - Divides a domain defined by Bounds into sub domains. - - Parameters - ---------- - Bounds : list of tuples - List of lower and upper bounds. - n_new_samples : TYPE - DESCRIPTION. - - Returns - ------- - Subdomains : TYPE - DESCRIPTION. - - """ - n_params = self.MetaModel.n_params - n_subdomains = n_new_samples + 1 - LinSpace = np.zeros((n_params, n_subdomains)) - - for i in range(n_params): - LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1], - num=n_subdomains) - Subdomains = [] - for k in range(n_subdomains-1): - mylist = [] - for i in range(n_params): - mylist.append((LinSpace[i, k+0], LinSpace[i, k+1])) - Subdomains.append(tuple(mylist)) - - return Subdomains - - # ------------------------------------------------------------------------- - def run_util_func(self, method, candidates, index, sigma2Dict=None, - var=None, X_MC=None): - """ - Runs the utility function based on the given method. - - Parameters - ---------- - method : string - Exploitation method: `VarOptDesign`, `BayesActDesign` and - `BayesOptDesign`. - candidates : array of shape (n_samples, n_params) - All candidate parameter sets. - index : int - ExpDesign index. - sigma2Dict : dict, optional - A dictionary containing the measurement errors (sigma^2). The - default is None. - var : string, optional - Utility function. The default is None. - X_MC : TYPE, optional - DESCRIPTION. The default is None. - - Returns - ------- - index : TYPE - DESCRIPTION. - List - Scores. - - """ - - if method.lower() == 'varoptdesign': - # U_J_d = self.util_VarBasedDesign(candidates, index, var) - U_J_d = np.zeros((candidates.shape[0])) - for idx, X_can in tqdm(enumerate(candidates), ascii=True, - desc="varoptdesign"): - U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var) - - elif method.lower() == 'bayesactdesign': - NCandidate = candidates.shape[0] - U_J_d = np.zeros((NCandidate)) - # Evaluate all candidates - y_can, std_can = self.MetaModel.eval_metamodel(samples=candidates) - # loop through candidates - for idx, X_can in tqdm(enumerate(candidates), ascii=True, - desc="BAL Design"): - y_hat = {key: items[idx] for key, items in y_can.items()} - std = {key: items[idx] for key, items in std_can.items()} - U_J_d[idx] = self.util_BayesianActiveDesign( - y_hat, std, sigma2Dict, var) - - elif method.lower() == 'bayesoptdesign': - NCandidate = candidates.shape[0] - U_J_d = np.zeros((NCandidate)) - for idx, X_can in tqdm(enumerate(candidates), ascii=True, - desc="OptBayesianDesign"): - U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict, - var) - return (index, -1 * U_J_d) - - # ------------------------------------------------------------------------- - def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No, - verbose=False): - """ - Exploration algorithim to find the optimum parameter space. - - Parameters - ---------- - method : string - Exploitation method: `VarOptDesign`, `BayesActDesign` and - `BayesOptDesign`. - Bounds : list of tuples - List of lower and upper boundaries of parameters. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - Run_No : int - Run number. - verbose : bool, optional - Print out a summary. The default is False. - - Returns - ------- - Run_No : int - Run number. - array - Optimial candidate. - - """ - - Model = self.Model - max_func_itr = self.MetaModel.ExpDesign.max_func_itr - - if method == 'VarOptDesign': - Res_Global = opt.dual_annealing(self.util_VarBasedDesign, - bounds=Bounds, - args=(Model, var), - maxfun=max_func_itr) - - elif method == 'BayesOptDesign': - Res_Global = opt.dual_annealing(self.util_BayesianDesign, - bounds=Bounds, - args=(Model, sigma2Dict, var), - maxfun=max_func_itr) - - if verbose: - print(f"global minimum: xmin = {Res_Global.x}, " - f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}") - - return (Run_No, Res_Global.x) - - # ------------------------------------------------------------------------- - def tradoff_weights(self, tradeoff_scheme, old_EDX, old_EDY): - """ - Calculates weights for exploration scores based on the requested - scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`. - - `None`: No exploration. - `equal`: Same weights for exploration and exploitation scores. - `epsilon-decreasing`: Start with more exploration and increase the - influence of exploitation along the way with a exponential decay - function - `adaptive`: An adaptive method based on: - Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling - approach for Kriging metamodeling by maximizing expected prediction - error." Computers & Chemical Engineering 106 (2017): 171-182. - - Parameters - ---------- - tradeoff_scheme : string - Trade-off scheme for exloration and exploitation scores. - old_EDX : array (n_samples, n_params) - Old experimental design (training points). - old_EDY : dict - Old model responses (targets). - - Returns - ------- - exploration_weight : float - Exploration weight. - exploitation_weight: float - Exploitation weight. - - """ - if tradeoff_scheme is None: - exploration_weight = 0 - - elif tradeoff_scheme == 'equal': - exploration_weight = 0.5 - - elif tradeoff_scheme == 'epsilon-decreasing': - # epsilon-decreasing scheme - # Start with more exploration and increase the influence of - # exploitation along the way with a exponential decay function - initNSamples = self.MetaModel.ExpDesign.n_init_samples - n_max_samples = self.MetaModel.ExpDesign.n_max_samples - - itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples) - itrNumber //= self.MetaModel.ExpDesign.n_new_samples - - tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8) - exploration_weight = signal.exponential(n_max_samples-initNSamples, - 0, tau2, False)[itrNumber] - - elif tradeoff_scheme == 'adaptive': - - # Extract itrNumber - initNSamples = self.MetaModel.ExpDesign.n_init_samples - n_max_samples = self.MetaModel.ExpDesign.n_max_samples - itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples) - itrNumber //= self.MetaModel.ExpDesign.n_new_samples - - if itrNumber == 0: - exploration_weight = 0.5 - else: - # New adaptive trade-off according to Liu et al. (2017) - # Mean squared error for last design point - last_EDX = old_EDX[-1].reshape(1, -1) - lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX) - pce_y = np.array(list(lastPCEY.values()))[:, 0] - y = np.array(list(old_EDY.values()))[:, -1, :] - mseError = mean_squared_error(pce_y, y) - - # Mean squared CV - error for last design point - pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0] - mseCVError = mean_squared_error(pce_y_prev, y) - - exploration_weight = min([0.5*mseError/mseCVError, 1]) - - # Exploitation weight - exploitation_weight = 1 - exploration_weight - - return exploration_weight, exploitation_weight - - # ------------------------------------------------------------------------- - def opt_SeqDesign(self, sigma2, n_candidates=5, var='DKL'): - """ - Runs optimal sequential design. - - Parameters - ---------- - sigma2 : dict, optional - A dictionary containing the measurement errors (sigma^2). The - default is None. - n_candidates : int, optional - Number of candidate samples. The default is 5. - var : string, optional - Utility function. The default is None. - - Raises - ------ - NameError - Wrong utility function. - - Returns - ------- - Xnew : array (n_samples, n_params) - Selected new training point(s). - """ - - # Initialization - MetaModel = self.MetaModel - Bounds = MetaModel.bound_tuples - n_new_samples = MetaModel.ExpDesign.n_new_samples - explore_method = MetaModel.ExpDesign.explore_method - exploit_method = MetaModel.ExpDesign.exploit_method - n_cand_groups = MetaModel.ExpDesign.n_cand_groups - tradeoff_scheme = MetaModel.ExpDesign.tradeoff_scheme - - old_EDX = MetaModel.ExpDesign.X - old_EDY = MetaModel.ExpDesign.Y.copy() - ndim = MetaModel.ExpDesign.X.shape[1] - OutputNames = MetaModel.ModelObj.Output.names - - # ----------------------------------------- - # ----------- CUSTOMIZED METHODS ---------- - # ----------------------------------------- - # Utility function exploit_method provided by user - if exploit_method.lower() == 'user': - - Xnew, filteredSamples = MetaModel.ExpDesign.ExploitFunction(self) - - print("\n") - print("\nXnew:\n", Xnew) - - return Xnew, filteredSamples - - # ----------------------------------------- - # ---------- EXPLORATION METHODS ---------- - # ----------------------------------------- - if explore_method == 'dual annealing': - # ------- EXPLORATION: OPTIMIZATION ------- - import time - start_time = time.time() - - # Divide the domain to subdomains - args = [] - subdomains = self.subdomain(Bounds, n_new_samples) - for i in range(n_new_samples): - args.append((exploit_method, subdomains[i], sigma2, var, i)) - - # Multiprocessing - pool = multiprocessing.Pool(multiprocessing.cpu_count()) - - # With Pool.starmap_async() - results = pool.starmap_async(self.dual_annealing, args).get() - - # Close the pool - pool.close() - - Xnew = np.array([results[i][1] for i in range(n_new_samples)]) - - print("\nXnew:\n", Xnew) - - elapsed_time = time.time() - start_time - print("\n") - print(f"elapsed_time: {round(elapsed_time,2)} sec.") - print('-'*20) - - elif explore_method == 'LOOCV': - # ----------------------------------------------------------------- - # TODO: LOOCV model construnction based on Feng et al. (2020) - # 'LOOCV': - # Initilize the ExploitScore array - - # Generate random samples - allCandidates = MetaModel.ExpDesign.generate_samples(n_candidates, - 'random') - - # Construct error model based on LCerror - errorModel = MetaModel.create_ModelError(old_EDX, self.LCerror) - self.errorModel.append(copy(errorModel)) - - # Evaluate the error models for allCandidates - eLCAllCands, _ = errorModel.eval_errormodel(allCandidates) - # Select the maximum as the representative error - eLCAllCands = np.dstack(eLCAllCands.values()) - eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0] - - # Normalize the error w.r.t the maximum error - scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates) - - else: - # ------- EXPLORATION: SPACE-FILLING DESIGN ------- - # Generate candidate samples from Exploration class - explore = Exploration(MetaModel, n_candidates) - explore.w = 100 # * ndim #500 - # Select criterion (mc-intersite-proj-th, mc-intersite-proj) - explore.mc_criterion = 'mc-intersite-proj' - allCandidates, scoreExploration = explore.get_exploration_samples() - - # Temp: ---- Plot all candidates ----- - if ndim == 2: - def plotter(points, allCandidates, Method, - scoreExploration=None): - if Method == 'Voronoi': - from scipy.spatial import Voronoi, voronoi_plot_2d - vor = Voronoi(points) - fig = voronoi_plot_2d(vor) - ax1 = fig.axes[0] - else: - fig = plt.figure() - ax1 = fig.add_subplot(111) - ax1.scatter(points[:, 0], points[:, 1], s=10, c='r', - marker="s", label='Old Design Points') - ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10, - c='b', marker="o", label='Design candidates') - for i in range(points.shape[0]): - txt = 'p'+str(i+1) - ax1.annotate(txt, (points[i, 0], points[i, 1])) - if scoreExploration is not None: - for i in range(allCandidates.shape[0]): - txt = str(round(scoreExploration[i], 5)) - ax1.annotate(txt, (allCandidates[i, 0], - allCandidates[i, 1])) - - plt.xlim(self.bound_tuples[0]) - plt.ylim(self.bound_tuples[1]) - # plt.show() - plt.legend(loc='upper left') - - # ----------------------------------------- - # --------- EXPLOITATION METHODS ---------- - # ----------------------------------------- - if exploit_method == 'BayesOptDesign' or\ - exploit_method == 'BayesActDesign': - - # ------- Calculate Exoploration weight ------- - # Compute exploration weight based on trade off scheme - explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme, - old_EDX, - old_EDY) - print(f"\n Exploration weight={explore_w:0.3f} " - f"Exploitation weight={exploit_w:0.3f}\n") - - # ------- EXPLOITATION: BayesOptDesign & ActiveLearning ------- - if explore_w != 1.0: - - # Create a sample pool for rejection sampling - MCsize = 15000 - X_MC = MetaModel.ExpDesign.generate_samples(MCsize, 'random') - candidates = MetaModel.ExpDesign.generate_samples( - MetaModel.ExpDesign.max_func_itr, 'latin_hypercube') - - # Split the candidates in groups for multiprocessing - split_cand = np.array_split( - candidates, n_cand_groups, axis=0 - ) - - results = Parallel(n_jobs=-1, backend='multiprocessing')( - delayed(self.run_util_func)( - exploit_method, split_cand[i], i, sigma2, var, X_MC) - for i in range(n_cand_groups)) - # out = map(self.run_util_func, - # [exploit_method]*n_cand_groups, - # split_cand, - # range(n_cand_groups), - # [sigma2] * n_cand_groups, - # [var] * n_cand_groups, - # [X_MC] * n_cand_groups - # ) - # results = list(out) - - # Retrieve the results and append them - U_J_d = np.concatenate([results[NofE][1] for NofE in - range(n_cand_groups)]) - - # Check if all scores are inf - if np.isinf(U_J_d).all() or np.isnan(U_J_d).all(): - U_J_d = np.ones(len(U_J_d)) - - # Get the expected value (mean) of the Utility score - # for each cell - if explore_method == 'Voronoi': - U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1) - - # create surrogate model for U_J_d - # from sklearn.preprocessing import MinMaxScaler - # # Take care of inf entries - # good_indices = [i for i, arr in enumerate(U_J_d) - # if np.isfinite(arr).all()] - # scaler = MinMaxScaler() - # X_S = scaler.fit_transform(candidates[good_indices]) - # gp = MetaModel.gaussian_process_emulator( - # X_S, U_J_d[good_indices], autoSelect=False - # ) - # U_J_d = gp.predict(scaler.transform(allCandidates)) - - # Normalize U_J_d - norm_U_J_d = U_J_d / np.sum(U_J_d) - else: - norm_U_J_d = np.zeros((len(scoreExploration))) - - # ------- Calculate Total score ------- - # ------- Trade off between EXPLORATION & EXPLOITATION ------- - # Accumulate the samples - # TODO: added this, recheck!! - finalCandidates = np.concatenate((allCandidates, candidates), axis = 0) - finalCandidates = np.unique(finalCandidates, axis = 0) - - # TODO: changed this from the above to take into account both exploration and exploitation samples without duplicates - totalScore = np.zeros(finalCandidates.shape[0]) - #self.totalScore = totalScore - - for cand_idx in range(finalCandidates.shape[0]): - # find candidate indices - idx1 = np.where(allCandidates == finalCandidates[cand_idx])[0] - idx2 = np.where(candidates == finalCandidates[cand_idx])[0] - - # exploration - if idx1 != []: - idx1 = idx1[0] - totalScore[cand_idx] += explore_w * scoreExploration[idx1] - - # exploitation - if idx2 != []: - idx2 = idx2[0] - totalScore[cand_idx] += exploit_w * norm_U_J_d[idx2] - - - # temp: Plot - # dim = self.ExpDesign.X.shape[1] - # if dim == 2: - # plotter(self.ExpDesign.X, allCandidates, explore_method) - - # ------- Select the best candidate ------- - # find an optimal point subset to add to the initial design by - # maximization of the utility score and taking care of NaN values - temp = totalScore.copy() - temp[np.isnan(totalScore)] = -np.inf - sorted_idxtotalScore = np.argsort(temp)[::-1] - bestIdx = sorted_idxtotalScore[:n_new_samples] - - # select the requested number of samples - if explore_method == 'Voronoi': - Xnew = np.zeros((n_new_samples, ndim)) - for i, idx in enumerate(bestIdx): - X_can = explore.closestPoints[idx] - - # Calculate the maxmin score for the region of interest - newSamples, maxminScore = explore.get_mc_samples(X_can) - - # select the requested number of samples - Xnew[i] = newSamples[np.argmax(maxminScore)] - else: - # TODO: changed this from allCandiates to full set of candidates - still not changed for e.g. 'Voronoi' - Xnew = finalCandidates[sorted_idxtotalScore[:n_new_samples]] # here candidates(exploitation) vs allCandidates (exploration)!! - - elif exploit_method == 'VarOptDesign': - # ------- EXPLOITATION: VarOptDesign ------- - UtilMethod = var - - # ------- Calculate Exoploration weight ------- - # Compute exploration weight based on trade off scheme - explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme, - old_EDX, - old_EDY) - print(f"\nweightExploration={explore_w:0.3f} " - f"weightExploitation={exploit_w:0.3f}") - - # Generate candidate samples from Exploration class - nMeasurement = old_EDY[OutputNames[0]].shape[1] - - # Find sensitive region - if UtilMethod == 'LOOCV': - LCerror = MetaModel.LCerror - allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames), - nMeasurement)) - for y_idx, y_key in enumerate(OutputNames): - for idx, key in enumerate(LCerror[y_key].keys()): - allModifiedLOO[:, y_idx, idx] = abs( - LCerror[y_key][key]) - - ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1) - - elif UtilMethod in ['EIGF', 'ALM']: - # ----- All other in ['EIGF', 'ALM'] ----- - # Initilize the ExploitScore array - ExploitScore = np.zeros((len(old_EDX), len(OutputNames))) - - # Split the candidates in groups for multiprocessing - if explore_method != 'Voronoi': - split_cand = np.array_split(allCandidates, - n_cand_groups, - axis=0) - goodSampleIdx = range(n_cand_groups) - else: - # Find indices of the Vornoi cells with samples - goodSampleIdx = [] - for idx in range(len(explore.closest_points)): - if len(explore.closest_points[idx]) != 0: - goodSampleIdx.append(idx) - split_cand = explore.closest_points - - # Split the candidates in groups for multiprocessing - args = [] - for index in goodSampleIdx: - args.append((exploit_method, split_cand[index], index, - sigma2, var)) - - # Multiprocessing - pool = multiprocessing.Pool(multiprocessing.cpu_count()) - # With Pool.starmap_async() - results = pool.starmap_async(self.run_util_func, args).get() - - # Close the pool - pool.close() - # out = map(self.run_util_func, - # [exploit_method]*len(goodSampleIdx), - # split_cand, - # range(len(goodSampleIdx)), - # [sigma2] * len(goodSampleIdx), - # [var] * len(goodSampleIdx) - # ) - # results = list(out) - - # Retrieve the results and append them - if explore_method == 'Voronoi': - ExploitScore = [np.mean(results[k][1]) for k in - range(len(goodSampleIdx))] - else: - ExploitScore = np.concatenate( - [results[k][1] for k in range(len(goodSampleIdx))]) - - else: - raise NameError('The requested utility function is not ' - 'available.') - - # find an optimal point subset to add to the initial design by - # maximization of the utility score and taking care of NaN values - # Total score - # Normalize U_J_d - ExploitScore = ExploitScore / np.sum(ExploitScore) - totalScore = exploit_w * ExploitScore - totalScore += explore_w * scoreExploration - - temp = totalScore.copy() - sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1] - bestIdx = sorted_idxtotalScore[:n_new_samples] - - Xnew = np.zeros((n_new_samples, ndim)) - if explore_method != 'Voronoi': - Xnew = allCandidates[bestIdx] - else: - for i, idx in enumerate(bestIdx.flatten()): - X_can = explore.closest_points[idx] - # plotter(self.ExpDesign.X, X_can, explore_method, - # scoreExploration=None) - - # Calculate the maxmin score for the region of interest - newSamples, maxminScore = explore.get_mc_samples(X_can) - - # select the requested number of samples - Xnew[i] = newSamples[np.argmax(maxminScore)] - - elif exploit_method == 'alphabetic': - # ------- EXPLOITATION: ALPHABETIC ------- - Xnew = self.util_AlphOptDesign(allCandidates, var) - - elif exploit_method == 'Space-filling': - # ------- EXPLOITATION: SPACE-FILLING ------- - totalScore = scoreExploration - - # ------- Select the best candidate ------- - # find an optimal point subset to add to the initial design by - # maximization of the utility score and taking care of NaN values - temp = totalScore.copy() - temp[np.isnan(totalScore)] = -np.inf - sorted_idxtotalScore = np.argsort(temp)[::-1] - - # select the requested number of samples - Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]] - - else: - raise NameError('The requested design method is not available.') - - print("\n") - print("\nRun No. {}:".format(old_EDX.shape[0]+1)) - print("Xnew:\n", Xnew) - - return Xnew, None - - # ------------------------------------------------------------------------- - def util_AlphOptDesign(self, candidates, var='D-Opt'): - """ - Enriches the Experimental design with the requested alphabetic - criterion based on exploring the space with number of sampling points. - - Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos - expansion: A review of sampling strategies., Computer Methods in - Applied Mechanics and Engineering, 332, 382-407. - - Arguments - --------- - NCandidate : int - Number of candidate points to be searched - - var : string - Alphabetic optimality criterion - - Returns - ------- - X_new : array of shape (1, n_params) - The new sampling location in the input space. - """ - MetaModelOrig = self - Model = self.Model - n_new_samples = MetaModelOrig.ExpDesign.n_new_samples - NCandidate = candidates.shape[0] - - # TODO: Loop over outputs - OutputName = Model.Output.names[0] - - # To avoid changes ub original aPCE object - MetaModel = deepcopy(MetaModelOrig) - - # Old Experimental design - oldExpDesignX = MetaModel.ExpDesign.X - - # TODO: Only one psi can be selected. - # Suggestion: Go for the one with the highest LOO error - Scores = list(MetaModel.score_dict[OutputName].values()) - ModifiedLOO = [1-score for score in Scores] - outIdx = np.argmax(ModifiedLOO) - - # Initialize Phi to save the criterion's values - Phi = np.zeros((NCandidate)) - - BasisIndices = MetaModelOrig.basis_dict[OutputName]["y_"+str(outIdx+1)] - P = len(BasisIndices) - - # ------ Old Psi ------------ - univ_p_val = MetaModelOrig.univ_basis_vals(oldExpDesignX) - Psi = MetaModelOrig.create_psi(BasisIndices, univ_p_val) - - # ------ New candidates (Psi_c) ------------ - # Assemble Psi_c - univ_p_val_c = self.univ_basis_vals(candidates) - Psi_c = self.create_psi(BasisIndices, univ_p_val_c) - - for idx in range(NCandidate): - - # Include the new row to the original Psi - Psi_cand = np.vstack((Psi, Psi_c[idx])) - - # Information matrix - PsiTPsi = np.dot(Psi_cand.T, Psi_cand) - M = PsiTPsi / (len(oldExpDesignX)+1) - - if np.linalg.cond(PsiTPsi) > 1e-12 \ - and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon: - # faster - invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray()) - else: - # stabler - invM = np.linalg.pinv(M) - - # ---------- Calculate optimality criterion ---------- - # Optimality criteria according to Section 4.5.1 in Ref. - - # D-Opt - if var == 'D-Opt': - Phi[idx] = (np.linalg.det(invM)) ** (1/P) - - # A-Opt - elif var == 'A-Opt': - Phi[idx] = np.trace(invM) - - # K-Opt - elif var == 'K-Opt': - Phi[idx] = np.linalg.cond(M) - - else: - raise Exception('The optimality criterion you requested has ' - 'not been implemented yet!') - - # find an optimal point subset to add to the initial design - # by minimization of the Phi - sorted_idxtotalScore = np.argsort(Phi) - - # select the requested number of samples - Xnew = candidates[sorted_idxtotalScore[:n_new_samples]] - - return Xnew - - # ------------------------------------------------------------------------- - def __normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s, - rmse=None): - - Model = self.Model - likelihoods = 1.0 - - # Loop over the outputs - for idx, out in enumerate(Model.Output.names): - - # (Meta)Model Output - nsamples, nout = y_hat_pce[out].shape - - # Prepare data and remove NaN - try: - data = obs_data[out].values[~np.isnan(obs_data[out])] - except AttributeError: - data = obs_data[out][~np.isnan(obs_data[out])] - - # Prepare sigma2s - non_nan_indices = ~np.isnan(total_sigma2s[out]) - tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values - - # Surrogate error if valid dataset is given. - if rmse is not None: - tot_sigma2s += rmse[out]**2 - else: - tot_sigma2s += np.mean(std_pce[out])**2 - - likelihoods *= stats.multivariate_normal.pdf( - y_hat_pce[out], data, np.diag(tot_sigma2s), - allow_singular=True) - - self.Likelihoods = likelihoods - - return likelihoods - - # ------------------------------------------------------------------------- - def __corr_factor_BME(self, obs_data, total_sigma2s, logBME): - """ - Calculates the correction factor for BMEs. - """ - MetaModel = self.MetaModel - samples = MetaModel.ExpDesign.X # valid_samples - model_outputs = MetaModel.ExpDesign.Y # valid_model_runs - Model = MetaModel.ModelObj - n_samples = samples.shape[0] - - # Extract the requested model outputs for likelihood calulation - output_names = Model.Output.names - - # TODO: Evaluate MetaModel on the experimental design and ValidSet - OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples) - - logLik_data = np.zeros((n_samples)) - logLik_model = np.zeros((n_samples)) - # Loop over the outputs - for idx, out in enumerate(output_names): - - # (Meta)Model Output - nsamples, nout = model_outputs[out].shape - - # Prepare data and remove NaN - try: - data = obs_data[out].values[~np.isnan(obs_data[out])] - except AttributeError: - data = obs_data[out][~np.isnan(obs_data[out])] - - # Prepare sigma2s - non_nan_indices = ~np.isnan(total_sigma2s[out]) - tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout] - - # Covariance Matrix - covMatrix_data = np.diag(tot_sigma2s) - - for i, sample in enumerate(samples): - - # Simulation run - y_m = model_outputs[out][i] - - # Surrogate prediction - y_m_hat = OutputRS[out][i] - - # CovMatrix with the surrogate error - # covMatrix = np.diag(stdOutputRS[out][i]**2) - covMatrix = np.diag((y_m-y_m_hat)**2) - covMatrix = np.diag( - np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2 - ) - - # Compute likelilhood output vs data - logLik_data[i] += self.__logpdf( - y_m_hat, data, covMatrix_data - ) - - # Compute likelilhood output vs surrogate - logLik_model[i] += self.__logpdf(y_m_hat, y_m, covMatrix) - - # Weight - logLik_data -= logBME - weights = np.exp(logLik_model+logLik_data) - - return np.log(np.mean(weights)) - - # ------------------------------------------------------------------------- - def __logpdf(self, x, mean, cov): - """ - computes the likelihood based on a multivariate normal distribution. - - Parameters - ---------- - x : TYPE - DESCRIPTION. - mean : array_like - Observation data. - cov : 2d array - Covariance matrix of the distribution. - - Returns - ------- - log_lik : float - Log likelihood. - - """ - n = len(mean) - L = linalg.cholesky(cov, lower=True) - beta = np.sum(np.log(np.diag(L))) - dev = x - mean - alpha = dev.dot(linalg.cho_solve((L, True), dev)) - log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi) - - return log_lik - - # ------------------------------------------------------------------------- - def __posteriorPlot(self, posterior, par_names, key): - - # Initialization - newpath = (r'Outputs_SeqPosteriorComparison/posterior') - os.makedirs(newpath, exist_ok=True) - - bound_tuples = self.MetaModel.bound_tuples - n_params = len(par_names) - font_size = 40 - if n_params == 2: - - figPosterior, ax = plt.subplots(figsize=(15, 15)) - - sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1], - fill=True, ax=ax, cmap=plt.cm.jet, - clip=bound_tuples) - # Axis labels - plt.xlabel(par_names[0], fontsize=font_size) - plt.ylabel(par_names[1], fontsize=font_size) - - # Set axis limit - plt.xlim(bound_tuples[0]) - plt.ylim(bound_tuples[1]) - - # Increase font size - plt.xticks(fontsize=font_size) - plt.yticks(fontsize=font_size) - - # Switch off the grids - plt.grid(False) - - else: - import corner - figPosterior = corner.corner(posterior, labels=par_names, - title_fmt='.2e', show_titles=True, - title_kwargs={"fontsize": 12}) - - figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight') - plt.close() - - # Save the posterior as .npy - np.save(f'./{newpath}/{key}.npy', posterior) - - return figPosterior - - # ------------------------------------------------------------------------- - def __hellinger_distance(self, P, Q): - """ - Hellinger distance between two continuous distributions. - - The maximum distance 1 is achieved when P assigns probability zero to - every set to which Q assigns a positive probability, and vice versa. - 0 (identical) and 1 (maximally different) - - Parameters - ---------- - P : array - Reference likelihood. - Q : array - Estimated likelihood. - - Returns - ------- - float - Hellinger distance of two distributions. - - """ - mu1 = P.mean() - Sigma1 = np.std(P) - - mu2 = Q.mean() - Sigma2 = np.std(Q) - - term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2)) - - term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2)) - - H_squared = 1 - term1 * term2 - - return np.sqrt(H_squared) - - # ------------------------------------------------------------------------- - def __BME_Calculator(self, MetaModel, obs_data, sigma2Dict, rmse=None): - """ - This function computes the Bayesian model evidence (BME) via Monte - Carlo integration. - - """ - # Initializations - if hasattr(MetaModel, 'valid_likelihoods'): - valid_likelihoods = MetaModel.valid_likelihoods - else: - valid_likelihoods = [] - - post_snapshot = MetaModel.ExpDesign.post_snapshot - #print(f'post_snapshot: {post_snapshot}') - if post_snapshot or len(valid_likelihoods) != 0: - newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref') - os.makedirs(newpath, exist_ok=True) - - SamplingMethod = 'random' - MCsize = 10000 - ESS = 0 - - # Estimation of the integral via Monte Varlo integration - while (ESS > MCsize) or (ESS < 1): - - # Generate samples for Monte Carlo simulation - X_MC = MetaModel.ExpDesign.generate_samples( - MCsize, SamplingMethod - ) - - # Monte Carlo simulation for the candidate design - Y_MC, std_MC = MetaModel.eval_metamodel(samples=X_MC) - - # Likelihood computation (Comparison of data and - # simulation results via PCE with candidate design) - Likelihoods = self.__normpdf( - Y_MC, std_MC, obs_data, sigma2Dict, rmse - ) - - # Check the Effective Sample Size (1000<ESS<MCsize) - ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods))) - - # Enlarge sample size if it doesn't fulfill the criteria - if (ESS > MCsize) or (ESS < 1): - print(f'ESS={ESS} MC size should be larger.') - MCsize *= 10 - ESS = 0 - - # Rejection Step - # Random numbers between 0 and 1 - unif = np.random.rand(1, MCsize)[0] - - # Reject the poorly performed prior - accepted = (Likelihoods/np.max(Likelihoods)) >= unif - X_Posterior = X_MC[accepted] - - # ------------------------------------------------------------ - # --- Kullback-Leibler Divergence & Information Entropy ------ - # ------------------------------------------------------------ - # Prior-based estimation of BME - logBME = np.log(np.nanmean(Likelihoods)) - - # TODO: Correction factor - # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME) - - # Posterior-based expectation of likelihoods - postExpLikelihoods = np.mean(np.log(Likelihoods[accepted])) - - # Posterior-based expectation of prior densities - postExpPrior = np.mean( - np.log(MetaModel.ExpDesign.JDist.pdf(X_Posterior.T)) - ) - - # Calculate Kullback-Leibler Divergence - # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME) - KLD = postExpLikelihoods - logBME - - # Information Entropy based on Entropy paper Eq. 38 - infEntropy = logBME - postExpPrior - postExpLikelihoods - - # If post_snapshot is True, plot likelihood vs refrence - if post_snapshot or valid_likelihoods: - # Hellinger distance - valid_likelihoods = np.array(valid_likelihoods) - ref_like = np.log(valid_likelihoods[(valid_likelihoods > 0)]) - est_like = np.log(Likelihoods[Likelihoods > 0]) - distHellinger = self.__hellinger_distance(ref_like, est_like) - - idx = len([name for name in os.listdir(newpath) if 'Likelihoods_' - in name and os.path.isfile(os.path.join(newpath, name))]) - fig, ax = plt.subplots() - try: - sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]), - shade=True, color="g", label='Ref. Likelihood') - sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True, - color="b", label='Likelihood with PCE') - except: - pass - - text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}" - "\n DKL={KLD:.3f}" - - plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat', - edgecolor='black', - boxstyle='round,pad=1'), - transform=ax.transAxes) - - fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf', - bbox_inches='tight') - plt.close() - - else: - distHellinger = 0.0 - - # Bayesian inference with Emulator only for 2D problem - if post_snapshot and MetaModel.n_params == 2 and not idx % 5: - BayesOpts = BayesInference(MetaModel) - BayesOpts.emulator = True - BayesOpts.plot_post_pred = False - - # Select the inference method - import emcee - BayesOpts.inference_method = "MCMC" - # Set the MCMC parameters passed to self.mcmc_params - BayesOpts.mcmc_params = { - 'n_steps': 1e5, - 'n_walkers': 30, - 'moves': emcee.moves.KDEMove(), - 'verbose': False - } - - # ----- Define the discrepancy model ------- - obs_data = pd.DataFrame(obs_data, columns=self.Model.Output.names) - BayesOpts.measurement_error = obs_data - - # # -- (Option B) -- - DiscrepancyOpts = Discrepancy('') - DiscrepancyOpts.type = 'Gaussian' - DiscrepancyOpts.parameters = obs_data**2 - BayesOpts.Discrepancy = DiscrepancyOpts - # Start the calibration/inference - Bayes_PCE = BayesOpts.create_inference() - X_Posterior = Bayes_PCE.posterior_df.values - - return (logBME, KLD, X_Posterior, Likelihoods, distHellinger) - - # ------------------------------------------------------------------------- - def __validError(self, MetaModel): - - # MetaModel = self.MetaModel - Model = MetaModel.ModelObj - OutputName = Model.Output.names - - # Extract the original model with the generated samples - valid_samples = MetaModel.valid_samples - valid_model_runs = MetaModel.valid_model_runs - - # Run the PCE model with the generated samples - valid_PCE_runs, _ = MetaModel.eval_metamodel(samples=valid_samples) - - rms_error = {} - valid_error = {} - # Loop over the keys and compute RMSE error. - for key in OutputName: - rms_error[key] = mean_squared_error( - valid_model_runs[key], valid_PCE_runs[key], - multioutput='raw_values', - sample_weight=None, - squared=False) - # Validation error - valid_error[key] = (rms_error[key]**2) - valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0) - - # Print a report table - print("\n>>>>> Updated Errors of {} <<<<<".format(key)) - print("\nIndex | RMSE | Validation Error") - print('-'*35) - print('\n'.join(f'{i+1} | {k:.3e} | {j:.3e}' for i, (k, j) - in enumerate(zip(rms_error[key], - valid_error[key])))) - - return rms_error, valid_error - - # ------------------------------------------------------------------------- - def __error_Mean_Std(self): - - MetaModel = self.MetaModel - # Extract the mean and std provided by user - df_MCReference = MetaModel.ModelObj.mc_reference - - # Compute the mean and std based on the MetaModel - pce_means, pce_stds = self._compute_pce_moments(MetaModel) - - # Compute the root mean squared error - for output in MetaModel.ModelObj.Output.names: - - # Compute the error between mean and std of MetaModel and OrigModel - RMSE_Mean = mean_squared_error( - df_MCReference['mean'], pce_means[output], squared=False - ) - RMSE_std = mean_squared_error( - df_MCReference['std'], pce_means[output], squared=False - ) - - return RMSE_Mean, RMSE_std - - # ------------------------------------------------------------------------- - def _compute_pce_moments(self, MetaModel): - """ - Computes the first two moments using the PCE-based meta-model. - - Returns - ------- - pce_means: dict - The first moment (mean) of the surrogate. - pce_stds: dict - The second moment (standard deviation) of the surrogate. - - """ - outputs = MetaModel.ModelObj.Output.names - pce_means_b = {} - pce_stds_b = {} - - # Loop over bootstrap iterations - for b_i in range(MetaModel.n_bootstrap_itrs): - # Loop over the metamodels - coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items() - means = {} - stds = {} - for output, coef_dict in coeffs_dicts: - - pce_mean = np.zeros((len(coef_dict))) - pce_var = np.zeros((len(coef_dict))) - - for index, values in coef_dict.items(): - idx = int(index.split('_')[1]) - 1 - coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index] - - # Mean = c_0 - if coeffs[0] != 0: - pce_mean[idx] = coeffs[0] - else: - clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output] - pce_mean[idx] = clf_poly[index].intercept_ - # Var = sum(coeffs[1:]**2) - pce_var[idx] = np.sum(np.square(coeffs[1:])) - - # Save predictions for each output - if MetaModel.dim_red_method.lower() == 'pca': - PCA = MetaModel.pca[f'b_{b_i+1}'][output] - means[output] = PCA.inverse_transform(pce_mean) - stds[output] = PCA.inverse_transform(np.sqrt(pce_var)) - else: - means[output] = pce_mean - stds[output] = np.sqrt(pce_var) - - # Save predictions for each bootstrap iteration - pce_means_b[b_i] = means - pce_stds_b[b_i] = stds - - # Change the order of nesting - mean_all = {} - for i in sorted(pce_means_b): - for k, v in pce_means_b[i].items(): - if k not in mean_all: - mean_all[k] = [None] * len(pce_means_b) - mean_all[k][i] = v - std_all = {} - for i in sorted(pce_stds_b): - for k, v in pce_stds_b[i].items(): - if k not in std_all: - std_all[k] = [None] * len(pce_stds_b) - std_all[k][i] = v - - # Back transformation if PCA is selected. - pce_means, pce_stds = {}, {} - for output in outputs: - pce_means[output] = np.mean(mean_all[output], axis=0) - pce_stds[output] = np.mean(std_all[output], axis=0) - - return pce_means, pce_stds diff --git a/examples/only-model/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py b/examples/only-model/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py deleted file mode 100644 index d4f99b8a19bb5dbdf41f093bd454c80c63a321bb..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py +++ /dev/null @@ -1,366 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Jul 15 14:08:59 2022 - -@author: farid -""" -import numpy as np -from sklearn.base import RegressorMixin -from sklearn.linear_model._base import LinearModel -from sklearn.utils import check_X_y - - -def corr(x, y): - return abs(x.dot(y))/np.sqrt((x**2).sum()) - - -class OrthogonalMatchingPursuit(LinearModel, RegressorMixin): - ''' - Regression with Orthogonal Matching Pursuit [1]. - - Parameters - ---------- - fit_intercept : boolean, optional (DEFAULT = True) - whether to calculate the intercept for this model. If set - to false, no intercept will be used in calculations - (e.g. data is expected to be already centered). - - copy_X : boolean, optional (DEFAULT = True) - If True, X will be copied; else, it may be overwritten. - - verbose : boolean, optional (DEFAULT = FALSE) - Verbose mode when fitting the model - - Attributes - ---------- - coef_ : array, shape = (n_features) - Coefficients of the regression model (mean of posterior distribution) - - active_ : array, dtype = np.bool, shape = (n_features) - True for non-zero coefficients, False otherwise - - References - ---------- - [1] Pati, Y., Rezaiifar, R., Krishnaprasad, P. (1993). Orthogonal matching - pursuit: recursive function approximation with application to wavelet - decomposition. Proceedings of 27th Asilomar Conference on Signals, - Systems and Computers, 40-44. - ''' - - def __init__(self, fit_intercept=True, normalize=False, copy_X=True, - verbose=False): - self.fit_intercept = fit_intercept - self.normalize = normalize - self.copy_X = copy_X - self.verbose = verbose - - def _preprocess_data(self, X, y): - """Center and scale data. - Centers data to have mean zero along axis 0. If fit_intercept=False or - if the X is a sparse matrix, no centering is done, but normalization - can still be applied. The function returns the statistics necessary to - reconstruct the input data, which are X_offset, y_offset, X_scale, such - that the output - X = (X - X_offset) / X_scale - X_scale is the L2 norm of X - X_offset. - """ - - if self.copy_X: - X = X.copy(order='K') - - y = np.asarray(y, dtype=X.dtype) - - if self.fit_intercept: - X_offset = np.average(X, axis=0) - X -= X_offset - if self.normalize: - X_scale = np.ones(X.shape[1], dtype=X.dtype) - std = np.sqrt(np.sum(X**2, axis=0)/(len(X)-1)) - X_scale[std != 0] = std[std != 0] - X /= X_scale - else: - X_scale = np.ones(X.shape[1], dtype=X.dtype) - y_offset = np.mean(y) - y = y - y_offset - else: - X_offset = np.zeros(X.shape[1], dtype=X.dtype) - X_scale = np.ones(X.shape[1], dtype=X.dtype) - if y.ndim == 1: - y_offset = X.dtype.type(0) - else: - y_offset = np.zeros(y.shape[1], dtype=X.dtype) - - return X, y, X_offset, y_offset, X_scale - - def fit(self, X, y): - ''' - Fits Regression with Orthogonal Matching Pursuit Algorithm. - - Parameters - ----------- - X: {array-like, sparse matrix} of size (n_samples, n_features) - Training data, matrix of explanatory variables - - y: array-like of size [n_samples, n_features] - Target values - - Returns - ------- - self : object - Returns self. - ''' - X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True) - n_samples, n_features = X.shape - - X, y, X_mean, y_mean, X_std = self._preprocess_data(X, y) - self._x_mean_ = X_mean - self._y_mean = y_mean - self._x_std = X_std - - # Normalize columns of Psi, so that each column has norm = 1 - norm_X = np.linalg.norm(X, axis=0) - X_norm = X/norm_X - - # Initialize residual vector to full model response and normalize - R = y - norm_y = np.sqrt(np.dot(y, y)) - r = y/norm_y - - # Check for constant regressors - const_indices = np.where(~np.diff(X, axis=0).any(axis=0))[0] - bool_const = not const_indices - - # Start regression using OPM algorithm - precision = 0 # Set precision criterion to precision of program - early_stop = True - cond_early = True # Initialize condition for early stop - ind = [] - iindx = [] # index of selected columns - indtot = np.arange(n_features) # Full index set for remaining columns - kmax = min(n_samples, n_features) # Maximum number of iterations - LOO = np.PINF * np.ones(kmax) # Store LOO error at each iteration - LOOmin = np.PINF # Initialize minimum value of LOO - coeff = np.zeros((n_features, kmax)) - count = 0 - k = 0.1 # Percentage of iteration history for early stop - - # Begin iteration over regressors set (Matrix X) - while (np.linalg.norm(R) > precision) and (count <= kmax-1) and \ - ((cond_early or early_stop) ^ ~cond_early): - - # Update index set of columns yet to select - if count != 0: - indtot = np.delete(indtot, iindx) - - # Find column of X that is most correlated with residual - h = abs(np.dot(r, X_norm)) - iindx = np.argmax(h[indtot]) - indx = indtot[iindx] - - # initialize with the constant regressor, if it exists in the basis - if (count == 0) and bool_const: - # overwrite values for iindx and indx - iindx = const_indices[0] - indx = indtot[iindx] - - # Invert the information matrix at the first iteration, later only - # update its value on the basis of the previously inverted one, - if count == 0: - M = 1 / np.dot(X[:, indx], X[:, indx]) - else: - x = np.dot(X[:, ind].T, X[:, indx]) - r = np.dot(X[:, indx], X[:, indx]) - M = self.blockwise_inverse(M, x, x.T, r) - - # Add newly found index to the selected indexes set - ind.append(indx) - - # Select regressors subset (Projection subspace) - Xpro = X[:, ind] - - # Obtain coefficient by performing OLS - TT = np.dot(y, Xpro) - beta = np.dot(M, TT) - coeff[ind, count] = beta - - # Compute LOO error - LOO[count] = self.loo_error(Xpro, M, y, beta) - - # Compute new residual due to new projection - R = y - np.dot(Xpro, beta) - - # Normalize residual - norm_R = np.sqrt(np.dot(R, R)) - r = R / norm_R - - # Update counters and early-stop criterions - countinf = max(0, int(count-k*kmax)) - LOOmin = min(LOOmin, LOO[count]) - - if count == 0: - cond_early = (LOO[0] <= LOOmin) - else: - cond_early = (min(LOO[countinf:count+1]) <= LOOmin) - - if self.verbose: - print(f'Iteration: {count+1}, mod. LOOCV error : ' - f'{LOO[count]:.2e}') - - # Update counter - count += 1 - - # Select projection with smallest cross-validation error - countmin = np.argmin(LOO[:-1]) - self.coef_ = coeff[:, countmin] - self.active = coeff[:, countmin] != 0.0 - - # set intercept_ - if self.fit_intercept: - self.coef_ = self.coef_ / X_std - self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T) - else: - self.intercept_ = 0. - - return self - - def predict(self, X): - ''' - Computes predictive distribution for test set. - - Parameters - ----------- - X: {array-like, sparse} (n_samples_test, n_features) - Test data, matrix of explanatory variables - - Returns - ------- - y_hat: numpy array of size (n_samples_test,) - Estimated values of targets on test set (i.e. mean of - predictive distribution) - ''' - - y_hat = np.dot(X, self.coef_) + self.intercept_ - - return y_hat - - def loo_error(self, psi, inv_inf_matrix, y, coeffs): - """ - Calculates the corrected LOO error for regression on regressor - matrix `psi` that generated the coefficients based on [1] and [2]. - - [1] Blatman, G., 2009. Adaptive sparse polynomial chaos expansions for - uncertainty propagation and sensitivity analysis (Doctoral - dissertation, Clermont-Ferrand 2). - - [2] Blatman, G. and Sudret, B., 2011. Adaptive sparse polynomial chaos - expansion based on least angle regression. Journal of computational - Physics, 230(6), pp.2345-2367. - - Parameters - ---------- - psi : array of shape (n_samples, n_feature) - Orthogonal bases evaluated at the samples. - inv_inf_matrix : array - Inverse of the information matrix. - y : array of shape (n_samples, ) - Targets. - coeffs : array - Computed regresssor cofficients. - - Returns - ------- - loo_error : float - Modified LOOCV error. - - """ - - # NrEvaluation (Size of experimental design) - N, P = psi.shape - - # h factor (the full matrix is not calculated explicitly, - # only the trace is, to save memory) - PsiM = np.dot(psi, inv_inf_matrix) - - h = np.sum(np.multiply(PsiM, psi), axis=1, dtype=np.float128) - - # ------ Calculate Error Loocv for each measurement point ---- - # Residuals - residual = np.dot(psi, coeffs) - y - - # Variance - varY = np.var(y) - - if varY == 0: - norm_emp_error = 0 - loo_error = 0 - else: - norm_emp_error = np.mean(residual**2)/varY - - loo_error = np.mean(np.square(residual / (1-h))) / varY - - # if there are NaNs, just return an infinite LOO error (this - # happens, e.g., when a strongly underdetermined problem is solved) - if np.isnan(loo_error): - loo_error = np.inf - - # Corrected Error for over-determined system - tr_M = np.trace(np.atleast_2d(inv_inf_matrix)) - if tr_M < 0 or abs(tr_M) > 1e6: - tr_M = np.trace(np.linalg.pinv(np.dot(psi.T, psi))) - - # Over-determined system of Equation - if N > P: - T_factor = N/(N-P) * (1 + tr_M) - - # Under-determined system of Equation - else: - T_factor = np.inf - - loo_error *= T_factor - - return loo_error - - def blockwise_inverse(self, Ainv, B, C, D): - """ - non-singular square matrix M defined as M = [[A B]; [C D]] . - B, C and D can have any dimension, provided their combination defines - a square matrix M. - - Parameters - ---------- - Ainv : float or array - inverse of the square-submatrix A. - B : float or array - Information matrix with all new regressor. - C : float or array - Transpose of B. - D : float or array - Information matrix with all selected regressors. - - Returns - ------- - M : array - Inverse of the information matrix. - - """ - if np.isscalar(D): - # Inverse of D - Dinv = 1/D - # Schur complement - SCinv = 1/(D - np.dot(C, np.dot(Ainv, B[:, None])))[0] - else: - # Inverse of D - Dinv = np.linalg.solve(D, np.eye(D.shape)) - # Schur complement - SCinv = np.linalg.solve((D - C*Ainv*B), np.eye(D.shape)) - - T1 = np.dot(Ainv, np.dot(B[:, None], SCinv)) - T2 = np.dot(C, Ainv) - - # Assemble the inverse matrix - M = np.vstack(( - np.hstack((Ainv+T1*T2, -T1)), - np.hstack((-(SCinv)*T2, SCinv)) - )) - return M diff --git a/examples/only-model/bayesvalidrox/surrogate_models/reg_fast_ard.py b/examples/only-model/bayesvalidrox/surrogate_models/reg_fast_ard.py deleted file mode 100644 index 44073da8e78642ba3b3914f6ce55a2d01986b1f1..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/reg_fast_ard.py +++ /dev/null @@ -1,475 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Tue Mar 24 19:41:45 2020 - -@author: farid -""" -import numpy as np -from scipy.linalg import solve_triangular -from numpy.linalg import LinAlgError -from sklearn.base import RegressorMixin -from sklearn.linear_model._base import LinearModel -import warnings -from sklearn.utils import check_X_y -from scipy.linalg import pinvh - - -def update_precisions(Q,S,q,s,A,active,tol,n_samples,clf_bias): - ''' - Selects one feature to be added/recomputed/deleted to model based on - effect it will have on value of log marginal likelihood. - ''' - # initialise vector holding changes in log marginal likelihood - deltaL = np.zeros(Q.shape[0]) - - # identify features that can be added , recomputed and deleted in model - theta = q**2 - s - add = (theta > 0) * (active == False) - recompute = (theta > 0) * (active == True) - delete = ~(add + recompute) - - # compute sparsity & quality parameters corresponding to features in - # three groups identified above - Qadd,Sadd = Q[add], S[add] - Qrec,Srec,Arec = Q[recompute], S[recompute], A[recompute] - Qdel,Sdel,Adel = Q[delete], S[delete], A[delete] - - # compute new alpha's (precision parameters) for features that are - # currently in model and will be recomputed - Anew = s[recompute]**2/ ( theta[recompute] + np.finfo(np.float32).eps) - delta_alpha = (1./Anew - 1./Arec) - - # compute change in log marginal likelihood - deltaL[add] = ( Qadd**2 - Sadd ) / Sadd + np.log(Sadd/Qadd**2 ) - deltaL[recompute] = Qrec**2 / (Srec + 1. / delta_alpha) - np.log(1 + Srec*delta_alpha) - deltaL[delete] = Qdel**2 / (Sdel - Adel) - np.log(1 - Sdel / Adel) - deltaL = deltaL / n_samples - - # find feature which caused largest change in likelihood - feature_index = np.argmax(deltaL) - - # no deletions or additions - same_features = np.sum( theta[~recompute] > 0) == 0 - - # changes in precision for features already in model is below threshold - no_delta = np.sum( abs( Anew - Arec ) > tol ) == 0 - # if same_features: print(abs( Anew - Arec )) - # print("same_features = {} no_delta = {}".format(same_features,no_delta)) - # check convergence: if no features to add or delete and small change in - # precision for current features then terminate - converged = False - if same_features and no_delta: - converged = True - return [A,converged] - - # if not converged update precision parameter of weights and return - if theta[feature_index] > 0: - A[feature_index] = s[feature_index]**2 / theta[feature_index] - if active[feature_index] == False: - active[feature_index] = True - else: - # at least two active features - if active[feature_index] == True and np.sum(active) >= 2: - # do not remove bias term in classification - # (in regression it is factored in through centering) - if not (feature_index == 0 and clf_bias): - active[feature_index] = False - A[feature_index] = np.PINF - - return [A,converged] - - -class RegressionFastARD(LinearModel, RegressorMixin): - ''' - Regression with Automatic Relevance Determination (Fast Version uses - Sparse Bayesian Learning) - https://github.com/AmazaspShumik/sklearn-bayes/blob/master/skbayes/rvm_ard_models/fast_rvm.py - - Parameters - ---------- - n_iter: int, optional (DEFAULT = 100) - Maximum number of iterations - - start: list, optional (DEFAULT = None) - Initial selected features. - - tol: float, optional (DEFAULT = 1e-3) - If absolute change in precision parameter for weights is below threshold - algorithm terminates. - - fit_intercept : boolean, optional (DEFAULT = True) - whether to calculate the intercept for this model. If set - to false, no intercept will be used in calculations - (e.g. data is expected to be already centered). - - copy_X : boolean, optional (DEFAULT = True) - If True, X will be copied; else, it may be overwritten. - - compute_score : bool, default=False - If True, compute the log marginal likelihood at each iteration of the - optimization. - - verbose : boolean, optional (DEFAULT = FALSE) - Verbose mode when fitting the model - - Attributes - ---------- - coef_ : array, shape = (n_features) - Coefficients of the regression model (mean of posterior distribution) - - alpha_ : float - estimated precision of the noise - - active_ : array, dtype = np.bool, shape = (n_features) - True for non-zero coefficients, False otherwise - - lambda_ : array, shape = (n_features) - estimated precisions of the coefficients - - sigma_ : array, shape = (n_features, n_features) - estimated covariance matrix of the weights, computed only - for non-zero coefficients - - scores_ : array-like of shape (n_iter_+1,) - If computed_score is True, value of the log marginal likelihood (to be - maximized) at each iteration of the optimization. - - References - ---------- - [1] Fast marginal likelihood maximisation for sparse Bayesian models - (Tipping & Faul 2003) (http://www.miketipping.com/papers/met-fastsbl.pdf) - [2] Analysis of sparse Bayesian learning (Tipping & Faul 2001) - (http://www.miketipping.com/abstracts.htm#Faul:NIPS01) - ''' - - def __init__(self, n_iter=300, start=None, tol=1e-3, fit_intercept=True, - normalize=False, copy_X=True, compute_score=False, verbose=False): - self.n_iter = n_iter - self.start = start - self.tol = tol - self.scores_ = list() - self.fit_intercept = fit_intercept - self.normalize = normalize - self.copy_X = copy_X - self.compute_score = compute_score - self.verbose = verbose - - def _preprocess_data(self, X, y): - """Center and scale data. - Centers data to have mean zero along axis 0. If fit_intercept=False or - if the X is a sparse matrix, no centering is done, but normalization - can still be applied. The function returns the statistics necessary to - reconstruct the input data, which are X_offset, y_offset, X_scale, such - that the output - X = (X - X_offset) / X_scale - X_scale is the L2 norm of X - X_offset. - """ - - if self.copy_X: - X = X.copy(order='K') - - y = np.asarray(y, dtype=X.dtype) - - if self.fit_intercept: - X_offset = np.average(X, axis=0) - X -= X_offset - if self.normalize: - X_scale = np.ones(X.shape[1], dtype=X.dtype) - std = np.sqrt(np.sum(X**2, axis=0)/(len(X)-1)) - X_scale[std != 0] = std[std != 0] - X /= X_scale - else: - X_scale = np.ones(X.shape[1], dtype=X.dtype) - y_offset = np.mean(y) - y = y - y_offset - else: - X_offset = np.zeros(X.shape[1], dtype=X.dtype) - X_scale = np.ones(X.shape[1], dtype=X.dtype) - if y.ndim == 1: - y_offset = X.dtype.type(0) - else: - y_offset = np.zeros(y.shape[1], dtype=X.dtype) - - return X, y, X_offset, y_offset, X_scale - - def fit(self, X, y): - ''' - Fits ARD Regression with Sequential Sparse Bayes Algorithm. - - Parameters - ----------- - X: {array-like, sparse matrix} of size (n_samples, n_features) - Training data, matrix of explanatory variables - - y: array-like of size [n_samples, n_features] - Target values - - Returns - ------- - self : object - Returns self. - ''' - X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True) - n_samples, n_features = X.shape - - X, y, X_mean, y_mean, X_std = self._preprocess_data(X, y) - self._x_mean_ = X_mean - self._y_mean = y_mean - self._x_std = X_std - - # precompute X'*Y , X'*X for faster iterations & allocate memory for - # sparsity & quality vectors - XY = np.dot(X.T, y) - XX = np.dot(X.T, X) - XXd = np.diag(XX) - - # initialise precision of noise & and coefficients - var_y = np.var(y) - - # check that variance is non zero !!! - if var_y == 0: - beta = 1e-2 - self.var_y = True - else: - beta = 1. / np.var(y) - self.var_y = False - - A = np.PINF * np.ones(n_features) - active = np.zeros(n_features, dtype=np.bool) - - if self.start is not None and not hasattr(self, 'active_'): - start = self.start - # start from a given start basis vector - proj = XY**2 / XXd - active[start] = True - A[start] = XXd[start]/(proj[start] - var_y) - - else: - # in case of almost perfect multicollinearity between some features - # start from feature 0 - if np.sum(XXd - X_mean**2 < np.finfo(np.float32).eps) > 0: - A[0] = np.finfo(np.float16).eps - active[0] = True - - else: - # start from a single basis vector with largest projection on - # targets - proj = XY**2 / XXd - start = np.argmax(proj) - active[start] = True - A[start] = XXd[start]/(proj[start] - var_y + - np.finfo(np.float32).eps) - - warning_flag = 0 - scores_ = [] - for i in range(self.n_iter): - # Handle variance zero - if self.var_y: - A[0] = y_mean - active[0] = True - converged = True - break - - XXa = XX[active, :][:, active] - XYa = XY[active] - Aa = A[active] - - # mean & covariance of posterior distribution - Mn, Ri, cholesky = self._posterior_dist(Aa, beta, XXa, XYa) - if cholesky: - Sdiag = np.sum(Ri**2, 0) - else: - Sdiag = np.copy(np.diag(Ri)) - warning_flag += 1 - - # raise warning in case cholesky fails - if warning_flag == 1: - warnings.warn(("Cholesky decomposition failed! Algorithm uses " - "pinvh, which is significantly slower, if you " - "use RVR it is advised to change parameters of " - "kernel")) - - # compute quality & sparsity parameters - s, q, S, Q = self._sparsity_quality(XX, XXd, XY, XYa, Aa, Ri, - active, beta, cholesky) - - # update precision parameter for noise distribution - rss = np.sum((y - np.dot(X[:, active], Mn))**2) - - # if near perfect fit , then terminate - if (rss / n_samples/var_y) < self.tol: - warnings.warn('Early termination due to near perfect fit') - converged = True - break - beta = n_samples - np.sum(active) + np.sum(Aa * Sdiag) - beta /= rss - # beta /= (rss + np.finfo(np.float32).eps) - - # update precision parameters of coefficients - A, converged = update_precisions(Q, S, q, s, A, active, self.tol, - n_samples, False) - - if self.compute_score: - scores_.append(self.log_marginal_like(XXa, XYa, Aa, beta)) - - if self.verbose: - print(('Iteration: {0}, number of features ' - 'in the model: {1}').format(i, np.sum(active))) - - if converged or i == self.n_iter - 1: - if converged and self.verbose: - print('Algorithm converged !') - break - - # after last update of alpha & beta update parameters - # of posterior distribution - XXa, XYa, Aa = XX[active, :][:, active], XY[active], A[active] - Mn, Sn, cholesky = self._posterior_dist(Aa, beta, XXa, XYa, True) - self.coef_ = np.zeros(n_features) - self.coef_[active] = Mn - self.sigma_ = Sn - self.active_ = active - self.lambda_ = A - self.alpha_ = beta - self.converged = converged - if self.compute_score: - self.scores_ = np.array(scores_) - - # set intercept_ - if self.fit_intercept: - self.coef_ = self.coef_ / X_std - self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T) - else: - self.intercept_ = 0. - return self - - def log_marginal_like(self, XXa, XYa, Aa, beta): - """Computes the log of the marginal likelihood.""" - N, M = XXa.shape - A = np.diag(Aa) - - Mn, sigma_, cholesky = self._posterior_dist(Aa, beta, XXa, XYa, - full_covar=True) - - C = sigma_ + np.dot(np.dot(XXa.T, np.linalg.pinv(A)), XXa) - - score = np.dot(np.dot(XYa.T, np.linalg.pinv(C)), XYa) +\ - np.log(np.linalg.det(C)) + N * np.log(2 * np.pi) - - return -0.5 * score - - def predict(self, X, return_std=False): - ''' - Computes predictive distribution for test set. - Predictive distribution for each data point is one dimensional - Gaussian and therefore is characterised by mean and variance based on - Ref.[1] Section 3.3.2. - - Parameters - ----------- - X: {array-like, sparse} (n_samples_test, n_features) - Test data, matrix of explanatory variables - - Returns - ------- - : list of length two [y_hat, var_hat] - - y_hat: numpy array of size (n_samples_test,) - Estimated values of targets on test set (i.e. mean of - predictive distribution) - - var_hat: numpy array of size (n_samples_test,) - Variance of predictive distribution - References - ---------- - [1] Bishop, C. M. (2006). Pattern recognition and machine learning. - springer. - ''' - - y_hat = np.dot(X, self.coef_) + self.intercept_ - - if return_std: - # Handle the zero variance case - if self.var_y: - return y_hat, np.zeros_like(y_hat) - - if self.normalize: - X -= self._x_mean_[self.active_] - X /= self._x_std[self.active_] - var_hat = 1./self.alpha_ - var_hat += np.sum(X.dot(self.sigma_) * X, axis=1) - std_hat = np.sqrt(var_hat) - return y_hat, std_hat - else: - return y_hat - - def _posterior_dist(self, A, beta, XX, XY, full_covar=False): - ''' - Calculates mean and covariance matrix of posterior distribution - of coefficients. - ''' - # compute precision matrix for active features - Sinv = beta * XX - np.fill_diagonal(Sinv, np.diag(Sinv) + A) - cholesky = True - - # try cholesky, if it fails go back to pinvh - try: - # find posterior mean : R*R.T*mean = beta*X.T*Y - # solve(R*z = beta*X.T*Y) =>find z=> solve(R.T*mean = z)=>find mean - R = np.linalg.cholesky(Sinv) - Z = solve_triangular(R, beta*XY, check_finite=True, lower=True) - Mn = solve_triangular(R.T, Z, check_finite=True, lower=False) - - # invert lower triangular matrix from cholesky decomposition - Ri = solve_triangular(R, np.eye(A.shape[0]), check_finite=False, - lower=True) - if full_covar: - Sn = np.dot(Ri.T, Ri) - return Mn, Sn, cholesky - else: - return Mn, Ri, cholesky - except LinAlgError: - cholesky = False - Sn = pinvh(Sinv) - Mn = beta*np.dot(Sinv, XY) - return Mn, Sn, cholesky - - def _sparsity_quality(self, XX, XXd, XY, XYa, Aa, Ri, active, beta, cholesky): - ''' - Calculates sparsity and quality parameters for each feature - - Theoretical Note: - ----------------- - Here we used Woodbury Identity for inverting covariance matrix - of target distribution - C = 1/beta + 1/alpha * X' * X - C^-1 = beta - beta^2 * X * Sn * X' - ''' - bxy = beta*XY - bxx = beta*XXd - if cholesky: - # here Ri is inverse of lower triangular matrix obtained from - # cholesky decomp - xxr = np.dot(XX[:, active], Ri.T) - rxy = np.dot(Ri, XYa) - S = bxx - beta**2 * np.sum(xxr**2, axis=1) - Q = bxy - beta**2 * np.dot(xxr, rxy) - else: - # here Ri is covariance matrix - XXa = XX[:, active] - XS = np.dot(XXa, Ri) - S = bxx - beta**2 * np.sum(XS*XXa, 1) - Q = bxy - beta**2 * np.dot(XS, XYa) - # Use following: - # (EQ 1) q = A*Q/(A - S) ; s = A*S/(A-S) - # so if A = np.PINF q = Q, s = S - qi = np.copy(Q) - si = np.copy(S) - # If A is not np.PINF, then it should be 'active' feature => use (EQ 1) - Qa, Sa = Q[active], S[active] - qi[active] = Aa * Qa / (Aa - Sa) - si[active] = Aa * Sa / (Aa - Sa) - - return [si, qi, S, Q] diff --git a/examples/only-model/bayesvalidrox/surrogate_models/reg_fast_laplace.py b/examples/only-model/bayesvalidrox/surrogate_models/reg_fast_laplace.py deleted file mode 100644 index bdff324ede818a42d226e9aa55aaf01666ca8fc8..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/reg_fast_laplace.py +++ /dev/null @@ -1,452 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import numpy as np -from sklearn.utils import as_float_array -from sklearn.model_selection import KFold - - -class RegressionFastLaplace(): - ''' - Sparse regression with Bayesian Compressive Sensing as described in Alg. 1 - (Fast Laplace) of Ref.[1], which updated formulas from [2]. - - sigma2: noise precision (sigma^2) - nu fixed to 0 - - uqlab/lib/uq_regression/BCS/uq_bsc.m - - Parameters - ---------- - n_iter: int, optional (DEFAULT = 1000) - Maximum number of iterations - - tol: float, optional (DEFAULT = 1e-7) - If absolute change in precision parameter for weights is below - threshold algorithm terminates. - - fit_intercept : boolean, optional (DEFAULT = True) - whether to calculate the intercept for this model. If set - to false, no intercept will be used in calculations - (e.g. data is expected to be already centered). - - copy_X : boolean, optional (DEFAULT = True) - If True, X will be copied; else, it may be overwritten. - - verbose : boolean, optional (DEFAULT = FALSE) - Verbose mode when fitting the model - - Attributes - ---------- - coef_ : array, shape = (n_features) - Coefficients of the regression model (mean of posterior distribution) - - alpha_ : float - estimated precision of the noise - - active_ : array, dtype = np.bool, shape = (n_features) - True for non-zero coefficients, False otherwise - - lambda_ : array, shape = (n_features) - estimated precisions of the coefficients - - sigma_ : array, shape = (n_features, n_features) - estimated covariance matrix of the weights, computed only - for non-zero coefficients - - References - ---------- - [1] Babacan, S. D., Molina, R., & Katsaggelos, A. K. (2009). Bayesian - compressive sensing using Laplace priors. IEEE Transactions on image - processing, 19(1), 53-63. - [2] Fast marginal likelihood maximisation for sparse Bayesian models - (Tipping & Faul 2003). - (http://www.miketipping.com/papers/met-fastsbl.pdf) - ''' - - def __init__(self, n_iter=1000, n_Kfold=10, tol=1e-7, fit_intercept=False, - bias_term=True, copy_X=True, verbose=False): - self.n_iter = n_iter - self.n_Kfold = n_Kfold - self.tol = tol - self.fit_intercept = fit_intercept - self.bias_term = bias_term - self.copy_X = copy_X - self.verbose = verbose - - def _center_data(self, X, y): - ''' Centers data''' - X = as_float_array(X, self.copy_X) - - # normalisation should be done in preprocessing! - X_std = np.ones(X.shape[1], dtype=X.dtype) - if self.fit_intercept: - X_mean = np.average(X, axis=0) - y_mean = np.average(y, axis=0) - X -= X_mean - y -= y_mean - else: - X_mean = np.zeros(X.shape[1], dtype=X.dtype) - y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype) - return X, y, X_mean, y_mean, X_std - - def fit(self, X, y): - - k_fold = KFold(n_splits=self.n_Kfold) - - varY = np.var(y, ddof=1) if np.var(y, ddof=1) != 0 else 1.0 - sigma2s = len(y)*varY*(10**np.linspace(-16, -1, self.n_Kfold)) - - errors = np.zeros((len(sigma2s), self.n_Kfold)) - for s, sigma2 in enumerate(sigma2s): - for k, (train, test) in enumerate(k_fold.split(X, y)): - self.fit_(X[train], y[train], sigma2) - errors[s, k] = np.linalg.norm( - y[test] - self.predict(X[test]) - )**2/len(test) - - KfCVerror = np.sum(errors, axis=1)/self.n_Kfold/varY - i_minCV = np.argmin(KfCVerror) - - self.kfoldCVerror = np.min(KfCVerror) - - return self.fit_(X, y, sigma2s[i_minCV]) - - def fit_(self, X, y, sigma2): - - N, P = X.shape - # n_samples, n_features = X.shape - - X, y, X_mean, y_mean, X_std = self._center_data(X, y) - self._x_mean_ = X_mean - self._y_mean = y_mean - self._x_std = X_std - - # check that variance is non zero !!! - if np.var(y) == 0: - self.var_y = True - else: - self.var_y = False - beta = 1./sigma2 - - # precompute X'*Y , X'*X for faster iterations & allocate memory for - # sparsity & quality vectors X=Psi - PsiTY = np.dot(X.T, y) - PsiTPsi = np.dot(X.T, X) - XXd = np.diag(PsiTPsi) - - # initialize with constant regressor, or if that one does not exist, - # with the one that has the largest correlation with Y - ind_global_to_local = np.zeros(P, dtype=np.int32) - - # identify constant regressors - constidx = np.where(~np.diff(X, axis=0).all(axis=0))[0] - - if self.bias_term and constidx.size != 0: - ind_start = constidx[0] - ind_global_to_local[ind_start] = True - else: - # start from a single basis vector with largest projection on - # targets - proj = np.divide(np.square(PsiTY), XXd) - ind_start = np.argmax(proj) - ind_global_to_local[ind_start] = True - - num_active = 1 - active_indices = [ind_start] - deleted_indices = [] - bcs_path = [ind_start] - gamma = np.zeros(P) - # for the initial value of gamma(ind_start), use the RVM formula - # gamma = (q^2 - s) / (s^2) - # and the fact that initially s = S = beta*Psi_i'*Psi_i and q = Q = - # beta*Psi_i'*Y - gamma[ind_start] = np.square(PsiTY[ind_start]) - gamma[ind_start] -= sigma2 * PsiTPsi[ind_start, ind_start] - gamma[ind_start] /= np.square(PsiTPsi[ind_start, ind_start]) - - Sigma = 1. / (beta * PsiTPsi[ind_start, ind_start] - + 1./gamma[ind_start]) - - mu = Sigma * PsiTY[ind_start] * beta - tmp1 = beta * PsiTPsi[ind_start] - S = beta * np.diag(PsiTPsi).T - Sigma * np.square(tmp1) - Q = beta * PsiTY.T - mu*(tmp1) - - tmp2 = np.ones(P) # alternative computation for the initial s,q - q0tilde = PsiTY[ind_start] - s0tilde = PsiTPsi[ind_start, ind_start] - tmp2[ind_start] = s0tilde / (q0tilde**2) / beta - s = np.divide(S, tmp2) - q = np.divide(Q, tmp2) - Lambda = 2*(num_active - 1) / np.sum(gamma) - - Delta_L_max = [] - for i in range(self.n_iter): - # Handle variance zero - if self.var_y: - mu = np.mean(y) - break - - if self.verbose: - print(' lambda = {0:.6e}\n'.format(Lambda)) - - # Calculate the potential updated value of each gamma[i] - if Lambda == 0.0: # RVM - gamma_potential = np.multiply(( - (q**2 - s) > Lambda), - np.divide(q**2 - s, s**2) - ) - else: - a = Lambda * s**2 - b = s**2 + 2*Lambda*s - c = Lambda + s - q**2 - gamma_potential = np.multiply( - (c < 0), np.divide( - -b + np.sqrt(b**2 - 4*np.multiply(a, c)), 2*a) - ) - - l_gamma = - np.log(np.absolute(1 + np.multiply(gamma, s))) - l_gamma += np.divide(np.multiply(q**2, gamma), - (1 + np.multiply(gamma, s))) - l_gamma -= Lambda*gamma # omitted the factor 1/2 - - # Contribution of each updated gamma(i) to L(gamma) - l_gamma_potential = - np.log( - np.absolute(1 + np.multiply(gamma_potential, s)) - ) - l_gamma_potential += np.divide( - np.multiply(q**2, gamma_potential), - (1 + np.multiply(gamma_potential, s)) - ) - # omitted the factor 1/2 - l_gamma_potential -= Lambda*gamma_potential - - # Check how L(gamma) would change if we replaced gamma(i) by the - # updated gamma_potential(i), for each i separately - Delta_L_potential = l_gamma_potential - l_gamma - - # deleted indices should not be chosen again - if len(deleted_indices) != 0: - values = -np.inf * np.ones(len(deleted_indices)) - Delta_L_potential[deleted_indices] = values - - Delta_L_max.append(np.nanmax(Delta_L_potential)) - ind_L_max = np.nanargmax(Delta_L_potential) - - # in case there is only 1 regressor in the model and it would now - # be deleted - if len(active_indices) == 1 and ind_L_max == active_indices[0] \ - and gamma_potential[ind_L_max] == 0.0: - Delta_L_potential[ind_L_max] = -np.inf - Delta_L_max[i] = np.max(Delta_L_potential) - ind_L_max = np.argmax(Delta_L_potential) - - # If L did not change significantly anymore, break - if Delta_L_max[i] <= 0.0 or\ - (i > 0 and all(np.absolute(Delta_L_max[i-1:]) - < sum(Delta_L_max)*self.tol)) or \ - (i > 0 and all(np.diff(bcs_path)[i-1:] == 0.0)): - if self.verbose: - print('Increase in L: {0:.6e} (eta = {1:.3e})\ - -- break\n'.format(Delta_L_max[i], self.tol)) - break - - # Print information - if self.verbose: - print(' Delta L = {0:.6e} \n'.format(Delta_L_max[i])) - - what_changed = int(gamma[ind_L_max] == 0.0) - what_changed -= int(gamma_potential[ind_L_max] == 0.0) - - # Print information - if self.verbose: - if what_changed < 0: - print(f'{i+1} - Remove regressor #{ind_L_max+1}..\n') - elif what_changed == 0: - print(f'{i+1} - Recompute regressor #{ind_L_max+1}..\n') - else: - print(f'{i+1} - Add regressor #{ind_L_max+1}..\n') - - # --- Update all quantities ---- - if what_changed == 1: - # adding a regressor - - # update gamma - gamma[ind_L_max] = gamma_potential[ind_L_max] - - Sigma_ii = 1.0 / (1.0/gamma[ind_L_max] + S[ind_L_max]) - try: - x_i = np.matmul( - Sigma, PsiTPsi[active_indices, ind_L_max].reshape(-1, 1) - ) - except ValueError: - x_i = Sigma * PsiTPsi[active_indices, ind_L_max] - tmp_1 = - (beta * Sigma_ii) * x_i - Sigma = np.vstack( - (np.hstack(((beta**2 * Sigma_ii) * np.dot(x_i, x_i.T) - + Sigma, tmp_1)), np.append(tmp_1.T, Sigma_ii)) - ) - mu_i = Sigma_ii * Q[ind_L_max] - mu = np.vstack((mu - (beta * mu_i) * x_i, mu_i)) - - tmp2_1 = PsiTPsi[:, ind_L_max] - beta * np.squeeze( - np.matmul(PsiTPsi[:, active_indices], x_i) - ) - if i == 0: - tmp2_1[0] /= 2 - tmp2 = beta * tmp2_1.T - S = S - Sigma_ii * np.square(tmp2) - Q = Q - mu_i * tmp2 - - num_active += 1 - ind_global_to_local[ind_L_max] = num_active - active_indices.append(ind_L_max) - bcs_path.append(ind_L_max) - - elif what_changed == 0: - # recomputation - # zero if regressor has not been chosen yet - if not ind_global_to_local[ind_L_max]: - raise Exception('cannot recompute index{0} -- not yet\ - part of the model!'.format(ind_L_max)) - Sigma = np.atleast_2d(Sigma) - mu = np.atleast_2d(mu) - gamma_i_new = gamma_potential[ind_L_max] - gamma_i_old = gamma[ind_L_max] - # update gamma - gamma[ind_L_max] = gamma_potential[ind_L_max] - - # index of regressor in Sigma - local_ind = ind_global_to_local[ind_L_max]-1 - - kappa_i = (1.0/gamma_i_new - 1.0/gamma_i_old) - kappa_i = 1.0 / kappa_i - kappa_i += Sigma[local_ind, local_ind] - kappa_i = 1 / kappa_i - Sigma_i_col = Sigma[:, local_ind] - - Sigma = Sigma - kappa_i * (Sigma_i_col * Sigma_i_col.T) - mu_i = mu[local_ind] - mu = mu - (kappa_i * mu_i) * Sigma_i_col[:, None] - - tmp1 = beta * np.dot( - Sigma_i_col.reshape(1, -1), PsiTPsi[active_indices])[0] - S = S + kappa_i * np.square(tmp1) - Q = Q + (kappa_i * mu_i) * tmp1 - - # no change in active_indices or ind_global_to_local - bcs_path.append(ind_L_max + 0.1) - - elif what_changed == -1: - gamma[ind_L_max] = 0 - - # index of regressor in Sigma - local_ind = ind_global_to_local[ind_L_max]-1 - - Sigma_ii_inv = 1. / Sigma[local_ind, local_ind] - Sigma_i_col = Sigma[:, local_ind] - - Sigma = Sigma - Sigma_ii_inv * (Sigma_i_col * Sigma_i_col.T) - - Sigma = np.delete( - np.delete(Sigma, local_ind, axis=0), local_ind, axis=1) - - mu = mu - (mu[local_ind] * Sigma_ii_inv) * Sigma_i_col[:, None] - mu = np.delete(mu, local_ind, axis=0) - - tmp1 = beta * np.dot(Sigma_i_col, PsiTPsi[active_indices]) - S = S + Sigma_ii_inv * np.square(tmp1) - Q = Q + (mu_i * Sigma_ii_inv) * tmp1 - - num_active -= 1 - ind_global_to_local[ind_L_max] = 0.0 - v = ind_global_to_local[ind_global_to_local > local_ind] - 1 - ind_global_to_local[ind_global_to_local > local_ind] = v - del active_indices[local_ind] - deleted_indices.append(ind_L_max) - # and therefore ineligible - bcs_path.append(-ind_L_max) - - # same for all three cases - tmp3 = 1 - np.multiply(gamma, S) - s = np.divide(S, tmp3) - q = np.divide(Q, tmp3) - - # Update lambda - Lambda = 2*(num_active - 1) / np.sum(gamma) - - # Prepare the result object - self.coef_ = np.zeros(P) - self.coef_[active_indices] = np.squeeze(mu) - self.sigma_ = Sigma - self.active_ = active_indices - self.gamma = gamma - self.Lambda = Lambda - self.beta = beta - self.bcs_path = bcs_path - - # set intercept_ - if self.fit_intercept: - self.coef_ = self.coef_ / X_std - self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T) - else: - self.intercept_ = 0. - - return self - - def predict(self, X, return_std=False): - ''' - Computes predictive distribution for test set. - Predictive distribution for each data point is one dimensional - Gaussian and therefore is characterised by mean and variance based on - Ref.[1] Section 3.3.2. - - Parameters - ----------- - X: {array-like, sparse} (n_samples_test, n_features) - Test data, matrix of explanatory variables - - Returns - ------- - : list of length two [y_hat, var_hat] - - y_hat: numpy array of size (n_samples_test,) - Estimated values of targets on test set (i.e. mean of - predictive distribution) - - var_hat: numpy array of size (n_samples_test,) - Variance of predictive distribution - - References - ---------- - [1] Bishop, C. M. (2006). Pattern recognition and machine learning. - springer. - ''' - y_hat = np.dot(X, self.coef_) + self.intercept_ - - if return_std: - # Handle the zero variance case - if self.var_y: - return y_hat, np.zeros_like(y_hat) - - var_hat = 1./self.beta - var_hat += np.sum(X.dot(self.sigma_) * X, axis=1) - std_hat = np.sqrt(var_hat) - return y_hat, std_hat - else: - return y_hat - -# l2norm = 0.0 -# for idx in range(10): -# sigma2 = np.genfromtxt('./test/sigma2_{0}.csv'.format(idx+1), delimiter=',') -# Psi_train = np.genfromtxt('./test/Psi_train_{0}.csv'.format(idx+1), delimiter=',') -# Y_train = np.genfromtxt('./test/Y_train_{0}.csv'.format(idx+1)) -# Psi_test = np.genfromtxt('./test/Psi_test_{0}.csv'.format(idx+1), delimiter=',') -# Y_test = np.genfromtxt('./test/Y_test_{0}.csv'.format(idx+1)) - -# clf = RegressionFastLaplace(verbose=True) -# clf.fit_(Psi_train, Y_train, sigma2) -# coeffs_fold = np.genfromtxt('./test/coeffs_fold_{0}.csv'.format(idx+1)) -# print("coeffs error: {0:.4g}".format(np.linalg.norm(clf.coef_ - coeffs_fold))) -# l2norm += np.linalg.norm(Y_test - clf.predict(Psi_test))**2/len(Y_test) -# print("l2norm error: {0:.4g}".format(l2norm)) diff --git a/examples/only-model/bayesvalidrox/surrogate_models/sequential_design.py b/examples/only-model/bayesvalidrox/surrogate_models/sequential_design.py deleted file mode 100644 index fc81dcd4529ca0708dfba47385aef4415992eb3e..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/sequential_design.py +++ /dev/null @@ -1,2187 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Jan 28 09:21:18 2022 - -@author: farid -""" -import numpy as np -from scipy import stats, signal, linalg, sparse -from scipy.spatial import distance -from copy import deepcopy, copy -from tqdm import tqdm -import scipy.optimize as opt -from sklearn.metrics import mean_squared_error -import multiprocessing -import matplotlib.pyplot as plt -import sys -import os -import gc -import seaborn as sns -from joblib import Parallel, delayed -import resource -from .exploration import Exploration - - -class SeqDesign(): - """ Sequential experimental design - This class provieds method for trainig the meta-model in an iterative - manners. - The main method to execute the task is `train_seq_design`, which - recieves a model object and returns the trained metamodel. - """ - - # ------------------------------------------------------------------------- - def train_seq_design(self, MetaModel): - """ - Starts the adaptive sequential design for refining the surrogate model - by selecting training points in a sequential manner. - - Parameters - ---------- - Model : object - An object containing all model specifications. - - Returns - ------- - MetaModel : object - Meta model object. - - """ - # MetaModel = self - Model = MetaModel.ModelObj - self.MetaModel = MetaModel - self.Model = Model - - # Initialization - MetaModel.SeqModifiedLOO = {} - MetaModel.seqValidError = {} - MetaModel.SeqBME = {} - MetaModel.SeqKLD = {} - MetaModel.SeqDistHellinger = {} - MetaModel.seqRMSEMean = {} - MetaModel.seqRMSEStd = {} - MetaModel.seqMinDist = [] - pce = True if MetaModel.meta_model_type.lower() != 'gpe' else False - mc_ref = True if bool(Model.mc_reference) else False - if mc_ref: - Model.read_mc_reference() - - if not hasattr(MetaModel, 'valid_likelihoods'): - MetaModel.valid_samples = [] - MetaModel.valid_model_runs = [] - MetaModel.valid_likelihoods = [] - - # Get the parameters - max_n_samples = MetaModel.ExpDesign.n_max_samples - mod_LOO_threshold = MetaModel.ExpDesign.mod_LOO_threshold - n_canddidate = MetaModel.ExpDesign.n_canddidate - post_snapshot = MetaModel.ExpDesign.post_snapshot - n_replication = MetaModel.ExpDesign.n_replication - util_func = MetaModel.ExpDesign.util_func - output_name = Model.Output.names - validError = None - # Handle if only one UtilityFunctions is provided - if not isinstance(util_func, list): - util_func = [MetaModel.ExpDesign.util_func] - - # Read observations or MCReference - if len(Model.observations) != 0 or Model.meas_file is not None: - self.observations = Model.read_observation() - obs_data = self.observations - else: - obs_data = [] - TotalSigma2 = {} - # ---------- Initial MetaModel ---------- - initMetaModel = deepcopy(MetaModel) - - # Validation error if validation set is provided. - if len(MetaModel.valid_model_runs) != 0: - init_rmse, init_valid_error = self.__validError(initMetaModel) - init_valid_error = list(init_valid_error.values()) - else: - init_rmse = None - - # Check if discrepancy is provided - if len(obs_data) != 0 and hasattr(MetaModel, 'Discrepancy'): - TotalSigma2 = MetaModel.Discrepancy.parameters - - # Calculate the initial BME - out = self.__BME_Calculator( - initMetaModel, obs_data, TotalSigma2, init_rmse) - init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out - print(f"\nInitial BME: {init_BME:.2f}") - print(f"Initial KLD: {init_KLD:.2f}") - - # Posterior snapshot (initial) - if post_snapshot: - parNames = MetaModel.ExpDesign.par_names - print('Posterior snapshot (initial) is being plotted...') - self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init') - - # Check the convergence of the Mean & Std - if mc_ref and pce: - init_rmse_mean, init_rmse_std = self.__error_Mean_Std() - print(f"Initial Mean and Std error: {init_rmse_mean}," - f" {init_rmse_std}") - - # Read the initial experimental design - Xinit = initMetaModel.ExpDesign.X - init_n_samples = len(MetaModel.ExpDesign.X) - initYprev = initMetaModel.ModelOutputDict - initLCerror = initMetaModel.LCerror - n_itrs = max_n_samples - init_n_samples - - # Read the initial ModifiedLOO - if pce: - Scores_all, varExpDesignY = [], [] - for out_name in output_name: - y = initMetaModel.ExpDesign.Y[out_name] - Scores_all.append(list( - initMetaModel.score_dict['b_1'][out_name].values())) - if MetaModel.dim_red_method.lower() == 'pca': - pca = MetaModel.pca['b_1'][out_name] - components = pca.transform(y) - varExpDesignY.append(np.var(components, axis=0)) - else: - varExpDesignY.append(np.var(y, axis=0)) - - Scores = [item for sublist in Scores_all for item in sublist] - weights = [item for sublist in varExpDesignY for item in sublist] - init_mod_LOO = [np.average([1-score for score in Scores], - weights=weights)] - - prevMetaModel_dict = {} - # Replicate the sequential design - for repIdx in range(n_replication): - print(f'\n>>>> Replication: {repIdx+1}<<<<') - - # To avoid changes ub original aPCE object - MetaModel.ExpDesign.X = Xinit - MetaModel.ExpDesign.Y = initYprev - MetaModel.LCerror = initLCerror - - for util_f in util_func: - print(f'\n>>>> Utility Function: {util_f} <<<<') - # To avoid changes ub original aPCE object - MetaModel.ExpDesign.X = Xinit - MetaModel.ExpDesign.Y = initYprev - MetaModel.LCerror = initLCerror - - # Set the experimental design - Xprev = Xinit - total_n_samples = init_n_samples - Yprev = initYprev - - Xfull = [] - Yfull = [] - - # Store the initial ModifiedLOO - if pce: - print("\nInitial ModifiedLOO:", init_mod_LOO) - SeqModifiedLOO = np.array(init_mod_LOO) - - if len(MetaModel.valid_model_runs) != 0: - SeqValidError = np.array(init_valid_error) - - # Check if data is provided - if len(obs_data) != 0: - SeqBME = np.array([init_BME]) - SeqKLD = np.array([init_KLD]) - SeqDistHellinger = np.array([init_dist_hellinger]) - - if mc_ref and pce: - seqRMSEMean = np.array([init_rmse_mean]) - seqRMSEStd = np.array([init_rmse_std]) - - # ------- Start Sequential Experimental Design ------- - postcnt = 1 - for itr_no in range(1, n_itrs+1): - print(f'\n>>>> Iteration number {itr_no} <<<<') - - # Save the metamodel prediction before updating - prevMetaModel_dict[itr_no] = deepcopy(MetaModel) - if itr_no > 1: - pc_model = prevMetaModel_dict[itr_no-1] - self._y_hat_prev, _ = pc_model.eval_metamodel( - samples=Xfull[-1].reshape(1, -1)) - - # Optimal Bayesian Design - m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - MetaModel.ExpDesignFlag = 'sequential' - Xnew, updatedPrior = self.opt_SeqDesign(TotalSigma2, - n_canddidate, - util_f) - m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - S = np.min(distance.cdist(Xinit, Xnew, 'euclidean')) - MetaModel.seqMinDist.append(S) - print(f"\nmin Dist from OldExpDesign: {S:2f}") - print("\n") - - # Evaluate the full model response at the new sample - Ynew, _ = Model.run_model_parallel( - Xnew, prevRun_No=total_n_samples - ) - total_n_samples += Xnew.shape[0] - # ------ Plot the surrogate model vs Origninal Model ------ - if hasattr(MetaModel, 'adapt_verbose') and \ - MetaModel.adapt_verbose: - from .adaptPlot import adaptPlot - y_hat, std_hat = MetaModel.eval_metamodel(samples=Xnew) - adaptPlot(MetaModel, Ynew, y_hat, std_hat, plotED=False) - - # -------- Retrain the surrogate model ------- - # Extend new experimental design - Xfull = np.vstack((Xprev, Xnew)) - - # Updating experimental design Y - for out_name in output_name: - Yfull = np.vstack((Yprev[out_name], Ynew[out_name])) - MetaModel.ModelOutputDict[out_name] = Yfull - - # Pass new design to the metamodel object - MetaModel.ExpDesign.sampling_method = 'user' - MetaModel.ExpDesign.X = Xfull - MetaModel.ExpDesign.Y = MetaModel.ModelOutputDict - - # Save the Experimental Design for next iteration - Xprev = Xfull - Yprev = MetaModel.ModelOutputDict - - # Pass the new prior as the input - MetaModel.input_obj.poly_coeffs_flag = False - if updatedPrior is not None: - MetaModel.input_obj.poly_coeffs_flag = True - print("updatedPrior:", updatedPrior.shape) - # Arbitrary polynomial chaos - for i in range(updatedPrior.shape[1]): - MetaModel.input_obj.Marginals[i].dist_type = None - x = updatedPrior[:, i] - MetaModel.input_obj.Marginals[i].raw_data = x - - # Train the surrogate model for new ExpDesign - MetaModel.train_norm_design(parallel=False) - m_3 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - - # -------- Evaluate the retrained surrogate model ------- - # Extract Modified LOO from Output - if pce: - Scores_all, varExpDesignY = [], [] - for out_name in output_name: - y = MetaModel.ExpDesign.Y[out_name] - Scores_all.append(list( - MetaModel.score_dict['b_1'][out_name].values())) - if MetaModel.dim_red_method.lower() == 'pca': - pca = MetaModel.pca['b_1'][out_name] - components = pca.transform(y) - varExpDesignY.append(np.var(components, - axis=0)) - else: - varExpDesignY.append(np.var(y, axis=0)) - Scores = [item for sublist in Scores_all for item - in sublist] - weights = [item for sublist in varExpDesignY for item - in sublist] - ModifiedLOO = [np.average( - [1-score for score in Scores], weights=weights)] - - print('\n') - print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO) - print('\n') - - # Compute the validation error - if len(MetaModel.valid_model_runs) != 0: - rmse, validError = self.__validError(MetaModel) - ValidError = list(validError.values()) - else: - rmse = None - - # Store updated ModifiedLOO - if pce: - SeqModifiedLOO = np.vstack( - (SeqModifiedLOO, ModifiedLOO)) - if len(MetaModel.valid_model_runs) != 0: - SeqValidError = np.vstack( - (SeqValidError, ValidError)) - m_4 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - # -------- Caclulation of BME as accuracy metric ------- - # Check if data is provided - if len(obs_data) != 0: - # Calculate the initial BME - out = self.__BME_Calculator(MetaModel, obs_data, - TotalSigma2, rmse) - BME, KLD, Posterior, likes, DistHellinger = out - print('\n') - print(f"Updated BME: {BME:.2f}") - print(f"Updated KLD: {KLD:.2f}") - print('\n') - - # Plot some snapshots of the posterior - step_snapshot = MetaModel.ExpDesign.step_snapshot - if post_snapshot and postcnt % step_snapshot == 0: - parNames = MetaModel.ExpDesign.par_names - print('Posterior snapshot is being plotted...') - self.__posteriorPlot(Posterior, parNames, - f'SeqPosterior_{postcnt}') - postcnt += 1 - m_5 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - - # Check the convergence of the Mean&Std - if mc_ref and pce: - print('\n') - RMSE_Mean, RMSE_std = self.__error_Mean_Std() - print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, " - f"{RMSE_std:.2f}") - print('\n') - - # Store the updated BME & KLD - # Check if data is provided - if len(obs_data) != 0: - SeqBME = np.vstack((SeqBME, BME)) - SeqKLD = np.vstack((SeqKLD, KLD)) - SeqDistHellinger = np.vstack((SeqDistHellinger, - DistHellinger)) - if mc_ref and pce: - seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean)) - seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std)) - - if pce and any(LOO < mod_LOO_threshold - for LOO in ModifiedLOO): - break - - print(f"Memory itr {itr_no}: I: {m_2-m_1:.2f} MB") - print(f"Memory itr {itr_no}: II: {m_3-m_2:.2f} MB") - print(f"Memory itr {itr_no}: III: {m_4-m_3:.2f} MB") - print(f"Memory itr {itr_no}: IV: {m_5-m_4:.2f} MB") - m_6 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - print(f"Memory itr {itr_no}: total: {m_6:.2f} MB") - - # Clean up - if len(obs_data) != 0: - del out - gc.collect() - print() - print('-'*50) - print() - - # Store updated ModifiedLOO and BME in dictonary - strKey = f'{util_f}_rep_{repIdx+1}' - if pce: - MetaModel.SeqModifiedLOO[strKey] = SeqModifiedLOO - if len(MetaModel.valid_model_runs) != 0: - MetaModel.seqValidError[strKey] = SeqValidError - - # Check if data is provided - if len(obs_data) != 0: - MetaModel.SeqBME[strKey] = SeqBME - MetaModel.SeqKLD[strKey] = SeqKLD - if len(MetaModel.valid_likelihoods) != 0: - MetaModel.SeqDistHellinger[strKey] = SeqDistHellinger - if mc_ref and pce: - MetaModel.seqRMSEMean[strKey] = seqRMSEMean - MetaModel.seqRMSEStd[strKey] = seqRMSEStd - - return MetaModel - - # ------------------------------------------------------------------------- - def util_VarBasedDesign(self, X_can, index, util_func='Entropy'): - """ - Computes the exploitation scores based on: - active learning MacKay(ALM) and active learning Cohn (ALC) - Paper: Sequential Design with Mutual Information for Computer - Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas - (2016) - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - index : int - Model output index. - UtilMethod : string, optional - Exploitation utility function. The default is 'Entropy'. - - Returns - ------- - float - Score. - - """ - MetaModel = self.MetaModel - ED_X = MetaModel.ExpDesign.X - out_dict_y = MetaModel.ExpDesign.Y - out_names = MetaModel.ModelObj.Output.names - - # Run the Metamodel for the candidate - X_can = X_can.reshape(1, -1) - Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can) - - if util_func.lower() == 'alm': - # ----- Entropy/MMSE/active learning MacKay(ALM) ----- - # Compute perdiction variance of the old model - canPredVar = {key: std_PC_can[key]**2 for key in out_names} - - varPCE = np.zeros((len(out_names), X_can.shape[0])) - for KeyIdx, key in enumerate(out_names): - varPCE[KeyIdx] = np.max(canPredVar[key], axis=1) - score = np.max(varPCE, axis=0) - - elif util_func.lower() == 'eigf': - # ----- Expected Improvement for Global fit ----- - # Find closest EDX to the candidate - distances = distance.cdist(ED_X, X_can, 'euclidean') - index = np.argmin(distances) - - # Compute perdiction error and variance of the old model - predError = {key: Y_PC_can[key] for key in out_names} - canPredVar = {key: std_PC_can[key]**2 for key in out_names} - - # Compute perdiction error and variance of the old model - # Eq (5) from Liu et al.(2018) - EIGF_PCE = np.zeros((len(out_names), X_can.shape[0])) - for KeyIdx, key in enumerate(out_names): - residual = predError[key] - out_dict_y[key][int(index)] - var = canPredVar[key] - EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1) - score = np.max(EIGF_PCE, axis=0) - - return -1 * score # -1 is for minimization instead of maximization - - # ------------------------------------------------------------------------- - def util_BayesianActiveDesign(self, X_can, sigma2Dict, var='DKL'): - """ - Computes scores based on Bayesian active design criterion (var). - - It is based on the following paper: - Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak. - "Bayesian3 active learning for the gaussian process emulator using - information theory." Entropy 22, no. 8 (2020): 890. - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - var : string, optional - BAL design criterion. The default is 'DKL'. - - Returns - ------- - float - Score. - - """ - - # Evaluate the PCE metamodels at that location ??? - Y_mean_can, Y_std_can = self.MetaModel.eval_metamodel( - samples=np.array([X_can]) - ) - - # Get the data - obs_data = self.observations - n_obs = self.Model.n_obs - # TODO: Analytical DKL - # Sample a distribution for a normal dist - # with Y_mean_can as the mean and Y_std_can as std. - - # priorMean, priorSigma2, Obs = np.empty((0)),np.empty((0)),np.empty((0)) - - # for key in list(Y_mean_can): - # # concatenate the measurement error - # Obs = np.hstack((Obs,ObservationData[key])) - - # # concatenate the mean and variance of prior predictive - # means, stds = Y_mean_can[key][0], Y_std_can[key][0] - # priorMean = np.hstack((priorSigma2,means)) - # priorSigma2 = np.hstack((priorSigma2,stds**2)) - - # # Covariance Matrix of prior - # covPrior = np.zeros((priorSigma2.shape[0], priorSigma2.shape[0]), float) - # np.fill_diagonal(covPrior, priorSigma2) - - # # Covariance Matrix of Likelihood - # covLikelihood = np.zeros((sigma2Dict.shape[0], sigma2Dict.shape[0]), float) - # np.fill_diagonal(covLikelihood, sigma2Dict) - - # # Calculate moments of the posterior (Analytical derivation) - # n = priorSigma2.shape[0] - # covPost = np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))),covLikelihood/n) - - # meanPost = np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))) , Obs) + \ - # np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))), - # priorMean/n) - # # Compute DKL from prior to posterior - # term1 = np.trace(np.dot(np.linalg.inv(covPrior),covPost)) - # deltaMean = priorMean-meanPost - # term2 = np.dot(np.dot(deltaMean,np.linalg.inv(covPrior)),deltaMean[:,None]) - # term3 = np.log(np.linalg.det(covPrior)/np.linalg.det(covPost)) - # DKL = 0.5 * (term1 + term2 - n + term3)[0] - - # ---------- Inner MC simulation for computing Utility Value ---------- - # Estimation of the integral via Monte Varlo integration - MCsize = 20000 - ESS = 0 - - while ((ESS > MCsize) or (ESS < 1)): - - # Sample a distribution for a normal dist - # with Y_mean_can as the mean and Y_std_can as std. - Y_MC, std_MC = {}, {} - logPriorLikelihoods = np.zeros((MCsize)) - for key in list(Y_mean_can): - means, stds = Y_mean_can[key][0], Y_std_can[key][0] - # cov = np.zeros((means.shape[0], means.shape[0]), float) - # np.fill_diagonal(cov, stds**2) - - Y_MC[key] = np.zeros((MCsize, n_obs)) - logsamples = np.zeros((MCsize, n_obs)) - for i in range(n_obs): - NormalDensity = stats.norm(means[i], stds[i]) - Y_MC[key][:, i] = NormalDensity.rvs(MCsize) - logsamples[:, i] = NormalDensity.logpdf(Y_MC[key][:, i]) - - logPriorLikelihoods = np.sum(logsamples, axis=1) - std_MC[key] = np.zeros((MCsize, means.shape[0])) - - # Likelihood computation (Comparison of data and simulation - # results via PCE with candidate design) - likelihoods = self.__normpdf(Y_MC, std_MC, obs_data, sigma2Dict) - - # Check the Effective Sample Size (1<ESS<MCsize) - ESS = 1 / np.sum(np.square(likelihoods/np.nansum(likelihoods))) - - # Enlarge sample size if it doesn't fulfill the criteria - if ((ESS > MCsize) or (ESS < 1)): - MCsize *= 10 - ESS = 0 - - # Rejection Step - # Random numbers between 0 and 1 - unif = np.random.rand(1, MCsize)[0] - - # Reject the poorly performed prior - accepted = (likelihoods/np.max(likelihoods)) >= unif - - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods)) - - # Posterior-based expectation of likelihoods - postLikelihoods = likelihoods[accepted] - postExpLikelihoods = np.mean(np.log(postLikelihoods)) - - # Posterior-based expectation of prior densities - postExpPrior = np.mean(logPriorLikelihoods[accepted]) - - # Utility function Eq.2 in Ref. (2) - # Posterior covariance matrix after observing data y - # Kullback-Leibler Divergence (Sergey's paper) - if var == 'DKL': - - # TODO: Calculate the correction factor for BME - # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can, - # ObservationData, sigma2Dict) - # BME += BMECorrFactor - # Haun et al implementation - # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME) - U_J_d = postExpLikelihoods - logBME - - # Marginal log likelihood - elif var == 'BME': - U_J_d = logBME - - # Entropy-based information gain - elif var == 'infEntropy': - logBME = np.log(np.nanmean(likelihoods)) - infEntropy = logBME - postExpPrior - postExpLikelihoods - U_J_d = infEntropy * -1 # -1 for minimization - - # Bayesian information criterion - elif var == 'BIC': - coeffs = self.MetaModel.coeffs_dict.values() - nModelParams = max(len(v) for val in coeffs for v in val.values()) - maxL = np.nanmax(likelihoods) - U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams - - # Akaike information criterion - elif var == 'AIC': - coeffs = self.MetaModel.coeffs_dict.values() - nModelParams = max(len(v) for val in coeffs for v in val.values()) - maxlogL = np.log(np.nanmax(likelihoods)) - AIC = -2 * maxlogL + 2 * nModelParams - # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1) - penTerm = 0 - U_J_d = 1*(AIC + penTerm) - - # Deviance information criterion - elif var == 'DIC': - # D_theta_bar = np.mean(-2 * Likelihoods) - N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0])) - Likelihoods_theta_mean = self.__normpdf( - Y_mean_can, Y_std_can, obs_data, sigma2Dict - ) - DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p - - U_J_d = DIC - - else: - print('The algorithm you requested has not been implemented yet!') - - # Handle inf and NaN (replace by zero) - if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf: - U_J_d = 0.0 - - # Clear memory - del likelihoods - del Y_MC - del std_MC - gc.collect(generation=2) - - return -1 * U_J_d # -1 is for minimization instead of maximization - - # ------------------------------------------------------------------------- - def update_metamodel(self, MetaModel, output, y_hat_can, univ_p_val, index, - new_pca=False): - BasisIndices = MetaModel.basis_dict[output]["y_"+str(index+1)] - clf_poly = MetaModel.clf_poly[output]["y_"+str(index+1)] - Mn = clf_poly.coef_ - Sn = clf_poly.sigma_ - beta = clf_poly.alpha_ - active = clf_poly.active_ - Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val) - - Sn_new_inv = np.linalg.inv(Sn) - Sn_new_inv += beta * np.dot(Psi[:, active].T, Psi[:, active]) - Sn_new = np.linalg.inv(Sn_new_inv) - - Mn_new = np.dot(Sn_new_inv, Mn[active]).reshape(-1, 1) - Mn_new += beta * np.dot(Psi[:, active].T, y_hat_can) - Mn_new = np.dot(Sn_new, Mn_new).flatten() - - # Compute the old and new moments of PCEs - mean_old = Mn[0] - mean_new = Mn_new[0] - std_old = np.sqrt(np.sum(np.square(Mn[1:]))) - std_new = np.sqrt(np.sum(np.square(Mn_new[1:]))) - - # Back transformation if PCA is selected. - if MetaModel.dim_red_method.lower() == 'pca': - old_pca = MetaModel.pca[output] - mean_old = old_pca.mean_[index] - mean_old += np.sum(mean_old * old_pca.components_[:, index]) - std_old = np.sqrt(np.sum(std_old**2 * - old_pca.components_[:, index]**2)) - mean_new = new_pca.mean_[index] - mean_new += np.sum(mean_new * new_pca.components_[:, index]) - std_new = np.sqrt(np.sum(std_new**2 * - new_pca.components_[:, index]**2)) - # print(f"mean_old: {mean_old:.2f} mean_new: {mean_new:.2f}") - # print(f"std_old: {std_old:.2f} std_new: {std_new:.2f}") - # Store the old and new moments of PCEs - results = { - 'mean_old': mean_old, - 'mean_new': mean_new, - 'std_old': std_old, - 'std_new': std_new - } - return results - - # ------------------------------------------------------------------------- - def util_BayesianDesign_old(self, X_can, X_MC, sigma2Dict, var='DKL'): - """ - Computes scores based on Bayesian sequential design criterion (var). - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - var : string, optional - Bayesian design criterion. The default is 'DKL'. - - Returns - ------- - float - Score. - - """ - - # To avoid changes ub original aPCE object - Model = self.Model - MetaModel = deepcopy(self.MetaModel) - old_EDY = MetaModel.ExpDesign.Y - - # Evaluate the PCE metamodels using the candidate design - Y_PC_can, Y_std_can = self.MetaModel.eval_metamodel( - samples=np.array([X_can]) - ) - - # Generate y from posterior predictive - m_size = 100 - y_hat_samples = {} - for idx, key in enumerate(Model.Output.names): - means, stds = Y_PC_can[key][0], Y_std_can[key][0] - y_hat_samples[key] = np.random.multivariate_normal( - means, np.diag(stds), m_size) - - # Create the SparseBayes-based PCE metamodel: - MetaModel.input_obj.poly_coeffs_flag = False - univ_p_val = self.MetaModel.univ_basis_vals(X_can) - G_n_m_all = np.zeros((m_size, len(Model.Output.names), Model.n_obs)) - - for i in range(m_size): - for idx, key in enumerate(Model.Output.names): - if MetaModel.dim_red_method.lower() == 'pca': - # Equal number of components - new_outputs = np.vstack( - (old_EDY[key], y_hat_samples[key][i]) - ) - new_pca, _ = MetaModel.pca_transformation(new_outputs) - target = new_pca.transform( - y_hat_samples[key][i].reshape(1, -1) - )[0] - else: - new_pca, target = False, y_hat_samples[key][i] - - for j in range(len(target)): - - # Update surrogate - result = self.update_metamodel( - MetaModel, key, target[j], univ_p_val, j, new_pca) - - # Compute Expected Information Gain (Eq. 39) - G_n_m = np.log(result['std_old']/result['std_new']) - 1./2 - G_n_m += result['std_new']**2 / (2*result['std_old']**2) - G_n_m += (result['mean_new'] - result['mean_old'])**2 /\ - (2*result['std_old']**2) - - G_n_m_all[i, idx, j] = G_n_m - - U_J_d = G_n_m_all.mean(axis=(1, 2)).mean() - return -1 * U_J_d - - # ------------------------------------------------------------------------- - def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'): - """ - Computes scores based on Bayesian sequential design criterion (var). - - Parameters - ---------- - X_can : array of shape (n_samples, n_params) - Candidate samples. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - var : string, optional - Bayesian design criterion. The default is 'DKL'. - - Returns - ------- - float - Score. - - """ - - # To avoid changes ub original aPCE object - Model = self.Model - MetaModel = deepcopy(self.MetaModel) - out_names = MetaModel.ModelObj.Output.names - if X_can.ndim == 1: - X_can = X_can.reshape(1, -1) - - # Compute the mean and std based on the MetaModel - # pce_means, pce_stds = self._compute_pce_moments(MetaModel) - if var == 'ALC': - Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC) - - # Old Experimental design - oldExpDesignX = MetaModel.ExpDesign.X - oldExpDesignY = MetaModel.ExpDesign.Y - - # Evaluate the PCE metamodels at that location ??? - Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can) - - # Add all suggestion as new ExpDesign - NewExpDesignX = np.vstack((oldExpDesignX, X_can)) - - NewExpDesignY = {} - for key in oldExpDesignY.keys(): - try: - NewExpDesignY[key] = np.vstack((oldExpDesignY[key], - Y_PC_can[key])) - except: - NewExpDesignY[key] = oldExpDesignY[key] - - MetaModel.ExpDesign.sampling_method = 'user' - MetaModel.ExpDesign.X = NewExpDesignX - MetaModel.ExpDesign.Y = NewExpDesignY - - # Train the model for the observed data using x_can - MetaModel.input_obj.poly_coeffs_flag = False - MetaModel.train_norm_design(parallel=False) - PCE_Model_can = MetaModel - - if var.lower() == 'mi': - # Mutual information based on Krause et al - # Adapted from Beck & Guillas (MICE) paper - _, std_PC_can = PCE_Model_can.eval_metamodel(samples=X_can) - std_can = {key: std_PC_can[key] for key in out_names} - - std_old = {key: Y_std_can[key] for key in out_names} - - varPCE = np.zeros((len(out_names))) - for i, key in enumerate(out_names): - varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2) - score = np.mean(varPCE) - - return -1 * score - - elif var.lower() == 'alc': - # Active learning based on Gramyc and Lee - # Adaptive design and analysis of supercomputer experiments Techno- - # metrics, 51 (2009), pp. 130–145. - - # Evaluate the MetaModel at the given samples - Y_MC_can, Y_MC_std_can = PCE_Model_can.eval_metamodel(samples=X_MC) - - # Compute the score - score = [] - for i, key in enumerate(out_names): - pce_var = Y_MC_std_can[key]**2 - pce_var_can = Y_MC_std[key]**2 - score.append(np.mean(pce_var-pce_var_can, axis=0)) - score = np.mean(score) - - return -1 * score - - # ---------- Inner MC simulation for computing Utility Value ---------- - # Estimation of the integral via Monte Varlo integration - MCsize = X_MC.shape[0] - ESS = 0 - - while ((ESS > MCsize) or (ESS < 1)): - - # Enriching Monte Carlo samples if need be - if ESS != 0: - X_MC = self.MetaModel.ExpDesign.generate_samples( - MCsize, 'random' - ) - - # Evaluate the MetaModel at the given samples - Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC) - - # Likelihood computation (Comparison of data and simulation - # results via PCE with candidate design) - likelihoods = self.__normpdf( - Y_MC, std_MC, self.observations, sigma2Dict - ) - - # Check the Effective Sample Size (1<ESS<MCsize) - ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods))) - - # Enlarge sample size if it doesn't fulfill the criteria - if ((ESS > MCsize) or (ESS < 1)): - print("--- increasing MC size---") - MCsize *= 10 - ESS = 0 - - # Rejection Step - # Random numbers between 0 and 1 - unif = np.random.rand(1, MCsize)[0] - - # Reject the poorly performed prior - accepted = (likelihoods/np.max(likelihoods)) >= unif - - # -------------------- Utility functions -------------------- - # Utility function Eq.2 in Ref. (2) - # Kullback-Leibler Divergence (Sergey's paper) - if var == 'DKL': - - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods, dtype=np.float128)) - - # Posterior-based expectation of likelihoods - postLikelihoods = likelihoods[accepted] - postExpLikelihoods = np.mean(np.log(postLikelihoods)) - - # Haun et al implementation - U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME) - - # U_J_d = np.sum(G_n_m_all) - # Ryan et al (2014) implementation - # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0]) - # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME - - # U_J_d = postExpLikelihoods - logBME - - # Marginal likelihood - elif var == 'BME': - - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods)) - U_J_d = logBME - - # Bayes risk likelihood - elif var == 'BayesRisk': - - U_J_d = -1 * np.var(likelihoods) - - # Entropy-based information gain - elif var == 'infEntropy': - # Prior-based estimation of BME - logBME = np.log(np.nanmean(likelihoods)) - - # Posterior-based expectation of likelihoods - postLikelihoods = likelihoods[accepted] / np.nansum(likelihoods[accepted]) - postExpLikelihoods = np.mean(np.log(postLikelihoods)) - - # Posterior-based expectation of prior densities - postExpPrior = np.mean(logPriorLikelihoods[accepted]) - - infEntropy = logBME - postExpPrior - postExpLikelihoods - - U_J_d = infEntropy * -1 # -1 for minimization - - # D-Posterior-precision - elif var == 'DPP': - X_Posterior = X_MC[accepted] - # covariance of the posterior parameters - U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior))) - - # A-Posterior-precision - elif var == 'APP': - X_Posterior = X_MC[accepted] - # trace of the posterior parameters - U_J_d = -np.log(np.trace(np.cov(X_Posterior))) - - else: - print('The algorithm you requested has not been implemented yet!') - - # Clear memory - del likelihoods - del Y_MC - del std_MC - gc.collect(generation=2) - - return -1 * U_J_d # -1 is for minimization instead of maximization - - # ------------------------------------------------------------------------- - def subdomain(self, Bounds, n_new_samples): - """ - Divides a domain defined by Bounds into sub domains. - - Parameters - ---------- - Bounds : list of tuples - List of lower and upper bounds. - n_new_samples : TYPE - DESCRIPTION. - - Returns - ------- - Subdomains : TYPE - DESCRIPTION. - - """ - n_params = self.MetaModel.n_params - n_subdomains = n_new_samples + 1 - LinSpace = np.zeros((n_params, n_subdomains)) - - for i in range(n_params): - LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1], - num=n_subdomains) - Subdomains = [] - for k in range(n_subdomains-1): - mylist = [] - for i in range(n_params): - mylist.append((LinSpace[i, k+0], LinSpace[i, k+1])) - Subdomains.append(tuple(mylist)) - - return Subdomains - - # ------------------------------------------------------------------------- - def run_util_func(self, method, candidates, index, sigma2Dict=None, - var=None, X_MC=None): - """ - Runs the utility function based on the given method. - - Parameters - ---------- - method : string - Exploitation method: `VarOptDesign`, `BayesActDesign` and - `BayesOptDesign`. - candidates : array of shape (n_samples, n_params) - All candidate parameter sets. - index : int - ExpDesign index. - sigma2Dict : dict, optional - A dictionary containing the measurement errors (sigma^2). The - default is None. - var : string, optional - Utility function. The default is None. - X_MC : TYPE, optional - DESCRIPTION. The default is None. - - Returns - ------- - index : TYPE - DESCRIPTION. - List - Scores. - - """ - - if method.lower() == 'varoptdesign': - # U_J_d = self.util_VarBasedDesign(candidates, index, var) - U_J_d = np.zeros((candidates.shape[0])) - for idx, X_can in tqdm(enumerate(candidates), ascii=True, - desc="varoptdesign"): - U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var) - - elif method.lower() == 'bayesactdesign': - NCandidate = candidates.shape[0] - U_J_d = np.zeros((NCandidate)) - for idx, X_can in tqdm(enumerate(candidates), ascii=True, - desc="OptBayesianDesign"): - U_J_d[idx] = self.util_BayesianActiveDesign(X_can, sigma2Dict, - var) - elif method.lower() == 'bayesoptdesign': - NCandidate = candidates.shape[0] - U_J_d = np.zeros((NCandidate)) - for idx, X_can in tqdm(enumerate(candidates), ascii=True, - desc="OptBayesianDesign"): - U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict, - var) - return (index, -1 * U_J_d) - - # ------------------------------------------------------------------------- - def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No, - verbose=False): - """ - Exploration algorithim to find the optimum parameter space. - - Parameters - ---------- - method : string - Exploitation method: `VarOptDesign`, `BayesActDesign` and - `BayesOptDesign`. - Bounds : list of tuples - List of lower and upper boundaries of parameters. - sigma2Dict : dict - A dictionary containing the measurement errors (sigma^2). - Run_No : int - Run number. - verbose : bool, optional - Print out a summary. The default is False. - - Returns - ------- - Run_No : int - Run number. - array - Optimial candidate. - - """ - - Model = self.Model - max_func_itr = self.MetaModel.ExpDesign.max_func_itr - - if method == 'VarOptDesign': - Res_Global = opt.dual_annealing(self.util_VarBasedDesign, - bounds=Bounds, - args=(Model, var), - maxfun=max_func_itr) - - elif method == 'BayesOptDesign': - Res_Global = opt.dual_annealing(self.util_BayesianDesign, - bounds=Bounds, - args=(Model, sigma2Dict, var), - maxfun=max_func_itr) - - if verbose: - print(f"global minimum: xmin = {Res_Global.x}, " - f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}") - - return (Run_No, Res_Global.x) - - # ------------------------------------------------------------------------- - def tradoff_weights(self, tradeoff_scheme, old_EDX, old_EDY): - """ - Calculates weights for exploration scores based on the requested - scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`. - - `None`: No exploration. - `equal`: Same weights for exploration and exploitation scores. - `epsilon-decreasing`: Start with more exploration and increase the - influence of exploitation along the way with a exponential decay - function - `adaptive`: An adaptive method based on: - Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling - approach for Kriging metamodeling by maximizing expected prediction - error." Computers & Chemical Engineering 106 (2017): 171-182. - - Parameters - ---------- - tradeoff_scheme : string - Trade-off scheme for exloration and exploitation scores. - old_EDX : array (n_samples, n_params) - Old experimental design (training points). - old_EDY : dict - Old model responses (targets). - - Returns - ------- - exploration_weight : float - Exploration weight. - exploitation_weight: float - Exploitation weight. - - """ - if tradeoff_scheme is None: - exploration_weight = 0 - - elif tradeoff_scheme == 'equal': - exploration_weight = 0.5 - - elif tradeoff_scheme == 'epsilon-decreasing': - # epsilon-decreasing scheme - # Start with more exploration and increase the influence of - # exploitation along the way with a exponential decay function - initNSamples = self.MetaModel.ExpDesign.n_init_samples - n_max_samples = self.MetaModel.ExpDesign.n_max_samples - - itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples) - itrNumber //= self.MetaModel.ExpDesign.n_new_samples - - tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8) - exploration_weight = signal.exponential(n_max_samples-initNSamples, - 0, tau2, False)[itrNumber] - - elif tradeoff_scheme == 'adaptive': - - # Extract itrNumber - initNSamples = self.MetaModel.ExpDesign.n_init_samples - n_max_samples = self.MetaModel.ExpDesign.n_max_samples - itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples) - itrNumber //= self.MetaModel.ExpDesign.n_new_samples - - if itrNumber == 0: - exploration_weight = 0.5 - else: - # New adaptive trade-off according to Liu et al. (2017) - # Mean squared error for last design point - last_EDX = old_EDX[-1].reshape(1, -1) - lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX) - pce_y = np.array(list(lastPCEY.values()))[:, 0] - y = np.array(list(old_EDY.values()))[:, -1, :] - mseError = mean_squared_error(pce_y, y) - - # Mean squared CV - error for last design point - pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0] - mseCVError = mean_squared_error(pce_y_prev, y) - - exploration_weight = min([0.5*mseError/mseCVError, 1]) - - # Exploitation weight - exploitation_weight = 1 - exploration_weight - - return exploration_weight, exploitation_weight - - # ------------------------------------------------------------------------- - def opt_SeqDesign(self, sigma2, n_candidates=5, var='DKL'): - """ - Runs optimal sequential design. - - Parameters - ---------- - sigma2 : dict, optional - A dictionary containing the measurement errors (sigma^2). The - default is None. - n_candidates : int, optional - Number of candidate samples. The default is 5. - var : string, optional - Utility function. The default is None. - - Raises - ------ - NameError - Wrong utility function. - - Returns - ------- - Xnew : array (n_samples, n_params) - Selected new training point(s). - """ - - # Initialization - MetaModel = self.MetaModel - Bounds = MetaModel.bound_tuples - n_new_samples = MetaModel.ExpDesign.n_new_samples - explore_method = MetaModel.ExpDesign.explore_method - exploit_method = MetaModel.ExpDesign.exploit_method - n_cand_groups = MetaModel.ExpDesign.n_cand_groups - tradeoff_scheme = MetaModel.ExpDesign.tradeoff_scheme - - old_EDX = MetaModel.ExpDesign.X - old_EDY = MetaModel.ExpDesign.Y.copy() - ndim = MetaModel.ExpDesign.X.shape[1] - OutputNames = MetaModel.ModelObj.Output.names - - # ----------------------------------------- - # ----------- CUSTOMIZED METHODS ---------- - # ----------------------------------------- - # Utility function exploit_method provided by user - if exploit_method.lower() == 'user': - - Xnew, filteredSamples = MetaModel.ExpDesign.ExploitFunction(self) - - print("\n") - print("\nXnew:\n", Xnew) - - return Xnew, filteredSamples - - # ----------------------------------------- - # ---------- EXPLORATION METHODS ---------- - # ----------------------------------------- - if explore_method == 'dual annealing': - # ------- EXPLORATION: OPTIMIZATION ------- - import time - start_time = time.time() - - # Divide the domain to subdomains - args = [] - subdomains = self.subdomain(Bounds, n_new_samples) - for i in range(n_new_samples): - args.append((exploit_method, subdomains[i], sigma2, var, i)) - - # Multiprocessing - pool = multiprocessing.Pool(multiprocessing.cpu_count()) - - # With Pool.starmap_async() - results = pool.starmap_async(self.dual_annealing, args).get() - - # Close the pool - pool.close() - - Xnew = np.array([results[i][1] for i in range(n_new_samples)]) - - print("\nXnew:\n", Xnew) - - elapsed_time = time.time() - start_time - print("\n") - print(f"elapsed_time: {round(elapsed_time,2)} sec.") - print('-'*20) - - elif explore_method == 'LOOCV': - # ----------------------------------------------------------------- - # TODO: LOOCV model construnction based on Feng et al. (2020) - # 'LOOCV': - # Initilize the ExploitScore array - - # Generate random samples - allCandidates = MetaModel.ExpDesign.generate_samples(n_candidates, - 'random') - - # Construct error model based on LCerror - errorModel = MetaModel.create_ModelError(old_EDX, self.LCerror) - self.errorModel.append(copy(errorModel)) - - # Evaluate the error models for allCandidates - eLCAllCands, _ = errorModel.eval_errormodel(allCandidates) - # Select the maximum as the representative error - eLCAllCands = np.dstack(eLCAllCands.values()) - eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0] - - # Normalize the error w.r.t the maximum error - scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates) - - else: - # ------- EXPLORATION: SPACE-FILLING DESIGN ------- - # Generate candidate samples from Exploration class - explore = Exploration(MetaModel, n_candidates) - explore.w = 100 # * ndim #500 - # Select criterion (mc-intersite-proj-th, mc-intersite-proj) - explore.mc_criterion = 'mc-intersite-proj' - allCandidates, scoreExploration = explore.get_exploration_samples() - - # Temp: ---- Plot all candidates ----- - if ndim == 2: - def plotter(points, allCandidates, Method, - scoreExploration=None): - if Method == 'Voronoi': - from scipy.spatial import Voronoi, voronoi_plot_2d - vor = Voronoi(points) - fig = voronoi_plot_2d(vor) - ax1 = fig.axes[0] - else: - fig = plt.figure() - ax1 = fig.add_subplot(111) - ax1.scatter(points[:, 0], points[:, 1], s=10, c='r', - marker="s", label='Old Design Points') - ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10, - c='b', marker="o", label='Design candidates') - for i in range(points.shape[0]): - txt = 'p'+str(i+1) - ax1.annotate(txt, (points[i, 0], points[i, 1])) - if scoreExploration is not None: - for i in range(allCandidates.shape[0]): - txt = str(round(scoreExploration[i], 5)) - ax1.annotate(txt, (allCandidates[i, 0], - allCandidates[i, 1])) - - plt.xlim(self.bound_tuples[0]) - plt.ylim(self.bound_tuples[1]) - # plt.show() - plt.legend(loc='upper left') - - # ----------------------------------------- - # --------- EXPLOITATION METHODS ---------- - # ----------------------------------------- - if exploit_method == 'BayesOptDesign' or\ - exploit_method == 'BayesActDesign': - - # ------- Calculate Exoploration weight ------- - # Compute exploration weight based on trade off scheme - explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme, - old_EDX, - old_EDY) - print(f"\n Exploration weight={explore_w:0.3f} " - f"Exploitation weight={exploit_w:0.3f}\n") - - # ------- EXPLOITATION: BayesOptDesign & ActiveLearning ------- - if explore_w != 1.0: - - # Create a sample pool for rejection sampling - MCsize = 15000 - X_MC = MetaModel.ExpDesign.generate_samples(MCsize, 'random') - candidates = MetaModel.ExpDesign.generate_samples( - MetaModel.ExpDesign.max_func_itr, 'latin_hypercube') - - # Split the candidates in groups for multiprocessing - split_cand = np.array_split( - candidates, n_cand_groups, axis=0 - ) - - results = Parallel(n_jobs=-1, backend='threading')( - delayed(self.run_util_func)( - exploit_method, split_cand[i], i, sigma2, var, X_MC) - for i in range(n_cand_groups)) - # out = map(self.run_util_func, - # [exploit_method]*n_cand_groups, - # split_cand, - # range(n_cand_groups), - # [sigma2] * n_cand_groups, - # [var] * n_cand_groups, - # [X_MC] * n_cand_groups - # ) - # results = list(out) - - # Retrieve the results and append them - U_J_d = np.concatenate([results[NofE][1] for NofE in - range(n_cand_groups)]) - - # Check if all scores are inf - if np.isinf(U_J_d).all() or np.isnan(U_J_d).all(): - U_J_d = np.ones(len(U_J_d)) - - # Get the expected value (mean) of the Utility score - # for each cell - if explore_method == 'Voronoi': - U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1) - - # create surrogate model for U_J_d - from sklearn.preprocessing import MinMaxScaler - # Take care of inf entries - good_indices = [i for i, arr in enumerate(U_J_d) - if np.isfinite(arr).all()] - scaler = MinMaxScaler() - X_S = scaler.fit_transform(candidates[good_indices]) - gp = MetaModel.gaussian_process_emulator( - X_S, U_J_d[good_indices], autoSelect=True - ) - U_J_d = gp.predict(scaler.transform(allCandidates)) - - # Normalize U_J_d - norm_U_J_d = U_J_d / np.sum(U_J_d) - print("norm_U_J_d:\n", norm_U_J_d) - else: - norm_U_J_d = np.zeros((len(scoreExploration))) - - # ------- Calculate Total score ------- - # ------- Trade off between EXPLORATION & EXPLOITATION ------- - # Total score - totalScore = exploit_w * norm_U_J_d - totalScore += explore_w * scoreExploration - - # temp: Plot - # dim = self.ExpDesign.X.shape[1] - # if dim == 2: - # plotter(self.ExpDesign.X, allCandidates, explore_method) - - # ------- Select the best candidate ------- - # find an optimal point subset to add to the initial design by - # maximization of the utility score and taking care of NaN values - temp = totalScore.copy() - temp[np.isnan(totalScore)] = -np.inf - sorted_idxtotalScore = np.argsort(temp)[::-1] - bestIdx = sorted_idxtotalScore[:n_new_samples] - - # select the requested number of samples - if explore_method == 'Voronoi': - Xnew = np.zeros((n_new_samples, ndim)) - for i, idx in enumerate(bestIdx): - X_can = explore.closestPoints[idx] - - # Calculate the maxmin score for the region of interest - newSamples, maxminScore = explore.get_mc_samples(X_can) - - # select the requested number of samples - Xnew[i] = newSamples[np.argmax(maxminScore)] - else: - Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]] - - elif exploit_method == 'VarOptDesign': - # ------- EXPLOITATION: VarOptDesign ------- - UtilMethod = var - - # ------- Calculate Exoploration weight ------- - # Compute exploration weight based on trade off scheme - explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme, - old_EDX, - old_EDY) - print(f"\nweightExploration={explore_w:0.3f} " - f"weightExploitation={exploit_w:0.3f}") - - # Generate candidate samples from Exploration class - nMeasurement = old_EDY[OutputNames[0]].shape[1] - - # Find sensitive region - if UtilMethod == 'LOOCV': - LCerror = MetaModel.LCerror - allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames), - nMeasurement)) - for y_idx, y_key in enumerate(OutputNames): - for idx, key in enumerate(LCerror[y_key].keys()): - allModifiedLOO[:, y_idx, idx] = abs( - LCerror[y_key][key]) - - ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1) - - elif UtilMethod in ['EIGF', 'ALM']: - # ----- All other in ['EIGF', 'ALM'] ----- - # Initilize the ExploitScore array - ExploitScore = np.zeros((len(old_EDX), len(OutputNames))) - - # Split the candidates in groups for multiprocessing - if explore_method != 'Voronoi': - split_cand = np.array_split(allCandidates, - n_cand_groups, - axis=0) - goodSampleIdx = range(n_cand_groups) - else: - # Find indices of the Vornoi cells with samples - goodSampleIdx = [] - for idx in range(len(explore.closest_points)): - if len(explore.closest_points[idx]) != 0: - goodSampleIdx.append(idx) - split_cand = explore.closest_points - - # Split the candidates in groups for multiprocessing - args = [] - for index in goodSampleIdx: - args.append((exploit_method, split_cand[index], index, - sigma2, var)) - - # Multiprocessing - pool = multiprocessing.Pool(multiprocessing.cpu_count()) - # With Pool.starmap_async() - results = pool.starmap_async(self.run_util_func, args).get() - - # Close the pool - pool.close() - # out = map(self.run_util_func, - # [exploit_method]*len(goodSampleIdx), - # split_cand, - # range(len(goodSampleIdx)), - # [sigma2] * len(goodSampleIdx), - # [var] * len(goodSampleIdx) - # ) - # results = list(out) - - # Retrieve the results and append them - if explore_method == 'Voronoi': - ExploitScore = [np.mean(results[k][1]) for k in - range(len(goodSampleIdx))] - else: - ExploitScore = np.concatenate( - [results[k][1] for k in range(len(goodSampleIdx))]) - - else: - raise NameError('The requested utility function is not ' - 'available.') - - # print("ExploitScore:\n", ExploitScore) - - # find an optimal point subset to add to the initial design by - # maximization of the utility score and taking care of NaN values - # Total score - # Normalize U_J_d - ExploitScore = ExploitScore / np.sum(ExploitScore) - totalScore = exploit_w * ExploitScore - totalScore += explore_w * scoreExploration - - temp = totalScore.copy() - sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1] - bestIdx = sorted_idxtotalScore[:n_new_samples] - - Xnew = np.zeros((n_new_samples, ndim)) - if explore_method != 'Voronoi': - Xnew = allCandidates[bestIdx] - else: - for i, idx in enumerate(bestIdx.flatten()): - X_can = explore.closest_points[idx] - # plotter(self.ExpDesign.X, X_can, explore_method, - # scoreExploration=None) - - # Calculate the maxmin score for the region of interest - newSamples, maxminScore = explore.get_mc_samples(X_can) - - # select the requested number of samples - Xnew[i] = newSamples[np.argmax(maxminScore)] - - elif exploit_method == 'alphabetic': - # ------- EXPLOITATION: ALPHABETIC ------- - Xnew = self.util_AlphOptDesign(allCandidates, var) - - elif exploit_method == 'Space-filling': - # ------- EXPLOITATION: SPACE-FILLING ------- - totalScore = scoreExploration - - # ------- Select the best candidate ------- - # find an optimal point subset to add to the initial design by - # maximization of the utility score and taking care of NaN values - temp = totalScore.copy() - temp[np.isnan(totalScore)] = -np.inf - sorted_idxtotalScore = np.argsort(temp)[::-1] - - # select the requested number of samples - Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]] - - else: - raise NameError('The requested design method is not available.') - - print("\n") - print("\nRun No. {}:".format(old_EDX.shape[0]+1)) - print("Xnew:\n", Xnew) - gc.collect() - - return Xnew, None - - # ------------------------------------------------------------------------- - def util_AlphOptDesign(self, candidates, var='D-Opt'): - """ - Enriches the Experimental design with the requested alphabetic - criterion based on exploring the space with number of sampling points. - - Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos - expansion: A review of sampling strategies., Computer Methods in - Applied Mechanics and Engineering, 332, 382-407. - - Arguments - --------- - NCandidate : int - Number of candidate points to be searched - - var : string - Alphabetic optimality criterion - - Returns - ------- - X_new : array of shape (1, n_params) - The new sampling location in the input space. - """ - MetaModelOrig = self - Model = self.Model - n_new_samples = MetaModelOrig.ExpDesign.n_new_samples - NCandidate = candidates.shape[0] - - # TODO: Loop over outputs - OutputName = Model.Output.names[0] - - # To avoid changes ub original aPCE object - MetaModel = deepcopy(MetaModelOrig) - - # Old Experimental design - oldExpDesignX = MetaModel.ExpDesign.X - - # TODO: Only one psi can be selected. - # Suggestion: Go for the one with the highest LOO error - Scores = list(MetaModel.score_dict[OutputName].values()) - ModifiedLOO = [1-score for score in Scores] - outIdx = np.argmax(ModifiedLOO) - - # Initialize Phi to save the criterion's values - Phi = np.zeros((NCandidate)) - - BasisIndices = MetaModelOrig.basis_dict[OutputName]["y_"+str(outIdx+1)] - P = len(BasisIndices) - - # ------ Old Psi ------------ - univ_p_val = MetaModelOrig.univ_basis_vals(oldExpDesignX) - Psi = MetaModelOrig.create_psi(BasisIndices, univ_p_val) - - # ------ New candidates (Psi_c) ------------ - # Assemble Psi_c - univ_p_val_c = self.univ_basis_vals(candidates) - Psi_c = self.create_psi(BasisIndices, univ_p_val_c) - - for idx in range(NCandidate): - - # Include the new row to the original Psi - Psi_cand = np.vstack((Psi, Psi_c[idx])) - - # Information matrix - PsiTPsi = np.dot(Psi_cand.T, Psi_cand) - M = PsiTPsi / (len(oldExpDesignX)+1) - - if np.linalg.cond(PsiTPsi) > 1e-12 \ - and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon: - # faster - invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray()) - else: - # stabler - invM = np.linalg.pinv(M) - - # ---------- Calculate optimality criterion ---------- - # Optimality criteria according to Section 4.5.1 in Ref. - - # D-Opt - if var == 'D-Opt': - Phi[idx] = (np.linalg.det(invM)) ** (1/P) - - # A-Opt - elif var == 'A-Opt': - Phi[idx] = np.trace(invM) - - # K-Opt - elif var == 'K-Opt': - Phi[idx] = np.linalg.cond(M) - - else: - raise Exception('The optimality criterion you requested has ' - 'not been implemented yet!') - - # find an optimal point subset to add to the initial design - # by minimization of the Phi - sorted_idxtotalScore = np.argsort(Phi) - - # select the requested number of samples - Xnew = candidates[sorted_idxtotalScore[:n_new_samples]] - - return Xnew - - # ------------------------------------------------------------------------- - def __normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s, - rmse=None): - - Model = self.Model - likelihoods = 1.0 - - # Loop over the outputs - for idx, out in enumerate(Model.Output.names): - - # (Meta)Model Output - nsamples, nout = y_hat_pce[out].shape - - # Prepare data and remove NaN - try: - data = obs_data[out].values[~np.isnan(obs_data[out])] - except AttributeError: - data = obs_data[out][~np.isnan(obs_data[out])] - - # Prepare sigma2s - non_nan_indices = ~np.isnan(total_sigma2s[out]) - tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values - - # Surrogate error if valid dataset is given. - if rmse is not None: - tot_sigma2s += rmse[out]**2 - - likelihoods *= stats.multivariate_normal.pdf( - y_hat_pce[out], data, np.diag(tot_sigma2s), - allow_singular=True) - self.Likelihoods = likelihoods - - return likelihoods - - # ------------------------------------------------------------------------- - def __corr_factor_BME(self, obs_data, total_sigma2s, logBME): - """ - Calculates the correction factor for BMEs. - """ - MetaModel = self.MetaModel - samples = MetaModel.ExpDesign.X # valid_samples - model_outputs = MetaModel.ExpDesign.Y # valid_model_runs - Model = MetaModel.ModelObj - n_samples = samples.shape[0] - - # Extract the requested model outputs for likelihood calulation - output_names = Model.Output.names - - # TODO: Evaluate MetaModel on the experimental design and ValidSet - OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples) - - logLik_data = np.zeros((n_samples)) - logLik_model = np.zeros((n_samples)) - # Loop over the outputs - for idx, out in enumerate(output_names): - - # (Meta)Model Output - nsamples, nout = model_outputs[out].shape - - # Prepare data and remove NaN - try: - data = obs_data[out].values[~np.isnan(obs_data[out])] - except AttributeError: - data = obs_data[out][~np.isnan(obs_data[out])] - - # Prepare sigma2s - non_nan_indices = ~np.isnan(total_sigma2s[out]) - tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout] - - # Covariance Matrix - covMatrix_data = np.diag(tot_sigma2s) - - for i, sample in enumerate(samples): - - # Simulation run - y_m = model_outputs[out][i] - - # Surrogate prediction - y_m_hat = OutputRS[out][i] - - # CovMatrix with the surrogate error - # covMatrix = np.diag(stdOutputRS[out][i]**2) - covMatrix = np.diag((y_m-y_m_hat)**2) - covMatrix = np.diag( - np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2 - ) - - # Compute likelilhood output vs data - logLik_data[i] += self.__logpdf( - y_m_hat, data, covMatrix_data - ) - - # Compute likelilhood output vs surrogate - logLik_model[i] += self.__logpdf(y_m_hat, y_m, covMatrix) - - # Weight - logLik_data -= logBME - weights = np.exp(logLik_model+logLik_data) - - return np.log(np.mean(weights)) - - # ------------------------------------------------------------------------- - def __logpdf(self, x, mean, cov): - """ - computes the likelihood based on a multivariate normal distribution. - - Parameters - ---------- - x : TYPE - DESCRIPTION. - mean : array_like - Observation data. - cov : 2d array - Covariance matrix of the distribution. - - Returns - ------- - log_lik : float - Log likelihood. - - """ - n = len(mean) - L = linalg.cholesky(cov, lower=True) - beta = np.sum(np.log(np.diag(L))) - dev = x - mean - alpha = dev.dot(linalg.cho_solve((L, True), dev)) - log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi) - - return log_lik - - # ------------------------------------------------------------------------- - def __posteriorPlot(self, posterior, par_names, key): - - # Initialization - newpath = (r'Outputs_SeqPosteriorComparison/posterior') - os.makedirs(newpath, exist_ok=True) - - bound_tuples = self.MetaModel.bound_tuples - n_params = len(par_names) - font_size = 40 - if n_params == 2: - - figPosterior, ax = plt.subplots(figsize=(15, 15)) - - sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1], - fill=True, ax=ax, cmap=plt.cm.jet, - clip=bound_tuples) - # Axis labels - plt.xlabel(par_names[0], fontsize=font_size) - plt.ylabel(par_names[1], fontsize=font_size) - - # Set axis limit - plt.xlim(bound_tuples[0]) - plt.ylim(bound_tuples[1]) - - # Increase font size - plt.xticks(fontsize=font_size) - plt.yticks(fontsize=font_size) - - # Switch off the grids - plt.grid(False) - - else: - import corner - figPosterior = corner.corner(posterior, labels=par_names, - title_fmt='.2e', show_titles=True, - title_kwargs={"fontsize": 12}) - - figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight') - plt.close() - - # Save the posterior as .npy - np.save(f'./{newpath}/{key}.npy', posterior) - - return figPosterior - - # ------------------------------------------------------------------------- - def __hellinger_distance(self, P, Q): - """ - Hellinger distance between two continuous distributions. - - The maximum distance 1 is achieved when P assigns probability zero to - every set to which Q assigns a positive probability, and vice versa. - 0 (identical) and 1 (maximally different) - - Parameters - ---------- - P : array - Reference likelihood. - Q : array - Estimated likelihood. - - Returns - ------- - float - Hellinger distance of two distributions. - - """ - mu1 = P.mean() - Sigma1 = np.std(P) - - mu2 = Q.mean() - Sigma2 = np.std(Q) - - term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2)) - - term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2)) - - H_squared = 1 - term1 * term2 - - return np.sqrt(H_squared) - - # ------------------------------------------------------------------------- - def __BME_Calculator(self, MetaModel, obs_data, sigma2Dict, rmse=None): - """ - This function computes the Bayesian model evidence (BME) via Monte - Carlo integration. - - """ - # Initializations - valid_likelihoods = MetaModel.valid_likelihoods - - post_snapshot = MetaModel.ExpDesign.post_snapshot - if post_snapshot or len(valid_likelihoods) != 0: - newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref') - os.makedirs(newpath, exist_ok=True) - - SamplingMethod = 'random' - MCsize = 10000 - ESS = 0 - - # Estimation of the integral via Monte Varlo integration - while (ESS > MCsize) or (ESS < 1): - - # Generate samples for Monte Carlo simulation - X_MC = MetaModel.ExpDesign.generate_samples( - MCsize, SamplingMethod - ) - - # Monte Carlo simulation for the candidate design - m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - Y_MC, std_MC = MetaModel.eval_metamodel(samples=X_MC) - m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - print(f"\nMemory eval_metamodel in BME: {m_2-m_1:.2f} MB") - - # Likelihood computation (Comparison of data and - # simulation results via PCE with candidate design) - Likelihoods = self.__normpdf( - Y_MC, std_MC, obs_data, sigma2Dict, rmse - ) - - # Check the Effective Sample Size (1000<ESS<MCsize) - ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods))) - - # Enlarge sample size if it doesn't fulfill the criteria - if (ESS > MCsize) or (ESS < 1): - print(f'ESS={ESS} MC size should be larger.') - MCsize *= 10 - ESS = 0 - - # Rejection Step - # Random numbers between 0 and 1 - unif = np.random.rand(1, MCsize)[0] - - # Reject the poorly performed prior - accepted = (Likelihoods/np.max(Likelihoods)) >= unif - X_Posterior = X_MC[accepted] - - # ------------------------------------------------------------ - # --- Kullback-Leibler Divergence & Information Entropy ------ - # ------------------------------------------------------------ - # Prior-based estimation of BME - logBME = np.log(np.nanmean(Likelihoods)) - - # TODO: Correction factor - # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME) - - # Posterior-based expectation of likelihoods - postExpLikelihoods = np.mean(np.log(Likelihoods[accepted])) - - # Posterior-based expectation of prior densities - postExpPrior = np.mean( - np.log(MetaModel.ExpDesign.JDist.pdf(X_Posterior.T)) - ) - - # Calculate Kullback-Leibler Divergence - # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME) - KLD = postExpLikelihoods - logBME - - # Information Entropy based on Entropy paper Eq. 38 - infEntropy = logBME - postExpPrior - postExpLikelihoods - - # If post_snapshot is True, plot likelihood vs refrence - if post_snapshot or len(valid_likelihoods) != 0: - # Hellinger distance - ref_like = np.log(valid_likelihoods[valid_likelihoods > 0]) - est_like = np.log(Likelihoods[Likelihoods > 0]) - distHellinger = self.__hellinger_distance(ref_like, est_like) - - idx = len([name for name in os.listdir(newpath) if 'Likelihoods_' - in name and os.path.isfile(os.path.join(newpath, name))]) - fig, ax = plt.subplots() - try: - sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]), - shade=True, color="g", label='Ref. Likelihood') - sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True, - color="b", label='Likelihood with PCE') - except: - pass - - text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}" - "\n DKL={KLD:.3f}" - - plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat', - edgecolor='black', - boxstyle='round,pad=1'), - transform=ax.transAxes) - - fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf', - bbox_inches='tight') - plt.close() - - else: - distHellinger = 0.0 - - # Bayesian inference with Emulator only for 2D problem - if post_snapshot and MetaModel.n_params == 2 and not idx % 5: - from bayes_inference.bayes_inference import BayesInference - from bayes_inference.discrepancy import Discrepancy - import pandas as pd - BayesOpts = BayesInference(MetaModel) - BayesOpts.emulator = True - BayesOpts.plot_post_pred = False - - # Select the inference method - import emcee - BayesOpts.inference_method = "MCMC" - # Set the MCMC parameters passed to self.mcmc_params - BayesOpts.mcmc_params = { - 'n_steps': 1e5, - 'n_walkers': 30, - 'moves': emcee.moves.KDEMove(), - 'verbose': False - } - - # ----- Define the discrepancy model ------- - obs_data = pd.DataFrame(obs_data, columns=self.Model.Output.names) - BayesOpts.measurement_error = obs_data - - # # -- (Option B) -- - DiscrepancyOpts = Discrepancy('') - DiscrepancyOpts.type = 'Gaussian' - DiscrepancyOpts.parameters = obs_data**2 - BayesOpts.Discrepancy = DiscrepancyOpts - # Start the calibration/inference - Bayes_PCE = BayesOpts.create_inference() - X_Posterior = Bayes_PCE.posterior_df.values - - # Clean up - del Y_MC, std_MC - gc.collect() - - return (logBME, KLD, X_Posterior, Likelihoods, distHellinger) - - # ------------------------------------------------------------------------- - def __validError(self, MetaModel): - - # MetaModel = self.MetaModel - Model = MetaModel.ModelObj - OutputName = Model.Output.names - - # Extract the original model with the generated samples - valid_samples = MetaModel.valid_samples - valid_model_runs = MetaModel.valid_model_runs - - # Run the PCE model with the generated samples - m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - valid_PCE_runs, valid_PCE_std = MetaModel.eval_metamodel(samples=valid_samples) - m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 - print(f"\nMemory eval_metamodel: {m_2-m_1:.2f} MB") - - rms_error = {} - valid_error = {} - # Loop over the keys and compute RMSE error. - for key in OutputName: - rms_error[key] = mean_squared_error( - valid_model_runs[key], valid_PCE_runs[key], - multioutput='raw_values', - sample_weight=None, - squared=False) - - # Validation error - valid_error[key] = (rms_error[key]**2) - valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0) - - # Print a report table - print("\n>>>>> Updated Errors of {} <<<<<".format(key)) - print("\nIndex | RMSE | Validation Error") - print('-'*35) - print('\n'.join(f'{i+1} | {k:.3e} | {j:.3e}' for i, (k, j) - in enumerate(zip(rms_error[key], - valid_error[key])))) - - return rms_error, valid_error - - # ------------------------------------------------------------------------- - def __error_Mean_Std(self): - - MetaModel = self.MetaModel - # Extract the mean and std provided by user - df_MCReference = MetaModel.ModelObj.mc_reference - - # Compute the mean and std based on the MetaModel - pce_means, pce_stds = self._compute_pce_moments(MetaModel) - - # Compute the root mean squared error - for output in MetaModel.ModelObj.Output.names: - - # Compute the error between mean and std of MetaModel and OrigModel - RMSE_Mean = mean_squared_error( - df_MCReference['mean'], pce_means[output], squared=False - ) - RMSE_std = mean_squared_error( - df_MCReference['std'], pce_means[output], squared=False - ) - - return RMSE_Mean, RMSE_std - - # ------------------------------------------------------------------------- - def _compute_pce_moments(self, MetaModel): - """ - Computes the first two moments using the PCE-based meta-model. - - Returns - ------- - pce_means: dict - The first moment (mean) of the surrogate. - pce_stds: dict - The second moment (standard deviation) of the surrogate. - - """ - outputs = MetaModel.ModelObj.Output.names - pce_means_b = {} - pce_stds_b = {} - - # Loop over bootstrap iterations - for b_i in range(MetaModel.n_bootstrap_itrs): - # Loop over the metamodels - coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items() - means = {} - stds = {} - for output, coef_dict in coeffs_dicts: - - pce_mean = np.zeros((len(coef_dict))) - pce_var = np.zeros((len(coef_dict))) - - for index, values in coef_dict.items(): - idx = int(index.split('_')[1]) - 1 - coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index] - - # Mean = c_0 - if coeffs[0] != 0: - pce_mean[idx] = coeffs[0] - else: - clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output] - pce_mean[idx] = clf_poly[index].intercept_ - # Var = sum(coeffs[1:]**2) - pce_var[idx] = np.sum(np.square(coeffs[1:])) - - # Save predictions for each output - if MetaModel.dim_red_method.lower() == 'pca': - PCA = MetaModel.pca[f'b_{b_i+1}'][output] - means[output] = PCA.mean_ + np.dot( - pce_mean, PCA.components_) - stds[output] = np.sqrt(np.dot(pce_var, - PCA.components_**2)) - else: - means[output] = pce_mean - stds[output] = np.sqrt(pce_var) - - # Save predictions for each bootstrap iteration - pce_means_b[b_i] = means - pce_stds_b[b_i] = stds - - # Change the order of nesting - mean_all = {} - for i in sorted(pce_means_b): - for k, v in pce_means_b[i].items(): - if k not in mean_all: - mean_all[k] = [None] * len(pce_means_b) - mean_all[k][i] = v - std_all = {} - for i in sorted(pce_stds_b): - for k, v in pce_stds_b[i].items(): - if k not in std_all: - std_all[k] = [None] * len(pce_stds_b) - std_all[k][i] = v - - # Back transformation if PCA is selected. - pce_means, pce_stds = {}, {} - for output in outputs: - pce_means[output] = np.mean(mean_all[output], axis=0) - pce_stds[output] = np.mean(std_all[output], axis=0) - - return pce_means, pce_stds diff --git a/examples/only-model/bayesvalidrox/surrogate_models/surrogate_models.py b/examples/only-model/bayesvalidrox/surrogate_models/surrogate_models.py deleted file mode 100644 index fc6b83947f6b51cd1aee297fe07794a3a31332d6..0000000000000000000000000000000000000000 --- a/examples/only-model/bayesvalidrox/surrogate_models/surrogate_models.py +++ /dev/null @@ -1,1498 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import warnings -import numpy as np -import math -import h5py -import matplotlib.pyplot as plt -from sklearn.preprocessing import MinMaxScaler -import scipy as sp -from tqdm import tqdm -from sklearn.decomposition import PCA as sklearnPCA -import sklearn.linear_model as lm -from sklearn.gaussian_process import GaussianProcessRegressor -import sklearn.gaussian_process.kernels as kernels -import os -from joblib import Parallel, delayed -import copy - -from bayesvalidrox.surrogate_models.exp_designs import ExpDesigns -from bayesvalidrox.surrogate_models.glexindex import glexindex -from bayesvalidrox.surrogate_models.eval_rec_rule import eval_univ_basis -from bayesvalidrox.surrogate_models.reg_fast_ard import RegressionFastARD -from bayesvalidrox.surrogate_models.reg_fast_laplace import RegressionFastLaplace -from bayesvalidrox.surrogate_models.orthogonal_matching_pursuit import OrthogonalMatchingPursuit -from bayesvalidrox.surrogate_models.bayes_linear import VBLinearRegression, EBLinearRegression -warnings.filterwarnings("ignore") -# Load the mplstyle -#plt.style.use(os.path.join(os.path.split(__file__)[0], -# '../', 'bayesvalidrox.mplstyle')) - - -class MetaModel(): - """ - Meta (surrogate) model - - This class trains a surrogate model. It accepts an input object (input_obj) - containing the specification of the distributions for uncertain parameters - and a model object with instructions on how to run the computational model. - - Attributes - ---------- - input_obj : obj - Input object with the information on the model input parameters. - meta_model_type : str - Surrogate model types. Three surrogate model types are supported: - polynomial chaos expansion (`PCE`), arbitrary PCE (`aPCE`) and - Gaussian process regression (`GPE`). Default is PCE. - pce_reg_method : str - PCE regression method to compute the coefficients. The following - regression methods are available: - - 1. OLS: Ordinary Least Square method - 2. BRR: Bayesian Ridge Regression - 3. LARS: Least angle regression - 4. ARD: Bayesian ARD Regression - 5. FastARD: Fast Bayesian ARD Regression - 6. VBL: Variational Bayesian Learning - 7. EBL: Emperical Bayesian Learning - Default is `OLS`. - bootstrap_method : str - Bootstraping method. Options are `'normal'` and `'fast'`. The default - is `'fast'`. It means that in each iteration except the first one, only - the coefficent are recalculated with the ordinary least square method. - n_bootstrap_itrs : int - Number of iterations for the bootstrap sampling. The default is `1`. - pce_deg : int or list of int - Polynomial degree(s). If a list is given, an adaptive algorithm is used - to find the best degree with the lowest Leave-One-Out cross-validation - (LOO) error (or the highest score=1-LOO). Default is `1`. - pce_q_norm : float - Hyperbolic (or q-norm) truncation for multi-indices of multivariate - polynomials. Default is `1.0`. - dim_red_method : str - Dimensionality reduction method for the output space. The available - method is based on principal component analysis (PCA). The Default is - `'no'`. There are two ways to select number of components: use - percentage of the explainable variance threshold (between 0 and 100) - (Option A) or direct prescription of components' number (Option B): - - >>> MetaModelOpts.dim_red_method = 'PCA' - >>> MetaModelOpts.var_pca_threshold = 99.999 # Option A - >>> MetaModelOpts.n_pca_components = 12 # Option B - - verbose : bool - Prints summary of the regression results. Default is `False`. - - Note - ------- - To define the sampling methods and the training set, an experimental design - instance shall be defined. This can be done by: - - >>> MetaModelOpts.add_ExpDesign() - - Two experimental design schemes are supported: one-shot (`normal`) and - adaptive sequential (`sequential`) designs. - For experimental design refer to `ExpDesigns`. - - """ - - def __init__(self, input_obj, model_obj = 'None', meta_model_type='PCE', - pce_reg_method='OLS', bootstrap_method='fast', - n_bootstrap_itrs=1, pce_deg=1, pce_q_norm=1.0, - dim_red_method='no', verbose=False): # added the 'None' behind model_obj - - self.input_obj = input_obj - self.ModelObj = model_obj - self.meta_model_type = meta_model_type - self.pce_reg_method = pce_reg_method - self.bootstrap_method = bootstrap_method - self.n_bootstrap_itrs = n_bootstrap_itrs - self.pce_deg = pce_deg - self.pce_q_norm = pce_q_norm - self.dim_red_method = dim_red_method - self.verbose = False - - # ------------------------------------------------------------------------- - def create_metamodel(self, ModelObj = None): # added ModelObj here - """ - Starts the training of the meta-model for the model objects containg - the given computational model. - - Returns - ------- - metamodel : obj - The meta model object. - - """ - if ModelObj: - self.ModelObj = ModelObj - Model = self.ModelObj - self.n_params = len(self.input_obj.Marginals) - self.ExpDesignFlag = 'normal' - # --- Prepare pce degree --- - if self.meta_model_type.lower() == 'pce': - if type(self.pce_deg) is not np.ndarray: - self.pce_deg = np.array(self.pce_deg) - - if self.ExpDesign.method == 'sequential': - raise Exception( - "Please use MetaModelEngine class for the sequential design!" - ) - - elif self.ExpDesign.method == 'normal': - self.train_norm_design(Model, verbose=True) - - else: - raise Exception("The method for experimental design you requested" - " has not been implemented yet.") - - # Zip the model run directories - if self.ModelObj.link_type.lower() == 'pylink' and\ - self.ExpDesign.sampling_method.lower() != 'user': - Model.zip_subdirs(Model.name, f'{Model.name}_') - - return self - - # ------------------------------------------------------------------------- - def train_norm_design(self, parallel=False, verbose=False): - """ - This function loops over the outputs and each time step/point and fits - the meta model. - - Parameters - ---------- - parallel : bool - The parallel computation of coefficents. The default is True. - verbose : bool, optional - Flag for a sequential design in silent mode. The default is False. - - Returns - ------- - self: obj - Meta-model object. - - """ - self.ExpDesignFlag = 'normal' - Model = self.ModelObj - # Get the collocation points to run the forward model - CollocationPoints, OutputDict = self.generate_ExpDesign(Model) - - # Initialize the nested dictionaries - if self.meta_model_type.lower() == 'gpe': - self.gp_poly = self.auto_vivification() - self.x_scaler = self.auto_vivification() - self.LCerror = self.auto_vivification() - else: - self.deg_dict = self.auto_vivification() - self.q_norm_dict = self.auto_vivification() - self.coeffs_dict = self.auto_vivification() - self.basis_dict = self.auto_vivification() - self.score_dict = self.auto_vivification() - self.clf_poly = self.auto_vivification() - self.LCerror = self.auto_vivification() - if self.dim_red_method.lower() == 'pca': - self.pca = self.auto_vivification() - - # Define an array containing the degrees - n_samples, ndim = CollocationPoints.shape - self.deg_array = self.__select_degree(ndim, n_samples) - - # Generate all basis indices - self.allBasisIndices = self.auto_vivification() - for deg in self.deg_array: - keys = self.allBasisIndices.keys() - if deg not in np.fromiter(keys, dtype=float): - # Generate the polynomial basis indices - for qidx, q in enumerate(self.pce_q_norm): - basis_indices = self.create_basis_indices(degree=deg, - q_norm=q) - self.allBasisIndices[str(deg)][str(q)] = basis_indices - - # Evaluate the univariate polynomials on ExpDesign - if self.meta_model_type.lower() != 'gpe': - univ_p_val = self.univ_basis_vals(CollocationPoints) # TODO: issue appears in here: 'ExpDesigns' object has no attribute 'polycoeffs' - - if 'x_values' in OutputDict: - self.ExpDesign.x_values = OutputDict['x_values'] - del OutputDict['x_values'] - - # --- Loop through data points and fit the surrogate --- - if verbose: - print(f"\n>>>> Training the {self.meta_model_type} metamodel " - "started. <<<<<<\n") - - # --- Bootstrap sampling --- - # Correct number of bootstrap if PCA transformation is required. - if self.dim_red_method.lower() == 'pca' and self.n_bootstrap_itrs == 1: - self.n_bootstrap_itrs = 100 - - # Check if fast version (update coeffs with OLS) is selected. - if self.bootstrap_method.lower() == 'fast': - fast_bootstrap = True - first_out = {} - n_comp_dict = {} - else: - fast_bootstrap = False - - # Prepare tqdm iteration maessage - if verbose and self.n_bootstrap_itrs > 1: - enum_obj = tqdm(range(self.n_bootstrap_itrs), - total=self.n_bootstrap_itrs, - desc="Boostraping the metamodel", - ascii=True) - else: - enum_obj = range(self.n_bootstrap_itrs) - - # Loop over the bootstrap iterations - for b_i in enum_obj: - if b_i > 0: - b_indices = np.random.randint(n_samples, size=n_samples) - else: - b_indices = np.arange(len(CollocationPoints)) - - X_train_b = CollocationPoints[b_indices] - - if verbose and self.n_bootstrap_itrs == 1: - items = tqdm(OutputDict.items(), desc="Fitting regression") - else: - items = OutputDict.items() - - # For loop over the components/outputs - for key, Output in items: - - # Dimensionality reduction with PCA, if specified - if self.dim_red_method.lower() == 'pca': - - # Use the stored n_comp for fast bootsrtrapping - if fast_bootstrap and b_i > 0: - self.n_pca_components = n_comp_dict[key] - - # Start transformation - pca, target, n_comp = self.pca_transformation( - Output[b_indices], verbose=False - ) - self.pca[f'b_{b_i+1}'][key] = pca - # Store the number of components for fast bootsrtrapping - if fast_bootstrap and b_i == 0: - n_comp_dict[key] = n_comp - else: - target = Output[b_indices] - - # Parallel fit regression - if self.meta_model_type.lower() == 'gpe': - # Prepare the input matrix - scaler = MinMaxScaler() - X_S = scaler.fit_transform(X_train_b) - - self.x_scaler[f'b_{b_i+1}'][key] = scaler - if parallel: - out = Parallel(n_jobs=-1, backend='multiprocessing')( - delayed(self.gaussian_process_emulator)( - X_S, target[:, idx]) for idx in - range(target.shape[1])) - else: - results = map(self.gaussian_process_emulator, - [X_train_b]*target.shape[1], - [target[:, idx] for idx in - range(target.shape[1])] - ) - out = list(results) - - for idx in range(target.shape[1]): - self.gp_poly[f'b_{b_i+1}'][key][f"y_{idx+1}"] = out[idx] - - else: - self.univ_p_val = univ_p_val[b_indices] - if parallel and (not fast_bootstrap or b_i == 0): - out = Parallel(n_jobs=-1, backend='multiprocessing')( - delayed(self.adaptive_regression)(X_train_b, - target[:, idx], - idx) - for idx in range(target.shape[1])) - elif not parallel and (not fast_bootstrap or b_i == 0): - results = map(self.adaptive_regression, - [X_train_b]*target.shape[1], - [target[:, idx] for idx in - range(target.shape[1])], - range(target.shape[1])) - out = list(results) - - # Store the first out dictionary - if fast_bootstrap and b_i == 0: - first_out[key] = copy.deepcopy(out) - - if b_i > 0 and fast_bootstrap: - - # fast bootstrap - out = self.update_pce_coeffs( - X_train_b, target, first_out[key]) - - for i in range(target.shape[1]): - # Create a dict to pass the variables - self.deg_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['degree'] - self.q_norm_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['qnorm'] - self.coeffs_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['coeffs'] - self.basis_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['multi_indices'] - self.score_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['LOOCVScore'] - self.clf_poly[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['clf_poly'] - #self.LCerror[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['LCerror'] - - if verbose: - print(f"\n>>>> Training the {self.meta_model_type} metamodel" - " sucessfully completed. <<<<<<\n") - - # ------------------------------------------------------------------------- - def update_pce_coeffs(self, X, y, out_dict): - """ - Updates the PCE coefficents using only the ordinary least square method - for the fast version of the bootsrtrapping. - - Parameters - ---------- - X : array of shape (n_samples, n_params) - Training set. - y : array of shape (n_samples, n_outs) - The (transformed) model responses. - out_dict : dict - The training output dictionary of the first iteration, i.e. - the surrogate model for the original experimental design. - - Returns - ------- - final_out_dict : dict - The updated training output dictionary. - - """ - # Make a copy - final_out_dict = copy.deepcopy(out_dict) - - # Loop over the points - for i in range(y.shape[1]): - - # Extract nonzero basis indices - nnz_idx = np.nonzero(out_dict[i]['coeffs'])[0] - if len(nnz_idx) != 0: - basis_indices = out_dict[i]['multi_indices'] - - # Evaluate the multivariate polynomials on CollocationPoints - psi = self.create_psi(basis_indices, self.univ_p_val) - - # Calulate the cofficients of surrogate model - updated_out = self.fit( - psi, y[:, i], basis_indices, reg_method='OLS', - sparsity=False - ) - - # Update coeffs in out_dict - final_out_dict[i]['coeffs'][nnz_idx] = updated_out['coeffs'] - - return final_out_dict - - # ------------------------------------------------------------------------- - def create_basis_indices(self, degree, q_norm): - """ - Creates set of selected multi-indices of multivariate polynomials for - certain parameter numbers, polynomial degree, hyperbolic (or q-norm) - truncation scheme. - - Parameters - ---------- - degree : int - Polynomial degree. - q_norm : float - hyperbolic (or q-norm) truncation. - - Returns - ------- - basis_indices : array of shape (n_terms, n_params) - Multi-indices of multivariate polynomials. - - """ - basis_indices = glexindex(start=0, stop=degree+1, - dimensions=self.n_params, - cross_truncation=q_norm, - reverse=False, graded=True) - return basis_indices - - # ------------------------------------------------------------------------- - def add_ExpDesign(self): - """ - Instanciates experimental design object. - - Returns - ------- - None. - - """ - self.ExpDesign = ExpDesigns(self.input_obj, - meta_Model=self.meta_model_type) - - # ------------------------------------------------------------------------- - def generate_ExpDesign(self, Model): - """ - Prepares the experimental design either by reading from the prescribed - data or running simulations. - - Parameters - ---------- - Model : obj - Model object. - - Raises - ------ - Exception - If model sumulations are not provided properly. - - Returns - ------- - ED_X_tr: array of shape (n_samples, n_params) - Training samples transformed by an isoprobabilistic transformation. - ED_Y: dict - Model simulations (target) for all outputs. - """ - ExpDesign = self.ExpDesign - if self.ExpDesignFlag != 'sequential': - # Read ExpDesign (training and targets) from the provided hdf5 - if ExpDesign.hdf5_file is not None: - - # Read hdf5 file - f = h5py.File(ExpDesign.hdf5_file, 'r+') - - # Read EDX and pass it to ExpDesign object - try: - ExpDesign.X = np.array(f["EDX/New_init_"]) - except KeyError: - ExpDesign.X = np.array(f["EDX/init_"]) - - # Update number of initial samples - ExpDesign.n_init_samples = ExpDesign.X.shape[0] - - # Read EDX and pass it to ExpDesign object - out_names = self.ModelObj.Output.names - ExpDesign.Y = {} - - # Extract x values - try: - ExpDesign.Y["x_values"] = dict() - for varIdx, var in enumerate(out_names): - x = np.array(f[f"x_values/{var}"]) - ExpDesign.Y["x_values"][var] = x - except KeyError: - ExpDesign.Y["x_values"] = np.array(f["x_values"]) - - # Store the output - for varIdx, var in enumerate(out_names): - try: - y = np.array(f[f"EDY/{var}/New_init_"]) - except KeyError: - y = np.array(f[f"EDY/{var}/init_"]) - ExpDesign.Y[var] = y - f.close() - else: # changed from else - # Check if an old hdf5 file exists: if yes, rename it - hdf5file = f'ExpDesign_{self.ModelObj.name}.hdf5' - if os.path.exists(hdf5file): - os.rename(hdf5file, 'old_'+hdf5file) - - # ---- Prepare X samples ---- - ED_X, ED_X_tr = ExpDesign.generate_ED(ExpDesign.n_init_samples, - ExpDesign.sampling_method, - transform=True, - max_pce_deg=np.max(self.pce_deg)) - ExpDesign.X = ED_X - ExpDesign.collocationPoints = ED_X_tr - self.bound_tuples = ExpDesign.bound_tuples - - # ---- Run simulations at X ---- - if not hasattr(ExpDesign, 'Y') or ExpDesign.Y is None: - print('\n Now the forward model needs to be run!\n') - ED_Y, up_ED_X = Model.run_model_parallel(ED_X) - ExpDesign.X = up_ED_X - self.ModelOutputDict = ED_Y - ExpDesign.Y = ED_Y - else: - # Check if a dict has been passed. - if type(ExpDesign.Y) is dict: - self.ModelOutputDict = ExpDesign.Y - else: - raise Exception('Please provide either a dictionary or a hdf5' - 'file to ExpDesign.hdf5_file argument.') - - return ED_X_tr, self.ModelOutputDict - - # ------------------------------------------------------------------------- - def univ_basis_vals(self, samples, n_max=None): - """ - Evaluates univariate regressors along input directions. - - Parameters - ---------- - samples : array of shape (n_samples, n_params) - Samples. - n_max : int, optional - Maximum polynomial degree. The default is `None`. - - Returns - ------- - univ_basis: array of shape (n_samples, n_params, n_max+1) - All univariate regressors up to n_max. - """ - # Extract information - poly_types = self.ExpDesign.poly_types - if samples.ndim != 2: - samples = samples.reshape(1, len(samples)) - n_max = np.max(self.pce_deg) if n_max is None else n_max - - # Extract poly coeffs - if self.ExpDesign.input_data_given or self.ExpDesign.apce: - apolycoeffs = self.ExpDesign.polycoeffs - else: - apolycoeffs = None - - # Evaluate univariate basis - univ_basis = eval_univ_basis(samples, n_max, poly_types, apolycoeffs) - - return univ_basis - - # ------------------------------------------------------------------------- - def create_psi(self, basis_indices, univ_p_val): - """ - This function assemble the design matrix Psi from the given basis index - set INDICES and the univariate polynomial evaluations univ_p_val. - - Parameters - ---------- - basis_indices : array of shape (n_terms, n_params) - Multi-indices of multivariate polynomials. - univ_p_val : array of (n_samples, n_params, n_max+1) - All univariate regressors up to `n_max`. - - Raises - ------ - ValueError - n_terms in arguments do not match. - - Returns - ------- - psi : array of shape (n_samples, n_terms) - Multivariate regressors. - - """ - # Check if BasisIndices is a sparse matrix - sparsity = sp.sparse.issparse(basis_indices) - if sparsity: - basis_indices = basis_indices.toarray() - - # Initialization and consistency checks - # number of input variables - n_params = univ_p_val.shape[1] - - # Size of the experimental design - n_samples = univ_p_val.shape[0] - - # number of basis terms - n_terms = basis_indices.shape[0] - - # check that the variables have consistent sizes - if n_params != basis_indices.shape[1]: - raise ValueError( - f"The shapes of basis_indices ({basis_indices.shape[1]}) and " - f"univ_p_val ({n_params}) don't match!!" - ) - - # Preallocate the Psi matrix for performance - psi = np.ones((n_samples, n_terms)) - # Assemble the Psi matrix - for m in range(basis_indices.shape[1]): - aa = np.where(basis_indices[:, m] > 0)[0] - try: - basisIdx = basis_indices[aa, m] - bb = univ_p_val[:, m, basisIdx].reshape(psi[:, aa].shape) - psi[:, aa] = np.multiply(psi[:, aa], bb) - except ValueError as err: - raise err - return psi - - # ------------------------------------------------------------------------- - def fit(self, X, y, basis_indices, reg_method=None, sparsity=True): - """ - Fit regression using the regression method provided. - - Parameters - ---------- - X : array of shape (n_samples, n_features) - Training vector, where n_samples is the number of samples and - n_features is the number of features. - y : array of shape (n_samples,) - Target values. - basis_indices : array of shape (n_terms, n_params) - Multi-indices of multivariate polynomials. - reg_method : str, optional - DESCRIPTION. The default is None. - - Returns - ------- - return_out_dict : Dict - Fitted estimator, spareMulti-Index, sparseX and coefficients. - - """ - if reg_method is None: - reg_method = self.pce_reg_method - - bias_term = self.dim_red_method.lower() != 'pca' - - compute_score = True if self.verbose else False - - # inverse of the observed variance of the data - if np.var(y) != 0: - Lambda = 1 / np.var(y) - else: - Lambda = 1e-6 - - # Bayes sparse adaptive aPCE - if reg_method.lower() == 'ols': - clf_poly = lm.LinearRegression(fit_intercept=False) - elif reg_method.lower() == 'brr': - clf_poly = lm.BayesianRidge(n_iter=1000, tol=1e-7, - fit_intercept=False, - normalize=True, - compute_score=compute_score, - alpha_1=1e-04, alpha_2=1e-04, - lambda_1=Lambda, lambda_2=Lambda) - clf_poly.converged = True - - elif reg_method.lower() == 'ard': - clf_poly = lm.ARDRegression(fit_intercept=False, - normalize=True, - compute_score=compute_score, - n_iter=1000, tol=0.0001, - alpha_1=1e-3, alpha_2=1e-3, - lambda_1=Lambda, lambda_2=Lambda) - - elif reg_method.lower() == 'fastard': - clf_poly = RegressionFastARD(fit_intercept=False, - normalize=True, - compute_score=compute_score, - n_iter=300, tol=1e-10) - - elif reg_method.lower() == 'bcs': - clf_poly = RegressionFastLaplace(fit_intercept=False, - bias_term=bias_term, - n_iter=1000, tol=1e-7) - - elif reg_method.lower() == 'lars': - clf_poly = lm.LassoLarsCV(fit_intercept=False) - - elif reg_method.lower() == 'sgdr': - clf_poly = lm.SGDRegressor(fit_intercept=False, - max_iter=5000, tol=1e-7) - - elif reg_method.lower() == 'omp': - clf_poly = OrthogonalMatchingPursuit(fit_intercept=False) - - elif reg_method.lower() == 'vbl': - clf_poly = VBLinearRegression(fit_intercept=False) - - elif reg_method.lower() == 'ebl': - clf_poly = EBLinearRegression(optimizer='em') - - # Fit - clf_poly.fit(X, y) - - # Select the nonzero entries of coefficients - if sparsity: - nnz_idx = np.nonzero(clf_poly.coef_)[0] - else: - nnz_idx = np.arange(clf_poly.coef_.shape[0]) - - # This is for the case where all outputs are zero, thereby - # all coefficients are zero - if (y == 0).all(): - nnz_idx = np.insert(np.nonzero(clf_poly.coef_)[0], 0, 0) - - sparse_basis_indices = basis_indices[nnz_idx] - sparse_X = X[:, nnz_idx] - coeffs = clf_poly.coef_[nnz_idx] - clf_poly.coef_ = coeffs - - # Create a dict to pass the outputs - return_out_dict = dict() - return_out_dict['clf_poly'] = clf_poly - return_out_dict['spareMulti-Index'] = sparse_basis_indices - return_out_dict['sparePsi'] = sparse_X - return_out_dict['coeffs'] = coeffs - return return_out_dict - - # -------------------------------------------------------------------------------------------------------- - def adaptive_regression(self, ED_X, ED_Y, varIdx, verbose=False): - """ - Adaptively fits the PCE model by comparing the scores of different - degrees and q-norm. - - Parameters - ---------- - ED_X : array of shape (n_samples, n_params) - Experimental design. - ED_Y : array of shape (n_samples,) - Target values, i.e. simulation results for the Experimental design. - varIdx : int - Index of the output. - verbose : bool, optional - Print out summary. The default is False. - - Returns - ------- - returnVars : Dict - Fitted estimator, best degree, best q-norm, LOOCVScore and - coefficients. - - """ - - n_samples, n_params = ED_X.shape - # Initialization - qAllCoeffs, AllCoeffs = {}, {} - qAllIndices_Sparse, AllIndices_Sparse = {}, {} - qAllclf_poly, Allclf_poly = {}, {} - qAllnTerms, AllnTerms = {}, {} - qAllLCerror, AllLCerror = {}, {} - - # Extract degree array and qnorm array - deg_array = np.array([*self.allBasisIndices], dtype=int) - qnorm = [*self.allBasisIndices[str(int(deg_array[0]))]] - - # Some options for EarlyStop - errorIncreases = False - # Stop degree, if LOO error does not decrease n_checks_degree times - n_checks_degree = 3 - # Stop qNorm, if criterion isn't fulfilled n_checks_qNorm times - n_checks_qNorm = 2 - nqnorms = len(qnorm) - qNormEarlyStop = True - if nqnorms < n_checks_qNorm+1: - qNormEarlyStop = False - - # ===================================================================== - # basis adaptive polynomial chaos: repeat the calculation by increasing - # polynomial degree until the highest accuracy is reached - # ===================================================================== - # For each degree check all q-norms and choose the best one - scores = -np.inf * np.ones(deg_array.shape[0]) - qNormScores = -np.inf * np.ones(nqnorms) - - for degIdx, deg in enumerate(deg_array): - - for qidx, q in enumerate(qnorm): - - # Extract the polynomial basis indices from the pool of - # allBasisIndices - BasisIndices = self.allBasisIndices[str(deg)][str(q)] - - # Assemble the Psi matrix - Psi = self.create_psi(BasisIndices, self.univ_p_val) - - # Calulate the cofficients of the meta model - outs = self.fit(Psi, ED_Y, BasisIndices) - - # Calculate and save the score of LOOCV - score, LCerror = self.corr_loocv_error(outs['clf_poly'], - outs['sparePsi'], - outs['coeffs'], - ED_Y) - - # Check the convergence of noise for FastARD - if self.pce_reg_method == 'FastARD' and \ - outs['clf_poly'].alpha_ < np.finfo(np.float32).eps: - score = -np.inf - - qNormScores[qidx] = score - qAllCoeffs[str(qidx+1)] = outs['coeffs'] - qAllIndices_Sparse[str(qidx+1)] = outs['spareMulti-Index'] - qAllclf_poly[str(qidx+1)] = outs['clf_poly'] - qAllnTerms[str(qidx+1)] = BasisIndices.shape[0] - qAllLCerror[str(qidx+1)] = LCerror - - # EarlyStop check - # if there are at least n_checks_qNorm entries after the - # best one, we stop - if qNormEarlyStop and \ - sum(np.isfinite(qNormScores)) > n_checks_qNorm: - # If the error has increased the last two iterations, stop! - qNormScores_nonInf = qNormScores[np.isfinite(qNormScores)] - deltas = np.sign(np.diff(qNormScores_nonInf)) - if sum(deltas[-n_checks_qNorm+1:]) == 2: - # stop the q-norm loop here - break - if np.var(ED_Y) == 0: - break - - # Store the score in the scores list - best_q = np.nanargmax(qNormScores) - scores[degIdx] = qNormScores[best_q] - - AllCoeffs[str(degIdx+1)] = qAllCoeffs[str(best_q+1)] - AllIndices_Sparse[str(degIdx+1)] = qAllIndices_Sparse[str(best_q+1)] - Allclf_poly[str(degIdx+1)] = qAllclf_poly[str(best_q+1)] - AllnTerms[str(degIdx+1)] = qAllnTerms[str(best_q+1)] - AllLCerror[str(degIdx+1)] = qAllLCerror[str(best_q+1)] - - # Check the direction of the error (on average): - # if it increases consistently stop the iterations - if len(scores[scores != -np.inf]) > n_checks_degree: - scores_nonInf = scores[scores != -np.inf] - ss = np.sign(scores_nonInf - np.max(scores_nonInf)) - # ss<0 error decreasing - errorIncreases = np.sum(np.sum(ss[-2:])) <= -1*n_checks_degree - - if errorIncreases: - break - - # Check only one degree, if target matrix has zero variance - if np.var(ED_Y) == 0: - break - - # ------------------ Summary of results ------------------ - # Select the one with the best score and save the necessary outputs - best_deg = np.nanargmax(scores)+1 - coeffs = AllCoeffs[str(best_deg)] - basis_indices = AllIndices_Sparse[str(best_deg)] - clf_poly = Allclf_poly[str(best_deg)] - LOOCVScore = np.nanmax(scores) - P = AllnTerms[str(best_deg)] - LCerror = AllLCerror[str(best_deg)] - degree = deg_array[np.nanargmax(scores)] - qnorm = float(qnorm[best_q]) - - # ------------------ Print out Summary of results ------------------ - if self.verbose: - # Create PSI_Sparse by removing redundent terms - nnz_idx = np.nonzero(coeffs)[0] - BasisIndices_Sparse = basis_indices[nnz_idx] - - print(f'Output variable {varIdx+1}:') - print('The estimation of PCE coefficients converged at polynomial ' - f'degree {deg_array[best_deg-1]} with ' - f'{len(BasisIndices_Sparse)} terms (Sparsity index = ' - f'{round(len(BasisIndices_Sparse)/P, 3)}).') - - print(f'Final ModLOO error estimate: {1-max(scores):.3e}') - print('\n'+'-'*50) - - if verbose: - print('='*50) - print(' '*10 + ' Summary of results ') - print('='*50) - - print("scores:\n", scores) - print("Best score's degree:", self.deg_array[best_deg-1]) - print("NO. of terms:", len(basis_indices)) - print("Sparsity index:", round(len(basis_indices)/P, 3)) - print("Best Indices:\n", basis_indices) - - if self.pce_reg_method in ['BRR', 'ARD']: - fig, ax = plt.subplots(figsize=(12, 10)) - plt.title("Marginal log-likelihood") - plt.plot(clf_poly.scores_, color='navy', linewidth=2) - plt.ylabel("Score") - plt.xlabel("Iterations") - if self.pce_reg_method.lower() == 'bbr': - text = f"$\\alpha={clf_poly.alpha_:.1f}$\n" - f"$\\lambda={clf_poly.lambda_:.3f}$\n" - f"$L={clf_poly.scores_[-1]:.1f}$" - else: - text = f"$\\alpha={clf_poly.alpha_:.1f}$\n$" - f"\\L={clf_poly.scores_[-1]:.1f}$" - - plt.text(0.75, 0.5, text, fontsize=18, transform=ax.transAxes) - plt.show() - print('='*80) - - # Create a dict to pass the outputs - returnVars = dict() - returnVars['clf_poly'] = clf_poly - returnVars['degree'] = degree - returnVars['qnorm'] = qnorm - returnVars['coeffs'] = coeffs - returnVars['multi_indices'] = basis_indices - returnVars['LOOCVScore'] = LOOCVScore - returnVars['LCerror'] = LCerror - - return returnVars - - # ------------------------------------------------------------------------- - def corr_loocv_error(self, clf, psi, coeffs, y): - """ - Calculates the corrected LOO error for regression on regressor - matrix `psi` that generated the coefficients based on [1] and [2]. - - [1] Blatman, G., 2009. Adaptive sparse polynomial chaos expansions for - uncertainty propagation and sensitivity analysis (Doctoral - dissertation, Clermont-Ferrand 2). - - [2] Blatman, G. and Sudret, B., 2011. Adaptive sparse polynomial chaos - expansion based on least angle regression. Journal of computational - Physics, 230(6), pp.2345-2367. - - Parameters - ---------- - clf : object - Fitted estimator. - psi : array of shape (n_samples, n_features) - The multivariate orthogonal polynomials (regressor). - coeffs : array-like of shape (n_features,) - Estimated cofficients. - y : array of shape (n_samples,) - Target values. - - Returns - ------- - R_2 : float - LOOCV Validation score (1-LOOCV erro). - residual : array of shape (n_samples,) - Residual values (y - predicted targets). - - """ - psi = np.array(psi, dtype=float) - - # Create PSI_Sparse by removing redundent terms - nnz_idx = np.nonzero(coeffs)[0] - if len(nnz_idx) == 0: - nnz_idx = [0] - psi_sparse = psi[:, nnz_idx] - - # NrCoeffs of aPCEs - P = len(nnz_idx) - # NrEvaluation (Size of experimental design) - N = psi.shape[0] - - # Build the projection matrix - PsiTPsi = np.dot(psi_sparse.T, psi_sparse) - - if np.linalg.cond(PsiTPsi) > 1e-12: #and \ - # np.linalg.cond(PsiTPsi) < 1/sys.float_info.epsilon: - # faster - M = sp.linalg.solve(PsiTPsi, - sp.sparse.eye(PsiTPsi.shape[0]).toarray()) - else: - # stabler - M = np.linalg.pinv(PsiTPsi) - - # h factor (the full matrix is not calculated explicitly, - # only the trace is, to save memory) - PsiM = np.dot(psi_sparse, M) - - h = np.sum(np.multiply(PsiM, psi_sparse), axis=1, dtype=np.longdouble) # changed from np.float128 - - # ------ Calculate Error Loocv for each measurement point ---- - # Residuals - try: - residual = clf.predict(psi) - y - except: - residual = np.dot(psi, coeffs) - y - - # Variance - var_y = np.var(y) - - if var_y == 0: - norm_emp_error = 0 - loo_error = 0 - LCerror = np.zeros((y.shape)) - return 1-loo_error, LCerror - else: - norm_emp_error = np.mean(residual**2)/var_y - - # LCerror = np.divide(residual, (1-h)) - LCerror = residual / (1-h) - loo_error = np.mean(np.square(LCerror)) / var_y - # if there are NaNs, just return an infinite LOO error (this - # happens, e.g., when a strongly underdetermined problem is solved) - if np.isnan(loo_error): - loo_error = np.inf - - # Corrected Error for over-determined system - tr_M = np.trace(M) - if tr_M < 0 or abs(tr_M) > 1e6: - tr_M = np.trace(np.linalg.pinv(np.dot(psi.T, psi))) - - # Over-determined system of Equation - if N > P: - T_factor = N/(N-P) * (1 + tr_M) - - # Under-determined system of Equation - else: - T_factor = np.inf - - corrected_loo_error = loo_error * T_factor - - R_2 = 1 - corrected_loo_error - - return R_2, LCerror - - # ------------------------------------------------------------------------- - def pca_transformation(self, target, verbose=False): - """ - Transforms the targets (outputs) via Principal Component Analysis - - Parameters - ---------- - target : array of shape (n_samples,) - Target values. - - Returns - ------- - pca : obj - Fitted sklearnPCA object. - OutputMatrix : array of shape (n_samples,) - Transformed target values. - n_pca_components : int - Number of selected principal components. - - """ - # Transform via Principal Component Analysis - if hasattr(self, 'var_pca_threshold'): - var_pca_threshold = self.var_pca_threshold - else: - var_pca_threshold = 100.0 - n_samples, n_features = target.shape - - if hasattr(self, 'n_pca_components'): - n_pca_components = self.n_pca_components - else: - # Instantiate and fit sklearnPCA object - covar_matrix = sklearnPCA(n_components=None) - covar_matrix.fit(target) - var = np.cumsum(np.round(covar_matrix.explained_variance_ratio_, - decimals=5)*100) - # Find the number of components to explain self.varPCAThreshold of - # variance - try: - n_components = np.where(var >= var_pca_threshold)[0][0] + 1 - except IndexError: - n_components = min(n_samples, n_features) - - n_pca_components = min(n_samples, n_features, n_components) - - # Print out a report - if verbose: - print() - print('-' * 50) - print(f"PCA transformation is performed with {n_pca_components}" - " components.") - print('-' * 50) - print() - - # Fit and transform with the selected number of components - pca = sklearnPCA(n_components=n_pca_components, svd_solver='arpack') - scaled_target = pca.fit_transform(target) - - return pca, scaled_target, n_pca_components - - # ------------------------------------------------------------------------- - def gaussian_process_emulator(self, X, y, nug_term=None, autoSelect=False, - varIdx=None): - """ - Fits a Gaussian Process Emulator to the target given the training - points. - - Parameters - ---------- - X : array of shape (n_samples, n_params) - Training points. - y : array of shape (n_samples,) - Target values. - nug_term : float, optional - Nugget term. The default is None, i.e. variance of y. - autoSelect : bool, optional - Loop over some kernels and select the best. The default is False. - varIdx : int, optional - The index number. The default is None. - - Returns - ------- - gp : object - Fitted estimator. - - """ - - nug_term = nug_term if nug_term else np.var(y) - - Kernels = [nug_term * kernels.RBF(length_scale=1.0, - length_scale_bounds=(1e-25, 1e15)), - nug_term * kernels.RationalQuadratic(length_scale=0.2, - alpha=1.0), - nug_term * kernels.Matern(length_scale=1.0, - length_scale_bounds=(1e-15, 1e5), - nu=1.5)] - - # Automatic selection of the kernel - if autoSelect: - gp = {} - BME = [] - for i, kernel in enumerate(Kernels): - gp[i] = GaussianProcessRegressor(kernel=kernel, - n_restarts_optimizer=3, - normalize_y=False) - - # Fit to data using Maximum Likelihood Estimation - gp[i].fit(X, y) - - # Store the MLE as BME score - BME.append(gp[i].log_marginal_likelihood()) - - gp = gp[np.argmax(BME)] - - else: - gp = GaussianProcessRegressor(kernel=Kernels[0], - n_restarts_optimizer=3, - normalize_y=False) - gp.fit(X, y) - - # Compute score - if varIdx is not None: - Score = gp.score(X, y) - print('-'*50) - print(f'Output variable {varIdx}:') - print('The estimation of GPE coefficients converged,') - print(f'with the R^2 score: {Score:.3f}') - print('-'*50) - - return gp - - # ------------------------------------------------------------------------- - def eval_metamodel(self, samples=None, nsamples=None, - sampling_method='random', return_samples=False): - """ - Evaluates meta-model at the requested samples. One can also generate - nsamples. - - Parameters - ---------- - samples : array of shape (n_samples, n_params), optional - Samples to evaluate meta-model at. The default is None. - nsamples : int, optional - Number of samples to generate, if no `samples` is provided. The - default is None. - sampling_method : str, optional - Type of sampling, if no `samples` is provided. The default is - 'random'. - return_samples : bool, optional - Retun samples, if no `samples` is provided. The default is False. - - Returns - ------- - mean_pred : dict - Mean of the predictions. - std_pred : dict - Standard deviatioon of the predictions. - """ - outputs = self.ModelObj.Output.names - - # Generate or transform (if need be) samples - if samples is None: - # Generate - samples = self.ExpDesign.generate_samples( - nsamples, - sampling_method - ) - - # Transform samples to the independent space - samples = self.ExpDesign.transform( - samples, - method='user' - ) - - # Compute univariate bases for the given samples - if self.meta_model_type.lower() != 'gpe': - univ_p_val = self.univ_basis_vals( - samples, - n_max=np.max(self.pce_deg) - ) - - mean_pred_b = {} - std_pred_b = {} - # Loop over bootstrap iterations - for b_i in range(self.n_bootstrap_itrs): - - # Extract model dictionary - if self.meta_model_type.lower() == 'gpe': - model_dict = self.gp_poly[f'b_{b_i+1}'] - else: - model_dict = self.coeffs_dict[f'b_{b_i+1}'] - - # Loop over outputs - mean_pred = {} - std_pred = {} - for output, values in model_dict.items(): - - mean = np.empty((len(samples), len(values))) - std = np.empty((len(samples), len(values))) - idx = 0 - for in_key, InIdxValues in values.items(): - - # Perdiction with GPE - if self.meta_model_type.lower() == 'gpe': - X_T = self.x_scaler[f'b_{b_i+1}'][output].transform(samples) - gp = self.gp_poly[f'b_{b_i+1}'][output][in_key] - y_mean, y_std = gp.predict(X_T, return_std=True) - - else: - # Perdiction with PCE - # Assemble Psi matrix - basis = self.basis_dict[f'b_{b_i+1}'][output][in_key] - psi = self.create_psi(basis, univ_p_val) - - # Perdiction - if self.bootstrap_method != 'fast' or b_i == 0: - # with error bar, i.e. use clf_poly - clf_poly = self.clf_poly[f'b_{b_i+1}'][output][in_key] - try: - y_mean, y_std = clf_poly.predict( - psi, return_std=True - ) - except TypeError: - y_mean = clf_poly.predict(psi) - y_std = np.zeros_like(y_mean) - else: - # without error bar - coeffs = self.coeffs_dict[f'b_{b_i+1}'][output][in_key] - y_mean = np.dot(psi, coeffs) - y_std = np.zeros_like(y_mean) - - mean[:, idx] = y_mean - std[:, idx] = y_std - idx += 1 - - # Save predictions for each output - if self.dim_red_method.lower() == 'pca': - PCA = self.pca[f'b_{b_i+1}'][output] - mean_pred[output] = PCA.inverse_transform(mean) - std_pred[output] = np.zeros(mean.shape) - else: - mean_pred[output] = mean - std_pred[output] = std - - # Save predictions for each bootstrap iteration - mean_pred_b[b_i] = mean_pred - std_pred_b[b_i] = std_pred - - # Change the order of nesting - mean_pred_all = {} - for i in sorted(mean_pred_b): - for k, v in mean_pred_b[i].items(): - if k not in mean_pred_all: - mean_pred_all[k] = [None] * len(mean_pred_b) - mean_pred_all[k][i] = v - - # Compute the moments of predictions over the predictions - for output in outputs: - # Only use bootstraps with finite values - finite_rows = np.isfinite( - mean_pred_all[output]).all(axis=2).all(axis=1) - outs = np.asarray(mean_pred_all[output])[finite_rows] - # Compute mean - mean_pred[output] = np.mean(outs, axis=0) - # Compute standard deviation - if self.n_bootstrap_itrs > 1: - std_pred[output] = np.std(outs, axis=0) - else: - std_pred[output] = std_pred_b[b_i][output] - - if return_samples: - return mean_pred, std_pred, samples - else: - return mean_pred, std_pred - - # ------------------------------------------------------------------------- - def create_model_error(self, X, y, name='Calib'): - """ - Fits a GPE-based model error. - - Parameters - ---------- - X : array of shape (n_outputs, n_inputs) - Input array. It can contain any forcing inputs or coordinates of - extracted data. - y : array of shape (n_outputs,) - The model response for the MAP parameter set. - name : str, optional - Calibration or validation. The default is `'Calib'`. - - Returns - ------- - self: object - Self object. - - """ - Model = self.ModelObj - outputNames = Model.Output.names - self.errorRegMethod = 'GPE' - self.errorclf_poly = self.auto_vivification() - self.errorScale = self.auto_vivification() - - # Read data - MeasuredData = Model.read_observation(case=name) - - # Fitting GPR based bias model - for out in outputNames: - nan_idx = ~np.isnan(MeasuredData[out]) - # Select data - try: - data = MeasuredData[out].values[nan_idx] - except AttributeError: - data = MeasuredData[out][nan_idx] - - # Prepare the input matrix - scaler = MinMaxScaler() - delta = data # - y[out][0] - BiasInputs = np.hstack((X[out], y[out].reshape(-1, 1))) - X_S = scaler.fit_transform(BiasInputs) - gp = self.gaussian_process_emulator(X_S, delta) - - self.errorScale[out]["y_1"] = scaler - self.errorclf_poly[out]["y_1"] = gp - - return self - - # ------------------------------------------------------------------------- - def eval_model_error(self, X, y_pred): - """ - Evaluates the error model. - - Parameters - ---------- - X : array - Inputs. - y_pred : dict - Predictions. - - Returns - ------- - mean_pred : dict - Mean predition of the GPE-based error model. - std_pred : dict - standard deviation of the GPE-based error model. - - """ - mean_pred = {} - std_pred = {} - - for Outkey, ValuesDict in self.errorclf_poly.items(): - - pred_mean = np.zeros_like(y_pred[Outkey]) - pred_std = np.zeros_like(y_pred[Outkey]) - - for Inkey, InIdxValues in ValuesDict.items(): - - gp = self.errorclf_poly[Outkey][Inkey] - scaler = self.errorScale[Outkey][Inkey] - - # Transform Samples using scaler - for j, pred in enumerate(y_pred[Outkey]): - BiasInputs = np.hstack((X[Outkey], pred.reshape(-1, 1))) - Samples_S = scaler.transform(BiasInputs) - y_hat, y_std = gp.predict(Samples_S, return_std=True) - pred_mean[j] = y_hat - pred_std[j] = y_std - # pred_mean[j] += pred - - mean_pred[Outkey] = pred_mean - std_pred[Outkey] = pred_std - - return mean_pred, std_pred - - # ------------------------------------------------------------------------- - class auto_vivification(dict): - """ - Implementation of perl's AutoVivification feature. - - Source: https://stackoverflow.com/a/651879/18082457 - """ - - def __getitem__(self, item): - try: - return dict.__getitem__(self, item) - except KeyError: - value = self[item] = type(self)() - return value - - # ------------------------------------------------------------------------- - def copy_meta_model_opts(self, InputObj, ModelObj = 'None'): # added the None here - """ - This method is a convinient function to copy the metamodel options. - - Parameters - ---------- - InputObj : object - The input object. - ModelObj : object - The Model object. - - Returns - ------- - new_MetaModelOpts : object - The copied object. - - """ - new_MetaModelOpts = copy.deepcopy(self) - new_MetaModelOpts.ModelObj = ModelObj - new_MetaModelOpts.input_obj = InputObj - new_MetaModelOpts.ExpDesign.meta_Model = 'aPCE' - new_MetaModelOpts.ExpDesign.InputObj = InputObj - new_MetaModelOpts.ExpDesign.ndim = len(InputObj.Marginals) - new_MetaModelOpts.n_params = len(InputObj.Marginals) - new_MetaModelOpts.ExpDesign.hdf5_file = None - - return new_MetaModelOpts - - # ------------------------------------------------------------------------- - def __select_degree(self, ndim, n_samples): - """ - Selects degree based on the number of samples and parameters in the - sequential design. - - Parameters - ---------- - ndim : int - Dimension of the parameter space. - n_samples : int - Number of samples. - - Returns - ------- - deg_array: array - Array containing the arrays. - - """ - # Define the deg_array - max_deg = np.max(self.pce_deg) - min_Deg = np.min(self.pce_deg) - nitr = n_samples - self.ExpDesign.n_init_samples - - # Check q-norm - if not np.isscalar(self.pce_q_norm): - self.pce_q_norm = np.array(self.pce_q_norm) - else: - self.pce_q_norm = np.array([self.pce_q_norm]) - - def M_uptoMax(maxDeg): - n_combo = np.zeros(maxDeg) - for i, d in enumerate(range(1, maxDeg+1)): - n_combo[i] = math.factorial(ndim+d) - n_combo[i] /= math.factorial(ndim) * math.factorial(d) - return n_combo - - if self.ExpDesignFlag != 'sequential': - deg_new = max_deg - else: - d = nitr if nitr != 0 and self.n_params > 5 else 1 - min_index = np.argmin(abs(M_uptoMax(max_deg)-ndim*n_samples*d)) - deg_new = max_deg - # deg_new = range(1, max_deg+1)[min_index] - - if deg_new > min_Deg and self.pce_reg_method.lower() != 'fastard': - deg_array = np.arange(min_Deg, deg_new+1) - else: - deg_array = np.array([deg_new]) - - return deg_array diff --git a/examples/only-model/data/synth_data.mat b/examples/only-model/data/synth_data.mat new file mode 100644 index 0000000000000000000000000000000000000000..352697fc3848c73bf8ae8819f065b873a62907f5 Binary files /dev/null and b/examples/only-model/data/synth_data.mat differ diff --git a/examples/only-model/test_analytical_function_noMetaMod.py b/examples/only-model/test_analytical_function_noMetaMod.py index 99842f80f22a126daeb602051f20ee14e1ffc280..1d5139b0d86f2c545b28a881b6fa0d9ad3a74a96 100644 --- a/examples/only-model/test_analytical_function_noMetaMod.py +++ b/examples/only-model/test_analytical_function_noMetaMod.py @@ -15,23 +15,22 @@ Pfaffenwaldring 61 Created on Fri Aug 9 2019 """ - import numpy as np import pandas as pd +import scipy.io as io import sys import joblib # import bayesvalidrox # Add BayesValidRox path -#sys.path.append("../../src/bayesvalidrox/") +sys.path.append("../../src/") -import bayesvalidrox as bv from bayesvalidrox.pylink.pylink import PyLinkForwardModel from bayesvalidrox.surrogate_models.inputs import Input -from bayesvalidrox.surrogate_models.surrogate_models import MetaModel -from bayesvalidrox.surrogate_models.meta_model_engine import MetaModelEngine -from bayesvalidrox.post_processing.post_processing import PostProcessing +from bayesvalidrox.surrogate_models.engine import Engine +from bayesvalidrox.surrogate_models.exp_designs import ExpDesigns from bayesvalidrox.bayes_inference.bayes_inference import BayesInference from bayesvalidrox.bayes_inference.discrepancy import Discrepancy +from bayesvalidrox.bayes_inference.bayes_model_comparison import BayesModelComparison import matplotlib matplotlib.use('agg') @@ -70,149 +69,50 @@ if __name__ == "__main__": # standard deviation Inputs = Input() - # Assuming dependent input variables - # Inputs.Rosenblatt = True - for i in range(ndim): Inputs.add_marginals() Inputs.Marginals[i].name = "$\\theta_{"+str(i+1)+"}$" Inputs.Marginals[i].dist_type = 'uniform' Inputs.Marginals[i].parameters = [-5, 5] - # arbitrary polynomial chaos - # inputParams = np.load('data/InputParameters_{}.npy'.format(ndim)) - # for i in range(ndim): - # Inputs.add_marginals() - # Inputs.Marginals[i].name = f'$X_{i+1}$' - # Inputs.Marginals[i].input_data = inputParams[:, i] - - # ===================================================== - # ========== DEFINITION OF THE METAMODEL ============ - # ===================================================== - MetaModelOpts = MetaModel(Inputs, Model) - - # Select if you want to preserve the spatial/temporal depencencies - # MetaModelOpts.dim_red_method = 'PCA' - # MetaModelOpts.var_pca_threshold = 99.999 - # MetaModelOpts.n_pca_components = 10 - - # Select your metamodel method - # 1) PCE (Polynomial Chaos Expansion) 2) aPCE (arbitrary PCE) - # 3) GPE (Gaussian Process Emulator) - MetaModelOpts.meta_model_type = 'aPCE' - - # ------------------------------------------------ - # ------------- PCE Specification ---------------- - # ------------------------------------------------ - # Select the sparse least-square minimization method for - # the PCE coefficients calculation: - # 1)OLS: Ordinary Least Square 2)BRR: Bayesian Ridge Regression - # 3)LARS: Least angle regression 4)ARD: Bayesian ARD Regression - # 5)FastARD: Fast Bayesian ARD Regression - # 6)BCS: Bayesian Compressive Sensing - # 7)OMP: Orthogonal Matching Pursuit - # 8)VBL: Variational Bayesian Learning - # 9)EBL: Emperical Bayesian Learning - MetaModelOpts.pce_reg_method = 'FastARD' - - # Bootstraping - # 1) normal 2) fast - MetaModelOpts.bootstrap_method = 'fast' - MetaModelOpts.n_bootstrap_itrs = 1 - - # Specify the max degree to be compared by the adaptive algorithm: - # The degree with the lowest Leave-One-Out cross-validation (LOO) - # error (or the highest score=1-LOO)estimator is chosen as the final - # metamodel. pce_deg accepts degree as a scalar or a range. - MetaModelOpts.pce_deg = 12 - - # q-quasi-norm 0<q<1 (default=1) - MetaModelOpts.pce_q_norm = 0.85 if ndim < 5 else 0.5 - - # Print summary of the regression results - # MetaModelOpts.verbose = True - # ------------------------------------------------ # ------ Experimental Design Configuration ------- # ------------------------------------------------ - MetaModelOpts.add_ExpDesign() - + ExpDesign = ExpDesigns(Inputs) + # One-shot (normal) or Sequential Adaptive (sequential) Design - MetaModelOpts.ExpDesign.method = 'sequential' - MetaModelOpts.ExpDesign.n_init_samples = 3*ndim + ExpDesign.method = 'normal' + ExpDesign.n_init_samples = 3*ndim # Sampling methods # 1) random 2) latin_hypercube 3) sobol 4) halton 5) hammersley # 6) chebyshev(FT) 7) grid(FT) 8)user - MetaModelOpts.ExpDesign.sampling_method = 'latin_hypercube' - + ExpDesign.sampling_method = 'latin_hypercube' + # Provide the experimental design object with a hdf5 file - # MetaModelOpts.ExpDesign.hdf5_file = 'ExpDesign_AnalyticFunc.hdf5' - - # ------------------------------------------------ - # ------- Sequential Design configuration -------- - # ------------------------------------------------ - # Set the sampling parameters - MetaModelOpts.ExpDesign.n_new_samples = 1 - MetaModelOpts.ExpDesign.n_max_samples = 10#150 - MetaModelOpts.ExpDesign.mod_LOO_threshold = 1e-16 - - # MetaModelOpts.adapt_verbose = True - # 1) None 2) 'equal' 3)'epsilon-decreasing' 4) 'adaptive' - MetaModelOpts.ExpDesign.tradeoff_scheme = None - # MetaModelOpts.ExpDesign.n_replication = 5 - # -------- Exploration ------ - # 1)'Voronoi' 2)'random' 3)'latin_hypercube' 4)'LOOCV' 5)'dual annealing' - MetaModelOpts.ExpDesign.explore_method = 'random' - - # Use when 'dual annealing' chosen - MetaModelOpts.ExpDesign.max_func_itr = 1000 - - # Use when 'Voronoi' or 'random' or 'latin_hypercube' chosen - MetaModelOpts.ExpDesign.n_canddidate = 1000 - MetaModelOpts.ExpDesign.n_cand_groups = 4 - - # -------- Exploitation ------ - # 1)'BayesOptDesign' 2)'BayesActDesign' 3)'VarOptDesign' 4)'alphabetic' - # 5)'Space-filling' - MetaModelOpts.ExpDesign.exploit_method = 'BayesActDesign' - - # BayesActDesign -> when data is available - # 1) BME (Bayesian model evidence) 2) infEntropy (Information entropy) - # 2)DKL (Kullback-Leibler Divergence) - MetaModelOpts.ExpDesign.util_func = 'DKL' + # ExpDesign.hdf5_file = 'ExpDesign_AnalyticFunc.hdf5' # Defining the measurement error, if it's known a priori obsData = pd.DataFrame(Model.observations, columns=Model.Output.names) DiscrepancyOpts = Discrepancy('') DiscrepancyOpts.type = 'Gaussian' DiscrepancyOpts.parameters = obsData**2 - MetaModelOpts.Discrepancy = DiscrepancyOpts - - # Plot the posterior snapshots for SeqDesign - MetaModelOpts.ExpDesign.post_snapshot = False - MetaModelOpts.ExpDesign.step_snapshot = 1 - MetaModelOpts.ExpDesign.max_a_post = [0] * ndim - - # For calculation of validation error for SeqDesign - prior = np.load(f"data/Prior_{ndim}.npy") - prior_outputs = np.load(f"data/origModelOutput_{ndim}.npy") - likelihood = np.load(f"data/validLikelihoods_{ndim}.npy") - MetaModelOpts.valid_samples = prior[:500] - MetaModelOpts.valid_model_runs = {'Z': prior_outputs[:500]} - # MetaModelOpts.valid_likelihoods = likelihood - - # >>>>>>>>>>>>>>>>>>>>>> Build Surrogate <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - # Train the meta model - meta_model_engine = MetaModelEngine(MetaModelOpts) - #meta_model_engine.run() - PCEModel = meta_model_engine.MetaModel - + # Create the engine + engine = Engine(None, Model, ExpDesign) + engine.train_normal() + + # ===================================================== + # ======== PostProcessing on the Model only ========== # ===================================================== - # ======== Bayesian inference with Emulator ========== + + # So far there is no postprocessing for the model without surrogates! + + # ===================================================== - BayesOpts = BayesInference(PCEModel) + # ======== Bayesian inference without Emulator ========== + # ===================================================== + BayesOpts = BayesInference(engine) BayesOpts.emulator = False BayesOpts.plot_post_pred = True @@ -230,8 +130,6 @@ if __name__ == "__main__": # ----- Define the discrepancy model ------- BayesOpts.measurement_error = obsData - - # # -- (Option B) -- DiscrepancyOpts = Discrepancy('') DiscrepancyOpts.type = 'Gaussian' DiscrepancyOpts.parameters = obsData**2 @@ -243,3 +141,177 @@ if __name__ == "__main__": # Save class objects with open(f'Bayes_{Model.name}.pkl', 'wb') as output: joblib.dump(Bayes_PCE, output, 2) + + # ===================================================== + # ======== Model Comparison without Emulator ========== + # ===================================================== + sigma = 0.6 + data = { + 'x [m]': np.linspace(0.25, 4.75, 15), + 'Z': io.loadmat('data/synth_data.mat')['d'].reshape(1, -1)[0] + } + + # ===================================================== + # ============ COMPUTATIONAL MODELS ================ + # ===================================================== + # Define the models options + # ---------- Linear model ------------- + myL2Model = PyLinkForwardModel() + + myL2Model.link_type = 'Function' + myL2Model.py_file = 'L2_model' + myL2Model.name = 'linear' + myL2Model.Output.names = ['Z'] + myL2Model.observations = data + myL2Model.store = False + + # -- Nonlinear exponential model ------- + myNL2Model = PyLinkForwardModel() + + myNL2Model.link_type = 'Function' + myNL2Model.py_file = 'NL2_model' + myNL2Model.name = 'exponential' + myNL2Model.Output.names = ['Z'] + myNL2Model.observations = data + myNL2Model.store = False + + # ------ Nonlinear cosine model --------- + # Data generating process + myNL4Model = PyLinkForwardModel() + + myNL4Model.link_type = 'Function' + myNL4Model.py_file = 'NL4_model' + myNL4Model.name = 'cosine' + myNL4Model.Output.names = ['Z'] + myNL4Model.observations = data + myNL4Model.store = False + + # ===================================================== + # ========= PROBABILISTIC INPUT MODEL ============== + # ===================================================== + # Define model inputs + n_sample = 10000 + # ---------- Linear model ------------- + L2_Inputs = Input() + L2_prior_mean = np.array([1, 0]) + L2_prior_cov = np.array( + [[0.04, -0.007], + [-0.007, 0.04]] + ) + L2_input_params = np.random.multivariate_normal( + L2_prior_mean, L2_prior_cov, size=n_sample + ) + + for i in range(L2_input_params.shape[1]): + L2_Inputs.add_marginals() + L2_Inputs.Marginals[i].name = f'$X_{i+1}$' + L2_Inputs.Marginals[i].input_data = L2_input_params[:, i] + + # ------ Nonlinear exponential model --------- + NL2_Inputs = Input() + NL2_prior_mean = np.array([0.4, -0.3]) + NL2_prior_cov = np.array( + [[0.003, -0.0001], + [-0.0001, 0.03]] + ) + NL2_input_params = np.random.multivariate_normal( + NL2_prior_mean, NL2_prior_cov, size=n_sample + ) + + for i in range(NL2_input_params.shape[1]): + NL2_Inputs.add_marginals() + NL2_Inputs.Marginals[i].name = f'$X_{i+1}$' + NL2_Inputs.Marginals[i].input_data = NL2_input_params[:, i] + + # ------ Nonlinear cosine model --------- + NL4_Inputs = Input() + NL4_prior_mean = np.array([2.6, 0.5, -2.8, 2.3]) + NL4_prior_cov = np.array( + [[0.46, -0.07, 0.24, -0.14], + [-0.07, 0.04, -0.05, 0.02], + [0.24, -0.05, 0.30, -0.16], + [-0.14, 0.02, -0.16, 0.30]] + ) + NL4_input_params = np.random.multivariate_normal( + NL4_prior_mean, NL4_prior_cov, size=n_sample + ) + + for i in range(NL4_input_params.shape[1]): + NL4_Inputs.add_marginals() + NL4_Inputs.Marginals[i].name = f'$X_{i+1}$' + NL4_Inputs.Marginals[i].input_data = NL4_input_params[:, i] + + # ------------------------------------------------ + # ------ Experimental Design Configuration ------- + # ------------------------------------------------ + #L2_MetaModelOpts.add_ExpDesign() + L2_ExpDesign = ExpDesigns(L2_Inputs) + + # One-shot (normal) or Sequential Adaptive (sequential) Design + L2_ExpDesign.n_init_samples = 100 + + # Sampling methods + # 1) random 2) latin_hypercube 3) sobol 4) halton 5) hammersley + # 6) chebyshev(FT) 7) grid(FT) 8)user + L2_ExpDesign.sampling_method = 'latin_hypercube' + + # ------ Nonlinear cosine model --------- + NL2_ExpDesign = ExpDesigns(NL2_Inputs) + NL2_ExpDesign.method = 'normal' + NL2_ExpDesign.n_init_samples = 100 + NL2_ExpDesign.sampling_method = 'latin_hypercube' + + # ------ Nonlinear cosine model --------- + NL4_ExpDesign = ExpDesigns(NL4_Inputs) + NL4_ExpDesign.method = 'normal' + NL4_ExpDesign.n_init_samples = 100 + NL4_ExpDesign.sampling_method = 'latin_hypercube' + + # >>>>>> Train the Surrogates <<<<<<<<<<< + L2_engine = Engine(None, myL2Model, L2_ExpDesign) + L2_engine.train_normal() + NL2_engine = Engine(None, myNL2Model, NL2_ExpDesign) + NL2_engine.train_normal() + NL4_engine = Engine(None, myNL4Model, NL4_ExpDesign) + NL4_engine.train_normal() + + # ===================================================== + # ========= BAYESIAN MULTIMODEL COMPARISON =========== + # ===================================================== + # ----- Define the discrepancy model ------- + sigma = np.ones(15) * np.array(sigma).flatten() + DiscrepancyOpts = Discrepancy('') + DiscrepancyOpts.type = 'Gaussian' + DiscrepancyOpts.parameters = pd.DataFrame(sigma**2, columns=['Z']) + + # ----- Define the options model ------- + meta_models = { + "linear": L2_engine, + "exponential": NL2_engine, + "cosine": NL4_engine + } + + # BME Bootstrap options + opts_bootstrap = { + "bootstrap": True, + "n_samples": 100,#0,#0, # TODO: difference between this and the n_bootstrap set below? + "Discrepancy": DiscrepancyOpts, + "emulator": False, + "plot_post_pred": False + } + + # Run model comparison + BayesOpts = BayesModelComparison( + justifiability=True, + n_bootstrap=100,#0,#00, + #just_n_meas=2 + emulator = False + ) + output_dict = BayesOpts.model_comparison_all( + meta_models, + opts_bootstrap + ) + + # Save the results + with open('model_comparison_output_dict.pkl', 'wb') as output: + joblib.dump(output_dict, output, 2) diff --git a/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py b/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py index fd01689d70e59031434cea0696e82079d03b83d2..1c0bf991722b86f24e3eaf20c9ce2e709d319453 100644 --- a/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py +++ b/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py @@ -248,8 +248,13 @@ class BayesModelComparison: # Evaluate metamodel runs = {} +<<<<<<< HEAD for key, metaModel in model_dict.items(): y_hat, _ = metaModel.eval_metamodel(nsamples=n_bootstarp) +======= + for key, engine in model_dict.items(): # TODO: add check for emulator vs model + y_hat, _ = engine.eval_metamodel(nsamples=n_bootstrap) +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) runs[key] = y_hat # Generate data diff --git a/src/bayesvalidrox/bayes_inference/mcmc.py b/src/bayesvalidrox/bayes_inference/mcmc.py index 56bcdea0344cbeca45ea638866d24a371772e11b..d1e751fe08d0523ddf0f9c6ab3a56ab832d6ba68 100755 --- a/src/bayesvalidrox/bayes_inference/mcmc.py +++ b/src/bayesvalidrox/bayes_inference/mcmc.py @@ -284,7 +284,7 @@ class MCMC: engine = self.engine Discrepancy = self.Discrepancy n_cpus = engine.Model.n_cpus - ndim = engine.MetaModel.n_params + ndim = engine.ExpDesign.ndim if not os.path.exists(self.out_dir): os.makedirs(self.out_dir) >>>>>>> 2306e76d (Decoupled MCMC from BayesInference and improved performance) @@ -645,7 +645,6 @@ class MCMC: Discrepancy = self.BayesOpts.Discrepancy ======= engine = self.engine - MetaModel = engine.MetaModel Discrepancy = self.Discrepancy >>>>>>> 2306e76d (Decoupled MCMC from BayesInference and improved performance) @@ -667,8 +666,8 @@ class MCMC: # Check if the sample is within the parameters' range if self._check_ranges(theta[i], params_range): # Check if all dists are uniform, if yes priors are equal. - if all(MetaModel.input_obj.Marginals[i].dist_type == 'uniform' - for i in range(MetaModel.n_params)): + if all(engine.ExpDesign.InputObj.Marginals[i].dist_type == 'uniform' + for i in range(engine.ExpDesign.ndim)): logprior[i] = 0.0 else: logprior[i] = np.log( diff --git a/src/bayesvalidrox/pylink/pylink.py b/src/bayesvalidrox/pylink/pylink.py index 227a51ab38cd834e7e85f6193d83563c7ed3437a..fdbb2411cfbce0f56733f2e7c8df5052d0feb004 100644 --- a/src/bayesvalidrox/pylink/pylink.py +++ b/src/bayesvalidrox/pylink/pylink.py @@ -159,7 +159,8 @@ class PyLinkForwardModel(object): output_file_names=[], output_names=[], output_parser='', multi_process=True, n_cpus=None, meas_file=None, meas_file_valid=None, mc_ref_file=None, obs_dict={}, - obs_dict_valid={}, mc_ref_dict={}): + obs_dict_valid={}, mc_ref_dict={}, store = True, + out_dir = ''): self.link_type = link_type self.name = name self.shell_command = shell_command @@ -183,6 +184,8 @@ class PyLinkForwardModel(object): self.observations = obs_dict self.observations_valid = obs_dict_valid self.mc_reference = mc_ref_dict + self.store = store + self.out_dir = out_dir # ------------------------------------------------------------------------- def read_observation(self, case='calib'): @@ -579,9 +582,10 @@ class PyLinkForwardModel(object): all_outputs["x_values"] = group_results[0]["x_values"] # Store simulations in a hdf5 file - self._store_simulations( - c_points, all_outputs, NaN_idx, key_str, prevRun_No - ) + if self.store: + self._store_simulations( + c_points, all_outputs, NaN_idx, key_str, prevRun_No + ) return all_outputs, new_c_points diff --git a/src/bayesvalidrox/surrogate_models/engine.py b/src/bayesvalidrox/surrogate_models/engine.py index e6048900ed28ead0f0fffe0d1a83fd9df05474a0..eed4db25a26c7ae654616134feee4c7e35034525 100644 --- a/src/bayesvalidrox/surrogate_models/engine.py +++ b/src/bayesvalidrox/surrogate_models/engine.py @@ -26,7 +26,11 @@ from joblib import Parallel, delayed from bayesvalidrox.bayes_inference.bayes_inference import BayesInference from bayesvalidrox.bayes_inference.discrepancy import Discrepancy from .exploration import Exploration +<<<<<<< HEAD import pathlib +======= +from.surrogate_models import MetaModel as MM +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) #from .inputs import Input #from .exp_designs import ExpDesigns @@ -143,7 +147,30 @@ class Engine(): self.Model = Model self.ExpDesign = ExpDes self.parallel = False +<<<<<<< HEAD +======= + self.trained = False + + # Init other parameters + self.bound_tuples = None + self.errorModel = None + self.LCerror = None + self.n_obs = None + self.observations = None + self.out_names = None + self.seqMinDist = None + self.seqRMSEStd = None + self.SeqKLD = None + self.SeqDistHellinger = None + self.SeqBME = None + self.seqValidError = None + self.SeqModifiedLOO = None + self.valid_likelihoods = None + self._y_hat_prev = None + self.emulator = False + +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) def start_engine(self) -> None: """ Do all the preparations that need to be run before the actual training @@ -154,10 +181,21 @@ class Engine(): """ self.out_names = self.Model.Output.names +<<<<<<< HEAD self.MetaModel.out_names = self.out_names def train_normal(self, parallel = False, verbose = False, save = False) -> None: +======= + if isinstance(self.MetaModel, MM): + print('MetaModel has been given, `emulator` will be set to `True`') + self.emulator = True + self.MetaModel.out_names = self.out_names + else: + print('MetaModel has not been given, `emulator` will be set to `False`') + + def train_normal(self, parallel=False, verbose=False, save=False) -> None: +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) """ Trains surrogate on static samples only. Samples are taken from the experimental design and the specified @@ -188,6 +226,10 @@ class Engine(): # Prepare X samples # For training the surrogate use ExpDesign.X_tr, ExpDesign.X is for the model to run on + if self.emulator: + maxdeg = np.max(MetaModel.pce_deg) + else: + maxdeg = 1 ExpDesign.generate_ED(ExpDesign.n_init_samples, <<<<<<< HEAD <<<<<<< HEAD @@ -196,10 +238,14 @@ class Engine(): ======= #transform=True, +<<<<<<< HEAD ======= # transform=True, >>>>>>> f8175f33 (Bug fix: ExpDesign.generate_ED no longer needs 'transform') max_pce_deg=np.max(MetaModel.pce_deg)) +======= + max_pce_deg=maxdeg) +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) >>>>>>> 2306e76d (Decoupled MCMC from BayesInference and improved performance) # Run simulations at X @@ -222,11 +268,19 @@ class Engine(): # Fit the surrogate +<<<<<<< HEAD MetaModel.fit(ExpDesign.X, ExpDesign.Y, parallel, verbose) +======= + if self.emulator: + MetaModel.fit(ExpDesign.X, ExpDesign.Y, parallel, verbose) + +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) # Save what there is to save if save: # Save surrogate + if not os.path.exists('surrogates/'): + os.makedirs('surrogates/') with open(f'surrogates/surrogate_{self.Model.name}.pk1', 'wb') as output: joblib.dump(MetaModel, output, 2) @@ -254,7 +308,8 @@ class Engine(): # ------------------------------------------------------------------------- def eval_metamodel(self, samples=None, nsamples=None, - sampling_method='random', return_samples=False): + sampling_method='random', return_samples=False, + parallel = False): """ Evaluates meta-model at the requested samples. One can also generate nsamples. @@ -271,6 +326,9 @@ class Engine(): 'random'. return_samples : bool, optional Retun samples, if no `samples` is provided. The default is False. + parallel : bool, optional + Set to true if the evaluations should be done in parallel. + The default is False. Returns ------- @@ -289,14 +347,29 @@ class Engine(): # Transformation to other space is to be done in the MetaModel # TODO: sort the transformations better - mean_pred, std_pred = self.MetaModel.eval_metamodel(samples) + if self.emulator: + mean_pred, std_pred = self.MetaModel.eval_metamodel(samples) + else: + mean_pred , X = self.Model.run_model_parallel(samples, mp=parallel) if return_samples: - return mean_pred, std_pred, samples + if self.emulator: + return mean_pred, std_pred, samples + else: + return mean_pred, samples else: +<<<<<<< HEAD return mean_pred, std_pred +======= + if self.emulator: + return mean_pred, std_pred + else: + return mean_pred + + +>>>>>>> 99013313 (Small fixes, start on ModelComp without MetaModel) # ------------------------------------------------------------------------- def train_seq_design(self, parallel = False, verbose = False): """