easyvvuq.sampling.stochastic_collocation
1from .base import BaseSamplingElement, Vary 2import chaospy as cp 3import numpy as np 4import pickle 5from itertools import product, chain 6import logging 7 8__author__ = "Wouter Edeling" 9__copyright__ = """ 10 11 Copyright 2018 Robin A. Richardson, David W. Wright 12 13 This file is part of EasyVVUQ 14 15 EasyVVUQ is free software: you can redistribute it and/or modify 16 it under the terms of the Lesser GNU General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version. 19 20 EasyVVUQ is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 Lesser GNU General Public License for more details. 24 25 You should have received a copy of the Lesser GNU General Public License 26 along with this program. If not, see <https://www.gnu.org/licenses/>. 27 28""" 29__license__ = "LGPL" 30 31 32class SCSampler(BaseSamplingElement, sampler_name="sc_sampler"): 33 """ 34 Stochastic Collocation sampler 35 """ 36 37 def __init__(self, 38 vary=None, 39 polynomial_order=4, 40 quadrature_rule="G", 41 count=0, 42 growth=False, 43 sparse=False, 44 midpoint_level1=True, 45 dimension_adaptive=False): 46 """ 47 Create the sampler for the Stochastic Collocation method. 48 49 Parameters 50 ---------- 51 vary: dict or None 52 keys = parameters to be sampled, values = distributions. 53 54 polynomial_order : int, optional 55 The polynomial order, default is 4. 56 57 quadrature_rule : char, optional 58 The quadrature method, default is Gaussian "G". 59 60 growth: bool, optional 61 Sets the growth rule to exponential for Clenshaw Curtis quadrature, 62 which makes it nested, and therefore more efficient for sparse grids. 63 Default is False. 64 65 sparse : bool, optional 66 If True use sparse grid instead of normal tensor product grid, 67 default is False. 68 69 midpoint_level1 : bool, optional 70 Used only for sparse=True (or for cp.DiscreteUniform distribution). 71 Determines how many points the 1st level of a sparse grid will have. 72 If True, order 0 quadrature will be generated, 73 default is True. 74 75 dimension_adaptive : bool, optional 76 Determines wether to use an insotropic sparse grid, or to 77 adapt the levels in the sparse grid based on a hierachical 78 error measure, default is False. 79 """ 80 81 self.vary = Vary(vary) 82 83 # List of the probability distributions of uncertain parameters 84 self.params_distribution = list(self.vary.get_values()) 85 # N = number of uncertain parameters 86 self.N = len(self.params_distribution) 87 88 logging.debug("param dist {}".format(self.params_distribution)) 89 90 91 self.joint_dist = cp.J(*self.params_distribution) 92 93 # The quadrature information: order, rule and sparsity 94 if isinstance(polynomial_order, int): 95 logging.debug('Received integer polynomial order, assuming isotropic grid') 96 self.polynomial_order = [polynomial_order for i in range(self.N)] 97 else: 98 self.polynomial_order = polynomial_order 99 100 self.quad_rule = quadrature_rule 101 self.sparse = sparse 102 self.midpoint_level1 = midpoint_level1 103 self.dimension_adaptive = dimension_adaptive 104 self.nadaptations = 0 105 self.growth = growth 106 self.check_max_quad_level() 107 108 # determine if a nested sparse grid is used 109 if self.sparse is True and self.growth is True and \ 110 (self.quad_rule == "C" or self.quad_rule == "clenshaw_curtis"): 111 self.nested = True 112 elif self.sparse is True and self.growth is False and self.quad_rule == "gauss_patterson": 113 self.nested = True 114 elif self.sparse is True and self.growth is True and self.quad_rule == "newton_cotes": 115 self.nested = True 116 else: 117 self.nested = False 118 119 # L = level of (sparse) grid 120 self.L = np.max(self.polynomial_order) 121 122 # compute the 1D collocation points (and quad weights) 123 self.compute_1D_points_weights(self.L, self.N) 124 125 # compute N-dimensional collocation points 126 if not self.sparse: 127 128 # generate collocation as a standard tensor product 129 l_norm = np.array([self.polynomial_order]) 130 self.xi_d = self.generate_grid(l_norm) 131 132 else: 133 self.l_norm = self.compute_sparse_multi_idx(self.L, self.N) 134 # create sparse grid of dimension N and level q using the 1d 135 # rules in self.xi_1d 136 self.xi_d = self.generate_grid(self.l_norm) 137 138 self._n_samples = self.xi_d.shape[0] 139 140 self.count = 0 141 142 @property 143 def analysis_class(self): 144 """Return a corresponding analysis class. 145 """ 146 from easyvvuq.analysis import SCAnalysis 147 return SCAnalysis 148 149 def compute_1D_points_weights(self, L, N): 150 """ 151 Computes 1D collocation points and quad weights, 152 and stores this in self.xi_1d, self.wi_1d. 153 154 Parameters 155 ---------- 156 L : (int) the max polynomial order of the (sparse) grid 157 N : (int) the number of uncertain parameters 158 159 Returns 160 ------- 161 None. 162 163 """ 164 # for every dimension (parameter), create a hierachy of 1D 165 # quadrature rules of increasing order 166 self.xi_1d = [{} for n in range(N)] 167 self.wi_1d = [{} for n in range(N)] 168 169 if self.sparse: 170 171 # if level one of the sparse grid is a midpoint rule, generate 172 # the quadrature with order 0 (1 quad point). Else set order at 173 # level 1 to 1 174 if self.midpoint_level1: 175 j = 0 176 else: 177 j = 1 178 179 for n in range(N): 180 # check if input is discrete uniform, in which case the 181 # rule and growth flag must be modified 182 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 183 rule = "discrete" 184 else: 185 rule = self.quad_rule 186 187 for i in range(L): 188 xi_i, wi_i = cp.generate_quadrature(i + j, 189 self.params_distribution[n], 190 rule=rule, 191 growth=self.growth) 192 self.xi_1d[n][i + 1] = xi_i[0] 193 self.wi_1d[n][i + 1] = wi_i 194 else: 195 for n in range(N): 196 # check if input is discrete uniform, in which case the 197 # rule flag must be modified 198 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 199 rule = "discrete" 200 else: 201 rule = self.quad_rule 202 203 xi_i, wi_i = cp.generate_quadrature(self.polynomial_order[n], 204 self.params_distribution[n], 205 rule=rule, 206 growth=self.growth) 207 208 self.xi_1d[n][self.polynomial_order[n]] = xi_i[0] 209 self.wi_1d[n][self.polynomial_order[n]] = wi_i 210 211 def check_max_quad_level(self): 212 """ 213 214 If a discrete variable is specified, there is the possibility of 215 non unique collocation points if the quadrature order is high enough. 216 This subroutine prevents that. 217 218 NOTE: Only detects cp.DiscreteUniform thus far 219 220 The max quad orders are stores in self.max_quad_order 221 222 Returns 223 ------- 224 None 225 226 """ 227 # assume no maximum by default 228 self.max_level = np.ones(self.N) * 1000 229 for n in range(self.N): 230 231 # if a discrete uniform is specified check max order 232 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 233 234 #TODO: it is assumed that self.sparse=True, but this assumption 235 #does not have to hold here!!! 236 237 # if level one of the sparse grid is a midpoint rule, generate 238 # the quadrature with order 0 (1 quad point). Else set order at 239 # level 1 to 1 240 if self.midpoint_level1: 241 j = 0 242 else: 243 j = 1 244 245 number_of_points = 0 246 for order in range(1000): 247 xi_i, wi_i = cp.generate_quadrature(order + j, 248 self.params_distribution[n], 249 growth=self.growth) 250 # if the quadrature points no longer grow with the quad order, 251 # then the max order has been reached 252 if xi_i.size == number_of_points: 253 break 254 number_of_points = xi_i.size 255 256 logging.debug("Input %d is discrete, setting max quadrature order to %d" 257 % (n, order - 1)) 258 # level 1 = order 0 etc 259 self.max_level[n] = order 260 261 def next_level_sparse_grid(self): 262 """ 263 Adds the points of the next level for isotropic hierarchical sparse grids. 264 265 Returns 266 ------- 267 None. 268 269 """ 270 271 if self.nested is False: 272 print('Only works for nested sparse grids') 273 return 274 275 self.look_ahead(self.l_norm) 276 self.l_norm = np.append(self.l_norm, self.admissible_idx, axis=0) 277 278 def look_ahead(self, current_multi_idx): 279 """ 280 The look-ahead step in (dimension-adaptive) sparse grid sampling. Allows 281 for anisotropic sampling plans. 282 283 Computes the admissible forward neighbors with respect to the current level 284 multi-indices. The admissible level indices l are added to self.admissible_idx. 285 The code will be evaluated next iteration at the new collocation points 286 corresponding to the levels in admissble_idx. 287 288 Source: Gerstner, Griebel, "Numerical integration using sparse grids" 289 290 Parameters 291 ---------- 292 current_multi_idx : array of the levels in the current iteration 293 of the sparse grid. 294 295 Returns 296 ------- 297 None. 298 299 """ 300 301 # compute all forward neighbors for every l in current_multi_idx 302 forward_neighbor = [] 303 e_n = np.eye(self.N, dtype=int) 304 for l in current_multi_idx: 305 for n in range(self.N): 306 # the forward neighbor is l plus a unit vector 307 forward_neighbor.append(l + e_n[n]) 308 309 # remove duplicates 310 forward_neighbor = np.unique(np.array(forward_neighbor), axis=0) 311 # remove those which are already in the grid 312 forward_neighbor = setdiff2d(forward_neighbor, current_multi_idx) 313 # make sure the final candidates are admissible (all backward neighbors 314 # must be in the current multi indices) 315 logging.debug('Computing admissible levels...') 316 admissible_idx = [] 317 for l in forward_neighbor: 318 admissible = True 319 for n in range(self.N): 320 backward_neighbor = l - e_n[n] 321 # find backward_neighbor in current_multi_idx 322 idx = np.where((backward_neighbor == current_multi_idx).all(axis=1))[0] 323 # if backward neighbor is not in the current index set 324 # and it is 'on the interior' (contains no 0): not admissible 325 if idx.size == 0 and backward_neighbor[n] != 0: 326 admissible = False 327 break 328 # if all backward neighbors are in the current index set: l is admissible 329 if admissible: 330 admissible_idx.append(l) 331 logging.debug('done') 332 333 self.admissible_idx = np.array(admissible_idx) 334 # make sure that all entries of each index are <= the max quadrature order 335 # The max quad order can be low for discrete input variables 336 idx = np.where((self.admissible_idx <= self.max_level).all(axis=1))[0] 337 self.admissible_idx = self.admissible_idx[idx] 338 logging.debug('Admissible multi-indices:\n%s', self.admissible_idx) 339 340 # determine the maximum level L of the new index set L = |l| - N + 1 341 # self.L = np.max(np.sum(self.admissible_idx, axis=1) - self.N + 1) 342 self.L = np.max(self.admissible_idx) 343 # recompute the 1D weights and collocation points 344 self.compute_1D_points_weights(self.L, self.N) 345 # compute collocation grid based on the admissible level indices 346 admissible_grid = self.generate_grid(self.admissible_idx) 347 # remove collocation points which have already been computed 348 if not hasattr(self, 'xi_d'): 349 self.xi_d = self.generate_grid(self.admissible_idx) 350 self._n_samples = self.xi_d.shape[0] 351 new_points = setdiff2d(admissible_grid, self.xi_d) 352 353 logging.debug('%d new points added' % new_points.shape[0]) 354 355 # keep track of the number of points added per iteration 356 if not hasattr(self, 'n_new_points'): 357 self.n_new_points = [] 358 self.n_new_points.append(new_points.shape[0]) 359 360 # update the number of samples 361 self._n_samples += new_points.shape[0] 362 363 # update the N-dimensional sparse grid if unsampled points are added 364 if new_points.shape[0] > 0: 365 self.xi_d = np.concatenate((self.xi_d, new_points)) 366 367 # count the number of times the dimensions were adapted 368 self.nadaptations += 1 369 370 def is_finite(self): 371 return True 372 373 @property 374 def n_samples(self): 375 """ 376 Number of samples (Ns) of SC method. 377 - When using tensor quadrature: Ns = (p + 1)**d 378 - When using sparid: Ns = bigO((p + 1)*log(p + 1)**(d-1)) 379 Where: p is the polynomial degree and d is the number of 380 uncertain parameters. 381 382 Ref: Eck et al. 'A guide to uncertainty quantification and 383 sensitivity analysis for cardiovascular applications' [2016]. 384 """ 385 return self._n_samples 386 387 # SC collocations points are not random, generate_runs simply returns 388 # one collocation point from the tensor product after the other 389 def __next__(self): 390 if self.count < self._n_samples: 391 run_dict = {} 392 i_par = 0 393 for param_name in self.vary.get_keys(): 394 # the current input parameter 395 current_param = self.xi_d[self.count][i_par] 396 # all parameters self.xi_d will store floats. If current param is 397 # DiscreteUniform, convert e.g. 2.0 to 2 before running 398 # the simulation. 399 if isinstance(self.params_distribution[i_par], cp.DiscreteUniform): 400 current_param = int(current_param) 401 run_dict[param_name] = current_param 402 i_par += 1 403 self.count += 1 404 return run_dict 405 else: 406 raise StopIteration 407 408 def save_state(self, filename): 409 logging.debug("Saving sampler state to %s" % filename) 410 file = open(filename, 'wb') 411 pickle.dump(self.__dict__, file) 412 file.close() 413 414 def load_state(self, filename): 415 logging.debug("Loading sampler state from %s" % filename) 416 file = open(filename, 'rb') 417 self.__dict__ = pickle.load(file) 418 file.close() 419 420 """ 421 ========================= 422 (SPARSE) GRID SUBROUTINES 423 ========================= 424 """ 425 426 def generate_grid(self, l_norm): 427 428 dimensions = range(self.N) 429 H_L_N = [] 430 # loop over all multi indices i 431 for l in l_norm: 432 # compute the tensor product of nodes indexed by i 433 X_l = [self.xi_1d[n][l[n]] for n in dimensions] 434 H_L_N.append(list(product(*X_l))) 435 # flatten the list of lists 436 H_L_N = np.array(list(chain(*H_L_N))) 437 # return unique nodes 438 return np.unique(H_L_N, axis=0) 439 440 # L : (int) max polynomial order 441 # N : (int) the number of uncertain parameters 442 def compute_sparse_multi_idx(self, L, N): 443 """ 444 computes all N dimensional multi-indices l = (l1,...,lN) such that 445 |l| <= L + N - 1, i.e. a simplex set: 446 3 * 447 2 * * (L=3 and N=2) 448 1 * * * 449 1 2 3 450 Here |l| is the internal sum of i (l1+...+lN) 451 """ 452 # old implementation: does not scale well 453 # P = np.array(list(product(range(1, L + 1), repeat=N))) 454 # multi_idx = P[np.where(np.sum(P, axis=1) <= L + N - 1)[0]] 455 456 # use the look_ahead subroutine to build an isotropic sparse grid (faster) 457 multi_idx = np.array([np.ones(self.N, dtype='int')]) 458 for l in range(self.L - 1): 459 self.look_ahead(multi_idx) 460 # accept all admissible indices to build an isotropic grid 461 multi_idx = np.append(multi_idx, self.admissible_idx, axis=0) 462 463 return multi_idx 464 465 466def setdiff2d(X, Y): 467 """ 468 Computes the difference of two 2D arrays X and Y 469 470 Parameters 471 ---------- 472 X : 2D numpy array 473 Y : 2D numpy array 474 475 Returns 476 ------- 477 The difference X \\ Y as a 2D array 478 479 """ 480 diff = set(map(tuple, X)) - set(map(tuple, Y)) 481 return np.array(list(diff))
33class SCSampler(BaseSamplingElement, sampler_name="sc_sampler"): 34 """ 35 Stochastic Collocation sampler 36 """ 37 38 def __init__(self, 39 vary=None, 40 polynomial_order=4, 41 quadrature_rule="G", 42 count=0, 43 growth=False, 44 sparse=False, 45 midpoint_level1=True, 46 dimension_adaptive=False): 47 """ 48 Create the sampler for the Stochastic Collocation method. 49 50 Parameters 51 ---------- 52 vary: dict or None 53 keys = parameters to be sampled, values = distributions. 54 55 polynomial_order : int, optional 56 The polynomial order, default is 4. 57 58 quadrature_rule : char, optional 59 The quadrature method, default is Gaussian "G". 60 61 growth: bool, optional 62 Sets the growth rule to exponential for Clenshaw Curtis quadrature, 63 which makes it nested, and therefore more efficient for sparse grids. 64 Default is False. 65 66 sparse : bool, optional 67 If True use sparse grid instead of normal tensor product grid, 68 default is False. 69 70 midpoint_level1 : bool, optional 71 Used only for sparse=True (or for cp.DiscreteUniform distribution). 72 Determines how many points the 1st level of a sparse grid will have. 73 If True, order 0 quadrature will be generated, 74 default is True. 75 76 dimension_adaptive : bool, optional 77 Determines wether to use an insotropic sparse grid, or to 78 adapt the levels in the sparse grid based on a hierachical 79 error measure, default is False. 80 """ 81 82 self.vary = Vary(vary) 83 84 # List of the probability distributions of uncertain parameters 85 self.params_distribution = list(self.vary.get_values()) 86 # N = number of uncertain parameters 87 self.N = len(self.params_distribution) 88 89 logging.debug("param dist {}".format(self.params_distribution)) 90 91 92 self.joint_dist = cp.J(*self.params_distribution) 93 94 # The quadrature information: order, rule and sparsity 95 if isinstance(polynomial_order, int): 96 logging.debug('Received integer polynomial order, assuming isotropic grid') 97 self.polynomial_order = [polynomial_order for i in range(self.N)] 98 else: 99 self.polynomial_order = polynomial_order 100 101 self.quad_rule = quadrature_rule 102 self.sparse = sparse 103 self.midpoint_level1 = midpoint_level1 104 self.dimension_adaptive = dimension_adaptive 105 self.nadaptations = 0 106 self.growth = growth 107 self.check_max_quad_level() 108 109 # determine if a nested sparse grid is used 110 if self.sparse is True and self.growth is True and \ 111 (self.quad_rule == "C" or self.quad_rule == "clenshaw_curtis"): 112 self.nested = True 113 elif self.sparse is True and self.growth is False and self.quad_rule == "gauss_patterson": 114 self.nested = True 115 elif self.sparse is True and self.growth is True and self.quad_rule == "newton_cotes": 116 self.nested = True 117 else: 118 self.nested = False 119 120 # L = level of (sparse) grid 121 self.L = np.max(self.polynomial_order) 122 123 # compute the 1D collocation points (and quad weights) 124 self.compute_1D_points_weights(self.L, self.N) 125 126 # compute N-dimensional collocation points 127 if not self.sparse: 128 129 # generate collocation as a standard tensor product 130 l_norm = np.array([self.polynomial_order]) 131 self.xi_d = self.generate_grid(l_norm) 132 133 else: 134 self.l_norm = self.compute_sparse_multi_idx(self.L, self.N) 135 # create sparse grid of dimension N and level q using the 1d 136 # rules in self.xi_1d 137 self.xi_d = self.generate_grid(self.l_norm) 138 139 self._n_samples = self.xi_d.shape[0] 140 141 self.count = 0 142 143 @property 144 def analysis_class(self): 145 """Return a corresponding analysis class. 146 """ 147 from easyvvuq.analysis import SCAnalysis 148 return SCAnalysis 149 150 def compute_1D_points_weights(self, L, N): 151 """ 152 Computes 1D collocation points and quad weights, 153 and stores this in self.xi_1d, self.wi_1d. 154 155 Parameters 156 ---------- 157 L : (int) the max polynomial order of the (sparse) grid 158 N : (int) the number of uncertain parameters 159 160 Returns 161 ------- 162 None. 163 164 """ 165 # for every dimension (parameter), create a hierachy of 1D 166 # quadrature rules of increasing order 167 self.xi_1d = [{} for n in range(N)] 168 self.wi_1d = [{} for n in range(N)] 169 170 if self.sparse: 171 172 # if level one of the sparse grid is a midpoint rule, generate 173 # the quadrature with order 0 (1 quad point). Else set order at 174 # level 1 to 1 175 if self.midpoint_level1: 176 j = 0 177 else: 178 j = 1 179 180 for n in range(N): 181 # check if input is discrete uniform, in which case the 182 # rule and growth flag must be modified 183 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 184 rule = "discrete" 185 else: 186 rule = self.quad_rule 187 188 for i in range(L): 189 xi_i, wi_i = cp.generate_quadrature(i + j, 190 self.params_distribution[n], 191 rule=rule, 192 growth=self.growth) 193 self.xi_1d[n][i + 1] = xi_i[0] 194 self.wi_1d[n][i + 1] = wi_i 195 else: 196 for n in range(N): 197 # check if input is discrete uniform, in which case the 198 # rule flag must be modified 199 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 200 rule = "discrete" 201 else: 202 rule = self.quad_rule 203 204 xi_i, wi_i = cp.generate_quadrature(self.polynomial_order[n], 205 self.params_distribution[n], 206 rule=rule, 207 growth=self.growth) 208 209 self.xi_1d[n][self.polynomial_order[n]] = xi_i[0] 210 self.wi_1d[n][self.polynomial_order[n]] = wi_i 211 212 def check_max_quad_level(self): 213 """ 214 215 If a discrete variable is specified, there is the possibility of 216 non unique collocation points if the quadrature order is high enough. 217 This subroutine prevents that. 218 219 NOTE: Only detects cp.DiscreteUniform thus far 220 221 The max quad orders are stores in self.max_quad_order 222 223 Returns 224 ------- 225 None 226 227 """ 228 # assume no maximum by default 229 self.max_level = np.ones(self.N) * 1000 230 for n in range(self.N): 231 232 # if a discrete uniform is specified check max order 233 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 234 235 #TODO: it is assumed that self.sparse=True, but this assumption 236 #does not have to hold here!!! 237 238 # if level one of the sparse grid is a midpoint rule, generate 239 # the quadrature with order 0 (1 quad point). Else set order at 240 # level 1 to 1 241 if self.midpoint_level1: 242 j = 0 243 else: 244 j = 1 245 246 number_of_points = 0 247 for order in range(1000): 248 xi_i, wi_i = cp.generate_quadrature(order + j, 249 self.params_distribution[n], 250 growth=self.growth) 251 # if the quadrature points no longer grow with the quad order, 252 # then the max order has been reached 253 if xi_i.size == number_of_points: 254 break 255 number_of_points = xi_i.size 256 257 logging.debug("Input %d is discrete, setting max quadrature order to %d" 258 % (n, order - 1)) 259 # level 1 = order 0 etc 260 self.max_level[n] = order 261 262 def next_level_sparse_grid(self): 263 """ 264 Adds the points of the next level for isotropic hierarchical sparse grids. 265 266 Returns 267 ------- 268 None. 269 270 """ 271 272 if self.nested is False: 273 print('Only works for nested sparse grids') 274 return 275 276 self.look_ahead(self.l_norm) 277 self.l_norm = np.append(self.l_norm, self.admissible_idx, axis=0) 278 279 def look_ahead(self, current_multi_idx): 280 """ 281 The look-ahead step in (dimension-adaptive) sparse grid sampling. Allows 282 for anisotropic sampling plans. 283 284 Computes the admissible forward neighbors with respect to the current level 285 multi-indices. The admissible level indices l are added to self.admissible_idx. 286 The code will be evaluated next iteration at the new collocation points 287 corresponding to the levels in admissble_idx. 288 289 Source: Gerstner, Griebel, "Numerical integration using sparse grids" 290 291 Parameters 292 ---------- 293 current_multi_idx : array of the levels in the current iteration 294 of the sparse grid. 295 296 Returns 297 ------- 298 None. 299 300 """ 301 302 # compute all forward neighbors for every l in current_multi_idx 303 forward_neighbor = [] 304 e_n = np.eye(self.N, dtype=int) 305 for l in current_multi_idx: 306 for n in range(self.N): 307 # the forward neighbor is l plus a unit vector 308 forward_neighbor.append(l + e_n[n]) 309 310 # remove duplicates 311 forward_neighbor = np.unique(np.array(forward_neighbor), axis=0) 312 # remove those which are already in the grid 313 forward_neighbor = setdiff2d(forward_neighbor, current_multi_idx) 314 # make sure the final candidates are admissible (all backward neighbors 315 # must be in the current multi indices) 316 logging.debug('Computing admissible levels...') 317 admissible_idx = [] 318 for l in forward_neighbor: 319 admissible = True 320 for n in range(self.N): 321 backward_neighbor = l - e_n[n] 322 # find backward_neighbor in current_multi_idx 323 idx = np.where((backward_neighbor == current_multi_idx).all(axis=1))[0] 324 # if backward neighbor is not in the current index set 325 # and it is 'on the interior' (contains no 0): not admissible 326 if idx.size == 0 and backward_neighbor[n] != 0: 327 admissible = False 328 break 329 # if all backward neighbors are in the current index set: l is admissible 330 if admissible: 331 admissible_idx.append(l) 332 logging.debug('done') 333 334 self.admissible_idx = np.array(admissible_idx) 335 # make sure that all entries of each index are <= the max quadrature order 336 # The max quad order can be low for discrete input variables 337 idx = np.where((self.admissible_idx <= self.max_level).all(axis=1))[0] 338 self.admissible_idx = self.admissible_idx[idx] 339 logging.debug('Admissible multi-indices:\n%s', self.admissible_idx) 340 341 # determine the maximum level L of the new index set L = |l| - N + 1 342 # self.L = np.max(np.sum(self.admissible_idx, axis=1) - self.N + 1) 343 self.L = np.max(self.admissible_idx) 344 # recompute the 1D weights and collocation points 345 self.compute_1D_points_weights(self.L, self.N) 346 # compute collocation grid based on the admissible level indices 347 admissible_grid = self.generate_grid(self.admissible_idx) 348 # remove collocation points which have already been computed 349 if not hasattr(self, 'xi_d'): 350 self.xi_d = self.generate_grid(self.admissible_idx) 351 self._n_samples = self.xi_d.shape[0] 352 new_points = setdiff2d(admissible_grid, self.xi_d) 353 354 logging.debug('%d new points added' % new_points.shape[0]) 355 356 # keep track of the number of points added per iteration 357 if not hasattr(self, 'n_new_points'): 358 self.n_new_points = [] 359 self.n_new_points.append(new_points.shape[0]) 360 361 # update the number of samples 362 self._n_samples += new_points.shape[0] 363 364 # update the N-dimensional sparse grid if unsampled points are added 365 if new_points.shape[0] > 0: 366 self.xi_d = np.concatenate((self.xi_d, new_points)) 367 368 # count the number of times the dimensions were adapted 369 self.nadaptations += 1 370 371 def is_finite(self): 372 return True 373 374 @property 375 def n_samples(self): 376 """ 377 Number of samples (Ns) of SC method. 378 - When using tensor quadrature: Ns = (p + 1)**d 379 - When using sparid: Ns = bigO((p + 1)*log(p + 1)**(d-1)) 380 Where: p is the polynomial degree and d is the number of 381 uncertain parameters. 382 383 Ref: Eck et al. 'A guide to uncertainty quantification and 384 sensitivity analysis for cardiovascular applications' [2016]. 385 """ 386 return self._n_samples 387 388 # SC collocations points are not random, generate_runs simply returns 389 # one collocation point from the tensor product after the other 390 def __next__(self): 391 if self.count < self._n_samples: 392 run_dict = {} 393 i_par = 0 394 for param_name in self.vary.get_keys(): 395 # the current input parameter 396 current_param = self.xi_d[self.count][i_par] 397 # all parameters self.xi_d will store floats. If current param is 398 # DiscreteUniform, convert e.g. 2.0 to 2 before running 399 # the simulation. 400 if isinstance(self.params_distribution[i_par], cp.DiscreteUniform): 401 current_param = int(current_param) 402 run_dict[param_name] = current_param 403 i_par += 1 404 self.count += 1 405 return run_dict 406 else: 407 raise StopIteration 408 409 def save_state(self, filename): 410 logging.debug("Saving sampler state to %s" % filename) 411 file = open(filename, 'wb') 412 pickle.dump(self.__dict__, file) 413 file.close() 414 415 def load_state(self, filename): 416 logging.debug("Loading sampler state from %s" % filename) 417 file = open(filename, 'rb') 418 self.__dict__ = pickle.load(file) 419 file.close() 420 421 """ 422 ========================= 423 (SPARSE) GRID SUBROUTINES 424 ========================= 425 """ 426 427 def generate_grid(self, l_norm): 428 429 dimensions = range(self.N) 430 H_L_N = [] 431 # loop over all multi indices i 432 for l in l_norm: 433 # compute the tensor product of nodes indexed by i 434 X_l = [self.xi_1d[n][l[n]] for n in dimensions] 435 H_L_N.append(list(product(*X_l))) 436 # flatten the list of lists 437 H_L_N = np.array(list(chain(*H_L_N))) 438 # return unique nodes 439 return np.unique(H_L_N, axis=0) 440 441 # L : (int) max polynomial order 442 # N : (int) the number of uncertain parameters 443 def compute_sparse_multi_idx(self, L, N): 444 """ 445 computes all N dimensional multi-indices l = (l1,...,lN) such that 446 |l| <= L + N - 1, i.e. a simplex set: 447 3 * 448 2 * * (L=3 and N=2) 449 1 * * * 450 1 2 3 451 Here |l| is the internal sum of i (l1+...+lN) 452 """ 453 # old implementation: does not scale well 454 # P = np.array(list(product(range(1, L + 1), repeat=N))) 455 # multi_idx = P[np.where(np.sum(P, axis=1) <= L + N - 1)[0]] 456 457 # use the look_ahead subroutine to build an isotropic sparse grid (faster) 458 multi_idx = np.array([np.ones(self.N, dtype='int')]) 459 for l in range(self.L - 1): 460 self.look_ahead(multi_idx) 461 # accept all admissible indices to build an isotropic grid 462 multi_idx = np.append(multi_idx, self.admissible_idx, axis=0) 463 464 return multi_idx
Stochastic Collocation sampler
38 def __init__(self, 39 vary=None, 40 polynomial_order=4, 41 quadrature_rule="G", 42 count=0, 43 growth=False, 44 sparse=False, 45 midpoint_level1=True, 46 dimension_adaptive=False): 47 """ 48 Create the sampler for the Stochastic Collocation method. 49 50 Parameters 51 ---------- 52 vary: dict or None 53 keys = parameters to be sampled, values = distributions. 54 55 polynomial_order : int, optional 56 The polynomial order, default is 4. 57 58 quadrature_rule : char, optional 59 The quadrature method, default is Gaussian "G". 60 61 growth: bool, optional 62 Sets the growth rule to exponential for Clenshaw Curtis quadrature, 63 which makes it nested, and therefore more efficient for sparse grids. 64 Default is False. 65 66 sparse : bool, optional 67 If True use sparse grid instead of normal tensor product grid, 68 default is False. 69 70 midpoint_level1 : bool, optional 71 Used only for sparse=True (or for cp.DiscreteUniform distribution). 72 Determines how many points the 1st level of a sparse grid will have. 73 If True, order 0 quadrature will be generated, 74 default is True. 75 76 dimension_adaptive : bool, optional 77 Determines wether to use an insotropic sparse grid, or to 78 adapt the levels in the sparse grid based on a hierachical 79 error measure, default is False. 80 """ 81 82 self.vary = Vary(vary) 83 84 # List of the probability distributions of uncertain parameters 85 self.params_distribution = list(self.vary.get_values()) 86 # N = number of uncertain parameters 87 self.N = len(self.params_distribution) 88 89 logging.debug("param dist {}".format(self.params_distribution)) 90 91 92 self.joint_dist = cp.J(*self.params_distribution) 93 94 # The quadrature information: order, rule and sparsity 95 if isinstance(polynomial_order, int): 96 logging.debug('Received integer polynomial order, assuming isotropic grid') 97 self.polynomial_order = [polynomial_order for i in range(self.N)] 98 else: 99 self.polynomial_order = polynomial_order 100 101 self.quad_rule = quadrature_rule 102 self.sparse = sparse 103 self.midpoint_level1 = midpoint_level1 104 self.dimension_adaptive = dimension_adaptive 105 self.nadaptations = 0 106 self.growth = growth 107 self.check_max_quad_level() 108 109 # determine if a nested sparse grid is used 110 if self.sparse is True and self.growth is True and \ 111 (self.quad_rule == "C" or self.quad_rule == "clenshaw_curtis"): 112 self.nested = True 113 elif self.sparse is True and self.growth is False and self.quad_rule == "gauss_patterson": 114 self.nested = True 115 elif self.sparse is True and self.growth is True and self.quad_rule == "newton_cotes": 116 self.nested = True 117 else: 118 self.nested = False 119 120 # L = level of (sparse) grid 121 self.L = np.max(self.polynomial_order) 122 123 # compute the 1D collocation points (and quad weights) 124 self.compute_1D_points_weights(self.L, self.N) 125 126 # compute N-dimensional collocation points 127 if not self.sparse: 128 129 # generate collocation as a standard tensor product 130 l_norm = np.array([self.polynomial_order]) 131 self.xi_d = self.generate_grid(l_norm) 132 133 else: 134 self.l_norm = self.compute_sparse_multi_idx(self.L, self.N) 135 # create sparse grid of dimension N and level q using the 1d 136 # rules in self.xi_1d 137 self.xi_d = self.generate_grid(self.l_norm) 138 139 self._n_samples = self.xi_d.shape[0] 140 141 self.count = 0
Create the sampler for the Stochastic Collocation method.
Parameters
- vary (dict or None): keys = parameters to be sampled, values = distributions.
- polynomial_order (int, optional): The polynomial order, default is 4.
- quadrature_rule (char, optional): The quadrature method, default is Gaussian "G".
- growth (bool, optional): Sets the growth rule to exponential for Clenshaw Curtis quadrature, which makes it nested, and therefore more efficient for sparse grids. Default is False.
- sparse (bool, optional): If True use sparse grid instead of normal tensor product grid, default is False.
- midpoint_level1 (bool, optional): Used only for sparse=True (or for cp.DiscreteUniform distribution). Determines how many points the 1st level of a sparse grid will have. If True, order 0 quadrature will be generated, default is True.
- dimension_adaptive (bool, optional): Determines wether to use an insotropic sparse grid, or to adapt the levels in the sparse grid based on a hierachical error measure, default is False.
143 @property 144 def analysis_class(self): 145 """Return a corresponding analysis class. 146 """ 147 from easyvvuq.analysis import SCAnalysis 148 return SCAnalysis
Return a corresponding analysis class.
150 def compute_1D_points_weights(self, L, N): 151 """ 152 Computes 1D collocation points and quad weights, 153 and stores this in self.xi_1d, self.wi_1d. 154 155 Parameters 156 ---------- 157 L : (int) the max polynomial order of the (sparse) grid 158 N : (int) the number of uncertain parameters 159 160 Returns 161 ------- 162 None. 163 164 """ 165 # for every dimension (parameter), create a hierachy of 1D 166 # quadrature rules of increasing order 167 self.xi_1d = [{} for n in range(N)] 168 self.wi_1d = [{} for n in range(N)] 169 170 if self.sparse: 171 172 # if level one of the sparse grid is a midpoint rule, generate 173 # the quadrature with order 0 (1 quad point). Else set order at 174 # level 1 to 1 175 if self.midpoint_level1: 176 j = 0 177 else: 178 j = 1 179 180 for n in range(N): 181 # check if input is discrete uniform, in which case the 182 # rule and growth flag must be modified 183 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 184 rule = "discrete" 185 else: 186 rule = self.quad_rule 187 188 for i in range(L): 189 xi_i, wi_i = cp.generate_quadrature(i + j, 190 self.params_distribution[n], 191 rule=rule, 192 growth=self.growth) 193 self.xi_1d[n][i + 1] = xi_i[0] 194 self.wi_1d[n][i + 1] = wi_i 195 else: 196 for n in range(N): 197 # check if input is discrete uniform, in which case the 198 # rule flag must be modified 199 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 200 rule = "discrete" 201 else: 202 rule = self.quad_rule 203 204 xi_i, wi_i = cp.generate_quadrature(self.polynomial_order[n], 205 self.params_distribution[n], 206 rule=rule, 207 growth=self.growth) 208 209 self.xi_1d[n][self.polynomial_order[n]] = xi_i[0] 210 self.wi_1d[n][self.polynomial_order[n]] = wi_i
Computes 1D collocation points and quad weights, and stores this in self.xi_1d, self.wi_1d.
Parameters
L ((int) the max polynomial order of the (sparse) grid):
N ((int) the number of uncertain parameters):
Returns
- None.
212 def check_max_quad_level(self): 213 """ 214 215 If a discrete variable is specified, there is the possibility of 216 non unique collocation points if the quadrature order is high enough. 217 This subroutine prevents that. 218 219 NOTE: Only detects cp.DiscreteUniform thus far 220 221 The max quad orders are stores in self.max_quad_order 222 223 Returns 224 ------- 225 None 226 227 """ 228 # assume no maximum by default 229 self.max_level = np.ones(self.N) * 1000 230 for n in range(self.N): 231 232 # if a discrete uniform is specified check max order 233 if isinstance(self.params_distribution[n], cp.DiscreteUniform): 234 235 #TODO: it is assumed that self.sparse=True, but this assumption 236 #does not have to hold here!!! 237 238 # if level one of the sparse grid is a midpoint rule, generate 239 # the quadrature with order 0 (1 quad point). Else set order at 240 # level 1 to 1 241 if self.midpoint_level1: 242 j = 0 243 else: 244 j = 1 245 246 number_of_points = 0 247 for order in range(1000): 248 xi_i, wi_i = cp.generate_quadrature(order + j, 249 self.params_distribution[n], 250 growth=self.growth) 251 # if the quadrature points no longer grow with the quad order, 252 # then the max order has been reached 253 if xi_i.size == number_of_points: 254 break 255 number_of_points = xi_i.size 256 257 logging.debug("Input %d is discrete, setting max quadrature order to %d" 258 % (n, order - 1)) 259 # level 1 = order 0 etc 260 self.max_level[n] = order
If a discrete variable is specified, there is the possibility of non unique collocation points if the quadrature order is high enough. This subroutine prevents that.
NOTE: Only detects cp.DiscreteUniform thus far
The max quad orders are stores in self.max_quad_order
Returns
- None
262 def next_level_sparse_grid(self): 263 """ 264 Adds the points of the next level for isotropic hierarchical sparse grids. 265 266 Returns 267 ------- 268 None. 269 270 """ 271 272 if self.nested is False: 273 print('Only works for nested sparse grids') 274 return 275 276 self.look_ahead(self.l_norm) 277 self.l_norm = np.append(self.l_norm, self.admissible_idx, axis=0)
Adds the points of the next level for isotropic hierarchical sparse grids.
Returns
- None.
279 def look_ahead(self, current_multi_idx): 280 """ 281 The look-ahead step in (dimension-adaptive) sparse grid sampling. Allows 282 for anisotropic sampling plans. 283 284 Computes the admissible forward neighbors with respect to the current level 285 multi-indices. The admissible level indices l are added to self.admissible_idx. 286 The code will be evaluated next iteration at the new collocation points 287 corresponding to the levels in admissble_idx. 288 289 Source: Gerstner, Griebel, "Numerical integration using sparse grids" 290 291 Parameters 292 ---------- 293 current_multi_idx : array of the levels in the current iteration 294 of the sparse grid. 295 296 Returns 297 ------- 298 None. 299 300 """ 301 302 # compute all forward neighbors for every l in current_multi_idx 303 forward_neighbor = [] 304 e_n = np.eye(self.N, dtype=int) 305 for l in current_multi_idx: 306 for n in range(self.N): 307 # the forward neighbor is l plus a unit vector 308 forward_neighbor.append(l + e_n[n]) 309 310 # remove duplicates 311 forward_neighbor = np.unique(np.array(forward_neighbor), axis=0) 312 # remove those which are already in the grid 313 forward_neighbor = setdiff2d(forward_neighbor, current_multi_idx) 314 # make sure the final candidates are admissible (all backward neighbors 315 # must be in the current multi indices) 316 logging.debug('Computing admissible levels...') 317 admissible_idx = [] 318 for l in forward_neighbor: 319 admissible = True 320 for n in range(self.N): 321 backward_neighbor = l - e_n[n] 322 # find backward_neighbor in current_multi_idx 323 idx = np.where((backward_neighbor == current_multi_idx).all(axis=1))[0] 324 # if backward neighbor is not in the current index set 325 # and it is 'on the interior' (contains no 0): not admissible 326 if idx.size == 0 and backward_neighbor[n] != 0: 327 admissible = False 328 break 329 # if all backward neighbors are in the current index set: l is admissible 330 if admissible: 331 admissible_idx.append(l) 332 logging.debug('done') 333 334 self.admissible_idx = np.array(admissible_idx) 335 # make sure that all entries of each index are <= the max quadrature order 336 # The max quad order can be low for discrete input variables 337 idx = np.where((self.admissible_idx <= self.max_level).all(axis=1))[0] 338 self.admissible_idx = self.admissible_idx[idx] 339 logging.debug('Admissible multi-indices:\n%s', self.admissible_idx) 340 341 # determine the maximum level L of the new index set L = |l| - N + 1 342 # self.L = np.max(np.sum(self.admissible_idx, axis=1) - self.N + 1) 343 self.L = np.max(self.admissible_idx) 344 # recompute the 1D weights and collocation points 345 self.compute_1D_points_weights(self.L, self.N) 346 # compute collocation grid based on the admissible level indices 347 admissible_grid = self.generate_grid(self.admissible_idx) 348 # remove collocation points which have already been computed 349 if not hasattr(self, 'xi_d'): 350 self.xi_d = self.generate_grid(self.admissible_idx) 351 self._n_samples = self.xi_d.shape[0] 352 new_points = setdiff2d(admissible_grid, self.xi_d) 353 354 logging.debug('%d new points added' % new_points.shape[0]) 355 356 # keep track of the number of points added per iteration 357 if not hasattr(self, 'n_new_points'): 358 self.n_new_points = [] 359 self.n_new_points.append(new_points.shape[0]) 360 361 # update the number of samples 362 self._n_samples += new_points.shape[0] 363 364 # update the N-dimensional sparse grid if unsampled points are added 365 if new_points.shape[0] > 0: 366 self.xi_d = np.concatenate((self.xi_d, new_points)) 367 368 # count the number of times the dimensions were adapted 369 self.nadaptations += 1
The look-ahead step in (dimension-adaptive) sparse grid sampling. Allows for anisotropic sampling plans.
Computes the admissible forward neighbors with respect to the current level multi-indices. The admissible level indices l are added to self.admissible_idx. The code will be evaluated next iteration at the new collocation points corresponding to the levels in admissble_idx.
Source: Gerstner, Griebel, "Numerical integration using sparse grids"
Parameters
current_multi_idx (array of the levels in the current iteration):
of the sparse grid.
Returns
- None.
374 @property 375 def n_samples(self): 376 """ 377 Number of samples (Ns) of SC method. 378 - When using tensor quadrature: Ns = (p + 1)**d 379 - When using sparid: Ns = bigO((p + 1)*log(p + 1)**(d-1)) 380 Where: p is the polynomial degree and d is the number of 381 uncertain parameters. 382 383 Ref: Eck et al. 'A guide to uncertainty quantification and 384 sensitivity analysis for cardiovascular applications' [2016]. 385 """ 386 return self._n_samples
Number of samples (Ns) of SC method.
- When using tensor quadrature: Ns = (p + 1)**d
- When using sparid: Ns = bigO((p + 1)log(p + 1)*(d-1)) Where: p is the polynomial degree and d is the number of uncertain parameters.
Ref: Eck et al. 'A guide to uncertainty quantification and sensitivity analysis for cardiovascular applications' [2016].
427 def generate_grid(self, l_norm): 428 429 dimensions = range(self.N) 430 H_L_N = [] 431 # loop over all multi indices i 432 for l in l_norm: 433 # compute the tensor product of nodes indexed by i 434 X_l = [self.xi_1d[n][l[n]] for n in dimensions] 435 H_L_N.append(list(product(*X_l))) 436 # flatten the list of lists 437 H_L_N = np.array(list(chain(*H_L_N))) 438 # return unique nodes 439 return np.unique(H_L_N, axis=0)
443 def compute_sparse_multi_idx(self, L, N): 444 """ 445 computes all N dimensional multi-indices l = (l1,...,lN) such that 446 |l| <= L + N - 1, i.e. a simplex set: 447 3 * 448 2 * * (L=3 and N=2) 449 1 * * * 450 1 2 3 451 Here |l| is the internal sum of i (l1+...+lN) 452 """ 453 # old implementation: does not scale well 454 # P = np.array(list(product(range(1, L + 1), repeat=N))) 455 # multi_idx = P[np.where(np.sum(P, axis=1) <= L + N - 1)[0]] 456 457 # use the look_ahead subroutine to build an isotropic sparse grid (faster) 458 multi_idx = np.array([np.ones(self.N, dtype='int')]) 459 for l in range(self.L - 1): 460 self.look_ahead(multi_idx) 461 # accept all admissible indices to build an isotropic grid 462 multi_idx = np.append(multi_idx, self.admissible_idx, axis=0) 463 464 return multi_idx
computes all N dimensional multi-indices l = (l1,...,lN) such that |l| <= L + N - 1, i.e. a simplex set: 3 * 2 * * (L=3 and N=2) 1 * * * 1 2 3 Here |l| is the internal sum of i (l1+...+lN)
467def setdiff2d(X, Y): 468 """ 469 Computes the difference of two 2D arrays X and Y 470 471 Parameters 472 ---------- 473 X : 2D numpy array 474 Y : 2D numpy array 475 476 Returns 477 ------- 478 The difference X \\ Y as a 2D array 479 480 """ 481 diff = set(map(tuple, X)) - set(map(tuple, Y)) 482 return np.array(list(diff))
Computes the difference of two 2D arrays X and Y
Parameters
X (2D numpy array):
Y (2D numpy array):
Returns
- The difference X \ Y as a 2D array