pipt.update_schemes.enrml
EnRML type schemes
1""" 2EnRML type schemes 3""" 4# External imports 5import pipt.misc_tools.analysis_tools as at 6from geostat.decomp import Cholesky 7from pipt.loop.ensemble import Ensemble 8from pipt.update_schemes.update_methods_ns.subspace_update import subspace_update 9from pipt.update_schemes.update_methods_ns.full_update import full_update 10from pipt.update_schemes.update_methods_ns.approx_update import approx_update 11import sys 12import pkgutil 13import inspect 14import numpy as np 15import copy as cp 16from scipy.linalg import cholesky, solve 17 18import importlib.util 19 20# List all available packages in the namespace package 21# Import those that are present 22import pipt.update_schemes.update_methods_ns as ns_pkg 23tot_ns_pkg = [] 24# extract all class methods from namespace 25for finder, name, ispkg in pkgutil.walk_packages(ns_pkg.__path__): 26 spec = finder.find_spec(name) 27 _module = importlib.util.module_from_spec(spec) 28 spec.loader.exec_module(_module) 29 tot_ns_pkg.extend(inspect.getmembers(_module, inspect.isclass)) 30 31# import standard libraries 32 33# Check and import (if present) from other namespace packages 34if 'margIS_update' in [el[0] for el in tot_ns_pkg]: # only compare package name 35 from pipt.update_schemes.update_methods_ns.margIS_update import margIS_update 36else: 37 class margIS_update: 38 pass 39 40# Internal imports 41 42 43class lmenrmlMixIn(Ensemble): 44 """ 45 This is an implementation of EnRML using Levenberg-Marquardt. The update scheme is selected by a MixIn with multiple 46 update_methods_ns. This class must therefore facititate many different update schemes. 47 """ 48 49 def __init__(self, keys_da, keys_fwd, sim): 50 """ 51 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 52 `pipt.input_output.pipt_init.ReadInitFile`. 53 54 Parameters 55 ---------- 56 init_file: str 57 PIPT init. file containing info. to run the inversion algorithm 58 """ 59 # Pass the init_file upwards in the hierarchy 60 super().__init__(keys_da, keys_fwd, sim) 61 62 if self.restart is False: 63 # Save prior state in separate variable 64 self.prior_state = cp.deepcopy(self.state) 65 66 # Extract parameters like conv. tol. and damping param. from ITERATION keyword in DATAASSIM 67 self._ext_iter_param() 68 69 # Within variables 70 self.prev_data_misfit = None # Data misfit at previous iteration 71 if 'actnum' in self.keys_da.keys(): 72 try: 73 self.actnum = np.load(self.keys_da['actnum'])['actnum'] 74 except: 75 print('ACTNUM file cannot be loaded!') 76 else: 77 self.actnum = None 78 # At the moment, the iterative loop is threated as an iterative smoother and thus we check if assim. indices 79 # are given as in the Simultaneous loop. 80 self.check_assimindex_simultaneous() 81 # define the assimilation index 82 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 83 # define the list of states 84 self.list_states = list(self.state.keys()) 85 # define the list of datatypes 86 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 87 self.obs_data, self.assim_index) 88 # Get the perturbed observations and observation scaling 89 self.data_random_state = cp.deepcopy(np.random.get_state()) 90 self._ext_obs() 91 # Get state scaling and svd of scaled prior 92 self._ext_state() 93 self.current_state = cp.deepcopy(self.state) 94 95 def calc_analysis(self): 96 """ 97 Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with 98 the sensitivity matrix approximated by the ensemble. 99 """ 100 101 # reformat predicted data 102 _, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 103 self.list_datatypes) 104 105 if self.iteration == 1: # first iteration 106 data_misfit = at.calc_objectivefun( 107 self.real_obs_data, self.aug_pred_data, self.cov_data) 108 109 # Store the (mean) data misfit (also for conv. check) 110 self.data_misfit = np.mean(data_misfit) 111 self.prior_data_misfit = np.mean(data_misfit) 112 self.data_misfit_std = np.std(data_misfit) 113 114 if self.lam == 'auto': 115 self.lam = (0.5 * self.prior_data_misfit)/self.aug_pred_data.shape[0] 116 117 self.logger.info( 118 f'Prior run complete with data misfit: {self.prior_data_misfit:0.1f}. Lambda for initial analysis: {self.lam}') 119 120 if 'localanalysis' in self.keys_da: 121 self.local_analysis_update() 122 else: 123 # Mean pred_data and perturbation matrix with scaling 124 if len(self.scale_data.shape) == 1: 125 self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 126 np.ones((1, self.ne))) * np.dot(self.aug_pred_data, self.proj) 127 else: 128 self.pert_preddata = solve( 129 self.scale_data, np.dot(self.aug_pred_data, self.proj)) 130 131 aug_state = at.aug_state(self.current_state, self.list_states) 132 self.update() # run ordinary analysis 133 if hasattr(self, 'step'): 134 aug_state_upd = aug_state + self.step 135 if hasattr(self, 'w_step'): 136 self.W = self.current_W + self.w_step 137 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 138 aug_state_upd = np.dot(aug_prior_state, (np.eye( 139 self.ne) + self.W / np.sqrt(self.ne - 1))) 140 141 # Extract updated state variables from aug_update 142 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 143 self.state = at.limits(self.state, self.prior_info) 144 145 def check_convergence(self): 146 """ 147 Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping 148 parameter. 149 150 Returns 151 ------- 152 conv: bool 153 Logic variable telling if algorithm has converged 154 why_stop: dict 155 Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been 156 met 157 """ 158 159 _, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 160 self.list_datatypes) 161 # Initialize the initial success value 162 success = False 163 164 # if inital conv. check, there are no prev_data_misfit 165 self.prev_data_misfit = self.data_misfit 166 self.prev_data_misfit_std = self.data_misfit_std 167 168 # Calc. std dev of data misfit (used to update lamda) 169 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector),1)), np.ones((1, self.ne))) # use the perturbed 170 # data instead. 171 172 data_misfit = at.calc_objectivefun(self.real_obs_data, pred_data, self.cov_data) 173 174 self.data_misfit = np.mean(data_misfit) 175 self.data_misfit_std = np.std(data_misfit) 176 177 # # Calc. mean data misfit for convergence check, using the updated state variable 178 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 179 # solve(cov_data, (mean_preddata - obs_data_vector))) 180 181 # Convergence check: Relative step size of data misfit or state change less than tolerance 182 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol \ 183 or self.lam >= self.lam_max: 184 # Logical variables for conv. criteria 185 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 186 'data_misfit': self.data_misfit, 187 'prev_data_misfit': self.prev_data_misfit, 188 'lambda': self.lam, 189 'lambda_stop': self.lam >= self.lam_max} 190 191 if self.data_misfit >= self.prev_data_misfit: 192 success = False 193 self.logger.info( 194 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 195 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 196 else: 197 self.logger.info( 198 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 199 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 200 # Return conv = True, why_stop var. 201 return True, success, why_stop 202 203 else: # conv. not met 204 # Logical variables for conv. criteria 205 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 206 'data_misfit': self.data_misfit, 207 'prev_data_misfit': self.prev_data_misfit, 208 'lambda': self.lam, 209 'lambda_stop': self.lam >= self.lam_max} 210 211 ############################################### 212 ##### update Lambda step-size values ########## 213 ############################################### 214 # If reduction in mean data misfit, reduce damping param 215 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 216 # Reduce damping parameter (divide calculations for ANALYSISDEBUG purpose) 217 if self.lam > self.lam_min: 218 self.lam = self.lam / self.gamma 219 success = True 220 self.current_state = cp.deepcopy(self.state) 221 if hasattr(self, 'W'): 222 self.current_W = cp.deepcopy(self.W) 223 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 224 # accept itaration, but keep lam the same 225 success = True 226 self.current_state = cp.deepcopy(self.state) 227 if hasattr(self, 'W'): 228 self.current_W = cp.deepcopy(self.W) 229 230 else: # Reject iteration, and increase lam 231 # Increase damping parameter (divide calculations for ANALYSISDEBUG purpose) 232 self.lam = self.lam * self.gamma 233 success = False 234 235 if success: 236 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 237 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for next analysis: ' 238 f'{self.lam}') 239 # self.prev_data_misfit = self.data_misfit 240 # self.prev_data_misfit_std = self.data_misfit_std 241 else: 242 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 243 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for repeated analysis: ' 244 f'{self.lam}') 245 # Reset the objective function after report 246 self.data_misfit = self.prev_data_misfit 247 self.data_misfit_std = self.prev_data_misfit_std 248 249 # Return conv = False, why_stop var. 250 return False, success, why_stop 251 252 def _ext_iter_param(self): 253 """ 254 Extract parameters needed in LM-EnRML from the ITERATION keyword given in the DATAASSIM part of PIPT init. 255 file. These parameters include convergence tolerances and parameters for the damping parameter. Default 256 values for these parameters have been given here, if they are not provided in ITERATION. 257 """ 258 259 # Predefine all the default values 260 self.data_misfit_tol = 0.01 261 self.step_tol = 0.01 262 self.lam = 100 263 self.lam_max = 1e10 264 self.lam_min = 0.01 265 self.gamma = 5 266 self.trunc_energy = 0.95 267 self.iteration = 0 268 269 # Loop over options in ITERATION and extract the parameters we want 270 for i, opt in enumerate(list(zip(*self.keys_da['iteration']))[0]): 271 if opt == 'data_misfit_tol': 272 self.data_misfit_tol = self.keys_da['iteration'][i][1] 273 if opt == 'step_tol': 274 self.step_tol = self.keys_da['iteration'][i][1] 275 if opt == 'lambda': 276 self.lam = self.keys_da['iteration'][i][1] 277 if opt == 'lambda_max': 278 self.lam_max = self.keys_da['iteration'][i][1] 279 if opt == 'lambda_min': 280 self.lam_min = self.keys_da['iteration'][i][1] 281 if opt == 'lambda_factor': 282 self.gamma = self.keys_da['iteration'][i][1] 283 284 if 'energy' in self.keys_da: 285 # initial energy (Remember to extract this) 286 self.trunc_energy = self.keys_da['energy'] 287 if self.trunc_energy > 1: # ensure that it is given as percentage 288 self.trunc_energy /= 100. 289 290 291class lmenrml_approx(lmenrmlMixIn, approx_update): 292 pass 293 294 295class lmenrml_full(lmenrmlMixIn, full_update): 296 pass 297 298 299class lmenrml_subspace(lmenrmlMixIn, subspace_update): 300 pass 301 302 303class gnenrmlMixIn(Ensemble): 304 """ 305 This is an implementation of EnRML using the Gauss-Newton approach. The update scheme is selected by a MixIn with multiple 306 update_methods_ns. This class must therefore facititate many different update schemes. 307 """ 308 309 def __init__(self, keys_da, keys_fwd, sim): 310 """ 311 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 312 `pipt.input_output.pipt_init.ReadInitFile`. 313 314 Parameters 315 ---------- 316 init_file: str 317 PIPT init. file containing info. to run the inversion algorithm 318 """ 319 # Pass the init_file upwards in the hierarchy 320 super().__init__(keys_da, keys_fwd, sim) 321 322 if self.restart is False: 323 # Save prior state in separate variable 324 self.prior_state = cp.deepcopy(self.state) 325 326 # extract and save state scaling 327 328 # Extract parameters like conv. tol. and damping param. from ITERATION keyword in DATAASSIM 329 self._ext_iter_param() 330 331 # Within variables 332 self.prev_data_misfit = None # Data misfit at previous iteration 333 if 'actnum' in self.keys_da.keys(): 334 try: 335 self.actnum = np.load(self.keys_da['actnum'])['actnum'] 336 except: 337 print('ACTNUM file cannot be loaded!') 338 else: 339 self.actnum = None 340 # At the moment, the iterative loop is threated as an iterative smoother and thus we check if assim. indices 341 # are given as in the Simultaneous loop. 342 self.check_assimindex_simultaneous() 343 # define the assimilation index 344 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 345 # define the list of states 346 self.list_states = list(self.state.keys()) 347 # define the list of datatypes 348 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 349 self.obs_data, self.assim_index) 350 # Get the perturbed observations and observation scaling 351 self._ext_obs() 352 # Get state scaling and svd of scaled prior 353 self._ext_state() 354 self.current_state = cp.deepcopy(self.state) 355 # ensure that the updates does not invoke the LM inflation of the Hessian. 356 self.lam = 0 357 358 def _ext_obs(self): 359 360 self.obs_data_vector, _ = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 361 self.list_datatypes) 362 363 # Generate the data auto-covariance matrix 364 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 365 if hasattr(self, 'cov_data'): # cd matrix has been imported 366 tmp_E = np.dot(cholesky(self.cov_data).T, 367 np.random.randn(self.cov_data.shape[0], self.ne)) 368 else: 369 tmp_E = at.extract_tot_empirical_cov( 370 self.datavar, self.assim_index, self.list_datatypes, self.ne) 371 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 372 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 373 tmp_E = at.screen_data(tmp_E, self.aug_pred_data, 374 self.obs_data_vector, self.iteration) 375 self.E = tmp_E 376 self.real_obs_data = self.obs_data_vector[:, np.newaxis] - tmp_E 377 378 self.cov_data = np.var(self.E, ddof=1, 379 axis=1) # calculate the variance, to be used for e.g. data misfit calc 380 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 381 self.scale_data = np.sqrt(self.cov_data) 382 else: 383 if not hasattr(self, 'cov_data'): # if cd is not loaded 384 self.cov_data = at.gen_covdata( 385 self.datavar, self.assim_index, self.list_datatypes) 386 # data screening 387 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 388 self.cov_data = at.screen_data( 389 self.cov_data, self.aug_pred_data, self.obs_data_vector, self.iteration) 390 391 init_en = Cholesky() # Initialize GeoStat class for generating realizations 392 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 393 return_chol=True) 394 395 def _ext_state(self): 396 # get vector of scaling 397 self.state_scaling = at.calc_scaling( 398 self.prior_state, self.list_states, self.prior_info) 399 400 delta_scaled_prior = self.state_scaling[:, None] * \ 401 np.dot(at.aug_state(self.prior_state, self.list_states), self.proj) 402 403 u_d, s_d, v_d = np.linalg.svd(delta_scaled_prior, full_matrices=False) 404 405 # remove the last singular value/vector. This is because numpy returns all ne values, while the last is actually 406 # zero. This part is a good place to include eventual additional truncation. 407 energy = 0 408 trunc_index = len(s_d) - 1 # inititallize 409 for c, elem in enumerate(s_d): 410 energy += elem 411 if energy / sum(s_d) >= self.trunc_energy: 412 trunc_index = c # take the index where all energy is preserved 413 break 414 u_d, s_d, v_d = u_d[:, :trunc_index + 415 1], s_d[:trunc_index + 1], v_d[:trunc_index + 1, :] 416 self.Am = np.dot(u_d, np.eye(trunc_index+1) * 417 ((s_d**(-1))[:, None])) # notation from paper 418 419 def calc_analysis(self): 420 """ 421 Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with 422 the sensitivity matrix approximated by the ensemble. 423 424 """ 425 426 # reformat predicted data 427 _, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 428 self.list_datatypes) 429 430 if self.iteration == 1: # first iteration 431 data_misfit = at.calc_objectivefun( 432 self.real_obs_data, self.aug_pred_data, self.cov_data) 433 434 # Store the (mean) data misfit (also for conv. check) 435 self.data_misfit = np.mean(data_misfit) 436 self.prior_data_misfit = np.mean(data_misfit) 437 self.data_misfit_std = np.std(data_misfit) 438 439 if self.gamma == 'auto': 440 self.gamma = 0.1 441 442 # Mean pred_data and perturbation matrix with scaling 443 if len(self.scale_data.shape) == 1: 444 self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 445 np.ones((1, self.ne))) * np.dot(self.aug_pred_data, self.proj) 446 else: 447 self.pert_preddata = solve( 448 self.scale_data, np.dot(self.aug_pred_data, self.proj)) 449 450 aug_state = at.aug_state(self.current_state, self.list_states) 451 452 self.update() # run analysis 453 if hasattr(self, 'step'): 454 aug_state_upd = aug_state + self.gamma*self.step 455 if hasattr(self, 'w_step'): 456 self.W = self.current_W + self.gamma*self.w_step 457 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 458 aug_state_upd = np.dot(aug_prior_state, (np.eye( 459 self.ne) + self.W / np.sqrt(self.ne - 1))) 460 if hasattr(self, 'sqrt_w_step'): # if we do a sqrt update 461 self.w = self.current_w + self.gamma*self.sqrt_w_step 462 new_mean_state = self.mean_prior + np.dot(self.X, self.w) 463 u, sigma, v = np.linalg.svd(self.C_w, full_matrices=True) 464 sigma_inv_sqrt = np.diag([el_s ** (-1 / 2) for el_s in sigma]) 465 C_w_inv_sqrt = np.dot(np.dot(u, sigma_inv_sqrt), v.T) 466 self.W = C_w_inv_sqrt * np.sqrt(self.ne - 1) 467 aug_state_upd = np.tile(new_mean_state, (self.ne, 1) 468 ).T + np.dot(self.X, self.W) 469 470 # Extract updated state variables from aug_update 471 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 472 self.state = at.limits(self.state, self.prior_info) 473 474 def check_convergence(self): 475 """ 476 Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping 477 parameter. 478 479 Returns 480 ------- 481 conv: bool 482 Logic variable telling if algorithm has converged 483 why_stop: dict 484 Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been 485 met 486 """ 487 # Prelude to calc. conv. check (everything done below is from calc_analysis) 488 if hasattr(self, 'list_datatypes'): 489 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 490 list_datatypes = self.list_datatypes 491 cov_data = self.cov_data 492 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 493 list_datatypes) 494 mean_preddata = np.mean(pred_data, 1) 495 else: 496 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 497 list_datatypes, _ = at.get_list_data_types(self.obs_data, assim_index) 498 # cov_data = at.gen_covdata(self.datavar, assim_index, list_datatypes) 499 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 500 list_datatypes) 501 # mean_preddata = np.mean(pred_data, 1) 502 503 # Initialize the initial success value 504 success = False 505 506 # if inital conv. check, there are no prev_data_misfit 507 if self.prev_data_misfit is None: 508 self.data_misfit = np.mean(self.data_misfit) 509 self.prev_data_misfit = self.data_misfit 510 self.prev_data_misfit_std = self.data_misfit_std 511 success = True 512 513 # update the last mismatch, only if this was a reduction of the misfit 514 if self.data_misfit < self.prev_data_misfit: 515 self.prev_data_misfit = self.data_misfit 516 self.prev_data_misfit_std = self.data_misfit_std 517 success = True 518 # if there was no reduction of the misfit, retain the old "valid" data misfit. 519 520 # Calc. std dev of data misfit (used to update lamda) 521 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector),1)), np.ones((1, self.ne))) # use the perturbed 522 # data instead. 523 mat_obs = self.real_obs_data 524 data_misfit = at.calc_objectivefun(mat_obs, pred_data, self.cov_data) 525 526 self.data_misfit = np.mean(data_misfit) 527 self.data_misfit_std = np.std(data_misfit) 528 529 # # Calc. mean data misfit for convergence check, using the updated state variable 530 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 531 # solve(cov_data, (mean_preddata - obs_data_vector))) 532 533 # Convergence check: Relative step size of data misfit or state change less than tolerance 534 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol: 535 # Logical variables for conv. criteria 536 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 537 'data_misfit': self.data_misfit, 538 'prev_data_misfit': self.prev_data_misfit, 539 'gamma': self.gamma, 540 } 541 542 if self.data_misfit >= self.prev_data_misfit: 543 success = False 544 self.logger.info( 545 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 546 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 547 else: 548 self.logger.info( 549 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 550 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 551 # Return conv = True, why_stop var. 552 return True, success, why_stop 553 554 else: # conv. not met 555 # Logical variables for conv. criteria 556 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 557 'data_misfit': self.data_misfit, 558 'prev_data_misfit': self.prev_data_misfit, 559 'gamma': self.gamma} 560 561 ############################################### 562 ##### update Lambda step-size values ########## 563 ############################################### 564 # If reduction in mean data misfit, reduce damping param 565 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 566 # Reduce damping parameter (divide calculations for ANALYSISDEBUG purpose) 567 self.gamma = self.gamma + (self.gamma_max - self.gamma) * 2 ** ( 568 -(self.iteration) / (self.gamma_factor - 1)) 569 success = True 570 self.current_state = cp.deepcopy(self.state) 571 if hasattr(self, 'W'): 572 self.current_W = cp.deepcopy(self.W) 573 574 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 575 # accept itaration, but keep lam the same 576 success = True 577 self.current_state = cp.deepcopy(self.state) 578 if hasattr(self, 'W'): 579 self.current_W = cp.deepcopy(self.W) 580 581 else: # Reject iteration, and increase lam 582 # Increase damping parameter (divide calculations for ANALYSISDEBUG purpose) 583 err_str = f"Misfit increased. Set new start step length and try again. Final ojective function value is {self.data_misfit:0.1f}" 584 self.logger.info(err_str) 585 sys.exit(err_str) 586 success = False 587 588 if success: 589 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 590 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Gamma for next analysis: ' 591 f'{self.gamma}') 592 else: 593 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 594 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Gamma for repeated analysis: ' 595 f'{self.gamma}') 596 597 # Return conv = False, why_stop var. 598 return False, success, why_stop 599 600 def _ext_iter_param(self): 601 """ 602 Extract parameters needed in LM-EnRML from the ITERATION keyword given in the DATAASSIM part of PIPT init. 603 file. These parameters include convergence tolerances and parameters for the damping parameter. Default 604 values for these parameters have been given here, if they are not provided in ITERATION. 605 """ 606 607 # Predefine all the default values 608 self.data_misfit_tol = 0.01 609 self.step_tol = 0.01 610 self.gamma = 0.2 611 self.gamma_max = 0.5 612 self.gamma_factor = 2.5 613 self.trunc_energy = 0.95 614 self.iteration = 0 615 616 # Loop over options in ITERATION and extract the parameters we want 617 for i, opt in enumerate(list(zip(*self.keys_da['iteration']))[0]): 618 if opt == 'data_misfit_tol': 619 self.data_misfit_tol = self.keys_da['iteration'][i][1] 620 if opt == 'step_tol': 621 self.step_tol = self.keys_da['iteration'][i][1] 622 if opt == 'gamma': 623 self.gamma = self.keys_da['iteration'][i][1] 624 if opt == 'gamma_max': 625 self.gamma_max = self.keys_da['iteration'][i][1] 626 if opt == 'gamma_factor': 627 self.gamma_factor = self.keys_da['iteration'][i][1] 628 629 if 'energy' in self.keys_da: 630 # initial energy (Remember to extract this) 631 self.trunc_energy = self.keys_da['energy'] 632 if self.trunc_energy > 1: # ensure that it is given as percentage 633 self.trunc_energy /= 100. 634 635 636class gnenrml_approx(gnenrmlMixIn, approx_update): 637 pass 638 639 640class gnenrml_full(gnenrmlMixIn, full_update): 641 pass 642 643 644class gnenrml_subspace(gnenrmlMixIn, subspace_update): 645 pass 646 647 648class gnenrml_margis(gnenrmlMixIn, margIS_update): 649 ''' 650 The marg-IS scheme is currently not available in this version of PIPT. To utilize the scheme you have to import the 651 *margIS_update* class from a standalone repository. 652 ''' 653 pass 654 655 656class co_lm_enrml(lmenrmlMixIn, approx_update): 657 """ 658 This is the implementation of the approximative LM-EnRML algorithm as described in [1]. 659 660 This algorithm is quite similar to the lm_enrml as provided above, and will therefore inherit most of its methods. 661 We only change the calc_analysis part... 662 663 % Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS 664 """ 665 666 def __init__(self, keys_da): 667 """ 668 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 669 `pipt.input_output.pipt_init.ReadInitFile`. 670 671 Parameters 672 ---------- 673 init_file: str 674 PIPT init. file containing info. to run the inversion algorithm 675 676 References 677 ---------- 678 [1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient 679 History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4): 689-703 680 """ 681 # Call __init__ in parent class 682 super().__init__(keys_da) 683 684 def calc_analysis(self): 685 """ 686 Calculate the update step in approximate LM-EnRML code. 687 688 Parameters 689 ---------- 690 iteration: int 691 Iteration number 692 693 Returns 694 ------- 695 success: bool 696 True if data mismatch is decreasing, False if increasing 697 698 References 699 ---------- 700 [1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient 701 History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4): 689-703 702 """ 703 # Get assimilation order as a list 704 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 705 706 # When handling large cases, it may be very costly to assemble the data covariance and localizaton matrix. 707 # To alleviate this in the simultuaneus-iterative scheme we store these matrices, the list of states and 708 # the list of data types after the first iteration. 709 710 if not hasattr(self, 'list_datatypes'): 711 # Get list of data types to be assimilated and of the free states. Do this once, because listing keys from a 712 # Python dictionary just when needed (in different places) may not yield the same list! 713 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 714 self.obs_data, self.assim_index) 715 list_datatypes = self.list_datatypes 716 self.list_states = list(self.state.keys()) 717 list_states = self.list_states 718 list_act_datatypes = self.list_act_datatypes 719 720 # self.cov_data = np.load('CD.npz')['arr_0'] 721 # Generate the realizations of the observed data once 722 # Augment observed and predicted data 723 self.obs_data_vector, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 724 self.list_datatypes) 725 obs_data_vector = self.obs_data_vector 726 727 # Generate the data auto-covariance matrix 728 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 729 if hasattr(self, 'cov_data'): # cd matrix has been imported 730 tmp_E = np.dot(cholesky(self.cov_data).T, 731 np.random.randn(self.cov_data.shape[0], self.ne)) 732 else: 733 tmp_E = at.extract_tot_empirical_cov( 734 self.datavar, self.assim_index, self.list_datatypes, self.ne) 735 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 736 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 737 tmp_E = at.screen_data(tmp_E, self.aug_pred_data, 738 obs_data_vector, self.iteration) 739 self.E = tmp_E 740 self.real_obs_data = obs_data_vector[:, np.newaxis] - tmp_E 741 742 self.cov_data = np.var(self.E, ddof=1, 743 axis=1) # calculate the variance, to be used for e.g. data misfit calc 744 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 745 self.scale_data = np.sqrt(self.cov_data) 746 else: 747 if not hasattr(self, 'cov_data'): # if cd is not loaded 748 self.cov_data = at.gen_covdata( 749 self.datavar, self.assim_index, self.list_datatypes) 750 # data screening 751 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 752 self.cov_data = at.screen_data( 753 self.cov_data, self.aug_pred_data, obs_data_vector, self.iteration) 754 755 init_en = Cholesky() # Initialize GeoStat class for generating realizations 756 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 757 return_chol=True) 758 759 self.datavar = at.update_datavar( 760 self.cov_data, self.datavar, self.assim_index, self.list_datatypes) 761 self.current_state = cp.deepcopy(self.state) 762 763 # Calc. misfit for the initial iteration 764 data_misfit = at.calc_objectivefun( 765 self.real_obs_data, self.aug_pred_data, self.cov_data) 766 # Store the (mean) data misfit (also for conv. check) 767 self.data_misfit = np.mean(data_misfit) 768 self.prior_data_misfit = np.mean(data_misfit) 769 self.data_misfit_std = np.std(data_misfit) 770 771 if self.lam == 'auto': 772 self.lam = 0.5 * self.prior_data_misfit 773 774 else: 775 _, self.aug_pred_data = at.aug_obs_pred_data( 776 self.obs_data, self.pred_data, assim_index, self.list_datatypes) 777 778 # Mean pred_data and perturbation matrix with scaling 779 mean_preddata = np.mean(self.aug_pred_data, 1) 780 if len(self.scale_data.shape) == 1: 781 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 782 pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * ( 783 self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) 784 else: 785 pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * ( 786 self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) / \ 787 (np.sqrt(self.ne - 1)) 788 else: 789 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 790 pert_preddata = solve(self.scale_data, self.aug_pred_data - 791 np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) 792 else: 793 pert_preddata = solve(self.scale_data, self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) / \ 794 (np.sqrt(self.ne - 1)) 795 self.pert_preddata = pert_preddata 796 797 self.update() 798 if hasattr(self, 'step'): 799 aug_state_upd = aug_state + self.step 800 if hasattr(self, 'w_step'): 801 self.W = self.current_W - self.w_step 802 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 803 aug_state_upd = np.dot(aug_prior_state, (np.eye( 804 self.ne) + self.W / np.sqrt(self.ne - 1))) 805 806 # Extract updated state variables from aug_update 807 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 808 self.state = at.limits(self.state, self.prior_info) 809 810 811class gn_enrml(lmenrmlMixIn): 812 """ 813 This is the implementation of the stochastig IES as described in [1]. More information about the method is found in 814 [2]. This implementation is the Gauss-Newton version. 815 816 This algorithm is quite similar to the `lm_enrml` as provided above, and will therefore inherit most of its methods. 817 We only change the calc_analysis part... 818 """ 819 820 def __init__(self, keys_da): 821 """ 822 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 823 `pipt.input_output.pipt_init.ReadInitFile`. 824 825 Parameters 826 ---------- 827 init_file: str 828 PIPT init. file containing info. to run the inversion algorithm 829 830 References 831 ---------- 832 [1] Raanes, P. N., Stordal, A. S., & Evensen, G. (2019). Revising the stochastic iterative ensemble smoother. 833 Nonlinear Processes in Geophysics, 26(3), 325-338. https://doi.org/10.5194/npg-26-325-2019 <br> 834 [2] Evensen, G., Raanes, P. N., Stordal, A. S., & Hove, J. (2019). Efficient Implementation of an Iterative 835 Ensemble Smoother for Data Assimilation and Reservoir History Matching. Frontiers in Applied Mathematics and 836 Statistics, 5(October), 114. https://doi.org/10.3389/fams.2019.00047 837 """ 838 # Call __init__ in parent class 839 super().__init__(keys_da) 840 841 def calc_analysis(self): 842 """ 843 Changelog 844 --------- 845 - KF 25/2-20 846 """ 847 # Get assimilation order as a list 848 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 849 850 # When handling large cases, it may be very costly to assemble the data covariance and localizaton matrix. 851 # To alleviate this in the simultuaneus-iterative scheme we store these matrices, the list of states and 852 # the list of data types after the first iteration. 853 854 if not hasattr(self, 'list_datatypes'): 855 # Get list of data types to be assimilated and of the free states. Do this once, because listing keys from a 856 # Python dictionary just when needed (in different places) may not yield the same list! 857 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 858 self.obs_data, assim_index) 859 list_datatypes = self.list_datatypes 860 self.list_states = list(self.state.keys()) 861 list_states = self.list_states 862 list_act_datatypes = self.list_act_datatypes 863 864 # Generate the realizations of the observed data once 865 # Augment observed and predicted data 866 self.obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 867 self.list_datatypes) 868 obs_data_vector = self.obs_data_vector 869 870 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 871 if hasattr(self, 'cov_data'): # cd matrix has been imported 872 tmp_E = np.dot(cholesky(self.cov_data).T, np.random.randn( 873 self.cov_data.shape[0], self.ne)) 874 else: 875 tmp_E = at.extract_tot_empirical_cov( 876 self.datavar, assim_index, self.list_datatypes, self.ne) 877 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 878 self.real_obs_data = obs_data_vector[:, np.newaxis] - tmp_E 879 880 self.cov_data = np.var(tmp_E, ddof=1, 881 axis=1) # calculate the variance, to be used for e.g. data misfit calc 882 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 883 self.scale_data = np.sqrt(self.cov_data) 884 else: 885 if not hasattr(self, 'cov_data'): # if cd is not loaded 886 self.cov_data = at.gen_covdata( 887 self.datavar, assim_index, self.list_datatypes) 888 # data screening 889 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 890 self.cov_data = at.screen_data( 891 self.cov_data, pred_data, obs_data_vector, self.iteration) 892 893 init_en = Cholesky() # Initialize GeoStat class for generating realizations 894 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 895 return_chol=True) 896 897 self.datavar = at.update_datavar( 898 self.cov_data, self.datavar, assim_index, self.list_datatypes) 899 cov_data = self.cov_data 900 obs_data = self.real_obs_data 901 # 902 self.current_state = cp.deepcopy(self.state) 903 # 904 self.aug_prior = cp.deepcopy(at.aug_state( 905 self.current_state, self.list_states)) 906 # self.mean_prior = aug_prior.mean(axis=1) 907 # self.X = (aug_prior - np.dot(np.resize(self.mean_prior, (len(self.mean_prior), 1)), 908 # np.ones((1, self.ne)))) 909 self.W = np.zeros((self.ne, self.ne)) 910 911 self.proj = (np.eye(self.ne) - (1 / self.ne) * 912 np.ones((self.ne, self.ne))) / np.sqrt(self.ne - 1) 913 self.E = np.dot(obs_data, self.proj) 914 915 # Calc. misfit for the initial iteration 916 if len(cov_data.shape) == 1: 917 tmp_data_misfit = np.diag(np.dot((pred_data - obs_data).T, 918 np.dot(np.expand_dims(self.cov_data ** (-1), axis=1), 919 np.ones((1, self.ne))) * (pred_data - obs_data))) 920 else: 921 tmp_data_misfit = np.diag( 922 np.dot((pred_data - obs_data).T, solve(self.cov_data, (pred_data - obs_data)))) 923 mean_data_misfit = np.mean(tmp_data_misfit) 924 # mean_data_misfit = np.median(tmp_data_misfit) 925 std_data_misfit = np.std(tmp_data_misfit) 926 927 # Store the (mean) data misfit (also for conv. check) 928 self.data_misfit = mean_data_misfit 929 self.prior_data_misfit = mean_data_misfit 930 self.data_misfit_std = std_data_misfit 931 932 else: 933 # for analysis debug... 934 list_datatypes = self.list_datatypes 935 list_act_datatypes = self.list_act_datatypes 936 list_states = self.list_states 937 cov_data = self.cov_data 938 obs_data_vector = self.obs_data_vector 939 _, pred_data = at.aug_obs_pred_data( 940 self.obs_data, self.pred_data, assim_index, self.list_datatypes) 941 obs_data = self.real_obs_data 942 943 if len(self.scale_data.shape) == 1: 944 Y = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * \ 945 np.dot(pred_data, self.proj) 946 else: 947 Y = solve(self.scale_data, np.dot(pred_data, self.proj)) 948 omega = np.eye(self.ne) + np.dot(self.W, self.proj) 949 LU = lu_factor(omega.T) 950 S = lu_solve(LU, Y.T).T 951 if len(self.scale_data.shape) == 1: 952 scaled_misfit = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 953 np.ones((1, self.ne))) * (obs_data - pred_data) 954 else: 955 scaled_misfit = solve(self.scale_data, (obs_data - pred_data)) 956 957 u, s, v = np.linalg.svd(S, full_matrices=False) 958 if self.trunc_energy < 1: 959 ti = (np.cumsum(s) / sum(s)) <= self.trunc_energy 960 u, s, v = u[:, ti].copy(), s[ti].copy(), v[ti, :].copy() 961 962 ps_inv = np.diag([el_s ** (-1) for el_s in s]) 963 if len(self.scale_data.shape) == 1: 964 X = np.dot(ps_inv, np.dot(u.T, np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 965 np.ones((1, self.ne))) * self.E)) 966 else: 967 X = np.dot(ps_inv, np.dot(u.T, solve(self.scale_data, self.E))) 968 Lam, z = np.linalg.eig(np.dot(X, X.T)) 969 970 X2 = np.dot(u, np.dot(ps_inv.T, z)) 971 972 X3_m = np.dot(S.T, X2) 973 # X3_old = np.dot(X2, np.linalg.solve(np.eye(len(Lam)) + np.diag(Lam), X2.T)) 974 step_m = np.dot(np.dot(X3_m, inv(np.eye(len(Lam)) + np.diag(Lam))), 975 np.dot(X3_m.T, self.W)) 976 977 if 'localization' in self.keys_da: 978 if self.keys_da['localization'][1][0] == 'autoadaloc': 979 loc_step_d = np.dot(np.linalg.pinv(self.aug_prior), self.localization.auto_ada_loc(self.aug_prior, 980 np.dot(np.dot(S.T, X2), 981 np.dot(inv( 982 np.eye(len(Lam)) + np.diag(Lam)), 983 np.dot(X2.T, scaled_misfit))), 984 self.list_states, 985 **{'prior_info': self.prior_info})) 986 self.step = self.lam * (self.W - (step_m + loc_step_d)) 987 else: 988 step_d = np.dot(np.linalg.inv(omega).T, np.dot(np.dot(Y.T, X2), 989 np.dot(inv(np.eye(len(Lam)) + np.diag(Lam)), 990 np.dot(X2.T, scaled_misfit)))) 991 self.step = self.lam * (self.W - (step_m + step_d)) 992 993 self.W -= self.step 994 995 aug_state_upd = np.dot(self.aug_prior, (np.eye( 996 self.ne) + self.W / np.sqrt(self.ne - 1))) 997 998 # Extract updated state variables from aug_update 999 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 1000 1001 self.state = at.limits(self.state, self.prior_info) 1002 1003 def check_convergence(self): 1004 """ 1005 Check if GN-EnRML have converged based on evaluation of change sizes of objective function, state and damping 1006 parameter. Very similar to original function, but exit if there is no reduction in obj. function. 1007 1008 Parameters 1009 ---------- 1010 prev_data_misfit : float 1011 Data misfit calculated from the previous update. 1012 1013 step : float 1014 Step size. 1015 1016 lam : float 1017 LM damping parameter. 1018 1019 Returns 1020 ------- 1021 conv : bool 1022 Logic variable indicating if the algorithm has converged. 1023 1024 status : bool 1025 Indicates whether the objective function has reduced. 1026 1027 why_stop : dict 1028 Dictionary with keys corresponding to convergence criteria, with logical variables indicating 1029 which of them has been met. 1030 1031 Changelog 1032 --------- 1033 - ST 3/6-16 1034 - ST 6/6-16: Added LM damping param. check 1035 - KF 16/11-20: Modified for GN-EnRML 1036 - KF 10/3-21: Output whether the method reduced the objective function 1037 """ 1038 # Prelude to calc. conv. check (everything done below is from calc_analysis) 1039 if hasattr(self, 'list_datatypes'): 1040 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 1041 list_datatypes = self.list_datatypes 1042 cov_data = self.cov_data 1043 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 1044 list_datatypes) 1045 mean_preddata = np.mean(pred_data, 1) 1046 else: 1047 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 1048 list_datatypes, _ = at.get_list_data_types(self.obs_data, assim_index) 1049 # cov_data = at.gen_covdata(self.datavar, assim_index, list_datatypes) 1050 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 1051 list_datatypes) 1052 # mean_preddata = np.mean(pred_data, 1) 1053 1054 success = False 1055 1056 # if inital conv. check, there are no prev_data_misfit 1057 if self.prev_data_misfit is None: 1058 self.data_misfit = np.mean(self.data_misfit) 1059 self.prev_data_misfit = self.data_misfit 1060 self.prev_data_misfit_std = self.data_misfit_std 1061 success = True 1062 # update the last mismatch, only if this was a reduction of the misfit 1063 if self.data_misfit < self.prev_data_misfit: 1064 self.prev_data_misfit = self.data_misfit 1065 self.prev_data_misfit_std = self.data_misfit_std 1066 success = True 1067 # if there was no reduction of the misfit, retain the old "valid" data misfit. 1068 1069 # Calc. std dev of data misfit (used to update lamda) 1070 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector), 1)), np.ones((1, self.ne))) # use the perturbed 1071 # data instead. 1072 mat_obs = self.real_obs_data 1073 if len(cov_data.shape) == 1: 1074 data_misfit = np.diag(np.dot((pred_data - mat_obs).T, 1075 np.dot(np.expand_dims(self.cov_data ** (-1), axis=1), 1076 np.ones((1, self.ne))) * (pred_data - mat_obs))) 1077 else: 1078 data_misfit = np.diag(np.dot((pred_data - mat_obs).T, 1079 solve(self.cov_data, (pred_data - mat_obs)))) 1080 self.data_misfit = np.mean(data_misfit) 1081 self.data_misfit_std = np.std(data_misfit) 1082 1083 # # Calc. mean data misfit for convergence check, using the updated state variable 1084 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 1085 # solve(cov_data, (mean_preddata - obs_data_vector))) 1086 # if self.data_misfit > self.prev_data_misfit: 1087 # print(f'\n\nMisfit increased from {self.prev_data_misfit:.1f} to {self.data_misfit:.1f}. Exiting') 1088 # self.logger.info(f'\n\nMisfit increased from {self.prev_data_misfit:.1f} to {self.data_misfit:.1f}. Exiting') 1089 1090 # Convergence check: Relative step size of data misfit or state change less than tolerance 1091 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol \ 1092 or np.any(abs(np.mean(self.step, 1)) < self.step_tol) \ 1093 or self.lam >= self.lam_max: 1094 # or self.data_misfit > self.prev_data_misfit: 1095 # Logical variables for conv. criteria 1096 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 1097 'data_misfit': self.data_misfit, 1098 'prev_data_misfit': self.prev_data_misfit, 1099 'step_size_stop': np.any(abs(np.mean(self.step, 1)) < self.step_tol), 1100 'step_size': self.step, 1101 'lambda': self.lam, 1102 'lambda_stop': self.lam >= self.lam_max} 1103 1104 if self.data_misfit >= self.prev_data_misfit: 1105 success = False 1106 self.logger.info(f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 1107 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 1108 else: 1109 self.logger.info(f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 1110 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 1111 1112 # Return conv = True, why_stop var. 1113 return True, success, why_stop 1114 1115 else: # conv. not met 1116 # Logical variables for conv. criteria 1117 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 1118 'data_misfit': self.data_misfit, 1119 'prev_data_misfit': self.prev_data_misfit, 1120 'step_size': self.step, 1121 'step_size_stop': np.any(abs(np.mean(self.step, 1)) < self.step_tol), 1122 'lambda': self.lam, 1123 'lambda_stop': self.lam >= self.lam_max} 1124 1125 ############################################### 1126 ##### update Lambda step-size values ########## 1127 ############################################### 1128 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 1129 # If reduction in mean data misfit, increase step length 1130 self.lam = self.lam + (self.lam_max - self.lam) * \ 1131 2 ** (-(self.iteration) / (self.gamma - 1)) 1132 success = True 1133 self.current_state = deepcopy(self.state) 1134 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 1135 # Accept itaration, but keep lam the same 1136 success = True 1137 self.current_state = deepcopy(self.state) 1138 else: # Reject iteration, and decrease step length 1139 self.lam = self.lam / self.gamma 1140 success = False 1141 1142 if success: 1143 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 1144 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for next analysis: ' 1145 f'{self.lam}') 1146 else: 1147 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 1148 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for repeated analysis: ' 1149 f'{self.lam}') 1150 # Reset data misfit to prev_data_misfit (because the current state is neglected) 1151 self.data_misfit = self.prev_data_misfit 1152 self.data_misfit_std = self.prev_data_misfit_std 1153 1154 return False, success, why_stop
44class lmenrmlMixIn(Ensemble): 45 """ 46 This is an implementation of EnRML using Levenberg-Marquardt. The update scheme is selected by a MixIn with multiple 47 update_methods_ns. This class must therefore facititate many different update schemes. 48 """ 49 50 def __init__(self, keys_da, keys_fwd, sim): 51 """ 52 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 53 `pipt.input_output.pipt_init.ReadInitFile`. 54 55 Parameters 56 ---------- 57 init_file: str 58 PIPT init. file containing info. to run the inversion algorithm 59 """ 60 # Pass the init_file upwards in the hierarchy 61 super().__init__(keys_da, keys_fwd, sim) 62 63 if self.restart is False: 64 # Save prior state in separate variable 65 self.prior_state = cp.deepcopy(self.state) 66 67 # Extract parameters like conv. tol. and damping param. from ITERATION keyword in DATAASSIM 68 self._ext_iter_param() 69 70 # Within variables 71 self.prev_data_misfit = None # Data misfit at previous iteration 72 if 'actnum' in self.keys_da.keys(): 73 try: 74 self.actnum = np.load(self.keys_da['actnum'])['actnum'] 75 except: 76 print('ACTNUM file cannot be loaded!') 77 else: 78 self.actnum = None 79 # At the moment, the iterative loop is threated as an iterative smoother and thus we check if assim. indices 80 # are given as in the Simultaneous loop. 81 self.check_assimindex_simultaneous() 82 # define the assimilation index 83 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 84 # define the list of states 85 self.list_states = list(self.state.keys()) 86 # define the list of datatypes 87 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 88 self.obs_data, self.assim_index) 89 # Get the perturbed observations and observation scaling 90 self.data_random_state = cp.deepcopy(np.random.get_state()) 91 self._ext_obs() 92 # Get state scaling and svd of scaled prior 93 self._ext_state() 94 self.current_state = cp.deepcopy(self.state) 95 96 def calc_analysis(self): 97 """ 98 Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with 99 the sensitivity matrix approximated by the ensemble. 100 """ 101 102 # reformat predicted data 103 _, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 104 self.list_datatypes) 105 106 if self.iteration == 1: # first iteration 107 data_misfit = at.calc_objectivefun( 108 self.real_obs_data, self.aug_pred_data, self.cov_data) 109 110 # Store the (mean) data misfit (also for conv. check) 111 self.data_misfit = np.mean(data_misfit) 112 self.prior_data_misfit = np.mean(data_misfit) 113 self.data_misfit_std = np.std(data_misfit) 114 115 if self.lam == 'auto': 116 self.lam = (0.5 * self.prior_data_misfit)/self.aug_pred_data.shape[0] 117 118 self.logger.info( 119 f'Prior run complete with data misfit: {self.prior_data_misfit:0.1f}. Lambda for initial analysis: {self.lam}') 120 121 if 'localanalysis' in self.keys_da: 122 self.local_analysis_update() 123 else: 124 # Mean pred_data and perturbation matrix with scaling 125 if len(self.scale_data.shape) == 1: 126 self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 127 np.ones((1, self.ne))) * np.dot(self.aug_pred_data, self.proj) 128 else: 129 self.pert_preddata = solve( 130 self.scale_data, np.dot(self.aug_pred_data, self.proj)) 131 132 aug_state = at.aug_state(self.current_state, self.list_states) 133 self.update() # run ordinary analysis 134 if hasattr(self, 'step'): 135 aug_state_upd = aug_state + self.step 136 if hasattr(self, 'w_step'): 137 self.W = self.current_W + self.w_step 138 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 139 aug_state_upd = np.dot(aug_prior_state, (np.eye( 140 self.ne) + self.W / np.sqrt(self.ne - 1))) 141 142 # Extract updated state variables from aug_update 143 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 144 self.state = at.limits(self.state, self.prior_info) 145 146 def check_convergence(self): 147 """ 148 Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping 149 parameter. 150 151 Returns 152 ------- 153 conv: bool 154 Logic variable telling if algorithm has converged 155 why_stop: dict 156 Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been 157 met 158 """ 159 160 _, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 161 self.list_datatypes) 162 # Initialize the initial success value 163 success = False 164 165 # if inital conv. check, there are no prev_data_misfit 166 self.prev_data_misfit = self.data_misfit 167 self.prev_data_misfit_std = self.data_misfit_std 168 169 # Calc. std dev of data misfit (used to update lamda) 170 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector),1)), np.ones((1, self.ne))) # use the perturbed 171 # data instead. 172 173 data_misfit = at.calc_objectivefun(self.real_obs_data, pred_data, self.cov_data) 174 175 self.data_misfit = np.mean(data_misfit) 176 self.data_misfit_std = np.std(data_misfit) 177 178 # # Calc. mean data misfit for convergence check, using the updated state variable 179 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 180 # solve(cov_data, (mean_preddata - obs_data_vector))) 181 182 # Convergence check: Relative step size of data misfit or state change less than tolerance 183 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol \ 184 or self.lam >= self.lam_max: 185 # Logical variables for conv. criteria 186 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 187 'data_misfit': self.data_misfit, 188 'prev_data_misfit': self.prev_data_misfit, 189 'lambda': self.lam, 190 'lambda_stop': self.lam >= self.lam_max} 191 192 if self.data_misfit >= self.prev_data_misfit: 193 success = False 194 self.logger.info( 195 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 196 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 197 else: 198 self.logger.info( 199 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 200 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 201 # Return conv = True, why_stop var. 202 return True, success, why_stop 203 204 else: # conv. not met 205 # Logical variables for conv. criteria 206 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 207 'data_misfit': self.data_misfit, 208 'prev_data_misfit': self.prev_data_misfit, 209 'lambda': self.lam, 210 'lambda_stop': self.lam >= self.lam_max} 211 212 ############################################### 213 ##### update Lambda step-size values ########## 214 ############################################### 215 # If reduction in mean data misfit, reduce damping param 216 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 217 # Reduce damping parameter (divide calculations for ANALYSISDEBUG purpose) 218 if self.lam > self.lam_min: 219 self.lam = self.lam / self.gamma 220 success = True 221 self.current_state = cp.deepcopy(self.state) 222 if hasattr(self, 'W'): 223 self.current_W = cp.deepcopy(self.W) 224 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 225 # accept itaration, but keep lam the same 226 success = True 227 self.current_state = cp.deepcopy(self.state) 228 if hasattr(self, 'W'): 229 self.current_W = cp.deepcopy(self.W) 230 231 else: # Reject iteration, and increase lam 232 # Increase damping parameter (divide calculations for ANALYSISDEBUG purpose) 233 self.lam = self.lam * self.gamma 234 success = False 235 236 if success: 237 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 238 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for next analysis: ' 239 f'{self.lam}') 240 # self.prev_data_misfit = self.data_misfit 241 # self.prev_data_misfit_std = self.data_misfit_std 242 else: 243 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 244 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for repeated analysis: ' 245 f'{self.lam}') 246 # Reset the objective function after report 247 self.data_misfit = self.prev_data_misfit 248 self.data_misfit_std = self.prev_data_misfit_std 249 250 # Return conv = False, why_stop var. 251 return False, success, why_stop 252 253 def _ext_iter_param(self): 254 """ 255 Extract parameters needed in LM-EnRML from the ITERATION keyword given in the DATAASSIM part of PIPT init. 256 file. These parameters include convergence tolerances and parameters for the damping parameter. Default 257 values for these parameters have been given here, if they are not provided in ITERATION. 258 """ 259 260 # Predefine all the default values 261 self.data_misfit_tol = 0.01 262 self.step_tol = 0.01 263 self.lam = 100 264 self.lam_max = 1e10 265 self.lam_min = 0.01 266 self.gamma = 5 267 self.trunc_energy = 0.95 268 self.iteration = 0 269 270 # Loop over options in ITERATION and extract the parameters we want 271 for i, opt in enumerate(list(zip(*self.keys_da['iteration']))[0]): 272 if opt == 'data_misfit_tol': 273 self.data_misfit_tol = self.keys_da['iteration'][i][1] 274 if opt == 'step_tol': 275 self.step_tol = self.keys_da['iteration'][i][1] 276 if opt == 'lambda': 277 self.lam = self.keys_da['iteration'][i][1] 278 if opt == 'lambda_max': 279 self.lam_max = self.keys_da['iteration'][i][1] 280 if opt == 'lambda_min': 281 self.lam_min = self.keys_da['iteration'][i][1] 282 if opt == 'lambda_factor': 283 self.gamma = self.keys_da['iteration'][i][1] 284 285 if 'energy' in self.keys_da: 286 # initial energy (Remember to extract this) 287 self.trunc_energy = self.keys_da['energy'] 288 if self.trunc_energy > 1: # ensure that it is given as percentage 289 self.trunc_energy /= 100.
This is an implementation of EnRML using Levenberg-Marquardt. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
50 def __init__(self, keys_da, keys_fwd, sim): 51 """ 52 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 53 `pipt.input_output.pipt_init.ReadInitFile`. 54 55 Parameters 56 ---------- 57 init_file: str 58 PIPT init. file containing info. to run the inversion algorithm 59 """ 60 # Pass the init_file upwards in the hierarchy 61 super().__init__(keys_da, keys_fwd, sim) 62 63 if self.restart is False: 64 # Save prior state in separate variable 65 self.prior_state = cp.deepcopy(self.state) 66 67 # Extract parameters like conv. tol. and damping param. from ITERATION keyword in DATAASSIM 68 self._ext_iter_param() 69 70 # Within variables 71 self.prev_data_misfit = None # Data misfit at previous iteration 72 if 'actnum' in self.keys_da.keys(): 73 try: 74 self.actnum = np.load(self.keys_da['actnum'])['actnum'] 75 except: 76 print('ACTNUM file cannot be loaded!') 77 else: 78 self.actnum = None 79 # At the moment, the iterative loop is threated as an iterative smoother and thus we check if assim. indices 80 # are given as in the Simultaneous loop. 81 self.check_assimindex_simultaneous() 82 # define the assimilation index 83 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 84 # define the list of states 85 self.list_states = list(self.state.keys()) 86 # define the list of datatypes 87 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 88 self.obs_data, self.assim_index) 89 # Get the perturbed observations and observation scaling 90 self.data_random_state = cp.deepcopy(np.random.get_state()) 91 self._ext_obs() 92 # Get state scaling and svd of scaled prior 93 self._ext_state() 94 self.current_state = cp.deepcopy(self.state)
The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in
pipt.input_output.pipt_init.ReadInitFile
.
Parameters
- init_file (str): PIPT init. file containing info. to run the inversion algorithm
96 def calc_analysis(self): 97 """ 98 Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with 99 the sensitivity matrix approximated by the ensemble. 100 """ 101 102 # reformat predicted data 103 _, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 104 self.list_datatypes) 105 106 if self.iteration == 1: # first iteration 107 data_misfit = at.calc_objectivefun( 108 self.real_obs_data, self.aug_pred_data, self.cov_data) 109 110 # Store the (mean) data misfit (also for conv. check) 111 self.data_misfit = np.mean(data_misfit) 112 self.prior_data_misfit = np.mean(data_misfit) 113 self.data_misfit_std = np.std(data_misfit) 114 115 if self.lam == 'auto': 116 self.lam = (0.5 * self.prior_data_misfit)/self.aug_pred_data.shape[0] 117 118 self.logger.info( 119 f'Prior run complete with data misfit: {self.prior_data_misfit:0.1f}. Lambda for initial analysis: {self.lam}') 120 121 if 'localanalysis' in self.keys_da: 122 self.local_analysis_update() 123 else: 124 # Mean pred_data and perturbation matrix with scaling 125 if len(self.scale_data.shape) == 1: 126 self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 127 np.ones((1, self.ne))) * np.dot(self.aug_pred_data, self.proj) 128 else: 129 self.pert_preddata = solve( 130 self.scale_data, np.dot(self.aug_pred_data, self.proj)) 131 132 aug_state = at.aug_state(self.current_state, self.list_states) 133 self.update() # run ordinary analysis 134 if hasattr(self, 'step'): 135 aug_state_upd = aug_state + self.step 136 if hasattr(self, 'w_step'): 137 self.W = self.current_W + self.w_step 138 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 139 aug_state_upd = np.dot(aug_prior_state, (np.eye( 140 self.ne) + self.W / np.sqrt(self.ne - 1))) 141 142 # Extract updated state variables from aug_update 143 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 144 self.state = at.limits(self.state, self.prior_info)
Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with the sensitivity matrix approximated by the ensemble.
146 def check_convergence(self): 147 """ 148 Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping 149 parameter. 150 151 Returns 152 ------- 153 conv: bool 154 Logic variable telling if algorithm has converged 155 why_stop: dict 156 Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been 157 met 158 """ 159 160 _, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 161 self.list_datatypes) 162 # Initialize the initial success value 163 success = False 164 165 # if inital conv. check, there are no prev_data_misfit 166 self.prev_data_misfit = self.data_misfit 167 self.prev_data_misfit_std = self.data_misfit_std 168 169 # Calc. std dev of data misfit (used to update lamda) 170 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector),1)), np.ones((1, self.ne))) # use the perturbed 171 # data instead. 172 173 data_misfit = at.calc_objectivefun(self.real_obs_data, pred_data, self.cov_data) 174 175 self.data_misfit = np.mean(data_misfit) 176 self.data_misfit_std = np.std(data_misfit) 177 178 # # Calc. mean data misfit for convergence check, using the updated state variable 179 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 180 # solve(cov_data, (mean_preddata - obs_data_vector))) 181 182 # Convergence check: Relative step size of data misfit or state change less than tolerance 183 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol \ 184 or self.lam >= self.lam_max: 185 # Logical variables for conv. criteria 186 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 187 'data_misfit': self.data_misfit, 188 'prev_data_misfit': self.prev_data_misfit, 189 'lambda': self.lam, 190 'lambda_stop': self.lam >= self.lam_max} 191 192 if self.data_misfit >= self.prev_data_misfit: 193 success = False 194 self.logger.info( 195 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 196 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 197 else: 198 self.logger.info( 199 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 200 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 201 # Return conv = True, why_stop var. 202 return True, success, why_stop 203 204 else: # conv. not met 205 # Logical variables for conv. criteria 206 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 207 'data_misfit': self.data_misfit, 208 'prev_data_misfit': self.prev_data_misfit, 209 'lambda': self.lam, 210 'lambda_stop': self.lam >= self.lam_max} 211 212 ############################################### 213 ##### update Lambda step-size values ########## 214 ############################################### 215 # If reduction in mean data misfit, reduce damping param 216 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 217 # Reduce damping parameter (divide calculations for ANALYSISDEBUG purpose) 218 if self.lam > self.lam_min: 219 self.lam = self.lam / self.gamma 220 success = True 221 self.current_state = cp.deepcopy(self.state) 222 if hasattr(self, 'W'): 223 self.current_W = cp.deepcopy(self.W) 224 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 225 # accept itaration, but keep lam the same 226 success = True 227 self.current_state = cp.deepcopy(self.state) 228 if hasattr(self, 'W'): 229 self.current_W = cp.deepcopy(self.W) 230 231 else: # Reject iteration, and increase lam 232 # Increase damping parameter (divide calculations for ANALYSISDEBUG purpose) 233 self.lam = self.lam * self.gamma 234 success = False 235 236 if success: 237 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 238 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for next analysis: ' 239 f'{self.lam}') 240 # self.prev_data_misfit = self.data_misfit 241 # self.prev_data_misfit_std = self.data_misfit_std 242 else: 243 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 244 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for repeated analysis: ' 245 f'{self.lam}') 246 # Reset the objective function after report 247 self.data_misfit = self.prev_data_misfit 248 self.data_misfit_std = self.prev_data_misfit_std 249 250 # Return conv = False, why_stop var. 251 return False, success, why_stop
Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping parameter.
Returns
- conv (bool): Logic variable telling if algorithm has converged
- why_stop (dict): Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been met
Inherited Members
This is an implementation of EnRML using Levenberg-Marquardt. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
Inherited Members
- pipt.loop.ensemble.Ensemble
- logger
- check_assimindex_sequential
- check_assimindex_simultaneous
- save_temp_state_assim
- save_temp_state_iter
- save_temp_state_mda
- save_temp_state_ml
- compress
- local_analysis_update
- ensemble.ensemble.Ensemble
- keys_en
- sim
- pred_data
- aux_input
- pickle_restart_file
- restart
- gen_init_ensemble
- get_list_assim_steps
- calc_prediction
- save
- load
- calc_ml_prediction
- pipt.update_schemes.update_methods_ns.approx_update.approx_update
- update
This is an implementation of EnRML using Levenberg-Marquardt. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
Inherited Members
- pipt.loop.ensemble.Ensemble
- logger
- check_assimindex_sequential
- check_assimindex_simultaneous
- save_temp_state_assim
- save_temp_state_iter
- save_temp_state_mda
- save_temp_state_ml
- compress
- local_analysis_update
- ensemble.ensemble.Ensemble
- keys_en
- sim
- pred_data
- aux_input
- pickle_restart_file
- restart
- gen_init_ensemble
- get_list_assim_steps
- calc_prediction
- save
- load
- calc_ml_prediction
- pipt.update_schemes.update_methods_ns.full_update.full_update
- update
This is an implementation of EnRML using Levenberg-Marquardt. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
Inherited Members
- pipt.loop.ensemble.Ensemble
- logger
- check_assimindex_sequential
- check_assimindex_simultaneous
- save_temp_state_assim
- save_temp_state_iter
- save_temp_state_mda
- save_temp_state_ml
- compress
- local_analysis_update
- ensemble.ensemble.Ensemble
- keys_en
- sim
- pred_data
- aux_input
- pickle_restart_file
- restart
- gen_init_ensemble
- get_list_assim_steps
- calc_prediction
- save
- load
- calc_ml_prediction
- pipt.update_schemes.update_methods_ns.subspace_update.subspace_update
- update
304class gnenrmlMixIn(Ensemble): 305 """ 306 This is an implementation of EnRML using the Gauss-Newton approach. The update scheme is selected by a MixIn with multiple 307 update_methods_ns. This class must therefore facititate many different update schemes. 308 """ 309 310 def __init__(self, keys_da, keys_fwd, sim): 311 """ 312 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 313 `pipt.input_output.pipt_init.ReadInitFile`. 314 315 Parameters 316 ---------- 317 init_file: str 318 PIPT init. file containing info. to run the inversion algorithm 319 """ 320 # Pass the init_file upwards in the hierarchy 321 super().__init__(keys_da, keys_fwd, sim) 322 323 if self.restart is False: 324 # Save prior state in separate variable 325 self.prior_state = cp.deepcopy(self.state) 326 327 # extract and save state scaling 328 329 # Extract parameters like conv. tol. and damping param. from ITERATION keyword in DATAASSIM 330 self._ext_iter_param() 331 332 # Within variables 333 self.prev_data_misfit = None # Data misfit at previous iteration 334 if 'actnum' in self.keys_da.keys(): 335 try: 336 self.actnum = np.load(self.keys_da['actnum'])['actnum'] 337 except: 338 print('ACTNUM file cannot be loaded!') 339 else: 340 self.actnum = None 341 # At the moment, the iterative loop is threated as an iterative smoother and thus we check if assim. indices 342 # are given as in the Simultaneous loop. 343 self.check_assimindex_simultaneous() 344 # define the assimilation index 345 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 346 # define the list of states 347 self.list_states = list(self.state.keys()) 348 # define the list of datatypes 349 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 350 self.obs_data, self.assim_index) 351 # Get the perturbed observations and observation scaling 352 self._ext_obs() 353 # Get state scaling and svd of scaled prior 354 self._ext_state() 355 self.current_state = cp.deepcopy(self.state) 356 # ensure that the updates does not invoke the LM inflation of the Hessian. 357 self.lam = 0 358 359 def _ext_obs(self): 360 361 self.obs_data_vector, _ = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 362 self.list_datatypes) 363 364 # Generate the data auto-covariance matrix 365 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 366 if hasattr(self, 'cov_data'): # cd matrix has been imported 367 tmp_E = np.dot(cholesky(self.cov_data).T, 368 np.random.randn(self.cov_data.shape[0], self.ne)) 369 else: 370 tmp_E = at.extract_tot_empirical_cov( 371 self.datavar, self.assim_index, self.list_datatypes, self.ne) 372 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 373 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 374 tmp_E = at.screen_data(tmp_E, self.aug_pred_data, 375 self.obs_data_vector, self.iteration) 376 self.E = tmp_E 377 self.real_obs_data = self.obs_data_vector[:, np.newaxis] - tmp_E 378 379 self.cov_data = np.var(self.E, ddof=1, 380 axis=1) # calculate the variance, to be used for e.g. data misfit calc 381 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 382 self.scale_data = np.sqrt(self.cov_data) 383 else: 384 if not hasattr(self, 'cov_data'): # if cd is not loaded 385 self.cov_data = at.gen_covdata( 386 self.datavar, self.assim_index, self.list_datatypes) 387 # data screening 388 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 389 self.cov_data = at.screen_data( 390 self.cov_data, self.aug_pred_data, self.obs_data_vector, self.iteration) 391 392 init_en = Cholesky() # Initialize GeoStat class for generating realizations 393 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 394 return_chol=True) 395 396 def _ext_state(self): 397 # get vector of scaling 398 self.state_scaling = at.calc_scaling( 399 self.prior_state, self.list_states, self.prior_info) 400 401 delta_scaled_prior = self.state_scaling[:, None] * \ 402 np.dot(at.aug_state(self.prior_state, self.list_states), self.proj) 403 404 u_d, s_d, v_d = np.linalg.svd(delta_scaled_prior, full_matrices=False) 405 406 # remove the last singular value/vector. This is because numpy returns all ne values, while the last is actually 407 # zero. This part is a good place to include eventual additional truncation. 408 energy = 0 409 trunc_index = len(s_d) - 1 # inititallize 410 for c, elem in enumerate(s_d): 411 energy += elem 412 if energy / sum(s_d) >= self.trunc_energy: 413 trunc_index = c # take the index where all energy is preserved 414 break 415 u_d, s_d, v_d = u_d[:, :trunc_index + 416 1], s_d[:trunc_index + 1], v_d[:trunc_index + 1, :] 417 self.Am = np.dot(u_d, np.eye(trunc_index+1) * 418 ((s_d**(-1))[:, None])) # notation from paper 419 420 def calc_analysis(self): 421 """ 422 Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with 423 the sensitivity matrix approximated by the ensemble. 424 425 """ 426 427 # reformat predicted data 428 _, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 429 self.list_datatypes) 430 431 if self.iteration == 1: # first iteration 432 data_misfit = at.calc_objectivefun( 433 self.real_obs_data, self.aug_pred_data, self.cov_data) 434 435 # Store the (mean) data misfit (also for conv. check) 436 self.data_misfit = np.mean(data_misfit) 437 self.prior_data_misfit = np.mean(data_misfit) 438 self.data_misfit_std = np.std(data_misfit) 439 440 if self.gamma == 'auto': 441 self.gamma = 0.1 442 443 # Mean pred_data and perturbation matrix with scaling 444 if len(self.scale_data.shape) == 1: 445 self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 446 np.ones((1, self.ne))) * np.dot(self.aug_pred_data, self.proj) 447 else: 448 self.pert_preddata = solve( 449 self.scale_data, np.dot(self.aug_pred_data, self.proj)) 450 451 aug_state = at.aug_state(self.current_state, self.list_states) 452 453 self.update() # run analysis 454 if hasattr(self, 'step'): 455 aug_state_upd = aug_state + self.gamma*self.step 456 if hasattr(self, 'w_step'): 457 self.W = self.current_W + self.gamma*self.w_step 458 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 459 aug_state_upd = np.dot(aug_prior_state, (np.eye( 460 self.ne) + self.W / np.sqrt(self.ne - 1))) 461 if hasattr(self, 'sqrt_w_step'): # if we do a sqrt update 462 self.w = self.current_w + self.gamma*self.sqrt_w_step 463 new_mean_state = self.mean_prior + np.dot(self.X, self.w) 464 u, sigma, v = np.linalg.svd(self.C_w, full_matrices=True) 465 sigma_inv_sqrt = np.diag([el_s ** (-1 / 2) for el_s in sigma]) 466 C_w_inv_sqrt = np.dot(np.dot(u, sigma_inv_sqrt), v.T) 467 self.W = C_w_inv_sqrt * np.sqrt(self.ne - 1) 468 aug_state_upd = np.tile(new_mean_state, (self.ne, 1) 469 ).T + np.dot(self.X, self.W) 470 471 # Extract updated state variables from aug_update 472 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 473 self.state = at.limits(self.state, self.prior_info) 474 475 def check_convergence(self): 476 """ 477 Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping 478 parameter. 479 480 Returns 481 ------- 482 conv: bool 483 Logic variable telling if algorithm has converged 484 why_stop: dict 485 Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been 486 met 487 """ 488 # Prelude to calc. conv. check (everything done below is from calc_analysis) 489 if hasattr(self, 'list_datatypes'): 490 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 491 list_datatypes = self.list_datatypes 492 cov_data = self.cov_data 493 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 494 list_datatypes) 495 mean_preddata = np.mean(pred_data, 1) 496 else: 497 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 498 list_datatypes, _ = at.get_list_data_types(self.obs_data, assim_index) 499 # cov_data = at.gen_covdata(self.datavar, assim_index, list_datatypes) 500 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 501 list_datatypes) 502 # mean_preddata = np.mean(pred_data, 1) 503 504 # Initialize the initial success value 505 success = False 506 507 # if inital conv. check, there are no prev_data_misfit 508 if self.prev_data_misfit is None: 509 self.data_misfit = np.mean(self.data_misfit) 510 self.prev_data_misfit = self.data_misfit 511 self.prev_data_misfit_std = self.data_misfit_std 512 success = True 513 514 # update the last mismatch, only if this was a reduction of the misfit 515 if self.data_misfit < self.prev_data_misfit: 516 self.prev_data_misfit = self.data_misfit 517 self.prev_data_misfit_std = self.data_misfit_std 518 success = True 519 # if there was no reduction of the misfit, retain the old "valid" data misfit. 520 521 # Calc. std dev of data misfit (used to update lamda) 522 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector),1)), np.ones((1, self.ne))) # use the perturbed 523 # data instead. 524 mat_obs = self.real_obs_data 525 data_misfit = at.calc_objectivefun(mat_obs, pred_data, self.cov_data) 526 527 self.data_misfit = np.mean(data_misfit) 528 self.data_misfit_std = np.std(data_misfit) 529 530 # # Calc. mean data misfit for convergence check, using the updated state variable 531 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 532 # solve(cov_data, (mean_preddata - obs_data_vector))) 533 534 # Convergence check: Relative step size of data misfit or state change less than tolerance 535 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol: 536 # Logical variables for conv. criteria 537 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 538 'data_misfit': self.data_misfit, 539 'prev_data_misfit': self.prev_data_misfit, 540 'gamma': self.gamma, 541 } 542 543 if self.data_misfit >= self.prev_data_misfit: 544 success = False 545 self.logger.info( 546 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 547 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 548 else: 549 self.logger.info( 550 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 551 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 552 # Return conv = True, why_stop var. 553 return True, success, why_stop 554 555 else: # conv. not met 556 # Logical variables for conv. criteria 557 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 558 'data_misfit': self.data_misfit, 559 'prev_data_misfit': self.prev_data_misfit, 560 'gamma': self.gamma} 561 562 ############################################### 563 ##### update Lambda step-size values ########## 564 ############################################### 565 # If reduction in mean data misfit, reduce damping param 566 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 567 # Reduce damping parameter (divide calculations for ANALYSISDEBUG purpose) 568 self.gamma = self.gamma + (self.gamma_max - self.gamma) * 2 ** ( 569 -(self.iteration) / (self.gamma_factor - 1)) 570 success = True 571 self.current_state = cp.deepcopy(self.state) 572 if hasattr(self, 'W'): 573 self.current_W = cp.deepcopy(self.W) 574 575 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 576 # accept itaration, but keep lam the same 577 success = True 578 self.current_state = cp.deepcopy(self.state) 579 if hasattr(self, 'W'): 580 self.current_W = cp.deepcopy(self.W) 581 582 else: # Reject iteration, and increase lam 583 # Increase damping parameter (divide calculations for ANALYSISDEBUG purpose) 584 err_str = f"Misfit increased. Set new start step length and try again. Final ojective function value is {self.data_misfit:0.1f}" 585 self.logger.info(err_str) 586 sys.exit(err_str) 587 success = False 588 589 if success: 590 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 591 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Gamma for next analysis: ' 592 f'{self.gamma}') 593 else: 594 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 595 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Gamma for repeated analysis: ' 596 f'{self.gamma}') 597 598 # Return conv = False, why_stop var. 599 return False, success, why_stop 600 601 def _ext_iter_param(self): 602 """ 603 Extract parameters needed in LM-EnRML from the ITERATION keyword given in the DATAASSIM part of PIPT init. 604 file. These parameters include convergence tolerances and parameters for the damping parameter. Default 605 values for these parameters have been given here, if they are not provided in ITERATION. 606 """ 607 608 # Predefine all the default values 609 self.data_misfit_tol = 0.01 610 self.step_tol = 0.01 611 self.gamma = 0.2 612 self.gamma_max = 0.5 613 self.gamma_factor = 2.5 614 self.trunc_energy = 0.95 615 self.iteration = 0 616 617 # Loop over options in ITERATION and extract the parameters we want 618 for i, opt in enumerate(list(zip(*self.keys_da['iteration']))[0]): 619 if opt == 'data_misfit_tol': 620 self.data_misfit_tol = self.keys_da['iteration'][i][1] 621 if opt == 'step_tol': 622 self.step_tol = self.keys_da['iteration'][i][1] 623 if opt == 'gamma': 624 self.gamma = self.keys_da['iteration'][i][1] 625 if opt == 'gamma_max': 626 self.gamma_max = self.keys_da['iteration'][i][1] 627 if opt == 'gamma_factor': 628 self.gamma_factor = self.keys_da['iteration'][i][1] 629 630 if 'energy' in self.keys_da: 631 # initial energy (Remember to extract this) 632 self.trunc_energy = self.keys_da['energy'] 633 if self.trunc_energy > 1: # ensure that it is given as percentage 634 self.trunc_energy /= 100.
This is an implementation of EnRML using the Gauss-Newton approach. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
310 def __init__(self, keys_da, keys_fwd, sim): 311 """ 312 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 313 `pipt.input_output.pipt_init.ReadInitFile`. 314 315 Parameters 316 ---------- 317 init_file: str 318 PIPT init. file containing info. to run the inversion algorithm 319 """ 320 # Pass the init_file upwards in the hierarchy 321 super().__init__(keys_da, keys_fwd, sim) 322 323 if self.restart is False: 324 # Save prior state in separate variable 325 self.prior_state = cp.deepcopy(self.state) 326 327 # extract and save state scaling 328 329 # Extract parameters like conv. tol. and damping param. from ITERATION keyword in DATAASSIM 330 self._ext_iter_param() 331 332 # Within variables 333 self.prev_data_misfit = None # Data misfit at previous iteration 334 if 'actnum' in self.keys_da.keys(): 335 try: 336 self.actnum = np.load(self.keys_da['actnum'])['actnum'] 337 except: 338 print('ACTNUM file cannot be loaded!') 339 else: 340 self.actnum = None 341 # At the moment, the iterative loop is threated as an iterative smoother and thus we check if assim. indices 342 # are given as in the Simultaneous loop. 343 self.check_assimindex_simultaneous() 344 # define the assimilation index 345 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 346 # define the list of states 347 self.list_states = list(self.state.keys()) 348 # define the list of datatypes 349 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 350 self.obs_data, self.assim_index) 351 # Get the perturbed observations and observation scaling 352 self._ext_obs() 353 # Get state scaling and svd of scaled prior 354 self._ext_state() 355 self.current_state = cp.deepcopy(self.state) 356 # ensure that the updates does not invoke the LM inflation of the Hessian. 357 self.lam = 0
The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in
pipt.input_output.pipt_init.ReadInitFile
.
Parameters
- init_file (str): PIPT init. file containing info. to run the inversion algorithm
420 def calc_analysis(self): 421 """ 422 Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with 423 the sensitivity matrix approximated by the ensemble. 424 425 """ 426 427 # reformat predicted data 428 _, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 429 self.list_datatypes) 430 431 if self.iteration == 1: # first iteration 432 data_misfit = at.calc_objectivefun( 433 self.real_obs_data, self.aug_pred_data, self.cov_data) 434 435 # Store the (mean) data misfit (also for conv. check) 436 self.data_misfit = np.mean(data_misfit) 437 self.prior_data_misfit = np.mean(data_misfit) 438 self.data_misfit_std = np.std(data_misfit) 439 440 if self.gamma == 'auto': 441 self.gamma = 0.1 442 443 # Mean pred_data and perturbation matrix with scaling 444 if len(self.scale_data.shape) == 1: 445 self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 446 np.ones((1, self.ne))) * np.dot(self.aug_pred_data, self.proj) 447 else: 448 self.pert_preddata = solve( 449 self.scale_data, np.dot(self.aug_pred_data, self.proj)) 450 451 aug_state = at.aug_state(self.current_state, self.list_states) 452 453 self.update() # run analysis 454 if hasattr(self, 'step'): 455 aug_state_upd = aug_state + self.gamma*self.step 456 if hasattr(self, 'w_step'): 457 self.W = self.current_W + self.gamma*self.w_step 458 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 459 aug_state_upd = np.dot(aug_prior_state, (np.eye( 460 self.ne) + self.W / np.sqrt(self.ne - 1))) 461 if hasattr(self, 'sqrt_w_step'): # if we do a sqrt update 462 self.w = self.current_w + self.gamma*self.sqrt_w_step 463 new_mean_state = self.mean_prior + np.dot(self.X, self.w) 464 u, sigma, v = np.linalg.svd(self.C_w, full_matrices=True) 465 sigma_inv_sqrt = np.diag([el_s ** (-1 / 2) for el_s in sigma]) 466 C_w_inv_sqrt = np.dot(np.dot(u, sigma_inv_sqrt), v.T) 467 self.W = C_w_inv_sqrt * np.sqrt(self.ne - 1) 468 aug_state_upd = np.tile(new_mean_state, (self.ne, 1) 469 ).T + np.dot(self.X, self.W) 470 471 # Extract updated state variables from aug_update 472 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 473 self.state = at.limits(self.state, self.prior_info)
Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with the sensitivity matrix approximated by the ensemble.
475 def check_convergence(self): 476 """ 477 Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping 478 parameter. 479 480 Returns 481 ------- 482 conv: bool 483 Logic variable telling if algorithm has converged 484 why_stop: dict 485 Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been 486 met 487 """ 488 # Prelude to calc. conv. check (everything done below is from calc_analysis) 489 if hasattr(self, 'list_datatypes'): 490 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 491 list_datatypes = self.list_datatypes 492 cov_data = self.cov_data 493 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 494 list_datatypes) 495 mean_preddata = np.mean(pred_data, 1) 496 else: 497 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 498 list_datatypes, _ = at.get_list_data_types(self.obs_data, assim_index) 499 # cov_data = at.gen_covdata(self.datavar, assim_index, list_datatypes) 500 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 501 list_datatypes) 502 # mean_preddata = np.mean(pred_data, 1) 503 504 # Initialize the initial success value 505 success = False 506 507 # if inital conv. check, there are no prev_data_misfit 508 if self.prev_data_misfit is None: 509 self.data_misfit = np.mean(self.data_misfit) 510 self.prev_data_misfit = self.data_misfit 511 self.prev_data_misfit_std = self.data_misfit_std 512 success = True 513 514 # update the last mismatch, only if this was a reduction of the misfit 515 if self.data_misfit < self.prev_data_misfit: 516 self.prev_data_misfit = self.data_misfit 517 self.prev_data_misfit_std = self.data_misfit_std 518 success = True 519 # if there was no reduction of the misfit, retain the old "valid" data misfit. 520 521 # Calc. std dev of data misfit (used to update lamda) 522 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector),1)), np.ones((1, self.ne))) # use the perturbed 523 # data instead. 524 mat_obs = self.real_obs_data 525 data_misfit = at.calc_objectivefun(mat_obs, pred_data, self.cov_data) 526 527 self.data_misfit = np.mean(data_misfit) 528 self.data_misfit_std = np.std(data_misfit) 529 530 # # Calc. mean data misfit for convergence check, using the updated state variable 531 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 532 # solve(cov_data, (mean_preddata - obs_data_vector))) 533 534 # Convergence check: Relative step size of data misfit or state change less than tolerance 535 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol: 536 # Logical variables for conv. criteria 537 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 538 'data_misfit': self.data_misfit, 539 'prev_data_misfit': self.prev_data_misfit, 540 'gamma': self.gamma, 541 } 542 543 if self.data_misfit >= self.prev_data_misfit: 544 success = False 545 self.logger.info( 546 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 547 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 548 else: 549 self.logger.info( 550 f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 551 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 552 # Return conv = True, why_stop var. 553 return True, success, why_stop 554 555 else: # conv. not met 556 # Logical variables for conv. criteria 557 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 558 'data_misfit': self.data_misfit, 559 'prev_data_misfit': self.prev_data_misfit, 560 'gamma': self.gamma} 561 562 ############################################### 563 ##### update Lambda step-size values ########## 564 ############################################### 565 # If reduction in mean data misfit, reduce damping param 566 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 567 # Reduce damping parameter (divide calculations for ANALYSISDEBUG purpose) 568 self.gamma = self.gamma + (self.gamma_max - self.gamma) * 2 ** ( 569 -(self.iteration) / (self.gamma_factor - 1)) 570 success = True 571 self.current_state = cp.deepcopy(self.state) 572 if hasattr(self, 'W'): 573 self.current_W = cp.deepcopy(self.W) 574 575 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 576 # accept itaration, but keep lam the same 577 success = True 578 self.current_state = cp.deepcopy(self.state) 579 if hasattr(self, 'W'): 580 self.current_W = cp.deepcopy(self.W) 581 582 else: # Reject iteration, and increase lam 583 # Increase damping parameter (divide calculations for ANALYSISDEBUG purpose) 584 err_str = f"Misfit increased. Set new start step length and try again. Final ojective function value is {self.data_misfit:0.1f}" 585 self.logger.info(err_str) 586 sys.exit(err_str) 587 success = False 588 589 if success: 590 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 591 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Gamma for next analysis: ' 592 f'{self.gamma}') 593 else: 594 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 595 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Gamma for repeated analysis: ' 596 f'{self.gamma}') 597 598 # Return conv = False, why_stop var. 599 return False, success, why_stop
Check if LM-EnRML have converged based on evaluation of change sizes of objective function, state and damping parameter.
Returns
- conv (bool): Logic variable telling if algorithm has converged
- why_stop (dict): Dict. with keys corresponding to conv. criteria, with logical variable telling which of them that has been met
Inherited Members
This is an implementation of EnRML using the Gauss-Newton approach. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
Inherited Members
- pipt.loop.ensemble.Ensemble
- logger
- check_assimindex_sequential
- check_assimindex_simultaneous
- save_temp_state_assim
- save_temp_state_iter
- save_temp_state_mda
- save_temp_state_ml
- compress
- local_analysis_update
- ensemble.ensemble.Ensemble
- keys_en
- sim
- pred_data
- aux_input
- pickle_restart_file
- restart
- gen_init_ensemble
- get_list_assim_steps
- calc_prediction
- save
- load
- calc_ml_prediction
- pipt.update_schemes.update_methods_ns.approx_update.approx_update
- update
This is an implementation of EnRML using the Gauss-Newton approach. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
Inherited Members
- pipt.loop.ensemble.Ensemble
- logger
- check_assimindex_sequential
- check_assimindex_simultaneous
- save_temp_state_assim
- save_temp_state_iter
- save_temp_state_mda
- save_temp_state_ml
- compress
- local_analysis_update
- ensemble.ensemble.Ensemble
- keys_en
- sim
- pred_data
- aux_input
- pickle_restart_file
- restart
- gen_init_ensemble
- get_list_assim_steps
- calc_prediction
- save
- load
- calc_ml_prediction
- pipt.update_schemes.update_methods_ns.full_update.full_update
- update
This is an implementation of EnRML using the Gauss-Newton approach. The update scheme is selected by a MixIn with multiple update_methods_ns. This class must therefore facititate many different update schemes.
Inherited Members
- pipt.loop.ensemble.Ensemble
- logger
- check_assimindex_sequential
- check_assimindex_simultaneous
- save_temp_state_assim
- save_temp_state_iter
- save_temp_state_mda
- save_temp_state_ml
- compress
- local_analysis_update
- ensemble.ensemble.Ensemble
- keys_en
- sim
- pred_data
- aux_input
- pickle_restart_file
- restart
- gen_init_ensemble
- get_list_assim_steps
- calc_prediction
- save
- load
- calc_ml_prediction
- pipt.update_schemes.update_methods_ns.subspace_update.subspace_update
- update
649class gnenrml_margis(gnenrmlMixIn, margIS_update): 650 ''' 651 The marg-IS scheme is currently not available in this version of PIPT. To utilize the scheme you have to import the 652 *margIS_update* class from a standalone repository. 653 ''' 654 pass
The marg-IS scheme is currently not available in this version of PIPT. To utilize the scheme you have to import the margIS_update class from a standalone repository.
Inherited Members
657class co_lm_enrml(lmenrmlMixIn, approx_update): 658 """ 659 This is the implementation of the approximative LM-EnRML algorithm as described in [1]. 660 661 This algorithm is quite similar to the lm_enrml as provided above, and will therefore inherit most of its methods. 662 We only change the calc_analysis part... 663 664 % Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS 665 """ 666 667 def __init__(self, keys_da): 668 """ 669 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 670 `pipt.input_output.pipt_init.ReadInitFile`. 671 672 Parameters 673 ---------- 674 init_file: str 675 PIPT init. file containing info. to run the inversion algorithm 676 677 References 678 ---------- 679 [1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient 680 History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4): 689-703 681 """ 682 # Call __init__ in parent class 683 super().__init__(keys_da) 684 685 def calc_analysis(self): 686 """ 687 Calculate the update step in approximate LM-EnRML code. 688 689 Parameters 690 ---------- 691 iteration: int 692 Iteration number 693 694 Returns 695 ------- 696 success: bool 697 True if data mismatch is decreasing, False if increasing 698 699 References 700 ---------- 701 [1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient 702 History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4): 689-703 703 """ 704 # Get assimilation order as a list 705 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 706 707 # When handling large cases, it may be very costly to assemble the data covariance and localizaton matrix. 708 # To alleviate this in the simultuaneus-iterative scheme we store these matrices, the list of states and 709 # the list of data types after the first iteration. 710 711 if not hasattr(self, 'list_datatypes'): 712 # Get list of data types to be assimilated and of the free states. Do this once, because listing keys from a 713 # Python dictionary just when needed (in different places) may not yield the same list! 714 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 715 self.obs_data, self.assim_index) 716 list_datatypes = self.list_datatypes 717 self.list_states = list(self.state.keys()) 718 list_states = self.list_states 719 list_act_datatypes = self.list_act_datatypes 720 721 # self.cov_data = np.load('CD.npz')['arr_0'] 722 # Generate the realizations of the observed data once 723 # Augment observed and predicted data 724 self.obs_data_vector, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 725 self.list_datatypes) 726 obs_data_vector = self.obs_data_vector 727 728 # Generate the data auto-covariance matrix 729 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 730 if hasattr(self, 'cov_data'): # cd matrix has been imported 731 tmp_E = np.dot(cholesky(self.cov_data).T, 732 np.random.randn(self.cov_data.shape[0], self.ne)) 733 else: 734 tmp_E = at.extract_tot_empirical_cov( 735 self.datavar, self.assim_index, self.list_datatypes, self.ne) 736 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 737 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 738 tmp_E = at.screen_data(tmp_E, self.aug_pred_data, 739 obs_data_vector, self.iteration) 740 self.E = tmp_E 741 self.real_obs_data = obs_data_vector[:, np.newaxis] - tmp_E 742 743 self.cov_data = np.var(self.E, ddof=1, 744 axis=1) # calculate the variance, to be used for e.g. data misfit calc 745 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 746 self.scale_data = np.sqrt(self.cov_data) 747 else: 748 if not hasattr(self, 'cov_data'): # if cd is not loaded 749 self.cov_data = at.gen_covdata( 750 self.datavar, self.assim_index, self.list_datatypes) 751 # data screening 752 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 753 self.cov_data = at.screen_data( 754 self.cov_data, self.aug_pred_data, obs_data_vector, self.iteration) 755 756 init_en = Cholesky() # Initialize GeoStat class for generating realizations 757 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 758 return_chol=True) 759 760 self.datavar = at.update_datavar( 761 self.cov_data, self.datavar, self.assim_index, self.list_datatypes) 762 self.current_state = cp.deepcopy(self.state) 763 764 # Calc. misfit for the initial iteration 765 data_misfit = at.calc_objectivefun( 766 self.real_obs_data, self.aug_pred_data, self.cov_data) 767 # Store the (mean) data misfit (also for conv. check) 768 self.data_misfit = np.mean(data_misfit) 769 self.prior_data_misfit = np.mean(data_misfit) 770 self.data_misfit_std = np.std(data_misfit) 771 772 if self.lam == 'auto': 773 self.lam = 0.5 * self.prior_data_misfit 774 775 else: 776 _, self.aug_pred_data = at.aug_obs_pred_data( 777 self.obs_data, self.pred_data, assim_index, self.list_datatypes) 778 779 # Mean pred_data and perturbation matrix with scaling 780 mean_preddata = np.mean(self.aug_pred_data, 1) 781 if len(self.scale_data.shape) == 1: 782 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 783 pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * ( 784 self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) 785 else: 786 pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * ( 787 self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) / \ 788 (np.sqrt(self.ne - 1)) 789 else: 790 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 791 pert_preddata = solve(self.scale_data, self.aug_pred_data - 792 np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) 793 else: 794 pert_preddata = solve(self.scale_data, self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) / \ 795 (np.sqrt(self.ne - 1)) 796 self.pert_preddata = pert_preddata 797 798 self.update() 799 if hasattr(self, 'step'): 800 aug_state_upd = aug_state + self.step 801 if hasattr(self, 'w_step'): 802 self.W = self.current_W - self.w_step 803 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 804 aug_state_upd = np.dot(aug_prior_state, (np.eye( 805 self.ne) + self.W / np.sqrt(self.ne - 1))) 806 807 # Extract updated state variables from aug_update 808 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 809 self.state = at.limits(self.state, self.prior_info)
This is the implementation of the approximative LM-EnRML algorithm as described in [1].
This algorithm is quite similar to the lm_enrml as provided above, and will therefore inherit most of its methods. We only change the calc_analysis part...
% Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS
667 def __init__(self, keys_da): 668 """ 669 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 670 `pipt.input_output.pipt_init.ReadInitFile`. 671 672 Parameters 673 ---------- 674 init_file: str 675 PIPT init. file containing info. to run the inversion algorithm 676 677 References 678 ---------- 679 [1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient 680 History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4): 689-703 681 """ 682 # Call __init__ in parent class 683 super().__init__(keys_da)
The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in
pipt.input_output.pipt_init.ReadInitFile
.
Parameters
- init_file (str): PIPT init. file containing info. to run the inversion algorithm
References
[1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4): 689-703
685 def calc_analysis(self): 686 """ 687 Calculate the update step in approximate LM-EnRML code. 688 689 Parameters 690 ---------- 691 iteration: int 692 Iteration number 693 694 Returns 695 ------- 696 success: bool 697 True if data mismatch is decreasing, False if increasing 698 699 References 700 ---------- 701 [1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient 702 History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4): 689-703 703 """ 704 # Get assimilation order as a list 705 self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 706 707 # When handling large cases, it may be very costly to assemble the data covariance and localizaton matrix. 708 # To alleviate this in the simultuaneus-iterative scheme we store these matrices, the list of states and 709 # the list of data types after the first iteration. 710 711 if not hasattr(self, 'list_datatypes'): 712 # Get list of data types to be assimilated and of the free states. Do this once, because listing keys from a 713 # Python dictionary just when needed (in different places) may not yield the same list! 714 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 715 self.obs_data, self.assim_index) 716 list_datatypes = self.list_datatypes 717 self.list_states = list(self.state.keys()) 718 list_states = self.list_states 719 list_act_datatypes = self.list_act_datatypes 720 721 # self.cov_data = np.load('CD.npz')['arr_0'] 722 # Generate the realizations of the observed data once 723 # Augment observed and predicted data 724 self.obs_data_vector, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, 725 self.list_datatypes) 726 obs_data_vector = self.obs_data_vector 727 728 # Generate the data auto-covariance matrix 729 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 730 if hasattr(self, 'cov_data'): # cd matrix has been imported 731 tmp_E = np.dot(cholesky(self.cov_data).T, 732 np.random.randn(self.cov_data.shape[0], self.ne)) 733 else: 734 tmp_E = at.extract_tot_empirical_cov( 735 self.datavar, self.assim_index, self.list_datatypes, self.ne) 736 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 737 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 738 tmp_E = at.screen_data(tmp_E, self.aug_pred_data, 739 obs_data_vector, self.iteration) 740 self.E = tmp_E 741 self.real_obs_data = obs_data_vector[:, np.newaxis] - tmp_E 742 743 self.cov_data = np.var(self.E, ddof=1, 744 axis=1) # calculate the variance, to be used for e.g. data misfit calc 745 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 746 self.scale_data = np.sqrt(self.cov_data) 747 else: 748 if not hasattr(self, 'cov_data'): # if cd is not loaded 749 self.cov_data = at.gen_covdata( 750 self.datavar, self.assim_index, self.list_datatypes) 751 # data screening 752 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 753 self.cov_data = at.screen_data( 754 self.cov_data, self.aug_pred_data, obs_data_vector, self.iteration) 755 756 init_en = Cholesky() # Initialize GeoStat class for generating realizations 757 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 758 return_chol=True) 759 760 self.datavar = at.update_datavar( 761 self.cov_data, self.datavar, self.assim_index, self.list_datatypes) 762 self.current_state = cp.deepcopy(self.state) 763 764 # Calc. misfit for the initial iteration 765 data_misfit = at.calc_objectivefun( 766 self.real_obs_data, self.aug_pred_data, self.cov_data) 767 # Store the (mean) data misfit (also for conv. check) 768 self.data_misfit = np.mean(data_misfit) 769 self.prior_data_misfit = np.mean(data_misfit) 770 self.data_misfit_std = np.std(data_misfit) 771 772 if self.lam == 'auto': 773 self.lam = 0.5 * self.prior_data_misfit 774 775 else: 776 _, self.aug_pred_data = at.aug_obs_pred_data( 777 self.obs_data, self.pred_data, assim_index, self.list_datatypes) 778 779 # Mean pred_data and perturbation matrix with scaling 780 mean_preddata = np.mean(self.aug_pred_data, 1) 781 if len(self.scale_data.shape) == 1: 782 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 783 pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * ( 784 self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) 785 else: 786 pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * ( 787 self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) / \ 788 (np.sqrt(self.ne - 1)) 789 else: 790 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 791 pert_preddata = solve(self.scale_data, self.aug_pred_data - 792 np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) 793 else: 794 pert_preddata = solve(self.scale_data, self.aug_pred_data - np.dot(mean_preddata[:, None], np.ones((1, self.ne)))) / \ 795 (np.sqrt(self.ne - 1)) 796 self.pert_preddata = pert_preddata 797 798 self.update() 799 if hasattr(self, 'step'): 800 aug_state_upd = aug_state + self.step 801 if hasattr(self, 'w_step'): 802 self.W = self.current_W - self.w_step 803 aug_prior_state = at.aug_state(self.prior_state, self.list_states) 804 aug_state_upd = np.dot(aug_prior_state, (np.eye( 805 self.ne) + self.W / np.sqrt(self.ne - 1))) 806 807 # Extract updated state variables from aug_update 808 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 809 self.state = at.limits(self.state, self.prior_info)
Calculate the update step in approximate LM-EnRML code.
Parameters
- iteration (int): Iteration number
Returns
- success (bool): True if data mismatch is decreasing, False if increasing
- References
- ----------
- [1] Chen Y. & Oliver D.S. 2013, Levenberg-Marquardt Forms of the Iterative Ensemble Smoother for Efficient
- History Matching and Uncertainty Quantification. Comput. Geosci., 17 (4) (689-703):
Inherited Members
- pipt.loop.ensemble.Ensemble
- logger
- check_assimindex_sequential
- check_assimindex_simultaneous
- save_temp_state_assim
- save_temp_state_iter
- save_temp_state_mda
- save_temp_state_ml
- compress
- local_analysis_update
- ensemble.ensemble.Ensemble
- keys_en
- sim
- pred_data
- aux_input
- pickle_restart_file
- restart
- gen_init_ensemble
- get_list_assim_steps
- calc_prediction
- save
- load
- calc_ml_prediction
- pipt.update_schemes.update_methods_ns.approx_update.approx_update
- update
812class gn_enrml(lmenrmlMixIn): 813 """ 814 This is the implementation of the stochastig IES as described in [1]. More information about the method is found in 815 [2]. This implementation is the Gauss-Newton version. 816 817 This algorithm is quite similar to the `lm_enrml` as provided above, and will therefore inherit most of its methods. 818 We only change the calc_analysis part... 819 """ 820 821 def __init__(self, keys_da): 822 """ 823 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 824 `pipt.input_output.pipt_init.ReadInitFile`. 825 826 Parameters 827 ---------- 828 init_file: str 829 PIPT init. file containing info. to run the inversion algorithm 830 831 References 832 ---------- 833 [1] Raanes, P. N., Stordal, A. S., & Evensen, G. (2019). Revising the stochastic iterative ensemble smoother. 834 Nonlinear Processes in Geophysics, 26(3), 325-338. https://doi.org/10.5194/npg-26-325-2019 <br> 835 [2] Evensen, G., Raanes, P. N., Stordal, A. S., & Hove, J. (2019). Efficient Implementation of an Iterative 836 Ensemble Smoother for Data Assimilation and Reservoir History Matching. Frontiers in Applied Mathematics and 837 Statistics, 5(October), 114. https://doi.org/10.3389/fams.2019.00047 838 """ 839 # Call __init__ in parent class 840 super().__init__(keys_da) 841 842 def calc_analysis(self): 843 """ 844 Changelog 845 --------- 846 - KF 25/2-20 847 """ 848 # Get assimilation order as a list 849 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 850 851 # When handling large cases, it may be very costly to assemble the data covariance and localizaton matrix. 852 # To alleviate this in the simultuaneus-iterative scheme we store these matrices, the list of states and 853 # the list of data types after the first iteration. 854 855 if not hasattr(self, 'list_datatypes'): 856 # Get list of data types to be assimilated and of the free states. Do this once, because listing keys from a 857 # Python dictionary just when needed (in different places) may not yield the same list! 858 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 859 self.obs_data, assim_index) 860 list_datatypes = self.list_datatypes 861 self.list_states = list(self.state.keys()) 862 list_states = self.list_states 863 list_act_datatypes = self.list_act_datatypes 864 865 # Generate the realizations of the observed data once 866 # Augment observed and predicted data 867 self.obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 868 self.list_datatypes) 869 obs_data_vector = self.obs_data_vector 870 871 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 872 if hasattr(self, 'cov_data'): # cd matrix has been imported 873 tmp_E = np.dot(cholesky(self.cov_data).T, np.random.randn( 874 self.cov_data.shape[0], self.ne)) 875 else: 876 tmp_E = at.extract_tot_empirical_cov( 877 self.datavar, assim_index, self.list_datatypes, self.ne) 878 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 879 self.real_obs_data = obs_data_vector[:, np.newaxis] - tmp_E 880 881 self.cov_data = np.var(tmp_E, ddof=1, 882 axis=1) # calculate the variance, to be used for e.g. data misfit calc 883 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 884 self.scale_data = np.sqrt(self.cov_data) 885 else: 886 if not hasattr(self, 'cov_data'): # if cd is not loaded 887 self.cov_data = at.gen_covdata( 888 self.datavar, assim_index, self.list_datatypes) 889 # data screening 890 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 891 self.cov_data = at.screen_data( 892 self.cov_data, pred_data, obs_data_vector, self.iteration) 893 894 init_en = Cholesky() # Initialize GeoStat class for generating realizations 895 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 896 return_chol=True) 897 898 self.datavar = at.update_datavar( 899 self.cov_data, self.datavar, assim_index, self.list_datatypes) 900 cov_data = self.cov_data 901 obs_data = self.real_obs_data 902 # 903 self.current_state = cp.deepcopy(self.state) 904 # 905 self.aug_prior = cp.deepcopy(at.aug_state( 906 self.current_state, self.list_states)) 907 # self.mean_prior = aug_prior.mean(axis=1) 908 # self.X = (aug_prior - np.dot(np.resize(self.mean_prior, (len(self.mean_prior), 1)), 909 # np.ones((1, self.ne)))) 910 self.W = np.zeros((self.ne, self.ne)) 911 912 self.proj = (np.eye(self.ne) - (1 / self.ne) * 913 np.ones((self.ne, self.ne))) / np.sqrt(self.ne - 1) 914 self.E = np.dot(obs_data, self.proj) 915 916 # Calc. misfit for the initial iteration 917 if len(cov_data.shape) == 1: 918 tmp_data_misfit = np.diag(np.dot((pred_data - obs_data).T, 919 np.dot(np.expand_dims(self.cov_data ** (-1), axis=1), 920 np.ones((1, self.ne))) * (pred_data - obs_data))) 921 else: 922 tmp_data_misfit = np.diag( 923 np.dot((pred_data - obs_data).T, solve(self.cov_data, (pred_data - obs_data)))) 924 mean_data_misfit = np.mean(tmp_data_misfit) 925 # mean_data_misfit = np.median(tmp_data_misfit) 926 std_data_misfit = np.std(tmp_data_misfit) 927 928 # Store the (mean) data misfit (also for conv. check) 929 self.data_misfit = mean_data_misfit 930 self.prior_data_misfit = mean_data_misfit 931 self.data_misfit_std = std_data_misfit 932 933 else: 934 # for analysis debug... 935 list_datatypes = self.list_datatypes 936 list_act_datatypes = self.list_act_datatypes 937 list_states = self.list_states 938 cov_data = self.cov_data 939 obs_data_vector = self.obs_data_vector 940 _, pred_data = at.aug_obs_pred_data( 941 self.obs_data, self.pred_data, assim_index, self.list_datatypes) 942 obs_data = self.real_obs_data 943 944 if len(self.scale_data.shape) == 1: 945 Y = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * \ 946 np.dot(pred_data, self.proj) 947 else: 948 Y = solve(self.scale_data, np.dot(pred_data, self.proj)) 949 omega = np.eye(self.ne) + np.dot(self.W, self.proj) 950 LU = lu_factor(omega.T) 951 S = lu_solve(LU, Y.T).T 952 if len(self.scale_data.shape) == 1: 953 scaled_misfit = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 954 np.ones((1, self.ne))) * (obs_data - pred_data) 955 else: 956 scaled_misfit = solve(self.scale_data, (obs_data - pred_data)) 957 958 u, s, v = np.linalg.svd(S, full_matrices=False) 959 if self.trunc_energy < 1: 960 ti = (np.cumsum(s) / sum(s)) <= self.trunc_energy 961 u, s, v = u[:, ti].copy(), s[ti].copy(), v[ti, :].copy() 962 963 ps_inv = np.diag([el_s ** (-1) for el_s in s]) 964 if len(self.scale_data.shape) == 1: 965 X = np.dot(ps_inv, np.dot(u.T, np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 966 np.ones((1, self.ne))) * self.E)) 967 else: 968 X = np.dot(ps_inv, np.dot(u.T, solve(self.scale_data, self.E))) 969 Lam, z = np.linalg.eig(np.dot(X, X.T)) 970 971 X2 = np.dot(u, np.dot(ps_inv.T, z)) 972 973 X3_m = np.dot(S.T, X2) 974 # X3_old = np.dot(X2, np.linalg.solve(np.eye(len(Lam)) + np.diag(Lam), X2.T)) 975 step_m = np.dot(np.dot(X3_m, inv(np.eye(len(Lam)) + np.diag(Lam))), 976 np.dot(X3_m.T, self.W)) 977 978 if 'localization' in self.keys_da: 979 if self.keys_da['localization'][1][0] == 'autoadaloc': 980 loc_step_d = np.dot(np.linalg.pinv(self.aug_prior), self.localization.auto_ada_loc(self.aug_prior, 981 np.dot(np.dot(S.T, X2), 982 np.dot(inv( 983 np.eye(len(Lam)) + np.diag(Lam)), 984 np.dot(X2.T, scaled_misfit))), 985 self.list_states, 986 **{'prior_info': self.prior_info})) 987 self.step = self.lam * (self.W - (step_m + loc_step_d)) 988 else: 989 step_d = np.dot(np.linalg.inv(omega).T, np.dot(np.dot(Y.T, X2), 990 np.dot(inv(np.eye(len(Lam)) + np.diag(Lam)), 991 np.dot(X2.T, scaled_misfit)))) 992 self.step = self.lam * (self.W - (step_m + step_d)) 993 994 self.W -= self.step 995 996 aug_state_upd = np.dot(self.aug_prior, (np.eye( 997 self.ne) + self.W / np.sqrt(self.ne - 1))) 998 999 # Extract updated state variables from aug_update 1000 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 1001 1002 self.state = at.limits(self.state, self.prior_info) 1003 1004 def check_convergence(self): 1005 """ 1006 Check if GN-EnRML have converged based on evaluation of change sizes of objective function, state and damping 1007 parameter. Very similar to original function, but exit if there is no reduction in obj. function. 1008 1009 Parameters 1010 ---------- 1011 prev_data_misfit : float 1012 Data misfit calculated from the previous update. 1013 1014 step : float 1015 Step size. 1016 1017 lam : float 1018 LM damping parameter. 1019 1020 Returns 1021 ------- 1022 conv : bool 1023 Logic variable indicating if the algorithm has converged. 1024 1025 status : bool 1026 Indicates whether the objective function has reduced. 1027 1028 why_stop : dict 1029 Dictionary with keys corresponding to convergence criteria, with logical variables indicating 1030 which of them has been met. 1031 1032 Changelog 1033 --------- 1034 - ST 3/6-16 1035 - ST 6/6-16: Added LM damping param. check 1036 - KF 16/11-20: Modified for GN-EnRML 1037 - KF 10/3-21: Output whether the method reduced the objective function 1038 """ 1039 # Prelude to calc. conv. check (everything done below is from calc_analysis) 1040 if hasattr(self, 'list_datatypes'): 1041 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 1042 list_datatypes = self.list_datatypes 1043 cov_data = self.cov_data 1044 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 1045 list_datatypes) 1046 mean_preddata = np.mean(pred_data, 1) 1047 else: 1048 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 1049 list_datatypes, _ = at.get_list_data_types(self.obs_data, assim_index) 1050 # cov_data = at.gen_covdata(self.datavar, assim_index, list_datatypes) 1051 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 1052 list_datatypes) 1053 # mean_preddata = np.mean(pred_data, 1) 1054 1055 success = False 1056 1057 # if inital conv. check, there are no prev_data_misfit 1058 if self.prev_data_misfit is None: 1059 self.data_misfit = np.mean(self.data_misfit) 1060 self.prev_data_misfit = self.data_misfit 1061 self.prev_data_misfit_std = self.data_misfit_std 1062 success = True 1063 # update the last mismatch, only if this was a reduction of the misfit 1064 if self.data_misfit < self.prev_data_misfit: 1065 self.prev_data_misfit = self.data_misfit 1066 self.prev_data_misfit_std = self.data_misfit_std 1067 success = True 1068 # if there was no reduction of the misfit, retain the old "valid" data misfit. 1069 1070 # Calc. std dev of data misfit (used to update lamda) 1071 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector), 1)), np.ones((1, self.ne))) # use the perturbed 1072 # data instead. 1073 mat_obs = self.real_obs_data 1074 if len(cov_data.shape) == 1: 1075 data_misfit = np.diag(np.dot((pred_data - mat_obs).T, 1076 np.dot(np.expand_dims(self.cov_data ** (-1), axis=1), 1077 np.ones((1, self.ne))) * (pred_data - mat_obs))) 1078 else: 1079 data_misfit = np.diag(np.dot((pred_data - mat_obs).T, 1080 solve(self.cov_data, (pred_data - mat_obs)))) 1081 self.data_misfit = np.mean(data_misfit) 1082 self.data_misfit_std = np.std(data_misfit) 1083 1084 # # Calc. mean data misfit for convergence check, using the updated state variable 1085 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 1086 # solve(cov_data, (mean_preddata - obs_data_vector))) 1087 # if self.data_misfit > self.prev_data_misfit: 1088 # print(f'\n\nMisfit increased from {self.prev_data_misfit:.1f} to {self.data_misfit:.1f}. Exiting') 1089 # self.logger.info(f'\n\nMisfit increased from {self.prev_data_misfit:.1f} to {self.data_misfit:.1f}. Exiting') 1090 1091 # Convergence check: Relative step size of data misfit or state change less than tolerance 1092 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol \ 1093 or np.any(abs(np.mean(self.step, 1)) < self.step_tol) \ 1094 or self.lam >= self.lam_max: 1095 # or self.data_misfit > self.prev_data_misfit: 1096 # Logical variables for conv. criteria 1097 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 1098 'data_misfit': self.data_misfit, 1099 'prev_data_misfit': self.prev_data_misfit, 1100 'step_size_stop': np.any(abs(np.mean(self.step, 1)) < self.step_tol), 1101 'step_size': self.step, 1102 'lambda': self.lam, 1103 'lambda_stop': self.lam >= self.lam_max} 1104 1105 if self.data_misfit >= self.prev_data_misfit: 1106 success = False 1107 self.logger.info(f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 1108 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 1109 else: 1110 self.logger.info(f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 1111 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 1112 1113 # Return conv = True, why_stop var. 1114 return True, success, why_stop 1115 1116 else: # conv. not met 1117 # Logical variables for conv. criteria 1118 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 1119 'data_misfit': self.data_misfit, 1120 'prev_data_misfit': self.prev_data_misfit, 1121 'step_size': self.step, 1122 'step_size_stop': np.any(abs(np.mean(self.step, 1)) < self.step_tol), 1123 'lambda': self.lam, 1124 'lambda_stop': self.lam >= self.lam_max} 1125 1126 ############################################### 1127 ##### update Lambda step-size values ########## 1128 ############################################### 1129 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 1130 # If reduction in mean data misfit, increase step length 1131 self.lam = self.lam + (self.lam_max - self.lam) * \ 1132 2 ** (-(self.iteration) / (self.gamma - 1)) 1133 success = True 1134 self.current_state = deepcopy(self.state) 1135 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 1136 # Accept itaration, but keep lam the same 1137 success = True 1138 self.current_state = deepcopy(self.state) 1139 else: # Reject iteration, and decrease step length 1140 self.lam = self.lam / self.gamma 1141 success = False 1142 1143 if success: 1144 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 1145 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for next analysis: ' 1146 f'{self.lam}') 1147 else: 1148 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 1149 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for repeated analysis: ' 1150 f'{self.lam}') 1151 # Reset data misfit to prev_data_misfit (because the current state is neglected) 1152 self.data_misfit = self.prev_data_misfit 1153 self.data_misfit_std = self.prev_data_misfit_std 1154 1155 return False, success, why_stop
This is the implementation of the stochastig IES as described in [1]. More information about the method is found in [2]. This implementation is the Gauss-Newton version.
This algorithm is quite similar to the lm_enrml
as provided above, and will therefore inherit most of its methods.
We only change the calc_analysis part...
821 def __init__(self, keys_da): 822 """ 823 The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in 824 `pipt.input_output.pipt_init.ReadInitFile`. 825 826 Parameters 827 ---------- 828 init_file: str 829 PIPT init. file containing info. to run the inversion algorithm 830 831 References 832 ---------- 833 [1] Raanes, P. N., Stordal, A. S., & Evensen, G. (2019). Revising the stochastic iterative ensemble smoother. 834 Nonlinear Processes in Geophysics, 26(3), 325-338. https://doi.org/10.5194/npg-26-325-2019 <br> 835 [2] Evensen, G., Raanes, P. N., Stordal, A. S., & Hove, J. (2019). Efficient Implementation of an Iterative 836 Ensemble Smoother for Data Assimilation and Reservoir History Matching. Frontiers in Applied Mathematics and 837 Statistics, 5(October), 114. https://doi.org/10.3389/fams.2019.00047 838 """ 839 # Call __init__ in parent class 840 super().__init__(keys_da)
The class is initialized by passing the PIPT init. file upwards in the hierarchy to be read and parsed in
pipt.input_output.pipt_init.ReadInitFile
.
Parameters
- init_file (str): PIPT init. file containing info. to run the inversion algorithm
References
[1] Raanes, P. N., Stordal, A. S., & Evensen, G. (2019). Revising the stochastic iterative ensemble smoother.
Nonlinear Processes in Geophysics, 26(3), 325-338. https://doi.org/10.5194/npg-26-325-2019
[2] Evensen, G., Raanes, P. N., Stordal, A. S., & Hove, J. (2019). Efficient Implementation of an Iterative
Ensemble Smoother for Data Assimilation and Reservoir History Matching. Frontiers in Applied Mathematics and
Statistics, 5(October), 114. https://doi.org/10.3389/fams.2019.00047
842 def calc_analysis(self): 843 """ 844 Changelog 845 --------- 846 - KF 25/2-20 847 """ 848 # Get assimilation order as a list 849 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 850 851 # When handling large cases, it may be very costly to assemble the data covariance and localizaton matrix. 852 # To alleviate this in the simultuaneus-iterative scheme we store these matrices, the list of states and 853 # the list of data types after the first iteration. 854 855 if not hasattr(self, 'list_datatypes'): 856 # Get list of data types to be assimilated and of the free states. Do this once, because listing keys from a 857 # Python dictionary just when needed (in different places) may not yield the same list! 858 self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( 859 self.obs_data, assim_index) 860 list_datatypes = self.list_datatypes 861 self.list_states = list(self.state.keys()) 862 list_states = self.list_states 863 list_act_datatypes = self.list_act_datatypes 864 865 # Generate the realizations of the observed data once 866 # Augment observed and predicted data 867 self.obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 868 self.list_datatypes) 869 obs_data_vector = self.obs_data_vector 870 871 if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': 872 if hasattr(self, 'cov_data'): # cd matrix has been imported 873 tmp_E = np.dot(cholesky(self.cov_data).T, np.random.randn( 874 self.cov_data.shape[0], self.ne)) 875 else: 876 tmp_E = at.extract_tot_empirical_cov( 877 self.datavar, assim_index, self.list_datatypes, self.ne) 878 # self.E = (tmp_E - tmp_E.mean(1)[:,np.newaxis])/np.sqrt(self.ne - 1)/ 879 self.real_obs_data = obs_data_vector[:, np.newaxis] - tmp_E 880 881 self.cov_data = np.var(tmp_E, ddof=1, 882 axis=1) # calculate the variance, to be used for e.g. data misfit calc 883 # self.cov_data = ((self.E * self.E)/(self.ne-1)).sum(axis=1) # calculate the variance, to be used for e.g. data misfit calc 884 self.scale_data = np.sqrt(self.cov_data) 885 else: 886 if not hasattr(self, 'cov_data'): # if cd is not loaded 887 self.cov_data = at.gen_covdata( 888 self.datavar, assim_index, self.list_datatypes) 889 # data screening 890 if 'screendata' in self.keys_da and self.keys_da['screendata'] == 'yes': 891 self.cov_data = at.screen_data( 892 self.cov_data, pred_data, obs_data_vector, self.iteration) 893 894 init_en = Cholesky() # Initialize GeoStat class for generating realizations 895 self.real_obs_data, self.scale_data = init_en.gen_real(self.obs_data_vector, self.cov_data, self.ne, 896 return_chol=True) 897 898 self.datavar = at.update_datavar( 899 self.cov_data, self.datavar, assim_index, self.list_datatypes) 900 cov_data = self.cov_data 901 obs_data = self.real_obs_data 902 # 903 self.current_state = cp.deepcopy(self.state) 904 # 905 self.aug_prior = cp.deepcopy(at.aug_state( 906 self.current_state, self.list_states)) 907 # self.mean_prior = aug_prior.mean(axis=1) 908 # self.X = (aug_prior - np.dot(np.resize(self.mean_prior, (len(self.mean_prior), 1)), 909 # np.ones((1, self.ne)))) 910 self.W = np.zeros((self.ne, self.ne)) 911 912 self.proj = (np.eye(self.ne) - (1 / self.ne) * 913 np.ones((self.ne, self.ne))) / np.sqrt(self.ne - 1) 914 self.E = np.dot(obs_data, self.proj) 915 916 # Calc. misfit for the initial iteration 917 if len(cov_data.shape) == 1: 918 tmp_data_misfit = np.diag(np.dot((pred_data - obs_data).T, 919 np.dot(np.expand_dims(self.cov_data ** (-1), axis=1), 920 np.ones((1, self.ne))) * (pred_data - obs_data))) 921 else: 922 tmp_data_misfit = np.diag( 923 np.dot((pred_data - obs_data).T, solve(self.cov_data, (pred_data - obs_data)))) 924 mean_data_misfit = np.mean(tmp_data_misfit) 925 # mean_data_misfit = np.median(tmp_data_misfit) 926 std_data_misfit = np.std(tmp_data_misfit) 927 928 # Store the (mean) data misfit (also for conv. check) 929 self.data_misfit = mean_data_misfit 930 self.prior_data_misfit = mean_data_misfit 931 self.data_misfit_std = std_data_misfit 932 933 else: 934 # for analysis debug... 935 list_datatypes = self.list_datatypes 936 list_act_datatypes = self.list_act_datatypes 937 list_states = self.list_states 938 cov_data = self.cov_data 939 obs_data_vector = self.obs_data_vector 940 _, pred_data = at.aug_obs_pred_data( 941 self.obs_data, self.pred_data, assim_index, self.list_datatypes) 942 obs_data = self.real_obs_data 943 944 if len(self.scale_data.shape) == 1: 945 Y = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * \ 946 np.dot(pred_data, self.proj) 947 else: 948 Y = solve(self.scale_data, np.dot(pred_data, self.proj)) 949 omega = np.eye(self.ne) + np.dot(self.W, self.proj) 950 LU = lu_factor(omega.T) 951 S = lu_solve(LU, Y.T).T 952 if len(self.scale_data.shape) == 1: 953 scaled_misfit = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 954 np.ones((1, self.ne))) * (obs_data - pred_data) 955 else: 956 scaled_misfit = solve(self.scale_data, (obs_data - pred_data)) 957 958 u, s, v = np.linalg.svd(S, full_matrices=False) 959 if self.trunc_energy < 1: 960 ti = (np.cumsum(s) / sum(s)) <= self.trunc_energy 961 u, s, v = u[:, ti].copy(), s[ti].copy(), v[ti, :].copy() 962 963 ps_inv = np.diag([el_s ** (-1) for el_s in s]) 964 if len(self.scale_data.shape) == 1: 965 X = np.dot(ps_inv, np.dot(u.T, np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), 966 np.ones((1, self.ne))) * self.E)) 967 else: 968 X = np.dot(ps_inv, np.dot(u.T, solve(self.scale_data, self.E))) 969 Lam, z = np.linalg.eig(np.dot(X, X.T)) 970 971 X2 = np.dot(u, np.dot(ps_inv.T, z)) 972 973 X3_m = np.dot(S.T, X2) 974 # X3_old = np.dot(X2, np.linalg.solve(np.eye(len(Lam)) + np.diag(Lam), X2.T)) 975 step_m = np.dot(np.dot(X3_m, inv(np.eye(len(Lam)) + np.diag(Lam))), 976 np.dot(X3_m.T, self.W)) 977 978 if 'localization' in self.keys_da: 979 if self.keys_da['localization'][1][0] == 'autoadaloc': 980 loc_step_d = np.dot(np.linalg.pinv(self.aug_prior), self.localization.auto_ada_loc(self.aug_prior, 981 np.dot(np.dot(S.T, X2), 982 np.dot(inv( 983 np.eye(len(Lam)) + np.diag(Lam)), 984 np.dot(X2.T, scaled_misfit))), 985 self.list_states, 986 **{'prior_info': self.prior_info})) 987 self.step = self.lam * (self.W - (step_m + loc_step_d)) 988 else: 989 step_d = np.dot(np.linalg.inv(omega).T, np.dot(np.dot(Y.T, X2), 990 np.dot(inv(np.eye(len(Lam)) + np.diag(Lam)), 991 np.dot(X2.T, scaled_misfit)))) 992 self.step = self.lam * (self.W - (step_m + step_d)) 993 994 self.W -= self.step 995 996 aug_state_upd = np.dot(self.aug_prior, (np.eye( 997 self.ne) + self.W / np.sqrt(self.ne - 1))) 998 999 # Extract updated state variables from aug_update 1000 self.state = at.update_state(aug_state_upd, self.state, self.list_states) 1001 1002 self.state = at.limits(self.state, self.prior_info)
Changelog
- KF 25/2-20
1004 def check_convergence(self): 1005 """ 1006 Check if GN-EnRML have converged based on evaluation of change sizes of objective function, state and damping 1007 parameter. Very similar to original function, but exit if there is no reduction in obj. function. 1008 1009 Parameters 1010 ---------- 1011 prev_data_misfit : float 1012 Data misfit calculated from the previous update. 1013 1014 step : float 1015 Step size. 1016 1017 lam : float 1018 LM damping parameter. 1019 1020 Returns 1021 ------- 1022 conv : bool 1023 Logic variable indicating if the algorithm has converged. 1024 1025 status : bool 1026 Indicates whether the objective function has reduced. 1027 1028 why_stop : dict 1029 Dictionary with keys corresponding to convergence criteria, with logical variables indicating 1030 which of them has been met. 1031 1032 Changelog 1033 --------- 1034 - ST 3/6-16 1035 - ST 6/6-16: Added LM damping param. check 1036 - KF 16/11-20: Modified for GN-EnRML 1037 - KF 10/3-21: Output whether the method reduced the objective function 1038 """ 1039 # Prelude to calc. conv. check (everything done below is from calc_analysis) 1040 if hasattr(self, 'list_datatypes'): 1041 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 1042 list_datatypes = self.list_datatypes 1043 cov_data = self.cov_data 1044 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 1045 list_datatypes) 1046 mean_preddata = np.mean(pred_data, 1) 1047 else: 1048 assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] 1049 list_datatypes, _ = at.get_list_data_types(self.obs_data, assim_index) 1050 # cov_data = at.gen_covdata(self.datavar, assim_index, list_datatypes) 1051 obs_data_vector, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, assim_index, 1052 list_datatypes) 1053 # mean_preddata = np.mean(pred_data, 1) 1054 1055 success = False 1056 1057 # if inital conv. check, there are no prev_data_misfit 1058 if self.prev_data_misfit is None: 1059 self.data_misfit = np.mean(self.data_misfit) 1060 self.prev_data_misfit = self.data_misfit 1061 self.prev_data_misfit_std = self.data_misfit_std 1062 success = True 1063 # update the last mismatch, only if this was a reduction of the misfit 1064 if self.data_misfit < self.prev_data_misfit: 1065 self.prev_data_misfit = self.data_misfit 1066 self.prev_data_misfit_std = self.data_misfit_std 1067 success = True 1068 # if there was no reduction of the misfit, retain the old "valid" data misfit. 1069 1070 # Calc. std dev of data misfit (used to update lamda) 1071 # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector), 1)), np.ones((1, self.ne))) # use the perturbed 1072 # data instead. 1073 mat_obs = self.real_obs_data 1074 if len(cov_data.shape) == 1: 1075 data_misfit = np.diag(np.dot((pred_data - mat_obs).T, 1076 np.dot(np.expand_dims(self.cov_data ** (-1), axis=1), 1077 np.ones((1, self.ne))) * (pred_data - mat_obs))) 1078 else: 1079 data_misfit = np.diag(np.dot((pred_data - mat_obs).T, 1080 solve(self.cov_data, (pred_data - mat_obs)))) 1081 self.data_misfit = np.mean(data_misfit) 1082 self.data_misfit_std = np.std(data_misfit) 1083 1084 # # Calc. mean data misfit for convergence check, using the updated state variable 1085 # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, 1086 # solve(cov_data, (mean_preddata - obs_data_vector))) 1087 # if self.data_misfit > self.prev_data_misfit: 1088 # print(f'\n\nMisfit increased from {self.prev_data_misfit:.1f} to {self.data_misfit:.1f}. Exiting') 1089 # self.logger.info(f'\n\nMisfit increased from {self.prev_data_misfit:.1f} to {self.data_misfit:.1f}. Exiting') 1090 1091 # Convergence check: Relative step size of data misfit or state change less than tolerance 1092 if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol \ 1093 or np.any(abs(np.mean(self.step, 1)) < self.step_tol) \ 1094 or self.lam >= self.lam_max: 1095 # or self.data_misfit > self.prev_data_misfit: 1096 # Logical variables for conv. criteria 1097 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 1098 'data_misfit': self.data_misfit, 1099 'prev_data_misfit': self.prev_data_misfit, 1100 'step_size_stop': np.any(abs(np.mean(self.step, 1)) < self.step_tol), 1101 'step_size': self.step, 1102 'lambda': self.lam, 1103 'lambda_stop': self.lam >= self.lam_max} 1104 1105 if self.data_misfit >= self.prev_data_misfit: 1106 success = False 1107 self.logger.info(f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 1108 f'from {self.prior_data_misfit:0.1f} to {self.prev_data_misfit:0.1f}') 1109 else: 1110 self.logger.info(f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' 1111 f'from {self.prior_data_misfit:0.1f} to {self.data_misfit:0.1f}') 1112 1113 # Return conv = True, why_stop var. 1114 return True, success, why_stop 1115 1116 else: # conv. not met 1117 # Logical variables for conv. criteria 1118 why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, 1119 'data_misfit': self.data_misfit, 1120 'prev_data_misfit': self.prev_data_misfit, 1121 'step_size': self.step, 1122 'step_size_stop': np.any(abs(np.mean(self.step, 1)) < self.step_tol), 1123 'lambda': self.lam, 1124 'lambda_stop': self.lam >= self.lam_max} 1125 1126 ############################################### 1127 ##### update Lambda step-size values ########## 1128 ############################################### 1129 if self.data_misfit < self.prev_data_misfit and self.data_misfit_std < self.prev_data_misfit_std: 1130 # If reduction in mean data misfit, increase step length 1131 self.lam = self.lam + (self.lam_max - self.lam) * \ 1132 2 ** (-(self.iteration) / (self.gamma - 1)) 1133 success = True 1134 self.current_state = deepcopy(self.state) 1135 elif self.data_misfit < self.prev_data_misfit and self.data_misfit_std >= self.prev_data_misfit_std: 1136 # Accept itaration, but keep lam the same 1137 success = True 1138 self.current_state = deepcopy(self.state) 1139 else: # Reject iteration, and decrease step length 1140 self.lam = self.lam / self.gamma 1141 success = False 1142 1143 if success: 1144 self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' 1145 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for next analysis: ' 1146 f'{self.lam}') 1147 else: 1148 self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' 1149 f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for repeated analysis: ' 1150 f'{self.lam}') 1151 # Reset data misfit to prev_data_misfit (because the current state is neglected) 1152 self.data_misfit = self.prev_data_misfit 1153 self.data_misfit_std = self.prev_data_misfit_std 1154 1155 return False, success, why_stop
Check if GN-EnRML have converged based on evaluation of change sizes of objective function, state and damping parameter. Very similar to original function, but exit if there is no reduction in obj. function.
Parameters
- prev_data_misfit (float): Data misfit calculated from the previous update.
- step (float): Step size.
- lam (float): LM damping parameter.
Returns
- conv (bool): Logic variable indicating if the algorithm has converged.
- status (bool): Indicates whether the objective function has reduced.
- why_stop (dict): Dictionary with keys corresponding to convergence criteria, with logical variables indicating which of them has been met.
Changelog
- ST 3/6-16
- ST 6/6-16: Added LM damping param. check
- KF 16/11-20: Modified for GN-EnRML
- KF 10/3-21: Output whether the method reduced the objective function