From d52d06f660e434ff8640f1a13d23b0ee51cddc65 Mon Sep 17 00:00:00 2001 From: Michael Gardner Date: Sat, 6 Nov 2021 16:26:36 -0700 Subject: [PATCH 01/41] Changes to RunModel to execute tiled MPI jobs on Frontera --- src/UQpy/RunModel.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/UQpy/RunModel.py b/src/UQpy/RunModel.py index 6f9949284..3be10df32 100755 --- a/src/UQpy/RunModel.py +++ b/src/UQpy/RunModel.py @@ -262,6 +262,8 @@ def __init__(self, samples=None, model_script=None, model_object_name=None, cores_per_task=1, nodes=1, cluster=False, resume=False, verbose=False, model_dir='Model_Runs', fmt=None, separator=', ', vec=True, delete_files=False, **kwargs): + + print("THIS IS CUSTOM!!!") # Check the platform and build appropriate call to Python if platform.system() in ['Windows']: self.python_command = "python" @@ -769,18 +771,30 @@ def _execute_parallel(self): # If running on SLURM cluster if self.cluster: - self.srun_string = "srun -N" + str(self.nodes) + " -n1 -c" + str(self.cores_per_task) + " --exclusive " - self.model_command_string = (self.parallel_string + "'(cd run_{1}" + " && " + self.srun_string + print("CLUSTER RUN!!") + # self.srun_string = "srun -N" + str(self.nodes) + " -n1 -c" + str(self.cores_per_task) + " --exclusive " + self.ibrun_string = "ibrun -n " + str(self.cores_per_task) + " \"$(({} * " + str(self.cores_per_task) + "))\" " + self.model_command_string = (self.parallel_string + "'(cd run_{1}" + " && " + self.ibrun_string + " " + self.python_command + " -u " + str(self.model_script) + - " {1})' ::: {" + str(self.nexist) + ".." + - str(self.nexist + self.nsim - 1) + "}") + " {1} &)' ::: {" + str(self.nexist) + ".." + + str(self.nexist + self.nsim - 1) + "}; wait") else: # If running locally - self.model_command_string = (self.parallel_string + " 'cd run_{1}" + " && " + + print("LOCAL RUN!!") + self.model_command_string = str(self.parallel_string + "'cd run_{1}" + " && " + self.python_command + " -u " + - str(self.model_script) + "' {1} ::: {" + str(self.nexist) + ".." + + str(self.model_script) + " {1}' ::: {" + str(self.nexist) + ".." + str(self.nexist + self.nsim - 1) + "}") - subprocess.run(self.model_command_string, shell=True) + + print("This is the current directory:", os.getcwd()) + print("This is the command string:\n", self.model_command_string) + # print("THIS IS THE MODEL SCRIPT: ", self.model_script) + result = subprocess.run(self.model_command_string, shell=True, capture_output=True, text=True, executable='/bin/bash') + + print("command:", result.args) + print("stdout:", result.stdout) + print("stderr:", result.stderr) + # subprocess.check_output(self.model_command_string, shell=True) #################################################################################################################### # Helper functions From a023a25d888685798361057c9077e707bbb8e287 Mon Sep 17 00:00:00 2001 From: Michael Gardner Date: Fri, 12 Nov 2021 00:08:48 -0600 Subject: [PATCH 02/41] Changes to run on Frontera: separate preprocessing, model run, and post processing to only use ibrun on computationally intensive portions --- src/UQpy/RunModel.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/UQpy/RunModel.py b/src/UQpy/RunModel.py index 3be10df32..a678fecce 100755 --- a/src/UQpy/RunModel.py +++ b/src/UQpy/RunModel.py @@ -711,6 +711,7 @@ def _execute_serial(self, index): :type index: int """ self.model_command = ([self.python_command, str(self.model_script), str(index)]) + print("THIS IS THE COMMAND STRING: ", self.model_command) subprocess.run(self.model_command) def _output_serial(self, index): @@ -767,23 +768,27 @@ def _execute_parallel(self): os.remove("logs/runtask.log") except OSError: pass - self.parallel_string = "parallel --delay 0.2 --joblog logs/runtask.log --resume -j " + str(self.ntasks) + " " + self.parallel_string = "parallel --delay 0.2 --joblog logs/runtask.log -j " + str(self.ntasks) + " " # If running on SLURM cluster if self.cluster: print("CLUSTER RUN!!") # self.srun_string = "srun -N" + str(self.nodes) + " -n1 -c" + str(self.cores_per_task) + " --exclusive " - self.ibrun_string = "ibrun -n " + str(self.cores_per_task) + " \"$(({} * " + str(self.cores_per_task) + "))\" " - self.model_command_string = (self.parallel_string + "'(cd run_{1}" + " && " + self.ibrun_string - + " " + self.python_command + " -u " + str(self.model_script) + - " {1} &)' ::: {" + str(self.nexist) + ".." + - str(self.nexist + self.nsim - 1) + "}; wait") + analysisScript = str("/home1/03383/tg826821/quoFEM/applicationsDir/surrogateRuns_intel ./InputFiles/config.cfg --logging --fileIO --vtkWriteFreq 100 --blockSize 50 --XBlocks 16 --YBlocks 8 --ZBlocks 6 --numLevels 1 --loadDistributionStrategy Morton --baseFolder ./OutputFiles --qoiFile qoiFile.txt --qoiIOfreq 1000") + + postProcessScript = str(self.python_command + " -u postProcessRockerRun.py ./OutputFiles/qoiFile.txt results.out ") + ibrun_string = "ibrun -n " + str(self.cores_per_task) + " \"$(({} * " + str(self.cores_per_task) + "))\" task_affinity " + self.model_command_string = str(self.parallel_string + "'cp *.py run_{1}/. && cp workflow_driver run_{1}/. && cp slopeRealization_template.json run_{1}/. && cp sparkRocks-assembly-1.0.jar run_{1}/. && cd run_{1} && chmod +x workflow_driver && " + + self.python_command + " -u " + str(self.model_script) + " {1} && " + ibrun_string + + analysisScript + + " && " + postProcessScript + "' ::: {" + str(self.nexist) + ".." + + str(self.nexist + self.nsim - 1) + "}; wait") else: # If running locally print("LOCAL RUN!!") self.model_command_string = str(self.parallel_string + "'cd run_{1}" + " && " + - self.python_command + " -u " + - str(self.model_script) + " {1}' ::: {" + str(self.nexist) + ".." + - str(self.nexist + self.nsim - 1) + "}") + self.python_command + " -u " + + str(self.model_script) + " {1}' ::: {" + str(self.nexist) + ".." + + str(self.nexist + self.nsim - 1) + "}") print("This is the current directory:", os.getcwd()) From 8dc7cd49910a5d8920655dc3ce62d6ea2e1a2399 Mon Sep 17 00:00:00 2001 From: Michael Gardner Date: Fri, 23 Sep 2022 15:27:04 -0700 Subject: [PATCH 03/41] Adding cluster execution model --- src/UQpy/run_model/RunModel.py | 35 ++++++-- .../model_execution/ClusterExecution.py | 82 +++++++++++++++++++ 2 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/UQpy/run_model/model_execution/ClusterExecution.py diff --git a/src/UQpy/run_model/RunModel.py b/src/UQpy/run_model/RunModel.py index db426bb5f..e7d35b5d8 100755 --- a/src/UQpy/run_model/RunModel.py +++ b/src/UQpy/run_model/RunModel.py @@ -22,16 +22,21 @@ import numpy as np from beartype import beartype +from enum import Enum, auto from UQpy.utilities.ValidationTypes import NumpyFloatArray +class RunType(Enum): + LOCAL = auto() + CLUSTER = auto() + class RunModel: # Authors: - # B.S. Aakash, Lohit Vandanapu, Michael D.Shields + # B.S. Aakash, Lohit Vandanapu, Michael D.Shields, Michael H. Gardner # # Last - # modified: 5 / 8 / 2020 by Michael D.Shields + # modified: 8 / 31 / 2022 by Michael H. Gardner @beartype def __init__( self, @@ -41,6 +46,8 @@ def __init__( cores_per_task: int = 1, nodes: int = 1, resume: bool = False, + run_type: str = 'LOCAL', + cluster_script: str = None ): """ Run a computational model at specified sample points. @@ -85,6 +92,9 @@ def __init__( self.model = model # Save option for resuming parallel execution self.resume = resume + # Set location for model runs + self.run_type = RunType[run_type] + self.cluster_script = cluster_script self.nodes = nodes self.ntasks = ntasks @@ -189,9 +199,24 @@ def parallel_execution(self): pickle.dump(self.model, filehandle) with open('samples.pkl', 'wb') as filehandle: pickle.dump(self.samples, filehandle) - os.system(f"mpirun python -m " - f"UQpy.run_model.model_execution.ParallelExecution {self.n_existing_simulations} " - f"{self.n_new_simulations}") + + if self.run_type is RunType.LOCAL: + os.system(f"mpirun python -m " + f"UQpy.run_model.model_execution.ParallelExecution {self.n_existing_simulations} " + f"{self.n_new_simulations}") + + elif self.run_type is RunType.CLUSTER: + print("YEEEEEHAAAAA!!!!!") + if self.cluster_script is None: + raise ValueError("\nUQpy: User-provided slurm script not input, please provide this input\n") + os.system(f"python -m UQpy.run_model.model_execution.ClusterExecution {self.cores_per_task} " + f"{self.n_new_simulations} {self.n_existing_simulations} {self.cluster_script}") + + print("DONE WITH CLUSTER") + + else: + raise ValueError("\nUQpy: RunType is not in currently supported list of cluster types\n") + with open('qoi.pkl', 'rb') as filehandle: results = pickle.load(filehandle) diff --git a/src/UQpy/run_model/model_execution/ClusterExecution.py b/src/UQpy/run_model/model_execution/ClusterExecution.py new file mode 100644 index 000000000..27b78f430 --- /dev/null +++ b/src/UQpy/run_model/model_execution/ClusterExecution.py @@ -0,0 +1,82 @@ +# pragma: no cover +from __future__ import print_function + +import math +import sys + +import numpy as np +import os +import pickle + +try: + print("Oh boy....") + model = None + samples = None + samples_per_process = 0 + samples_shape = None + samples_list = None + ranges_list = None + local_ranges = None + local_samples = None + + cores_per_task = int(sys.argv[1]) + n_new_simulations = int(sys.argv[2]) + n_existing_simulations = int(sys.argv[3]) + cluster_script = str(sys.argv[4]) + + with open('model.pkl', 'rb') as filehandle: + model = pickle.load(filehandle) + + with open('samples.pkl', 'rb') as filehandle: + samples = pickle.load(filehandle) + + print(len(samples)) + print(samples[0].shape) + + # Loop over the number of samples and create input files in a folder in current directory + for i in range(len(samples)): + new_text = model._find_and_replace_var_names_with_values(samples[i]) + folder_to_write = 'run_' + str(i+n_existing_simulations) + '/InputFiles' + # Write the new text to the input file + model._create_input_files(file_name=model.input_template, num=i+n_existing_simulations, + text=new_text, new_folder=folder_to_write) + + # Use model script to perform necessary preprocessing prior to model execution + for i in range(len(samples)): + sample = 'sample' # Sample input in original third-party model, though doesn't seem to use it + model.execute_single_sample(i, sample) + + # Run user-provided cluster script--for now, it is assumed the user knows how to + # tile jobs in the script + print("This is the cluster script:", cluster_script) + os.system(f"{cluster_script} {cores_per_task} {n_new_simulations} {n_existing_simulations}") + + results = [] + + for i in range(len(samples)): + # Change current working directory to model run directory + work_dir = os.path.join(model.model_dir, "run_" + str(i)) + # if model.verbose: + # print('\nUQpy: Changing to the following directory for output processing:\n' + work_dir) + os.chdir(work_dir) + + output = model._output_serial(i) + results.append(output) + + with open('qoi.pkl', 'wb') as filehandle: + pickle.dump(results, filehandle) + + + + # CONTINUE HERE TO SEE + + + # if comm.rank == 0: + # result = [] + # [result.extend(el) for el in qoi] + # with open('qoi.pkl', 'wb') as filehandle: + # pickle.dump(result, filehandle) + +except Exception as e: + print(e) + From bc6fda0c50af9f75af22d7fffe0e8416264044f1 Mon Sep 17 00:00:00 2001 From: Michael Gardner Date: Fri, 23 Sep 2022 16:28:59 -0700 Subject: [PATCH 04/41] Update to fix issue related to Iterable --- src/UQpy/run_model/model_execution/ThirdPartyModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UQpy/run_model/model_execution/ThirdPartyModel.py b/src/UQpy/run_model/model_execution/ThirdPartyModel.py index 548ed5a83..0d09334fc 100644 --- a/src/UQpy/run_model/model_execution/ThirdPartyModel.py +++ b/src/UQpy/run_model/model_execution/ThirdPartyModel.py @@ -310,7 +310,7 @@ def _find_and_replace_var_names_with_values(self, sample): print("\nUQpy: Index Error: {0}\n".format(err)) raise IndexError("{0}".format(err)) - if isinstance(temp, collections.Iterable): + if isinstance(temp, collections.abc.Iterable): # If it is iterable, flatten and write as text file with designated separator temp = np.array(temp).flatten() to_add = "" From 8d93c72ca078339a9b5fb36ab6fc97edbc99e822 Mon Sep 17 00:00:00 2001 From: Michael Gardner Date: Wed, 19 Oct 2022 06:20:23 -0700 Subject: [PATCH 05/41] Finished adding cluster exectution model, will test multi-node deployment on HPC next --- src/UQpy/run_model/RunModel.py | 6 +----- .../model_execution/ClusterExecution.py | 19 +++---------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/UQpy/run_model/RunModel.py b/src/UQpy/run_model/RunModel.py index e7d35b5d8..8ddfaf548 100755 --- a/src/UQpy/run_model/RunModel.py +++ b/src/UQpy/run_model/RunModel.py @@ -206,17 +206,13 @@ def parallel_execution(self): f"{self.n_new_simulations}") elif self.run_type is RunType.CLUSTER: - print("YEEEEEHAAAAA!!!!!") if self.cluster_script is None: raise ValueError("\nUQpy: User-provided slurm script not input, please provide this input\n") os.system(f"python -m UQpy.run_model.model_execution.ClusterExecution {self.cores_per_task} " f"{self.n_new_simulations} {self.n_existing_simulations} {self.cluster_script}") - - print("DONE WITH CLUSTER") - else: raise ValueError("\nUQpy: RunType is not in currently supported list of cluster types\n") - + with open('qoi.pkl', 'rb') as filehandle: results = pickle.load(filehandle) diff --git a/src/UQpy/run_model/model_execution/ClusterExecution.py b/src/UQpy/run_model/model_execution/ClusterExecution.py index 27b78f430..b094e2841 100644 --- a/src/UQpy/run_model/model_execution/ClusterExecution.py +++ b/src/UQpy/run_model/model_execution/ClusterExecution.py @@ -9,7 +9,6 @@ import pickle try: - print("Oh boy....") model = None samples = None samples_per_process = 0 @@ -30,9 +29,6 @@ with open('samples.pkl', 'rb') as filehandle: samples = pickle.load(filehandle) - print(len(samples)) - print(samples[0].shape) - # Loop over the number of samples and create input files in a folder in current directory for i in range(len(samples)): new_text = model._find_and_replace_var_names_with_values(samples[i]) @@ -48,7 +44,6 @@ # Run user-provided cluster script--for now, it is assumed the user knows how to # tile jobs in the script - print("This is the cluster script:", cluster_script) os.system(f"{cluster_script} {cores_per_task} {n_new_simulations} {n_existing_simulations}") results = [] @@ -63,20 +58,12 @@ output = model._output_serial(i) results.append(output) + # Change back to model directory + os.chdir(model.model_dir) + with open('qoi.pkl', 'wb') as filehandle: pickle.dump(results, filehandle) - - - # CONTINUE HERE TO SEE - - - # if comm.rank == 0: - # result = [] - # [result.extend(el) for el in qoi] - # with open('qoi.pkl', 'wb') as filehandle: - # pickle.dump(result, filehandle) - except Exception as e: print(e) From cfdf3dfd512b910f8c3ec7d58d0ebeb8d78fa9ae Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Tue, 22 Nov 2022 12:29:42 -0500 Subject: [PATCH 06/41] Adds Parallel and Sequential Tempering files --- src/UQpy/sampling/__init__.py | 1 + .../tempering_mcmc/ParallelTemperingMCMC.py | 254 +++++++++++++ .../tempering_mcmc/SequentialTemperingMCMC.py | 336 ++++++++++++++++++ .../sampling/tempering_mcmc/TemperingMCMC.py | 118 ++++++ src/UQpy/sampling/tempering_mcmc/__init__.py | 3 + 5 files changed, 712 insertions(+) create mode 100644 src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py create mode 100644 src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py create mode 100644 src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py create mode 100644 src/UQpy/sampling/tempering_mcmc/__init__.py diff --git a/src/UQpy/sampling/__init__.py b/src/UQpy/sampling/__init__.py index c583907fb..b40106e15 100644 --- a/src/UQpy/sampling/__init__.py +++ b/src/UQpy/sampling/__init__.py @@ -1,6 +1,7 @@ from UQpy.sampling.mcmc import * from UQpy.sampling.adaptive_kriging_functions import * from UQpy.sampling.stratified_sampling import * +from UQpy.sampling.tempering_mcmc import * from UQpy.sampling.AdaptiveKriging import AdaptiveKriging from UQpy.sampling.ImportanceSampling import ImportanceSampling diff --git a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py new file mode 100644 index 000000000..291dcbe1b --- /dev/null +++ b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py @@ -0,0 +1,254 @@ +from scipy.special import logsumexp +from scipy.integrate import trapz + +from UQpy.sampling.mcmc import MetropolisHastings +from UQpy.sampling.mcmc.baseclass.MCMC import * +from UQpy.sampling.tempering_mcmc.TemperingMCMC import TemperingMCMC + + +class ParallelTemperingMCMC(TemperingMCMC): + """ + Parallel-Tempering MCMC + + This algorithms runs the chains sampling from various tempered distributions in parallel. Periodically during the + run, the different temperatures swap members of their ensemble in a way that + preserves detailed balance.The chains closer to the reference chain (hot chains) can sample from regions that have + low probability under the target and thus allow a better exploration of the parameter space, while the cold chains + can better explore the regions of high likelihood. + + **References** + + 1. Parallel Tempering: Theory, Applications, and New Perspectives, Earl and Deem + 2. Adaptive Parallel Tempering MCMC + 3. emcee the MCMC Hammer python package + + **Inputs:** + + Many inputs are similar to MCMC algorithms. Additional inputs are: + + * **niter_between_sweeps** + + * **mcmc_class** + + **Methods:** + + """ + + def __init__(self, niter_between_sweeps, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), + distribution_reference=None, nburn=0, jump=1, dimension=None, seed=None, + save_log_pdf=False, nsamples=None, nsamples_per_chain=None, nchains=None, verbose=False, + random_state=None, temper_param_list=None, n_temper_params=None, mcmc_class=MetropolisHastings, **kwargs_mcmc): + + super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, + args_pdf_intermediate=args_pdf_intermediate, distribution_reference=None, dimension=dimension, + save_log_pdf=save_log_pdf, random_state=random_state) + self.distribution_reference = distribution_reference + self.evaluate_log_reference = self._preprocess_reference(self.distribution_reference) + + # Initialize PT specific inputs: niter_between_sweeps and temperatures + self.niter_between_sweeps = niter_between_sweeps + if not (isinstance(self.niter_between_sweeps, int) and self.niter_between_sweeps >= 1): + raise ValueError('UQpy: input niter_between_sweeps should be a strictly positive integer.') + self.temper_param_list = temper_param_list + self.n_temper_params = n_temper_params + if self.temper_param_list is None: + if self.n_temper_params is None: + raise ValueError('UQpy: either input temper_param_list or n_temper_params should be provided.') + elif not (isinstance(self.n_temper_params, int) and self.n_temper_params >= 2): + raise ValueError('UQpy: input n_temper_params should be a integer >= 2.') + else: + self.temper_param_list = [1. / np.sqrt(2) ** i for i in range(self.n_temper_params - 1, -1, -1)] + elif (not isinstance(self.temper_param_list, (list, tuple)) + or not (all(isinstance(t, (int, float)) and (0 < t <= 1.) for t in self.temper_param_list)) + # or float(self.temperatures[0]) != 1. + ): + raise ValueError( + 'UQpy: temper_param_list should be a list of floats in [0, 1], starting at 0. and increasing to 1.') + else: + self.n_temper_params = len(self.temper_param_list) + + # Initialize mcmc objects, need as many as number of temperatures + if not issubclass(mcmc_class, MCMC): + raise ValueError('UQpy: mcmc_class should be a subclass of MCMC.') + if not all((isinstance(val, (list, tuple)) and len(val) == self.n_temper_params) + for val in kwargs_mcmc.values()): + raise ValueError( + 'UQpy: additional kwargs arguments should be mcmc algorithm specific inputs, given as lists of length ' + 'the number of temperatures.') + # default value + if isinstance(mcmc_class, MetropolisHastings) and len(kwargs_mcmc) == 0: + from UQpy.distributions import JointIndependent, Normal + kwargs_mcmc = {'proposal_is_symmetric': [True, ] * self.n_temper_params, + 'proposal': [JointIndependent([Normal(scale=1. / np.sqrt(temper_param))] * dimension) + for temper_param in self.temper_param_list]} + + # Initialize algorithm specific inputs: target pdfs + self.thermodynamic_integration_results = None + + self.mcmc_samplers = [] + for i, temper_param in enumerate(self.temper_param_list): + # log_pdf_target = self._target_generator( + # self.evaluate_log_intermediate, self.evaluate_log_reference, temper_param) + log_pdf_target = (lambda x, temper_param=temper_param: self.evaluate_log_reference( + x) + self.evaluate_log_intermediate(x, temper_param)) + self.mcmc_samplers.append( + mcmc_class(log_pdf_target=log_pdf_target, + dimension=dimension, seed=seed, nburn=nburn, jump=jump, save_log_pdf=save_log_pdf, + concat_chains=True, verbose=verbose, random_state=self.random_state, nchains=nchains, + **dict([(key, val[i]) for key, val in kwargs_mcmc.items()]))) + + # Samples connect to posterior samples, i.e. the chain with temperature 1. + # self.samples = self.mcmc_samplers[0].samples + # if self.save_log_pdf: + # self.log_pdf_values = self.mcmc_samplers[0].samples + + if self.verbose: + print('\nUQpy: Initialization of ' + self.__class__.__name__ + ' algorithm complete.') + + # If nsamples is provided, run the algorithm + if (nsamples is not None) or (nsamples_per_chain is not None): + self._run(nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) + + def _run(self, nsamples=None, nsamples_per_chain=None): + """ + Run the MCMC algorithm. + + This function samples from the MCMC chains and appends samples to existing ones (if any). This method leverages + the ``run_iterations`` method that is specific to each algorithm. + + **Inputs:** + + * **nsamples** (`int`): + Number of samples to generate. + + * **nsamples_per_chain** (`int`) + Number of samples to generate per chain. + + Either `nsamples` or `nsamples_per_chain` must be provided (not both). Not that if `nsamples` is not a multiple + of `nchains`, `nsamples` is set to the next largest integer that is a multiple of `nchains`. + + """ + # Initialize the runs: allocate space for the new samples and log pdf values + final_ns, final_ns_per_chain, current_state_t, current_log_pdf_t = self.mcmc_samplers[0]._initialize_samples( + nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) + current_state, current_log_pdf = [current_state_t.copy(), ], [current_log_pdf_t.copy(), ] + for mcmc_sampler in self.mcmc_samplers[1:]: + _, _, current_state_t, current_log_pdf_t = mcmc_sampler._initialize_samples( + nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) + current_state.append(current_state_t.copy()) + current_log_pdf.append(current_log_pdf_t.copy()) + + if self.verbose: + print('UQpy: Running MCMC...') + + # Run nsims iterations of the MCMC algorithm, starting at current_state + while self.mcmc_samplers[0].nsamples_per_chain < final_ns_per_chain: + # update the total number of iterations + # self.mcmc_samplers[0].niterations += 1 + + # run one iteration of MCMC algorithms at various temperatures + new_state, new_log_pdf = [], [] + for t, sampler in enumerate(self.mcmc_samplers): + sampler.niterations += 1 + new_state_t, new_log_pdf_t = sampler.run_one_iteration( + current_state[t], current_log_pdf[t]) + new_state.append(new_state_t.copy()) + new_log_pdf.append(new_log_pdf_t.copy()) + + # Do sweeps if necessary + if self.mcmc_samplers[-1].niterations % self.niter_between_sweeps == 0: + for i in range(self.n_temper_params - 1): + log_accept = (self.mcmc_samplers[i].evaluate_log_target(new_state[i + 1]) + + self.mcmc_samplers[i + 1].evaluate_log_target(new_state[i]) - + self.mcmc_samplers[i].evaluate_log_target(new_state[i]) - + self.mcmc_samplers[i + 1].evaluate_log_target(new_state[i + 1])) + for nc, log_accept_chain in enumerate(log_accept): + if np.log(self.random_state.rand()) < log_accept_chain: + new_state[i][nc], new_state[i + 1][nc] = new_state[i + 1][nc], new_state[i][nc] + new_log_pdf[i][nc], new_log_pdf[i + 1][nc] = new_log_pdf[i + 1][nc], new_log_pdf[i][nc] + + # Update the chain, only if burn-in is over and the sample is not being jumped over + # also increase the current number of samples and samples_per_chain + if self.mcmc_samplers[-1].niterations > self.mcmc_samplers[-1].nburn and \ + (self.mcmc_samplers[-1].niterations - self.mcmc_samplers[-1].nburn) % self.mcmc_samplers[ + -1].jump == 0: + for t, sampler in enumerate(self.mcmc_samplers): + sampler.samples[sampler.nsamples_per_chain, :, :] = new_state[t].copy() + if self.save_log_pdf: + sampler.log_pdf_values[sampler.nsamples_per_chain, :] = new_log_pdf[t].copy() + sampler.nsamples_per_chain += 1 + sampler.nsamples += sampler.nchains + # self.nsamples_per_chain += 1 + # self.nsamples += self.nchains + + if self.verbose: + print('UQpy: MCMC run successfully !') + + # Concatenate chains maybe + if self.mcmc_samplers[-1].concat_chains: + for t, mcmc_sampler in enumerate(self.mcmc_samplers): + mcmc_sampler._concatenate_chains() + + # Samples connect to posterior samples, i.e. the chain with beta=1. + self.intermediate_samples = [sampler.samples for sampler in self.mcmc_samplers] + self.samples = self.mcmc_samplers[-1].samples + if self.save_log_pdf: + self.log_pdf_values = self.mcmc_samplers[-1].log_pdf_values + + def evaluate_normalization_constant(self, compute_potential, log_Z0=None, nsamples_from_p0=None): + """ + Evaluate new log free energy as + + :math:`\log{Z_{1}} = \log{Z_{0}} + \int_{0}^{1} E_{x~p_{beta}} \left[ U_{\beta}(x) \right] d\beta` + + References (for the Bayesian case): + * https://emcee.readthedocs.io/en/v2.2.1/user/pt/ + + **Inputs:** + + * **compute_potential** (callable): + Function that takes three inputs (`x`, `log_factor_tempered_values`, `beta`) and computes the potential + :math:`U_{\beta}(x)`. `log_factor_tempered_values` are the values saved during sampling of + :math:`\log{p_{\beta}(x)}` at saved samples x. + + * **log_Z0** (`float`): + Value of :math:`\log{Z_{0}}` + + * **nsamples_from_p0** (`int`): + N samples from the reference distribution p0. Then :math:`\log{Z_{0}}` is evaluate via MC sampling + as :math:`\frac{1}{N} \sum{p_{\beta=0}(x)}`. Used only if input *log_Z0* is not provided. + + """ + if not self.save_log_pdf: + raise NotImplementedError('UQpy: the evidence cannot be computed when save_log_pdf is set to False.') + if log_Z0 is None and nsamples_from_p0 is None: + raise ValueError('UQpy: input log_Z0 or nsamples_from_p0 should be provided.') + # compute average of log_target for the target at various temperatures + log_pdf_averages = [] + for i, (temper_param, sampler) in enumerate(zip(self.temper_param_list, self.mcmc_samplers)): + log_factor_values = sampler.log_pdf_values - self.evaluate_log_reference(sampler.samples) + potential_values = compute_potential( + x=sampler.samples, temper_param=temper_param, log_intermediate_values=log_factor_values) + log_pdf_averages.append(np.mean(potential_values)) + + # use quadrature to integrate between 0 and 1 + temper_param_list_for_integration = np.copy(np.array(self.temper_param_list)) + log_pdf_averages = np.array(log_pdf_averages) + # if self.temper_param_list[-1] != 1.: + # log_pdf_averages = np.append(log_pdf_averages, log_pdf_averages[-1]) + # slope_linear = (log_pdf_averages[-1]-log_pdf_averages[-2]) / ( + # betas_for_integration[-1] - betas_for_integration[-2]) + # log_pdf_averages = np.append( + # log_pdf_averages, log_pdf_averages[-1] + (1. - betas_for_integration[-1]) * slope_linear) + # betas_for_integration = np.append(betas_for_integration, 1.) + int_value = trapz(x=temper_param_list_for_integration, y=log_pdf_averages) + if log_Z0 is None: + samples_p0 = self.distribution_reference.rvs(nsamples=nsamples_from_p0) + log_Z0 = np.log(1. / nsamples_from_p0) + logsumexp( + self.evaluate_log_intermediate(x=samples_p0, temper_param=self.temper_param_list[0])) + + self.thermodynamic_integration_results = { + 'log_Z0': log_Z0, 'temper_param_list': temper_param_list_for_integration, + 'expect_potentials': log_pdf_averages} + + return np.exp(int_value + log_Z0) diff --git a/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py new file mode 100644 index 000000000..f1ea34af9 --- /dev/null +++ b/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py @@ -0,0 +1,336 @@ +import copy + +import scipy.stats as stats + +from UQpy.sampling.mcmc.MetropolisHastings import MetropolisHastings +from UQpy.distributions.collection.MultivariateNormal import MultivariateNormal +from UQpy.sampling.mcmc.baseclass.MCMC import * +from UQpy.sampling.tempering_mcmc.TemperingMCMC import TemperingMCMC + + +class SequentialTemperingMCMC(TemperingMCMC): + """ + Sequential-Tempering MCMC + + This algorithms samples from a series of intermediate targets that are each tempered versions of the final/true + target. In going from one intermediate distribution to the next, the existing samples are resampled according to + some weights (similar to importance sampling). To ensure that there aren't a large number of duplicates, the + resampling step is followed by a short (or even single-step) MCMC run that disperses the samples while remaining + within the correct intermediate distribution. The final intermediate target is the required target distribution. + + **References** + + 1. Ching and Chen, "Transitional Markov Chain Monte Carlo Method for Bayesian Model Updating, + Model Class Selection, and Model Averaging", Journal of Engineering Mechanics/ASCE, 2007 + + **Inputs:** + + Many inputs are similar to MCMC algorithms. Additional inputs are: + + * **mcmc_class** + * **recalc_w** + * **nburn_resample** + * **nburn_mcmc** + + **Methods:** + """ + + @beartype + def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), + distribution_reference=None, + mcmc_class: MCMC = None, + dimension=None, seed=None, + nsamples: PositiveInteger = None, + recalc_w=False, + nburn_resample=0, save_intermediate_samples=False, nchains=1, + percentage_resampling=100, random_state=None, + proposal_is_symmetric=True): + + self.logger = logging.getLogger(__name__) + + super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, + args_pdf_intermediate=args_pdf_intermediate, distribution_reference=distribution_reference, + dimension=dimension, random_state=random_state) + + # Initialize inputs + self.save_intermediate_samples = save_intermediate_samples + self.recalc_w = recalc_w + self.nburn_resample = nburn_resample + self.nchains = nchains + self.resample_frac = percentage_resampling / 100 + self.proposal_is_symmetric=proposal_is_symmetric + + self.nspc = int(np.floor(((1 - self.resample_frac) * nsamples) / self.nchains)) + self.nresample = int(nsamples - (self.nspc * self.nchains)) + self.mcmc_class:MCMC = mcmc_class + + # Initialize input distributions + self.evaluate_log_reference, self.seed = self._preprocess_reference(dist_=distribution_reference, + seed_=seed, nsamples=nsamples, + dimension=self.dimension) + + # Initialize flag that indicates whether default proposal is to be used (default proposal defined adaptively + # during run) + if self.proposal is None: + self.proposal_given_flag = False + else: + self.proposal_given_flag = True + + # Initialize attributes + self.temper_param_list = None + self.evidence = None + self.evidence_cov = None + + # Call the run function + if nsamples is not None: + self._run(nsamples=nsamples) + else: + raise ValueError('UQpy: a value for "nsamples" must be specified ') + + @beartype + def _run(self, nsamples: PositiveInteger = None): + + self.logger.info('TMCMC Start') + + if self.samples is not None: + raise RuntimeError('UQpy: run method cannot be called multiple times for the same object') + + pts = self.seed # Generated Samples from prior for zero-th tempering level + + # Initializing other variables + temper_param = 0.0 # Intermediate exponent + temper_param_prev = temper_param + self.temper_param_list = np.array(temper_param) + pts_index = np.arange(nsamples) # Array storing sample indices + w = np.zeros(nsamples) # Array storing plausibility weights + wp = np.zeros(nsamples) # Array storing plausibility weight probabilities + exp_q0 = 0 + for i in range(nsamples): + exp_q0 += np.exp(self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), 0.0)) + S = exp_q0 / nsamples + + if self.save_intermediate_samples is True: + self.intermediate_samples = [] + self.intermediate_samples += [pts.copy()] + + # Looping over all adaptively decided tempering levels + while temper_param < 1: + + # Adaptively set the tempering exponent for the current level + temper_param_prev = temper_param + temper_param = self._find_temper_param(temper_param_prev, pts, self.evaluate_log_intermediate, nsamples) + # d_exp = temper_param - temper_param_prev + self.temper_param_list = np.append(self.temper_param_list, temper_param) + + self.logger.info('beta selected') + + # Calculate the plausibility weights + for i in range(nsamples): + w[i] = np.exp(self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param) + - self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param_prev)) + + # Calculate normalizing constant for the plausibility weights (sum of the weights) + w_sum = np.sum(w) + # Calculate evidence from each tempering level + S = S * (w_sum / nsamples) + # Normalize plausibility weight probabilities + wp = (w / w_sum) + + # Calculate covariance matrix for the default proposal + cov_scale = 0.2 + w_th_sum = np.zeros(self.dimension) + for i in range(nsamples): + for j in range(self.dimension): + w_th_sum[j] += w[i] * pts[i, j] + sig_mat = np.zeros((self.dimension, self.dimension)) + for i in range(nsamples): + pts_deviation = np.zeros((self.dimension, 1)) + for j in range(self.dimension): + pts_deviation[j, 0] = pts[i, j] - (w_th_sum[j] / w_sum) + sig_mat += (w[i] / w_sum) * np.dot(pts_deviation, + pts_deviation.T) # Normalized by w_sum as per Betz et al + sig_mat = cov_scale * cov_scale * sig_mat + + mcmc_log_pdf_target = self._target_generator(self.evaluate_log_intermediate, + self.evaluate_log_reference, temper_param) + + self.logger.info('Begin Resampling') + # Resampling and MH-MCMC step + for i in range(self.nresample): + + # Resampling from previous tempering level + lead_index = int(np.random.choice(pts_index, p=wp)) + lead = pts[lead_index] + + # Defining the default proposal + if self.proposal_given_flag is False: + self.proposal = MultivariateNormal(lead, cov=sig_mat) + + # Single MH-MCMC step + x = MetropolisHastings(dimension=self.dimension, log_pdf_target=mcmc_log_pdf_target, seed=lead, + nsamples=1, nchains=1, nburn=self.nburn_resample, proposal=self.proposal, + proposal_is_symmetric=self.proposal_is_symmetric) + + # Setting the generated sample in the array + pts[i] = x.samples + + if self.recalc_w: + w[i] = np.exp(self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param) + - self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param_prev)) + wp[i] = w[i] / w_sum + + self.logger.info('Begin MCMC') + mcmc_seed = self._mcmc_seed_generator(resampled_pts=pts[0:self.nresample, :], arr_length=self.nresample, + seed_length=self.nchains) + + y=copy.deepcopy(self.mcmc_class) + self.update_target_and_seed(y, mcmc_seed, mcmc_log_pdf_target) + # y = self.mcmc_class(log_pdf_target=mcmc_log_pdf_target, seed=mcmc_seed, dimension=self.dimension, + # nchains=self.nchains, nsamples_per_chain=self.nspc, nburn=self.nburn_mcmc, + # jump=self.jump_mcmc, concat_chains=True) + pts[self.nresample:, :] = y.samples + + if self.save_intermediate_samples is True: + self.intermediate_samples += [pts.copy()] + + self.logger.info('Tempering level ended') + + # Setting the calculated values to the attributes + self.samples = pts + self.evidence = S + + def update_target_and_seed(self, mcmc_class, mcmc_seed, mcmc_log_pdf_target): + mcmc_class.seed = mcmc_seed + mcmc_class.log_pdf_target = mcmc_log_pdf_target + mcmc_class.pdf_target = None + (mcmc_class.evaluate_log_target, mcmc_class.evaluate_log_target_marginals,) = \ + mcmc_class._preprocess_target(pdf_=None, log_pdf_=mcmc_class.log_pdf_target, args=None) + + def evaluate_normalization_constant(self): + return self.evidence + + @staticmethod + def _find_temper_param(temper_param_prev, samples, q_func, n, iter_lim=1000, iter_thresh=0.00001): + """ + Find the tempering parameter for the next intermediate target using bisection search between 1.0 and the + previous tempering parameter (taken to be 0.0 for the first level). + + **Inputs:** + + * **temper_param_prev** ('float'): + The value of the previous tempering parameter + + * **samples** (`ndarray`): + Generated samples from the previous intermediate target distribution + + * **q_func** (callable): + The intermediate distribution (called 'self.evaluate_log_intermediate' in this code) + + * **n** ('int'): + Number of samples + + * **iter_lim** ('int'): + Number of iterations to run the bisection search algorithm for, to avoid infinite loops + + * **iter_thresh** ('float'): + Threshold on the bisection interval, to avoid infinite loops + """ + bot = temper_param_prev + top = 1.0 + flag = 0 # Indicates when the tempering exponent has been found (flag = 1 => solution found) + loop_counter = 0 + while flag == 0: + loop_counter += 1 + q_scaled = np.zeros(n) + temper_param_trial = ((bot + top) / 2) + for i2 in range(0, n): + q_scaled[i2] = np.exp(q_func(samples[i2, :].reshape((1, -1)), 1) + - q_func(samples[i2, :].reshape((1, -1)), temper_param_prev)) + sigma_1 = np.std(q_scaled) + mu_1 = np.mean(q_scaled) + if sigma_1 < mu_1: + flag = 1 + temper_param_trial = 1 + continue + for i3 in range(0, n): + q_scaled[i3] = np.exp(q_func(samples[i3, :].reshape((1, -1)), temper_param_trial) + - q_func(samples[i3, :].reshape((1, -1)), temper_param_prev)) + sigma = np.std(q_scaled) + mu = np.mean(q_scaled) + if sigma < (0.9 * mu): + bot = temper_param_trial + elif sigma > (1.1 * mu): + top = temper_param_trial + else: + flag = 1 + if loop_counter > iter_lim: + flag = 2 + raise RuntimeError('UQpy: unable to find tempering exponent due to nonconvergence') + if top - bot <= iter_thresh: + flag = 3 + raise RuntimeError('UQpy: unable to find tempering exponent due to nonconvergence') + return temper_param_trial + + def _preprocess_reference(self, dist_, seed_=None, nsamples=None, dimension=None): + """ + Preprocess the target pdf inputs. + + Utility function (static method), that if given a distribution object, returns the log pdf of the target + distribution of the first tempering level (the prior in a Bayesian setting), and generates the samples from this + level. If instead the samples of the first level are passed, then the function passes these samples to the rest + of the algorithm, and does a Kernel Density Approximation to estimate the log pdf of the target distribution for + this level (as specified by the given sample points). + + **Inputs:** + + * seed_ ('ndarray'): The samples of the first tempering level + * prior_ ('Distribution' object): Target distribution for the first tempering level + * nsamples (int): Number of samples to be generated + * dimension (int): The dimension of the sample space + + **Output/Returns:** + + * evaluate_log_pdf (callable): Callable that computes the log of the target density function (the prior) + """ + + if dist_ is not None and seed_ is not None: + raise ValueError('UQpy: both prior and seed values cannot be provided') + elif dist_ is not None: + if not (isinstance(dist_, Distribution)): + raise TypeError('UQpy: A UQpy.Distribution object must be provided.') + else: + evaluate_log_pdf = (lambda x: dist_.log_pdf(x)) + seed_values = dist_.rvs(nsamples=nsamples) + elif seed_ is not None: + if seed_.shape[0] == nsamples and seed_.shape[1] == dimension: + seed_values = seed_ + kernel = stats.gaussian_kde(seed_) + evaluate_log_pdf = (lambda x: kernel.logpdf(x)) + else: + raise TypeError('UQpy: the seed values should be a numpy array of size (nsamples, dimension)') + else: + raise ValueError('UQpy: either prior distribution or seed values must be provided') + return evaluate_log_pdf, seed_values + + @staticmethod + def _mcmc_seed_generator(resampled_pts, arr_length, seed_length): + """ + Generates the seed from the resampled samples for the mcmc step + + Utility function (static method), that returns a selection of the resampled points (at any tempering level) to + be used as the seed for the following mcmc exploration step. + + **Inputs:** + + * resampled_pts ('ndarray'): The resampled samples of the tempering level + * arr_length (int): Length of resampled_pts + * seed_length (int): Number of samples needed in the seed (same as nchains) + + **Output/Returns:** + + * evaluate_log_pdf (callable): Callable that computes the log of the target density function (the prior) + """ + index_arr = np.arange(arr_length) + seed_indices = np.random.choice(index_arr, size=seed_length, replace=False) + mcmc_seed = resampled_pts[seed_indices, :] + return mcmc_seed diff --git a/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py new file mode 100644 index 000000000..8781186a9 --- /dev/null +++ b/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py @@ -0,0 +1,118 @@ +from UQpy.sampling.mcmc.baseclass.MCMC import * +from abc import ABC + + +class TemperingMCMC(ABC): + """ + Parent class to parallel and sequential tempering MCMC algorithms. + + To sample from the target distribution :math:`p(x)`, a sequence of intermediate densities + :math:`p(x, \beta) \propto q(x, \beta) p_{0}(x)` for values of the parameter :math:`\beta` between 0 and 1, + where :math:`p_{0}` is a reference distribution (often set as the prior in a Bayesian setting). + Setting :math:`\beta = 1` equates sampling from the target, while + :math:`\beta \rightarrow 0` samples from the reference distribution. + + **Inputs:** + + **Methods:** + """ + + def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), + distribution_reference=None, dimension=None, save_log_pdf=True, random_state=None): + self.logger = logging.getLogger(__name__) + # Check a few inputs + self.dimension = dimension + self.save_log_pdf = save_log_pdf + self.random_state = process_random_state(random_state) + + # Initialize the prior and likelihood + self.evaluate_log_intermediate = self._preprocess_intermediate( + log_pdf_=log_pdf_intermediate, pdf_=pdf_intermediate, args=args_pdf_intermediate) + if not (isinstance(distribution_reference, Distribution) or (distribution_reference is None)): + raise TypeError('UQpy: if provided, input distribution_reference should be a UQpy.Distribution object.') + # self.evaluate_log_reference = self._preprocess_reference(dist_=distribution_reference, args=()) + + # Initialize the outputs + self.samples = None + self.intermediate_samples = None + if self.save_log_pdf: + self.log_pdf_values = None + + def _run(self, nsamples): + """ Run the tempering MCMC algorithms to generate nsamples from the target posterior """ + pass + + def evaluate_normalization_constant(self, **kwargs): + """ Computes the normalization constant :math:`Z_{1}=\int{q_{1}(x) p_{0}(x)dx}` where p0 is the reference pdf + and q1 is the intermediate density with :math:`\beta=1`, thus q1 p0 is the target pdf.""" + pass + + def _preprocess_reference(self, dist_, **kwargs): + """ + Preprocess the target pdf inputs. + + Utility function (static method), that transforms the log_pdf, pdf, args inputs into a function that evaluates + log_pdf_target(x) for a given x. If the target is given as a list of callables (marginal pdfs), the list of + log margianals is also returned. + + **Inputs:** + + * dist_ (distribution object) + + **Output/Returns:** + + * evaluate_log_pdf (callable): Callable that computes the log of the target density function + + """ + # log_pdf is provided + if dist_ is None: + evaluate_log_pdf = None + elif isinstance(dist_, Distribution): + evaluate_log_pdf = (lambda x: dist_.log_pdf(x)) + else: + raise TypeError('UQpy: A UQpy.Distribution object must be provided.') + return evaluate_log_pdf + + @staticmethod + def _preprocess_intermediate(log_pdf_, pdf_, args): + """ + Preprocess the target pdf inputs. + + Utility function (static method), that transforms the log_pdf, pdf, args inputs into a function that evaluates + log_pdf_target(x, beta) for a given x. If the target is given as a list of callables (marginal pdfs), the list of + log margianals is also returned. + + **Inputs:** + + * log_pdf_ (callable): Log of the target density function from which to draw random samples. Either + pdf_target or log_pdf_target must be provided. + * pdf_ (callable): Target density function from which to draw random samples. Either pdf_target or + log_pdf_target must be provided. + * args (tuple): Positional arguments of the pdf target. + + **Output/Returns:** + + * evaluate_log_pdf (callable): Callable that computes the log of the target density function + + """ + # log_pdf is provided + if log_pdf_ is not None: + if not callable(log_pdf_): + raise TypeError('UQpy: log_pdf_intermediate must be a callable') + if args is None: + args = () + evaluate_log_pdf = (lambda x, temper_param: log_pdf_(x, temper_param, *args)) + elif pdf_ is not None: + if not callable(pdf_): + raise TypeError('UQpy: pdf_intermediate must be a callable') + if args is None: + args = () + evaluate_log_pdf = (lambda x, temper_param: np.log( + np.maximum(pdf_(x, temper_param, *args), 10 ** (-320) * np.ones((x.shape[0],))))) + else: + raise ValueError('UQpy: log_pdf_intermediate or pdf_intermediate must be provided') + return evaluate_log_pdf + + @staticmethod + def _target_generator(intermediate_logpdf_, reference_logpdf_, temper_param_): + return lambda x: (reference_logpdf_(x) + intermediate_logpdf_(x, temper_param_)) diff --git a/src/UQpy/sampling/tempering_mcmc/__init__.py b/src/UQpy/sampling/tempering_mcmc/__init__.py new file mode 100644 index 000000000..2540f20eb --- /dev/null +++ b/src/UQpy/sampling/tempering_mcmc/__init__.py @@ -0,0 +1,3 @@ +from UQpy.sampling.tempering_mcmc.TemperingMCMC import TemperingMCMC +from UQpy.sampling.tempering_mcmc.SequentialTemperingMCMC import SequentialTemperingMCMC +from UQpy.sampling.tempering_mcmc.ParallelTemperingMCMC import ParallelTemperingMCMC From b87cf474ab6f2ca054ee8bd23e9d89d24a777c85 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Wed, 23 Nov 2022 15:10:15 -0500 Subject: [PATCH 07/41] Starts TMCMC porting --- src/UQpy/sampling/mcmc/MetropolisHastings.py | 69 +++++-- src/UQpy/sampling/mcmc/baseclass/MCMC.py | 25 +-- .../tempering_mcmc/ParallelTemperingMCMC.py | 63 +++--- .../tempering_mcmc/SequentialTemperingMCMC.py | 193 ++++++++++-------- .../sampling/tempering_mcmc/TemperingMCMC.py | 3 +- tests/unit_tests/sampling/test_tempering.py | 68 ++++++ 6 files changed, 257 insertions(+), 164 deletions(-) create mode 100644 tests/unit_tests/sampling/test_tempering.py diff --git a/src/UQpy/sampling/mcmc/MetropolisHastings.py b/src/UQpy/sampling/mcmc/MetropolisHastings.py index a09a9380e..070420fea 100644 --- a/src/UQpy/sampling/mcmc/MetropolisHastings.py +++ b/src/UQpy/sampling/mcmc/MetropolisHastings.py @@ -13,22 +13,22 @@ class MetropolisHastings(MCMC): @beartype def __init__( - self, - pdf_target: Union[Callable, list[Callable]] = None, - log_pdf_target: Union[Callable, list[Callable]] = None, - args_target: tuple = None, - burn_length: Annotated[int, Is[lambda x: x >= 0]] = 0, - jump: int = 1, - dimension: int = None, - seed: list = None, - save_log_pdf: bool = False, - concatenate_chains: bool = True, - n_chains: int = None, - proposal: Distribution = None, - proposal_is_symmetric: bool = False, - random_state: RandomStateType = None, - nsamples: PositiveInteger = None, - nsamples_per_chain: PositiveInteger = None, + self, + pdf_target: Union[Callable, list[Callable]] = None, + log_pdf_target: Union[Callable, list[Callable]] = None, + args_target: tuple = None, + burn_length: Annotated[int, Is[lambda x: x >= 0]] = 0, + jump: int = 1, + dimension: int = None, + seed: list = None, + save_log_pdf: bool = False, + concatenate_chains: bool = True, + n_chains: int = None, + proposal: Distribution = None, + proposal_is_symmetric: bool = False, + random_state: RandomStateType = None, + nsamples: PositiveInteger = None, + nsamples_per_chain: PositiveInteger = None, ): """ Metropolis-Hastings algorithm :cite:`MCMC1` :cite:`MCMC2` @@ -115,7 +115,7 @@ def __init__( self.logger.info("\nUQpy: Initialization of " + self.__class__.__name__ + " algorithm complete.") if (nsamples is not None) or (nsamples_per_chain is not None): - self.run(nsamples=nsamples, nsamples_per_chain=nsamples_per_chain,) + self.run(nsamples=nsamples, nsamples_per_chain=nsamples_per_chain, ) def run_one_iteration(self, current_state: np.ndarray, current_log_pdf: np.ndarray): """ @@ -144,11 +144,11 @@ def run_one_iteration(self, current_state: np.ndarray, current_log_pdf: np.ndarr ) # this vector will be used to compute accept_ratio of each chain unif_rvs = ( Uniform() - .rvs(nsamples=self.n_chains, random_state=self.random_state) - .reshape((-1,)) + .rvs(nsamples=self.n_chains, random_state=self.random_state) + .reshape((-1,)) ) for nc, (cand, log_p_cand, r_) in enumerate( - zip(candidate, log_p_candidate, log_ratios) + zip(candidate, log_p_candidate, log_ratios) ): accept = np.log(unif_rvs[nc]) < r_ if accept: @@ -159,3 +159,32 @@ def run_one_iteration(self, current_state: np.ndarray, current_log_pdf: np.ndarr self._update_acceptance_rate(accept_vec) return current_state, current_log_pdf + + def __copy__(self, **kwargs): + pdf_target = self.pdf_target if kwargs['pdf_target'] is None else kwargs['pdf_target'] + log_pdf_target = self.log_pdf_target if kwargs['log_pdf_target'] is None else kwargs['log_pdf_target'] + args_target = self.args_target if kwargs['args_target'] is None else kwargs['args_target'] + burn_length = self.burn_length if kwargs['burn_length'] is None else kwargs['burn_length'] + jump = self.jump if kwargs['jump'] is None else kwargs['jump'] + dimension = self.dimension if kwargs['dimension'] is None else kwargs['dimension'] + seed = self.seed if kwargs['seed'] is None else kwargs['seed'] + save_log_pdf = self.save_log_pdf if kwargs['save_log_pdf'] is None else kwargs['save_log_pdf'] + concatenate_chains = self.concatenate_chains if kwargs['concatenate_chains'] is None\ + else kwargs['concatenate_chains'] + n_chains = self.n_chains if kwargs['n_chains'] is None else kwargs['n_chains'] + proposal = self.proposal if kwargs['proposal'] is None else kwargs['proposal'] + proposal_is_symmetric = self.proposal_is_symmetric if kwargs['proposal_is_symmetric'] is None \ + else kwargs['proposal_is_symmetric'] + random_state = self.random_state if kwargs['random_state'] is None else kwargs['random_state'] + nsamples = self.nsamples if kwargs['nsamples'] is None else kwargs['nsamples'] + nsamples_per_chain = self.nsamples_per_chain if kwargs['nsamples_per_chain'] is None \ + else kwargs['nsamples_per_chain'] + + new = self.__class__(pdf_target=pdf_target, log_pdf_target=log_pdf_target, args_target=args_target, + burn_length=burn_length, jump=jump, dimension=dimension, seed=seed, + save_log_pdf=save_log_pdf, concatenate_chains=concatenate_chains, + proposal=proposal, proposal_is_symmetric=proposal_is_symmetric, n_chains=n_chains, + random_state=random_state, nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) + + return new + diff --git a/src/UQpy/sampling/mcmc/baseclass/MCMC.py b/src/UQpy/sampling/mcmc/baseclass/MCMC.py index a863c9d7b..f8df33285 100644 --- a/src/UQpy/sampling/mcmc/baseclass/MCMC.py +++ b/src/UQpy/sampling/mcmc/baseclass/MCMC.py @@ -8,7 +8,7 @@ from UQpy.distributions import Distribution from UQpy.utilities.ValidationTypes import * from UQpy.utilities.Utilities import process_random_state -from abc import ABC +from abc import ABC, abstractmethod class MCMC(ABC): @@ -177,29 +177,22 @@ def _concatenate_chains(self): return None def _unconcatenate_chains(self): - self.samples = self.samples.reshape( - (-1, self.n_chains, self.dimension), order="C" - ) + self.samples = self.samples.reshape((-1, self.n_chains, self.dimension), order="C") if self.save_log_pdf: - self.log_pdf_values = self.log_pdf_values.reshape( - (-1, self.n_chains), order="C" - ) + self.log_pdf_values = self.log_pdf_values.reshape((-1, self.n_chains), order="C") return None def _initialize_samples(self, nsamples, nsamples_per_chain): if ((nsamples is not None) and (nsamples_per_chain is not None)) \ or (nsamples is None and nsamples_per_chain is None): raise ValueError("UQpy: Either nsamples or nsamples_per_chain must be provided (not both)") - if nsamples_per_chain is not None: - if not (isinstance(nsamples_per_chain, int) and nsamples_per_chain >= 0): - raise TypeError("UQpy: nsamples_per_chain must be an integer >= 0.") - nsamples = int(nsamples_per_chain * self.n_chains) - else: + if nsamples_per_chain is None: if not (isinstance(nsamples, int) and nsamples >= 0): raise TypeError("UQpy: nsamples must be an integer >= 0.") nsamples_per_chain = int(np.ceil(nsamples / self.n_chains)) - nsamples = int(nsamples_per_chain * self.n_chains) - + elif not (isinstance(nsamples_per_chain, int) and nsamples_per_chain >= 0): + raise TypeError("UQpy: nsamples_per_chain must be an integer >= 0.") + nsamples = int(nsamples_per_chain * self.n_chains) if self.samples is None: # very first call of run, set current_state as the seed and initialize self.samples self.samples = np.zeros((nsamples_per_chain, self.n_chains, self.dimension)) if self.save_log_pdf: @@ -316,3 +309,7 @@ def _check_methods_proposal(proposal_distribution): raise AttributeError("UQpy: The proposal should have a log_pdf or pdf method") proposal_distribution.log_pdf = lambda x: np.log( np.maximum(proposal_distribution.pdf(x), 10 ** (-320) * np.ones((x.shape[0],)))) + + @abstractmethod + def __copy__(self, **kwargs): + pass diff --git a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py index 291dcbe1b..d051fdea4 100644 --- a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py @@ -10,7 +10,7 @@ class ParallelTemperingMCMC(TemperingMCMC): """ Parallel-Tempering MCMC - This algorithms runs the chains sampling from various tempered distributions in parallel. Periodically during the + This algorithm runs the chains sampling from various tempered distributions in parallel. Periodically during the run, the different temperatures swap members of their ensemble in a way that preserves detailed balance.The chains closer to the reference chain (hot chains) can sample from regions that have low probability under the target and thus allow a better exploration of the parameter space, while the cold chains @@ -35,13 +35,18 @@ class ParallelTemperingMCMC(TemperingMCMC): """ def __init__(self, niter_between_sweeps, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), - distribution_reference=None, nburn=0, jump=1, dimension=None, seed=None, - save_log_pdf=False, nsamples=None, nsamples_per_chain=None, nchains=None, verbose=False, - random_state=None, temper_param_list=None, n_temper_params=None, mcmc_class=MetropolisHastings, **kwargs_mcmc): + distribution_reference=None, + save_log_pdf=False, nsamples=None, nsamples_per_chain=None, + random_state=None, + temper_param_list=None, n_temper_params=None, + sampler: Union[MCMC, list[MCMC]] = None): super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, - args_pdf_intermediate=args_pdf_intermediate, distribution_reference=None, dimension=dimension, + args_pdf_intermediate=args_pdf_intermediate, distribution_reference=None, save_log_pdf=save_log_pdf, random_state=random_state) + self.logger = logging.getLogger(__name__) + self.sampler = sampler + self.distribution_reference = distribution_reference self.evaluate_log_reference = self._preprocess_reference(self.distribution_reference) @@ -68,18 +73,18 @@ def __init__(self, niter_between_sweeps, pdf_intermediate=None, log_pdf_intermed self.n_temper_params = len(self.temper_param_list) # Initialize mcmc objects, need as many as number of temperatures - if not issubclass(mcmc_class, MCMC): - raise ValueError('UQpy: mcmc_class should be a subclass of MCMC.') - if not all((isinstance(val, (list, tuple)) and len(val) == self.n_temper_params) - for val in kwargs_mcmc.values()): - raise ValueError( - 'UQpy: additional kwargs arguments should be mcmc algorithm specific inputs, given as lists of length ' - 'the number of temperatures.') + # if not all((isinstance(val, (list, tuple)) and len(val) == self.n_temper_params) + # for val in kwargs_mcmc.values()): + # raise ValueError( + # 'UQpy: additional kwargs arguments should be mcmc algorithm specific inputs, given as lists of length ' + # 'the number of temperatures.') # default value - if isinstance(mcmc_class, MetropolisHastings) and len(kwargs_mcmc) == 0: + kwargs_mcmc = {} + if isinstance(self.sampler, MetropolisHastings) and not kwargs_mcmc: from UQpy.distributions import JointIndependent, Normal kwargs_mcmc = {'proposal_is_symmetric': [True, ] * self.n_temper_params, - 'proposal': [JointIndependent([Normal(scale=1. / np.sqrt(temper_param))] * dimension) + 'proposal': [JointIndependent([Normal(scale=1. / np.sqrt(temper_param))] * + self.sampler.dimension) for temper_param in self.temper_param_list]} # Initialize algorithm specific inputs: target pdfs @@ -87,23 +92,12 @@ def __init__(self, niter_between_sweeps, pdf_intermediate=None, log_pdf_intermed self.mcmc_samplers = [] for i, temper_param in enumerate(self.temper_param_list): - # log_pdf_target = self._target_generator( - # self.evaluate_log_intermediate, self.evaluate_log_reference, temper_param) log_pdf_target = (lambda x, temper_param=temper_param: self.evaluate_log_reference( x) + self.evaluate_log_intermediate(x, temper_param)) - self.mcmc_samplers.append( - mcmc_class(log_pdf_target=log_pdf_target, - dimension=dimension, seed=seed, nburn=nburn, jump=jump, save_log_pdf=save_log_pdf, - concat_chains=True, verbose=verbose, random_state=self.random_state, nchains=nchains, - **dict([(key, val[i]) for key, val in kwargs_mcmc.items()]))) - - # Samples connect to posterior samples, i.e. the chain with temperature 1. - # self.samples = self.mcmc_samplers[0].samples - # if self.save_log_pdf: - # self.log_pdf_values = self.mcmc_samplers[0].samples + self.mcmc_samplers.append(sampler.__copy__(log_pdf_target=log_pdf_target, concatenate_chains=True, + **kwargs_mcmc)) - if self.verbose: - print('\nUQpy: Initialization of ' + self.__class__.__name__ + ' algorithm complete.') + self.logger.info('\nUQpy: Initialization of ' + self.__class__.__name__ + ' algorithm complete.') # If nsamples is provided, run the algorithm if (nsamples is not None) or (nsamples_per_chain is not None): @@ -138,8 +132,7 @@ def _run(self, nsamples=None, nsamples_per_chain=None): current_state.append(current_state_t.copy()) current_log_pdf.append(current_log_pdf_t.copy()) - if self.verbose: - print('UQpy: Running MCMC...') + self.logger.info('UQpy: Running MCMC...') # Run nsims iterations of the MCMC algorithm, starting at current_state while self.mcmc_samplers[0].nsamples_per_chain < final_ns_per_chain: @@ -181,8 +174,7 @@ def _run(self, nsamples=None, nsamples_per_chain=None): # self.nsamples_per_chain += 1 # self.nsamples += self.nchains - if self.verbose: - print('UQpy: MCMC run successfully !') + self.logger.info('UQpy: MCMC run successfully !') # Concatenate chains maybe if self.mcmc_samplers[-1].concat_chains: @@ -234,13 +226,6 @@ def evaluate_normalization_constant(self, compute_potential, log_Z0=None, nsampl # use quadrature to integrate between 0 and 1 temper_param_list_for_integration = np.copy(np.array(self.temper_param_list)) log_pdf_averages = np.array(log_pdf_averages) - # if self.temper_param_list[-1] != 1.: - # log_pdf_averages = np.append(log_pdf_averages, log_pdf_averages[-1]) - # slope_linear = (log_pdf_averages[-1]-log_pdf_averages[-2]) / ( - # betas_for_integration[-1] - betas_for_integration[-2]) - # log_pdf_averages = np.append( - # log_pdf_averages, log_pdf_averages[-1] + (1. - betas_for_integration[-1]) * slope_linear) - # betas_for_integration = np.append(betas_for_integration, 1.) int_value = trapz(x=temper_param_list_for_integration, y=log_pdf_averages) if log_Z0 is None: samples_p0 = self.distribution_reference.rvs(nsamples=nsamples_from_p0) diff --git a/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py index f1ea34af9..f9e98f525 100644 --- a/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py @@ -12,7 +12,7 @@ class SequentialTemperingMCMC(TemperingMCMC): """ Sequential-Tempering MCMC - This algorithms samples from a series of intermediate targets that are each tempered versions of the final/true + This algorithm samples from a series of intermediate targets that are each tempered versions of the final/true target. In going from one intermediate distribution to the next, the existing samples are resampled according to some weights (similar to importance sampling). To ensure that there aren't a large number of duplicates, the resampling step is followed by a short (or even single-step) MCMC run that disperses the samples while remaining @@ -36,50 +36,62 @@ class SequentialTemperingMCMC(TemperingMCMC): """ @beartype + # def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), + # distribution_reference=None, + # mcmc_class: MCMC = None, + # dimension=None, seed=None, + # nsamples: PositiveInteger = None, + # recalc_w=False, + # nburn_resample=0, save_intermediate_samples=False, nchains=1, + # percentage_resampling=100, random_state=None, + # proposal_is_symmetric=True): def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), distribution_reference=None, - mcmc_class: MCMC = None, - dimension=None, seed=None, + sampler: MCMC = None, + seed=None, nsamples: PositiveInteger = None, - recalc_w=False, - nburn_resample=0, save_intermediate_samples=False, nchains=1, - percentage_resampling=100, random_state=None, - proposal_is_symmetric=True): - + recalculate_weights=False, + save_intermediate_samples=False, + percentage_resampling=100, + random_state=None, + resampling_burn_length=0, + resampling_proposal=None, + resampling_proposal_is_symmetric=True): + self.proposal = resampling_proposal + self.proposal_is_symmetric = resampling_proposal_is_symmetric + self.resampling_burn_length = resampling_burn_length self.logger = logging.getLogger(__name__) super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, args_pdf_intermediate=args_pdf_intermediate, distribution_reference=distribution_reference, - dimension=dimension, random_state=random_state) + random_state=random_state) + self.logger = logging.getLogger(__name__) + self.sampler = sampler # Initialize inputs self.save_intermediate_samples = save_intermediate_samples - self.recalc_w = recalc_w - self.nburn_resample = nburn_resample - self.nchains = nchains - self.resample_frac = percentage_resampling / 100 - self.proposal_is_symmetric=proposal_is_symmetric + self.recalculate_weights = recalculate_weights + + self.resample_fraction = percentage_resampling / 100 - self.nspc = int(np.floor(((1 - self.resample_frac) * nsamples) / self.nchains)) - self.nresample = int(nsamples - (self.nspc * self.nchains)) - self.mcmc_class:MCMC = mcmc_class + self.__dimension = sampler.dimension + self.__n_chains = sampler.n_chains + + self.n_samples_per_chain = int(np.floor(((1 - self.resample_fraction) * nsamples) / self.__n_chains)) + self.n_resamples = int(nsamples - (self.n_samples_per_chain * self.__n_chains)) # Initialize input distributions self.evaluate_log_reference, self.seed = self._preprocess_reference(dist_=distribution_reference, seed_=seed, nsamples=nsamples, - dimension=self.dimension) + dimension=self.__dimension) # Initialize flag that indicates whether default proposal is to be used (default proposal defined adaptively # during run) - if self.proposal is None: - self.proposal_given_flag = False - else: - self.proposal_given_flag = True - + self.proposal_given_flag = self.proposal is not None # Initialize attributes - self.temper_param_list = None + self.tempering_parameters = None self.evidence = None - self.evidence_cov = None + self.evidence_CoV = None # Call the run function if nsamples is not None: @@ -95,109 +107,114 @@ def _run(self, nsamples: PositiveInteger = None): if self.samples is not None: raise RuntimeError('UQpy: run method cannot be called multiple times for the same object') - pts = self.seed # Generated Samples from prior for zero-th tempering level + points = self.seed # Generated Samples from prior for zero-th tempering level # Initializing other variables - temper_param = 0.0 # Intermediate exponent - temper_param_prev = temper_param - self.temper_param_list = np.array(temper_param) + current_tempering_parameter = 0.0 # Intermediate exponent + previous_tempering_parameter = current_tempering_parameter + self.tempering_parameters = np.array(current_tempering_parameter) pts_index = np.arange(nsamples) # Array storing sample indices - w = np.zeros(nsamples) # Array storing plausibility weights - wp = np.zeros(nsamples) # Array storing plausibility weight probabilities - exp_q0 = 0 - for i in range(nsamples): - exp_q0 += np.exp(self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), 0.0)) - S = exp_q0 / nsamples + weights = np.zeros(nsamples) # Array storing plausibility weights + weight_probabilities = np.zeros(nsamples) # Array storing plausibility weight probabilities + expected_q0 = sum( + np.exp(self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), 0.0)) + for i in range(nsamples))/nsamples + + evidence_estimator = expected_q0 if self.save_intermediate_samples is True: self.intermediate_samples = [] - self.intermediate_samples += [pts.copy()] + self.intermediate_samples += [points.copy()] + # Calculate covariance matrix for the default proposal + cov_scale = 0.2 # Looping over all adaptively decided tempering levels - while temper_param < 1: + while current_tempering_parameter < 1: # Adaptively set the tempering exponent for the current level - temper_param_prev = temper_param - temper_param = self._find_temper_param(temper_param_prev, pts, self.evaluate_log_intermediate, nsamples) + previous_tempering_parameter = current_tempering_parameter + current_tempering_parameter = self._find_temper_param(previous_tempering_parameter, points, + self.evaluate_log_intermediate, nsamples) # d_exp = temper_param - temper_param_prev - self.temper_param_list = np.append(self.temper_param_list, temper_param) + self.tempering_parameters = np.append(self.tempering_parameters, current_tempering_parameter) self.logger.info('beta selected') # Calculate the plausibility weights for i in range(nsamples): - w[i] = np.exp(self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param) - - self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param_prev)) + weights[i] = np.exp(self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), + current_tempering_parameter) + - self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), + previous_tempering_parameter)) # Calculate normalizing constant for the plausibility weights (sum of the weights) - w_sum = np.sum(w) + w_sum = np.sum(weights) # Calculate evidence from each tempering level - S = S * (w_sum / nsamples) + evidence_estimator = evidence_estimator * (w_sum / nsamples) # Normalize plausibility weight probabilities - wp = (w / w_sum) + weight_probabilities = (weights / w_sum) - # Calculate covariance matrix for the default proposal - cov_scale = 0.2 - w_th_sum = np.zeros(self.dimension) + w_theta_sum = np.zeros(self.__dimension) for i in range(nsamples): - for j in range(self.dimension): - w_th_sum[j] += w[i] * pts[i, j] - sig_mat = np.zeros((self.dimension, self.dimension)) + for j in range(self.__dimension): + w_theta_sum[j] += weights[i] * points[i, j] + sigma_matrix = np.zeros((self.__dimension, self.__dimension)) for i in range(nsamples): - pts_deviation = np.zeros((self.dimension, 1)) - for j in range(self.dimension): - pts_deviation[j, 0] = pts[i, j] - (w_th_sum[j] / w_sum) - sig_mat += (w[i] / w_sum) * np.dot(pts_deviation, - pts_deviation.T) # Normalized by w_sum as per Betz et al - sig_mat = cov_scale * cov_scale * sig_mat + points_deviation = np.zeros((self.__dimension, 1)) + for j in range(self.__dimension): + points_deviation[j, 0] = points[i, j] - (w_theta_sum[j] / w_sum) + sigma_matrix += (weights[i] / w_sum) * np.dot(points_deviation, + points_deviation.T) # Normalized by w_sum as per Betz et al + sigma_matrix = cov_scale ** 2 * sigma_matrix mcmc_log_pdf_target = self._target_generator(self.evaluate_log_intermediate, - self.evaluate_log_reference, temper_param) + self.evaluate_log_reference, current_tempering_parameter) self.logger.info('Begin Resampling') # Resampling and MH-MCMC step - for i in range(self.nresample): + for i in range(self.n_resamples): # Resampling from previous tempering level - lead_index = int(np.random.choice(pts_index, p=wp)) - lead = pts[lead_index] + lead_index = int(np.random.choice(pts_index, p=weight_probabilities)) + lead = points[lead_index] # Defining the default proposal if self.proposal_given_flag is False: - self.proposal = MultivariateNormal(lead, cov=sig_mat) + self.proposal = MultivariateNormal(lead, cov=sigma_matrix) # Single MH-MCMC step - x = MetropolisHastings(dimension=self.dimension, log_pdf_target=mcmc_log_pdf_target, seed=lead, - nsamples=1, nchains=1, nburn=self.nburn_resample, proposal=self.proposal, + x = MetropolisHastings(dimension=self.__dimension, log_pdf_target=mcmc_log_pdf_target, seed=lead, + nsamples=1, nchains=1, nburn=self.resampling_burn_length, proposal=self.proposal, proposal_is_symmetric=self.proposal_is_symmetric) # Setting the generated sample in the array - pts[i] = x.samples + points[i] = x.samples - if self.recalc_w: - w[i] = np.exp(self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param) - - self.evaluate_log_intermediate(pts[i, :].reshape((1, -1)), temper_param_prev)) - wp[i] = w[i] / w_sum + if self.recalculate_weights: + weights[i] = np.exp( + self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), current_tempering_parameter) + - self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), previous_tempering_parameter)) + weight_probabilities[i] = weights[i] / w_sum self.logger.info('Begin MCMC') - mcmc_seed = self._mcmc_seed_generator(resampled_pts=pts[0:self.nresample, :], arr_length=self.nresample, - seed_length=self.nchains) + mcmc_seed = self._mcmc_seed_generator(resampled_pts=points[0:self.n_resamples, :], + arr_length=self.n_resamples, + seed_length=self.__n_chains) - y=copy.deepcopy(self.mcmc_class) + y = copy.deepcopy(self.sampler) self.update_target_and_seed(y, mcmc_seed, mcmc_log_pdf_target) - # y = self.mcmc_class(log_pdf_target=mcmc_log_pdf_target, seed=mcmc_seed, dimension=self.dimension, - # nchains=self.nchains, nsamples_per_chain=self.nspc, nburn=self.nburn_mcmc, - # jump=self.jump_mcmc, concat_chains=True) - pts[self.nresample:, :] = y.samples + y = self.sampler.__copy__(log_pdf_target=mcmc_log_pdf_target, seed=mcmc_seed, + nsamples_per_chain=self.n_samples_per_chain, concat_chains=True) + points[self.n_resamples:, :] = y.samples if self.save_intermediate_samples is True: - self.intermediate_samples += [pts.copy()] + self.intermediate_samples += [points.copy()] self.logger.info('Tempering level ended') # Setting the calculated values to the attributes - self.samples = pts - self.evidence = S + self.samples = points + self.evidence = evidence_estimator def update_target_and_seed(self, mcmc_class, mcmc_seed, mcmc_log_pdf_target): mcmc_class.seed = mcmc_seed @@ -243,7 +260,7 @@ def _find_temper_param(temper_param_prev, samples, q_func, n, iter_lim=1000, ite loop_counter += 1 q_scaled = np.zeros(n) temper_param_trial = ((bot + top) / 2) - for i2 in range(0, n): + for i2 in range(n): q_scaled[i2] = np.exp(q_func(samples[i2, :].reshape((1, -1)), 1) - q_func(samples[i2, :].reshape((1, -1)), temper_param_prev)) sigma_1 = np.std(q_scaled) @@ -252,7 +269,7 @@ def _find_temper_param(temper_param_prev, samples, q_func, n, iter_lim=1000, ite flag = 1 temper_param_trial = 1 continue - for i3 in range(0, n): + for i3 in range(n): q_scaled[i3] = np.exp(q_func(samples[i3, :].reshape((1, -1)), temper_param_trial) - q_func(samples[i3, :].reshape((1, -1)), temper_param_prev)) sigma = np.std(q_scaled) @@ -298,16 +315,14 @@ def _preprocess_reference(self, dist_, seed_=None, nsamples=None, dimension=None elif dist_ is not None: if not (isinstance(dist_, Distribution)): raise TypeError('UQpy: A UQpy.Distribution object must be provided.') - else: - evaluate_log_pdf = (lambda x: dist_.log_pdf(x)) - seed_values = dist_.rvs(nsamples=nsamples) + evaluate_log_pdf = (lambda x: dist_.log_pdf(x)) + seed_values = dist_.rvs(nsamples=nsamples) elif seed_ is not None: - if seed_.shape[0] == nsamples and seed_.shape[1] == dimension: - seed_values = seed_ - kernel = stats.gaussian_kde(seed_) - evaluate_log_pdf = (lambda x: kernel.logpdf(x)) - else: + if seed_.shape[0] != nsamples or seed_.shape[1] != dimension: raise TypeError('UQpy: the seed values should be a numpy array of size (nsamples, dimension)') + seed_values = seed_ + kernel = stats.gaussian_kde(seed_) + evaluate_log_pdf = (lambda x: kernel.logpdf(x)) else: raise ValueError('UQpy: either prior distribution or seed values must be provided') return evaluate_log_pdf, seed_values diff --git a/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py index 8781186a9..571f79546 100644 --- a/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py +++ b/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py @@ -18,10 +18,9 @@ class TemperingMCMC(ABC): """ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), - distribution_reference=None, dimension=None, save_log_pdf=True, random_state=None): + distribution_reference=None, save_log_pdf=True, random_state=None): self.logger = logging.getLogger(__name__) # Check a few inputs - self.dimension = dimension self.save_log_pdf = save_log_pdf self.random_state = process_random_state(random_state) diff --git a/tests/unit_tests/sampling/test_tempering.py b/tests/unit_tests/sampling/test_tempering.py new file mode 100644 index 000000000..c0c7106f4 --- /dev/null +++ b/tests/unit_tests/sampling/test_tempering.py @@ -0,0 +1,68 @@ +import numpy as np +from scipy.stats import multivariate_normal + +from UQpy.distributions import Uniform, JointIndependent +from UQpy.sampling.tempering_mcmc import ParallelTemperingMCMC, SequentialTemperingMCMC +from UQpy.sampling.mcmc import MetropolisHastings + + +def log_rosenbrock(x): + return -(100 * (x[:, 1] - x[:, 0] ** 2) ** 2 + (1 - x[:, 0]) ** 2) / 20 + + +def log_intermediate(x, beta): + return beta * log_rosenbrock(x) + + +def log_prior(x): + loc, scale = -20., 40. + return Uniform(loc=loc, scale=scale).log_pdf(x[:, 0]) + Uniform(loc=loc, scale=scale).log_pdf(x[:, 1]) + + +def compute_potential(x, beta, log_intermediate_values): + return log_intermediate_values / beta + + +random_state = np.random.RandomState(1234) +seed = -2. + 4. * random_state.rand(5, 2) +betas = [1. / np.sqrt(2.) ** i for i in range(10 - 1, -1, -1)] + + +def test_parallel(): + mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, log_pdf_reference=log_prior, + niter_between_sweeps=4, betas=betas, save_log_pdf=True, + mcmc_class=MH, nburn=10, jump=2, seed=seed, dimension=2, random_state=3456) + mcmc.run(nsamples_per_chain=100) + assert mcmc.samples.shape == (500, 2) + + +def test_thermodynamic_integration(): + mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, log_pdf_reference=log_prior, + niter_between_sweeps=4, betas=betas, save_log_pdf=True, + mcmc_class=MH, nburn=10, jump=2, seed=seed, dimension=2, random_state=3456) + mcmc.run(nsamples_per_chain=100) + log_ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, log_p0=0.) + assert np.round(np.exp(log_ev), 4) == 0.1885 + + +def likelihood(x, b): + mu1 = np.array([1., 1.]) + mu2 = -0.8 * np.ones(2) + w1 = 0.5 + # Width of 0.1 in each dimension + sigma1 = np.diag([0.02, 0.05]) + sigma2 = np.diag([0.05, 0.02]) + + # Posterior is a mixture of two gaussians + like = np.exp(np.logaddexp(np.log(w1) + multivariate_normal.logpdf(x=x, mean=mu1, cov=sigma1), + np.log(1.-w1) + multivariate_normal.logpdf(x=x, mean=mu2, cov=sigma2))) + return like**b + + +def test_sequential(): + prior = JointIndependent(marginals=[Uniform(loc=-2.0, scale=4.0), Uniform(loc=-2.0, scale=4.0)]) + test = SequentialTemperingMCMC(dimension=2, nsamples=100, pdf_intermediate=likelihood, + distribution_reference=prior, nchains=20, save_intermediate_samples=True, + percentage_resampling=10, mcmc_class=MH, verbose=False, random_state=960242069) + assert np.round(test.evidence, 4) == 0.0656 + From e758876f4393805f45f8edd3d45571e1de4cb842 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Fri, 25 Nov 2022 16:26:19 -0500 Subject: [PATCH 08/41] Ports ParallelTemperingMCMC.py to v4 --- src/UQpy/sampling/mcmc/MetropolisHastings.py | 29 ------ src/UQpy/sampling/mcmc/baseclass/MCMC.py | 20 +++- .../tempering_mcmc/ParallelTemperingMCMC.py | 94 +++++++++---------- .../sampling/tempering_mcmc/TemperingMCMC.py | 2 +- tests/unit_tests/sampling/test_tempering.py | 40 ++++---- 5 files changed, 89 insertions(+), 96 deletions(-) diff --git a/src/UQpy/sampling/mcmc/MetropolisHastings.py b/src/UQpy/sampling/mcmc/MetropolisHastings.py index 070420fea..c8f9a4c1a 100644 --- a/src/UQpy/sampling/mcmc/MetropolisHastings.py +++ b/src/UQpy/sampling/mcmc/MetropolisHastings.py @@ -159,32 +159,3 @@ def run_one_iteration(self, current_state: np.ndarray, current_log_pdf: np.ndarr self._update_acceptance_rate(accept_vec) return current_state, current_log_pdf - - def __copy__(self, **kwargs): - pdf_target = self.pdf_target if kwargs['pdf_target'] is None else kwargs['pdf_target'] - log_pdf_target = self.log_pdf_target if kwargs['log_pdf_target'] is None else kwargs['log_pdf_target'] - args_target = self.args_target if kwargs['args_target'] is None else kwargs['args_target'] - burn_length = self.burn_length if kwargs['burn_length'] is None else kwargs['burn_length'] - jump = self.jump if kwargs['jump'] is None else kwargs['jump'] - dimension = self.dimension if kwargs['dimension'] is None else kwargs['dimension'] - seed = self.seed if kwargs['seed'] is None else kwargs['seed'] - save_log_pdf = self.save_log_pdf if kwargs['save_log_pdf'] is None else kwargs['save_log_pdf'] - concatenate_chains = self.concatenate_chains if kwargs['concatenate_chains'] is None\ - else kwargs['concatenate_chains'] - n_chains = self.n_chains if kwargs['n_chains'] is None else kwargs['n_chains'] - proposal = self.proposal if kwargs['proposal'] is None else kwargs['proposal'] - proposal_is_symmetric = self.proposal_is_symmetric if kwargs['proposal_is_symmetric'] is None \ - else kwargs['proposal_is_symmetric'] - random_state = self.random_state if kwargs['random_state'] is None else kwargs['random_state'] - nsamples = self.nsamples if kwargs['nsamples'] is None else kwargs['nsamples'] - nsamples_per_chain = self.nsamples_per_chain if kwargs['nsamples_per_chain'] is None \ - else kwargs['nsamples_per_chain'] - - new = self.__class__(pdf_target=pdf_target, log_pdf_target=log_pdf_target, args_target=args_target, - burn_length=burn_length, jump=jump, dimension=dimension, seed=seed, - save_log_pdf=save_log_pdf, concatenate_chains=concatenate_chains, - proposal=proposal, proposal_is_symmetric=proposal_is_symmetric, n_chains=n_chains, - random_state=random_state, nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) - - return new - diff --git a/src/UQpy/sampling/mcmc/baseclass/MCMC.py b/src/UQpy/sampling/mcmc/baseclass/MCMC.py index f8df33285..2a4936830 100644 --- a/src/UQpy/sampling/mcmc/baseclass/MCMC.py +++ b/src/UQpy/sampling/mcmc/baseclass/MCMC.py @@ -310,6 +310,22 @@ def _check_methods_proposal(proposal_distribution): proposal_distribution.log_pdf = lambda x: np.log( np.maximum(proposal_distribution.pdf(x), 10 ** (-320) * np.ones((x.shape[0],)))) - @abstractmethod def __copy__(self, **kwargs): - pass + keys = kwargs.keys() + attributes = self.__dict__ + import inspect + initializer_parameters = inspect.signature(self.__class__.__init__).parameters.keys() + + for key in attributes.keys(): + if key not in initializer_parameters: + continue + new_value = attributes[key] if key not in keys else kwargs[key] + if new_value is not None: + kwargs[key] = new_value + + if 'seed' in kwargs.keys(): + kwargs['seed'] = list(kwargs['seed']) + if 'nsamples_per_chain' in kwargs.keys() and kwargs['nsamples_per_chain'] == 0: + del kwargs['nsamples_per_chain'] + + return self.__class__(**kwargs) diff --git a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py index d051fdea4..c63da088c 100644 --- a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py @@ -34,11 +34,13 @@ class ParallelTemperingMCMC(TemperingMCMC): """ - def __init__(self, niter_between_sweeps, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), + def __init__(self, n_iterations_between_sweeps: PositiveInteger, + pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), distribution_reference=None, save_log_pdf=False, nsamples=None, nsamples_per_chain=None, random_state=None, - temper_param_list=None, n_temper_params=None, + tempering_parameters=None, + n_tempering_parameters=None, sampler: Union[MCMC, list[MCMC]] = None): super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, @@ -51,59 +53,53 @@ def __init__(self, niter_between_sweeps, pdf_intermediate=None, log_pdf_intermed self.evaluate_log_reference = self._preprocess_reference(self.distribution_reference) # Initialize PT specific inputs: niter_between_sweeps and temperatures - self.niter_between_sweeps = niter_between_sweeps - if not (isinstance(self.niter_between_sweeps, int) and self.niter_between_sweeps >= 1): - raise ValueError('UQpy: input niter_between_sweeps should be a strictly positive integer.') - self.temper_param_list = temper_param_list - self.n_temper_params = n_temper_params - if self.temper_param_list is None: - if self.n_temper_params is None: + self.n_iterations_between_sweeps = n_iterations_between_sweeps + self.tempering_parameters = tempering_parameters + self.n_tempering_parameters = n_tempering_parameters + if self.tempering_parameters is None: + if self.n_tempering_parameters is None: raise ValueError('UQpy: either input temper_param_list or n_temper_params should be provided.') - elif not (isinstance(self.n_temper_params, int) and self.n_temper_params >= 2): + elif not (isinstance(self.n_tempering_parameters, int) and self.n_tempering_parameters >= 2): raise ValueError('UQpy: input n_temper_params should be a integer >= 2.') else: - self.temper_param_list = [1. / np.sqrt(2) ** i for i in range(self.n_temper_params - 1, -1, -1)] - elif (not isinstance(self.temper_param_list, (list, tuple)) - or not (all(isinstance(t, (int, float)) and (0 < t <= 1.) for t in self.temper_param_list)) + self.tempering_parameters = [1. / np.sqrt(2) ** i for i in + range(self.n_tempering_parameters - 1, -1, -1)] + elif (not isinstance(self.tempering_parameters, (list, tuple)) + or not (all(isinstance(t, (int, float)) and (0 < t <= 1.) for t in self.tempering_parameters)) # or float(self.temperatures[0]) != 1. ): raise ValueError( 'UQpy: temper_param_list should be a list of floats in [0, 1], starting at 0. and increasing to 1.') else: - self.n_temper_params = len(self.temper_param_list) - - # Initialize mcmc objects, need as many as number of temperatures - # if not all((isinstance(val, (list, tuple)) and len(val) == self.n_temper_params) - # for val in kwargs_mcmc.values()): - # raise ValueError( - # 'UQpy: additional kwargs arguments should be mcmc algorithm specific inputs, given as lists of length ' - # 'the number of temperatures.') + self.n_tempering_parameters = len(self.tempering_parameters) + # default value kwargs_mcmc = {} - if isinstance(self.sampler, MetropolisHastings) and not kwargs_mcmc: + if isinstance(self.sampler, MetropolisHastings) and self.sampler.proposal is None: from UQpy.distributions import JointIndependent, Normal - kwargs_mcmc = {'proposal_is_symmetric': [True, ] * self.n_temper_params, + kwargs_mcmc = {'proposal_is_symmetric': [True, ] * self.n_tempering_parameters, 'proposal': [JointIndependent([Normal(scale=1. / np.sqrt(temper_param))] * self.sampler.dimension) - for temper_param in self.temper_param_list]} + for temper_param in self.tempering_parameters]} # Initialize algorithm specific inputs: target pdfs self.thermodynamic_integration_results = None self.mcmc_samplers = [] - for i, temper_param in enumerate(self.temper_param_list): + for i, temper_param in enumerate(self.tempering_parameters): log_pdf_target = (lambda x, temper_param=temper_param: self.evaluate_log_reference( x) + self.evaluate_log_intermediate(x, temper_param)) self.mcmc_samplers.append(sampler.__copy__(log_pdf_target=log_pdf_target, concatenate_chains=True, - **kwargs_mcmc)) + save_log_pdf=save_log_pdf, random_state=self.random_state, + **dict([(key, val[i]) for key, val in kwargs_mcmc.items()]))) self.logger.info('\nUQpy: Initialization of ' + self.__class__.__name__ + ' algorithm complete.') # If nsamples is provided, run the algorithm if (nsamples is not None) or (nsamples_per_chain is not None): - self._run(nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) + self.run(nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) - def _run(self, nsamples=None, nsamples_per_chain=None): + def run(self, nsamples=None, nsamples_per_chain=None): """ Run the MCMC algorithm. @@ -122,35 +118,39 @@ def _run(self, nsamples=None, nsamples_per_chain=None): of `nchains`, `nsamples` is set to the next largest integer that is a multiple of `nchains`. """ - # Initialize the runs: allocate space for the new samples and log pdf values - final_ns, final_ns_per_chain, current_state_t, current_log_pdf_t = self.mcmc_samplers[0]._initialize_samples( - nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) - current_state, current_log_pdf = [current_state_t.copy(), ], [current_log_pdf_t.copy(), ] - for mcmc_sampler in self.mcmc_samplers[1:]: - _, _, current_state_t, current_log_pdf_t = mcmc_sampler._initialize_samples( + current_state, current_log_pdf = [], [] + final_ns_per_chain = 0 + for i, mcmc_sampler in enumerate(self.mcmc_samplers): + if mcmc_sampler.evaluate_log_target is None and mcmc_sampler.evaluate_log_target_marginals is None: + (mcmc_sampler.evaluate_log_target, mcmc_sampler.evaluate_log_target_marginals,) = \ + mcmc_sampler._preprocess_target(pdf_=mcmc_sampler.pdf_target, + log_pdf_=mcmc_sampler.log_pdf_target, + args=mcmc_sampler.args_target) + ns, ns_per_chain, current_state_t, current_log_pdf_t = mcmc_sampler._initialize_samples( nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) current_state.append(current_state_t.copy()) current_log_pdf.append(current_log_pdf_t.copy()) + if i == 0: + final_ns_per_chain = ns_per_chain self.logger.info('UQpy: Running MCMC...') # Run nsims iterations of the MCMC algorithm, starting at current_state while self.mcmc_samplers[0].nsamples_per_chain < final_ns_per_chain: # update the total number of iterations - # self.mcmc_samplers[0].niterations += 1 # run one iteration of MCMC algorithms at various temperatures new_state, new_log_pdf = [], [] for t, sampler in enumerate(self.mcmc_samplers): - sampler.niterations += 1 + sampler.iterations_number += 1 new_state_t, new_log_pdf_t = sampler.run_one_iteration( current_state[t], current_log_pdf[t]) new_state.append(new_state_t.copy()) new_log_pdf.append(new_log_pdf_t.copy()) # Do sweeps if necessary - if self.mcmc_samplers[-1].niterations % self.niter_between_sweeps == 0: - for i in range(self.n_temper_params - 1): + if self.mcmc_samplers[-1].iterations_number % self.n_iterations_between_sweeps == 0: + for i in range(self.n_tempering_parameters - 1): log_accept = (self.mcmc_samplers[i].evaluate_log_target(new_state[i + 1]) + self.mcmc_samplers[i + 1].evaluate_log_target(new_state[i]) - self.mcmc_samplers[i].evaluate_log_target(new_state[i]) - @@ -162,22 +162,20 @@ def _run(self, nsamples=None, nsamples_per_chain=None): # Update the chain, only if burn-in is over and the sample is not being jumped over # also increase the current number of samples and samples_per_chain - if self.mcmc_samplers[-1].niterations > self.mcmc_samplers[-1].nburn and \ - (self.mcmc_samplers[-1].niterations - self.mcmc_samplers[-1].nburn) % self.mcmc_samplers[ - -1].jump == 0: + if self.mcmc_samplers[-1].iterations_number > self.mcmc_samplers[-1].burn_length and \ + (self.mcmc_samplers[-1].iterations_number - + self.mcmc_samplers[-1].burn_length) % self.mcmc_samplers[-1].jump == 0: for t, sampler in enumerate(self.mcmc_samplers): sampler.samples[sampler.nsamples_per_chain, :, :] = new_state[t].copy() if self.save_log_pdf: sampler.log_pdf_values[sampler.nsamples_per_chain, :] = new_log_pdf[t].copy() sampler.nsamples_per_chain += 1 - sampler.nsamples += sampler.nchains - # self.nsamples_per_chain += 1 - # self.nsamples += self.nchains + sampler.samples_counter += sampler.n_chains self.logger.info('UQpy: MCMC run successfully !') # Concatenate chains maybe - if self.mcmc_samplers[-1].concat_chains: + if self.mcmc_samplers[-1].concatenate_chains: for t, mcmc_sampler in enumerate(self.mcmc_samplers): mcmc_sampler._concatenate_chains() @@ -217,20 +215,20 @@ def evaluate_normalization_constant(self, compute_potential, log_Z0=None, nsampl raise ValueError('UQpy: input log_Z0 or nsamples_from_p0 should be provided.') # compute average of log_target for the target at various temperatures log_pdf_averages = [] - for i, (temper_param, sampler) in enumerate(zip(self.temper_param_list, self.mcmc_samplers)): + for i, (temper_param, sampler) in enumerate(zip(self.tempering_parameters, self.mcmc_samplers)): log_factor_values = sampler.log_pdf_values - self.evaluate_log_reference(sampler.samples) potential_values = compute_potential( x=sampler.samples, temper_param=temper_param, log_intermediate_values=log_factor_values) log_pdf_averages.append(np.mean(potential_values)) # use quadrature to integrate between 0 and 1 - temper_param_list_for_integration = np.copy(np.array(self.temper_param_list)) + temper_param_list_for_integration = np.copy(np.array(self.tempering_parameters)) log_pdf_averages = np.array(log_pdf_averages) int_value = trapz(x=temper_param_list_for_integration, y=log_pdf_averages) if log_Z0 is None: samples_p0 = self.distribution_reference.rvs(nsamples=nsamples_from_p0) log_Z0 = np.log(1. / nsamples_from_p0) + logsumexp( - self.evaluate_log_intermediate(x=samples_p0, temper_param=self.temper_param_list[0])) + self.evaluate_log_intermediate(x=samples_p0, temper_param=self.tempering_parameters[0])) self.thermodynamic_integration_results = { 'log_Z0': log_Z0, 'temper_param_list': temper_param_list_for_integration, diff --git a/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py index 571f79546..2d7b10080 100644 --- a/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py +++ b/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py @@ -37,7 +37,7 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in if self.save_log_pdf: self.log_pdf_values = None - def _run(self, nsamples): + def run(self, nsamples): """ Run the tempering MCMC algorithms to generate nsamples from the target posterior """ pass diff --git a/tests/unit_tests/sampling/test_tempering.py b/tests/unit_tests/sampling/test_tempering.py index c0c7106f4..c97d0ef3b 100644 --- a/tests/unit_tests/sampling/test_tempering.py +++ b/tests/unit_tests/sampling/test_tempering.py @@ -1,9 +1,7 @@ import numpy as np from scipy.stats import multivariate_normal - from UQpy.distributions import Uniform, JointIndependent -from UQpy.sampling.tempering_mcmc import ParallelTemperingMCMC, SequentialTemperingMCMC -from UQpy.sampling.mcmc import MetropolisHastings +from UQpy.sampling import MetropolisHastings, ParallelTemperingMCMC, SequentialTemperingMCMC def log_rosenbrock(x): @@ -19,30 +17,41 @@ def log_prior(x): return Uniform(loc=loc, scale=scale).log_pdf(x[:, 0]) + Uniform(loc=loc, scale=scale).log_pdf(x[:, 1]) -def compute_potential(x, beta, log_intermediate_values): - return log_intermediate_values / beta +def compute_potential(x, temper_param, log_intermediate_values): + return log_intermediate_values / temper_param random_state = np.random.RandomState(1234) seed = -2. + 4. * random_state.rand(5, 2) betas = [1. / np.sqrt(2.) ** i for i in range(10 - 1, -1, -1)] +prior_distribution = JointIndependent(marginals=[Uniform(loc=-2, scale=4), Uniform(loc=-2, scale=4)]) + def test_parallel(): - mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, log_pdf_reference=log_prior, - niter_between_sweeps=4, betas=betas, save_log_pdf=True, - mcmc_class=MH, nburn=10, jump=2, seed=seed, dimension=2, random_state=3456) + sampler = MetropolisHastings(burn_length=10, jump=2, seed=list(seed), dimension=2) + mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, + distribution_reference=prior_distribution, + n_iterations_between_sweeps=4, + tempering_parameters=betas, + random_state=3456, + save_log_pdf=False, sampler=sampler) mcmc.run(nsamples_per_chain=100) assert mcmc.samples.shape == (500, 2) def test_thermodynamic_integration(): - mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, log_pdf_reference=log_prior, - niter_between_sweeps=4, betas=betas, save_log_pdf=True, - mcmc_class=MH, nburn=10, jump=2, seed=seed, dimension=2, random_state=3456) + sampler = MetropolisHastings(burn_length=10, jump=2, seed=list(seed), dimension=2) + mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, + distribution_reference=prior_distribution, + n_iterations_between_sweeps=4, + tempering_parameters=betas, + save_log_pdf=True, + random_state=3456, + sampler=sampler) mcmc.run(nsamples_per_chain=100) - log_ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, log_p0=0.) - assert np.round(np.exp(log_ev), 4) == 0.1885 + log_ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, log_Z0=0.) + assert np.round(log_ev, 4) == 0.203 def likelihood(x, b): @@ -55,8 +64,8 @@ def likelihood(x, b): # Posterior is a mixture of two gaussians like = np.exp(np.logaddexp(np.log(w1) + multivariate_normal.logpdf(x=x, mean=mu1, cov=sigma1), - np.log(1.-w1) + multivariate_normal.logpdf(x=x, mean=mu2, cov=sigma2))) - return like**b + np.log(1. - w1) + multivariate_normal.logpdf(x=x, mean=mu2, cov=sigma2))) + return like ** b def test_sequential(): @@ -65,4 +74,3 @@ def test_sequential(): distribution_reference=prior, nchains=20, save_intermediate_samples=True, percentage_resampling=10, mcmc_class=MH, verbose=False, random_state=960242069) assert np.round(test.evidence, 4) == 0.0656 - From 1b479bdfbd21439c1433588a060cfaa7a5de90eb Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Fri, 25 Nov 2022 17:15:34 -0500 Subject: [PATCH 09/41] Ports SequentialTemperingMCMC.py to v4 --- .../tempering_mcmc/SequentialTemperingMCMC.py | 28 +++++++++++-------- tests/unit_tests/sampling/test_tempering.py | 11 ++++++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py index f9e98f525..d8aa43416 100644 --- a/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py @@ -83,7 +83,8 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in # Initialize input distributions self.evaluate_log_reference, self.seed = self._preprocess_reference(dist_=distribution_reference, seed_=seed, nsamples=nsamples, - dimension=self.__dimension) + dimension=self.__dimension, + random_state=self.random_state) # Initialize flag that indicates whether default proposal is to be used (default proposal defined adaptively # during run) @@ -95,12 +96,12 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in # Call the run function if nsamples is not None: - self._run(nsamples=nsamples) + self.run(nsamples=nsamples) else: raise ValueError('UQpy: a value for "nsamples" must be specified ') @beartype - def _run(self, nsamples: PositiveInteger = None): + def run(self, nsamples: PositiveInteger = None): self.logger.info('TMCMC Start') @@ -175,7 +176,7 @@ def _run(self, nsamples: PositiveInteger = None): for i in range(self.n_resamples): # Resampling from previous tempering level - lead_index = int(np.random.choice(pts_index, p=weight_probabilities)) + lead_index = int(self.random_state.choice(pts_index, p=weight_probabilities)) lead = points[lead_index] # Defining the default proposal @@ -183,8 +184,9 @@ def _run(self, nsamples: PositiveInteger = None): self.proposal = MultivariateNormal(lead, cov=sigma_matrix) # Single MH-MCMC step - x = MetropolisHastings(dimension=self.__dimension, log_pdf_target=mcmc_log_pdf_target, seed=lead, - nsamples=1, nchains=1, nburn=self.resampling_burn_length, proposal=self.proposal, + x = MetropolisHastings(dimension=self.__dimension, log_pdf_target=mcmc_log_pdf_target, seed=list(lead), + nsamples=1, n_chains=1, burn_length=self.resampling_burn_length, + proposal=self.proposal, random_state=self.random_state, proposal_is_symmetric=self.proposal_is_symmetric) # Setting the generated sample in the array @@ -199,12 +201,14 @@ def _run(self, nsamples: PositiveInteger = None): self.logger.info('Begin MCMC') mcmc_seed = self._mcmc_seed_generator(resampled_pts=points[0:self.n_resamples, :], arr_length=self.n_resamples, - seed_length=self.__n_chains) + seed_length=self.__n_chains, + random_state=self.random_state) y = copy.deepcopy(self.sampler) self.update_target_and_seed(y, mcmc_seed, mcmc_log_pdf_target) y = self.sampler.__copy__(log_pdf_target=mcmc_log_pdf_target, seed=mcmc_seed, - nsamples_per_chain=self.n_samples_per_chain, concat_chains=True) + nsamples_per_chain=self.n_samples_per_chain, + concatenate_chains=True, random_state=self.random_state) points[self.n_resamples:, :] = y.samples if self.save_intermediate_samples is True: @@ -288,7 +292,7 @@ def _find_temper_param(temper_param_prev, samples, q_func, n, iter_lim=1000, ite raise RuntimeError('UQpy: unable to find tempering exponent due to nonconvergence') return temper_param_trial - def _preprocess_reference(self, dist_, seed_=None, nsamples=None, dimension=None): + def _preprocess_reference(self, dist_, seed_=None, nsamples=None, dimension=None, random_state=None): """ Preprocess the target pdf inputs. @@ -316,7 +320,7 @@ def _preprocess_reference(self, dist_, seed_=None, nsamples=None, dimension=None if not (isinstance(dist_, Distribution)): raise TypeError('UQpy: A UQpy.Distribution object must be provided.') evaluate_log_pdf = (lambda x: dist_.log_pdf(x)) - seed_values = dist_.rvs(nsamples=nsamples) + seed_values = dist_.rvs(nsamples=nsamples, random_state=random_state) elif seed_ is not None: if seed_.shape[0] != nsamples or seed_.shape[1] != dimension: raise TypeError('UQpy: the seed values should be a numpy array of size (nsamples, dimension)') @@ -328,7 +332,7 @@ def _preprocess_reference(self, dist_, seed_=None, nsamples=None, dimension=None return evaluate_log_pdf, seed_values @staticmethod - def _mcmc_seed_generator(resampled_pts, arr_length, seed_length): + def _mcmc_seed_generator(resampled_pts, arr_length, seed_length, random_state): """ Generates the seed from the resampled samples for the mcmc step @@ -346,6 +350,6 @@ def _mcmc_seed_generator(resampled_pts, arr_length, seed_length): * evaluate_log_pdf (callable): Callable that computes the log of the target density function (the prior) """ index_arr = np.arange(arr_length) - seed_indices = np.random.choice(index_arr, size=seed_length, replace=False) + seed_indices = random_state.choice(index_arr, size=seed_length, replace=False) mcmc_seed = resampled_pts[seed_indices, :] return mcmc_seed diff --git a/tests/unit_tests/sampling/test_tempering.py b/tests/unit_tests/sampling/test_tempering.py index c97d0ef3b..72e3b0f09 100644 --- a/tests/unit_tests/sampling/test_tempering.py +++ b/tests/unit_tests/sampling/test_tempering.py @@ -70,7 +70,12 @@ def likelihood(x, b): def test_sequential(): prior = JointIndependent(marginals=[Uniform(loc=-2.0, scale=4.0), Uniform(loc=-2.0, scale=4.0)]) - test = SequentialTemperingMCMC(dimension=2, nsamples=100, pdf_intermediate=likelihood, - distribution_reference=prior, nchains=20, save_intermediate_samples=True, - percentage_resampling=10, mcmc_class=MH, verbose=False, random_state=960242069) + sampler = MetropolisHastings(dimension=2, n_chains=20) + test = SequentialTemperingMCMC(pdf_intermediate=likelihood, + distribution_reference=prior, + save_intermediate_samples=True, + percentage_resampling=10, + random_state=960242069, + sampler=sampler, + nsamples=100) assert np.round(test.evidence, 4) == 0.0656 From 89827631c7fa5091f0ed2270a288fbe4125b578c Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Tue, 29 Nov 2022 13:47:17 -0500 Subject: [PATCH 10/41] Port TMCMC examples to v4 --- docs/code/sampling/tempering/README.rst | 2 + .../tempering/local_reliability_funcs.py | 5 + .../sampling/tempering/parallel_tempering.py | 446 ++++++++++++++++++ .../tempering/sequential_tempering.py | 283 +++++++++++ .../tempering_mcmc/ParallelTemperingMCMC.py | 24 +- tests/unit_tests/sampling/test_tempering.py | 8 +- 6 files changed, 752 insertions(+), 16 deletions(-) create mode 100644 docs/code/sampling/tempering/README.rst create mode 100644 docs/code/sampling/tempering/local_reliability_funcs.py create mode 100644 docs/code/sampling/tempering/parallel_tempering.py create mode 100644 docs/code/sampling/tempering/sequential_tempering.py diff --git a/docs/code/sampling/tempering/README.rst b/docs/code/sampling/tempering/README.rst new file mode 100644 index 000000000..36ae64cfb --- /dev/null +++ b/docs/code/sampling/tempering/README.rst @@ -0,0 +1,2 @@ +Tempering MCMC Examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/code/sampling/tempering/local_reliability_funcs.py b/docs/code/sampling/tempering/local_reliability_funcs.py new file mode 100644 index 000000000..5b78b1e87 --- /dev/null +++ b/docs/code/sampling/tempering/local_reliability_funcs.py @@ -0,0 +1,5 @@ +import numpy as np + + +def correlated_gaussian(samples, b_eff, d): + return [b_eff * np.sqrt(d) - np.sum(samples[i, :]) for i in range(samples.shape[0])] diff --git a/docs/code/sampling/tempering/parallel_tempering.py b/docs/code/sampling/tempering/parallel_tempering.py new file mode 100644 index 000000000..2a6eee1ad --- /dev/null +++ b/docs/code/sampling/tempering/parallel_tempering.py @@ -0,0 +1,446 @@ +""" + +Parallel Tempering for Bayesian Inference and Reliability analyses +==================================================================== +""" + +# %% md +# The general framework: one wants to sample from a distribution of the form +# +# $$ p_{1}(x) = \frac{q_{1}(x) p_{0}(x)}{Z_{1}} $$ +# +# where $q_{1}(x)$ and $p_{0}(x)$ can be evaluated; and potentially estimate the constant $Z_{1}=\int{q_{1}(x) p_{0}(x)dx}$. Parallel tempering introduces a sequence of intermediate distributions: +# +# $$ p_{\beta}(x) \propto q(x, \beta) p_{0}(x) $$ +# +# for values of $\beta$ in [0, 1] (note: $\beta$ is $1/T$ where $T$ is often referred as the temperature). Setting $\beta=1$ equates sampling from the target, while $\beta \rightarrow 0$ samples from the reference distribution $p_{0}$. Periodically during the run, the different temperatures swap members of their ensemble in a way that preserves detailed balance. The chains closer to the reference chain (hot chains) can sample from regions that have low probability under the target and thus allow a better exploration of the parameter space, while the cold chains can better explore the regions of high likelihood. +# +# The normalizing constant $Z_{1}$ is estimated via thermodynamic integration: +# +# $$ \ln{Z_{\beta=1}} = \ln{Z_{\beta=0}} + \int_{0}^{1} E_{p_{\beta}} \left[ \frac{\partial \ln{q_{\beta}(x)}}{\partial \beta} \right] d\beta = \ln{Z_{\beta=0}} + \int_{0}^{1} E_{p_{\beta}} \left[ U_{\beta}(x) \right] d\beta$$ +# +# where $\ln{Z_{\beta=0}}=\int{q_{\beta=0}(x) p_{0}(x)dx}$ can be determined by simple MC sampling since $q_{\beta=0}(x)$ is close to the reference distribution $p_{0}$. The function $U_{\beta}(x)=\frac{\partial \ln{q_{\beta}(x)}}{\partial \beta}$ is called the potential, and can be evaluated using posterior samples from $p_{\beta}(x)$. +# +# In the code, the user must define: +# - a function to evaluate the reference distribution $p_{0}(x)$, +# - a function to evaluate the intermediate factor $q(x, \beta)$ (function that takes in two inputs: x and $\beta$), +# - if evaluation of $Z_{1}$ is of interest, a function that evaluates the potential $U_{\beta}(x)$, from evaluations of $\ln{(x, \beta)}$ which are saved during the MCMC run for the various chains (different $\beta$ values). +# +# Bayesian inference +# +# In the Bayesian setting, $p_{0}$ is the prior and, given a likelihood $L(data; x)$: +# +# $$ q_{T}(x) = L(data; x) ^{\beta} $$ +# +# Then for the model evidence: +# +# $$ U_{\beta}(x) = \ln{L(data; x)} $$ +# +# %% + +import numpy as np +import matplotlib.pyplot as plt + +from UQpy.run_model import RunModel, PythonModel +from UQpy.distributions import MultivariateNormal, JointIndependent, Normal, Uniform + +# %% md +# %% + +from scipy.stats import multivariate_normal, norm, uniform + +# bimodal posterior +mu1 = np.array([1., 1.]) +mu2 = -0.8 * np.ones(2) +w1 = 0.5 +# Width of 0.1 in each dimension +sigma1 = np.diag([0.02, 0.05]) +sigma2 = np.diag([0.05, 0.02]) + +# define prior, likelihood and target (posterior) +prior_distribution = JointIndependent(marginals=[Uniform(loc=-2, scale=4), Uniform(loc=-2, scale=4)]) + + +def log_likelihood(x): + # Posterior is a mixture of two gaussians + return np.logaddexp(np.log(w1) + multivariate_normal.logpdf(x=x, mean=mu1, cov=sigma1), + np.log(1. - w1) + multivariate_normal.logpdf(x=x, mean=mu2, cov=sigma2)) + + +def log_target(x): + return log_likelihood(x) + prior_distribution.log_pdf(x) + + +# %% md +# %% + +# estimate evidence +def estimate_evidence_from_prior_samples(size): + samples = -2. + 4 * np.random.uniform(size=size * 2).reshape((size, 2)) + return np.mean(np.exp(log_likelihood(samples))) + + +def func_integration(x1, x2): + x = np.array([x1, x2]).reshape((1, 2)) + return np.exp(log_likelihood(x)) * (1. / 4) ** 2 + + +def estimate_evidence_from_quadrature(): + from scipy.integrate import dblquad + ev = dblquad(func=func_integration, a=-2, b=2, gfun=lambda x: -2, hfun=lambda x: 2) + return ev + + +x = np.arange(-2, 2, 0.02) +y = np.arange(-2, 2, 0.02) +xx, yy = np.meshgrid(x, y) +z = np.exp(log_likelihood(np.concatenate([xx.reshape((-1, 1)), yy.reshape((-1, 1))], axis=-1))) +h = plt.contourf(x, y, z.reshape(xx.shape)) +plt.title('Likelihood') +plt.axis('equal') +plt.show() + +print('Evidence computed analytically = {}'.format(estimate_evidence_from_quadrature()[0])) + +# %% md +# %% + +from UQpy.sampling.mcmc import MetropolisHastings + +seed = -2. + 4. * np.random.rand(100, 2) +mcmc0 = MetropolisHastings(log_pdf_target=log_target, burn_length=100, jump=3, seed=list(seed), dimension=2, + random_state=123, save_log_pdf=True) +mcmc0.run(nsamples_per_chain=200) + +print(mcmc0.samples.shape) +fig, ax = plt.subplots(ncols=1, figsize=(6, 4)) +ax.scatter(mcmc0.samples[:, 0], mcmc0.samples[:, 1], alpha=0.5) +ax.set_xlim([-2, 2]) +ax.set_ylim([-2, 2]) +plt.show() + + +def estimate_evidence_from_posterior_samples(log_posterior_values, posterior_samples): + log_like = log_likelihood(posterior_samples) # log_posterior_values - log_prior(posterior_samples) + ev = 1. / np.mean(1. / np.exp(log_like)) + return ev + + +evidence = estimate_evidence_from_posterior_samples( + log_posterior_values=mcmc0.log_pdf_values, posterior_samples=mcmc0.samples) +print('Estimated evidence by HM={}'.format(evidence)) + + +# %% md +# + +# %% + +def log_intermediate(x, temper_param): + return temper_param * log_likelihood(x) # + (1. - 1. / temperature) * log_prior(x) + + +# %% md +# + +# %% + +from UQpy.sampling import MetropolisHastings, ParallelTemperingMCMC + +seed = -2. + 4. * np.random.rand(5, 2) +betas = [1. / np.sqrt(2.) ** i for i in range(20 - 1, -1, -1)] +print(len(betas)) +print(betas) + +samplers = [MetropolisHastings(burn_length=100, jump=3, seed=list(seed), dimension=2) for _ in range(len(betas))] +mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, + distribution_reference=prior_distribution, + n_iterations_between_sweeps=10, + tempering_parameters=betas, + random_state=123, + save_log_pdf=True, samplers=samplers) + +mcmc.run(nsamples_per_chain=200) +print(mcmc.samples.shape) +print(mcmc.mcmc_samplers[-1].samples.shape) + +# %% md +# + +# %% + +# the intermediate samples can be accessed via the mcmc_samplers.samples attributes or +# directly via the intermediate_samples attribute +fig, ax = plt.subplots(ncols=3, figsize=(12, 3.5)) +for j, ind in enumerate([0, -6, -1]): + ax[j].scatter(mcmc.mcmc_samplers[ind].samples[:, 0], mcmc.mcmc_samplers[ind].samples[:, 1], alpha=0.5, + color='orange') + # ax[j].scatter(mcmc.intermediate_samples[ind][:, 0], mcmc.intermediate_samples[ind][:, 1], alpha=0.5, + # color='orange') + ax[j].set_xlim([-2, 2]) + ax[j].set_ylim([-2, 2]) + ax[j].set_title(r'$\beta$ = {:.3f}'.format(mcmc.tempering_parameters[ind]), fontsize=16) + ax[j].set_xlabel(r'$\theta_{1}$', fontsize=14) + ax[j].set_ylabel(r'$\theta_{2}$', fontsize=14) +plt.tight_layout() +plt.show() + + +# %% md +# + +# %% + +def compute_potential(x, temper_param, log_intermediate_values): + """ """ + return log_intermediate_values / temper_param + + +# %% md +# + +# %% + +ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, log_Z0=0.) +print('Estimate of evidence by thermodynamic integration = {:.4f}'.format(ev)) + +ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, nsamples_from_p0=5000) +print('Estimate of evidence by thermodynamic integration = {:.4f}'.format(ev)) + +# %% md +# ## Reliability +# +# In a reliability context, $p_{0}$ is the pdf of the parameters and we have: +# +# $$ q_{\beta}(x) = I_{\beta}(x) = \frac{1}{1 + \exp{ \left( \frac{G(x)}{1/\beta-1}\right)}} $$ +# +# where $G(x)$ is the performance function, negative if the system fails, and $I_{\beta}(x)$ are smoothed versions of the indicator function. Then to compute the probability of failure, the potential can be computed as: +# +# $$ U_{\beta}(x) = \frac{- \frac{G(x)}{(1-\beta)^2}}{1 + \exp{ \left( -\frac{G(x)}{1/\beta-1} \right) }} = - \frac{1 - I_{\beta}(x)}{\beta (1 - \beta)} \ln{ \left[ \frac{1 - I_{\beta}(x)}{I_{\beta}(x)} \right] }$$ + +# %% + +from scipy.stats import norm + + +def indic_sigmoid(y, beta): + return 1. / (1. + np.exp(y / (1. / beta - 1.))) + + +fig, ax = plt.subplots(figsize=(4, 3.5)) +ys = np.linspace(-5, 5, 100) +for i, s in enumerate(1. / np.array([1.01, 1.25, 2., 4., 70.])): + ax.plot(ys, indic_sigmoid(y=ys, beta=s), label=r'$\beta={:.2f}$'.format(s), color='blue', alpha=1. - i / 6) +ax.set_xlabel(r'$y=g(\theta)$', fontsize=13) +ax.set_ylabel(r'$q_{\beta}(\theta)=I_{\beta}(y)$', fontsize=13) +# ax.set_title(r'Smooth versions of the indicator function', fontsize=14) +ax.legend(fontsize=8.5) +plt.show() + +# %% md +# + +# %% + +beta = 2 # Specified Reliability Index +rho = 0.7 # Specified Correlation +dim = 2 # Dimension + +# Define the correlation matrix +C = np.ones((dim, dim)) * rho +np.fill_diagonal(C, 1) +print(C) + +# Print information related to the true probability of failure +e, v = np.linalg.eig(np.asarray(C)) +beff = np.sqrt(np.max(e)) * beta +print(beff) +from scipy.stats import norm + +pf_true = norm.cdf(-beta) +print('True pf={}'.format(pf_true)) + + +# %% md +# + +# %% + +def estimate_Pf_0(samples, model_values): + mask = model_values <= 0 + return np.sum(mask) / len(mask) + + +# %% md +# + +# %% + +# Sample from the prior +model = RunModel(model=PythonModel(model_script='local_reliability_funcs.py', model_object_name="correlated_gaussian", + b_eff=beff, d=dim)) +samples = MultivariateNormal(mean=np.zeros((2,)), cov=np.array([[1, 0.7], [0.7, 1]])).rvs(nsamples=20000) +model.run(samples=samples, append_samples=False) +model_values = np.array(model.qoi_list) + +print('Prob. failure (MC) = {}'.format(estimate_Pf_0(samples, model_values))) + +fig, ax = plt.subplots(figsize=(4, 3.5)) +mask = model_values <= 0 +ax.scatter(samples[np.squeeze(mask), 0], samples[np.squeeze(mask), 1], color='red', label='fail', alpha=0.5, marker='d') +ax.scatter(samples[~np.squeeze(mask), 0], samples[~np.squeeze(mask), 1], color='blue', label='safe', alpha=0.5) +plt.axis('equal') +plt.xlabel(r'$\theta_{1}$', fontsize=13) +plt.ylabel(r'$\theta_{2}$', fontsize=13) +ax.legend(fontsize=13) +fig.tight_layout() +plt.show() + +# %% md +# + +# %% + +distribution_reference = MultivariateNormal(mean=np.zeros((2,)), cov=np.array([[1, 0.7], [0.7, 1]])) + + +def log_factor_temp(x, temper_param): + model.run(samples=x, append_samples=False) + G_values = np.array(model.qoi_list) + return np.squeeze(np.log(indic_sigmoid(G_values, temper_param))) + + +# %% md +# + +# %% + +betas = (1. / np.array([1.01, 1.02, 1.05, 1.1, 1.2, 1.5, 2., 3., 5., 10., 25., 70.]))[::-1] + +print(len(betas)) +print(betas) + +fig, ax = plt.subplots(figsize=(5, 4)) +ys = np.linspace(-5, 5, 100) +for i, s in enumerate(betas): + ax.plot(ys, indic_sigmoid(y=ys, beta=s), label=r'$\beta={:.2f}$'.format(s), color='blue', alpha=1. - i / 15) +ax.set_xlabel(r'$y=g(\theta)$', fontsize=13) +ax.set_ylabel(r'$I_{\beta}(y)$', fontsize=13) +ax.set_title(r'Smooth versions of the indicator function', fontsize=14) +ax.legend() +plt.show() + +scales = [0.1 / np.sqrt(beta) for beta in betas] +print(scales) + +# %% md +# + +# %% + +from UQpy.sampling import MetropolisHastings, ParallelTemperingMCMC + +seed = -2. + 4. * np.random.rand(5, 2) + +print(betas) +samplers = [MetropolisHastings(burn_length=5000, jump=5, seed=list(seed), dimension=2, + proposal_is_symmetric=True, + proposal=JointIndependent([Normal(scale=scale)] * 2)) for scale in scales] +mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_factor_temp, + distribution_reference=distribution_reference, + n_iterations_between_sweeps=10, + tempering_parameters=list(betas), + random_state=123, + save_log_pdf=True, samplers=samplers) + +mcmc.run(nsamples_per_chain=250) +print(mcmc.samples.shape) +print(mcmc.mcmc_samplers[0].samples.shape) + +# %% md +# + +# %% + +fig, ax = plt.subplots(ncols=3, figsize=(12, 3.5)) +for j, ind in enumerate([0, 6, -1]): + ax[j].scatter(mcmc.mcmc_samplers[ind].samples[:, 0], mcmc.mcmc_samplers[ind].samples[:, 1], alpha=0.25) + ax[j].set_xlim([-4, 4]) + ax[j].set_ylim([-4, 4]) + ax[j].set_title(r'$\beta$ = {:.3f}'.format(mcmc.tempering_parameters[ind]), fontsize=15) + ax[j].set_xlabel(r'$\theta_{1}$', fontsize=13) + ax[j].set_ylabel(r'$\theta_{2}$', fontsize=13) +fig.tight_layout() +plt.show() + + +# %% md +# + +# %% + +def compute_potential(x, temper_param, log_intermediate_values): + indic_beta = np.exp(log_intermediate_values) + indic_beta = np.where(indic_beta > 1. - 1e-16, 1. - 1e-16, indic_beta) + indic_beta = np.where(indic_beta < 1e-16, 1e-16, indic_beta) + tmp_log = np.log((1. - indic_beta) / indic_beta) + return - (1. - indic_beta) / (temper_param * (1. - temper_param)) * tmp_log + + +# %% md +# + +# %% + +ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, log_Z0=np.log(0.5)) +print('Estimate of evidence by thermodynamic integration = {}'.format(ev)) + +# %% md +# + +# %% + +plt.plot(mcmc.thermodynamic_integration_results['temper_param_list'], + mcmc.thermodynamic_integration_results['expect_potentials'], marker='x') +plt.grid(True) + +# %% md +# + +# %% + +seed = -2. + 4. * np.random.rand(5, 2) + +samplers = [MetropolisHastings(burn_length=5000, jump=5, seed=list(seed), dimension=2, + proposal_is_symmetric=True, + proposal=JointIndependent([Normal(scale=scale)] * 2)) for scale in scales] +mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_factor_temp, + distribution_reference=distribution_reference, + n_iterations_between_sweeps=10, + tempering_parameters=list(betas), + random_state=123, + save_log_pdf=True, samplers=samplers) + +list_ev_0, list_ev_1 = [], [] +nsamples_per_chain = 0 +for i in range(50): + nsamples_per_chain += 50 + mcmc.run(nsamples_per_chain=nsamples_per_chain) + ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, log_Z0=np.log(0.5)) + # print(np.exp(log_ev)) + list_ev_0.append(ev) + ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, nsamples_from_p0=100000) + list_ev_1.append(ev) + +# %% md +# + +# %% + +fig, ax = plt.subplots(figsize=(5, 3.5)) +list_samples = [5 * i * 50 for i in range(1, 51)] +ax.plot(list_samples, list_ev_0) +ax.grid(True) +ax.set_ylabel(r'$Z_{1}$ = proba. failure', fontsize=14) +ax.set_xlabel(r'nb. saved samples per chain', fontsize=14) +plt.show() diff --git a/docs/code/sampling/tempering/sequential_tempering.py b/docs/code/sampling/tempering/sequential_tempering.py new file mode 100644 index 000000000..8385aa5cb --- /dev/null +++ b/docs/code/sampling/tempering/sequential_tempering.py @@ -0,0 +1,283 @@ +""" + +Sequential Tempering for Bayesian Inference and Reliability analyses +==================================================================== +""" + +# %% md +# The general framework: one wants to sample from a distribution of the form +# +# \begin{equation} +# p_1 \left( x \right) = \frac{q_1 \left( x \right)p_0 \left( x \right)}{Z_1} +# \end{equation} +# +# where $ q_1 \left( x \right) $ and $ p_0 \left( x \right) $ can be evaluated; and potentially estimate the constant $ Z_1 = \int q_1 \left( x \right)p_0 \left( x \right) dx $. +# +# Sequential tempering introduces a sequence of intermediate distributions: +# +# \begin{equation} +# p_{\beta_j} \left( x \right) \propto q \left( x, \beta_j \right)p_0 \left( x \right) +# \end{equation} +# +# for values of $ \beta_j $ in $ [0, 1] $. The algorithm starts with $ \beta_0 = 0 $, which samples from the reference distribution $ p_0 $, and ends for some $ j = m $ such that $ \beta_m = 1 $, sampling from the target. First, a set of sample points is generated from $ p_0 = p_{\beta_0} $, and then these are resampled according to some weights $ w_0 $ such that after resampling the points follow $ p_{\beta_1} $. This procedure of resampling is carried out at each intermediate level $ j $ - resampling the points distributed as $ p_{\beta_{j}} $ according to weights $ w_{j} $ such that after resampling, the points are distributed according to $ p_{\beta_{j+1}} $. As the points are sequentially resampled to follow each intermediate distribution, eventually they are resampled from $ p_{\beta_{m-1}} $ to follow $ p_{\beta_{m}} = p_1 $. +# +# The weights are calculated as +# +# \begin{equation} +# w_j = \frac{q \left( x, \beta_{j+1} \right)}{q \left( x, \beta_j \right)} +# \end{equation} +# +# The normalizing constant is calculated during the generation of samples, as +# +# \begin{equation} +# Z_1 = \prod_{j = 0}^{m-1} \left\{ \frac{\sum_{i = 1}^{N_j} w_j}{N_j} \right\} +# \end{equation} +# +# where $ N_j $ is the number of sample points generated from the intermediate distribution $ p_{\beta_j} $. +# %% + +# %% md +# Bayesian Inference +# +# In the Bayesian setting, $ p_0 $ is the prior, and $ q \left( x, \beta_j \right) = \mathcal{L}\left( data, x \right) ^{\beta_j} $ + +# %% + +from UQpy.run_model import RunModel, PythonModel +import numpy as np +from UQpy.distributions import Uniform, Normal, JointIndependent, MultivariateNormal +from UQpy.sampling import SequentialTemperingMCMC +import matplotlib.pyplot as plt +from scipy.stats import multivariate_normal, norm, uniform +from UQpy.sampling.mcmc import * + + +# %% md +# + +# %% + +def likelihood(x, b): + mu1 = np.array([1., 1.]) + mu2 = -0.8 * np.ones(2) + w1 = 0.5 + # Width of 0.1 in each dimension + sigma1 = np.diag([0.02, 0.05]) + sigma2 = np.diag([0.05, 0.02]) + + # Posterior is a mixture of two gaussians + like = np.exp(np.logaddexp(np.log(w1) + multivariate_normal.logpdf(x=x, mean=mu1, cov=sigma1), + np.log(1. - w1) + multivariate_normal.logpdf(x=x, mean=mu2, cov=sigma2))) + return like ** b + + +prior = JointIndependent(marginals=[Uniform(loc=-2.0, scale=4.0), Uniform(loc=-2.0, scale=4.0)]) + + +# %% md +# + +# %% + +# estimate evidence +def estimate_evidence_from_prior_samples(size): + samples = -2. + 4 * np.random.uniform(size=size * 2).reshape((size, 2)) + return np.mean(likelihood(samples, 1.0)) + + +def func_integration(x1, x2): + x = np.array([x1, x2]).reshape((1, 2)) + return likelihood(x, 1.0) * (1. / 4) ** 2 + + +def estimate_evidence_from_quadrature(): + from scipy.integrate import dblquad + ev = dblquad(func=func_integration, a=-2, b=2, gfun=lambda x: -2, hfun=lambda x: 2) + return ev + + +x = np.arange(-2, 2, 0.02) +y = np.arange(-2, 2, 0.02) +xx, yy = np.meshgrid(x, y) +z = likelihood(np.concatenate([xx.reshape((-1, 1)), yy.reshape((-1, 1))], axis=-1), 1.0) +h = plt.contourf(x, y, z.reshape(xx.shape)) +plt.title('Likelihood') +plt.axis('equal') +plt.show() + +# for nMC in [50000, 100000, 500000, 1000000]: +# print('Evidence = {}'.format(estimate_evidence_from_prior_samples(nMC))) +print('Evidence computed analytically = {}'.format(estimate_evidence_from_quadrature()[0])) + +# %% md +# + +# %% +sampler = MetropolisHastings(dimension=2, n_chains=20) +test = SequentialTemperingMCMC(pdf_intermediate=likelihood, + distribution_reference=prior, + save_intermediate_samples=True, + percentage_resampling=10, + sampler=sampler, + nsamples=4000) + +# %% md +# + +# %% + +print('Normalizing Constant = ' + str(test.evidence)) +print('Tempering Parameters = ' + str(test.tempering_parameters)) + +plt.figure() +plt.scatter(test.intermediate_samples[0][:, 0], test.intermediate_samples[0][:, 1]) +plt.title(r'$\beta = $' + str(test.tempering_parameters[0])) +plt.show() + +plt.figure() +plt.scatter(test.intermediate_samples[2][:, 0], test.intermediate_samples[2][:, 1]) +plt.title(r'$\beta = $' + str(test.tempering_parameters[2])) +plt.show() + +plt.figure() +plt.scatter(test.samples[:, 0], test.samples[:, 1]) +plt.title(r'$\beta = $' + str(test.tempering_parameters[-1])) +plt.show() + +# %% md +# # Reliability +# +# In the reliability context, $ p_0 $ is the pdf of the parameters, and +# +# \begin{equation} +# q \left( x, \beta_j \right) = I_{\beta_j} \left( x \right) = \frac{1}{1 + \exp{\left( \frac{G \left( x \right)}{\frac{1}{\beta_j} - 1} \right)}} +# \end{equation} +# +# where $ G \left( x \right) $ is the performance function, negative if the system fails, and $ I_{\beta_j} \left( x \right) $ are smoothed versions of the indicator function. + +# %% + +from scipy.stats import norm + + +def indic_sigmoid(y, beta): + return 1. / (1. + np.exp(y / (1. / beta - 1.))) + + +fig, ax = plt.subplots(figsize=(4, 3.5)) +ys = np.linspace(-5, 5, 100) +for i, s in enumerate(1. / np.array([1.01, 1.25, 2., 4., 70.])): + ax.plot(ys, indic_sigmoid(y=ys, beta=s), label=r'$\beta={:.2f}$'.format(s), color='blue', alpha=1. - i / 6) +ax.set_xlabel(r'$y=g(\theta)$', fontsize=13) +ax.set_ylabel(r'$q_{\beta}(\theta)=I_{\beta}(y)$', fontsize=13) +# ax.set_title(r'Smooth versions of the indicator function', fontsize=14) +ax.legend(fontsize=8.5) +plt.show() + +# %% md +# + +# %% + +beta = 2 # Specified Reliability Index +rho = 0.7 # Specified Correlation +dim = 2 # Dimension + +# Define the correlation matrix +C = np.ones((dim, dim)) * rho +np.fill_diagonal(C, 1) +print(C) + +# Print information related to the true probability of failure +e, v = np.linalg.eig(np.asarray(C)) +beff = np.sqrt(np.max(e)) * beta +print(beff) +from scipy.stats import norm + +pf_true = norm.cdf(-beta) +print('True pf={}'.format(pf_true)) + + +# %% md +# + +# %% + + +def estimate_Pf_0(samples, model_values): + mask = model_values <= 0 + return np.sum(mask) / len(mask) + + +model = RunModel(model=PythonModel(model_script='local_reliability_funcs.py', model_object_name="correlated_gaussian", + b_eff=beff, d=dim)) +# model = RunModel(model_script='TMCMC_test_reliability_fn.py', model_object_name="correlated_gaussian", ntasks=1, +# b_eff=beff, d=dim) +samples = MultivariateNormal(mean=np.zeros((2,)), cov=np.array([[1, 0.7], [0.7, 1]])).rvs(nsamples=20000) +model.run(samples=samples, append_samples=False) +model_values = np.array(model.qoi_list) + +print('Prob. failure (MC) = {}'.format(estimate_Pf_0(samples, model_values))) + +fig, ax = plt.subplots(figsize=(4, 3.5)) +mask = np.squeeze(model_values <= 0) +ax.scatter(samples[mask, 0], samples[mask, 1], color='red', label='fail', alpha=0.5, marker='d') +ax.scatter(samples[~mask, 0], samples[~mask, 1], color='blue', label='safe', alpha=0.5) +plt.axis('equal') +# plt.title('Failure domain for reliability problem', fontsize=14) +plt.xlabel(r'$\theta_{1}$', fontsize=13) +plt.ylabel(r'$\theta_{2}$', fontsize=13) +ax.legend(fontsize=13) +fig.tight_layout() +plt.show() + + +# %% md +# + +# %% + +def indic_sigmoid(y, b): + return 1.0 / (1.0 + np.exp((y * b) / (1.0 - b))) + + +def factor_param(x, b): + model.run(samples=x, append_samples=False) + G_values = np.array(model.qoi_list) + return np.squeeze(indic_sigmoid(G_values, b)) + + +prior = MultivariateNormal(mean=np.zeros((2,)), cov=C) + +sampler = MetropolisHastings(dimension=2, n_chains=20) +test = SequentialTemperingMCMC(pdf_intermediate=factor_param, + distribution_reference=prior, + save_intermediate_samples=True, + percentage_resampling=10, + random_state=960242069, + sampler=sampler, + nsamples=3000) + + +# %% md +# + +# %% + +print('Estimated Probability of Failure = ' + str(test.evidence)) +print('Tempering Parameters = ' + str(test.tempering_parameters)) + +plt.figure() +plt.scatter(test.intermediate_samples[0][:, 0], test.intermediate_samples[0][:, 1]) +plt.title(r'$\beta = $' + str(test.tempering_parameters[0])) +plt.show() + +plt.figure() +plt.scatter(test.intermediate_samples[2][:, 0], test.intermediate_samples[2][:, 1]) +plt.title(r'$\beta = $' + str(test.tempering_parameters[2])) +plt.show() + +plt.figure() +plt.scatter(test.samples[:, 0], test.samples[:, 1]) +plt.title(r'$\beta = $' + str(test.tempering_parameters[-1])) +plt.show() diff --git a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py index c63da088c..e944b0801 100644 --- a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py @@ -41,13 +41,13 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, random_state=None, tempering_parameters=None, n_tempering_parameters=None, - sampler: Union[MCMC, list[MCMC]] = None): + samplers: list[MCMC] = None): super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, args_pdf_intermediate=args_pdf_intermediate, distribution_reference=None, save_log_pdf=save_log_pdf, random_state=random_state) self.logger = logging.getLogger(__name__) - self.sampler = sampler + self.samplers = samplers self.distribution_reference = distribution_reference self.evaluate_log_reference = self._preprocess_reference(self.distribution_reference) @@ -74,13 +74,13 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, self.n_tempering_parameters = len(self.tempering_parameters) # default value - kwargs_mcmc = {} - if isinstance(self.sampler, MetropolisHastings) and self.sampler.proposal is None: - from UQpy.distributions import JointIndependent, Normal - kwargs_mcmc = {'proposal_is_symmetric': [True, ] * self.n_tempering_parameters, - 'proposal': [JointIndependent([Normal(scale=1. / np.sqrt(temper_param))] * - self.sampler.dimension) - for temper_param in self.tempering_parameters]} + for i, sampler in enumerate(self.samplers): + if isinstance(sampler, MetropolisHastings) and sampler.proposal is None: + from UQpy.distributions import JointIndependent, Normal + self.samplers[i] = sampler.__copy__(proposal_is_symmetric=True, + proposal=JointIndependent( + [Normal(scale=1. / np.sqrt(self.tempering_parameters[i]))] * + sampler.dimension)) # Initialize algorithm specific inputs: target pdfs self.thermodynamic_integration_results = None @@ -89,9 +89,9 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, for i, temper_param in enumerate(self.tempering_parameters): log_pdf_target = (lambda x, temper_param=temper_param: self.evaluate_log_reference( x) + self.evaluate_log_intermediate(x, temper_param)) - self.mcmc_samplers.append(sampler.__copy__(log_pdf_target=log_pdf_target, concatenate_chains=True, - save_log_pdf=save_log_pdf, random_state=self.random_state, - **dict([(key, val[i]) for key, val in kwargs_mcmc.items()]))) + self.mcmc_samplers.append(self.samplers[i].__copy__(log_pdf_target=log_pdf_target, concatenate_chains=True, + save_log_pdf=save_log_pdf, + random_state=self.random_state)) self.logger.info('\nUQpy: Initialization of ' + self.__class__.__name__ + ' algorithm complete.') diff --git a/tests/unit_tests/sampling/test_tempering.py b/tests/unit_tests/sampling/test_tempering.py index 72e3b0f09..680f9cdb7 100644 --- a/tests/unit_tests/sampling/test_tempering.py +++ b/tests/unit_tests/sampling/test_tempering.py @@ -29,26 +29,26 @@ def compute_potential(x, temper_param, log_intermediate_values): def test_parallel(): - sampler = MetropolisHastings(burn_length=10, jump=2, seed=list(seed), dimension=2) + samplers = [MetropolisHastings(burn_length=10, jump=2, seed=list(seed), dimension=2) for _ in range(len(betas))] mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, distribution_reference=prior_distribution, n_iterations_between_sweeps=4, tempering_parameters=betas, random_state=3456, - save_log_pdf=False, sampler=sampler) + save_log_pdf=False, samplers=samplers) mcmc.run(nsamples_per_chain=100) assert mcmc.samples.shape == (500, 2) def test_thermodynamic_integration(): - sampler = MetropolisHastings(burn_length=10, jump=2, seed=list(seed), dimension=2) + samplers = [MetropolisHastings(burn_length=10, jump=2, seed=list(seed), dimension=2) for _ in range(len(betas))] mcmc = ParallelTemperingMCMC(log_pdf_intermediate=log_intermediate, distribution_reference=prior_distribution, n_iterations_between_sweeps=4, tempering_parameters=betas, save_log_pdf=True, random_state=3456, - sampler=sampler) + samplers=samplers) mcmc.run(nsamples_per_chain=100) log_ev = mcmc.evaluate_normalization_constant(compute_potential=compute_potential, log_Z0=0.) assert np.round(log_ev, 4) == 0.203 From 8fae05274ecc266c03c595fec54ae4be4d40f674 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Fri, 2 Dec 2022 14:20:53 -0500 Subject: [PATCH 11/41] Moves TemperingMCMC files inside mcmc folder --- src/UQpy/sampling/__init__.py | 2 +- src/UQpy/sampling/mcmc/__init__.py | 2 ++ .../tempering_mcmc/ParallelTemperingMCMC.py | 21 +++++++------ .../tempering_mcmc/SequentialTemperingMCMC.py | 31 +++++++------------ .../sampling/mcmc/tempering_mcmc/__init__.py | 3 ++ .../baseclass}/TemperingMCMC.py | 2 ++ .../mcmc/tempering_mcmc/baseclass/__init__.py | 1 + src/UQpy/sampling/tempering_mcmc/__init__.py | 3 -- tests/unit_tests/sampling/test_tempering.py | 1 + .../sampling/test_true_stratified.py | 1 + 10 files changed, 34 insertions(+), 33 deletions(-) rename src/UQpy/sampling/{ => mcmc}/tempering_mcmc/ParallelTemperingMCMC.py (95%) rename src/UQpy/sampling/{ => mcmc}/tempering_mcmc/SequentialTemperingMCMC.py (93%) create mode 100644 src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py rename src/UQpy/sampling/{tempering_mcmc => mcmc/tempering_mcmc/baseclass}/TemperingMCMC.py (99%) create mode 100644 src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/__init__.py delete mode 100644 src/UQpy/sampling/tempering_mcmc/__init__.py diff --git a/src/UQpy/sampling/__init__.py b/src/UQpy/sampling/__init__.py index b40106e15..d4b2ea7c9 100644 --- a/src/UQpy/sampling/__init__.py +++ b/src/UQpy/sampling/__init__.py @@ -1,7 +1,7 @@ from UQpy.sampling.mcmc import * from UQpy.sampling.adaptive_kriging_functions import * from UQpy.sampling.stratified_sampling import * -from UQpy.sampling.tempering_mcmc import * +from UQpy.sampling.mcmc.tempering_mcmc import * from UQpy.sampling.AdaptiveKriging import AdaptiveKriging from UQpy.sampling.ImportanceSampling import ImportanceSampling diff --git a/src/UQpy/sampling/mcmc/__init__.py b/src/UQpy/sampling/mcmc/__init__.py index cbf9288ea..5a8eac1aa 100644 --- a/src/UQpy/sampling/mcmc/__init__.py +++ b/src/UQpy/sampling/mcmc/__init__.py @@ -3,4 +3,6 @@ from UQpy.sampling.mcmc.Stretch import Stretch from UQpy.sampling.mcmc.DRAM import DRAM from UQpy.sampling.mcmc.DREAM import DREAM + from UQpy.sampling.mcmc.baseclass.MCMC import MCMC +from UQpy.sampling.mcmc.tempering_mcmc import * diff --git a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py similarity index 95% rename from src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py rename to src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py index e944b0801..633d7cc90 100644 --- a/src/UQpy/sampling/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py @@ -3,7 +3,7 @@ from UQpy.sampling.mcmc import MetropolisHastings from UQpy.sampling.mcmc.baseclass.MCMC import * -from UQpy.sampling.tempering_mcmc.TemperingMCMC import TemperingMCMC +from UQpy.sampling.mcmc.tempering_mcmc.baseclass.TemperingMCMC import TemperingMCMC class ParallelTemperingMCMC(TemperingMCMC): @@ -33,14 +33,15 @@ class ParallelTemperingMCMC(TemperingMCMC): **Methods:** """ - + @beartype def __init__(self, n_iterations_between_sweeps: PositiveInteger, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), - distribution_reference=None, - save_log_pdf=False, nsamples=None, nsamples_per_chain=None, - random_state=None, - tempering_parameters=None, - n_tempering_parameters=None, + distribution_reference: Distribution = None, + save_log_pdf: bool = False, nsamples: PositiveInteger = None, + nsamples_per_chain: PositiveInteger = None, + random_state: RandomStateType = None, + tempering_parameters: list = None, + n_tempering_parameters: int = None, samplers: list[MCMC] = None): super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, @@ -99,7 +100,8 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, if (nsamples is not None) or (nsamples_per_chain is not None): self.run(nsamples=nsamples, nsamples_per_chain=nsamples_per_chain) - def run(self, nsamples=None, nsamples_per_chain=None): + @beartype + def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInteger = None): """ Run the MCMC algorithm. @@ -185,7 +187,8 @@ def run(self, nsamples=None, nsamples_per_chain=None): if self.save_log_pdf: self.log_pdf_values = self.mcmc_samplers[-1].log_pdf_values - def evaluate_normalization_constant(self, compute_potential, log_Z0=None, nsamples_from_p0=None): + @beartype + def evaluate_normalization_constant(self, compute_potential, log_Z0: float=None, nsamples_from_p0: int=None): """ Evaluate new log free energy as diff --git a/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py similarity index 93% rename from src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py rename to src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py index d8aa43416..aaa94d665 100644 --- a/src/UQpy/sampling/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py @@ -5,7 +5,7 @@ from UQpy.sampling.mcmc.MetropolisHastings import MetropolisHastings from UQpy.distributions.collection.MultivariateNormal import MultivariateNormal from UQpy.sampling.mcmc.baseclass.MCMC import * -from UQpy.sampling.tempering_mcmc.TemperingMCMC import TemperingMCMC +from UQpy.sampling.mcmc.tempering_mcmc.baseclass.TemperingMCMC import TemperingMCMC class SequentialTemperingMCMC(TemperingMCMC): @@ -36,27 +36,18 @@ class SequentialTemperingMCMC(TemperingMCMC): """ @beartype - # def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), - # distribution_reference=None, - # mcmc_class: MCMC = None, - # dimension=None, seed=None, - # nsamples: PositiveInteger = None, - # recalc_w=False, - # nburn_resample=0, save_intermediate_samples=False, nchains=1, - # percentage_resampling=100, random_state=None, - # proposal_is_symmetric=True): def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), - distribution_reference=None, + distribution_reference: Distribution = None, sampler: MCMC = None, - seed=None, + seed: list = None, nsamples: PositiveInteger = None, - recalculate_weights=False, + recalculate_weights: bool = False, save_intermediate_samples=False, - percentage_resampling=100, - random_state=None, - resampling_burn_length=0, - resampling_proposal=None, - resampling_proposal_is_symmetric=True): + percentage_resampling: int = 100, + random_state: RandomStateType = None, + resampling_burn_length: int = 0, + resampling_proposal: Distribution = None, + resampling_proposal_is_symmetric: bool = True): self.proposal = resampling_proposal self.proposal_is_symmetric = resampling_proposal_is_symmetric self.resampling_burn_length = resampling_burn_length @@ -119,7 +110,7 @@ def run(self, nsamples: PositiveInteger = None): weight_probabilities = np.zeros(nsamples) # Array storing plausibility weight probabilities expected_q0 = sum( np.exp(self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), 0.0)) - for i in range(nsamples))/nsamples + for i in range(nsamples)) / nsamples evidence_estimator = expected_q0 @@ -165,7 +156,7 @@ def run(self, nsamples: PositiveInteger = None): for j in range(self.__dimension): points_deviation[j, 0] = points[i, j] - (w_theta_sum[j] / w_sum) sigma_matrix += (weights[i] / w_sum) * np.dot(points_deviation, - points_deviation.T) # Normalized by w_sum as per Betz et al + points_deviation.T) # Normalized by w_sum as per Betz et al sigma_matrix = cov_scale ** 2 * sigma_matrix mcmc_log_pdf_target = self._target_generator(self.evaluate_log_intermediate, diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py b/src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py new file mode 100644 index 000000000..742833005 --- /dev/null +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py @@ -0,0 +1,3 @@ +from UQpy.sampling.mcmc.tempering_mcmc.ParallelTemperingMCMC import ParallelTemperingMCMC + +from UQpy.sampling.mcmc.tempering_mcmc.baseclass import * diff --git a/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py similarity index 99% rename from src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py rename to src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py index 2d7b10080..9b11ef036 100644 --- a/src/UQpy/sampling/tempering_mcmc/TemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py @@ -37,10 +37,12 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in if self.save_log_pdf: self.log_pdf_values = None + @abstractmethod def run(self, nsamples): """ Run the tempering MCMC algorithms to generate nsamples from the target posterior """ pass + @abstractmethod def evaluate_normalization_constant(self, **kwargs): """ Computes the normalization constant :math:`Z_{1}=\int{q_{1}(x) p_{0}(x)dx}` where p0 is the reference pdf and q1 is the intermediate density with :math:`\beta=1`, thus q1 p0 is the target pdf.""" diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/__init__.py b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/__init__.py new file mode 100644 index 000000000..589679839 --- /dev/null +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/__init__.py @@ -0,0 +1 @@ +from UQpy.sampling.mcmc.tempering_mcmc.baseclass.TemperingMCMC import TemperingMCMC diff --git a/src/UQpy/sampling/tempering_mcmc/__init__.py b/src/UQpy/sampling/tempering_mcmc/__init__.py deleted file mode 100644 index 2540f20eb..000000000 --- a/src/UQpy/sampling/tempering_mcmc/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from UQpy.sampling.tempering_mcmc.TemperingMCMC import TemperingMCMC -from UQpy.sampling.tempering_mcmc.SequentialTemperingMCMC import SequentialTemperingMCMC -from UQpy.sampling.tempering_mcmc.ParallelTemperingMCMC import ParallelTemperingMCMC diff --git a/tests/unit_tests/sampling/test_tempering.py b/tests/unit_tests/sampling/test_tempering.py index 680f9cdb7..30e3596c8 100644 --- a/tests/unit_tests/sampling/test_tempering.py +++ b/tests/unit_tests/sampling/test_tempering.py @@ -79,3 +79,4 @@ def test_sequential(): sampler=sampler, nsamples=100) assert np.round(test.evidence, 4) == 0.0656 + diff --git a/tests/unit_tests/sampling/test_true_stratified.py b/tests/unit_tests/sampling/test_true_stratified.py index 1114d566c..e5881a3eb 100644 --- a/tests/unit_tests/sampling/test_true_stratified.py +++ b/tests/unit_tests/sampling/test_true_stratified.py @@ -18,6 +18,7 @@ def test_rectangular_sts(): assert x.samples[9, 1] == 0.5495253722712197 + def test_delaunay_sts(): marginals = [Exponential(loc=1., scale=1.), Exponential(loc=1., scale=1.)] seeds = np.array([[0, 0], [0.4, 0.8], [1, 0], [1, 1]]) From 8917c83cfaf1872d474ee2a49edc5d84eb0a9e07 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Tue, 6 Dec 2022 11:33:40 -0500 Subject: [PATCH 12/41] Allows either a single MCMC sampler or list to be provided in ParallelTemperingMCMC.py --- .../mcmc/tempering_mcmc/ParallelTemperingMCMC.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py index 633d7cc90..0f1154faf 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py @@ -33,6 +33,7 @@ class ParallelTemperingMCMC(TemperingMCMC): **Methods:** """ + @beartype def __init__(self, n_iterations_between_sweeps: PositiveInteger, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), @@ -42,13 +43,16 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, random_state: RandomStateType = None, tempering_parameters: list = None, n_tempering_parameters: int = None, - samplers: list[MCMC] = None): + samplers: Union[MCMC, list[MCMC]] = None): super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, args_pdf_intermediate=args_pdf_intermediate, distribution_reference=None, save_log_pdf=save_log_pdf, random_state=random_state) self.logger = logging.getLogger(__name__) - self.samplers = samplers + if not isinstance(samplers, list): + self.samplers = [samplers.__copy__() for _ in range(len(tempering_parameters))] + else: + self.samplers = samplers self.distribution_reference = distribution_reference self.evaluate_log_reference = self._preprocess_reference(self.distribution_reference) @@ -188,7 +192,7 @@ def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInte self.log_pdf_values = self.mcmc_samplers[-1].log_pdf_values @beartype - def evaluate_normalization_constant(self, compute_potential, log_Z0: float=None, nsamples_from_p0: int=None): + def evaluate_normalization_constant(self, compute_potential, log_Z0: float = None, nsamples_from_p0: int = None): """ Evaluate new log free energy as From 02a53b6e7d30ff528f9090c0c0ab38d80aceb2a9 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Mon, 16 Jan 2023 14:52:16 -0500 Subject: [PATCH 13/41] Correction for FORM --- src/UQpy/reliability/taylor_series/FORM.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/UQpy/reliability/taylor_series/FORM.py b/src/UQpy/reliability/taylor_series/FORM.py index a3cd250fe..e100cbb16 100644 --- a/src/UQpy/reliability/taylor_series/FORM.py +++ b/src/UQpy/reliability/taylor_series/FORM.py @@ -185,7 +185,7 @@ def run(self, seed_x: Union[list, np.ndarray] = None, g_record.append(0.0) dg_u_record = np.zeros([self.n_iterations + 1, self.dimension]) - while not converged: + while not converged and k < self.n_iterations: self.logger.info("Number of iteration: %i", k) # FORM always starts from the standard normal space if k == 0: @@ -303,6 +303,7 @@ def run(self, seed_x: Union[list, np.ndarray] = None, else: k = k + 1 + elif (self.tol1 is None) and (self.tol2 is not None) and (self.tol3 is not None): error2 = np.linalg.norm(beta[k + 1] - beta[k]) error3 = np.linalg.norm(dg_u_record[k + 1, :] - dg_u_record[k, :]) @@ -312,10 +313,8 @@ def run(self, seed_x: Union[list, np.ndarray] = None, else: k = k + 1 - self.logger.error("Error: %s", error_record[-1]) + self.logger.info("Error: %s", error_record[-1]) - if converged is True or k > self.n_iterations: - break if k > self.n_iterations: self.logger.info("UQpy: Maximum number of iterations {0} was reached before convergence." From d7db12b28a476d9b16dd46c130d46e622d6c2a77 Mon Sep 17 00:00:00 2001 From: Michael Gardner Date: Fri, 20 Jan 2023 10:59:13 -0800 Subject: [PATCH 14/41] Added documentation and example for launching parallel job on cluster --- .../ClusterScript_Example/add_numbers.py | 25 ++++++ .../ClusterScript_Example/addition_run.py | 18 +++++ .../inputRealization.json | 4 + .../process_addition_output.py | 25 ++++++ .../ClusterScript_Example/run_script.sh | 38 +++++++++ docs/code/RunModel/cluster_script_example.py | 78 +++++++++++++++++++ docs/source/runmodel_doc.rst | 15 ++++ 7 files changed, 203 insertions(+) create mode 100644 docs/code/RunModel/ClusterScript_Example/add_numbers.py create mode 100644 docs/code/RunModel/ClusterScript_Example/addition_run.py create mode 100644 docs/code/RunModel/ClusterScript_Example/inputRealization.json create mode 100644 docs/code/RunModel/ClusterScript_Example/process_addition_output.py create mode 100755 docs/code/RunModel/ClusterScript_Example/run_script.sh create mode 100644 docs/code/RunModel/cluster_script_example.py diff --git a/docs/code/RunModel/ClusterScript_Example/add_numbers.py b/docs/code/RunModel/ClusterScript_Example/add_numbers.py new file mode 100644 index 000000000..89e104291 --- /dev/null +++ b/docs/code/RunModel/ClusterScript_Example/add_numbers.py @@ -0,0 +1,25 @@ +import sys +import os +import json +import numpy as np + +def addNumbers(): + inputPath = sys.argv[1] + outputPath = sys.argv[2] + + # Open JSON file + with open(inputPath, "r") as jsonFile: + data = json.load(jsonFile) + + # Read generated numbers + number1 = data["number1"] + number2 = data["number2"] + + randomAddition = number1 + number2 + + # Write addition to file + with open(outputPath, 'w') as outputFile: + outputFile.write('{}\n'.format(randomAddition)) + +if __name__ == '__main__': + addNumbers() diff --git a/docs/code/RunModel/ClusterScript_Example/addition_run.py b/docs/code/RunModel/ClusterScript_Example/addition_run.py new file mode 100644 index 000000000..3adfa62a0 --- /dev/null +++ b/docs/code/RunModel/ClusterScript_Example/addition_run.py @@ -0,0 +1,18 @@ +import os +import shutil +import fire + +def runAddition(index): + index = int(index) + + inputRealizationPath = os.path.join(os.getcwd(), 'run_' + str(index), 'InputFiles', 'inputRealization_' \ + + str(index) + ".json") + outputPath = os.path.join(os.getcwd(), 'OutputFiles') + + # This is where pre-processing commands would be executed prior to running the cluster script. + command1 = ("echo \"This is where pre-processing would be happening\"") + + os.system(command1) + +if __name__ == '__main__': + fire.Fire(runAddition) diff --git a/docs/code/RunModel/ClusterScript_Example/inputRealization.json b/docs/code/RunModel/ClusterScript_Example/inputRealization.json new file mode 100644 index 000000000..8de6601f6 --- /dev/null +++ b/docs/code/RunModel/ClusterScript_Example/inputRealization.json @@ -0,0 +1,4 @@ +{ + "number1" : , + "number2" : +} diff --git a/docs/code/RunModel/ClusterScript_Example/process_addition_output.py b/docs/code/RunModel/ClusterScript_Example/process_addition_output.py new file mode 100644 index 000000000..393c66fe3 --- /dev/null +++ b/docs/code/RunModel/ClusterScript_Example/process_addition_output.py @@ -0,0 +1,25 @@ +import numpy as np +from pathlib import Path + +class OutputProcessor: + + def __init__(self, index): + filePath = Path("./OutputFiles/qoiFile_" + str(index) + ".txt") + self.numberOfColumns = 0 + self.numberOfLines = 0 + addedNumbers = [] + + # Check if file exists + if filePath.is_file(): + # Now, open and read data + with open(filePath) as f: + for line in f: + currentLine = line.split() + + if len(currentLine) != 0: + addedNumbers.append(currentLine[:]) + + if len(addedNumbers) != 0: + self.qoi = np.vstack(addedNumbers) + else: + self.qoi = np.empty(shape=(0,0)) diff --git a/docs/code/RunModel/ClusterScript_Example/run_script.sh b/docs/code/RunModel/ClusterScript_Example/run_script.sh new file mode 100755 index 000000000..513af71f2 --- /dev/null +++ b/docs/code/RunModel/ClusterScript_Example/run_script.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# NOTE: The job configuration etc. would be in the batch script that launches +# your python script that uses UQpy. This script would then utilize those +# resources by using the appropriate commands here to launch parallel jobs. For +# example, TACC uses slurm and ibrun, so you would launch your python script in +# the slurm batch script and then use ibrun here to tile parallel runs. + +# This function is where you can define all the parts of a single +taskFunction(){ + coresPerProc=$1 + runNumber=$2 + + let offset=$coresPerProc*$runNumber # Sometimes, this might be necessary to pass as an argument to launch jobs. Not used here. + + cd run_$runNumber + # Here, we launch a parallel job. The example uses multiple cores to add numbers, + # which is somewhat pointless. This is just to illustrate the process for how tiled + # parallel jobs are launched and where MPI-capable applications would be initiated + mkdir -p ./OutputFiles + mpirun -n $coresPerProc python3 ../add_numbers.py ./InputFiles/inputRealization_$runNumber.json ./OutputFiles/qoiFile_$runNumber.txt + cd .. +} + +# This is the loop that launches taskFunction in parallel +coresPerProcess=$1 +numberOfJobs=$2 + +echo +echo "Starting parallel job launch" +for i in $(seq 0 $((numberOfJobs-1))) +do + # Launch task function and put into the background + taskFunction $coresPerProcess $i & +done + +wait # This wait call is necessary so that loop above completes before script returns +echo "Analyses done!" diff --git a/docs/code/RunModel/cluster_script_example.py b/docs/code/RunModel/cluster_script_example.py new file mode 100644 index 000000000..ac81bf6f4 --- /dev/null +++ b/docs/code/RunModel/cluster_script_example.py @@ -0,0 +1,78 @@ +""" + +Cluster Script Example for Third-party +====================================== +""" + +# %% md +# +# In this case, we're just running a simple addition of random numbers, but +# the process is exactly the same for more complicated workflows. The pre- +# and post-processing is done through `model_script` and `output_script` +# respectively, while the computationally intensive portion of the workflow +# is launched in `cluster_script. The example below provides a minimal framework +# from which more complex cases can be constructed. +# +# Import the necessary libraries + +# %% +from UQpy.sampling import LatinHypercubeSampling +from UQpy.run_model.RunModel import RunModel +from UQpy.run_model.model_execution.ThirdPartyModel import ThirdPartyModel +from UQpy.distributions import Uniform +import numpy as np +import time +import csv + +# %% md +# +# Define the distribution objects. + +# %% + +var_names=["var_1", "var_2"] +distributions = [Uniform(250.0, 40.0), Uniform(66.0, 24.0)] + +# %% md +# +# Draw the samples using Latin Hypercube Sampling. + +# %% + +x_lhs = LatinHypercubeSampling(distributions, nsamples=4) + +# %% md +# +# Run the model. + +# %% + +model = ThirdPartyModel(var_names=var_names, input_template='inputRealization.json', model_script='addition_run.py', + output_script='process_addition_output.py', output_object_name='OutputProcessor', + model_dir='AdditionRuns') + +t = time.time() +modelRunner = RunModel(model=model, samples=x_lhs.samples, ntasks=1, + cores_per_task=2, nodes=1, resume=False, + run_type='CLUSTER', cluster_script='./run_script.sh') + +t_total = time.time() - t +print("\nTotal time for all experiments:") +print(t_total, "\n") + +# %% md +# +# Print model results--this is just for illustration + +# %% +for index, experiment in enumerate(modelRunner.qoi_list, 0): + if len(experiment.qoi) != 0: + for item in experiment.qoi: + print("These are the random numbers for sample {}:".format(index)) + for sample in x_lhs.samples[index]: + print("{}\t".format(sample)) + + print("This is their sum:") + for result in item: + print("{}\t".format(result)) + print() diff --git a/docs/source/runmodel_doc.rst b/docs/source/runmodel_doc.rst index dd094dcb1..9c8ffaf72 100644 --- a/docs/source/runmodel_doc.rst +++ b/docs/source/runmodel_doc.rst @@ -106,6 +106,21 @@ at `https://www.open-mpi.org/faq/?category=building Date: Thu, 2 Feb 2023 09:25:53 -0800 Subject: [PATCH 15/41] first try at tempering / parallel tempering docstrings --- .../tempering_mcmc/ParallelTemperingMCMC.py | 97 ++++++++++--------- .../tempering_mcmc/baseclass/TemperingMCMC.py | 44 ++++++--- 2 files changed, 85 insertions(+), 56 deletions(-) diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py index 0f1154faf..789edb5eb 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py @@ -11,10 +11,16 @@ class ParallelTemperingMCMC(TemperingMCMC): Parallel-Tempering MCMC This algorithm runs the chains sampling from various tempered distributions in parallel. Periodically during the - run, the different temperatures swap members of their ensemble in a way that - preserves detailed balance.The chains closer to the reference chain (hot chains) can sample from regions that have - low probability under the target and thus allow a better exploration of the parameter space, while the cold chains - can better explore the regions of high likelihood. + run, the different temperatures swap members of their ensemble in a way that preserves detailed balance.The chains + closer to the reference chain (hot chains) can sample from regions that have low probability under the target and + thus allow a better exploration of the parameter space, while the cold chains can better explore the regions of high + likelihood. + + In parallel tempering, the normalizing constant :math:`Z_1` is evaluated via thermodynamic integration. Define + the potential function :math:`U_{\beta}(x)=\frac{\partial \log{q_{\beta}(x)}}{\partial \beta}`, then + :math:`\log{Z_{1}} = \log{Z_{0}} + \int_{0}^{1} E_{x~p_{beta}} \left[ U_{\beta}(x) \right] d\beta` + where the expectations are approximated via MC sampling using samples from the intermediate distributions (see + :method:`evaluate_normalization_constant`). **References** @@ -24,14 +30,18 @@ class ParallelTemperingMCMC(TemperingMCMC): **Inputs:** - Many inputs are similar to MCMC algorithms. Additional inputs are: - - * **niter_between_sweeps** - - * **mcmc_class** - - **Methods:** - + Many inputs are similar to MCMC algorithms (`n_samples`, `n_samples_per_chain`, 'random_state') + :param save_log_pdf: boolean, see :class:MCMC documentation. Importantly, this needs to be set to True if one wants + to evaluate the normalization constant via thermodynamic integration. + :param n_iterations_between_sweeps: number of iterations (sampling steps) between sweeps between chains + :param tempering_parameters: list of :math:`\beta` values so that + :math:`0 < \beta_1 < \beta_2 < \cdots < \beta_N \leq 1`. Either `tempering_parameters` or `n_tempering_parameters` + should be provided. + :param n_tempering_parameters: number of tempering levels N, the tempering parameters are selected to follow a + geometric suite :math:`\frac{1}{\sqrt{2}^(N-n)}` for n in 1:N. + :param samplers: :class:`MCMC` object or list of such objects: MCMC samplers used to sample the parallel chains. If + only one object is provided, the same MCMC sampler is used for all chains. Default to running a simple MH algorithm, + where the proposal covariance for a given chain is :math:`\frac{1}{\beta}`. """ @beartype @@ -63,9 +73,9 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, self.n_tempering_parameters = n_tempering_parameters if self.tempering_parameters is None: if self.n_tempering_parameters is None: - raise ValueError('UQpy: either input temper_param_list or n_temper_params should be provided.') + raise ValueError('UQpy: either input tempering_parameters or n_tempering_parameters should be provided.') elif not (isinstance(self.n_tempering_parameters, int) and self.n_tempering_parameters >= 2): - raise ValueError('UQpy: input n_temper_params should be a integer >= 2.') + raise ValueError('UQpy: input n_tempering_parameters should be a integer >= 2.') else: self.tempering_parameters = [1. / np.sqrt(2) ** i for i in range(self.n_tempering_parameters - 1, -1, -1)] @@ -74,7 +84,7 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, # or float(self.temperatures[0]) != 1. ): raise ValueError( - 'UQpy: temper_param_list should be a list of floats in [0, 1], starting at 0. and increasing to 1.') + 'UQpy: tempering_parameters should be a list of floats in [0, 1], starting at 0. and increasing to 1.') else: self.n_tempering_parameters = len(self.tempering_parameters) @@ -89,8 +99,10 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, # Initialize algorithm specific inputs: target pdfs self.thermodynamic_integration_results = None + """Results of the thermodynamic integration (normalization constant). """ self.mcmc_samplers = [] + """List of MCMC samplers, one per tempering level. """ for i, temper_param in enumerate(self.tempering_parameters): log_pdf_target = (lambda x, temper_param=temper_param: self.evaluate_log_reference( x) + self.evaluate_log_intermediate(x, temper_param)) @@ -110,18 +122,13 @@ def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInte Run the MCMC algorithm. This function samples from the MCMC chains and appends samples to existing ones (if any). This method leverages - the ``run_iterations`` method that is specific to each algorithm. - - **Inputs:** - - * **nsamples** (`int`): - Number of samples to generate. + the :method:`run_iterations` method specific to each of the samplers. - * **nsamples_per_chain** (`int`) - Number of samples to generate per chain. + :param nsamples: Number of samples to generate from the target (the same number of samples will be generated + for all intermediate distributions). - Either `nsamples` or `nsamples_per_chain` must be provided (not both). Not that if `nsamples` is not a multiple - of `nchains`, `nsamples` is set to the next largest integer that is a multiple of `nchains`. + :param nsamples_per_chain: Number of samples to generate per chain of the target MCMC sampler. Either `nsamples` + or `nsamples_per_chain` must be provided (not both). """ current_state, current_log_pdf = [], [] @@ -187,33 +194,33 @@ def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInte # Samples connect to posterior samples, i.e. the chain with beta=1. self.intermediate_samples = [sampler.samples for sampler in self.mcmc_samplers] + """List of samples for the intermediate tempering levels. """ self.samples = self.mcmc_samplers[-1].samples + """Samples from the target distribution. """ if self.save_log_pdf: self.log_pdf_values = self.mcmc_samplers[-1].log_pdf_values + """Log pdf values for samples from the target. """ @beartype def evaluate_normalization_constant(self, compute_potential, log_Z0: float = None, nsamples_from_p0: int = None): """ - Evaluate new log free energy as - - :math:`\log{Z_{1}} = \log{Z_{0}} + \int_{0}^{1} E_{x~p_{beta}} \left[ U_{\beta}(x) \right] d\beta` - - References (for the Bayesian case): - * https://emcee.readthedocs.io/en/v2.2.1/user/pt/ - - **Inputs:** - - * **compute_potential** (callable): - Function that takes three inputs (`x`, `log_factor_tempered_values`, `beta`) and computes the potential - :math:`U_{\beta}(x)`. `log_factor_tempered_values` are the values saved during sampling of - :math:`\log{p_{\beta}(x)}` at saved samples x. - - * **log_Z0** (`float`): - Value of :math:`\log{Z_{0}}` - - * **nsamples_from_p0** (`int`): - N samples from the reference distribution p0. Then :math:`\log{Z_{0}}` is evaluate via MC sampling - as :math:`\frac{1}{N} \sum{p_{\beta=0}(x)}`. Used only if input *log_Z0* is not provided. + Evaluate normalization constant :math:`Z_1` as: + :math:`\log{Z_{1}} \approx \log{Z_{\beta_{1}}} + \int_{\beta_{1}}^{\beta_{N}} \frac{1}{M} \sum_m U_{\beta}(x_m) + d\beta` + where :math:`x_m` are samples from the intermediate distributions. The integration is performed via a + trapezoidal rule. The function returns an approximation of :math:`Z_1`, and also saves intermediate results + (value of :math:`log_Z0`, list of tempering parameters used in integration, and values of the associated + expected potentials :math:`E_{x \sim p_{\beta} \left[ U_{\beta}(x) \right]}\frac{1}{M} \sum_m U_{\beta}(x_m)`) + + :param compute_potential: Function that takes three inputs: :code:`x` (sample points where to evaluate the + potential), :code:`log_factor_tempered_values` (values of :math:`\log{q_{\beta}(x)}` at these points), `beta` + (tempering parameter) and evaluates the potential + :math:`U_{\beta}(x)=\frac{\partial \log{q_{\beta}(x)}}{\partial \beta}`. + + :param log_Z0: Value of :math:`\log{Z_{0}}` (float), if unknwon, see `nsamples_from_p0` + :param nsamples_from_p0: number of samples :math:`M_0` from the reference distribution :math:`p_0` to be used + to evaluate :math:`\log{Z_{0}} \approx \frac{1}{M_0} \sum{p_{\beta=0}(x)}`. Used only if input `log_Z0` is not + provided. """ if not self.save_log_pdf: diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py index 9b11ef036..9fba5aafb 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py @@ -6,15 +6,37 @@ class TemperingMCMC(ABC): """ Parent class to parallel and sequential tempering MCMC algorithms. - To sample from the target distribution :math:`p(x)`, a sequence of intermediate densities - :math:`p(x, \beta) \propto q(x, \beta) p_{0}(x)` for values of the parameter :math:`\beta` between 0 and 1, - where :math:`p_{0}` is a reference distribution (often set as the prior in a Bayesian setting). - Setting :math:`\beta = 1` equates sampling from the target, while - :math:`\beta \rightarrow 0` samples from the reference distribution. - - **Inputs:** - - **Methods:** + These algorithms aim at sampling from a target distribution :math:`p_1(x)` of the form + :math:`p_1(x)=\frac{q_1(x)p_0(x)}{Z_1}` where the intermediate factor :math:`q_1(x)` and reference distribution + :math:`p_0(x)` can be evaluated. Additionally, these algorithms return an estimate of the normalization + constant :math:`Z_1=\int{q_{1}(x) p_{0}(x)dx}`. + + The algorithms sample from a sequence of intermediate densities + :math:`p_{\beta}(x) \propto q_{\beta}(x) p_{0}(x)` for values of the parameter :math:`\beta` between 0 and 1 + (:math:`\beta=\frac{1}{T}` where :math:`T` is sometimes called the temperature). + Setting :math:`\beta = 1` equates sampling from the target, while :math:`\beta \rightarrow 0` samples from the + reference distribution. + + Parallel tempering samples from all distributions simultaneously, and the tempering parameters :math:`\beta` must be + chosen in advance by the user. Sequential tempering samples from the various distributions sequentially, starting + from the reference distribution, and the tempering parameters are selected adaptively by the algorithm. + + Inputs that are common to both parallel and sequential tempering algorithms are: + :param pdf_intermediate: callable that computes the intermediate factor :math:`q_{\beta}(x)`. It should take at + least two inputs :code:`x` (ndarray, point(s) at which to evaluate the function), and :code:`temper_param` (float, + tempering parameter :math:`\beta`). Either `pdf_intermediate` or `log_pdf_intermediate` must be provided + (`log_pdf_intermediate` is preferred). Within the code, the `log_pdf_intermediate` is evaluated as: + :code:`log q_{\beta}(x) = log_pdf_intermediate(x, \beta, *args_pdf_intermediate)` + where `args_pdf_intermediate` are additional positional arguments that are provided to the class via its + `args_pdf_intermediate` input. + + :param log_pdf_intermediate: see `pdf_intermediate` + :param args_pdf_intermediate: see `pdf_intermediate` + + :param distribution_reference: reference pdf :math:`p_0` as a :class:`.Distribution` object + + :param save_log_pdf: see same input in :class:`MCMC` + :param random_state """ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), @@ -44,8 +66,8 @@ def run(self, nsamples): @abstractmethod def evaluate_normalization_constant(self, **kwargs): - """ Computes the normalization constant :math:`Z_{1}=\int{q_{1}(x) p_{0}(x)dx}` where p0 is the reference pdf - and q1 is the intermediate density with :math:`\beta=1`, thus q1 p0 is the target pdf.""" + """ Computes the normalization constant :math:`Z_{1}=\int{q_{1}(x) p_{0}(x)dx}` where :math:`p_0` is the + reference pdf and :math:`q_1` is the target factor.""" pass def _preprocess_reference(self, dist_, **kwargs): From 885bcfddbd90b1026f1030b5aafb35778d0d44a5 Mon Sep 17 00:00:00 2001 From: audreyolivier Date: Thu, 2 Feb 2023 16:37:16 -0800 Subject: [PATCH 16/41] tempering and parallel tempering documentation --- docs/source/bibliography.bib | 17 +++- docs/source/sampling/mcmc/index.rst | 1 + docs/source/sampling/mcmc/tempering.rst | 56 ++++++++++++ .../tempering_mcmc/ParallelTemperingMCMC.py | 91 ++++++++----------- .../tempering_mcmc/baseclass/TemperingMCMC.py | 55 ++++------- 5 files changed, 129 insertions(+), 91 deletions(-) create mode 100644 docs/source/sampling/mcmc/tempering.rst diff --git a/docs/source/bibliography.bib b/docs/source/bibliography.bib index f46fc199a..e01f8d20e 100644 --- a/docs/source/bibliography.bib +++ b/docs/source/bibliography.bib @@ -827,4 +827,19 @@ @article{saltelli_2002 url = {https://www.sciencedirect.com/science/article/pii/S0010465502002801}, author = {Andrea Saltelli}, keywords = {Sensitivity analysis, Sensitivity measures, Sensitivity indices, Importance measures}, -} \ No newline at end of file +} + +@article{PTMCMC1, +title = {Parallel Tempering: Theory, Applications, and New Perspectives}, +author = {David J. Earl, Michael W. Deem}, +year = {2005}, +doi = {https://doi.org/10.48550/arXiv.physics/0508111} +} + +@inproceedings{PTMCMC2, +title = {Using Thermodynamic Integration to Calculate the Posterior Probability in Bayesian Model Selection Problems}, +booktitle = {AIP Conference Proceedings 707, 59}, +year = {2004}, +doi = {https://doi.org/10.1063/1.1751356}, +author = {Paul M. Goggans and Ying Chi} +} diff --git a/docs/source/sampling/mcmc/index.rst b/docs/source/sampling/mcmc/index.rst index 7ba4f66ae..a54e0f392 100644 --- a/docs/source/sampling/mcmc/index.rst +++ b/docs/source/sampling/mcmc/index.rst @@ -75,6 +75,7 @@ List of MCMC algorithms DRAM DREAM Stretch + Tempering MCMC Adding New MCMC Algorithms diff --git a/docs/source/sampling/mcmc/tempering.rst b/docs/source/sampling/mcmc/tempering.rst new file mode 100644 index 000000000..57073a51e --- /dev/null +++ b/docs/source/sampling/mcmc/tempering.rst @@ -0,0 +1,56 @@ +Tempering MCMC +~~~~~~~~~~~~~~ + +Tempering MCMC algorithms aim at sampling from a target distribution :math:`p_1(x)` of the form +:math:`p_1(x)=\frac{q_1(x)p_0(x)}{Z_1}` where the factor :math:`q_1(x)` and reference distribution +:math:`p_0(x)` can be evaluated. Additionally, these algorithms return an estimate of the normalization +constant :math:`Z_1=\int{q_{1}(x) p_{0}(x)dx}`. + +The algorithms sample from a sequence of intermediate densities +:math:`p_{\beta}(x) \propto q_{\beta}(x) p_{0}(x)` for values of the parameter :math:`\beta` between 0 and 1 +(:math:`\beta=\frac{1}{T}` where :math:`T` is sometimes called the temperature, :math:`q_{\beta}(x)` is referred to as the intermediate factor associated with tempering parameter :math:`\beta`). +Setting :math:`\beta = 1` equates sampling from the target, while :math:`\beta \rightarrow 0` samples from the +reference distribution. + +Parallel tempering samples from all distributions simultaneously, and the tempering parameters :math:`0 < \beta_1 < \beta_2 < \cdots < \beta_{N} \leq 1` must be +chosen in advance by the user. Sequential tempering on the other hand samples from the various distributions sequentially, starting +from the reference distribution, and the tempering parameters are selected adaptively by the algorithm. + +The :class:`.TemperingMCMC` base class defines inputs that are common to parallel and sequential tempering: + +.. autoclass:: UQpy.sampling.mcmc.tempering_mcmc.TemperingMCMC + :members: + :exclude-members: run, evaluate_normalization_constant + +Parallel Tempering +^^^^^^^^^^^^^^^^^^^^ + +This algorithm (see e.g. :cite:`PTMCMC1` for theory about parallel tempering) runs the chains sampling from the various tempered distributions simultaneously. Periodically during the +run, the different temperatures swap members of their ensemble in a way that preserves detailed balance. The chains +closer to the reference chain (hot chains) can sample from regions that have low probability under the target and +thus allow a better exploration of the parameter space, while the cold chains can better explore regions of high +likelihood. + +In parallel tempering, the normalizing constant :math:`Z_1` is evaluated via thermodynamic integration (:cite:`PTMCMC2`). Defining +the potential function :math:`U_{\beta}(x)=\frac{\partial \log{q_{\beta}(x)}}{\partial \beta}`, then + +:math:`\ln{Z_1} = \log{Z_0} + \int_{0}^{1} E_{x \sim p_{\beta}} \left[ U_{\beta}(x) \right] d\beta` + +where the expectations are approximated via MC sampling using saved samples from the intermediate distributions. A trapezoidal rule is used for integration. + +The :class:`.ParallelTemperingMCMC` class is imported using the following command: + +>>> from UQpy.sampling.mcmc.tempering_mcmc.ParallelTemperingMCMC import ParallelTemperingMCMC + +.. autoclass:: UQpy.sampling.mcmc.tempering_mcmc.ParallelTemperingMCMC + :members: + +Sequential Tempering +^^^^^^^^^^^^^^^^^^^^ + +The :class:`.SequentialTemperingMCMC` class is imported using the following command: + +>>> from UQpy.sampling.mcmc.tempering_mcmc.SequentialTemperingMCMC import SequentialTemperingMCMC + +.. autoclass:: UQpy.sampling.mcmc.tempering_mcmc.SequentialTemperingMCMC + :members: diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py index 789edb5eb..e9349e596 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py @@ -7,42 +7,6 @@ class ParallelTemperingMCMC(TemperingMCMC): - """ - Parallel-Tempering MCMC - - This algorithm runs the chains sampling from various tempered distributions in parallel. Periodically during the - run, the different temperatures swap members of their ensemble in a way that preserves detailed balance.The chains - closer to the reference chain (hot chains) can sample from regions that have low probability under the target and - thus allow a better exploration of the parameter space, while the cold chains can better explore the regions of high - likelihood. - - In parallel tempering, the normalizing constant :math:`Z_1` is evaluated via thermodynamic integration. Define - the potential function :math:`U_{\beta}(x)=\frac{\partial \log{q_{\beta}(x)}}{\partial \beta}`, then - :math:`\log{Z_{1}} = \log{Z_{0}} + \int_{0}^{1} E_{x~p_{beta}} \left[ U_{\beta}(x) \right] d\beta` - where the expectations are approximated via MC sampling using samples from the intermediate distributions (see - :method:`evaluate_normalization_constant`). - - **References** - - 1. Parallel Tempering: Theory, Applications, and New Perspectives, Earl and Deem - 2. Adaptive Parallel Tempering MCMC - 3. emcee the MCMC Hammer python package - - **Inputs:** - - Many inputs are similar to MCMC algorithms (`n_samples`, `n_samples_per_chain`, 'random_state') - :param save_log_pdf: boolean, see :class:MCMC documentation. Importantly, this needs to be set to True if one wants - to evaluate the normalization constant via thermodynamic integration. - :param n_iterations_between_sweeps: number of iterations (sampling steps) between sweeps between chains - :param tempering_parameters: list of :math:`\beta` values so that - :math:`0 < \beta_1 < \beta_2 < \cdots < \beta_N \leq 1`. Either `tempering_parameters` or `n_tempering_parameters` - should be provided. - :param n_tempering_parameters: number of tempering levels N, the tempering parameters are selected to follow a - geometric suite :math:`\frac{1}{\sqrt{2}^(N-n)}` for n in 1:N. - :param samplers: :class:`MCMC` object or list of such objects: MCMC samplers used to sample the parallel chains. If - only one object is provided, the same MCMC sampler is used for all chains. Default to running a simple MH algorithm, - where the proposal covariance for a given chain is :math:`\frac{1}{\beta}`. - """ @beartype def __init__(self, n_iterations_between_sweeps: PositiveInteger, @@ -55,6 +19,27 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, n_tempering_parameters: int = None, samplers: Union[MCMC, list[MCMC]] = None): + """ + Class for Parallel-Tempering MCMC. + + :param save_log_pdf: boolean, see :class:`MCMC` documentation. Importantly, this needs to be set to True if + one wants to evaluate the normalization constant via thermodynamic integration. + + :param n_iterations_between_sweeps: number of iterations (sampling steps) between sweeps between chains. + + :param tempering_parameters: tempering parameters, as a list of N floats increasing from 0. to 1. Either + `tempering_parameters` or `n_tempering_parameters` should be provided + + :param n_tempering_parameters: number of tempering levels N, the tempering parameters are selected to follow + a geometric suite by default + + :param samplers: :class:`MCMC` object or list of such objects: MCMC samplers used to sample the parallel + chains. If only one object is provided, the same MCMC sampler is used for all chains. Default to running a + simple MH algorithm, where the proposal covariance for a given chain is inversely proportional to the + tempering parameter. + + """ + super().__init__(pdf_intermediate=pdf_intermediate, log_pdf_intermediate=log_pdf_intermediate, args_pdf_intermediate=args_pdf_intermediate, distribution_reference=None, save_log_pdf=save_log_pdf, random_state=random_state) @@ -121,14 +106,14 @@ def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInte """ Run the MCMC algorithm. - This function samples from the MCMC chains and appends samples to existing ones (if any). This method leverages - the :method:`run_iterations` method specific to each of the samplers. + This function samples from the MCMC chains and appends samples to existing ones (if any). This method + leverages the `run_iterations` method specific to each of the samplers. :param nsamples: Number of samples to generate from the target (the same number of samples will be generated for all intermediate distributions). - :param nsamples_per_chain: Number of samples to generate per chain of the target MCMC sampler. Either `nsamples` - or `nsamples_per_chain` must be provided (not both). + :param nsamples_per_chain: Number of samples per chain to generate from the target. Either + `nsamples` or `nsamples_per_chain` must be provided (not both) """ current_state, current_log_pdf = [], [] @@ -204,23 +189,19 @@ def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInte @beartype def evaluate_normalization_constant(self, compute_potential, log_Z0: float = None, nsamples_from_p0: int = None): """ - Evaluate normalization constant :math:`Z_1` as: - :math:`\log{Z_{1}} \approx \log{Z_{\beta_{1}}} + \int_{\beta_{1}}^{\beta_{N}} \frac{1}{M} \sum_m U_{\beta}(x_m) - d\beta` - where :math:`x_m` are samples from the intermediate distributions. The integration is performed via a - trapezoidal rule. The function returns an approximation of :math:`Z_1`, and also saves intermediate results - (value of :math:`log_Z0`, list of tempering parameters used in integration, and values of the associated - expected potentials :math:`E_{x \sim p_{\beta} \left[ U_{\beta}(x) \right]}\frac{1}{M} \sum_m U_{\beta}(x_m)`) + Evaluate normalization constant :math:`Z_1`. + + The function returns an approximation of :math:`Z_1`, and saves intermediate results + (value of :math:`\ln{Z_0}`, list of tempering parameters used in integration, and values of the associated + expected potentials. :param compute_potential: Function that takes three inputs: :code:`x` (sample points where to evaluate the - potential), :code:`log_factor_tempered_values` (values of :math:`\log{q_{\beta}(x)}` at these points), `beta` - (tempering parameter) and evaluates the potential - :math:`U_{\beta}(x)=\frac{\partial \log{q_{\beta}(x)}}{\partial \beta}`. - - :param log_Z0: Value of :math:`\log{Z_{0}}` (float), if unknwon, see `nsamples_from_p0` - :param nsamples_from_p0: number of samples :math:`M_0` from the reference distribution :math:`p_0` to be used - to evaluate :math:`\log{Z_{0}} \approx \frac{1}{M_0} \sum{p_{\beta=0}(x)}`. Used only if input `log_Z0` is not - provided. + potential), :code:`log_factor_tempered_values` (values of the log intermediate factors evaluated at points + :code:`x`), :code:`temper_param`(tempering parameter) and evaluates the potential: + + :param log_Z0: Value of :math:`\ln{Z_{0}}` (float), if unknwon, see `nsamples_from_p0`. + + :param nsamples_from_p0: number of samples from the reference distribution to sample to evaluate :math:`\ln{Z_{0}}`. Used only if input `log_Z0` is not provided. """ if not self.save_log_pdf: diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py index 9fba5aafb..ef08bf4df 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py @@ -3,44 +3,29 @@ class TemperingMCMC(ABC): - """ - Parent class to parallel and sequential tempering MCMC algorithms. - - These algorithms aim at sampling from a target distribution :math:`p_1(x)` of the form - :math:`p_1(x)=\frac{q_1(x)p_0(x)}{Z_1}` where the intermediate factor :math:`q_1(x)` and reference distribution - :math:`p_0(x)` can be evaluated. Additionally, these algorithms return an estimate of the normalization - constant :math:`Z_1=\int{q_{1}(x) p_{0}(x)dx}`. - - The algorithms sample from a sequence of intermediate densities - :math:`p_{\beta}(x) \propto q_{\beta}(x) p_{0}(x)` for values of the parameter :math:`\beta` between 0 and 1 - (:math:`\beta=\frac{1}{T}` where :math:`T` is sometimes called the temperature). - Setting :math:`\beta = 1` equates sampling from the target, while :math:`\beta \rightarrow 0` samples from the - reference distribution. - - Parallel tempering samples from all distributions simultaneously, and the tempering parameters :math:`\beta` must be - chosen in advance by the user. Sequential tempering samples from the various distributions sequentially, starting - from the reference distribution, and the tempering parameters are selected adaptively by the algorithm. - - Inputs that are common to both parallel and sequential tempering algorithms are: - :param pdf_intermediate: callable that computes the intermediate factor :math:`q_{\beta}(x)`. It should take at - least two inputs :code:`x` (ndarray, point(s) at which to evaluate the function), and :code:`temper_param` (float, - tempering parameter :math:`\beta`). Either `pdf_intermediate` or `log_pdf_intermediate` must be provided - (`log_pdf_intermediate` is preferred). Within the code, the `log_pdf_intermediate` is evaluated as: - :code:`log q_{\beta}(x) = log_pdf_intermediate(x, \beta, *args_pdf_intermediate)` - where `args_pdf_intermediate` are additional positional arguments that are provided to the class via its - `args_pdf_intermediate` input. - - :param log_pdf_intermediate: see `pdf_intermediate` - :param args_pdf_intermediate: see `pdf_intermediate` - - :param distribution_reference: reference pdf :math:`p_0` as a :class:`.Distribution` object - - :param save_log_pdf: see same input in :class:`MCMC` - :param random_state - """ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), distribution_reference=None, save_log_pdf=True, random_state=None): + """ + Parent class to parallel and sequential tempering MCMC algorithms. + + :param pdf_intermediate: callable that computes the intermediate factor. It should take at + least two inputs :code:`x` (ndarray, point(s) at which to evaluate the function), and :code:`temper_param` (float, + tempering parameter). Either `pdf_intermediate` or `log_pdf_intermediate` must be provided + (`log_pdf_intermediate` is preferred). Within the code, the `log_pdf_intermediate` is evaluated as: + + :code:`log_pdf_intermediate(x, temper_param, *args_pdf_intermediate)` + + where `args_pdf_intermediate` are additional positional arguments that are provided to the class via its + `args_pdf_intermediate` input + + :param log_pdf_intermediate: see `pdf_intermediate` + :param args_pdf_intermediate: see `pdf_intermediate` + + :param distribution_reference: reference pdf :math:`p_0` as a :class:`.Distribution` object + + :param save_log_pdf: see same input in :class:`MCMC` + """ self.logger = logging.getLogger(__name__) # Check a few inputs self.save_log_pdf = save_log_pdf From 21fb69509e460ead4bd54f4b593127757acb17cf Mon Sep 17 00:00:00 2001 From: audreyolivier Date: Thu, 2 Feb 2023 16:53:00 -0800 Subject: [PATCH 17/41] parallel tempering docstrings: changes to output docs --- .../mcmc/tempering_mcmc/ParallelTemperingMCMC.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py index e9349e596..ac800e7a8 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py @@ -82,9 +82,15 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, [Normal(scale=1. / np.sqrt(self.tempering_parameters[i]))] * sampler.dimension)) - # Initialize algorithm specific inputs: target pdfs + # Initialize algorithm outputs + self.intermediate_samples = None + """List of samples from the intermediate tempering levels. """ + self.samples = None + """ Samples from the target distribution (tempering parameter = 1). """ + self.log_pdf_values = None + """ Log pdf values of saved samples from the target. """ self.thermodynamic_integration_results = None - """Results of the thermodynamic integration (normalization constant). """ + """ Results of the thermodynamic integration (see method `evaluate_normalization_constant`). """ self.mcmc_samplers = [] """List of MCMC samplers, one per tempering level. """ @@ -179,12 +185,9 @@ def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInte # Samples connect to posterior samples, i.e. the chain with beta=1. self.intermediate_samples = [sampler.samples for sampler in self.mcmc_samplers] - """List of samples for the intermediate tempering levels. """ self.samples = self.mcmc_samplers[-1].samples - """Samples from the target distribution. """ if self.save_log_pdf: self.log_pdf_values = self.mcmc_samplers[-1].log_pdf_values - """Log pdf values for samples from the target. """ @beartype def evaluate_normalization_constant(self, compute_potential, log_Z0: float = None, nsamples_from_p0: int = None): From 0fcdb28349fac0b5f9a22f15fd4477137bef7c9d Mon Sep 17 00:00:00 2001 From: Michael Gardner Date: Fri, 3 Feb 2023 16:33:50 -0800 Subject: [PATCH 18/41] Update run_script.sh to tile jobs using bash loop and mpirun --- .../ClusterScript_Example/run_script.sh | 23 +++++++++++++++++-- docs/code/RunModel/cluster_script_example.py | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/code/RunModel/ClusterScript_Example/run_script.sh b/docs/code/RunModel/ClusterScript_Example/run_script.sh index 513af71f2..c46b62ae3 100755 --- a/docs/code/RunModel/ClusterScript_Example/run_script.sh +++ b/docs/code/RunModel/ClusterScript_Example/run_script.sh @@ -10,6 +10,7 @@ taskFunction(){ coresPerProc=$1 runNumber=$2 + host=$3 let offset=$coresPerProc*$runNumber # Sometimes, this might be necessary to pass as an argument to launch jobs. Not used here. @@ -18,20 +19,38 @@ taskFunction(){ # which is somewhat pointless. This is just to illustrate the process for how tiled # parallel jobs are launched and where MPI-capable applications would be initiated mkdir -p ./OutputFiles - mpirun -n $coresPerProc python3 ../add_numbers.py ./InputFiles/inputRealization_$runNumber.json ./OutputFiles/qoiFile_$runNumber.txt + mpirun -n $coresPerProc --host $host:$coresPerProc python3 ../add_numbers.py ./InputFiles/inputRealization_$runNumber.json ./OutputFiles/qoiFile_$runNumber.txt cd .. } +# Get list of hosts +echo $SLURM_NODELIST > hostfile + +# Split by comma +IFS="," read -ra HOSTS < hostfile + # This is the loop that launches taskFunction in parallel coresPerProcess=$1 numberOfJobs=$2 +# This number will vary depending on the number of cores per node. In this case, it is 32. +N=32 echo echo "Starting parallel job launch" + +declare -i index=0 + for i in $(seq 0 $((numberOfJobs-1))) do # Launch task function and put into the background - taskFunction $coresPerProcess $i & + echo "Launching job number ${i} on ${HOSTS[$index]}" + taskFunction $coresPerProcess $i ${HOSTS[$index]}& + + # Increment host when all nodes allocated on current node + if !((${i}%N)) && [ $i -ne 0 ] + then + index=${index}+1 + fi done wait # This wait call is necessary so that loop above completes before script returns diff --git a/docs/code/RunModel/cluster_script_example.py b/docs/code/RunModel/cluster_script_example.py index ac81bf6f4..8abecd6f1 100644 --- a/docs/code/RunModel/cluster_script_example.py +++ b/docs/code/RunModel/cluster_script_example.py @@ -39,7 +39,7 @@ # %% -x_lhs = LatinHypercubeSampling(distributions, nsamples=4) +x_lhs = LatinHypercubeSampling(distributions, nsamples=64) # %% md # From ed1b5bd142ce7753ab6394843caf60f69e0d4e93 Mon Sep 17 00:00:00 2001 From: PromitChakroborty Date: Thu, 9 Feb 2023 15:31:42 -0500 Subject: [PATCH 19/41] STMCMC bug fixes and unit tests --- .../tempering_mcmc/SequentialTemperingMCMC.py | 30 ++++----- tests/unit_tests/sampling/test_tempering.py | 61 ++++++++++++++++++- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py index aaa94d665..048925770 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py @@ -36,10 +36,9 @@ class SequentialTemperingMCMC(TemperingMCMC): """ @beartype - def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), + def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), seed=None, distribution_reference: Distribution = None, sampler: MCMC = None, - seed: list = None, nsamples: PositiveInteger = None, recalculate_weights: bool = False, save_intermediate_samples=False, @@ -187,7 +186,9 @@ def run(self, nsamples: PositiveInteger = None): weights[i] = np.exp( self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), current_tempering_parameter) - self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), previous_tempering_parameter)) - weight_probabilities[i] = weights[i] / w_sum + w_sum = np.sum(weights) + for j in range(nsamples): + weight_probabilities[j] = weights[j] / w_sum self.logger.info('Begin MCMC') mcmc_seed = self._mcmc_seed_generator(resampled_pts=points[0:self.n_resamples, :], @@ -305,21 +306,20 @@ def _preprocess_reference(self, dist_, seed_=None, nsamples=None, dimension=None * evaluate_log_pdf (callable): Callable that computes the log of the target density function (the prior) """ - if dist_ is not None and seed_ is not None: - raise ValueError('UQpy: both prior and seed values cannot be provided') - elif dist_ is not None: + if dist_ is not None: if not (isinstance(dist_, Distribution)): raise TypeError('UQpy: A UQpy.Distribution object must be provided.') - evaluate_log_pdf = (lambda x: dist_.log_pdf(x)) - seed_values = dist_.rvs(nsamples=nsamples, random_state=random_state) - elif seed_ is not None: - if seed_.shape[0] != nsamples or seed_.shape[1] != dimension: - raise TypeError('UQpy: the seed values should be a numpy array of size (nsamples, dimension)') - seed_values = seed_ - kernel = stats.gaussian_kde(seed_) - evaluate_log_pdf = (lambda x: kernel.logpdf(x)) + else: + evaluate_log_pdf = (lambda x: dist_.log_pdf(x)) + if seed_ is not None: + if seed_.shape[0] == nsamples and seed_.shape[1] == dimension: + seed_values = seed_ + else: + raise TypeError('UQpy: the seed values should be a numpy array of size (nsamples, dimension)') + else: + seed_values = dist_.rvs(nsamples=nsamples, random_state=random_state) else: - raise ValueError('UQpy: either prior distribution or seed values must be provided') + raise ValueError('UQpy: prior distribution must be provided') return evaluate_log_pdf, seed_values @staticmethod diff --git a/tests/unit_tests/sampling/test_tempering.py b/tests/unit_tests/sampling/test_tempering.py index 30e3596c8..910ed6e91 100644 --- a/tests/unit_tests/sampling/test_tempering.py +++ b/tests/unit_tests/sampling/test_tempering.py @@ -1,7 +1,8 @@ import numpy as np -from scipy.stats import multivariate_normal -from UQpy.distributions import Uniform, JointIndependent -from UQpy.sampling import MetropolisHastings, ParallelTemperingMCMC, SequentialTemperingMCMC +from scipy.stats import multivariate_normal, uniform +from UQpy.distributions import Uniform, JointIndependent, MultivariateNormal +from UQpy.sampling import MetropolisHastings, ParallelTemperingMCMC +from UQpy.sampling.mcmc.tempering_mcmc.SequentialTemperingMCMC import SequentialTemperingMCMC def log_rosenbrock(x): @@ -80,3 +81,57 @@ def test_sequential(): nsamples=100) assert np.round(test.evidence, 4) == 0.0656 + +def test_sequential_recalculated_weights(): + prior = JointIndependent(marginals=[Uniform(loc=-2.0, scale=4.0), Uniform(loc=-2.0, scale=4.0)]) + sampler = MetropolisHastings(dimension=2, n_chains=20) + test = SequentialTemperingMCMC(recalculate_weights=True, + pdf_intermediate=likelihood, + distribution_reference=prior, + percentage_resampling=10, + sampler=sampler, + nsamples=100, + random_state=960242069) + assert np.round(test.evidence, 4) == 0.0396 + + +def test_sequential_evaluate_normalization_constant_method_check(): + prior = JointIndependent(marginals=[Uniform(loc=-2.0, scale=4.0), Uniform(loc=-2.0, scale=4.0)]) + sampler = MetropolisHastings(dimension=2, n_chains=20) + test = SequentialTemperingMCMC(pdf_intermediate=likelihood, + distribution_reference=prior, + percentage_resampling=10, + sampler=sampler, + nsamples=100, + random_state=960242069) + assert np.round(test.evaluate_normalization_constant(), 4) == 0.0656 + + +def test_sequential_proposal_given(): + prior = JointIndependent(marginals=[Uniform(loc=-2.0, scale=4.0), Uniform(loc=-2.0, scale=4.0)]) + sampler = MetropolisHastings(dimension=2, n_chains=20) + test = SequentialTemperingMCMC(pdf_intermediate=likelihood, + resampling_proposal=MultivariateNormal(mean=np.zeros(2), cov=1.0), + distribution_reference=prior, + percentage_resampling=10, + sampler=sampler, + nsamples=100, + random_state=960242069) + assert np.round(test.evaluate_normalization_constant(), 4) == 0.0656 + + +def test_sequential_seed_given(): + prior = JointIndependent(marginals=[Uniform(loc=-2.0, scale=4.0), Uniform(loc=-2.0, scale=4.0)]) + sampler = MetropolisHastings(dimension=2, n_chains=20) + samps = (np.array([uniform.rvs(loc=-2.0, scale=4.0, size=100, random_state=960242069), + uniform.rvs(loc=-2.0, scale=4.0, size=100, random_state=24969420)]).T) + test = SequentialTemperingMCMC(pdf_intermediate=likelihood, + seed=samps, + distribution_reference=prior, + save_intermediate_samples=True, + percentage_resampling=10, + sampler=sampler, + nsamples=100, + random_state=960242069) + assert np.round(test.evaluate_normalization_constant(), 4) == 0.0579 + From 8921f9bccac6707bae98157bbbf1e6a5ea366e09 Mon Sep 17 00:00:00 2001 From: PromitChakroborty Date: Thu, 9 Feb 2023 18:17:18 -0500 Subject: [PATCH 20/41] STMCMC documentation and docstrings --- docs/source/bibliography.bib | 7 +++ docs/source/sampling/mcmc/tempering.rst | 13 ++++ .../tempering_mcmc/SequentialTemperingMCMC.py | 60 +++++++++++-------- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/docs/source/bibliography.bib b/docs/source/bibliography.bib index e01f8d20e..21290e885 100644 --- a/docs/source/bibliography.bib +++ b/docs/source/bibliography.bib @@ -843,3 +843,10 @@ @inproceedings{PTMCMC2 doi = {https://doi.org/10.1063/1.1751356}, author = {Paul M. Goggans and Ying Chi} } + +@article{STMCMC_ChingChen, +title = {Transitional Markov Chain Monte Carlo Method for Bayesian Model Updating, Model Class Selection, and Model Averaging}, +author = {Jianye Ching, Yi-Chu Chen}, +year = {2007}, +doi = {https://doi.org/10.1061/(ASCE)0733-9399(2007)133:7(816)} +} diff --git a/docs/source/sampling/mcmc/tempering.rst b/docs/source/sampling/mcmc/tempering.rst index 57073a51e..01f4384f8 100644 --- a/docs/source/sampling/mcmc/tempering.rst +++ b/docs/source/sampling/mcmc/tempering.rst @@ -48,6 +48,19 @@ The :class:`.ParallelTemperingMCMC` class is imported using the following comman Sequential Tempering ^^^^^^^^^^^^^^^^^^^^ +This algorithm (first introduced in :cite:`STMCMC_ChingChen`) samples from a series of intermediate targets that are each tempered versions of the final/true +target. In going from one intermediate distribution to the next, the existing samples are resampled according to +some weights (similar to importance sampling). To ensure that there aren't a large number of duplicates, the +resampling step is followed by a short (or even single-step) Metropolis Hastings run that disperses the samples while +remaining within the correct intermediate distribution. The final intermediate target is the required target distribution, +and the samples following this distribution are the required samples. + +The normalization constant :math:`Z_1` is estimated as the product of the normalized sums of the resampling weights for +each intermediate distribution, i.e. if :math:`w_{\beta_j}(x_{j_i})` is the resampling weight corresponding to tempering +parameter :math:`\beta_j`, calculated for the i-th sample for the intermediate distribution associated with :math:`\beta_j`, +then :math:`Z_1 = \prod_{j=1}^{N} \left\[ \sum_{i=i}^{\text{nsamples}} \right\]`. The Coefficient of Variance (COV) for this +estimator is also given in :cite:`STMCMC_ChingChen`. + The :class:`.SequentialTemperingMCMC` class is imported using the following command: >>> from UQpy.sampling.mcmc.tempering_mcmc.SequentialTemperingMCMC import SequentialTemperingMCMC diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py index 048925770..83279b683 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py @@ -9,31 +9,6 @@ class SequentialTemperingMCMC(TemperingMCMC): - """ - Sequential-Tempering MCMC - - This algorithm samples from a series of intermediate targets that are each tempered versions of the final/true - target. In going from one intermediate distribution to the next, the existing samples are resampled according to - some weights (similar to importance sampling). To ensure that there aren't a large number of duplicates, the - resampling step is followed by a short (or even single-step) MCMC run that disperses the samples while remaining - within the correct intermediate distribution. The final intermediate target is the required target distribution. - - **References** - - 1. Ching and Chen, "Transitional Markov Chain Monte Carlo Method for Bayesian Model Updating, - Model Class Selection, and Model Averaging", Journal of Engineering Mechanics/ASCE, 2007 - - **Inputs:** - - Many inputs are similar to MCMC algorithms. Additional inputs are: - - * **mcmc_class** - * **recalc_w** - * **nburn_resample** - * **nburn_mcmc** - - **Methods:** - """ @beartype def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_intermediate=(), seed=None, @@ -47,6 +22,32 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in resampling_burn_length: int = 0, resampling_proposal: Distribution = None, resampling_proposal_is_symmetric: bool = True): + + """ + Class for Sequential-Tempering MCMC + + :param sampler: :class:`MCMC` object: MCMC samplers used to draw the remaining samples for the intermediate + distribution after the resampling step. Default to running a simple MH algorithm, where the proposal covariance + is calculated as per the procedure given in Ching and Chen (2007) and Betz et. al. (2016). + + :param recalculate_weights: boolean: To be set to true if the resampling weights are to be recalculated after + each point is generated during the resampling step. This is done so that the resampling weights are in accordance + with the new sample generated after Metropolis Hastings is used for dispersion to ensure uniqueness of the samples. + + :param save_intermediate_samples: boolean: To be set to true to save the samples that are generated according to + the intermediate distributions. + + :param percentage_resampling: float: Indicates what percentage of samples for a given intermediate distribution + are to be generated through resampling from the set of samples generated for the previous intermediate distribution. + + :param resampling_burn_length: int: Burn-in length for the Metropolis Hastings dispersion step to ensure uniqueness. + + :param resampling_proposal: :class:`.Distribution` object. The proposal distribution for the Metropolis Hastings + dispersion step. + + :param resampling_proposal_is_symmetric: boolean: Indicates whether the provided resampling proposal is symmetric. + """ + self.proposal = resampling_proposal self.proposal_is_symmetric = resampling_proposal_is_symmetric self.resampling_burn_length = resampling_burn_length @@ -92,7 +93,16 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in @beartype def run(self, nsamples: PositiveInteger = None): + """ + Run the MCMC algorithm. + + This function samples from each intermediate distribution until samples from the target are generated. Samples + cannot be appended to existing samples in this method. It leverages the `run_iterations` method specific to the sampler. + :param nsamples: Number of samples to generate from the target (the same number of samples will be generated + for all intermediate distributions). + + """ self.logger.info('TMCMC Start') if self.samples is not None: From 6e2f2b7786beed5bdbde03fb3cdc34ee32de1113 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Mon, 20 Feb 2023 14:05:58 -0500 Subject: [PATCH 21/41] Finalizes TMCMC docs --- .../sampling/tempering/parallel_tempering.py | 62 ++++++++++++------- .../tempering/sequential_tempering.py | 48 +++++++------- docs/source/conf.py | 2 + docs/source/sampling/mcmc/tempering.rst | 13 +++- .../tempering_mcmc/ParallelTemperingMCMC.py | 30 ++++----- .../tempering_mcmc/SequentialTemperingMCMC.py | 33 +++++----- .../sampling/mcmc/tempering_mcmc/__init__.py | 1 + .../tempering_mcmc/baseclass/TemperingMCMC.py | 15 ++--- 8 files changed, 110 insertions(+), 94 deletions(-) diff --git a/docs/code/sampling/tempering/parallel_tempering.py b/docs/code/sampling/tempering/parallel_tempering.py index 2a6eee1ad..14c5ed317 100644 --- a/docs/code/sampling/tempering/parallel_tempering.py +++ b/docs/code/sampling/tempering/parallel_tempering.py @@ -2,42 +2,59 @@ Parallel Tempering for Bayesian Inference and Reliability analyses ==================================================================== + """ # %% md +# # The general framework: one wants to sample from a distribution of the form # -# $$ p_{1}(x) = \frac{q_{1}(x) p_{0}(x)}{Z_{1}} $$ +# .. math:: p_{1}(x)=\dfrac{q_1(x)p_{0}(x)}{Z_{1}} # -# where $q_{1}(x)$ and $p_{0}(x)$ can be evaluated; and potentially estimate the constant $Z_{1}=\int{q_{1}(x) p_{0}(x)dx}$. Parallel tempering introduces a sequence of intermediate distributions: +# where :math:`q_{1}(x)` and :math:`p_{0}(x)` can be evaluated; and potentially estimate the constant +# :math:`Z_{1}=\int{q_{1}(x) p_{0}(x)dx}`. Parallel tempering introduces a sequence of intermediate distributions: # -# $$ p_{\beta}(x) \propto q(x, \beta) p_{0}(x) $$ +# .. math:: p_{\beta}(x) \propto q(x, \beta) p_{0}(x) # -# for values of $\beta$ in [0, 1] (note: $\beta$ is $1/T$ where $T$ is often referred as the temperature). Setting $\beta=1$ equates sampling from the target, while $\beta \rightarrow 0$ samples from the reference distribution $p_{0}$. Periodically during the run, the different temperatures swap members of their ensemble in a way that preserves detailed balance. The chains closer to the reference chain (hot chains) can sample from regions that have low probability under the target and thus allow a better exploration of the parameter space, while the cold chains can better explore the regions of high likelihood. +# for values of :math:`\beta` in [0, 1] (note: :math:`\beta` is :math:`1/T` where :math:`T` is often referred as the +# temperature). Setting :math:`\beta=1` equates sampling from the target, while :math:`\beta \rightarrow 0` samples from +# the reference distribution :math:`p_{0}`. Periodically during the run, the different temperatures swap members of +# their ensemble in a way that preserves detailed balance. The chains closer to the reference chain (hot chains) can +# sample from regions that have low probability under the target and thus allow a better exploration of the parameter +# space, while the cold chains can better explore the regions of high likelihood. # -# The normalizing constant $Z_{1}$ is estimated via thermodynamic integration: +# The normalizing constant :math:`Z_{1}` is estimated via thermodynamic integration: # -# $$ \ln{Z_{\beta=1}} = \ln{Z_{\beta=0}} + \int_{0}^{1} E_{p_{\beta}} \left[ \frac{\partial \ln{q_{\beta}(x)}}{\partial \beta} \right] d\beta = \ln{Z_{\beta=0}} + \int_{0}^{1} E_{p_{\beta}} \left[ U_{\beta}(x) \right] d\beta$$ +# .. math:: \ln{Z_{\beta=1}} = \ln{Z_{\beta=0}} + \int_{0}^{1} E_{p_{\beta}} \left[ \frac{\partial \ln{q_{\beta}(x)}}{\partial \beta} \right] d\beta = \ln{Z_{\beta=0}} + \int_{0}^{1} E_{p_{\beta}} \left[ U_{\beta}(x) \right] d\beta # -# where $\ln{Z_{\beta=0}}=\int{q_{\beta=0}(x) p_{0}(x)dx}$ can be determined by simple MC sampling since $q_{\beta=0}(x)$ is close to the reference distribution $p_{0}$. The function $U_{\beta}(x)=\frac{\partial \ln{q_{\beta}(x)}}{\partial \beta}$ is called the potential, and can be evaluated using posterior samples from $p_{\beta}(x)$. +# where :math:`\ln{Z_{\beta=0}}=\int{q_{\beta=0}(x) p_{0}(x)dx}` can be determined by simple MC sampling since +# :math:`q_{\beta=0}(x)` is close to the reference distribution :math:`p_{0}`. The function +# :math:`U_{\beta}(x)=\frac{\partial \ln{q_{\beta}(x)}}{\partial \beta}` is called the potential, and can be evaluated +# using posterior samples from :math:`p_{\beta}(x)`. # # In the code, the user must define: -# - a function to evaluate the reference distribution $p_{0}(x)$, -# - a function to evaluate the intermediate factor $q(x, \beta)$ (function that takes in two inputs: x and $\beta$), -# - if evaluation of $Z_{1}$ is of interest, a function that evaluates the potential $U_{\beta}(x)$, from evaluations of $\ln{(x, \beta)}$ which are saved during the MCMC run for the various chains (different $\beta$ values). +# - a function to evaluate the reference distribution :math:`p_{0}(x)`, +# - a function to evaluate the intermediate factor :math:`q(x, \beta)` (function that takes in two inputs: x and +# :math:`\beta`), +# - if evaluation of :math:`Z_{1}` is of interest, a function that evaluates the potential :math:`U_{\beta}(x)`, from +# evaluations of :math:`\ln{(x, \beta)}` which are saved during the MCMC run for the various chains (different +# :math:`\beta` values). # # Bayesian inference # -# In the Bayesian setting, $p_{0}$ is the prior and, given a likelihood $L(data; x)$: +# In the Bayesian setting, :math:`p_{0}` is the prior and, given a likelihood :math:`L(data; x)`: # -# $$ q_{T}(x) = L(data; x) ^{\beta} $$ +# .. math:: q_{T}(x) = L(data; x) ^{\beta} # # Then for the model evidence: # -# $$ U_{\beta}(x) = \ln{L(data; x)} $$ -# +# .. math:: U_{\beta}(x) = \ln{L(data; x)} + + # %% + + import numpy as np import matplotlib.pyplot as plt @@ -72,7 +89,7 @@ def log_target(x): # %% md -# %% +# # estimate evidence def estimate_evidence_from_prior_samples(size): @@ -103,7 +120,7 @@ def estimate_evidence_from_quadrature(): print('Evidence computed analytically = {}'.format(estimate_evidence_from_quadrature()[0])) # %% md -# %% +# from UQpy.sampling.mcmc import MetropolisHastings @@ -208,15 +225,16 @@ def compute_potential(x, temper_param, log_intermediate_values): print('Estimate of evidence by thermodynamic integration = {:.4f}'.format(ev)) # %% md -# ## Reliability -# -# In a reliability context, $p_{0}$ is the pdf of the parameters and we have: +# Reliability +# ------------ +# In a reliability context, :math:`p_{0}` is the pdf of the parameters and we have: # -# $$ q_{\beta}(x) = I_{\beta}(x) = \frac{1}{1 + \exp{ \left( \frac{G(x)}{1/\beta-1}\right)}} $$ +# .. math:: q_{\beta}(x) = I_{\beta}(x) = \frac{1}{1 + \exp{ \left( \frac{G(x)}{1/\beta-1}\right)}} # -# where $G(x)$ is the performance function, negative if the system fails, and $I_{\beta}(x)$ are smoothed versions of the indicator function. Then to compute the probability of failure, the potential can be computed as: +# where :math:`G(x)` is the performance function, negative if the system fails, and :math:`I_{\beta}(x)` are smoothed +# versions of the indicator function. Then to compute the probability of failure, the potential can be computed as: # -# $$ U_{\beta}(x) = \frac{- \frac{G(x)}{(1-\beta)^2}}{1 + \exp{ \left( -\frac{G(x)}{1/\beta-1} \right) }} = - \frac{1 - I_{\beta}(x)}{\beta (1 - \beta)} \ln{ \left[ \frac{1 - I_{\beta}(x)}{I_{\beta}(x)} \right] }$$ +# .. math:: U_{\beta}(x) = \frac{- \frac{G(x)}{(1-\beta)^2}}{1 + \exp{ \left( -\frac{G(x)}{1/\beta-1} \right) }} = - \frac{1 - I_{\beta}(x)}{\beta (1 - \beta)} \ln{ \left[ \frac{1 - I_{\beta}(x)}{I_{\beta}(x)} \right] } # %% diff --git a/docs/code/sampling/tempering/sequential_tempering.py b/docs/code/sampling/tempering/sequential_tempering.py index 8385aa5cb..e166fe1f1 100644 --- a/docs/code/sampling/tempering/sequential_tempering.py +++ b/docs/code/sampling/tempering/sequential_tempering.py @@ -7,39 +7,42 @@ # %% md # The general framework: one wants to sample from a distribution of the form # -# \begin{equation} -# p_1 \left( x \right) = \frac{q_1 \left( x \right)p_0 \left( x \right)}{Z_1} -# \end{equation} +# .. math:: p_1 \left( x \right) = \frac{q_1 \left( x \right)p_0 \left( x \right)}{Z_1} # -# where $ q_1 \left( x \right) $ and $ p_0 \left( x \right) $ can be evaluated; and potentially estimate the constant $ Z_1 = \int q_1 \left( x \right)p_0 \left( x \right) dx $. +# +# where :math:`q_1 \left( x \right)` and :math:`p_0 \left( x \right)` can be evaluated; and potentially estimate the +# constant :math:`Z_1 = \int q_1 \left( x \right)p_0 \left( x \right) dx`. # # Sequential tempering introduces a sequence of intermediate distributions: # -# \begin{equation} -# p_{\beta_j} \left( x \right) \propto q \left( x, \beta_j \right)p_0 \left( x \right) -# \end{equation} +# .. math:: p_{\beta_j} \left( x \right) \propto q \left( x, \beta_j \right)p_0 \left( x \right) # -# for values of $ \beta_j $ in $ [0, 1] $. The algorithm starts with $ \beta_0 = 0 $, which samples from the reference distribution $ p_0 $, and ends for some $ j = m $ such that $ \beta_m = 1 $, sampling from the target. First, a set of sample points is generated from $ p_0 = p_{\beta_0} $, and then these are resampled according to some weights $ w_0 $ such that after resampling the points follow $ p_{\beta_1} $. This procedure of resampling is carried out at each intermediate level $ j $ - resampling the points distributed as $ p_{\beta_{j}} $ according to weights $ w_{j} $ such that after resampling, the points are distributed according to $ p_{\beta_{j+1}} $. As the points are sequentially resampled to follow each intermediate distribution, eventually they are resampled from $ p_{\beta_{m-1}} $ to follow $ p_{\beta_{m}} = p_1 $. +# for values of :math:`\beta_j` in :math:`[0, 1]`. The algorithm starts with :math:`\beta_0 = 0`, which samples +# from the reference distribution :math:`p_0`, and ends for some :math:`j = m` such that :math:`\beta_m = 1`, sampling +# from the target. First, a set of sample points is generated from :math:`p_0 = p_{\beta_0}`, and then these are +# resampled according to some weights :math:`w_0` such that after resampling the points follow :math:`p_{\beta_1}`. +# This procedure of resampling is carried out at each intermediate level :math:`j` - resampling the points distributed +# as :math:`p_{\beta_{j}}` according to weights :math:`w_{j}` such that after resampling, the points are distributed +# according to :math:`p_{\beta_{j+1}}`. As the points are sequentially resampled to follow each intermediate +# distribution, eventually they are resampled from :math:`p_{\beta_{m-1}}` to follow :math:`p_{\beta_{m}} = p_1`. # # The weights are calculated as # -# \begin{equation} -# w_j = \frac{q \left( x, \beta_{j+1} \right)}{q \left( x, \beta_j \right)} -# \end{equation} +# .. math:: w_j = \frac{q \left( x, \beta_{j+1} \right)}{q \left( x, \beta_j \right)} # # The normalizing constant is calculated during the generation of samples, as # -# \begin{equation} -# Z_1 = \prod_{j = 0}^{m-1} \left\{ \frac{\sum_{i = 1}^{N_j} w_j}{N_j} \right\} -# \end{equation} +# .. math:: Z_1 = \prod_{j = 0}^{m-1} \left\{ \frac{\sum_{i = 1}^{N_j} w_j}{N_j} \right\} # -# where $ N_j $ is the number of sample points generated from the intermediate distribution $ p_{\beta_j} $. +# where :math:`N_j` is the number of sample points generated from the intermediate distribution :math:`p_{\beta_j}`. + # %% # %% md # Bayesian Inference +# ------------------- # -# In the Bayesian setting, $ p_0 $ is the prior, and $ q \left( x, \beta_j \right) = \mathcal{L}\left( data, x \right) ^{\beta_j} $ +# In the Bayesian setting, :math:`p_0` is the prior, and :math:`q \left( x, \beta_j \right) = \mathcal{L}\left( data, x \right) ^{\beta_j}` # %% @@ -145,15 +148,14 @@ def estimate_evidence_from_quadrature(): plt.show() # %% md -# # Reliability +# Reliability +# ------------------- # -# In the reliability context, $ p_0 $ is the pdf of the parameters, and +# In the reliability context, :math:`p_0` is the pdf of the parameters, and # -# \begin{equation} -# q \left( x, \beta_j \right) = I_{\beta_j} \left( x \right) = \frac{1}{1 + \exp{\left( \frac{G \left( x \right)}{\frac{1}{\beta_j} - 1} \right)}} -# \end{equation} +# .. math:: q \left( x, \beta_j \right) = I_{\beta_j} \left( x \right) = \frac{1}{1 + \exp{\left( \frac{G \left( x \right)}{\frac{1}{\beta_j} - 1} \right)}} # -# where $ G \left( x \right) $ is the performance function, negative if the system fails, and $ I_{\beta_j} \left( x \right) $ are smoothed versions of the indicator function. +# where :math:`G \left( x \right)` is the performance function, negative if the system fails, and :math:`I_{\beta_j} \left( x \right)` are smoothed versions of the indicator function. # %% @@ -211,8 +213,6 @@ def estimate_Pf_0(samples, model_values): model = RunModel(model=PythonModel(model_script='local_reliability_funcs.py', model_object_name="correlated_gaussian", b_eff=beff, d=dim)) -# model = RunModel(model_script='TMCMC_test_reliability_fn.py', model_object_name="correlated_gaussian", ntasks=1, -# b_eff=beff, d=dim) samples = MultivariateNormal(mean=np.zeros((2,)), cov=np.array([[1, 0.7], [0.7, 1]])).rvs(nsamples=20000) model.run(samples=samples, append_samples=False) model_values = np.array(model.qoi_list) diff --git a/docs/source/conf.py b/docs/source/conf.py index 3c7c10316..a1f6ce634 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -82,6 +82,7 @@ "../code/sampling/monte_carlo", "../code/sampling/latin_hypercube", "../code/sampling/mcmc", + "../code/sampling/tempering", "../code/sampling/simplex", "../code/sampling/true_stratified_sampling", "../code/sampling/refined_stratified_sampling", @@ -121,6 +122,7 @@ "auto_examples/sampling/monte_carlo", "auto_examples/sampling/latin_hypercube", "auto_examples/sampling/mcmc", + "auto_examples/sampling/tempering", "auto_examples/sampling/simplex", "auto_examples/sampling/true_stratified_sampling", "auto_examples/sampling/refined_stratified_sampling", diff --git a/docs/source/sampling/mcmc/tempering.rst b/docs/source/sampling/mcmc/tempering.rst index 01f4384f8..d81751a06 100644 --- a/docs/source/sampling/mcmc/tempering.rst +++ b/docs/source/sampling/mcmc/tempering.rst @@ -42,8 +42,8 @@ The :class:`.ParallelTemperingMCMC` class is imported using the following comman >>> from UQpy.sampling.mcmc.tempering_mcmc.ParallelTemperingMCMC import ParallelTemperingMCMC -.. autoclass:: UQpy.sampling.mcmc.tempering_mcmc.ParallelTemperingMCMC - :members: +.. autoclass:: UQpy.sampling.mcmc.ParallelTemperingMCMC + :members: run, evaluate_normalization_constant Sequential Tempering ^^^^^^^^^^^^^^^^^^^^ @@ -58,7 +58,7 @@ and the samples following this distribution are the required samples. The normalization constant :math:`Z_1` is estimated as the product of the normalized sums of the resampling weights for each intermediate distribution, i.e. if :math:`w_{\beta_j}(x_{j_i})` is the resampling weight corresponding to tempering parameter :math:`\beta_j`, calculated for the i-th sample for the intermediate distribution associated with :math:`\beta_j`, -then :math:`Z_1 = \prod_{j=1}^{N} \left\[ \sum_{i=i}^{\text{nsamples}} \right\]`. The Coefficient of Variance (COV) for this +then :math:`Z_1 = \prod_{j=1}^{N} \ [ \sum_{i=i}^{\text{nsamples}} \ ]`. The Coefficient of Variance (COV) for this estimator is also given in :cite:`STMCMC_ChingChen`. The :class:`.SequentialTemperingMCMC` class is imported using the following command: @@ -67,3 +67,10 @@ The :class:`.SequentialTemperingMCMC` class is imported using the following comm .. autoclass:: UQpy.sampling.mcmc.tempering_mcmc.SequentialTemperingMCMC :members: + +Examples +~~~~~~~~~~~~~~~~~~ + +.. toctree:: + + Tempering Examples <../../auto_examples/sampling/tempering/index> \ No newline at end of file diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py index ac800e7a8..9cd6346f6 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/ParallelTemperingMCMC.py @@ -23,20 +23,16 @@ def __init__(self, n_iterations_between_sweeps: PositiveInteger, Class for Parallel-Tempering MCMC. :param save_log_pdf: boolean, see :class:`MCMC` documentation. Importantly, this needs to be set to True if - one wants to evaluate the normalization constant via thermodynamic integration. - + one wants to evaluate the normalization constant via thermodynamic integration. :param n_iterations_between_sweeps: number of iterations (sampling steps) between sweeps between chains. - :param tempering_parameters: tempering parameters, as a list of N floats increasing from 0. to 1. Either - `tempering_parameters` or `n_tempering_parameters` should be provided - + `tempering_parameters` or `n_tempering_parameters` should be provided :param n_tempering_parameters: number of tempering levels N, the tempering parameters are selected to follow - a geometric suite by default - + a geometric suite by default :param samplers: :class:`MCMC` object or list of such objects: MCMC samplers used to sample the parallel - chains. If only one object is provided, the same MCMC sampler is used for all chains. Default to running a - simple MH algorithm, where the proposal covariance for a given chain is inversely proportional to the - tempering parameter. + chains. If only one object is provided, the same MCMC sampler is used for all chains. Default to running a + simple MH algorithm, where the proposal covariance for a given chain is inversely proportional to the + tempering parameter. """ @@ -116,10 +112,9 @@ def run(self, nsamples: PositiveInteger = None, nsamples_per_chain: PositiveInte leverages the `run_iterations` method specific to each of the samplers. :param nsamples: Number of samples to generate from the target (the same number of samples will be generated - for all intermediate distributions). - + for all intermediate distributions). :param nsamples_per_chain: Number of samples per chain to generate from the target. Either - `nsamples` or `nsamples_per_chain` must be provided (not both) + `nsamples` or `nsamples_per_chain` must be provided (not both) """ current_state, current_log_pdf = [], [] @@ -199,12 +194,11 @@ def evaluate_normalization_constant(self, compute_potential, log_Z0: float = Non expected potentials. :param compute_potential: Function that takes three inputs: :code:`x` (sample points where to evaluate the - potential), :code:`log_factor_tempered_values` (values of the log intermediate factors evaluated at points - :code:`x`), :code:`temper_param`(tempering parameter) and evaluates the potential: - + potential), :code:`log_factor_tempered_values` (values of the log intermediate factors evaluated at points + :code:`x`), :code:`temper_param` (tempering parameter) and evaluates the potential: :param log_Z0: Value of :math:`\ln{Z_{0}}` (float), if unknwon, see `nsamples_from_p0`. - - :param nsamples_from_p0: number of samples from the reference distribution to sample to evaluate :math:`\ln{Z_{0}}`. Used only if input `log_Z0` is not provided. + :param nsamples_from_p0: number of samples from the reference distribution to sample to evaluate + :math:`\ln{Z_{0}}`. Used only if input `log_Z0` is not provided. """ if not self.save_log_pdf: diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py index 83279b683..954c972a5 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py @@ -27,25 +27,23 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in Class for Sequential-Tempering MCMC :param sampler: :class:`MCMC` object: MCMC samplers used to draw the remaining samples for the intermediate - distribution after the resampling step. Default to running a simple MH algorithm, where the proposal covariance - is calculated as per the procedure given in Ching and Chen (2007) and Betz et. al. (2016). - + distribution after the resampling step. Default to running a simple MH algorithm, where the proposal covariance + is calculated as per the procedure given in Ching and Chen (2007) and Betz et. al. (2016). :param recalculate_weights: boolean: To be set to true if the resampling weights are to be recalculated after - each point is generated during the resampling step. This is done so that the resampling weights are in accordance - with the new sample generated after Metropolis Hastings is used for dispersion to ensure uniqueness of the samples. - + each point is generated during the resampling step. This is done so that the resampling weights are in + accordance with the new sample generated after Metropolis Hastings is used for dispersion to ensure uniqueness + of the samples. :param save_intermediate_samples: boolean: To be set to true to save the samples that are generated according to - the intermediate distributions. - + the intermediate distributions. :param percentage_resampling: float: Indicates what percentage of samples for a given intermediate distribution - are to be generated through resampling from the set of samples generated for the previous intermediate distribution. - - :param resampling_burn_length: int: Burn-in length for the Metropolis Hastings dispersion step to ensure uniqueness. - + are to be generated through resampling from the set of samples generated for the previous intermediate + distribution. + :param resampling_burn_length: int: Burn-in length for the Metropolis Hastings dispersion step to ensure + uniqueness. :param resampling_proposal: :class:`.Distribution` object. The proposal distribution for the Metropolis Hastings - dispersion step. - - :param resampling_proposal_is_symmetric: boolean: Indicates whether the provided resampling proposal is symmetric. + dispersion step. + :param resampling_proposal_is_symmetric: boolean: Indicates whether the provided resampling proposal is + symmetric. """ self.proposal = resampling_proposal @@ -97,10 +95,11 @@ def run(self, nsamples: PositiveInteger = None): Run the MCMC algorithm. This function samples from each intermediate distribution until samples from the target are generated. Samples - cannot be appended to existing samples in this method. It leverages the `run_iterations` method specific to the sampler. + cannot be appended to existing samples in this method. It leverages the `run_iterations` method specific to the + sampler. :param nsamples: Number of samples to generate from the target (the same number of samples will be generated - for all intermediate distributions). + for all intermediate distributions). """ self.logger.info('TMCMC Start') diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py b/src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py index 742833005..50f45aa2f 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/__init__.py @@ -1,3 +1,4 @@ from UQpy.sampling.mcmc.tempering_mcmc.ParallelTemperingMCMC import ParallelTemperingMCMC +from UQpy.sampling.mcmc.tempering_mcmc.SequentialTemperingMCMC import SequentialTemperingMCMC from UQpy.sampling.mcmc.tempering_mcmc.baseclass import * diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py index ef08bf4df..00d0921ba 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/baseclass/TemperingMCMC.py @@ -10,20 +10,15 @@ def __init__(self, pdf_intermediate=None, log_pdf_intermediate=None, args_pdf_in Parent class to parallel and sequential tempering MCMC algorithms. :param pdf_intermediate: callable that computes the intermediate factor. It should take at - least two inputs :code:`x` (ndarray, point(s) at which to evaluate the function), and :code:`temper_param` (float, - tempering parameter). Either `pdf_intermediate` or `log_pdf_intermediate` must be provided - (`log_pdf_intermediate` is preferred). Within the code, the `log_pdf_intermediate` is evaluated as: - + least two inputs :code:`x` (ndarray, point(s) at which to evaluate the function), and :code:`temper_param` (float, + tempering parameter). Eit her `pdf_intermediate` or `log_pdf_intermediate` must be provided + (`log_pdf_intermediate` is preferred). Within the code, the `log_pdf_intermediate` is evaluated as: :code:`log_pdf_intermediate(x, temper_param, *args_pdf_intermediate)` - - where `args_pdf_intermediate` are additional positional arguments that are provided to the class via its - `args_pdf_intermediate` input - + where `args_pdf_intermediate` are additional positional arguments that are provided to the class via its + `args_pdf_intermediate` input :param log_pdf_intermediate: see `pdf_intermediate` :param args_pdf_intermediate: see `pdf_intermediate` - :param distribution_reference: reference pdf :math:`p_0` as a :class:`.Distribution` object - :param save_log_pdf: see same input in :class:`MCMC` """ self.logger = logging.getLogger(__name__) From c8db82ec649205b4fc528cbe459bd79b3fc69f1e Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Wed, 1 Mar 2023 20:08:51 -0500 Subject: [PATCH 22/41] Minor formatting changes in cluster execution example --- .../RunModel/ClusterScript_Example/add_numbers.py | 6 ++++-- .../ClusterScript_Example/addition_run.py | 8 +++++--- .../process_addition_output.py | 15 ++++++++------- docs/code/RunModel/cluster_script_example.py | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/code/RunModel/ClusterScript_Example/add_numbers.py b/docs/code/RunModel/ClusterScript_Example/add_numbers.py index 89e104291..3d3606ee5 100644 --- a/docs/code/RunModel/ClusterScript_Example/add_numbers.py +++ b/docs/code/RunModel/ClusterScript_Example/add_numbers.py @@ -3,6 +3,7 @@ import json import numpy as np + def addNumbers(): inputPath = sys.argv[1] outputPath = sys.argv[2] @@ -16,10 +17,11 @@ def addNumbers(): number2 = data["number2"] randomAddition = number1 + number2 - + # Write addition to file with open(outputPath, 'w') as outputFile: - outputFile.write('{}\n'.format(randomAddition)) + outputFile.write('{}\n'.format(randomAddition)) + if __name__ == '__main__': addNumbers() diff --git a/docs/code/RunModel/ClusterScript_Example/addition_run.py b/docs/code/RunModel/ClusterScript_Example/addition_run.py index 3adfa62a0..af558e08e 100644 --- a/docs/code/RunModel/ClusterScript_Example/addition_run.py +++ b/docs/code/RunModel/ClusterScript_Example/addition_run.py @@ -2,17 +2,19 @@ import shutil import fire + def runAddition(index): index = int(index) inputRealizationPath = os.path.join(os.getcwd(), 'run_' + str(index), 'InputFiles', 'inputRealization_' \ + str(index) + ".json") outputPath = os.path.join(os.getcwd(), 'OutputFiles') - + # This is where pre-processing commands would be executed prior to running the cluster script. command1 = ("echo \"This is where pre-processing would be happening\"") - - os.system(command1) + + os.system(command1) + if __name__ == '__main__': fire.Fire(runAddition) diff --git a/docs/code/RunModel/ClusterScript_Example/process_addition_output.py b/docs/code/RunModel/ClusterScript_Example/process_addition_output.py index 393c66fe3..a7980bfec 100644 --- a/docs/code/RunModel/ClusterScript_Example/process_addition_output.py +++ b/docs/code/RunModel/ClusterScript_Example/process_addition_output.py @@ -1,14 +1,15 @@ import numpy as np from pathlib import Path + class OutputProcessor: - - def __init__(self, index): + + def __init__(self, index): filePath = Path("./OutputFiles/qoiFile_" + str(index) + ".txt") self.numberOfColumns = 0 self.numberOfLines = 0 addedNumbers = [] - + # Check if file exists if filePath.is_file(): # Now, open and read data @@ -18,8 +19,8 @@ def __init__(self, index): if len(currentLine) != 0: addedNumbers.append(currentLine[:]) - - if len(addedNumbers) != 0: - self.qoi = np.vstack(addedNumbers) + + if not addedNumbers: + self.qoi = np.empty(shape=(0, 0)) else: - self.qoi = np.empty(shape=(0,0)) + self.qoi = np.vstack(addedNumbers) diff --git a/docs/code/RunModel/cluster_script_example.py b/docs/code/RunModel/cluster_script_example.py index 8abecd6f1..93e511cbb 100644 --- a/docs/code/RunModel/cluster_script_example.py +++ b/docs/code/RunModel/cluster_script_example.py @@ -10,7 +10,7 @@ # the process is exactly the same for more complicated workflows. The pre- # and post-processing is done through `model_script` and `output_script` # respectively, while the computationally intensive portion of the workflow -# is launched in `cluster_script. The example below provides a minimal framework +# is launched in `cluster_script`. The example below provides a minimal framework # from which more complex cases can be constructed. # # Import the necessary libraries From accae159a44929ec111791c4194d2cda823b1d86 Mon Sep 17 00:00:00 2001 From: PromitChakroborty Date: Fri, 10 Mar 2023 13:51:20 -0500 Subject: [PATCH 23/41] STMCMC test error debug push --- .../mcmc/tempering_mcmc/SequentialTemperingMCMC.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py index 83279b683..982e78793 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py @@ -190,12 +190,12 @@ def run(self, nsamples: PositiveInteger = None): proposal_is_symmetric=self.proposal_is_symmetric) # Setting the generated sample in the array - points[i] = x.samples + points[lead_index] = x.samples if self.recalculate_weights: - weights[i] = np.exp( - self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), current_tempering_parameter) - - self.evaluate_log_intermediate(points[i, :].reshape((1, -1)), previous_tempering_parameter)) + weights[lead_index] = np.exp( + self.evaluate_log_intermediate(points[lead_index, :].reshape((1, -1)), current_tempering_parameter) + - self.evaluate_log_intermediate(points[lead_index, :].reshape((1, -1)), previous_tempering_parameter)) w_sum = np.sum(weights) for j in range(nsamples): weight_probabilities[j] = weights[j] / w_sum From e5ee158821f80105787304b96ca6f47471c92610 Mon Sep 17 00:00:00 2001 From: PromitChakroborty Date: Fri, 10 Mar 2023 15:37:56 -0500 Subject: [PATCH 24/41] STMCMC test error debug push 2 --- .../mcmc/tempering_mcmc/SequentialTemperingMCMC.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py index 74934ce66..bed012745 100644 --- a/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py +++ b/src/UQpy/sampling/mcmc/tempering_mcmc/SequentialTemperingMCMC.py @@ -131,6 +131,9 @@ def run(self, nsamples: PositiveInteger = None): # Looping over all adaptively decided tempering levels while current_tempering_parameter < 1: + # Copy the state of the points array + points_copy = np.copy(points) + # Adaptively set the tempering exponent for the current level previous_tempering_parameter = current_tempering_parameter current_tempering_parameter = self._find_temper_param(previous_tempering_parameter, points, @@ -176,7 +179,7 @@ def run(self, nsamples: PositiveInteger = None): # Resampling from previous tempering level lead_index = int(self.random_state.choice(pts_index, p=weight_probabilities)) - lead = points[lead_index] + lead = points_copy[lead_index] # Defining the default proposal if self.proposal_given_flag is False: @@ -189,7 +192,8 @@ def run(self, nsamples: PositiveInteger = None): proposal_is_symmetric=self.proposal_is_symmetric) # Setting the generated sample in the array - points[lead_index] = x.samples + points[i] = x.samples + points_copy[lead_index] = x.samples if self.recalculate_weights: weights[lead_index] = np.exp( From dec5d7220dccd6a219652518c9d4dd11d241f438 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Fri, 10 Mar 2023 15:40:28 -0500 Subject: [PATCH 25/41] Updates test values @promitchakroborty --- tests/unit_tests/sampling/test_tempering.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit_tests/sampling/test_tempering.py b/tests/unit_tests/sampling/test_tempering.py index 910ed6e91..c264ff3d1 100644 --- a/tests/unit_tests/sampling/test_tempering.py +++ b/tests/unit_tests/sampling/test_tempering.py @@ -79,7 +79,7 @@ def test_sequential(): random_state=960242069, sampler=sampler, nsamples=100) - assert np.round(test.evidence, 4) == 0.0656 + assert np.round(test.evidence, 4) == 0.0437 def test_sequential_recalculated_weights(): @@ -92,7 +92,7 @@ def test_sequential_recalculated_weights(): sampler=sampler, nsamples=100, random_state=960242069) - assert np.round(test.evidence, 4) == 0.0396 + assert np.round(test.evidence, 4) == 0.082 def test_sequential_evaluate_normalization_constant_method_check(): @@ -104,7 +104,7 @@ def test_sequential_evaluate_normalization_constant_method_check(): sampler=sampler, nsamples=100, random_state=960242069) - assert np.round(test.evaluate_normalization_constant(), 4) == 0.0656 + assert np.round(test.evaluate_normalization_constant(), 4) == 0.0437 def test_sequential_proposal_given(): @@ -117,7 +117,7 @@ def test_sequential_proposal_given(): sampler=sampler, nsamples=100, random_state=960242069) - assert np.round(test.evaluate_normalization_constant(), 4) == 0.0656 + assert np.round(test.evaluate_normalization_constant(), 4) == 0.0437 def test_sequential_seed_given(): @@ -133,5 +133,5 @@ def test_sequential_seed_given(): sampler=sampler, nsamples=100, random_state=960242069) - assert np.round(test.evaluate_normalization_constant(), 4) == 0.0579 + assert np.round(test.evaluate_normalization_constant(), 4) == 0.0489 From bdba54a6302b4208d2a596d0b033b23988dafa70 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Fri, 17 Mar 2023 11:45:41 -0400 Subject: [PATCH 26/41] Updates numpy version from 1.21.4 to 1.24.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d11b73a6c..b5c8bb68b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy == 1.21.4 +numpy == 1.24.2 scipy == 1.8.0 matplotlib == 3.5.2 scikit-learn == 1.0.2 From ee6540a852541c9fced90b2098be8541da51415d Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Fri, 17 Mar 2023 13:13:31 -0400 Subject: [PATCH 27/41] Updates numpy version from 1.24.2 to 1.22.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b5c8bb68b..8331e2b10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy == 1.24.2 +numpy == 1.22.0 scipy == 1.8.0 matplotlib == 3.5.2 scikit-learn == 1.0.2 From d8f2de03978cd5d2825ad19187bcd29692dd9edd Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Tue, 21 Mar 2023 15:49:29 -0400 Subject: [PATCH 28/41] Adds missing file copy in ClusterExecution.py --- src/UQpy/run_model/model_execution/ClusterExecution.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/UQpy/run_model/model_execution/ClusterExecution.py b/src/UQpy/run_model/model_execution/ClusterExecution.py index b094e2841..894988fbf 100644 --- a/src/UQpy/run_model/model_execution/ClusterExecution.py +++ b/src/UQpy/run_model/model_execution/ClusterExecution.py @@ -31,6 +31,8 @@ # Loop over the number of samples and create input files in a folder in current directory for i in range(len(samples)): + work_dir = os.path.join(model.model_dir, "run_" + str(i)) + model._copy_files(work_dir=work_dir) new_text = model._find_and_replace_var_names_with_values(samples[i]) folder_to_write = 'run_' + str(i+n_existing_simulations) + '/InputFiles' # Write the new text to the input file From fd26576517e0178f137f26d0752c5dffe7fd0549 Mon Sep 17 00:00:00 2001 From: Lukas Novak <95358264+NovakLBUT@users.noreply.github.com> Date: Tue, 18 Apr 2023 16:57:12 +0200 Subject: [PATCH 29/41] New functions for PCE basis --- .../polynomials/baseclass/Polynomials.py | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py b/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py index 2a3154cb3..085eb970a 100644 --- a/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py +++ b/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py @@ -4,7 +4,7 @@ import numpy as np import scipy.integrate as integrate from beartype import beartype - +from scipy import stats as stats from UQpy.distributions.baseclass import Distribution import warnings @@ -25,6 +25,64 @@ def __init__(self, distributions: Union[Distribution, list[Distribution]], degre """ self.distributions = distributions self.degree = degree + 1 + + @staticmethod + def standardize_sample(x,joint_distribution): + """ + Static method: Standardize data based on the joint probability distribution. + + :param x: Input data generated from a joint probability distribution. + :param joint_distribution: joint probability distribution from :py:mod:`UQpy` distribution object + :return: Standardized data. + """ + + + s=np.zeros(x.shape) + inputs_number = len(x[0,:]) + if inputs_number == 1: + marginals = [joint_distribution] + else: + marginals = joint_distribution.marginals + + for i in range (inputs_number): + if type(marginals[i]) == Normal: + s[:,i]=Polynomials.standardize_normal(x[:,i],mean=marginals[i].parameters['loc'],std=marginals[i].parameters['scale']) + + if type(marginals[i]) == Uniform: + s[:,i]=Polynomials.standardize_uniform(x[:,i], marginals[i]) + + else: + raise TypeError("standarize_sample is defined only for Uniform and Gaussian marginal distributions") + return s + + @staticmethod + def standardize_pdf(x,joint_distribution): + """ + Static method: PDF of standardized distributions associated to Hermite or Legendre polynomials. + + :param x: Input data generated from a joint probability distribution + :param joint_distribution: joint probability distribution from :py:mod:`UQpy` distribution object + :return: Value of standardized PDF calculated for x + """ + + inputs_number = len(x[0,:]) + pdf_val=1 + s=Polynomials.standardize_sample(x,joint_distribution) + + if inputs_number == 1: + marginals = [joint_distribution] + else: + marginals = joint_distribution.marginals + + for i in range (inputs_number): + if type(marginals[i]) == Normal: + pdf_val = pdf_val * (stats.norm.pdf(s[:,i])) + if type(marginals[i]) == Uniform: + pdf_val = pdf_val * (stats.uniform.pdf(s[:,i], loc=-1, scale=2)) + + else: + raise TypeError("standardize_pdf is defined only for Uniform and Gaussian marginal distributions") + return pdf_val @staticmethod def standardize_normal(tensor: np.ndarray, mean: float, std: float): @@ -111,4 +169,4 @@ def scale(self): def evaluate(self, x: np.ndarray): pass - distribution_to_polynomial = { } \ No newline at end of file + distribution_to_polynomial = { } From b4502edaddad0e3946fb4d8de80fb60c0ed634e9 Mon Sep 17 00:00:00 2001 From: Lukas Novak <95358264+NovakLBUT@users.noreply.github.com> Date: Tue, 18 Apr 2023 16:58:57 +0200 Subject: [PATCH 30/41] Active learning for PCE Class --- src/UQpy/sampling/ThetaCriterionPCE.py | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/UQpy/sampling/ThetaCriterionPCE.py diff --git a/src/UQpy/sampling/ThetaCriterionPCE.py b/src/UQpy/sampling/ThetaCriterionPCE.py new file mode 100644 index 000000000..baf02d78a --- /dev/null +++ b/src/UQpy/sampling/ThetaCriterionPCE.py @@ -0,0 +1,111 @@ +import numpy as np +import UQpy +from UQpy.distributions import Uniform, JointIndependent +from UQpy.surrogates import polynomial_chaos +from scipy.spatial.distance import cdist +from beartype import beartype + +class ThetaCriterionPCE: + @beartype + def __init__(self,surrogates: list): + """ + Active learning for polynomial chaos expansion using Theta criterion balancing between exploration and exploitation. + + :param surrogates: list of objects of the :py:meth:`UQpy` :class:`PolynomialChaosExpansion` class + """ + + self.surrogates=surrogates + + + def run(self, X: np.ndarray, Xcandidate: np.ndarray, nadd=1, WeightsS=None, WeightsSCandidate=None, WeightsPCE=None, Criterium=False): + + """ + Execute the :class:`.ThetaCriterionPCE` active learning. + :param X: Samples in existing ED used for construction of pces. + :param Xcandidate: Candidate samples for selectiong by Theta criterion. + :param WeightsS: Weights associated to X samples (e.g. from Coherence Sampling). + :param WeightsSCandidate: Weights associated to candidate samples (e.g. from Coherence Sampling). + :param nadd: Number of samples selected from candidate set in a single run of this algorithm + :param WeightsPCE: Weights associated to each PCE (e.g. Eigen values from dimension-reduction techniques) + + The :meth:`run` method is the function that performs iterations in the :class:`.ThetaCriterionPCE` class. + The :meth:`run` method of the :class:`.ThetaCriterionPCE` class can be invoked many times for sequential sampling. + + :return: Position of the best candidate in candidate set. If ``Criterium = True``, values of Theta criterion (variance density, average variance density, geometrical part, total Theta criterion) for all candidates are returned instead of a position. + + """ + + pces=self.surrogates + + npce=len(pces) + nsimexisting, nvar = X.shape + nsimcandidate, nvar = Xcandidate.shape + l = np.zeros(nsimcandidate) + criterium = np.zeros(nsimcandidate) + if WeightsS is None: + WeightsS = np.ones(nsimexisting) + + if WeightsSCandidate is None: + WeightsSCandidate = np.ones(nsimcandidate) + + if WeightsPCE is None: + WeightsPCE = np.ones(npce) + + pos=[] + + for n in range (nadd): + + + S=polynomial_chaos.Polynomials.standardize_sample(X,pces[0].polynomial_basis.distributions) + Scandidate=polynomial_chaos.Polynomials.standardize_sample(Xcandidate,pces[0].polynomial_basis.distributions) + + lengths = cdist(Scandidate, S) + closestS_pos = np.argmin(lengths, axis=1) + closest_valueX = X[closestS_pos] + l = np.nanmin(lengths, axis=1) + variance_candidate=0 + variance_closest=0 + + for i in range(npce): + variance_candidatei=0 + variance_closesti=0 + pce=pces[i] + variance_candidatei = self.LocalVariance(Xcandidate, pce, WeightsSCandidate) + variance_closesti = self.LocalVariance(closest_valueX, pce, WeightsS[closestS_pos]) + + variance_candidate=variance_candidate+variance_candidatei*WeightsPCE[i] + variance_closest=variance_closest+variance_closesti*WeightsPCE[i] + + criteriumV = np.sqrt(variance_candidate * variance_closest) + criteriumL = l**nvar + criterium = criteriumV * criteriumL + pos.append(np.argmax(criterium)) + X=np.append(X,Xcandidate[pos,:],axis=0) + WeightsS=np.append(WeightsS,WeightsSCandidate[pos]) + + if Criterium == False: + if nadd==1: + pos=pos[0] + return pos + else: + return variance_candidate, criteriumV, criteriumL, criterium + + + # calculate variance density of PCE for Theta Criterion + def LocalVariance(self,coord,pce,Weight=1): + Beta=pce.coefficients + Beta[0] = 0 + + # product = PolynomialBasis(LARmultindex, [polynomial], coord) + product=pce.polynomial_basis.evaluate_basis(coord) + + # product *= Weight + product = np.transpose(np.transpose(product)*Weight) + product = product.dot(Beta) + + product = np.sum(product,axis=1) + + product= product**2 + product = product *polynomial_chaos.Polynomials.standardize_pdf(coord,pce.polynomial_basis.distributions) + + return product From 1790f3728c015b332b01c4a380ec45e100d262c2 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Wed, 19 Apr 2023 10:54:35 -0400 Subject: [PATCH 31/41] Minor naming conventions changes --- src/UQpy/sampling/ThetaCriterionPCE.py | 133 +++++++++--------- .../polynomials/baseclass/Polynomials.py | 53 ++++--- 2 files changed, 92 insertions(+), 94 deletions(-) diff --git a/src/UQpy/sampling/ThetaCriterionPCE.py b/src/UQpy/sampling/ThetaCriterionPCE.py index baf02d78a..da732c510 100644 --- a/src/UQpy/sampling/ThetaCriterionPCE.py +++ b/src/UQpy/sampling/ThetaCriterionPCE.py @@ -1,111 +1,110 @@ import numpy as np import UQpy -from UQpy.distributions import Uniform, JointIndependent from UQpy.surrogates import polynomial_chaos from scipy.spatial.distance import cdist from beartype import beartype + class ThetaCriterionPCE: @beartype - def __init__(self,surrogates: list): + def __init__(self, surrogates: list[UQpy.surrogates.polynomial_chaos.PolynomialChaosExpansion]): """ - Active learning for polynomial chaos expansion using Theta criterion balancing between exploration and exploitation. + Active learning for polynomial chaos expansion using Theta criterion balancing between exploration and + exploitation. :param surrogates: list of objects of the :py:meth:`UQpy` :class:`PolynomialChaosExpansion` class """ - - self.surrogates=surrogates - - - def run(self, X: np.ndarray, Xcandidate: np.ndarray, nadd=1, WeightsS=None, WeightsSCandidate=None, WeightsPCE=None, Criterium=False): + + self.surrogates = surrogates + + def run(self, existing_samples: np.ndarray, candidate_samples: np.ndarray, nsamples=1, samples_weights=None, + candidate_weights=None, pce_weights=None, enable_criterium: bool=False): """ Execute the :class:`.ThetaCriterionPCE` active learning. - :param X: Samples in existing ED used for construction of pces. - :param Xcandidate: Candidate samples for selectiong by Theta criterion. - :param WeightsS: Weights associated to X samples (e.g. from Coherence Sampling). - :param WeightsSCandidate: Weights associated to candidate samples (e.g. from Coherence Sampling). - :param nadd: Number of samples selected from candidate set in a single run of this algorithm - :param WeightsPCE: Weights associated to each PCE (e.g. Eigen values from dimension-reduction techniques) - - The :meth:`run` method is the function that performs iterations in the :class:`.ThetaCriterionPCE` class. - The :meth:`run` method of the :class:`.ThetaCriterionPCE` class can be invoked many times for sequential sampling. - - :return: Position of the best candidate in candidate set. If ``Criterium = True``, values of Theta criterion (variance density, average variance density, geometrical part, total Theta criterion) for all candidates are returned instead of a position. - + :param existing_samples: Samples in existing ED used for construction of PCEs. + :param candidate_samples: Candidate samples for selecting by Theta criterion. + :param samples_weights: Weights associated to X samples (e.g. from Coherence Sampling). + :param candidate_weights: Weights associated to candidate samples (e.g. from Coherence Sampling). + :param nsamples: Number of samples selected from candidate set in a single run of this algorithm + :param pce_weights: Weights associated to each PCE (e.g. Eigen values from dimension-reduction techniques) + The :meth:`run` method is the function that performs iterations in the :class:`.ThetaCriterionPCE` class. + The :meth:`run` method of the :class:`.ThetaCriterionPCE` class can be invoked many times for sequential + sampling. + :return: Position of the best candidate in candidate set. If ``enable_criterium = True``, values of Theta + criterion (variance density, average variance density, geometrical part, total Theta criterion) for all + candidates are returned instead of a position. """ - pces=self.surrogates + pces = self.surrogates - npce=len(pces) - nsimexisting, nvar = X.shape - nsimcandidate, nvar = Xcandidate.shape + npce = len(pces) + nsimexisting, nvar = existing_samples.shape + nsimcandidate, nvar = candidate_samples.shape l = np.zeros(nsimcandidate) criterium = np.zeros(nsimcandidate) - if WeightsS is None: - WeightsS = np.ones(nsimexisting) + if samples_weights is None: + samples_weights = np.ones(nsimexisting) - if WeightsSCandidate is None: - WeightsSCandidate = np.ones(nsimcandidate) - - if WeightsPCE is None: - WeightsPCE = np.ones(npce) + if candidate_weights is None: + candidate_weights = np.ones(nsimcandidate) - pos=[] - - for n in range (nadd): + if pce_weights is None: + pce_weights = np.ones(npce) - - S=polynomial_chaos.Polynomials.standardize_sample(X,pces[0].polynomial_basis.distributions) - Scandidate=polynomial_chaos.Polynomials.standardize_sample(Xcandidate,pces[0].polynomial_basis.distributions) + pos = [] + + for n in range(nsamples): + S = polynomial_chaos.Polynomials.standardize_sample(existing_samples, pces[0].polynomial_basis.distributions) + Scandidate = polynomial_chaos.Polynomials.standardize_sample(candidate_samples, + pces[0].polynomial_basis.distributions) lengths = cdist(Scandidate, S) closestS_pos = np.argmin(lengths, axis=1) - closest_valueX = X[closestS_pos] + closest_valueX = existing_samples[closestS_pos] l = np.nanmin(lengths, axis=1) - variance_candidate=0 - variance_closest=0 + variance_candidate = 0 + variance_closest = 0 for i in range(npce): - variance_candidatei=0 - variance_closesti=0 - pce=pces[i] - variance_candidatei = self.LocalVariance(Xcandidate, pce, WeightsSCandidate) - variance_closesti = self.LocalVariance(closest_valueX, pce, WeightsS[closestS_pos]) + variance_candidatei = 0 + variance_closesti = 0 + pce = pces[i] + variance_candidatei = self._local_variance(candidate_samples, pce, candidate_weights) + variance_closesti = self._local_variance(closest_valueX, pce, samples_weights[closestS_pos]) - variance_candidate=variance_candidate+variance_candidatei*WeightsPCE[i] - variance_closest=variance_closest+variance_closesti*WeightsPCE[i] + variance_candidate = variance_candidate + variance_candidatei * pce_weights[i] + variance_closest = variance_closest + variance_closesti * pce_weights[i] criteriumV = np.sqrt(variance_candidate * variance_closest) - criteriumL = l**nvar + criteriumL = l ** nvar criterium = criteriumV * criteriumL pos.append(np.argmax(criterium)) - X=np.append(X,Xcandidate[pos,:],axis=0) - WeightsS=np.append(WeightsS,WeightsSCandidate[pos]) - - if Criterium == False: - if nadd==1: - pos=pos[0] + existing_samples = np.append(existing_samples, candidate_samples[pos, :], axis=0) + samples_weights = np.append(samples_weights, candidate_weights[pos]) + + if not enable_criterium: + if nsamples == 1: + pos = pos[0] return pos else: return variance_candidate, criteriumV, criteriumL, criterium - - + # calculate variance density of PCE for Theta Criterion - def LocalVariance(self,coord,pce,Weight=1): - Beta=pce.coefficients - Beta[0] = 0 + @staticmethod + def _local_variance(coordinates, pce, weight=1): + beta = pce.coefficients + beta[0] = 0 - # product = PolynomialBasis(LARmultindex, [polynomial], coord) - product=pce.polynomial_basis.evaluate_basis(coord) + product = pce.polynomial_basis.evaluate_basis(coordinates) - # product *= Weight - product = np.transpose(np.transpose(product)*Weight) - product = product.dot(Beta) + product = np.transpose(np.transpose(product) * weight) + product = product.dot(beta) - product = np.sum(product,axis=1) + product = np.sum(product, axis=1) - product= product**2 - product = product *polynomial_chaos.Polynomials.standardize_pdf(coord,pce.polynomial_basis.distributions) + product = product ** 2 + product = product * polynomial_chaos.Polynomials.standardize_pdf(coordinates, + pce.polynomial_basis.distributions) return product diff --git a/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py b/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py index 085eb970a..cb9e1c59b 100644 --- a/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py +++ b/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py @@ -25,9 +25,9 @@ def __init__(self, distributions: Union[Distribution, list[Distribution]], degre """ self.distributions = distributions self.degree = degree + 1 - + @staticmethod - def standardize_sample(x,joint_distribution): + def standardize_sample(x, joint_distribution): """ Static method: Standardize data based on the joint probability distribution. @@ -35,28 +35,28 @@ def standardize_sample(x,joint_distribution): :param joint_distribution: joint probability distribution from :py:mod:`UQpy` distribution object :return: Standardized data. """ - - - s=np.zeros(x.shape) - inputs_number = len(x[0,:]) + + s = np.zeros(x.shape) + inputs_number = len(x[0, :]) if inputs_number == 1: marginals = [joint_distribution] else: marginals = joint_distribution.marginals - - for i in range (inputs_number): - if type(marginals[i]) == Normal: - s[:,i]=Polynomials.standardize_normal(x[:,i],mean=marginals[i].parameters['loc'],std=marginals[i].parameters['scale']) - + + for i in range(inputs_number): + if type(marginals[i]) == Normal: + s[:, i] = Polynomials.standardize_normal(x[:, i], mean=marginals[i].parameters['loc'], + std=marginals[i].parameters['scale']) + if type(marginals[i]) == Uniform: - s[:,i]=Polynomials.standardize_uniform(x[:,i], marginals[i]) - + s[:, i] = Polynomials.standardize_uniform(x[:, i], marginals[i]) + else: raise TypeError("standarize_sample is defined only for Uniform and Gaussian marginal distributions") return s - + @staticmethod - def standardize_pdf(x,joint_distribution): + def standardize_pdf(x, joint_distribution): """ Static method: PDF of standardized distributions associated to Hermite or Legendre polynomials. @@ -64,22 +64,21 @@ def standardize_pdf(x,joint_distribution): :param joint_distribution: joint probability distribution from :py:mod:`UQpy` distribution object :return: Value of standardized PDF calculated for x """ - - inputs_number = len(x[0,:]) - pdf_val=1 - s=Polynomials.standardize_sample(x,joint_distribution) - + + inputs_number = len(x[0, :]) + pdf_val = 1 + s = Polynomials.standardize_sample(x, joint_distribution) + if inputs_number == 1: marginals = [joint_distribution] else: marginals = joint_distribution.marginals - for i in range (inputs_number): - if type(marginals[i]) == Normal: - pdf_val = pdf_val * (stats.norm.pdf(s[:,i])) + for i in range(inputs_number): + if type(marginals[i]) == Normal: + pdf_val *= (stats.norm.pdf(s[:, i])) if type(marginals[i]) == Uniform: - pdf_val = pdf_val * (stats.uniform.pdf(s[:,i], loc=-1, scale=2)) - + pdf_val *= (stats.uniform.pdf(s[:, i], loc=-1, scale=2)) else: raise TypeError("standardize_pdf is defined only for Uniform and Gaussian marginal distributions") return pdf_val @@ -104,7 +103,7 @@ def standardize_uniform(x, uniform): return (2 * x - loc - upper) / (upper - loc) @staticmethod - def normalized(degree: int, samples: np.ndarray, a: float, b: float, pdf_st: Callable, p:list): + def normalized(degree: int, samples: np.ndarray, a: float, b: float, pdf_st: Callable, p: list): """ Calculates design matrix and normalized polynomials. @@ -169,4 +168,4 @@ def scale(self): def evaluate(self, x: np.ndarray): pass - distribution_to_polynomial = { } + distribution_to_polynomial = {} From 07d267330dc9f0adc1809aff6716008d7bab7f92 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Thu, 20 Apr 2023 09:09:08 -0400 Subject: [PATCH 32/41] Adds theta criterion PCE to __init__.py --- src/UQpy/sampling/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UQpy/sampling/__init__.py b/src/UQpy/sampling/__init__.py index d4b2ea7c9..9629c9b48 100644 --- a/src/UQpy/sampling/__init__.py +++ b/src/UQpy/sampling/__init__.py @@ -8,3 +8,4 @@ from UQpy.sampling.MonteCarloSampling import MonteCarloSampling from UQpy.sampling.SimplexSampling import SimplexSampling +from UQpy.sampling.ThetaCriterionPCE import ThetaCriterionPCE From 441e35b572b5e9411164fc0606e734be4eb25c51 Mon Sep 17 00:00:00 2001 From: Lukas Novak <95358264+NovakLBUT@users.noreply.github.com> Date: Fri, 21 Apr 2023 20:31:28 +0200 Subject: [PATCH 33/41] Update test_pce.py new unit test for active learning --- tests/unit_tests/surrogates/test_pce.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/unit_tests/surrogates/test_pce.py b/tests/unit_tests/surrogates/test_pce.py index 2a0b95199..b0e6f8e5c 100644 --- a/tests/unit_tests/surrogates/test_pce.py +++ b/tests/unit_tests/surrogates/test_pce.py @@ -382,3 +382,24 @@ def test_21(): assert all((np.argwhere(np.round(pce2_lar_sens.calculate_generalized_total_order_indices(), 3) > 0) == [[0], [2], [3]])) + +def test_22(): + """ + Test Active Learning based on Theta Criterion + """ + polynomial_basis = TotalDegreeBasis(dist, max_degree) + least_squares = LeastSquareRegression() + pce = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) + uniform_x=np.zeros((3,1)) + uniform_x[:,0]=np.array([0,5,10]) + pce.fit(uniform_x, uniform_x) + + adapted_x=uniform_x + candidates_x=np.zeros((5,1)) + candidates_x[:,0]=np.array([1.1,4,5.1,6,9]) + + + ThetaSampling_complete=ThetaCriterionPCE([pce]) + pos=ThetaSampling_complete.run(adapted_x,candidates_x,nadd=2) + best_candidates=candidates_x[pos,:] + assert best_candidates[0,0]==1.1 and best_candidates[1,0]==9 From e33a67385e04131fcca1f2605c6838fc19d63d9c Mon Sep 17 00:00:00 2001 From: Lukas Novak <95358264+NovakLBUT@users.noreply.github.com> Date: Fri, 21 Apr 2023 20:39:34 +0200 Subject: [PATCH 34/41] Update ThetaCriterionPCE.py Added description of optional parameter --- src/UQpy/sampling/ThetaCriterionPCE.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/UQpy/sampling/ThetaCriterionPCE.py b/src/UQpy/sampling/ThetaCriterionPCE.py index da732c510..f91dda468 100644 --- a/src/UQpy/sampling/ThetaCriterionPCE.py +++ b/src/UQpy/sampling/ThetaCriterionPCE.py @@ -28,6 +28,8 @@ def run(self, existing_samples: np.ndarray, candidate_samples: np.ndarray, nsamp :param candidate_weights: Weights associated to candidate samples (e.g. from Coherence Sampling). :param nsamples: Number of samples selected from candidate set in a single run of this algorithm :param pce_weights: Weights associated to each PCE (e.g. Eigen values from dimension-reduction techniques) + :param enable_criterium: If True, values of Theta criterion (variance density, average variance density, geometrical part, total Theta criterion) for all + candidates are returned instead of a positions of best candidates The :meth:`run` method is the function that performs iterations in the :class:`.ThetaCriterionPCE` class. The :meth:`run` method of the :class:`.ThetaCriterionPCE` class can be invoked many times for sequential sampling. From 1697947ae5779f706096d4ffbfb52f135fc348e1 Mon Sep 17 00:00:00 2001 From: Lukas Novak <95358264+NovakLBUT@users.noreply.github.com> Date: Fri, 21 Apr 2023 20:51:20 +0200 Subject: [PATCH 35/41] Example for Theta Criterion --- .../pce/plot_pce_theta_criterion.py | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 docs/code/surrogates/pce/plot_pce_theta_criterion.py diff --git a/docs/code/surrogates/pce/plot_pce_theta_criterion.py b/docs/code/surrogates/pce/plot_pce_theta_criterion.py new file mode 100644 index 000000000..59bd76838 --- /dev/null +++ b/docs/code/surrogates/pce/plot_pce_theta_criterion.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python +# coding: utf-8 + +# ### Polynomial Chaos Expansion example: Active Learning for Multiple Surrogate Models +# + +# Authors: Lukas Novak \ +# Date: April 14 2023 + +# In this example, we use active learning for construction of optimal experimental design with respect to exploration of the design domain and exploitation of given surrogate models in form of Polynomial Chaos Expansion (PCE). ACtive learning is based on $\Theta$ criterion recently proposed in +# +# L. Novák, M. Vořechovský, V. Sadílek, M. D. Shields, *Variance-based adaptive sequential sampling for polynomial chaos expansion*, 637 Computer Methods in Applied Mechanics and Engineering 386 (2021) 114105. doi:10.1016/j.cma.2021.114105. + +# We start with the necessary imports. + +# In[3]: + + +# import packages +import numpy as np +import matplotlib.pyplot as plt +from UQpy.sampling.ThetaCriterionPCE import * + +from UQpy.distributions import Uniform, JointIndependent, Normal +from UQpy.surrogates import * + +from UQpy.sampling import LatinHypercubeSampling +from UQpy.sampling.stratified_sampling.latin_hypercube_criteria import * + + +# The example involves a~$2$D function with mirrored quarter-circle arc line singularities \cite{NovVorSadShi:CMAME:21}. The form of the function is give by: +# \begin{equation} +# f(\mathbf{X})= \frac{1}{ \lvert 0.3-X_1^2 - X_2^2\rvert + \delta}- +# \frac{1}{ \lvert 0.3-(1-X_1)^2 - (1-X_2)^2\rvert + \delta}, \quad \mathbf{X} \sim \mathcal{U}[0,1]^2 +# , +# \end{equation} +# where the strength of the singularities is controlled by the parameter $\delta$, which we set as $\delta=0.01$. \ +# +# + +# In[2]: + + +def Model2DComplete(X,delta=0.01): + Y=Model2D1(X)+Model2D2(X) + return Y + + +# In order to show the possibilities of active learning for multiple surrogate models, we split the function into the two parts as follows: +# +# \begin{equation} +# f_1(\mathbf{X})= +# \begin{cases} +# \frac{1}{ \lvert 0.3-X_1^2 - X_2^2\rvert + \delta}- +# \frac{1}{ \lvert 0.3-(1-X_1)^2 - (1-X_2)^2\rvert + \delta} \quad \text{for} \quad X_1X_2\\ +# 0 \quad \text{otherwise} +# \end{cases}, +# \end{equation} + +# In[3]: + + +def Model2D1(X,delta=0.01): + M=X[:,0]X[:,1] + Y=1/(np.abs(0.3 - X[:, 0]**2 - X[:, 1]**2) + delta)-1/(np.abs(0.3 - (1 - X[:, 0])**2 - (1 - X[:, 1])**2) + delta) + Y[M]=0 + return Y + + +# The mathematical models have indepdent random inputs, which are uniformly distributed in interval $[0, 1]$. + +# In[4]: + + +# input distributions +dist1 = Uniform(loc=0, scale=1) +dist2 = Uniform(loc=0, scale=1) + +marg = [dist1, dist2] +joint = JointIndependent(marginals=marg) + + +# We must now select a polynomial basis. Here we opt for a total-degree (TD) basis, such that the univariate polynomials have a maximum degree equal to $P$ and all multivariate polynomial have a total-degree (sum of degrees of corresponding univariate polynomials) at most equal to $P$. The size of the basis is then given by +# $$\frac{(N+P)!}{N! P!},$$ +# where $N$ is the number of random inputs (here, $N=2$). +# +# +# + +# In[5]: + + +# realizations of random inputs +# training data +# maximum polynomial degree +P = 10 +# construct total-degree polynomial basis and use OLS for estimation of coefficients +polynomial_basis = TotalDegreeBasis(joint,P) + + +# We must now compute the PCE coefficients. For that we first need a training sample of input random variable realizations and the corresponding model outputs. These two data sets form what is also known as an ''experimental design''. In case of adaptive construction of PCE by the best model selection algorithm, size of ED is given apriori and the most suitable basis functions are adaptively selected. Here we have two surrogate models with identical training samples of input random vector and two sets of corresponding mathematical models. This ED represents small initial ED, which will be further extended by active learning algorithm. + +# In[6]: + + +# number of inital traning samples +sample_size = 15 + +# realizations of input random vector +xx_train = joint.rvs(sample_size) + +# corresponding model outputs +yy_train1 = Model2D1(xx_train) +yy_train2 = Model2D2(xx_train) + + +# We now fit the PCE coefficients by solving a regression problem. Here we opt for the _np.linalg.lstsq_ method, which is based on the _dgelsd_ solver of LAPACK. This original PCE class will be used for further selection of the best basis functions. Once we have created the PCE containing all basis functions generated by TD algorithm, it is possible to reduce the number of basis functions by LAR algorithm. We create sparse PCE approximations for both mathematical models as follows. + +# In[7]: + + + +least_squares = LeastSquareRegression() + +# fit model 1 +pce1 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) +pce1.fit(xx_train, yy_train1) +pceLAR1=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce1) + + +# fit model 2 +pce2 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) +pce2.fit(xx_train, yy_train2) +pceLAR2=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce2) + + +# The active learning algorithm based on $\Theta$ criterion selects the best sample from given large set of candidates coverign uniformly the whole design domain. Here we set number of samples as $n_{cand}=10^4$ and use LHS-MaxiMin for sampling, though any sampling technique can be employed. + +# In[8]: + + +# number of candidates for the active learning algorithm +n_cand=10000 + +# MaxiMin LHS samples uniformly covering the whole input random space +lhs_maximin_cand = LatinHypercubeSampling(distributions=[dist1, dist2], + criterion=MaxiMin(metric=DistanceMetric.CHEBYSHEV), + nsamples=n_cand) + +# candidates for active learning +Xaptive = lhs_maximin_cand._samples + + +# Start of the active learning algorithm interatively selecting $n_{add}=400$ samples one-by-one. Note that the class ThetaCriterionPCE takes a list of surrogate models as input, here we have 2 PCEs approximated 2 mathematical models. The active learning further selects the best candidate in each run by variance-based $\Theta$ criterion. + +# In[ ]: + + +# total number of added points by the active learning algorithm +naddedsims=400 + +# loading of existing ED for both PCEs +Xadapted=xx_train +Yadapted1=yy_train1 +Yadapted2=yy_train2 + + +# adaptive algorithm and reconstruction of PCE + +for i in range (0,int(naddedsims)): +# create ThetaCriterion class for active learning + ThetaSampling=ThetaCriterionPCE([pceLAR1,pceLAR2]) + +# find the best candidate according to given criterium (variance and distance) + pos=ThetaSampling.run(Xadapted,Xaptive) + newpointX=np.array([Xaptive[pos,:]]) + + newpointres1=Model2D1(newpointX) + newpointres2=Model2D2(newpointX) + +# add the best candidate to experimental design + Xadapted=np.append(Xadapted,newpointX,axis=0) + Yadapted1=np.r_[Yadapted1, newpointres1] + Yadapted2=np.r_[Yadapted2, newpointres2] + + + +# reconstruct the PCE 1 + pce1.fit(Xadapted, Yadapted1) + pceLAR1=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce1) + + +# reconstruct the PCE 2 + pce2.fit(Xadapted, Yadapted2) + pceLAR2=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce2) + + + if i%10==0: + print('\nNumber of added simulations:', i) + + + +# plot final ED +fig, ax_nstd = plt.subplots(figsize=(6, 6)) +ax_nstd.plot(Xadapted[:,0],Xadapted[:,1], 'ro',label='Adapted ED') +ax_nstd.plot(xx_train[:,0],xx_train[:,1], 'bo',label='Original ED') +ax_nstd.set_ylabel(r'$X_2$') +ax_nstd.set_xlabel(r'$X_1$') +ax_nstd.legend(loc='upper left'); + + +# For a comparison, we construct also a surrogate model of the full original mathematical model and further run active learning similarly as for the previous reduced models. Note that the final ED for this complete mathematical model should be almost identical as in the previous case with the two PCEs approximating reduced models. + +# In[13]: + + +yy_train3 = Model2DComplete(xx_train) +pce3 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) +pce3.fit(xx_train, yy_train3) +pceLAR3=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce3) + + +# In[ ]: + + +Xadapted3=xx_train +Yadapted3=yy_train3 + + +# adaptive algorithm and reconstruction of PCE +for i in range (0,int(400)): +# create ThetaCriterion class for active learning + ThetaSampling_complete=ThetaCriterionPCE([pceLAR3]) + +# find the best candidate according to given criterium (variance and distance) + pos=ThetaSampling_complete.run(Xadapted3,Xaptive) + newpointX=np.array([Xaptive[pos,:]]) + newpointres=Model2DComplete(newpointX) + + +# add the best candidate to experimental design + Xadapted3=np.append(Xadapted3,newpointX,axis=0) + Yadapted3=np.r_[Yadapted3, newpointres] + + + pce3.fit(Xadapted3, Yadapted3) + pceLAR3=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce3) + + + if i%10==0: + print('\nNumber of added simulations:', i) + + + +# plot final ED +fig, ax_nstd = plt.subplots(figsize=(6, 6)) +ax_nstd.plot(Xadapted3[:,0],Xadapted3[:,1], 'ro',label='Adapted ED') +ax_nstd.plot(xx_train[:,0],xx_train[:,1], 'bo',label='Original ED') +ax_nstd.set_ylabel(r'$X_2$') +ax_nstd.set_xlabel(r'$X_1$') +ax_nstd.legend(loc='upper left'); + + +# In[ ]: + + + + + +# In[ ]: + + + + + +# In[ ]: + + + + From f52aa944a1e73b9a71aa8f4b32e0f4eaea9ef243 Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Mon, 24 Apr 2023 10:40:51 -0400 Subject: [PATCH 36/41] Fixes failing ThetaCriterion test --- tests/unit_tests/surrogates/test_pce.py | 29 +++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/unit_tests/surrogates/test_pce.py b/tests/unit_tests/surrogates/test_pce.py index b0e6f8e5c..bd19ed653 100644 --- a/tests/unit_tests/surrogates/test_pce.py +++ b/tests/unit_tests/surrogates/test_pce.py @@ -1,5 +1,6 @@ import pytest +from UQpy import ThetaCriterionPCE from UQpy.distributions import JointIndependent, Normal from UQpy.sampling import MonteCarloSampling from UQpy.distributions import Uniform @@ -39,7 +40,7 @@ def test_2(): Test tp basis """ polynomial_basis = TensorProductBasis(distributions=dist, - max_degree=max_degree).polynomials + max_degree=max_degree).polynomials value = polynomial_basis[1].evaluate(x)[0] assert round(value, 4) == -0.2874 @@ -382,7 +383,8 @@ def test_21(): assert all((np.argwhere(np.round(pce2_lar_sens.calculate_generalized_total_order_indices(), 3) > 0) == [[0], [2], [3]])) - + + def test_22(): """ Test Active Learning based on Theta Criterion @@ -390,16 +392,15 @@ def test_22(): polynomial_basis = TotalDegreeBasis(dist, max_degree) least_squares = LeastSquareRegression() pce = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) - uniform_x=np.zeros((3,1)) - uniform_x[:,0]=np.array([0,5,10]) + uniform_x = np.zeros((3, 1)) + uniform_x[:, 0] = np.array([0, 5, 10]) pce.fit(uniform_x, uniform_x) - - adapted_x=uniform_x - candidates_x=np.zeros((5,1)) - candidates_x[:,0]=np.array([1.1,4,5.1,6,9]) - - - ThetaSampling_complete=ThetaCriterionPCE([pce]) - pos=ThetaSampling_complete.run(adapted_x,candidates_x,nadd=2) - best_candidates=candidates_x[pos,:] - assert best_candidates[0,0]==1.1 and best_candidates[1,0]==9 + + adapted_x = uniform_x + candidates_x = np.zeros((5, 1)) + candidates_x[:, 0] = np.array([1.1, 4, 5.1, 6, 9]) + + ThetaSampling_complete = ThetaCriterionPCE([pce]) + pos = ThetaSampling_complete.run(adapted_x, candidates_x, nsamples=2) + best_candidates = candidates_x[pos, :] + assert best_candidates[0, 0] == 1.1 and best_candidates[1, 0] == 9 From e859d3cad4c8ec905d520fb3dc3e6f3c0389b07f Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Mon, 24 Apr 2023 11:43:48 -0400 Subject: [PATCH 37/41] Updates documentation files --- docs/code/sampling/theta_criterion/README.rst | 2 + .../theta_criterion/pce_theta_criterion.py | 246 +++++++++++++++ .../pce/plot_pce_theta_criterion.py | 297 ------------------ docs/source/conf.py | 2 + docs/source/sampling/index.rst | 3 + docs/source/sampling/theta_criterion.rst | 24 ++ src/UQpy/sampling/ThetaCriterionPCE.py | 26 +- 7 files changed, 289 insertions(+), 311 deletions(-) create mode 100644 docs/code/sampling/theta_criterion/README.rst create mode 100644 docs/code/sampling/theta_criterion/pce_theta_criterion.py delete mode 100644 docs/code/surrogates/pce/plot_pce_theta_criterion.py create mode 100644 docs/source/sampling/theta_criterion.rst diff --git a/docs/code/sampling/theta_criterion/README.rst b/docs/code/sampling/theta_criterion/README.rst new file mode 100644 index 000000000..13ac8db64 --- /dev/null +++ b/docs/code/sampling/theta_criterion/README.rst @@ -0,0 +1,2 @@ +Theta Criterion PCE Examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/code/sampling/theta_criterion/pce_theta_criterion.py b/docs/code/sampling/theta_criterion/pce_theta_criterion.py new file mode 100644 index 000000000..ef37babd4 --- /dev/null +++ b/docs/code/sampling/theta_criterion/pce_theta_criterion.py @@ -0,0 +1,246 @@ +""" +Polynomial Chaos Expansion example: Active Learning for Multiple Surrogate Models +====================================================================================== + +In this example, we use active learning for construction of optimal experimental design with respect to exploration of +the design domain and exploitation of given surrogate models in form of Polynomial Chaos Expansion (PCE). Active learning is based on :math:`\Theta` criterion recently proposed in + +L. Novák, M. Vořechovský, V. Sadílek, M. D. Shields, *Variance-based adaptive sequential sampling for polynomial chaos expansion*, 637 Computer Methods in Applied Mechanics and Engineering 386 (2021) 114105. doi:10.1016/j.cma.2021.114105. +""" + +# %% md +# We start with the necessary imports. + +# %% + +import numpy as np +import matplotlib.pyplot as plt +from UQpy.sampling.ThetaCriterionPCE import * + +from UQpy.distributions import Uniform, JointIndependent, Normal +from UQpy.surrogates import * + +from UQpy.sampling import LatinHypercubeSampling +from UQpy.sampling.stratified_sampling.latin_hypercube_criteria import * + + +# %% md +# The example involves a :math:`2D` function with mirrored quarter-circle arc line singularities. The form of the function is give by: +# +# .. math:: f(\mathbf{X})= \frac{1}{ \lvert 0.3-X_1^2 - X_2^2\rvert + \delta}- \frac{1}{ \lvert 0.3-(1-X_1)^2 - (1-X_2)^2\rvert + \delta}, \quad \mathbf{X} \sim \mathcal{U}[0,1]^2 +# +# where the strength of the singularities is controlled by the parameter :math:`\delta`, which we set as :math:`\delta=0.01`. +# +# + +# %% + + +def Model2DComplete(X, delta=0.01): + Y = Model2D1(X) + Model2D2(X) + return Y + + +# %% md +# In order to show the possibilities of active learning for multiple surrogate models, we split the function into the two parts as follows: +# +# .. math:: f_1(\mathbf{X})= \begin{cases} \frac{1}{ \lvert 0.3-X_1^2 - X_2^2\rvert + \delta}-\frac{1}{ \lvert 0.3-(1-X_1)^2 - (1-X_2)^2\rvert + \delta} \quad \text{for} \quad X_1X_2\\ 0 \quad \text{otherwise} \end{cases} + +# %% + + +def Model2D1(X, delta=0.01): + M = X[:, 0] < X[:, 1] + Y = 1 / (np.abs(0.3 - X[:, 0] ** 2 - X[:, 1] ** 2) + delta) - 1 / ( + np.abs(0.3 - (1 - X[:, 0]) ** 2 - (1 - X[:, 1]) ** 2) + delta) + Y[M] = 0 + return Y + + +def Model2D2(X, delta=0.01): + M = X[:, 0] > X[:, 1] + Y = 1 / (np.abs(0.3 - X[:, 0] ** 2 - X[:, 1] ** 2) + delta) - 1 / ( + np.abs(0.3 - (1 - X[:, 0]) ** 2 - (1 - X[:, 1]) ** 2) + delta) + Y[M] = 0 + return Y + +# %% md +# The mathematical models have independent random inputs, which are uniformly distributed in interval :math:`[0, 1]`. + +# %% + + +# input distributions +dist1 = Uniform(loc=0, scale=1) +dist2 = Uniform(loc=0, scale=1) + +marg = [dist1, dist2] +joint = JointIndependent(marginals=marg) + +# %% md +# We must now select a polynomial basis. Here we opt for a total-degree (TD) basis, such that the univariate polynomials have a maximum degree equal to :math:`P` and all multivariate polynomial have a total-degree (sum of degrees of corresponding univariate polynomials) at most equal to :math:`P`. The size of the basis is then given by +# +# .. math:: \frac{(N+P)!}{N! P!} +# +# where :math:`N` is the number of random inputs (here, :math:`N=2`). +# + +# %% + + +# realizations of random inputs +# training data +# maximum polynomial degree +P = 10 +# construct total-degree polynomial basis and use OLS for estimation of coefficients +polynomial_basis = TotalDegreeBasis(joint, P) + +# %% md +# We must now compute the PCE coefficients. For that we first need a training sample of input random variable realizations and the corresponding model outputs. These two data sets form what is also known as an ''experimental design''. In case of adaptive construction of PCE by the best model selection algorithm, size of ED is given apriori and the most suitable basis functions are adaptively selected. Here we have two surrogate models with identical training samples of input random vector and two sets of corresponding mathematical models. This ED represents small initial ED, which will be further extended by active learning algorithm. + +# %% + + +# number of inital traning samples +sample_size = 15 + +# realizations of input random vector +xx_train = joint.rvs(sample_size) + +# corresponding model outputs +yy_train1 = Model2D1(xx_train) +yy_train2 = Model2D2(xx_train) + +# %% md +# We now fit the PCE coefficients by solving a regression problem. Here we opt for the :code:`np.linalg.lstsq` method, which is based on the _dgelsd_ solver of LAPACK. This original PCE class will be used for further selection of the best basis functions. Once we have created the PCE containing all basis functions generated by TD algorithm, it is possible to reduce the number of basis functions by LAR algorithm. We create sparse PCE approximations for both mathematical models as follows. + +# %% + + +least_squares = LeastSquareRegression() + +# fit model 1 +pce1 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) +pce1.fit(xx_train, yy_train1) +pceLAR1 = polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce1) + +# fit model 2 +pce2 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) +pce2.fit(xx_train, yy_train2) +pceLAR2 = polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce2) + +# %% md +# The active learning algorithm based on $\Theta$ criterion selects the best sample from given large set of candidates coverign uniformly the whole design domain. Here we set number of samples as :math:`n_{cand}=10^4` and use LHS-MaxiMin for sampling, though any sampling technique can be employed. + +# %% + + +# number of candidates for the active learning algorithm +n_cand = 10000 + +# MaxiMin LHS samples uniformly covering the whole input random space +lhs_maximin_cand = LatinHypercubeSampling(distributions=[dist1, dist2], + criterion=MaxiMin(metric=DistanceMetric.CHEBYSHEV), + nsamples=n_cand) + +# candidates for active learning +Xaptive = lhs_maximin_cand._samples + +# %% md +# Start of the active learning algorithm interatively selecting :math:`nsamples=400` samples one-by-one. Note that the class :code:`ThetaCriterionPCE` takes a list of surrogate models as input, here we have 2 PCEs approximated 2 mathematical models. The active learning further selects the best candidate in each run by variance-based :math:`\Theta` criterion. + +# %% + + +# total number of added points by the active learning algorithm +naddedsims = 400 + +# loading of existing ED for both PCEs +Xadapted = xx_train +Yadapted1 = yy_train1 +Yadapted2 = yy_train2 + +# adaptive algorithm and reconstruction of PCE + +for i in range(0, int(naddedsims)): + # create ThetaCriterion class for active learning + ThetaSampling = ThetaCriterionPCE([pceLAR1, pceLAR2]) + + # find the best candidate according to given criterium (variance and distance) + pos = ThetaSampling.run(Xadapted, Xaptive) + newpointX = np.array([Xaptive[pos, :]]) + + newpointres1 = Model2D1(newpointX) + newpointres2 = Model2D2(newpointX) + + # add the best candidate to experimental design + Xadapted = np.append(Xadapted, newpointX, axis=0) + Yadapted1 = np.r_[Yadapted1, newpointres1] + Yadapted2 = np.r_[Yadapted2, newpointres2] + + # reconstruct the PCE 1 + pce1.fit(Xadapted, Yadapted1) + pceLAR1 = polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce1) + + # reconstruct the PCE 2 + pce2.fit(Xadapted, Yadapted2) + pceLAR2 = polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce2) + + if i % 10 == 0: + print('\nNumber of added simulations:', i) + +# plot final ED +fig, ax_nstd = plt.subplots(figsize=(6, 6)) +ax_nstd.plot(Xadapted[:, 0], Xadapted[:, 1], 'ro', label='Adapted ED') +ax_nstd.plot(xx_train[:, 0], xx_train[:, 1], 'bo', label='Original ED') +ax_nstd.set_ylabel(r'$X_2$') +ax_nstd.set_xlabel(r'$X_1$') +ax_nstd.legend(loc='upper left'); + +# %% md +# For a comparison, we construct also a surrogate model of the full original mathematical model and further run active learning similarly as for the previous reduced models. Note that the final ED for this complete mathematical model should be almost identical as in the previous case with the two PCEs approximating reduced models. + +# %% + + +yy_train3 = Model2DComplete(xx_train) +pce3 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) +pce3.fit(xx_train, yy_train3) +pceLAR3 = polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce3) + + + +Xadapted3 = xx_train +Yadapted3 = yy_train3 + +# adaptive algorithm and reconstruction of PCE +for i in range(0, int(400)): + # create ThetaCriterion class for active learning + ThetaSampling_complete = ThetaCriterionPCE([pceLAR3]) + + # find the best candidate according to given criterium (variance and distance) + pos = ThetaSampling_complete.run(Xadapted3, Xaptive) + newpointX = np.array([Xaptive[pos, :]]) + newpointres = Model2DComplete(newpointX) + + # add the best candidate to experimental design + Xadapted3 = np.append(Xadapted3, newpointX, axis=0) + Yadapted3 = np.r_[Yadapted3, newpointres] + + pce3.fit(Xadapted3, Yadapted3) + pceLAR3 = polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce3) + + if i % 10 == 0: + print('\nNumber of added simulations:', i) + +# plot final ED +fig, ax_nstd = plt.subplots(figsize=(6, 6)) +ax_nstd.plot(Xadapted3[:, 0], Xadapted3[:, 1], 'ro', label='Adapted ED') +ax_nstd.plot(xx_train[:, 0], xx_train[:, 1], 'bo', label='Original ED') +ax_nstd.set_ylabel(r'$X_2$') +ax_nstd.set_xlabel(r'$X_1$') +ax_nstd.legend(loc='upper left'); diff --git a/docs/code/surrogates/pce/plot_pce_theta_criterion.py b/docs/code/surrogates/pce/plot_pce_theta_criterion.py deleted file mode 100644 index 59bd76838..000000000 --- a/docs/code/surrogates/pce/plot_pce_theta_criterion.py +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -# ### Polynomial Chaos Expansion example: Active Learning for Multiple Surrogate Models -# - -# Authors: Lukas Novak \ -# Date: April 14 2023 - -# In this example, we use active learning for construction of optimal experimental design with respect to exploration of the design domain and exploitation of given surrogate models in form of Polynomial Chaos Expansion (PCE). ACtive learning is based on $\Theta$ criterion recently proposed in -# -# L. Novák, M. Vořechovský, V. Sadílek, M. D. Shields, *Variance-based adaptive sequential sampling for polynomial chaos expansion*, 637 Computer Methods in Applied Mechanics and Engineering 386 (2021) 114105. doi:10.1016/j.cma.2021.114105. - -# We start with the necessary imports. - -# In[3]: - - -# import packages -import numpy as np -import matplotlib.pyplot as plt -from UQpy.sampling.ThetaCriterionPCE import * - -from UQpy.distributions import Uniform, JointIndependent, Normal -from UQpy.surrogates import * - -from UQpy.sampling import LatinHypercubeSampling -from UQpy.sampling.stratified_sampling.latin_hypercube_criteria import * - - -# The example involves a~$2$D function with mirrored quarter-circle arc line singularities \cite{NovVorSadShi:CMAME:21}. The form of the function is give by: -# \begin{equation} -# f(\mathbf{X})= \frac{1}{ \lvert 0.3-X_1^2 - X_2^2\rvert + \delta}- -# \frac{1}{ \lvert 0.3-(1-X_1)^2 - (1-X_2)^2\rvert + \delta}, \quad \mathbf{X} \sim \mathcal{U}[0,1]^2 -# , -# \end{equation} -# where the strength of the singularities is controlled by the parameter $\delta$, which we set as $\delta=0.01$. \ -# -# - -# In[2]: - - -def Model2DComplete(X,delta=0.01): - Y=Model2D1(X)+Model2D2(X) - return Y - - -# In order to show the possibilities of active learning for multiple surrogate models, we split the function into the two parts as follows: -# -# \begin{equation} -# f_1(\mathbf{X})= -# \begin{cases} -# \frac{1}{ \lvert 0.3-X_1^2 - X_2^2\rvert + \delta}- -# \frac{1}{ \lvert 0.3-(1-X_1)^2 - (1-X_2)^2\rvert + \delta} \quad \text{for} \quad X_1X_2\\ -# 0 \quad \text{otherwise} -# \end{cases}, -# \end{equation} - -# In[3]: - - -def Model2D1(X,delta=0.01): - M=X[:,0]X[:,1] - Y=1/(np.abs(0.3 - X[:, 0]**2 - X[:, 1]**2) + delta)-1/(np.abs(0.3 - (1 - X[:, 0])**2 - (1 - X[:, 1])**2) + delta) - Y[M]=0 - return Y - - -# The mathematical models have indepdent random inputs, which are uniformly distributed in interval $[0, 1]$. - -# In[4]: - - -# input distributions -dist1 = Uniform(loc=0, scale=1) -dist2 = Uniform(loc=0, scale=1) - -marg = [dist1, dist2] -joint = JointIndependent(marginals=marg) - - -# We must now select a polynomial basis. Here we opt for a total-degree (TD) basis, such that the univariate polynomials have a maximum degree equal to $P$ and all multivariate polynomial have a total-degree (sum of degrees of corresponding univariate polynomials) at most equal to $P$. The size of the basis is then given by -# $$\frac{(N+P)!}{N! P!},$$ -# where $N$ is the number of random inputs (here, $N=2$). -# -# -# - -# In[5]: - - -# realizations of random inputs -# training data -# maximum polynomial degree -P = 10 -# construct total-degree polynomial basis and use OLS for estimation of coefficients -polynomial_basis = TotalDegreeBasis(joint,P) - - -# We must now compute the PCE coefficients. For that we first need a training sample of input random variable realizations and the corresponding model outputs. These two data sets form what is also known as an ''experimental design''. In case of adaptive construction of PCE by the best model selection algorithm, size of ED is given apriori and the most suitable basis functions are adaptively selected. Here we have two surrogate models with identical training samples of input random vector and two sets of corresponding mathematical models. This ED represents small initial ED, which will be further extended by active learning algorithm. - -# In[6]: - - -# number of inital traning samples -sample_size = 15 - -# realizations of input random vector -xx_train = joint.rvs(sample_size) - -# corresponding model outputs -yy_train1 = Model2D1(xx_train) -yy_train2 = Model2D2(xx_train) - - -# We now fit the PCE coefficients by solving a regression problem. Here we opt for the _np.linalg.lstsq_ method, which is based on the _dgelsd_ solver of LAPACK. This original PCE class will be used for further selection of the best basis functions. Once we have created the PCE containing all basis functions generated by TD algorithm, it is possible to reduce the number of basis functions by LAR algorithm. We create sparse PCE approximations for both mathematical models as follows. - -# In[7]: - - - -least_squares = LeastSquareRegression() - -# fit model 1 -pce1 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) -pce1.fit(xx_train, yy_train1) -pceLAR1=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce1) - - -# fit model 2 -pce2 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) -pce2.fit(xx_train, yy_train2) -pceLAR2=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce2) - - -# The active learning algorithm based on $\Theta$ criterion selects the best sample from given large set of candidates coverign uniformly the whole design domain. Here we set number of samples as $n_{cand}=10^4$ and use LHS-MaxiMin for sampling, though any sampling technique can be employed. - -# In[8]: - - -# number of candidates for the active learning algorithm -n_cand=10000 - -# MaxiMin LHS samples uniformly covering the whole input random space -lhs_maximin_cand = LatinHypercubeSampling(distributions=[dist1, dist2], - criterion=MaxiMin(metric=DistanceMetric.CHEBYSHEV), - nsamples=n_cand) - -# candidates for active learning -Xaptive = lhs_maximin_cand._samples - - -# Start of the active learning algorithm interatively selecting $n_{add}=400$ samples one-by-one. Note that the class ThetaCriterionPCE takes a list of surrogate models as input, here we have 2 PCEs approximated 2 mathematical models. The active learning further selects the best candidate in each run by variance-based $\Theta$ criterion. - -# In[ ]: - - -# total number of added points by the active learning algorithm -naddedsims=400 - -# loading of existing ED for both PCEs -Xadapted=xx_train -Yadapted1=yy_train1 -Yadapted2=yy_train2 - - -# adaptive algorithm and reconstruction of PCE - -for i in range (0,int(naddedsims)): -# create ThetaCriterion class for active learning - ThetaSampling=ThetaCriterionPCE([pceLAR1,pceLAR2]) - -# find the best candidate according to given criterium (variance and distance) - pos=ThetaSampling.run(Xadapted,Xaptive) - newpointX=np.array([Xaptive[pos,:]]) - - newpointres1=Model2D1(newpointX) - newpointres2=Model2D2(newpointX) - -# add the best candidate to experimental design - Xadapted=np.append(Xadapted,newpointX,axis=0) - Yadapted1=np.r_[Yadapted1, newpointres1] - Yadapted2=np.r_[Yadapted2, newpointres2] - - - -# reconstruct the PCE 1 - pce1.fit(Xadapted, Yadapted1) - pceLAR1=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce1) - - -# reconstruct the PCE 2 - pce2.fit(Xadapted, Yadapted2) - pceLAR2=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce2) - - - if i%10==0: - print('\nNumber of added simulations:', i) - - - -# plot final ED -fig, ax_nstd = plt.subplots(figsize=(6, 6)) -ax_nstd.plot(Xadapted[:,0],Xadapted[:,1], 'ro',label='Adapted ED') -ax_nstd.plot(xx_train[:,0],xx_train[:,1], 'bo',label='Original ED') -ax_nstd.set_ylabel(r'$X_2$') -ax_nstd.set_xlabel(r'$X_1$') -ax_nstd.legend(loc='upper left'); - - -# For a comparison, we construct also a surrogate model of the full original mathematical model and further run active learning similarly as for the previous reduced models. Note that the final ED for this complete mathematical model should be almost identical as in the previous case with the two PCEs approximating reduced models. - -# In[13]: - - -yy_train3 = Model2DComplete(xx_train) -pce3 = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) -pce3.fit(xx_train, yy_train3) -pceLAR3=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce3) - - -# In[ ]: - - -Xadapted3=xx_train -Yadapted3=yy_train3 - - -# adaptive algorithm and reconstruction of PCE -for i in range (0,int(400)): -# create ThetaCriterion class for active learning - ThetaSampling_complete=ThetaCriterionPCE([pceLAR3]) - -# find the best candidate according to given criterium (variance and distance) - pos=ThetaSampling_complete.run(Xadapted3,Xaptive) - newpointX=np.array([Xaptive[pos,:]]) - newpointres=Model2DComplete(newpointX) - - -# add the best candidate to experimental design - Xadapted3=np.append(Xadapted3,newpointX,axis=0) - Yadapted3=np.r_[Yadapted3, newpointres] - - - pce3.fit(Xadapted3, Yadapted3) - pceLAR3=polynomial_chaos.regressions.LeastAngleRegression.model_selection(pce3) - - - if i%10==0: - print('\nNumber of added simulations:', i) - - - -# plot final ED -fig, ax_nstd = plt.subplots(figsize=(6, 6)) -ax_nstd.plot(Xadapted3[:,0],Xadapted3[:,1], 'ro',label='Adapted ED') -ax_nstd.plot(xx_train[:,0],xx_train[:,1], 'bo',label='Original ED') -ax_nstd.set_ylabel(r'$X_2$') -ax_nstd.set_xlabel(r'$X_1$') -ax_nstd.legend(loc='upper left'); - - -# In[ ]: - - - - - -# In[ ]: - - - - - -# In[ ]: - - - - diff --git a/docs/source/conf.py b/docs/source/conf.py index c4230d73b..4d44183fb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -84,6 +84,7 @@ "../code/sampling/mcmc", "../code/sampling/tempering", "../code/sampling/simplex", + "../code/sampling/theta_criterion", "../code/sampling/true_stratified_sampling", "../code/sampling/refined_stratified_sampling", "../code/inference/mle", @@ -125,6 +126,7 @@ "auto_examples/sampling/mcmc", "auto_examples/sampling/tempering", "auto_examples/sampling/simplex", + "auto_examples/sampling/theta_criterion", "auto_examples/sampling/true_stratified_sampling", "auto_examples/sampling/refined_stratified_sampling", "auto_examples/inference/mle", diff --git a/docs/source/sampling/index.rst b/docs/source/sampling/index.rst index 6dc5fa454..fac6a95b2 100644 --- a/docs/source/sampling/index.rst +++ b/docs/source/sampling/index.rst @@ -17,6 +17,8 @@ The module currently contains the following classes: - :class:`.AdaptiveKriging`: Class generating samples adaptively using a specified Kriging-based learning function in a general Adaptive Kriging-Monte Carlo Sampling (AKMCS) framework +- :class:`.ThetaCriterionPCE`: Active learning for polynomial chaos expansion using Theta criterion balancing between exploration and exploitation. + - :class:`.MCMC`: The goal of Markov Chain Monte Carlo is to draw samples from some probability distribution which is hard to compute - :class:`.ImportanceSampling`: Importance sampling (IS) is based on the idea of sampling from an alternate distribution and reweighing the samples to be representative of the target distribution @@ -31,6 +33,7 @@ The module currently contains the following classes: Refined Stratified Sampling Simplex Sampling Adaptive Kriging + Theta Criterion Markov Chain Monte Carlo Importance Sampling diff --git a/docs/source/sampling/theta_criterion.rst b/docs/source/sampling/theta_criterion.rst new file mode 100644 index 000000000..0e24e7b88 --- /dev/null +++ b/docs/source/sampling/theta_criterion.rst @@ -0,0 +1,24 @@ +Theta Criterion +--------------- + + +ThetaCriterionPCE Class +^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`.ThetaCriterionPCE` class is imported using the following command: + +>>> from UQpy.sampling.ThetaCriterionPCE import ThetaCriterionPCE + + +Methods +""""""""""" +.. autoclass:: UQpy.sampling.ThetaCriterionPCE + :members: run + + +Examples +""""""""""" + +.. toctree:: + + Theta Criterion Examples <../auto_examples/sampling/theta_criterion/index> diff --git a/src/UQpy/sampling/ThetaCriterionPCE.py b/src/UQpy/sampling/ThetaCriterionPCE.py index f91dda468..5a71fbd6f 100644 --- a/src/UQpy/sampling/ThetaCriterionPCE.py +++ b/src/UQpy/sampling/ThetaCriterionPCE.py @@ -10,7 +10,7 @@ class ThetaCriterionPCE: def __init__(self, surrogates: list[UQpy.surrogates.polynomial_chaos.PolynomialChaosExpansion]): """ Active learning for polynomial chaos expansion using Theta criterion balancing between exploration and - exploitation. + exploitation. :param surrogates: list of objects of the :py:meth:`UQpy` :class:`PolynomialChaosExpansion` class """ @@ -22,6 +22,7 @@ def run(self, existing_samples: np.ndarray, candidate_samples: np.ndarray, nsamp """ Execute the :class:`.ThetaCriterionPCE` active learning. + :param existing_samples: Samples in existing ED used for construction of PCEs. :param candidate_samples: Candidate samples for selecting by Theta criterion. :param samples_weights: Weights associated to X samples (e.g. from Coherence Sampling). @@ -43,7 +44,6 @@ def run(self, existing_samples: np.ndarray, candidate_samples: np.ndarray, nsamp npce = len(pces) nsimexisting, nvar = existing_samples.shape nsimcandidate, nvar = candidate_samples.shape - l = np.zeros(nsimcandidate) criterium = np.zeros(nsimcandidate) if samples_weights is None: samples_weights = np.ones(nsimexisting) @@ -56,31 +56,29 @@ def run(self, existing_samples: np.ndarray, candidate_samples: np.ndarray, nsamp pos = [] - for n in range(nsamples): + for _ in range(nsamples): S = polynomial_chaos.Polynomials.standardize_sample(existing_samples, pces[0].polynomial_basis.distributions) - Scandidate = polynomial_chaos.Polynomials.standardize_sample(candidate_samples, + s_candidate = polynomial_chaos.Polynomials.standardize_sample(candidate_samples, pces[0].polynomial_basis.distributions) - lengths = cdist(Scandidate, S) - closestS_pos = np.argmin(lengths, axis=1) - closest_valueX = existing_samples[closestS_pos] + lengths = cdist(s_candidate, S) + closest_s_position = np.argmin(lengths, axis=1) + closest_value_x = existing_samples[closest_s_position] l = np.nanmin(lengths, axis=1) variance_candidate = 0 variance_closest = 0 for i in range(npce): - variance_candidatei = 0 - variance_closesti = 0 pce = pces[i] variance_candidatei = self._local_variance(candidate_samples, pce, candidate_weights) - variance_closesti = self._local_variance(closest_valueX, pce, samples_weights[closestS_pos]) + variance_closesti = self._local_variance(closest_value_x, pce, samples_weights[closest_s_position]) variance_candidate = variance_candidate + variance_candidatei * pce_weights[i] variance_closest = variance_closest + variance_closesti * pce_weights[i] - criteriumV = np.sqrt(variance_candidate * variance_closest) - criteriumL = l ** nvar - criterium = criteriumV * criteriumL + criterium_v = np.sqrt(variance_candidate * variance_closest) + criterium_l = l ** nvar + criterium = criterium_v * criterium_l pos.append(np.argmax(criterium)) existing_samples = np.append(existing_samples, candidate_samples[pos, :], axis=0) samples_weights = np.append(samples_weights, candidate_weights[pos]) @@ -90,7 +88,7 @@ def run(self, existing_samples: np.ndarray, candidate_samples: np.ndarray, nsamp pos = pos[0] return pos else: - return variance_candidate, criteriumV, criteriumL, criterium + return variance_candidate, criterium_v, criterium_l, criterium # calculate variance density of PCE for Theta Criterion @staticmethod From 62845753ade9947526158d1a75d776f8f4c8e547 Mon Sep 17 00:00:00 2001 From: Lukas Novak <95358264+NovakLBUT@users.noreply.github.com> Date: Mon, 24 Apr 2023 18:04:39 +0200 Subject: [PATCH 38/41] Added one unit test for standardization --- tests/unit_tests/surrogates/test_pce.py | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/unit_tests/surrogates/test_pce.py b/tests/unit_tests/surrogates/test_pce.py index bd19ed653..3c85c1e1d 100644 --- a/tests/unit_tests/surrogates/test_pce.py +++ b/tests/unit_tests/surrogates/test_pce.py @@ -6,6 +6,7 @@ from UQpy.distributions import Uniform from UQpy.sensitivity.PceSensitivity import PceSensitivity from UQpy.surrogates import * +from scipy import stats as stats import numpy as np from UQpy.surrogates.polynomial_chaos.polynomials.TotalDegreeBasis import TotalDegreeBasis @@ -404,3 +405,35 @@ def test_22(): pos = ThetaSampling_complete.run(adapted_x, candidates_x, nsamples=2) best_candidates = candidates_x[pos, :] assert best_candidates[0, 0] == 1.1 and best_candidates[1, 0] == 9 + +def test_23(): + """ + Test Standardization of sample and associated PDF + """ + dist1 = Uniform(loc=0, scale=10) + dist2 = Normal(loc=6, scale=2) + marg = [dist1, dist2] + joint = JointIndependent(marginals=marg) + + polynomial_basis = TotalDegreeBasis(joint, max_degree) + least_squares = LeastSquareRegression() + pce = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) + X=np.zeros((3, 2)) + X[:, 1] = np.array([0, 6, 10]) + X[:, 0] = np.array([0, 5, 10]) + + pce.fit(X, X) + + standardized_samples=polynomial_chaos.Polynomials.standardize_sample(X,pce.polynomial_basis.distributions) + standardized_pdf=polynomial_chaos.Polynomials.standardize_pdf(X,pce.polynomial_basis.distributions) + + ref_sample1 = np.array([-1, 0, 1]) + ref_pdf1 = np.array([0.5, 0.5, 0.5]) + ref_sample2 = np.array([-3, 0, 2]) + ref_pdf2 = stats.norm.pdf(ref_sample2) + + ref_sample = np.zeros((3, 2)) + ref_sample[:, 0] = ref_sample1 + ref_sample[:, 1] = ref_sample2 + ref_pdf = ref_pdf1 * ref_pdf2 + assert (standardized_samples==ref_sample).all() and (standardized_pdf==ref_pdf).all() From e059b2e3a7c07aee7a0c1274a4f770acb458c479 Mon Sep 17 00:00:00 2001 From: Lukas Novak <95358264+NovakLBUT@users.noreply.github.com> Date: Mon, 24 Apr 2023 18:35:01 +0200 Subject: [PATCH 39/41] Update documentation of ThetaActiveLearning --- docs/source/sampling/theta_criterion.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/source/sampling/theta_criterion.rst b/docs/source/sampling/theta_criterion.rst index 0e24e7b88..1eae0b8bc 100644 --- a/docs/source/sampling/theta_criterion.rst +++ b/docs/source/sampling/theta_criterion.rst @@ -5,6 +5,15 @@ Theta Criterion ThetaCriterionPCE Class ^^^^^^^^^^^^^^^^^^^^^^^^ +The technique enables one-by-one extension of an experimental design while trying to obtain an optimal sample at each stage of the adaptive sequential surrogate model +construction process. The sequential sampling strategy based on :math:`\Theta` criterion selects from a pool of candidate points by trying to cover the design domain +proportionally to their local variance contribution. The proposed criterion for the sample selection balances both exploitation of the surrogate model using variance +density derived analytically from Polynomial Chaos Expansion and exploration of the design domain. The active learning technique based on :math:`\Theta` criterion can be +combined with arbitrary sampling technique employed for construction of a pool of candidate points. More details can be found in: + +L. Novák, M. Vořechovský, V. Sadílek, M. D. Shields, *Variance-based adaptive sequential sampling for polynomial chaos expansion*, +637 Computer Methods in Applied Mechanics and Engineering 386 (2021) 114105. doi:10.1016/j.cma.2021.114105 + The :class:`.ThetaCriterionPCE` class is imported using the following command: >>> from UQpy.sampling.ThetaCriterionPCE import ThetaCriterionPCE From 36269b15d4593e97f9f459353fc99089c3dae9bc Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Mon, 24 Apr 2023 13:14:05 -0400 Subject: [PATCH 40/41] Updates standardization code for Uniform and Normal --- docs/source/sampling/theta_criterion.rst | 17 +++++++-------- .../polynomials/baseclass/Polynomials.py | 6 ++---- tests/unit_tests/surrogates/test_pce.py | 21 ++++++++++--------- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/docs/source/sampling/theta_criterion.rst b/docs/source/sampling/theta_criterion.rst index 1eae0b8bc..34d243a0e 100644 --- a/docs/source/sampling/theta_criterion.rst +++ b/docs/source/sampling/theta_criterion.rst @@ -1,19 +1,18 @@ Theta Criterion --------------- - - -ThetaCriterionPCE Class -^^^^^^^^^^^^^^^^^^^^^^^^ - The technique enables one-by-one extension of an experimental design while trying to obtain an optimal sample at each stage of the adaptive sequential surrogate model -construction process. The sequential sampling strategy based on :math:`\Theta` criterion selects from a pool of candidate points by trying to cover the design domain -proportionally to their local variance contribution. The proposed criterion for the sample selection balances both exploitation of the surrogate model using variance -density derived analytically from Polynomial Chaos Expansion and exploration of the design domain. The active learning technique based on :math:`\Theta` criterion can be +construction process. The sequential sampling strategy based on :math:`\Theta` criterion selects from a pool of candidate points by trying to cover the design domain +proportionally to their local variance contribution. The proposed criterion for the sample selection balances both exploitation of the surrogate model using variance +density derived analytically from Polynomial Chaos Expansion and exploration of the design domain. The active learning technique based on :math:`\Theta` criterion can be combined with arbitrary sampling technique employed for construction of a pool of candidate points. More details can be found in: -L. Novák, M. Vořechovský, V. Sadílek, M. D. Shields, *Variance-based adaptive sequential sampling for polynomial chaos expansion*, +L. Novák, M. Vořechovský, V. Sadílek, M. D. Shields, *Variance-based adaptive sequential sampling for polynomial chaos expansion*, 637 Computer Methods in Applied Mechanics and Engineering 386 (2021) 114105. doi:10.1016/j.cma.2021.114105 + +ThetaCriterionPCE Class +^^^^^^^^^^^^^^^^^^^^^^^^ + The :class:`.ThetaCriterionPCE` class is imported using the following command: >>> from UQpy.sampling.ThetaCriterionPCE import ThetaCriterionPCE diff --git a/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py b/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py index cb9e1c59b..2f60144a2 100644 --- a/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py +++ b/src/UQpy/surrogates/polynomial_chaos/polynomials/baseclass/Polynomials.py @@ -47,10 +47,8 @@ def standardize_sample(x, joint_distribution): if type(marginals[i]) == Normal: s[:, i] = Polynomials.standardize_normal(x[:, i], mean=marginals[i].parameters['loc'], std=marginals[i].parameters['scale']) - - if type(marginals[i]) == Uniform: + elif type(marginals[i]) == Uniform: s[:, i] = Polynomials.standardize_uniform(x[:, i], marginals[i]) - else: raise TypeError("standarize_sample is defined only for Uniform and Gaussian marginal distributions") return s @@ -77,7 +75,7 @@ def standardize_pdf(x, joint_distribution): for i in range(inputs_number): if type(marginals[i]) == Normal: pdf_val *= (stats.norm.pdf(s[:, i])) - if type(marginals[i]) == Uniform: + elif type(marginals[i]) == Uniform: pdf_val *= (stats.uniform.pdf(s[:, i], loc=-1, scale=2)) else: raise TypeError("standardize_pdf is defined only for Uniform and Gaussian marginal distributions") diff --git a/tests/unit_tests/surrogates/test_pce.py b/tests/unit_tests/surrogates/test_pce.py index 3c85c1e1d..c01d32496 100644 --- a/tests/unit_tests/surrogates/test_pce.py +++ b/tests/unit_tests/surrogates/test_pce.py @@ -405,12 +405,13 @@ def test_22(): pos = ThetaSampling_complete.run(adapted_x, candidates_x, nsamples=2) best_candidates = candidates_x[pos, :] assert best_candidates[0, 0] == 1.1 and best_candidates[1, 0] == 9 - + + def test_23(): """ Test Standardization of sample and associated PDF """ - dist1 = Uniform(loc=0, scale=10) + dist1 = Uniform(loc=0, scale=10) dist2 = Normal(loc=6, scale=2) marg = [dist1, dist2] joint = JointIndependent(marginals=marg) @@ -418,22 +419,22 @@ def test_23(): polynomial_basis = TotalDegreeBasis(joint, max_degree) least_squares = LeastSquareRegression() pce = PolynomialChaosExpansion(polynomial_basis=polynomial_basis, regression_method=least_squares) - X=np.zeros((3, 2)) + X = np.zeros((3, 2)) X[:, 1] = np.array([0, 6, 10]) X[:, 0] = np.array([0, 5, 10]) - + pce.fit(X, X) - - standardized_samples=polynomial_chaos.Polynomials.standardize_sample(X,pce.polynomial_basis.distributions) - standardized_pdf=polynomial_chaos.Polynomials.standardize_pdf(X,pce.polynomial_basis.distributions) - + + standardized_samples = polynomial_chaos.Polynomials.standardize_sample(X, pce.polynomial_basis.distributions) + standardized_pdf = polynomial_chaos.Polynomials.standardize_pdf(X, pce.polynomial_basis.distributions) + ref_sample1 = np.array([-1, 0, 1]) ref_pdf1 = np.array([0.5, 0.5, 0.5]) ref_sample2 = np.array([-3, 0, 2]) ref_pdf2 = stats.norm.pdf(ref_sample2) - + ref_sample = np.zeros((3, 2)) ref_sample[:, 0] = ref_sample1 ref_sample[:, 1] = ref_sample2 ref_pdf = ref_pdf1 * ref_pdf2 - assert (standardized_samples==ref_sample).all() and (standardized_pdf==ref_pdf).all() + assert (standardized_samples == ref_sample).all() and (standardized_pdf == ref_pdf).all() From 4724b58d563a6c131236ecd2443b6a05aea2054b Mon Sep 17 00:00:00 2001 From: Dimitris Tsapetis Date: Fri, 28 Apr 2023 13:01:10 -0400 Subject: [PATCH 41/41] +semver: minor --- GitVersion.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/GitVersion.yml b/GitVersion.yml index 44a3b9c9c..47e0f3c43 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -7,3 +7,4 @@ branches: {} ignore: sha: [] merge-message-formats: {} +