From cf957da39a4b0a1a667a249b20bc850394329413 Mon Sep 17 00:00:00 2001 From: mlienorce Date: Fri, 15 Nov 2024 16:05:45 +0100 Subject: [PATCH 01/15] GIES and AVO Smeaheia in progress (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Martha Økland Lien Co-authored-by: Rolf J. Lorentzen --- ensemble/ensemble.py | 14 + pipt/loop/assimilation.py | 8 +- pipt/loop/ensemble.py | 4 +- pipt/misc_tools/analysis_tools.py | 5 +- pipt/update_schemes/gies/gies_base.py | 284 ++++++ pipt/update_schemes/gies/gies_rlmmac.py | 7 + pipt/update_schemes/gies/rlmmac_update.py | 214 ++++ setup.py | 3 +- simulator/calc_pem.py | 105 ++ simulator/flow_rock.py | 550 +++++++++- simulator/flow_rock_backup.py | 1120 ++++++++++++++++++++ simulator/flow_rock_mali.py | 1130 +++++++++++++++++++++ 12 files changed, 3382 insertions(+), 62 deletions(-) create mode 100644 pipt/update_schemes/gies/gies_base.py create mode 100644 pipt/update_schemes/gies/gies_rlmmac.py create mode 100644 pipt/update_schemes/gies/rlmmac_update.py create mode 100644 simulator/calc_pem.py create mode 100644 simulator/flow_rock_backup.py create mode 100644 simulator/flow_rock_mali.py diff --git a/ensemble/ensemble.py b/ensemble/ensemble.py index fb984d5..676a059 100644 --- a/ensemble/ensemble.py +++ b/ensemble/ensemble.py @@ -553,6 +553,20 @@ def calc_prediction(self, input_state=None, save_prediction=None): # Index list of ensemble members list_member_index = list(range(self.ne)) + # modified by xluo, for including the simulation of the mean reservoir model + # as used in the RLM-MAC algorithm + if self.keys_da['daalg'][1] == 'gies': + list_state.append({}) + list_member_index.append(self.ne) + + for key in self.state.keys(): + tmp_state = np.zeros(list_state[0][key].shape[0]) + + for i in range(self.ne): + tmp_state += list_state[i][key] + + list_state[self.ne][key] = tmp_state / self.ne + if no_tot_run==1: # if not in parallel we use regular loop en_pred = [self.sim.run_fwd_sim(state, member_index) for state, member_index in tqdm(zip(list_state, list_member_index), total=len(list_state))] diff --git a/pipt/loop/assimilation.py b/pipt/loop/assimilation.py index 0ccd6dd..926b1a8 100644 --- a/pipt/loop/assimilation.py +++ b/pipt/loop/assimilation.py @@ -580,8 +580,12 @@ def post_process_forecast(self): for k in pred_data_tmp[i]: # DATATYPE if vintage < len(self.ensemble.sparse_info['mask']) and \ len(pred_data_tmp[i][k]) == int(np.sum(self.ensemble.sparse_info['mask'][vintage])): - self.ensemble.pred_data[i][k] = np.zeros( - (len(self.ensemble.obs_data[i][k]), self.ensemble.ne)) + if self.ensemble.keys_da['daalg'][1] == 'gies': + self.ensemble.pred_data[i][k] = np.zeros( + (len(self.ensemble.obs_data[i][k]), self.ensemble.ne+1)) + else: + self.ensemble.pred_data[i][k] = np.zeros( + (len(self.ensemble.obs_data[i][k]), self.ensemble.ne)) for m in range(pred_data_tmp[i][k].shape[1]): data_array = self.ensemble.compress(pred_data_tmp[i][k][:, m], vintage, self.ensemble.sparse_info['use_ensemble']) diff --git a/pipt/loop/ensemble.py b/pipt/loop/ensemble.py index 675fcc3..131b3c1 100644 --- a/pipt/loop/ensemble.py +++ b/pipt/loop/ensemble.py @@ -732,7 +732,7 @@ def compress(self, data=None, vintage=0, aug_coeff=None): data_array = None - elif aug_coeff is None: + elif aug_coeff is None: # compress predicted data data_array, wdec_rec = self.sparse_data[vintage].compress(data) rec = self.sparse_data[vintage].reconstruct( @@ -741,7 +741,7 @@ def compress(self, data=None, vintage=0, aug_coeff=None): self.data_rec.append([]) self.data_rec[vintage].append(rec) - elif not aug_coeff: + elif not aug_coeff: # compress true data, aug_coeff = false options = copy(self.sparse_info) # find the correct mask for the vintage diff --git a/pipt/misc_tools/analysis_tools.py b/pipt/misc_tools/analysis_tools.py index 86e7b4c..04e51c5 100644 --- a/pipt/misc_tools/analysis_tools.py +++ b/pipt/misc_tools/analysis_tools.py @@ -542,8 +542,9 @@ def calc_objectivefun(pert_obs, pred_data, Cd): data_misfit : array-like Nex1 array containing objective function values. """ - ne = pred_data.shape[1] - r = (pred_data - pert_obs) + #ne = pred_data.shape[1] + ne = pert_obs.shape[1] + r = (pred_data[:, :ne] - pert_obs) # This is necessary to use to gies code that xilu has implemented if len(Cd.shape) == 1: precission = Cd**(-1) data_misfit = np.diag(r.T.dot(r*precission[:, None])) diff --git a/pipt/update_schemes/gies/gies_base.py b/pipt/update_schemes/gies/gies_base.py new file mode 100644 index 0000000..f967f63 --- /dev/null +++ b/pipt/update_schemes/gies/gies_base.py @@ -0,0 +1,284 @@ +""" +EnRML type schemes +""" +# External imports +import pipt.misc_tools.analysis_tools as at +from geostat.decomp import Cholesky +from pipt.loop.ensemble import Ensemble +from pipt.update_schemes.update_methods_ns.subspace_update import subspace_update +from pipt.update_schemes.update_methods_ns.full_update import full_update +from pipt.update_schemes.update_methods_ns.approx_update import approx_update +import sys +import pkgutil +import inspect +import numpy as np +import copy as cp +from scipy.linalg import cholesky, solve + +# Internal imports + + +class GIESMixIn(Ensemble): + """ + This is a base template for implementating the generalized iterative ensemble smoother (GIES) in the following papers: + Luo, Xiaodong. "Novel iterative ensemble smoothers derived from a class of generalized cost functions." + Computational Geosciences 25.3 (2021): 1159-1189. + Luo, Xiaodong, and William C. Cruz. "Data assimilation with soft constraints (DASC) through a generalized iterative + ensemble smoother." Computational Geosciences 26.3 (2022): 571-594. + """ + + def __init__(self, keys_da, keys_fwd, sim): + """ + 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 + """ + # Pass the init_file upwards in the hierarchy + super().__init__(keys_da, keys_fwd, sim) + + if self.restart is False: + # Save prior state in separate variable + self.prior_state = cp.deepcopy(self.state) + + # Extract parameters like conv. tol. and damping param. from ITERATION keyword in DATAASSIM + self._ext_iter_param() + + # Within variables + self.prev_data_misfit = None # Data misfit at previous iteration + if 'actnum' in self.keys_da.keys(): + try: + self.actnum = np.load(self.keys_da['actnum'])['actnum'] + except: + print('ACTNUM file cannot be loaded!') + else: + self.actnum = None + # At the moment, the iterative loop is threated as an iterative smoother and thus we check if assim. indices + # are given as in the Simultaneous loop. + self.check_assimindex_simultaneous() + # define the assimilation index + self.assim_index = [self.keys_da['obsname'], self.keys_da['assimindex'][0]] + # define the list of states + self.list_states = list(self.state.keys()) + # define the list of datatypes + self.list_datatypes, self.list_act_datatypes = at.get_list_data_types( + self.obs_data, self.assim_index) + # Get the perturbed observations and observation scaling + self.data_random_state = cp.deepcopy(np.random.get_state()) + self._ext_obs() + # Get state scaling and svd of scaled prior + self._ext_state() + self.current_state = cp.deepcopy(self.state) + + def calc_analysis(self): + """ + Calculate the update step in LM-EnRML, which is just the Levenberg-Marquardt update algorithm with + the sensitivity matrix approximated by the ensemble. + """ + + # reformat predicted data + _, self.aug_pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, + self.list_datatypes) + + if self.iteration == 1: # first iteration + data_misfit = at.calc_objectivefun( + self.real_obs_data, self.aug_pred_data, self.cov_data) + + # Store the (mean) data misfit (also for conv. check) + self.data_misfit = np.mean(data_misfit) + self.prior_data_misfit = np.mean(data_misfit) + self.data_misfit_std = np.std(data_misfit) + + #self.logger.info( + # f'Prior run complete with data misfit: {self.prior_data_misfit:0.1f}. Lambda for initial analysis: {self.lam}') + + if 'localanalysis' in self.keys_da: + self.local_analysis_update() + else: + # Mean pred_data and perturbation matrix with scaling + if len(self.scale_data.shape) == 1: + #self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), + # np.ones((1, self.ne))) * np.dot(self.aug_pred_data[:, 0:self.ne], self.proj) + self.pert_preddata = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), + np.ones((1, self.ne))) * (self.aug_pred_data[:, 0:self.ne] - self.aug_pred_data[:, self.ne, None]) + else: + #self.pert_preddata = solve( + # self.scale_data, np.dot(self.aug_pred_data[:, 0:self.ne], self.proj)) + self.pert_preddata = solve( + self.scale_data, self.aug_pred_data[:, 0:self.ne] - self.aug_pred_data[:, self.ne, None]) + + aug_state = at.aug_state(self.current_state, self.list_states) + self.update() # run ordinary analysis + if hasattr(self, 'step'): + aug_state_upd = aug_state + self.step + if hasattr(self, 'w_step'): + self.W = self.current_W + self.w_step + aug_prior_state = at.aug_state(self.prior_state, self.list_states) + aug_state_upd = np.dot(aug_prior_state, (np.eye( + self.ne) + self.W / np.sqrt(self.ne - 1))) + + # Extract updated state variables from aug_update + self.state = at.update_state(aug_state_upd, self.state, self.list_states) + self.state = at.limits(self.state, self.prior_info) + + def check_convergence(self): + """ + 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 + """ + + _, pred_data = at.aug_obs_pred_data(self.obs_data, self.pred_data, self.assim_index, + self.list_datatypes) + # Initialize the initial success value + success = False + + # if inital conv. check, there are no prev_data_misfit + self.prev_data_misfit = self.data_misfit + self.prev_data_misfit_std = self.data_misfit_std + + # Calc. std dev of data misfit (used to update lamda) + # mat_obs = np.dot(obs_data_vector.reshape((len(obs_data_vector),1)), np.ones((1, self.ne))) # use the perturbed + # data instead. + + data_misfit = at.calc_objectivefun(self.real_obs_data, pred_data, self.cov_data) + + self.data_misfit = np.mean(data_misfit) + self.data_misfit_std = np.std(data_misfit) + + # # Calc. mean data misfit for convergence check, using the updated state variable + # self.data_misfit = np.dot((mean_preddata - obs_data_vector).T, + # solve(cov_data, (mean_preddata - obs_data_vector))) + + # Convergence check: Relative step size of data misfit or state change less than tolerance + if abs(1 - (self.data_misfit / self.prev_data_misfit)) < self.data_misfit_tol: + #or self.lam >= self.lam_max: + # Logical variables for conv. criteria + why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, + 'data_misfit': self.data_misfit, + 'prev_data_misfit': self.prev_data_misfit, + 'lambda': self.lam} + if hasattr(self, 'lam_max'): + why_stop['lambda_stop'] = (self.lam >= self.lam_max) + + if self.data_misfit >= self.prev_data_misfit: + success = False + self.logger.info( + f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' + f'from {self.prior_data_misfit:0.2f} to {self.prev_data_misfit:0.2f}') + else: + self.logger.info( + f'Iterations have converged after {self.iteration} iterations. Objective function reduced ' + f'from {self.prior_data_misfit:0.2f} to {self.data_misfit:0.2f}') + # Return conv = True, why_stop var. + return True, success, why_stop + + else: # conv. not met + # Logical variables for conv. criteria + why_stop = {'data_misfit_stop': 1 - (self.data_misfit / self.prev_data_misfit) < self.data_misfit_tol, + 'data_misfit': self.data_misfit, + 'prev_data_misfit': self.prev_data_misfit, + 'lambda': self.lam} + if hasattr(self, 'lam_max'): + why_stop['lambda_stop'] = (self.lam >= self.lam_max) + + ############################################### + ##### update Lambda step-size values ########## + ############################################### + # If reduction in mean data misfit, reduce damping param + if self.data_misfit < self.prev_data_misfit: + # Reduce damping parameter (divide calculations for ANALYSISDEBUG purpose) + if not hasattr(self, 'lam_min'): + self.lam = self.lam / self.gamma + else: + if self.lam > self.lam_min: + self.lam = self.lam / self.gamma + + success = True + self.current_state = cp.deepcopy(self.state) + if hasattr(self, 'W'): + self.current_W = cp.deepcopy(self.W) + + else: # Reject iteration, and increase lam + # Increase damping parameter (divide calculations for ANALYSISDEBUG purpose) + self.lam = self.lam * self.gamma + success = False + + self.logger.info(f'Iter {self.iteration}: ') + if success: + #self.logger.info(f'Successfull iteration number {self.iteration}! Objective function reduced from ' + # f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for next analysis: ' + # f'{self.lam}') + pass + + # self.prev_data_misfit = self.data_misfit + # self.prev_data_misfit_std = self.data_misfit_std + else: + #self.logger.info(f'Failed iteration number {self.iteration}! Objective function increased from ' + # f'{self.prev_data_misfit:0.1f} to {self.data_misfit:0.1f}. New Lamba for repeated analysis: ' + # f'{self.lam}') + # Reset the objective function after report + self.data_misfit = self.prev_data_misfit + self.data_misfit_std = self.prev_data_misfit_std + + # Return conv = False, why_stop var. + return False, success, why_stop + + def _ext_iter_param(self): + """ + Extract parameters needed in LM-EnRML from the ITERATION keyword given in the DATAASSIM part of PIPT init. + file. These parameters include convergence tolerances and parameters for the damping parameter. Default + values for these parameters have been given here, if they are not provided in ITERATION. + """ + + # Predefine all the default values + self.data_misfit_tol = 0.01 + self.step_tol = 0.01 + self.lam = 1.0 + #self.lam_max = 1e10 + #self.lam_min = 0.01 + self.gamma = 2 + self.trunc_energy = 0.95 + self.iteration = 0 + + # Loop over options in ITERATION and extract the parameters we want + for i, opt in enumerate(list(zip(*self.keys_da['iteration']))[0]): + if opt == 'data_misfit_tol': + self.data_misfit_tol = self.keys_da['iteration'][i][1] + if opt == 'step_tol': + self.step_tol = self.keys_da['iteration'][i][1] + if opt == 'lambda': + self.lam = self.keys_da['iteration'][i][1] + if opt == 'lambda_max': + self.lam_max = self.keys_da['iteration'][i][1] + if opt == 'lambda_min': + self.lam_min = self.keys_da['iteration'][i][1] + if opt == 'lambda_factor': + self.gamma = self.keys_da['iteration'][i][1] + + if 'energy' in self.keys_da: + # initial energy (Remember to extract this) + self.trunc_energy = self.keys_da['energy'] + if self.trunc_energy > 1: # ensure that it is given as percentage + self.trunc_energy /= 100. + + + + diff --git a/pipt/update_schemes/gies/gies_rlmmac.py b/pipt/update_schemes/gies/gies_rlmmac.py new file mode 100644 index 0000000..80d03fa --- /dev/null +++ b/pipt/update_schemes/gies/gies_rlmmac.py @@ -0,0 +1,7 @@ + +from pipt.update_schemes.gies.gies_base import GIESMixIn +from pipt.update_schemes.gies.rlmmac_update import rlmmac_update + + +class gies_rlmmac(GIESMixIn, rlmmac_update): + pass \ No newline at end of file diff --git a/pipt/update_schemes/gies/rlmmac_update.py b/pipt/update_schemes/gies/rlmmac_update.py new file mode 100644 index 0000000..ba70301 --- /dev/null +++ b/pipt/update_schemes/gies/rlmmac_update.py @@ -0,0 +1,214 @@ +"""EnRML (IES) without the prior increment term.""" + +import numpy as np +from copy import deepcopy +import copy as cp +from scipy.linalg import solve, solve_banded, cholesky, lu_solve, lu_factor, inv +import pickle +import pipt.misc_tools.analysis_tools as at +from pipt.misc_tools.cov_regularization import _calc_loc + +class rlmmac_update(): + """ + Regularized Levenburg-Marquardt algorithm for Minimum Average Cost (RLM-MAC) problem, following the paper: + Luo, Xiaodong, et al. "Iterative ensemble smoother as an approximate solution to a regularized + minimum-average-cost problem: theory and applications." SPE Journal 2015: 962-982. + """ + + def update(self): + # calc the svd of the scaled data pertubation matrix + u_d, s_d, v_d = np.linalg.svd(self.pert_preddata, full_matrices=False) + aug_state = at.aug_state(self.current_state, self.list_states, self.cell_index) + + # remove the last singular value/vector. This is because numpy returns all ne values, while the last is actually + # zero. This part is a good place to include eventual additional truncation. + if self.trunc_energy < 1: + ti = (np.cumsum(s_d) / sum(s_d)) <= self.trunc_energy + u_d, s_d, v_d = u_d[:, ti].copy(), s_d[ti].copy(), v_d[ti, :].copy() + if 'localization' in self.keys_da: + if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': + if len(self.scale_data.shape) == 1: + E_hat = np.dot(np.expand_dims(self.scale_data ** (-1), + axis=1), np.ones((1, self.ne))) * self.E + x_0 = np.dot(np.diag(s_d[:] ** (-1)), np.dot(u_d[:, :].T, E_hat)) + Lam, z = np.linalg.eig(np.dot(x_0, x_0.T)) + else: + E_hat = solve(self.scale_data, self.E) + # x_0 = np.diag(s_d[:] ** (-1)) @ u_d[:, :].T @ E_hat + x_0 = np.dot(np.diag(s_d[:] ** (-1)), np.dot(u_d[:, :].T, E_hat)) + Lam, z = np.linalg.eig(np.dot(x_0, x_0.T)) + + # assume S_d = U S V^T; then Kalman gain K = S_m S_d^T (S_d S_d^T + gamma E E^T)^{-1} (d^o - g(m)) + # For this particular part A + # = S_d^T (S_d S_d^T + gamma E E^T)^{-1} + # = ... + # = V (I + gamma (S^-1 U^T E) (S^-1 U^T E)^T)^{-1} (S^-1 U^T) + # let S^-1 U^T E = Z L Z^T + # = V Z (I + gamma L)^{-1} Z^T S^-1 U^T + alpha = self.lam * np.sum(Lam**2) / len(Lam) + X = v_d.T @ z @ np.diag(1 / (alpha * Lam + np.eye(len(Lam)))) @ z.T @ np.diag(s_d[:] ** (-1)) @ u_d.T + #X = np.dot(np.dot(v_d.T, z), solve((self.lam + 1) * np.diag(Lam) + np.eye(len(Lam)), + # np.dot(u_d[:, :], np.dot(np.diag(s_d[:] ** (-1)).T, z)).T)) + + else: + alpha = self.lam * np.sum(s_d**2) / len(s_d) + X = v_d.T @ np.diag(s_d / (s_d ** 2 + alpha)) @ u_d.T + #X = np.dot(np.dot(v_d.T, np.diag(s_d)), solve(((self.lam + 1) * np.eye(len(s_d)) + np.diag(s_d ** 2)), u_d.T)) + + # we must perform localization + # store the size of all data + data_size = [[self.obs_data[int(time)][data].size if self.obs_data[int(time)][data] is not None else 0 + for data in self.list_datatypes] for time in self.assim_index[1]] + + f = self.keys_da['localization'] + + if f[1][0] == 'autoadaloc': + + # Mean state and perturbation matrix + mean_state = np.mean(aug_state, 1) + if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': + pert_state = (self.state_scaling**(-1))[:, None] * (aug_state - np.dot(np.resize(mean_state, (len(mean_state), 1)), + np.ones((1, self.ne)))) + else: + pert_state = (self.state_scaling**(-1) + )[:, None] * np.dot(aug_state, self.proj) + if len(self.scale_data.shape) == 1: + scaled_delta_data = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), + np.ones((1, pert_state.shape[1]))) * ( + self.real_obs_data - self.aug_pred_data[:, 0:self.ne]) + else: + scaled_delta_data = solve( + self.scale_data, (self.real_obs_data - self.aug_pred_data[:, 0:self.ne])) + + self.step = self.localization.auto_ada_loc(self.state_scaling[:, None] * pert_state, np.dot(X, scaled_delta_data), + self.list_states, + **{'prior_info': self.prior_info}) + elif 'localanalysis' in self.localization.loc_info and self.localization.loc_info['localanalysis']: + if 'distance' in self.localization.loc_info: + weight = _calc_loc(self.localization.loc_info['range'], self.localization.loc_info['distance'], + self.prior_info[self.list_states[0]], self.localization.loc_info['type'], self.ne) + else: + # if no distance, do full update + weight = np.ones((aug_state.shape[0], X.shape[1])) + mean_state = np.mean(aug_state, 1) + if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': + pert_state = (aug_state - np.dot(np.resize(mean_state, (len(mean_state), 1)), + np.ones((1, self.ne)))) + else: + pert_state = (aug_state - np.dot(np.resize(mean_state, (len(mean_state), 1)), + np.ones((1, self.ne)))) / (np.sqrt(self.ne - 1)) + + if len(self.scale_data.shape) == 1: + scaled_delta_data = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), + np.ones((1, pert_state.shape[1]))) * ( + self.real_obs_data - self.aug_pred_data) + else: + scaled_delta_data = solve( + self.scale_data, (self.real_obs_data - self.aug_pred_data)) + try: + self.step = weight.multiply( + np.dot(pert_state, X)).dot(scaled_delta_data) + except: + self.step = (weight*(np.dot(pert_state, X))).dot(scaled_delta_data) + + elif sum(['dist_loc' in el for el in f]) >= 1: + local_mask = self.localization.localize(self.list_datatypes, [self.keys_da['truedataindex'][int(elem)] + for elem in self.assim_index[1]], + self.list_states, self.ne, self.prior_info, data_size) + mean_state = np.mean(aug_state, 1) + if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': + pert_state = (aug_state - np.dot(np.resize(mean_state, (len(mean_state), 1)), + np.ones((1, self.ne)))) + else: + pert_state = (aug_state - np.dot(np.resize(mean_state, (len(mean_state), 1)), + np.ones((1, self.ne)))) / (np.sqrt(self.ne - 1)) + + if len(self.scale_data.shape) == 1: + scaled_delta_data = np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), + np.ones((1, pert_state.shape[1]))) * ( + self.real_obs_data - self.aug_pred_data) + else: + scaled_delta_data = solve( + self.scale_data, (self.real_obs_data - self.aug_pred_data)) + + self.step = local_mask.multiply( + np.dot(pert_state, X)).dot(scaled_delta_data) + + else: + act_data_list = {} + count = 0 + for i in self.assim_index[1]: + for el in self.list_datatypes: + if self.real_obs_data[int(i)][el] is not None: + act_data_list[( + el, float(self.keys_da['truedataindex'][int(i)]))] = count + count += 1 + + well = [w for w in + set([el[0] for el in self.localization.loc_info.keys() if type(el) == tuple])] + times = [t for t in set( + [el[1] for el in self.localization.loc_info.keys() if type(el) == tuple])] + tot_dat_index = {} + for uniq_well in well: + tmp_index = [] + for t in times: + if (uniq_well, t) in act_data_list: + tmp_index.append(act_data_list[(uniq_well, t)]) + tot_dat_index[uniq_well] = tmp_index + if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': + emp_cov = True + else: + emp_cov = False + + self.step = at.parallel_upd(self.list_states, self.prior_info, self.current_state, X, + self.localization.loc_info, self.real_obs_data, self.aug_pred_data, + int(self.keys_fwd['parallel']), + actnum=self.localization.loc_info['actnum'], + field_dim=self.localization.loc_info['field'], + act_data_list=tot_dat_index, + scale_data=self.scale_data, + num_states=len( + [el for el in self.list_states]), + emp_d_cov=emp_cov) + self.step = at.aug_state(self.step, self.list_states) + + else: + # Mean state and perturbation matrix + mean_state = np.mean(aug_state, 1) + if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': + pert_state = (self.state_scaling**(-1))[:, None] * (aug_state - np.dot(np.resize(mean_state, (len(mean_state), 1)), + np.ones((1, self.ne)))) + else: + pert_state = (self.state_scaling**(-1) + )[:, None] * np.dot(aug_state, self.proj) + if 'emp_cov' in self.keys_da and self.keys_da['emp_cov'] == 'yes': + if len(self.scale_data.shape) == 1: + E_hat = np.dot(np.expand_dims(self.scale_data ** (-1), + axis=1), np.ones((1, self.ne))) * self.E + x_0 = np.dot(np.diag(s_d[:] ** (-1)), np.dot(u_d[:, :].T, E_hat)) + Lam, z = np.linalg.eig(np.dot(x_0, x_0.T)) + x_1 = np.dot(np.dot(u_d[:, :], np.dot(np.diag(s_d[:] ** (-1)).T, z)).T, + np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * + (self.real_obs_data - self.aug_pred_data)) + else: + E_hat = solve(self.scale_data, self.E) + x_0 = np.dot(np.diag(s_d[:] ** (-1)), np.dot(u_d[:, :].T, E_hat)) + Lam, z = np.linalg.eig(np.dot(x_0, x_0.T)) + x_1 = np.dot(np.dot(u_d[:, :], np.dot(np.diag(s_d[:] ** (-1)).T, z)).T, + solve(self.scale_data, (self.real_obs_data - self.aug_pred_data))) + + x_2 = solve((self.lam + 1) * np.diag(Lam) + np.eye(len(Lam)), x_1) + x_3 = np.dot(np.dot(v_d.T, z), x_2) + delta_1 = np.dot(self.state_scaling[:, None] * pert_state, x_3) + self.step = delta_1 + else: + # Compute the approximate update (follow notation in paper) + if len(self.scale_data.shape) == 1: + x_1 = np.dot(u_d.T, np.dot(np.expand_dims(self.scale_data ** (-1), axis=1), np.ones((1, self.ne))) * + (self.real_obs_data - self.aug_pred_data[:, 0:self.ne])) + else: + x_1 = np.dot(u_d.T, solve(self.scale_data, + (self.real_obs_data - self.aug_pred_data[:, 0:self.ne]))) + x_2 = solve(((self.lam + 1) * np.eye(len(s_d)) + np.diag(s_d ** 2)), x_1) + x_3 = np.dot(np.dot(v_d.T, np.diag(s_d)), x_2) + self.step = np.dot(self.state_scaling[:, None] * pert_state, x_3) diff --git a/setup.py b/setup.py index 605c44b..e1bfbf0 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ 'tomli-w', 'pyyaml', 'libecalc', - 'scikit-learn' + 'scikit-learn', + 'pylops' ] + EXTRAS['doc'], ) diff --git a/simulator/calc_pem.py b/simulator/calc_pem.py new file mode 100644 index 0000000..6186f42 --- /dev/null +++ b/simulator/calc_pem.py @@ -0,0 +1,105 @@ +from simulator.opm import flow +from importlib import import_module +import datetime as dt +import numpy as np +import os +from misc import ecl, grdecl +import shutil +import glob +from subprocess import Popen, PIPE +import mat73 +from copy import deepcopy +from sklearn.cluster import KMeans +from sklearn.preprocessing import StandardScaler +from mako.lookup import TemplateLookup +from mako.runtime import Context + +# from pylops import avo +from pylops.utils.wavelets import ricker +from pylops.signalprocessing import Convolve1D +from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master +from scipy.interpolate import interp1d +from pipt.misc_tools.analysis_tools import store_ensemble_sim_information +from geostat.decomp import Cholesky +from simulator.eclipse import ecl_100 + + + +def calc_pem(self, time): + # fluid phases written to restart file from simulator run + phases = self.ecl_case.init.phases + + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + for var in phases: + tmp = self.ecl_case.cell_data(var, time) + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) # only active, and conv. to float + + if 'RS' in self.ecl_case.cell_data: + tmp = self.ecl_case.cell_data('RS', time) + pem_input['RS'] = np.array(tmp[~tmp.mask], dtype=float) + else: + pem_input['RS'] = None + print('RS is not a variable in the ecl_case') + + # extract pressure + tmp = self.ecl_case.cell_data('PRESSURE', time) + pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] + + tmp = self.ecl_case.cell_data('PRESSURE', 1) + + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] + else: + P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first + + if 'press_conv' in self.pem_input: + P_init = P_init * self.pem_input['press_conv'] + + # extract saturations + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model + saturations = [pem_input['S{}'.format(ph)] for ph in phases] + else: + print('Type and number of fluids are unspecified in calc_pem') + + # fluid saturations in dictionary + tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + self.sats.extend([tmp_s]) + + # Get elastic parameters + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + else: + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) + + + diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 3f34466..31aa975 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -12,7 +12,17 @@ from copy import deepcopy from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler - +from mako.lookup import TemplateLookup +from mako.runtime import Context + +# from pylops import avo +from pylops.utils.wavelets import ricker +from pylops.signalprocessing import Convolve1D +from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master +from scipy.interpolate import interp1d +from pipt.misc_tools.analysis_tools import store_ensemble_sim_information +from geostat.decomp import Cholesky +from simulator.eclipse import ecl_100 class flow_sim2seis(flow): """ @@ -55,7 +65,7 @@ def _getpeminfo(self, input_dict): self.pem_input['depth'] = elem[1] if elem[0] == 'actnum': # the npz of actnum values self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment + if elem[0] == 'baseline': # the time for the baseline 4D measurement self.pem_input['baseline'] = elem[1] if elem[0] == 'vintage': self.pem_input['vintage'] = elem[1] @@ -106,64 +116,20 @@ def call_sim(self, folder=None, wait_for_proc=False): grid = self.ecl_case.grid() phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + self.sats = [] + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - tmp = self.ecl_case.cell_data(var, time) - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) + self.calc_pem(time) #mali: update class inhere flor_rock. Include calc_pem as method in flow_rock - grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { + grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { 'Vs': self.pem.getShearVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v+1}.grdecl', { + grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v+1}.grdecl', { 'Vp': self.pem.getBulkVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/rho{v+1}.grdecl', + grdecl.write(f'En_{str(self.ensemble_member)}/rho{v+1}.grdecl', {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) current_folder = os.getcwd() @@ -656,3 +622,477 @@ def extract_data(self, member): if self.true_prim[1][prim_ind] in self.pem_input['vintage']: v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) self.pred_data[prim_ind][key] = self.bar_result[v].flatten() + + +class flow_avo(flow_sim2seis): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' + self._get_avo_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run() + + def run_fwd_sim(self, state, member_i, del_folder=True): + """ + Setup and run the AVO forward simulator. + + Parameters + ---------- + state : dict + Dictionary containing the ensemble state. + + member_i : int + Index of the ensemble member. any index < 0 (e.g., -1) means the ground truth in synthetic case studies + + del_folder : bool, optional + Boolean to determine if the ensemble folder should be deleted. Default is False. + """ + + if member_i >= 0: + folder = 'En_' + str(member_i) + os.sep + if not os.path.exists(folder): + os.mkdir(folder) + else: # XLUO: any negative member_i is considered as the index for the true model + assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ + "the folder containing the true model" + if not os.path.exists(self.input_dict['truth_folder']): + os.mkdir(self.input_dict['truth_folder']) + folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ + else self.input_dict['truth_folder'] + del_folder = False # never delete this folder + self.folder = folder + self.ensemble_member = member_i + + state['member'] = member_i + + # start by generating the .DATA file, using the .mako template situated in ../folder + self._runMako(folder, state) + success = False + rerun = self.rerun + while rerun >= 0 and not success: + success = self.call_sim(folder, True) + rerun -= 1 + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if hasattr(self, 'redund_sim') and self.redund_sim is not None: + success = self.redund_sim.call_sim(folder, True) + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if del_folder: + self.remove_folder(member_i) + return False + else: + if del_folder: + self.remove_folder(member_i) + return False + + + def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): + # replace the sim2seis part (which is unusable) by avo based on Pylops + + if folder is None: + folder = self.folder + + # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" + if hasattr(self, 'run_reservoir_model'): + run_reservoir_model = self.run_reservoir_model + + if run_reservoir_model is None: + run_reservoir_model = True + + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) + success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) + #ecl = ecl_100(filename=self.file) + #ecl.options = self.options + #success = ecl.call_sim(folder, wait_for_proc) + else: + success = True + + if success: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + #phases = self.ecl_case.init.phases + self.sats = [] + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + # extract dynamic variables from simulation run + self.calc_pem(time) + # vp, vs, density in reservoir + # The properties in pem is only given in the active cells + # indices of active cells: + if grid['ACTNUM'].shape[0] == self.NX: + true_indices = np.where(grid['ACTNUM']) + elif grid['ACTNUM'].shape[0] == self.NZ: + actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) + true_indices = np.where(actnum) + else: + print('warning: dimension mismatch in line 750 flow_rock.py') + + if len(self.pem.getBulkVel()) == len(true_indices[0]): + self.vp = np.zeros(grid['DIMENS']) + self.vp[true_indices] = (self.pem.getBulkVel() * .1) + self.vs = np.zeros(grid['DIMENS']) + self.vs[true_indices] = (self.pem.getShearVel() * .1) + self.rho = np.zeros(grid['DIMENS']) + self.rho[true_indices] = (self.pem.getDens()) + else: + self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') + self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') + self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 + + save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} + if save_folder is not None: + file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"vp_vs_rho_vint{v}.npz" + else: + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"vp_vs_rho_vint{v}.npz" + else: + file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" + + #with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + # avo data + self._calc_avo_props() + + avo = self.avo_data.flatten(order="F") + + # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies + # the corresonding (noisy) data are observations in data assimilation + if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: + non_nan_idx = np.argwhere(~np.isnan(avo)) + data_std = np.std(avo[non_nan_idx]) + if self.input_dict['add_synthetic_noise'][0] == 'snr': + noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std + avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) + else: + noise_std = 0.0 # simulated data don't contain noise + + save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} + if save_folder is not None: + file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"avo_vint{v}.npz" + else: + file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"avo_vint{v}.npz" + + #with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super(flow_sim2seis, self).extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'avo' in key: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ + else self.folder + key + '_vint' + str(idx) + '.npz' + with np.load(filename) as f: + self.pred_data[prim_ind][key] = f[key] + + def _runMako(self, folder, state, addfiles=['properties']): + """ + Hard coding, maybe a better way possible + addfiles: additional files that need to be included into ECLIPSE/OPM DATA file + """ + super()._runMako(folder, state) + + lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') + for file in addfiles: + if os.path.exists(file + '.mako'): + tmpl = lkup.get_template('%s.mako' % file) + + # use a context and render onto a file + with open('{0}'.format(folder + file), 'w') as f: + ctx = Context(f, **state) + tmpl.render_context(ctx) + + def calc_pem(self, time): + # fluid phases written to restart file from simulator run + phases = self.ecl_case.init.phases + + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + + + if 'RS' in self.pem_input: #ecl_case.cell_data: # to be more robust! + tmp = self.ecl_case.cell_data('RS', time) + pem_input['RS'] = np.array(tmp[~tmp.mask], dtype=float) + else: + pem_input['RS'] = None + print('RS is not a variable in the ecl_case') + + # extract pressure + tmp = self.ecl_case.cell_data('PRESSURE', time) + pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] + + + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] + else: + P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first + + if 'press_conv' in self.pem_input: + P_init = P_init * self.pem_input['press_conv'] + + # extract saturations + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + for var in phases: + if var in ['WAT', 'GAS']: + tmp = self.ecl_case.cell_data('S{}'.format(var), time) + pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model + for var in phases: + if var in ['GAS']: + tmp = self.ecl_case.cell_data('S{}'.format(var), time) + pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + saturations = [1 - (pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] + else: + print('Type and number of fluids are unspecified in calc_pem') + + # fluid saturations in dictionary + tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + self.sats.extend([tmp_s]) + + # Get elastic parameters + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + else: + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) + + def _get_avo_info(self, avo_config=None): + """ + AVO configuration + """ + # list of configuration parameters in the "AVO" section + config_para_list = ['dz', 'tops', 'angle', 'frequency', 'wave_len', 'vp_shale', 'vs_shale', + 'den_shale', 't_min', 't_max', 't_sampling', 'pp_func'] + if 'avo' in self.input_dict: + self.avo_config = {} + for elem in self.input_dict['avo']: + assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + self.avo_config[elem[0]] = elem[1] + + # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later + if isinstance(self.avo_config['angle'], float): + self.avo_config['angle'] = [self.avo_config['angle']] + + # self._get_DZ(file=self.avo_config['dz']) # =>self.DZ + kw_file = {'DZ': self.avo_config['dz'], 'TOPS': self.avo_config['tops']} + self._get_props(kw_file) + self.overburden = self.pem_input['overburden'] + + # make sure that the "pylops" package is installed + # See https://github.com/PyLops/pylops + self.pp_func = getattr(import_module('pylops.avo.avo'), self.avo_config['pp_func']) + + else: + self.avo_config = None + + def _get_props(self, kw_file): + # extract properties (specified by keywords) in (possibly) different files + # kw_file: a dictionary contains "keyword: file" pairs + # Note that all properties are reshaped into the reservoir model dimension (NX, NY, NZ) + # using the "F" order + for kw in kw_file: + file = kw_file[kw] + if file.endswith('.npz'): + with np.load(file) as f: + exec(f'self.{kw} = f[ "{kw}" ]') + self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] + else: + reader = GRDECL_Parser(filename=file) + reader.read_GRDECL() + exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") + self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ + eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') + + def _calc_avo_props(self, dt=0.0005): + # dt is the fine resolution sampling rate + # convert properties in reservoir model to time domain + vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) + vs_shale = self.avo_config['vs_shale'] # scalar value + rho_shale = self.avo_config['den_shale'] # scalar value + + # Two-way travel time of the top of the reservoir + # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer + top_res = 2 * self.TOPS[:, :, 0] / vp_shale + + # Cumulative traveling time trough the reservoir in vertical direction + cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] + # total travel time + cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) + + # add overburden and underburden of Vp, Vs and Density + vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), + self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), + self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) + rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 + self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) + + # search for the lowest grid cell thickness and sample the time according to + # that grid thickness to preserve the thin layer effect + time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) + if time_sample.shape[0] == 1: + time_sample = time_sample.reshape(-1) + time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) + + vp_sample = np.tile(vp[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + vs_sample = np.tile(vs[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + rho_sample = np.tile(rho[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + + for m in range(self.NX): + for l in range(self.NY): + for k in range(time_sample.shape[2]): + # find the right interval of time_sample[m, l, k] belonging to, and use + # this information to allocate vp, vs, rho + idx = np.searchsorted(cum_time[m, l, :], time_sample[m, l, k], side='left') + idx = idx if idx < len(cum_time[m, l, :]) else len(cum_time[m, l, :]) - 1 + vp_sample[m, l, k] = vp[m, l, idx] + vs_sample[m, l, k] = vs[m, l, idx] + rho_sample[m, l, k] = rho[m, l, idx] + + + # from matplotlib import pyplot as plt + # plt.plot(vp_sample[0, 0, :]) + # plt.show() + + #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) + #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) + #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) + + #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] + #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] + #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] + + #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) + #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg + + # PP reflection coefficients, see, e.g., + # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" + # So far, it seems that "approx_zoeppritz_pp" is the only available option + # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) + avo_data_list = [] + + # Ricker wavelet + wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), + f0=self.avo_config['frequency']) + + # Travel time corresponds to reflectivity sereis + t = time_sample[:, :, 0:-1] + + # interpolation time + t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) + trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) + + # number of pp reflection coefficients in the vertial direction + nz_rpp = vp_sample.shape[2] - 1 + + for i in range(len(self.avo_config['angle'])): + angle = self.avo_config['angle'][i] + Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], + vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) + + for m in range(self.NX): + for l in range(self.NY): + # convolution with the Ricker wavelet + conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + w_trace = conv_op * Rpp[m, l, :] + + # Sample the trace into regular time interval + f = interp1d(np.squeeze(t[m, l, :]), np.squeeze(w_trace), + kind='nearest', fill_value='extrapolate') + trace_interp[m, l, :] = f(t_interp) + + if i == 0: + avo_data = trace_interp # 3D + elif i == 1: + avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D + else: + avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D + + self.avo_data = avo_data + + @classmethod + def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): + """ + XILU: Quantities read by "EclipseData.cell_data" are put in the axis order of [nz, ny, nx]. To be consisent with + ECLIPSE/OPM custom, we need to change the axis order. We further flatten the array according to the specified order + """ + array = np.array(array) + if len(array.shape) != 1: # if array is a 1D array, then do nothing + assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" + + # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] + new_array = np.transpose(array, axes=[2, 1, 0]) + if flatten: + new_array = new_array.flatten(order=order) + + return new_array + else: + return array diff --git a/simulator/flow_rock_backup.py b/simulator/flow_rock_backup.py new file mode 100644 index 0000000..7eb2e03 --- /dev/null +++ b/simulator/flow_rock_backup.py @@ -0,0 +1,1120 @@ +from simulator.opm import flow +from importlib import import_module +import datetime as dt +import numpy as np +import os +from misc import ecl, grdecl +import shutil +import glob +from subprocess import Popen, PIPE +import mat73 +from copy import deepcopy +from sklearn.cluster import KMeans +from sklearn.preprocessing import StandardScaler +from mako.lookup import TemplateLookup +from mako.runtime import Context + +# from pylops import avo +from pylops.utils.wavelets import ricker +from pylops.signalprocessing import Convolve1D +from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master +from scipy.interpolate import interp1d +from pipt.misc_tools.analysis_tools import store_ensemble_sim_information +from geostat.decomp import Cholesky +from simulator.eclipse import ecl_100 + +class flow_sim2seis(flow): + """ + Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic + quantities can be calculated. Inherit the flow class, and use super to call similar functions. + """ + + def __init__(self, input_dict=None, filename=None, options=None): + super().__init__(input_dict, filename, options) + self._getpeminfo(input_dict) + + self.dum_file_root = 'dummy.txt' + self.dum_entry = str(0) + self.date_slack = None + if 'date_slack' in input_dict: + self.date_slack = int(input_dict['date_slack']) + + # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can + # run a user defined code to do this. + self.saveinfo = None + if 'savesiminfo' in input_dict: + # Make sure "ANALYSISDEBUG" gives a list + if isinstance(input_dict['savesiminfo'], list): + self.saveinfo = input_dict['savesiminfo'] + else: + self.saveinfo = [input_dict['savesiminfo']] + + self.scale = [] + + def _getpeminfo(self, input_dict): + """ + Get, and return, flow and PEM modules + """ + if 'pem' in input_dict: + self.pem_input = {} + for elem in input_dict['pem']: + if elem[0] == 'model': # Set the petro-elastic model + self.pem_input['model'] = elem[1] + if elem[0] == 'depth': # provide the npz of depth values + self.pem_input['depth'] = elem[1] + if elem[0] == 'actnum': # the npz of actnum values + self.pem_input['actnum'] = elem[1] + if elem[0] == 'baseline': # the time for the baseline 4D measurement + self.pem_input['baseline'] = elem[1] + if elem[0] == 'vintage': + self.pem_input['vintage'] = elem[1] + if not type(self.pem_input['vintage']) == list: + self.pem_input['vintage'] = [elem[1]] + if elem[0] == 'ntg': + self.pem_input['ntg'] = elem[1] + if elem[0] == 'press_conv': + self.pem_input['press_conv'] = elem[1] + if elem[0] == 'compaction': + self.pem_input['compaction'] = True + if elem[0] == 'overburden': # the npz of overburden values + self.pem_input['overburden'] = elem[1] + if elem[0] == 'percentile': # use for scaling + self.pem_input['percentile'] = elem[1] + + pem = getattr(import_module('simulator.rockphysics.' + + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) + + self.pem = pem(self.pem_input) + + else: + self.pem = None + + def setup_fwd_run(self): + super().setup_fwd_run() + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + success = super().call_sim(folder, wait_for_proc) + + if success: + # need a if to check that we have correct sim2seis + # copy relevant sim2seis files into folder. + for file in glob.glob('sim2seis_config/*'): + shutil.copy(file, 'En_' + str(self.ensemble_member) + os.sep) + + self.ecl_case = ecl.EclipseCase( + 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') + grid = self.ecl_case.grid() + + phases = self.ecl_case.init.phases + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + tmp = self.ecl_case.cell_data(var, time) + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + tmp = self.ecl_case.cell_data('PRESSURE', 1) + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] + else: + # initial pressure is first + P_init = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + P_init = P_init*self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + + grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { + 'Vs': self.pem.getShearVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) + grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v+1}.grdecl', { + 'Vp': self.pem.getBulkVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) + grdecl.write(f'En_{str(self.ensemble_member)}/rho{v+1}.grdecl', + {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) + + current_folder = os.getcwd() + run_folder = current_folder + os.sep + 'En_' + str(self.ensemble_member) + # The sim2seis is invoked via a shell script. The simulations provides outputs. Run, and get all output. Search + # for Done. If not finished in reasonable time -> kill + p = Popen(['./sim2seis.sh', run_folder], stdout=PIPE) + start = time + while b'done' not in p.stdout.readline(): + pass + + # Todo: handle sim2seis or pem error + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super().extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if key in ['sim2seis']: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + result = mat73.loadmat(f'En_{member}/Data_conv.mat')['data_conv'] + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = np.sum( + np.abs(result[:, :, :, v]), axis=0).flatten() + +class flow_rock(flow): + """ + Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic + quantities can be calculated. Inherit the flow class, and use super to call similar functions. + """ + + def __init__(self, input_dict=None, filename=None, options=None): + super().__init__(input_dict, filename, options) + self._getpeminfo(input_dict) + + self.dum_file_root = 'dummy.txt' + self.dum_entry = str(0) + self.date_slack = None + if 'date_slack' in input_dict: + self.date_slack = int(input_dict['date_slack']) + + # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can + # run a user defined code to do this. + self.saveinfo = None + if 'savesiminfo' in input_dict: + # Make sure "ANALYSISDEBUG" gives a list + if isinstance(input_dict['savesiminfo'], list): + self.saveinfo = input_dict['savesiminfo'] + else: + self.saveinfo = [input_dict['savesiminfo']] + + self.scale = [] + + def _getpeminfo(self, input_dict): + """ + Get, and return, flow and PEM modules + """ + if 'pem' in input_dict: + self.pem_input = {} + for elem in input_dict['pem']: + if elem[0] == 'model': # Set the petro-elastic model + self.pem_input['model'] = elem[1] + if elem[0] == 'depth': # provide the npz of depth values + self.pem_input['depth'] = elem[1] + if elem[0] == 'actnum': # the npz of actnum values + self.pem_input['actnum'] = elem[1] + if elem[0] == 'baseline': # the time for the baseline 4D measurment + self.pem_input['baseline'] = elem[1] + if elem[0] == 'vintage': + self.pem_input['vintage'] = elem[1] + if not type(self.pem_input['vintage']) == list: + self.pem_input['vintage'] = [elem[1]] + if elem[0] == 'ntg': + self.pem_input['ntg'] = elem[1] + if elem[0] == 'press_conv': + self.pem_input['press_conv'] = elem[1] + if elem[0] == 'compaction': + self.pem_input['compaction'] = True + if elem[0] == 'overburden': # the npz of overburden values + self.pem_input['overburden'] = elem[1] + if elem[0] == 'percentile': # use for scaling + self.pem_input['percentile'] = elem[1] + + pem = getattr(import_module('simulator.rockphysics.' + + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) + + self.pem = pem(self.pem_input) + + else: + self.pem = None + + def setup_fwd_run(self, redund_sim): + super().setup_fwd_run(redund_sim=redund_sim) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + success = super().call_sim(folder, wait_for_proc) + + if success: + self.ecl_case = ecl.EclipseCase( + 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') + phases = self.ecl_case.init.phases + #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + if 'WAT' in phases and 'GAS' in phases: + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + tmp = self.ecl_case.cell_data('PRESSURE', 1) + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] + else: + # initial pressure is first + P_init = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + P_init = P_init*self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + # run filter + self.pem._filter() + vintage.append(deepcopy(self.pem.bulkimp)) + + if hasattr(self.pem, 'baseline'): # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + # pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', base_time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, base_time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=None) + + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + # kill if values are inf or nan + assert not np.isnan(tmp_value).any() + assert not np.isinf(tmp_value).any() + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + self.pem._filter() + + # 4D response + self.pem_result = [] + for i, elem in enumerate(vintage): + self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) + else: + for i, elem in enumerate(vintage): + self.pem_result.append(elem) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super().extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if key in ['bulkimp']: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() + +class flow_barycenter(flow): + """ + Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic + quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the + barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are + identified using k-means clustering, and the number of objects are determined using and elbow strategy. + """ + + def __init__(self, input_dict=None, filename=None, options=None): + super().__init__(input_dict, filename, options) + self._getpeminfo(input_dict) + + self.dum_file_root = 'dummy.txt' + self.dum_entry = str(0) + self.date_slack = None + if 'date_slack' in input_dict: + self.date_slack = int(input_dict['date_slack']) + + # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can + # run a user defined code to do this. + self.saveinfo = None + if 'savesiminfo' in input_dict: + # Make sure "ANALYSISDEBUG" gives a list + if isinstance(input_dict['savesiminfo'], list): + self.saveinfo = input_dict['savesiminfo'] + else: + self.saveinfo = [input_dict['savesiminfo']] + + self.scale = [] + self.pem_result = [] + self.bar_result = [] + + def _getpeminfo(self, input_dict): + """ + Get, and return, flow and PEM modules + """ + if 'pem' in input_dict: + self.pem_input = {} + for elem in input_dict['pem']: + if elem[0] == 'model': # Set the petro-elastic model + self.pem_input['model'] = elem[1] + if elem[0] == 'depth': # provide the npz of depth values + self.pem_input['depth'] = elem[1] + if elem[0] == 'actnum': # the npz of actnum values + self.pem_input['actnum'] = elem[1] + if elem[0] == 'baseline': # the time for the baseline 4D measurment + self.pem_input['baseline'] = elem[1] + if elem[0] == 'vintage': + self.pem_input['vintage'] = elem[1] + if not type(self.pem_input['vintage']) == list: + self.pem_input['vintage'] = [elem[1]] + if elem[0] == 'ntg': + self.pem_input['ntg'] = elem[1] + if elem[0] == 'press_conv': + self.pem_input['press_conv'] = elem[1] + if elem[0] == 'compaction': + self.pem_input['compaction'] = True + if elem[0] == 'overburden': # the npz of overburden values + self.pem_input['overburden'] = elem[1] + if elem[0] == 'percentile': # use for scaling + self.pem_input['percentile'] = elem[1] + if elem[0] == 'clusters': # number of clusters for each barycenter + self.pem_input['clusters'] = elem[1] + + pem = getattr(import_module('simulator.rockphysics.' + + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) + + self.pem = pem(self.pem_input) + + else: + self.pem = None + + def setup_fwd_run(self, redund_sim): + super().setup_fwd_run(redund_sim=redund_sim) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + success = super().call_sim(folder, wait_for_proc) + + if success: + self.ecl_case = ecl.EclipseCase( + 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') + phases = self.ecl_case.init.phases + #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + if 'WAT' in phases and 'GAS' in phases: + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + tmp = self.ecl_case.cell_data('PRESSURE', 1) + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] + else: + # initial pressure is first + P_init = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + P_init = P_init*self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + # run filter + self.pem._filter() + vintage.append(deepcopy(self.pem.bulkimp)) + + if hasattr(self.pem, 'baseline'): # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + # pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', base_time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, base_time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=None) + + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + # kill if values are inf or nan + assert not np.isnan(tmp_value).any() + assert not np.isinf(tmp_value).any() + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + self.pem._filter() + + # 4D response + for i, elem in enumerate(vintage): + self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) + else: + for i, elem in enumerate(vintage): + self.pem_result.append(elem) + + # Extract k-means centers and interias for each element in pem_result + if 'clusters' in self.pem_input: + npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) + n_clusters_list = npzfile['n_clusters_list'] + npzfile.close() + else: + n_clusters_list = len(self.pem_result)*[2] + kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} + for i, bulkimp in enumerate(self.pem_result): + std = np.std(bulkimp) + features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) + scaler = StandardScaler() + scaled_features = scaler.fit_transform(features) + kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) + kmeans.fit(scaled_features) + kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements + self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super().extract_data(member) + + # get the barycenters and inertias + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if key in ['barycenter']: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.bar_result[v].flatten() + +class flow_avo(flow_sim2seis): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' + self._get_avo_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run() + + def run_fwd_sim(self, state, member_i, del_folder=True): + """ + Setup and run the AVO forward simulator. + + Parameters + ---------- + state : dict + Dictionary containing the ensemble state. + + member_i : int + Index of the ensemble member. any index < 0 (e.g., -1) means the ground truth in synthetic case studies + + del_folder : bool, optional + Boolean to determine if the ensemble folder should be deleted. Default is False. + """ + + if member_i >= 0: + folder = 'En_' + str(member_i) + os.sep + if not os.path.exists(folder): + os.mkdir(folder) + else: # XLUO: any negative member_i is considered as the index for the true model + assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ + "the folder containing the true model" + if not os.path.exists(self.input_dict['truth_folder']): + os.mkdir(self.input_dict['truth_folder']) + folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ + else self.input_dict['truth_folder'] + del_folder = False # never delete this folder + self.folder = folder + self.ensemble_member = member_i + + state['member'] = member_i + + # start by generating the .DATA file, using the .mako template situated in ../folder + self._runMako(folder, state) + success = False + rerun = self.rerun + while rerun >= 0 and not success: + success = self.call_sim(folder, True) + rerun -= 1 + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if hasattr(self, 'redund_sim') and self.redund_sim is not None: + success = self.redund_sim.call_sim(folder, True) + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if del_folder: + self.remove_folder(member_i) + return False + else: + if del_folder: + self.remove_folder(member_i) + return False + + + def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): + # replace the sim2seis part (which is unusable) by avo based on Pylops + + if folder is None: + folder = self.folder + + # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" + if hasattr(self, 'run_reservoir_model'): + run_reservoir_model = self.run_reservoir_model + + if run_reservoir_model is None: + run_reservoir_model = True + + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) + success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) + #ecl = ecl_100(filename=self.file) + #ecl.options = self.options + #success = ecl.call_sim(folder, wait_for_proc) + else: + success = True + + if success: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + phases = self.ecl_case.init.phases + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(multfactor * tmp), dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + tmp = self.ecl_case.cell_data(var, time) + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input[var] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + #pem_input['PRESSURE'] = self._reformat3D_then_flatten(pem_input['PRESSURE'] * + # self.pem_input['press_conv']) + + tmp = self.ecl_case.cell_data('PRESSURE', 0) + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] + #P_init = self._reformat3D_then_flatten(self.pem.p_init.reshape(tmp.shape) * np.ones(tmp.shape)) + else: + # initial pressure is first + P_init = np.array(tmp[~tmp.mask], dtype=float) + #P_init = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + if 'press_conv' in self.pem_input: + P_init = P_init * self.pem_input['press_conv'] + #P_init = self._reformat3D_then_flatten(P_init * self.pem_input['press_conv']) + + saturations = [ + 1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + #saturations = [self._reformat3D_then_flatten(1 - (pem_input['SWAT'] + pem_input['SGAS'])) + # if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] + + # Get the pressure + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + else: + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) + + #grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { + # 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) + #grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { + # 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) + #grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', + # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) + + # vp, vs, density + self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') + self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') + self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 + + save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} + if save_folder is not None: + file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"vp_vs_rho_vint{v}.npz" + else: + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"vp_vs_rho_vint{v}.npz" + else: + file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" + + #with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + # avo data + self._calc_avo_props() + + avo = self.avo_data.flatten(order="F") + + # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies + # the corresonding (noisy) data are observations in data assimilation + if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: + non_nan_idx = np.argwhere(~np.isnan(avo)) + data_std = np.std(avo[non_nan_idx]) + if self.input_dict['add_synthetic_noise'][0] == 'snr': + noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std + avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) + else: + noise_std = 0.0 # simulated data don't contain noise + + save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} + if save_folder is not None: + file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"avo_vint{v}.npz" + else: + # if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + # (self.ensemble_member >= 0): + # file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ + # else folder + f"avo_vint{v}.npz" + # else: + # file_name = os.getcwd() + os.sep + f"avo_vint{v}.npz" + file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"avo_vint{v}.npz" + + #with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super(flow_sim2seis, self).extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'avo' in key: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ + else self.folder + key + '_vint' + str(idx) + '.npz' + with np.load(filename) as f: + self.pred_data[prim_ind][key] = f[key] + + def _runMako(self, folder, state, addfiles=['properties']): + """ + Hard coding, maybe a better way possible + addfiles: additional files that need to be included into ECLIPSE/OPM DATA file + """ + super()._runMako(folder, state) + + lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') + for file in addfiles: + if os.path.exists(file + '.mako'): + tmpl = lkup.get_template('%s.mako' % file) + + # use a context and render onto a file + with open('{0}'.format(folder + file), 'w') as f: + ctx = Context(f, **state) + tmpl.render_context(ctx) + + def calc_pem(self, time): + + + def _get_avo_info(self, avo_config=None): + """ + AVO configuration + """ + # list of configuration parameters in the "AVO" section + config_para_list = ['dz', 'tops', 'angle', 'frequency', 'wave_len', 'vp_shale', 'vs_shale', + 'den_shale', 't_min', 't_max', 't_sampling', 'pp_func'] + if 'avo' in self.input_dict: + self.avo_config = {} + for elem in self.input_dict['avo']: + assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + self.avo_config[elem[0]] = elem[1] + + # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later + if isinstance(self.avo_config['angle'], float): + self.avo_config['angle'] = [self.avo_config['angle']] + + # self._get_DZ(file=self.avo_config['dz']) # =>self.DZ + kw_file = {'DZ': self.avo_config['dz'], 'TOPS': self.avo_config['tops']} + self._get_props(kw_file) + self.overburden = self.pem_input['overburden'] + + # make sure that the "pylops" package is installed + # See https://github.com/PyLops/pylops + self.pp_func = getattr(import_module('pylops.avo.avo'), self.avo_config['pp_func']) + + else: + self.avo_config = None + + def _get_props(self, kw_file): + # extract properties (specified by keywords) in (possibly) different files + # kw_file: a dictionary contains "keyword: file" pairs + # Note that all properties are reshaped into the reservoir model dimension (NX, NY, NZ) + # using the "F" order + for kw in kw_file: + file = kw_file[kw] + if file.endswith('.npz'): + with np.load(file) as f: + exec(f'self.{kw} = f[ "{kw}" ]') + self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] + else: + reader = GRDECL_Parser(filename=file) + reader.read_GRDECL() + exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") + self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ + eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') + + def _calc_avo_props(self, dt=0.0005): + # dt is the fine resolution sampling rate + # convert properties in reservoir model to time domain + vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) + vs_shale = self.avo_config['vs_shale'] # scalar value + rho_shale = self.avo_config['den_shale'] # scalar value + + # Two-way travel time of the top of the reservoir + # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer + top_res = 2 * self.TOPS[:, :, 0] / vp_shale + + # Cumulative traveling time trough the reservoir in vertical direction + cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] + # total travel time + cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) + + # add overburden and underburden of Vp, Vs and Density + vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), + self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), + self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) + rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 + self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) + + # search for the lowest grid cell thickness and sample the time according to + # that grid thickness to preserve the thin layer effect + time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) + if time_sample.shape[0] == 1: + time_sample = time_sample.reshape(-1) + time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) + + vp_sample = np.tile(vp[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + vs_sample = np.tile(vs[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + rho_sample = np.tile(rho[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + + for m in range(self.NX): + for l in range(self.NY): + for k in range(time_sample.shape[2]): + # find the right interval of time_sample[m, l, k] belonging to, and use + # this information to allocate vp, vs, rho + idx = np.searchsorted(cum_time[m, l, :], time_sample[m, l, k], side='left') + idx = idx if idx < len(cum_time[m, l, :]) else len(cum_time[m, l, :]) - 1 + vp_sample[m, l, k] = vp[m, l, idx] + vs_sample[m, l, k] = vs[m, l, idx] + rho_sample[m, l, k] = rho[m, l, idx] + + + # from matplotlib import pyplot as plt + # plt.plot(vp_sample[0, 0, :]) + # plt.show() + + #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) + #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) + #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) + + #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] + #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] + #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] + + #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) + #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg + + # PP reflection coefficients, see, e.g., + # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" + # So far, it seems that "approx_zoeppritz_pp" is the only available option + # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) + avo_data_list = [] + + # Ricker wavelet + wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), + f0=self.avo_config['frequency']) + + # Travel time corresponds to reflectivity sereis + t = time_sample[:, :, 0:-1] + + # interpolation time + t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) + trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) + + # number of pp reflection coefficients in the vertial direction + nz_rpp = vp_sample.shape[2] - 1 + + for i in range(len(self.avo_config['angle'])): + angle = self.avo_config['angle'][i] + Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], + vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) + + for m in range(self.NX): + for l in range(self.NY): + # convolution with the Ricker wavelet + conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + w_trace = conv_op * Rpp[m, l, :] + + # Sample the trace into regular time interval + f = interp1d(np.squeeze(t[m, l, :]), np.squeeze(w_trace), + kind='nearest', fill_value='extrapolate') + trace_interp[m, l, :] = f(t_interp) + + if i == 0: + avo_data = trace_interp # 3D + elif i == 1: + avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D + else: + avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D + + self.avo_data = avo_data + + @classmethod + def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): + """ + XILU: Quantities read by "EclipseData.cell_data" are put in the axis order of [nz, ny, nx]. To be consisent with + ECLIPSE/OPM custom, we need to change the axis order. We further flatten the array according to the specified order + """ + array = np.array(array) + if len(array.shape) != 1: # if array is a 1D array, then do nothing + assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" + + # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] + new_array = np.transpose(array, axes=[2, 1, 0]) + if flatten: + new_array = new_array.flatten(order=order) + + return new_array + else: + return array \ No newline at end of file diff --git a/simulator/flow_rock_mali.py b/simulator/flow_rock_mali.py new file mode 100644 index 0000000..6abb95d --- /dev/null +++ b/simulator/flow_rock_mali.py @@ -0,0 +1,1130 @@ +from simulator.opm import flow +from importlib import import_module +import datetime as dt +import numpy as np +import os +from misc import ecl, grdecl +import shutil +import glob +from subprocess import Popen, PIPE +import mat73 +from copy import deepcopy +from sklearn.cluster import KMeans +from sklearn.preprocessing import StandardScaler +from mako.lookup import TemplateLookup +from mako.runtime import Context + +# from pylops import avo +from pylops.utils.wavelets import ricker +from pylops.signalprocessing import Convolve1D +from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master +from scipy.interpolate import interp1d +from pipt.misc_tools.analysis_tools import store_ensemble_sim_information +from geostat.decomp import Cholesky +from simulator.eclipse import ecl_100 + +class flow_sim2seis(flow): + """ + Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic + quantities can be calculated. Inherit the flow class, and use super to call similar functions. + """ + + def __init__(self, input_dict=None, filename=None, options=None): + super().__init__(input_dict, filename, options) + self._getpeminfo(input_dict) + + self.dum_file_root = 'dummy.txt' + self.dum_entry = str(0) + self.date_slack = None + if 'date_slack' in input_dict: + self.date_slack = int(input_dict['date_slack']) + + # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can + # run a user defined code to do this. + self.saveinfo = None + if 'savesiminfo' in input_dict: + # Make sure "ANALYSISDEBUG" gives a list + if isinstance(input_dict['savesiminfo'], list): + self.saveinfo = input_dict['savesiminfo'] + else: + self.saveinfo = [input_dict['savesiminfo']] + + self.scale = [] + self.sats = [] + + def _getpeminfo(self, input_dict): + """ + Get, and return, flow and PEM modules + """ + if 'pem' in input_dict: + self.pem_input = {} + for elem in input_dict['pem']: + if elem[0] == 'model': # Set the petro-elastic model + self.pem_input['model'] = elem[1] + if elem[0] == 'depth': # provide the npz of depth values + self.pem_input['depth'] = elem[1] + if elem[0] == 'actnum': # the npz of actnum values + self.pem_input['actnum'] = elem[1] + if elem[0] == 'baseline': # the time for the baseline 4D measurement + self.pem_input['baseline'] = elem[1] + if elem[0] == 'vintage': + self.pem_input['vintage'] = elem[1] + if not type(self.pem_input['vintage']) == list: + self.pem_input['vintage'] = [elem[1]] + if elem[0] == 'ntg': + self.pem_input['ntg'] = elem[1] + if elem[0] == 'press_conv': + self.pem_input['press_conv'] = elem[1] + if elem[0] == 'compaction': + self.pem_input['compaction'] = True + if elem[0] == 'overburden': # the npz of overburden values + self.pem_input['overburden'] = elem[1] + if elem[0] == 'percentile': # use for scaling + self.pem_input['percentile'] = elem[1] + + pem = getattr(import_module('simulator.rockphysics.' + + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) + + self.pem = pem(self.pem_input) + + else: + self.pem = None + + def calc_pem(self, v, time, phases): + + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + for var in ['PRESSURE', 'RS', 'SWAT', 'SGAS']: + tmp = self.ecl_case.cell_data(var, time) + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) # only active, and conv. to float + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] + + tmp = self.ecl_case.cell_data('PRESSURE', 1) + + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] + else: + P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first + + if 'press_conv' in self.pem_input: + P_init = P_init * self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + + # fluid saturations in dictionary + tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + self.sats.extend([tmp_s]) + + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=None) + + + + def setup_fwd_run(self): + super().setup_fwd_run() + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + success = super().call_sim(folder, wait_for_proc) + + if success: + # need a if to check that we have correct sim2seis + # copy relevant sim2seis files into folder. + for file in glob.glob('sim2seis_config/*'): + shutil.copy(file, 'En_' + str(self.ensemble_member) + os.sep) + + self.ecl_case = ecl.EclipseCase( + 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') + + grid = self.ecl_case.grid() + phases = self.ecl_case.init.phases + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + vintage = [] + self.sats = [] + # loop over seismic vintages + for v, assim_time in enumerate([0.0] + self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + + self.calc_pem(v, time, phases) + + grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { + 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) + grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { + 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) + grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', + {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) + + current_folder = os.getcwd() + run_folder = current_folder + os.sep + 'En_' + str(self.ensemble_member) + # The sim2seis is invoked via a shell script. The simulations provides outputs. Run, and get all output. Search + # for Done. If not finished in reasonable time -> kill + p = Popen(['./sim2seis.sh', run_folder], stdout=PIPE) + start = time + while b'done' not in p.stdout.readline(): + pass + + # Todo: handle sim2seis or pem error + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super().extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if key in ['sim2seis']: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + result = mat73.loadmat(f'En_{member}/Data_conv.mat')['data_conv'] + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = np.sum( + np.abs(result[:, :, :, v]), axis=0).flatten() + +class flow_rock(flow): + """ + Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic + quantities can be calculated. Inherit the flow class, and use super to call similar functions. + """ + + def __init__(self, input_dict=None, filename=None, options=None): + super().__init__(input_dict, filename, options) + self._getpeminfo(input_dict) + + self.dum_file_root = 'dummy.txt' + self.dum_entry = str(0) + self.date_slack = None + if 'date_slack' in input_dict: + self.date_slack = int(input_dict['date_slack']) + + # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can + # run a user defined code to do this. + self.saveinfo = None + if 'savesiminfo' in input_dict: + # Make sure "ANALYSISDEBUG" gives a list + if isinstance(input_dict['savesiminfo'], list): + self.saveinfo = input_dict['savesiminfo'] + else: + self.saveinfo = [input_dict['savesiminfo']] + + self.scale = [] + + + def _getpeminfo(self, input_dict): + """ + Get, and return, flow and PEM modules + """ + if 'pem' in input_dict: + self.pem_input = {} + for elem in input_dict['pem']: + if elem[0] == 'model': # Set the petro-elastic model + self.pem_input['model'] = elem[1] + if elem[0] == 'depth': # provide the npz of depth values + self.pem_input['depth'] = elem[1] + if elem[0] == 'actnum': # the npz of actnum values + self.pem_input['actnum'] = elem[1] + if elem[0] == 'baseline': # the time for the baseline 4D measurment + self.pem_input['baseline'] = elem[1] + if elem[0] == 'vintage': + self.pem_input['vintage'] = elem[1] + if not type(self.pem_input['vintage']) == list: + self.pem_input['vintage'] = [elem[1]] + if elem[0] == 'ntg': + self.pem_input['ntg'] = elem[1] + if elem[0] == 'press_conv': + self.pem_input['press_conv'] = elem[1] + if elem[0] == 'compaction': + self.pem_input['compaction'] = True + if elem[0] == 'overburden': # the npz of overburden values + self.pem_input['overburden'] = elem[1] + if elem[0] == 'percentile': # use for scaling + self.pem_input['percentile'] = elem[1] + + pem = getattr(import_module('simulator.rockphysics.' + + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) + + self.pem = pem(self.pem_input) + + else: + self.pem = None + + def setup_fwd_run(self, redund_sim): + super().setup_fwd_run(redund_sim=redund_sim) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + success = super().call_sim(folder, wait_for_proc) + + if success: + self.ecl_case = ecl.EclipseCase( + 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') + phases = self.ecl_case.init.phases + #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + if 'WAT' in phases and 'GAS' in phases: + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + tmp = self.ecl_case.cell_data('PRESSURE', 1) + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] + else: + # initial pressure is first + P_init = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + P_init = P_init*self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + # run filter + self.pem._filter() + vintage.append(deepcopy(self.pem.bulkimp)) + + if hasattr(self.pem, 'baseline'): # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + # pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', base_time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, base_time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=None) + + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + # kill if values are inf or nan + assert not np.isnan(tmp_value).any() + assert not np.isinf(tmp_value).any() + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + self.pem._filter() + + # 4D response + self.pem_result = [] + for i, elem in enumerate(vintage): + self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) + else: + for i, elem in enumerate(vintage): + self.pem_result.append(elem) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super().extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if key in ['bulkimp']: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() + +class flow_barycenter(flow): + """ + Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic + quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the + barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are + identified using k-means clustering, and the number of objects are determined using and elbow strategy. + """ + + def __init__(self, input_dict=None, filename=None, options=None): + super().__init__(input_dict, filename, options) + self._getpeminfo(input_dict) + + self.dum_file_root = 'dummy.txt' + self.dum_entry = str(0) + self.date_slack = None + if 'date_slack' in input_dict: + self.date_slack = int(input_dict['date_slack']) + + # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can + # run a user defined code to do this. + self.saveinfo = None + if 'savesiminfo' in input_dict: + # Make sure "ANALYSISDEBUG" gives a list + if isinstance(input_dict['savesiminfo'], list): + self.saveinfo = input_dict['savesiminfo'] + else: + self.saveinfo = [input_dict['savesiminfo']] + + self.scale = [] + self.pem_result = [] + self.bar_result = [] + + def _getpeminfo(self, input_dict): + """ + Get, and return, flow and PEM modules + """ + if 'pem' in input_dict: + self.pem_input = {} + for elem in input_dict['pem']: + if elem[0] == 'model': # Set the petro-elastic model + self.pem_input['model'] = elem[1] + if elem[0] == 'depth': # provide the npz of depth values + self.pem_input['depth'] = elem[1] + if elem[0] == 'actnum': # the npz of actnum values + self.pem_input['actnum'] = elem[1] + if elem[0] == 'baseline': # the time for the baseline 4D measurment + self.pem_input['baseline'] = elem[1] + if elem[0] == 'vintage': + self.pem_input['vintage'] = elem[1] + if not type(self.pem_input['vintage']) == list: + self.pem_input['vintage'] = [elem[1]] + if elem[0] == 'ntg': + self.pem_input['ntg'] = elem[1] + if elem[0] == 'press_conv': + self.pem_input['press_conv'] = elem[1] + if elem[0] == 'compaction': + self.pem_input['compaction'] = True + if elem[0] == 'overburden': # the npz of overburden values + self.pem_input['overburden'] = elem[1] + if elem[0] == 'percentile': # use for scaling + self.pem_input['percentile'] = elem[1] + if elem[0] == 'clusters': # number of clusters for each barycenter + self.pem_input['clusters'] = elem[1] + + pem = getattr(import_module('simulator.rockphysics.' + + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) + + self.pem = pem(self.pem_input) + + else: + self.pem = None + + def setup_fwd_run(self, redund_sim): + super().setup_fwd_run(redund_sim=redund_sim) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + success = super().call_sim(folder, wait_for_proc) + + if success: + self.ecl_case = ecl.EclipseCase( + 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') + phases = self.ecl_case.init.phases + #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + if 'WAT' in phases and 'GAS' in phases: + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + tmp = self.ecl_case.cell_data('PRESSURE', 1) + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] + else: + # initial pressure is first + P_init = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + P_init = P_init*self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + # run filter + self.pem._filter() + vintage.append(deepcopy(self.pem.bulkimp)) + + if hasattr(self.pem, 'baseline'): # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + # pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', base_time) + + pem_input['PORO'] = np.array( + multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + pem_input['RS'] = None + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + try: + tmp = self.ecl_case.cell_data(var, base_time) + except: + pass + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + # Get the pressure + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=None) + + # mask the bulkimp to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + # kill if values are inf or nan + assert not np.isnan(tmp_value).any() + assert not np.isinf(tmp_value).any() + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + self.pem._filter() + + # 4D response + for i, elem in enumerate(vintage): + self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) + else: + for i, elem in enumerate(vintage): + self.pem_result.append(elem) + + # Extract k-means centers and interias for each element in pem_result + if 'clusters' in self.pem_input: + npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) + n_clusters_list = npzfile['n_clusters_list'] + npzfile.close() + else: + n_clusters_list = len(self.pem_result)*[2] + kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} + for i, bulkimp in enumerate(self.pem_result): + std = np.std(bulkimp) + features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) + scaler = StandardScaler() + scaled_features = scaler.fit_transform(features) + kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) + kmeans.fit(scaled_features) + kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements + self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super().extract_data(member) + + # get the barycenters and inertias + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if key in ['barycenter']: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.bar_result[v].flatten() + +class flow_avo(flow_sim2seis): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' + self._get_avo_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run() + + def run_fwd_sim(self, state, member_i, del_folder=True): + """ + Setup and run the AVO forward simulator. + + Parameters + ---------- + state : dict + Dictionary containing the ensemble state. + + member_i : int + Index of the ensemble member. any index < 0 (e.g., -1) means the ground truth in synthetic case studies + + del_folder : bool, optional + Boolean to determine if the ensemble folder should be deleted. Default is False. + """ + + if member_i >= 0: + folder = 'En_' + str(member_i) + os.sep + if not os.path.exists(folder): + os.mkdir(folder) + else: # XLUO: any negative member_i is considered as the index for the true model + assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ + "the folder containing the true model" + if not os.path.exists(self.input_dict['truth_folder']): + os.mkdir(self.input_dict['truth_folder']) + folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ + else self.input_dict['truth_folder'] + del_folder = False # never delete this folder + self.folder = folder + self.ensemble_member = member_i + + state['member'] = member_i + + # start by generating the .DATA file, using the .mako template situated in ../folder + self._runMako(folder, state) + success = False + rerun = self.rerun + while rerun >= 0 and not success: + success = self.call_sim(folder, True) + rerun -= 1 + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if hasattr(self, 'redund_sim') and self.redund_sim is not None: + success = self.redund_sim.call_sim(folder, True) + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if del_folder: + self.remove_folder(member_i) + return False + else: + if del_folder: + self.remove_folder(member_i) + return False + + + def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): + # replace the sim2seis part (which is unusable) by avo based on Pylops + + if folder is None: + folder = self.folder + + # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" + if hasattr(self, 'run_reservoir_model'): + run_reservoir_model = self.run_reservoir_model + + if run_reservoir_model is None: + run_reservoir_model = True + + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) + success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) + #ecl = ecl_100(filename=self.file) + #ecl.options = self.options + #success = ecl.call_sim(folder, wait_for_proc) + else: + success = True + + if success: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + + self.calc_pem() + + grid = self.ecl_case.grid() + + phases = self.ecl_case.init.phases + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(multfactor * tmp), dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: + tmp = self.ecl_case.cell_data(var, time) + # only active, and conv. to float + pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) + #pem_input[var] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ + self.pem_input['press_conv'] + #pem_input['PRESSURE'] = self._reformat3D_then_flatten(pem_input['PRESSURE'] * + # self.pem_input['press_conv']) + + tmp = self.ecl_case.cell_data('PRESSURE', 0) + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] + #P_init = self._reformat3D_then_flatten(self.pem.p_init.reshape(tmp.shape) * np.ones(tmp.shape)) + else: + # initial pressure is first + P_init = np.array(tmp[~tmp.mask], dtype=float) + #P_init = np.array(self._reformat3D_then_flatten(tmp), dtype=float) + + if 'press_conv' in self.pem_input: + P_init = P_init * self.pem_input['press_conv'] + #P_init = self._reformat3D_then_flatten(P_init * self.pem_input['press_conv']) + + saturations = [ + 1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + #saturations = [self._reformat3D_then_flatten(1 - (pem_input['SWAT'] + pem_input['SGAS'])) + # if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] + + # Get the pressure + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + else: + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) + + #grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { + # 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) + #grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { + # 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) + #grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', + # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) + + # vp, vs, density + self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') + self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') + self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 + + save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} + if save_folder is not None: + file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"vp_vs_rho_vint{v}.npz" + else: + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"vp_vs_rho_vint{v}.npz" + else: + file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" + + #with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + # avo data + self._calc_avo_props() + + avo = self.avo_data.flatten(order="F") + + # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies + # the corresonding (noisy) data are observations in data assimilation + if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: + non_nan_idx = np.argwhere(~np.isnan(avo)) + data_std = np.std(avo[non_nan_idx]) + if self.input_dict['add_synthetic_noise'][0] == 'snr': + noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std + avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) + else: + noise_std = 0.0 # simulated data don't contain noise + + save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} + if save_folder is not None: + file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"avo_vint{v}.npz" + else: + # if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + # (self.ensemble_member >= 0): + # file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ + # else folder + f"avo_vint{v}.npz" + # else: + # file_name = os.getcwd() + os.sep + f"avo_vint{v}.npz" + file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"avo_vint{v}.npz" + + #with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super(flow_sim2seis, self).extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'avo' in key: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ + else self.folder + key + '_vint' + str(idx) + '.npz' + with np.load(filename) as f: + self.pred_data[prim_ind][key] = f[key] + + def _runMako(self, folder, state, addfiles=['properties']): + """ + Hard coding, maybe a better way possible + addfiles: additional files that need to be included into ECLIPSE/OPM DATA file + """ + super()._runMako(folder, state) + + lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') + for file in addfiles: + tmpl = lkup.get_template('%s.mako' % file) + + # use a context and render onto a file + with open('{0}'.format(folder + file), 'w') as f: + ctx = Context(f, **state) + tmpl.render_context(ctx) + + def _get_avo_info(self, avo_config=None): + """ + AVO configuration + """ + # list of configuration parameters in the "AVO" section + config_para_list = ['dz', 'tops', 'angle', 'frequency', 'wave_len', 'vp_shale', 'vs_shale', + 'den_shale', 't_min', 't_max', 't_sampling', 'pp_func'] + if 'avo' in self.input_dict: + self.avo_config = {} + for elem in self.input_dict['avo']: + assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + self.avo_config[elem[0]] = elem[1] + + # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later + if isinstance(self.avo_config['angle'], float): + self.avo_config['angle'] = [self.avo_config['angle']] + + # self._get_DZ(file=self.avo_config['dz']) # =>self.DZ + kw_file = {'DZ': self.avo_config['dz'], 'TOPS': self.avo_config['tops']} + self._get_props(kw_file) + self.overburden = self.pem_input['overburden'] + + # make sure that the "pylops" package is installed + # See https://github.com/PyLops/pylops + self.pp_func = getattr(import_module('pylops.avo.avo'), self.avo_config['pp_func']) + + else: + self.avo_config = None + + def _get_props(self, kw_file): + # extract properties (specified by keywords) in (possibly) different files + # kw_file: a dictionary contains "keyword: file" pairs + # Note that all properties are reshaped into the reservoir model dimension (NX, NY, NZ) + # using the "F" order + for kw in kw_file: + file = kw_file[kw] + if file.endswith('.npz'): + with np.load(file) as f: + exec(f'self.{kw} = f[ "{kw}" ]') + self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] + else: + reader = GRDECL_Parser(filename=file) + reader.read_GRDECL() + exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") + self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ + eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') + + def _calc_avo_props(self, dt=0.0005): + # dt is the fine resolution sampling rate + # convert properties in reservoir model to time domain + vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) + vs_shale = self.avo_config['vs_shale'] # scalar value + rho_shale = self.avo_config['den_shale'] # scalar value + + # Two-way travel time of the top of the reservoir + # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer + top_res = 2 * self.TOPS[:, :, 0] / vp_shale + + # Cumulative traveling time trough the reservoir in vertical direction + cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] + cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) + + # add overburden and underburden of Vp, Vs and Density + vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), + self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), + self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) + rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 + self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) + + # search for the lowest grid cell thickness and sample the time according to + # that grid thickness to preserve the thin layer effect + time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) + if time_sample.shape[0] == 1: + time_sample = time_sample.reshape(-1) + time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) + + vp_sample = np.tile(vp[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + vs_sample = np.tile(vs[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + rho_sample = np.tile(rho[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) + + for m in range(self.NX): + for l in range(self.NY): + for k in range(time_sample.shape[2]): + # find the right interval of time_sample[m, l, k] belonging to, and use + # this information to allocate vp, vs, rho + idx = np.searchsorted(cum_time[m, l, :], time_sample[m, l, k], side='left') + idx = idx if idx < len(cum_time[m, l, :]) else len(cum_time[m, l, :]) - 1 + vp_sample[m, l, k] = vp[m, l, idx] + vs_sample[m, l, k] = vs[m, l, idx] + rho_sample[m, l, k] = rho[m, l, idx] + + + # from matplotlib import pyplot as plt + # plt.plot(vp_sample[0, 0, :]) + # plt.show() + + #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) + #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) + #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) + + #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] + #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] + #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] + + #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) + #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg + + # PP reflection coefficients, see, e.g., + # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" + # So far, it seems that "approx_zoeppritz_pp" is the only available option + # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) + avo_data_list = [] + + # Ricker wavelet + wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), + f0=self.avo_config['frequency']) + + # Travel time corresponds to reflectivity sereis + t = time_sample[:, :, 0:-1] + + # interpolation time + t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) + trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) + + # number of pp reflection coefficients in the vertial direction + nz_rpp = vp_sample.shape[2] - 1 + + for i in range(len(self.avo_config['angle'])): + angle = self.avo_config['angle'][i] + Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], + vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) + + for m in range(self.NX): + for l in range(self.NY): + # convolution with the Ricker wavelet + conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + w_trace = conv_op * Rpp[m, l, :] + + # Sample the trace into regular time interval + f = interp1d(np.squeeze(t[m, l, :]), np.squeeze(w_trace), + kind='nearest', fill_value='extrapolate') + trace_interp[m, l, :] = f(t_interp) + + if i == 0: + avo_data = trace_interp # 3D + elif i == 1: + avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D + else: + avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D + + self.avo_data = avo_data + + @classmethod + def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): + """ + XILU: Quantities read by "EclipseData.cell_data" are put in the axis order of [nz, ny, nx]. To be consisent with + ECLIPSE/OPM custom, we need to change the axis order. We further flatten the array according to the specified order + """ + array = np.array(array) + if len(array.shape) != 1: # if array is a 1D array, then do nothing + assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" + + # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] + new_array = np.transpose(array, axes=[2, 1, 0]) + if flatten: + new_array = new_array.flatten(order=order) + + return new_array + else: + return array \ No newline at end of file From 20390d2cc0ecbc4bcb5034b4feeebdfa2329e77e Mon Sep 17 00:00:00 2001 From: mlienorce Date: Tue, 21 Jan 2025 13:36:09 +0100 Subject: [PATCH 02/15] Ramon co pet (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * GIES and AVO Smeaheia in progress * avo updted, grav included, method for joint grav and avo. calc_pem method in flow_rock that is read by flow_avo * softsand.py represents soft sand rock physics model, added 4D gravity and 4D avo * give overburden velocity values to inactive cells * minor edits --------- Co-authored-by: Martha Økland Lien Co-authored-by: Rolf J. Lorentzen --- setup.py | 1 + simulator/flow_rock.py | 1313 +++++++++++++++++---------- simulator/rockphysics/softsandrp.py | 616 +++++++++++++ simulator/rockphysics/standardrp.py | 7 +- 4 files changed, 1479 insertions(+), 458 deletions(-) create mode 100644 simulator/rockphysics/softsandrp.py diff --git a/setup.py b/setup.py index e1bfbf0..4c9e9f9 100644 --- a/setup.py +++ b/setup.py @@ -45,5 +45,6 @@ 'libecalc', 'scikit-learn', 'pylops' + ] + EXTRAS['doc'], ) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 31aa975..ef7f03c 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -18,12 +18,256 @@ # from pylops import avo from pylops.utils.wavelets import ricker from pylops.signalprocessing import Convolve1D -from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master +import sys +#sys.path.append("/home/AD.NORCERESEARCH.NO/mlie/") +from PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master from scipy.interpolate import interp1d +from scipy.interpolate import griddata from pipt.misc_tools.analysis_tools import store_ensemble_sim_information from geostat.decomp import Cholesky from simulator.eclipse import ecl_100 +class flow_rock(flow): + """ + Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic + quantities can be calculated. Inherit the flow class, and use super to call similar functions. + """ + + def __init__(self, input_dict=None, filename=None, options=None): + super().__init__(input_dict, filename, options) + self._getpeminfo(input_dict) + + self.dum_file_root = 'dummy.txt' + self.dum_entry = str(0) + self.date_slack = None + if 'date_slack' in input_dict: + self.date_slack = int(input_dict['date_slack']) + + # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can + # run a user defined code to do this. + self.saveinfo = None + if 'savesiminfo' in input_dict: + # Make sure "ANALYSISDEBUG" gives a list + if isinstance(input_dict['savesiminfo'], list): + self.saveinfo = input_dict['savesiminfo'] + else: + self.saveinfo = [input_dict['savesiminfo']] + + self.scale = [] + + def _getpeminfo(self, input_dict): + """ + Get, and return, flow and PEM modules + """ + if 'pem' in input_dict: + self.pem_input = {} + for elem in input_dict['pem']: + if elem[0] == 'model': # Set the petro-elastic model + self.pem_input['model'] = elem[1] + if elem[0] == 'depth': # provide the npz of depth values + self.pem_input['depth'] = elem[1] + if elem[0] == 'actnum': # the npz of actnum values + self.pem_input['actnum'] = elem[1] + if elem[0] == 'baseline': # the time for the baseline 4D measurement + self.pem_input['baseline'] = elem[1] + if elem[0] == 'vintage': + self.pem_input['vintage'] = elem[1] + if not type(self.pem_input['vintage']) == list: + self.pem_input['vintage'] = [elem[1]] + if elem[0] == 'ntg': + self.pem_input['ntg'] = elem[1] + if elem[0] == 'press_conv': + self.pem_input['press_conv'] = elem[1] + if elem[0] == 'compaction': + self.pem_input['compaction'] = True + if elem[0] == 'overburden': # the npz of overburden values + self.pem_input['overburden'] = elem[1] + if elem[0] == 'percentile': # use for scaling + self.pem_input['percentile'] = elem[1] + + pem = getattr(import_module('simulator.rockphysics.' + + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) + + self.pem = pem(self.pem_input) + + else: + self.pem = None + + def calc_pem(self, time): + # fluid phases written to restart file from simulator run + phases = self.ecl_case.init.phases + + pem_input = {} + # get active porosity + tmp = self.ecl_case.cell_data('PORO') + if 'compaction' in self.pem_input: + multfactor = self.ecl_case.cell_data('PORV_RC', time) + + pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) + else: + pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + # get active NTG if needed + if 'ntg' in self.pem_input: + if self.pem_input['ntg'] == 'no': + pem_input['NTG'] = None + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + else: + tmp = self.ecl_case.cell_data('NTG') + pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) + + + + if 'RS' in self.pem_input: #ecl_case.cell_data: # to be more robust! + tmp = self.ecl_case.cell_data('RS', time) + pem_input['RS'] = np.array(tmp[~tmp.mask], dtype=float) + else: + pem_input['RS'] = None + print('RS is not a variable in the ecl_case') + + # extract pressure + tmp = self.ecl_case.cell_data('PRESSURE', time) + pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) + + if 'press_conv' in self.pem_input: + pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] + + + if hasattr(self.pem, 'p_init'): + P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] + else: + P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first + + if 'press_conv' in self.pem_input: + P_init = P_init * self.pem_input['press_conv'] + + # extract saturations + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + for var in phases: + if var in ['WAT', 'GAS']: + tmp = self.ecl_case.cell_data('S{}'.format(var), time) + pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + + saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + for ph in phases] + elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model + for var in phases: + if var in ['GAS']: + tmp = self.ecl_case.cell_data('S{}'.format(var), time) + pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + saturations = [1 - (pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] + else: + print('Type and number of fluids are unspecified in calc_pem') + + # fluid saturations in dictionary + tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + self.sats.extend([tmp_s]) + + + #for key in self.all_data_types: + # if 'grav' in key: + # for var in phases: + # # fluid densities + # dens = [var + '_DEN'] + # tmp = self.ecl_case.cell_data(dens, time) + # pem_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + + # # pore volumes at each assimilation step + # tmp = self.ecl_case.cell_data('RPORV', time) + # pem_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + + # Get elastic parameters + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + ensembleMember=self.ensemble_member) + else: + self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], + ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) + + def setup_fwd_run(self, redund_sim): + super().setup_fwd_run(redund_sim=redund_sim) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + success = super().call_sim(folder, wait_for_proc) + + if success: + self.ecl_case = ecl.EclipseCase( + 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') + phases = self.ecl_case.init.phases + self.sats = [] + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + + self.calc_pem(time) + + # mask the bulk imp. to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + # run filter + self.pem._filter() + vintage.append(deepcopy(self.pem.bulkimp)) + + if hasattr(self.pem, 'baseline'): # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + # + self.calc_pem(base_time) + + # mask the bulk imp. to get proper dimensions + tmp_value = np.zeros(self.ecl_case.init.shape) + + tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp + # kill if values are inf or nan + assert not np.isnan(tmp_value).any() + assert not np.isinf(tmp_value).any() + self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, + mask=deepcopy(self.ecl_case.init.mask)) + self.pem._filter() + + # 4D response + self.pem_result = [] + for i, elem in enumerate(vintage): + self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) + else: + for i, elem in enumerate(vintage): + self.pem_result.append(elem) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator + super().extract_data(member) + + # get the sim2seis from file + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if key in ['bulkimp']: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() + + + + class flow_sim2seis(flow): """ Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic @@ -106,7 +350,7 @@ def call_sim(self, folder=None, wait_for_proc=False): success = super().call_sim(folder, wait_for_proc) if success: - # need a if to check that we have correct sim2seis + # need an if to check that we have correct sim2seis # copy relevant sim2seis files into folder. for file in glob.glob('sim2seis_config/*'): shutil.copy(file, 'En_' + str(self.ensemble_member) + os.sep) @@ -123,7 +367,7 @@ def call_sim(self, folder=None, wait_for_proc=False): time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ dt.timedelta(days=assim_time) - self.calc_pem(time) #mali: update class inhere flor_rock. Include calc_pem as method in flow_rock + self.calc_pem(time) #mali: update class inherent in flow_rock. Include calc_pem as method in flow_rock grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { 'Vs': self.pem.getShearVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) @@ -160,10 +404,12 @@ def extract_data(self, member): self.pred_data[prim_ind][key] = np.sum( np.abs(result[:, :, :, v]), axis=0).flatten() -class flow_rock(flow): +class flow_barycenter(flow): """ Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. + quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the + barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are + identified using k-means clustering, and the number of objects are determined using and elbow strategy. """ def __init__(self, input_dict=None, filename=None, options=None): @@ -187,6 +433,8 @@ def __init__(self, input_dict=None, filename=None, options=None): self.saveinfo = [input_dict['savesiminfo']] self.scale = [] + self.pem_result = [] + self.bar_result = [] def _getpeminfo(self, input_dict): """ @@ -217,6 +465,8 @@ def _getpeminfo(self, input_dict): self.pem_input['overburden'] = elem[1] if elem[0] == 'percentile': # use for scaling self.pem_input['percentile'] = elem[1] + if elem[0] == 'clusters': # number of clusters for each barycenter + self.pem_input['clusters'] = elem[1] pem = getattr(import_module('simulator.rockphysics.' + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) @@ -358,263 +608,37 @@ def call_sim(self, folder=None, wait_for_proc=False): self.pem._filter() # 4D response - self.pem_result = [] for i, elem in enumerate(vintage): self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) else: for i, elem in enumerate(vintage): self.pem_result.append(elem) + # Extract k-means centers and interias for each element in pem_result + if 'clusters' in self.pem_input: + npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) + n_clusters_list = npzfile['n_clusters_list'] + npzfile.close() + else: + n_clusters_list = len(self.pem_result)*[2] + kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} + for i, bulkimp in enumerate(self.pem_result): + std = np.std(bulkimp) + features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) + scaler = StandardScaler() + scaled_features = scaler.fit_transform(features) + kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) + kmeans.fit(scaled_features) + kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements + self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) + return success def extract_data(self, member): # start by getting the data from the flow simulator super().extract_data(member) - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['bulkimp']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() - -class flow_barycenter(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the - barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are - identified using k-means clustering, and the number of objects are determined using and elbow strategy. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - self.pem_result = [] - self.bar_result = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - if elem[0] == 'clusters': # number of clusters for each barycenter - self.pem_input['clusters'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - # Extract k-means centers and interias for each element in pem_result - if 'clusters' in self.pem_input: - npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) - n_clusters_list = npzfile['n_clusters_list'] - npzfile.close() - else: - n_clusters_list = len(self.pem_result)*[2] - kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} - for i, bulkimp in enumerate(self.pem_result): - std = np.std(bulkimp) - features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) - scaler = StandardScaler() - scaled_features = scaler.fit_transform(features) - kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) - kmeans.fit(scaled_features) - kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements - self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the barycenters and inertias + # get the barycenters and inertias for prim_ind in self.l_prim: # Loop over all keys in pred_data (all data types) for key in self.all_data_types: @@ -622,9 +646,10 @@ def extract_data(self, member): if self.true_prim[1][prim_ind] in self.pem_input['vintage']: v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) self.pred_data[prim_ind][key] = self.bar_result[v].flatten() - -class flow_avo(flow_sim2seis): + +class flow_avo(flow_rock): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): super().__init__(input_dict, filename, options) @@ -634,7 +659,7 @@ def __init__(self, input_dict=None, filename=None, options=None, **kwargs): def setup_fwd_run(self, **kwargs): self.__dict__.update(kwargs) - super().setup_fwd_run() + super().setup_fwd_run(redund_sim=None) def run_fwd_sim(self, state, member_i, del_folder=True): """ @@ -702,7 +727,6 @@ def run_fwd_sim(self, state, member_i, del_folder=True): self.remove_folder(member_i) return False - def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): # replace the sim2seis part (which is unusable) by avo based on Pylops @@ -719,7 +743,7 @@ def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, s # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. # Then, get the pem. if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) - success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) + success = super(flow_rock, self).call_sim(folder, wait_for_proc) #ecl = ecl_100(filename=self.file) #ecl.options = self.options #success = ecl.call_sim(folder, wait_for_proc) @@ -727,91 +751,114 @@ def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, s success = True if success: - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - grid = self.ecl_case.grid() + self.get_avo_result(folder, save_folder) - #phases = self.ecl_case.init.phases - self.sats = [] - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - # extract dynamic variables from simulation run - self.calc_pem(time) - # vp, vs, density in reservoir - # The properties in pem is only given in the active cells - # indices of active cells: - if grid['ACTNUM'].shape[0] == self.NX: - true_indices = np.where(grid['ACTNUM']) - elif grid['ACTNUM'].shape[0] == self.NZ: - actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) - true_indices = np.where(actnum) - else: - print('warning: dimension mismatch in line 750 flow_rock.py') - - if len(self.pem.getBulkVel()) == len(true_indices[0]): - self.vp = np.zeros(grid['DIMENS']) - self.vp[true_indices] = (self.pem.getBulkVel() * .1) - self.vs = np.zeros(grid['DIMENS']) - self.vs[true_indices] = (self.pem.getShearVel() * .1) - self.rho = np.zeros(grid['DIMENS']) - self.rho[true_indices] = (self.pem.getDens()) - else: - self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 - - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} - if save_folder is not None: - file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"vp_vs_rho_vint{v}.npz" - else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"vp_vs_rho_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" + return success - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) + def get_avo_result(self, folder, save_folder): + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + # phases = self.ecl_case.init.phases + self.sats = [] + vintage = [] + # loop over seismic vintages + for v, assim_time in enumerate(self.pem_input['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + # extract dynamic variables from simulation run + self.calc_pem(time) + + # vp, vs, density in reservoir + self.calc_velocities(folder, save_folder, grid, v) + + # avo data + self._calc_avo_props() + + avo = self.avo_data.flatten(order="F") + + # MLIE: implement 4D avo + if 'baseline' in self.pem_input: # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem_input['baseline']) + self.calc_pem(base_time) + # vp, vs, density in reservoir + self.calc_velocities(folder, save_folder, grid, -1) # avo data self._calc_avo_props() + + avo_baseline = self.avo_data.flatten(order="F") + avo = avo - avo_baseline + + + # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies + # the corresonding (noisy) data are observations in data assimilation + if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: + non_nan_idx = np.argwhere(~np.isnan(avo)) + data_std = np.std(avo[non_nan_idx]) + if self.input_dict['add_synthetic_noise'][0] == 'snr': + noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std + avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) + else: + noise_std = 0.0 # simulated data don't contain noise - avo = self.avo_data.flatten(order="F") - - # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies - # the corresonding (noisy) data are observations in data assimilation - if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: - non_nan_idx = np.argwhere(~np.isnan(avo)) - data_std = np.std(avo[non_nan_idx]) - if self.input_dict['add_synthetic_noise'][0] == 'snr': - noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std - avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) - else: - noise_std = 0.0 # simulated data don't contain noise + save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} + if save_folder is not None: + file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"avo_vint{v}.npz" + else: + file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"avo_vint{v}.npz" - save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - if save_folder is not None: - file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"avo_vint{v}.npz" - else: - file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"avo_vint{v}.npz" + # with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + def calc_velocities(self, folder, save_folder, grid, v): + # The properties in pem are only given in the active cells + # indices of active cells: + if grid['ACTNUM'].shape[0] == self.NX: + true_indices = np.where(grid['ACTNUM']) + elif grid['ACTNUM'].shape[0] == self.NZ: + actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) + true_indices = np.where(actnum) + else: + print('warning: dimension mismatch in line 750 flow_rock.py') + + if len(self.pem.getBulkVel()) == len(true_indices[0]): + self.vp = np.full(grid['DIMENS'], self.avo_config['vp_shale']) + self.vp[true_indices] = (self.pem.getBulkVel()) + self.vs = np.full(grid['DIMENS'], self.avo_config['vs_shale']) + self.vs[true_indices] = (self.pem.getShearVel()) + self.rho = np.full(grid['DIMENS'], self.avo_config['den_shale']) + self.rho[true_indices] = (self.pem.getDens()) + else: + self.vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ), order='F') + self.vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ), order='F') + self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 + + save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} + if save_folder is not None: + file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"vp_vs_rho_vint{v}.npz" + else: + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"vp_vs_rho_vint{v}.npz" + else: + file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) + # with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) - return success def extract_data(self, member): # start by getting the data from the flow simulator - super(flow_sim2seis, self).extract_data(member) + super(flow_rock, self).extract_data(member) # get the sim2seis from file for prim_ind in self.l_prim: @@ -825,105 +872,6 @@ def extract_data(self, member): with np.load(filename) as f: self.pred_data[prim_ind][key] = f[key] - def _runMako(self, folder, state, addfiles=['properties']): - """ - Hard coding, maybe a better way possible - addfiles: additional files that need to be included into ECLIPSE/OPM DATA file - """ - super()._runMako(folder, state) - - lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') - for file in addfiles: - if os.path.exists(file + '.mako'): - tmpl = lkup.get_template('%s.mako' % file) - - # use a context and render onto a file - with open('{0}'.format(folder + file), 'w') as f: - ctx = Context(f, **state) - tmpl.render_context(ctx) - - def calc_pem(self, time): - # fluid phases written to restart file from simulator run - phases = self.ecl_case.init.phases - - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - - - if 'RS' in self.pem_input: #ecl_case.cell_data: # to be more robust! - tmp = self.ecl_case.cell_data('RS', time) - pem_input['RS'] = np.array(tmp[~tmp.mask], dtype=float) - else: - pem_input['RS'] = None - print('RS is not a variable in the ecl_case') - - # extract pressure - tmp = self.ecl_case.cell_data('PRESSURE', time) - pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] - - - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] - else: - P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first - - if 'press_conv' in self.pem_input: - P_init = P_init * self.pem_input['press_conv'] - - # extract saturations - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - for var in phases: - if var in ['WAT', 'GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) - pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model - for var in phases: - if var in ['GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) - pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) - saturations = [1 - (pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - else: - print('Type and number of fluids are unspecified in calc_pem') - - # fluid saturations in dictionary - tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} - self.sats.extend([tmp_s]) - - # Get elastic parameters - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - else: - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) - def _get_avo_info(self, avo_config=None): """ AVO configuration @@ -982,18 +930,27 @@ def _calc_avo_props(self, dt=0.0005): # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer top_res = 2 * self.TOPS[:, :, 0] / vp_shale - # Cumulative traveling time trough the reservoir in vertical direction + # Cumulative traveling time through the reservoir in vertical direction cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] + + # assumes underburden to be constant. No reflections from underburden. Hence set traveltime to underburden very large + underburden = top_res + np.max(cum_time_res) + # total travel time - cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) + # cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res), axis=2) + cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, underburden[:, :, np.newaxis]), axis=2) + # add overburden and underburden of Vp, Vs and Density vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) - rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 - self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) + + #rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 + # self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) + rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)), + self.rho, rho_shale * np.ones((self.NX, self.NY, 1))), axis=2) # search for the lowest grid cell thickness and sample the time according to # that grid thickness to preserve the thin layer effect @@ -1017,40 +974,20 @@ def _calc_avo_props(self, dt=0.0005): vs_sample[m, l, k] = vs[m, l, idx] rho_sample[m, l, k] = rho[m, l, idx] - - # from matplotlib import pyplot as plt - # plt.plot(vp_sample[0, 0, :]) - # plt.show() - - #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) - #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) - #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) - - #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] - #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] - #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] - - #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) - #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg - - # PP reflection coefficients, see, e.g., - # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" - # So far, it seems that "approx_zoeppritz_pp" is the only available option - # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) - avo_data_list = [] - # Ricker wavelet wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), f0=self.avo_config['frequency']) - # Travel time corresponds to reflectivity sereis + + # Travel time corresponds to reflectivity series t = time_sample[:, :, 0:-1] # interpolation time t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) - # number of pp reflection coefficients in the vertial direction + # number of pp reflection coefficients in the vertical direction + nz_rpp = vp_sample.shape[2] - 1 for i in range(len(self.avo_config['angle'])): @@ -1096,3 +1033,469 @@ def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): return new_array else: return array + +class flow_grav(flow_rock): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + self.grav_input = {} + assert 'grav' in input_dict, 'To do GRAV simulation, please specify an "GRAV" section in the "FWDSIM" part' + self._get_grav_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run(redund_sim=None) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + #self.ensemble_member = member_i + #self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + #return self.pred_data + + if member_i >= 0: + folder = 'En_' + str(member_i) + os.sep + if not os.path.exists(folder): + os.mkdir(folder) + else: # XLUO: any negative member_i is considered as the index for the true model + assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ + "the folder containing the true model" + if not os.path.exists(self.input_dict['truth_folder']): + os.mkdir(self.input_dict['truth_folder']) + folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ + else self.input_dict['truth_folder'] + del_folder = False # never delete this folder + self.folder = folder + self.ensemble_member = member_i + + state['member'] = member_i + + # start by generating the .DATA file, using the .mako template situated in ../folder + self._runMako(folder, state) + success = False + rerun = self.rerun + while rerun >= 0 and not success: + success = self.call_sim(folder, True) + rerun -= 1 + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if hasattr(self, 'redund_sim') and self.redund_sim is not None: + success = self.redund_sim.call_sim(folder, True) + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if del_folder: + self.remove_folder(member_i) + return False + else: + if del_folder: + self.remove_folder(member_i) + return False + + def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if folder is None: + folder = self.folder + + # run flow simulator + success = super(flow_rock, self).call_sim(folder, True) + # + # use output from flow simulator to forward model gravity response + if success: + self.get_grav_result(folder, save_folder) + + return success + + def get_grav_result(self, folder, save_folder): + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + # cell centers + self.find_cell_centers(grid) + + # receiver locations + self.measurement_locations(grid) + + # loop over vintages with gravity acquisitions + grav_struct = {} + + for v, assim_time in enumerate(self.grav_config['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + + # porosity, saturation, densities, and fluid mass at individual time-steps + grav_struct[v] = self.calc_mass(time) # calculate the mass of each fluid in each grid cell + + # TODO save densities, saturation and mass for each vintage for plotting? + # grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { + # 'Vs': self.pem.getShearVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) + # grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v+1}.grdecl', { + # 'Vp': self.pem.getBulkVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) + # grdecl.write(f'En_{str(self.ensemble_member)}/rho{v+1}.grdecl', + # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) + if 'baseline' in self.grav_config: # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.grav_config['baseline']) + # porosity, saturation, densities, and fluid mass at time of baseline survey + grav_base = self.calc_mass(base_time) + + + else: + # seafloor gravity only work in 4D mode + print('Need to specify Baseline survey in pipt file') + + vintage = [] + + for v, assim_time in enumerate(self.grav_config['vintage']): + dg = self.calc_grav(grid, grav_base, grav_struct[v]) + vintage.append(deepcopy(dg)) + + save_dic = {'grav': dg, **self.grav_config} + if save_folder is not None: + file_name = save_folder + os.sep + f"grav_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"grav_vint{v}.npz" + else: + file_name = folder + os.sep + f"grav_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"grav_vint{v}.npz" + + # with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + # 4D response + self.grav_result = [] + for i, elem in enumerate(vintage): + self.grav_result.append(elem) + + def calc_mass(self, time): + # fluid phases written to restart file from simulator run + phases = self.ecl_case.init.phases + + grav_input = {} + # get active porosity + # pore volumes at each assimilation step + tmp = self.ecl_case.cell_data('RPORV', time) + grav_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + + # extract saturation + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended + for var in phases: + if var in ['WAT', 'GAS']: + tmp = self.ecl_case.cell_data('S{}'.format(var), time) + grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + + grav_input['SOIL'] = 1 - (grav_input['SWAT'] + grav_input['SGAS']) + + elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model + for var in phases: + if var in ['GAS']: + tmp = self.ecl_case.cell_data('S{}'.format(var), time) + grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + + grav_input['SOIL'] = 1 - (grav_input['SGAS']) + + else: + print('Type and number of fluids are unspecified in calc_mass') + + + + # fluid densities + for var in phases: + dens = var + '_DEN' + tmp = self.ecl_case.cell_data(dens, time) + grav_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + + + #fluid masses + for var in phases: + mass = var + '_mass' + grav_input[mass] = grav_input[var + '_DEN'] * grav_input['S' + var] * grav_input['PORO'] + + return grav_input + + def calc_grav(self, grid, grav_base, grav_repeat): + + + + #cell_centre = [x, y, z] + cell_centre = self.grav_config['cell_centre'] + x = cell_centre[0] + y = cell_centre[1] + z = cell_centre[2] + + pos = self.grav_config['meas_location'] + + # Initialize dg as a zero array, with shape depending on the condition + # assumes the length of each vector gives the total number of measurement points + N_meas = (len(pos['x'])) + dg = np.zeros(N_meas) # 1D array for dg + + # total fluid mass at this time + phases = self.ecl_case.init.phases + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: + dm = grav_repeat['OIL_mass'] + grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['WAT_mass'] + grav_base['GAS_mass']) + + elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model + dm = grav_repeat['OIL_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['GAS_mass']) + + else: + print('Type and number of fluids are unspecified in calc_grav') + + + for j in range(N_meas): + + # Calculate dg for the current measurement location (j, i) + dg_tmp = (z - pos['z'][j]) / ((x[j] - pos['x'][j]) ** 2 + (y[j] - pos['y'][j]) ** 2 + ( + z - pos['z'][j]) ** 2) ** (3 / 2) + + dg[j] = np.dot(dg_tmp, dm) + print(f'Progress: {j + 1}/{N_meas}') # Mimicking waitbar + + # Scale dg by the constant + dg *= 6.67e-3 + + return dg + + def measurement_locations(self, grid): + # Determine the size of the target area as defined by the reservoir area + + #cell_centre = [x, y, z] + cell_centre = self.grav_config['cell_centre'] + xmin = np.min(cell_centre[0]) + xmax = np.max(cell_centre[0]) + ymin = np.min(cell_centre[1]) + ymax = np.max(cell_centre[1]) + + # Make a mesh of the area + pad = self.grav_config.get('padding_reservoir', 3000) # 3 km padding around the reservoir + if 'padding_reservoir' not in self.grav_config: + print('Please specify extent of measurement locations (Padding in pipt file), using 3 km as default') + + xmin -= pad + xmax += pad + ymin -= pad + ymax += pad + + xspan = xmax - xmin + yspan = ymax - ymin + + dxy = self.grav_config.get('grid_spacing', 1500) # + if 'grid_spacing' not in self.grav_config: + print('Please specify grid spacing in pipt file, using 1.5 km as default') + + Nx = int(np.ceil(xspan / dxy)) + Ny = int(np.ceil(yspan / dxy)) + + xvec = np.linspace(xmin, xmax, Nx) + yvec = np.linspace(ymin, ymax, Ny) + + x, y = np.meshgrid(xvec, yvec) + + pos = {'x': x.flatten(), 'y': y.flatten()} + + # Handle seabed map or water depth scalar if defined in pipt + if 'seabed' in self.grav_config and self.grav_config['seabed'] is not None: + pos['z'] = griddata((self.grav_config['seabed']['x'], self.grav_config['seabed']['y']), + self.grav_config['seabed']['z'], (pos['x'], pos['y']), method='nearest') + else: + pos['z'] = np.ones_like(pos['x']) * self.grav_config.get('water_depth', 300) + + if 'water_depth' not in self.grav_config: + print('Please specify water depths in pipt file, using 300 m as default') + + #return pos + self.grav_config['meas_location'] = pos + + def find_cell_centers(self, grid): + + # Find indices where the boolean array is True + indices = np.where(grid['ACTNUM']) + + # `indices` will be a tuple of arrays: (x_indices, y_indices, z_indices) + #nactive = len(actind) # Number of active cells + + coord = grid['COORD'] + zcorn = grid['ZCORN'] + + # Unpack dimensions + #N1, N2, N3 = grid['DIMENS'] + + + c, a, b = indices + # Calculate xt, yt, zt + xb = 0.25 * (coord[a, b, 0, 0] + coord[a, b + 1, 0, 0] + coord[a + 1, b, 0, 0] + coord[a + 1, b + 1, 0, 0]) + yb = 0.25 * (coord[a, b, 0, 1] + coord[a, b + 1, 0, 1] + coord[a + 1, b, 0, 1] + coord[a + 1, b + 1, 0, 1]) + zb = 0.25 * (coord[a, b, 0, 2] + coord[a, b + 1, 0, 2] + coord[a + 1, b, 0, 2] + coord[a + 1, b + 1, 0, 2]) + + xt = 0.25 * (coord[a, b, 1, 0] + coord[a, b + 1, 1, 0] + coord[a + 1, b, 1, 0] + coord[a + 1, b + 1, 1, 0]) + yt = 0.25 * (coord[a, b, 1, 1] + coord[a, b + 1, 1, 1] + coord[a + 1, b, 1, 1] + coord[a + 1, b + 1, 1, 1]) + zt = 0.25 * (coord[a, b, 1, 2] + coord[a, b + 1, 1, 2] + coord[a + 1, b, 1, 2] + coord[a + 1, b + 1, 1, 2]) + + # Calculate z, x, and y positions + z = (zcorn[c, 0, a, 0, b, 0] + zcorn[c, 0, a, 1, b, 0] + zcorn[c, 0, a, 0, b, 1] + zcorn[c, 0, a, 1, b, 1] + + zcorn[c, 1, a, 0, b, 0] + zcorn[c, 1, a, 1, b, 0] + zcorn[c, 1, a, 0, b, 1] + zcorn[c, 1, a, 1, b, 1]) / 8 + + x = xb + (xt - xb) * (z - zb) / (zt - zb) + y = yb + (yt - yb) * (z - zb) / (zt - zb) + + + cell_centre = [x, y, z] + self.grav_config['cell_centre'] = cell_centre + + def _get_grav_info(self, grav_config=None): + """ + GRAV configuration + """ + # list of configuration parameters in the "Grav" section of teh pipt file + config_para_list = ['baseline', 'vintage', 'water_depth', 'padding', 'grid_spacing'] + + if 'grav' in self.input_dict: + self.grav_config = {} + for elem in self.input_dict['grav']: + assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + self.grav_config[elem[0]] = elem[1] + + + else: + self.grav_config = None + + def extract_data(self, member): + # start by getting the data from the flow simulator + super(flow_rock, self).extract_data(member) + + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'grav' in key: + if self.true_prim[1][prim_ind] in self.grav_config['vintage']: + v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.grav_result[v].flatten() + +class flow_grav_and_avo(flow_avo, flow_grav): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + self.grav_input = {} + assert 'grav' in input_dict, 'To do GRAV simulation, please specify an "GRAV" section in the "FWDSIM" part' + self._get_grav_info() + + assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' + self._get_avo_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run(redund_sim=None) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + #self.ensemble_member = member_i + #self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + + #return self.pred_data + + if member_i >= 0: + folder = 'En_' + str(member_i) + os.sep + if not os.path.exists(folder): + os.mkdir(folder) + else: # XLUO: any negative member_i is considered as the index for the true model + assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ + "the folder containing the true model" + if not os.path.exists(self.input_dict['truth_folder']): + os.mkdir(self.input_dict['truth_folder']) + folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ + else self.input_dict['truth_folder'] + del_folder = False # never delete this folder + self.folder = folder + self.ensemble_member = member_i + + state['member'] = member_i + + # start by generating the .DATA file, using the .mako template situated in ../folder + self._runMako(folder, state) + success = False + rerun = self.rerun + while rerun >= 0 and not success: + success = self.call_sim(folder, True) + rerun -= 1 + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if hasattr(self, 'redund_sim') and self.redund_sim is not None: + success = self.redund_sim.call_sim(folder, True) + if success: + self.extract_data(member_i) + if del_folder: + if self.saveinfo is not None: # Try to save information + store_ensemble_sim_information(self.saveinfo, member_i) + self.remove_folder(member_i) + return self.pred_data + else: + if del_folder: + self.remove_folder(member_i) + return False + else: + if del_folder: + self.remove_folder(member_i) + return False + + def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if folder is None: + folder = self.folder + + # run flow simulator + success = super(flow_rock, self).call_sim(folder, True) + + # use output from flow simulator to forward model gravity response + if success: + # calculate gravity data based on flow simulation output + self.get_grav_result(folder, save_folder) + # calculate avo data based on flow simulation output + self.get_avo_result(folder, save_folder) + + return success + + + def extract_data(self, member): + # start by getting the data from the flow simulator i.e. prod. and inj. data + super(flow_rock, self).extract_data(member) + + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'grav' in key: + if self.true_prim[1][prim_ind] in self.grav_config['vintage']: + v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.grav_result[v].flatten() + + if 'avo' in key: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ + else self.folder + key + '_vint' + str(idx) + '.npz' + with np.load(filename) as f: + self.pred_data[prim_ind][key] = f[key] + diff --git a/simulator/rockphysics/softsandrp.py b/simulator/rockphysics/softsandrp.py new file mode 100644 index 0000000..974e2e5 --- /dev/null +++ b/simulator/rockphysics/softsandrp.py @@ -0,0 +1,616 @@ +"""Descriptive description.""" + +__author__ = {'TM', 'TB', 'ML'} + +# standardrp.py +import numpy as np +import sys +import multiprocessing as mp + +from numpy.random import poisson + +# internal load +from misc.system_tools.environ_var import OpenBlasSingleThread # Single threaded OpenBLAS runs + + +class elasticproperties: + """ + Calculate elastic properties from standard + rock-physics models, specifically following Batzle + and Wang, Geophysics, 1992, for fluid properties, and + Report 1 in Abul Fahimuddin's thesis at Universty of + Bergen (2010) for other properties. + + Example + ------- + >>> porosity = 0.2 + ... pressure = 5 + ... phases = ["Oil","Water"] + ... saturations = [0.3, 0.5] + ... + ... satrock = Elasticproperties() + ... satrock.calc_props(phases, saturations, pressure, porosity) + """ + + def __init__(self, input_dict): + self.dens = None + self.bulkmod = None + self.shearmod = None + self.bulkvel = None + self.shearvel = None + self.bulkimp = None + self.shearimp = None + # The overburden for each grid cell must be + # specified as values on an .npz-file whose + # name is given in input_dict. + self.input_dict = input_dict + self._extInfoInputDict() + + def _extInfoInputDict(self): + # The key word for the file name in the + # dictionary must read "overburden" + if 'overburden' in self.input_dict: + obfile = self.input_dict['overburden'] + npzfile = np.load(obfile) + # The values of overburden must have been + # stored on file using: + # np.savez(, + # obvalues=) + self.overburden = npzfile['obvalues'] + npzfile.close() + #else: + # # Norne litho pressure equation in Bar + # P_litho = -49.6 + 0.2027 * Z + 6.127e-6 * Z ** 2 # Using e-6 for scientific notation + # # Convert reservoir pore pressure from Bar to MPa + # P_litho *= 0.1 + # self.overburden = P_litho + + if 'baseline' in self.input_dict: + self.baseline = self.input_dict['baseline'] # 4D baseline + if 'parallel' in self.input_dict: + self.parallel = self.input_dict['parallel'] + + def _filter(self): + bulkmod = self.bulkimp + self.bulkimp = bulkmod.flatten() + + def setup_fwd_run(self, state): + """ + Setup the input parameters to be used in the PEM simulator. Parameters can be an ensemble or a single array. + State is set as an attribute of the simulator, and the correct value is determined in self.pem.calc_props() + + Parameters + ---------- + state : dict + Dictionary of input parameters or states. + + Changelog + --------- + - KF 11/12-2018 + """ + # self.inv_state = {} + # list_pem_param =[el for el in [foo for foo in self.pem['garn'].keys()] + [foo for foo in self.filter.keys()] + + # [foo for foo in self.__dict__.keys()]] + + # list_tot_param = state.keys() + # for param in list_tot_param: + # if param in list_pem_param or (param.split('_')[-1] in ['garn', 'rest']): + # self.inv_state[param] = state[param] + + pass + + def calc_props(self, phases, saturations, pressure, + porosity, dens = None, wait_for_proc=None, ntg=None, Rs=None, press_init=None, ensembleMember=None): + ### + # TODO add fluid densities here -needs to be added as optional input + # + if not isinstance(phases, list): + phases = [phases] + if not isinstance(saturations, list): + saturations = [saturations] + if not isinstance(pressure, list) and \ + type(pressure).__module__ != 'numpy': + pressure = [pressure] + if not isinstance(porosity, list) and \ + type(porosity).__module__ != 'numpy': + porosity = [porosity] + # + # Load "overburden" pressures into local variable to + # comply with remaining code parts + poverburden = self.overburden + if press_init is None: + p_init = self.p_init + else: + p_init = press_init + + # Average number of contacts that each grain has with surrounding grains + coordnumber = self._coordination_number() + + # porosity value separating the porous media's mechanical and acoustic behaviour + phicritical = self._critical_porosity() + + + # Check that no. of phases is equal to no. of + # entries in saturations list + # + assert (len(saturations) == len(phases)) + # + # Make saturation a Numpy array (so that we + # can easily access the values for each + # phase at one grid cell) + # + # Transpose makes it a no. grid cells x phases + # array + saturations = np.array(saturations).T + # + # Check if we actually inputted saturation values + # for a single grid cell. If yes, we redefine + # saturations to get it on the correct form (no. + # grid cells x phases array). + # + if saturations.ndim == 1: + saturations = \ + np.array([[x] for x in saturations]).T + # + # Loop over all grid cells and calculate the + # various saturated properties + # + self.phases = phases + + self.dens = np.zeros(len(saturations[:, 0])) + self.bulkmod = np.zeros(len(saturations[:, 0])) + self.shearmod = np.zeros(len(saturations[:, 0])) + self.bulkvel = np.zeros(len(saturations[:, 0])) + self.shearvel = np.zeros(len(saturations[:, 0])) + self.bulkimp = np.zeros(len(saturations[:, 0])) + self.shearimp = np.zeros(len(saturations[:, 0])) + + if ntg is None: + ntg = [None for _ in range(len(saturations[:, 0]))] + if Rs is None: + Rs = [None for _ in range(len(saturations[:, 0]))] + if p_init is None: + p_init = [None for _ in range(len(saturations[:, 0]))] + + for i in range(len(saturations[:, 0])): + # + # Calculate fluid properties + # + # set Rs if needed + densf, bulkf = \ + self._fluidprops(self.phases, + saturations[i, :], pressure[i], Rs[i]) + # + #denss, bulks, shears = \ + # self._solidprops(porosity[i], ntg[i], i) + + # + denss, bulks, shears = \ + self._solidprops_Johansen() + # + # Calculate dry rock moduli + # + + #bulkd, sheard = \ + # self._dryrockmoduli(porosity[i], + # overburden[i], + # pressure[i], bulks, + # shears, i, ntg[i], p_init[i], denss, Rs[i], self.phases) + # + peff = self._effective_pressure(poverburden[i], pressure[i]) + + + bulkd, sheard = \ + self._dryrockmoduli_Smeaheia(coordnumber, phicritical, porosity[i], peff, bulks, shears) + + # ------------------------------- + # Calculate saturated properties + # ------------------------------- + # + # Density (kg/m3) + # + self.dens[i] = (porosity[i]*densf + + (1-porosity[i])*denss) + # + # Moduli (MPa) + # + self.bulkmod[i] = \ + bulkd + (1 - bulkd/bulks)**2 / \ + (porosity[i]/bulkf + + (1-porosity[i])/bulks - + bulkd/(bulks**2)) + self.shearmod[i] = sheard + # + # Velocities (km/s) + # + self.bulkvel[i] = \ + np.sqrt((abs(self.bulkmod[i]) + + 4*self.shearmod[i]/3)/(self.dens[i])) + self.shearvel[i] = \ + np.sqrt(self.shearmod[i] / + (self.dens[i])) + # + # convert from (km/s) to (m/s) + # + self.bulkvel[i] *= 1000 + self.shearvel[i] *= 1000 + # + # Impedance (m/s)*(kg/m3) + # + self.bulkimp[i] = self.dens[i] * \ + self.bulkvel[i] + self.shearimp[i] = self.dens[i] * \ + self.shearvel[i] + + def getMatchProp(self, petElProp): + if petElProp.lower() == 'density': + self.match_prop = self.getDens() + elif petElProp.lower() == 'bulk_modulus': + self.match_prop = self.getBulkMod() + elif petElProp.lower() == 'shear_modulus': + self.match_prop = self.getShearMod() + elif petElProp.lower() == 'bulk_velocity': + self.match_prop = self.getBulkVel() + elif petElProp.lower() == 'shear_velocity': + self.match_prop = self.getShearVel() + elif petElProp.lower() == "bulk_impedance": + self.match_prop = self.getBulkImp() + elif petElProp.lower() == 'shear_impedance': + self.match_prop = self.getShearImp() + else: + print("\nError in getMatchProp method") + print("No model output type selected for " + "data match.") + print("Legal model output types are " + "(case insensitive):") + print("Density, bulk modulus, shear " + "modulus, bulk velocity,") + print("shear velocity, bulk impedance, " + "shear impedance") + sys.exit(1) + return self.match_prop + + def getDens(self): + return self.dens + + def getBulkMod(self): + return self.bulkmod + + def getShearMod(self): + return self.shearmod + + def getBulkVel(self): + return self.bulkvel + + def getShearVel(self): + return self.shearvel + + def getBulkImp(self): + return self.bulkimp + + def getShearImp(self): + return self.shearimp + + # + # =================================================== + # Fluid properties start + # =================================================== + # + def _fluidprops(self, fphases, fsats, fpress, Rs=None): + # + # Calculate fluid density and bulk modulus + # + # + # Input + # fphases - fluid phases present; Oil + # and/or Water and/or Gas + # fsats - fluid saturation values for + # fluid phases in "fphases" + # fpress - fluid pressure value (MPa) + # Rs - Gas oil ratio. Default value None + + # + # Output + # fdens - density of fluid mixture for + # pressure value "fpress" inherited + # from phaseprops) + # fbulk - bulk modulus of fluid mixture for + # pressure value "fpress" (unit + # inherited from phaseprops) + # + # ----------------------------------------------- + # + fdens = 0.0 + fbinv = 0.0 + + for i in range(len(fphases)): + # + # Calculate mixture properties by summing + # over individual phase properties + # + pdens, pbulk = self._phaseprops(fphases[i], + fpress, Rs) + fdens = fdens + fsats[i]*abs(pdens) + fbinv = fbinv + fsats[i]/abs(pbulk) + fbulk = 1.0/fbinv + # + return fdens, fbulk + # + # --------------------------------------------------- + # + + def _phaseprops(self, fphase, press, Rs=None): + # + # Calculate properties for a single fluid phase + # + # + # Input + # fphase - fluid phase; Oil, Water or Gas + # press - fluid pressure value (MPa) + # + # Output + # pdens - phase density of fluid phase + # "fphase" for pressure value + # "press" (kg/m³) + # pbulk - bulk modulus of fluid phase + # "fphase" for pressure value + # "press" (MPa) + # + # ----------------------------------------------- + # + if fphase.lower() == "oil": + coeffsrho = np.array([0.8, 829.9]) + coeffsbulk = np.array([10.42, 995.79]) + elif fphase.lower() == "wat": + coeffsrho = np.array([0.3, 1067.3]) + coeffsbulk = np.array([9.0, 2807.6]) + elif fphase.lower() == "gas": + coeffsrho = np.array([4.7, 13.4]) + coeffsbulk = np.array([2.75, 0.0]) + else: + print("\nError in phaseprops method") + print("Illegal fluid phase name.") + print("Legal fluid phase names are (case " + "insensitive): Oil, Wat, and Gas.") + sys.exit(1) + # + # Assume simple linear pressure dependencies. + # Coefficients are inferred from + # plots in Batzle and Wang, Geophysics, 1992, + # (where I set the temperature to be 100 degrees + # Celsius, Note also that they give densities in + # g/cc). The resulting straight lines do not fit + # the data extremely well, but they should + # be sufficiently accurate for the purpose of + # this project. + # + pdens = coeffsrho[0]*press + coeffsrho[1] + pbulk = coeffsbulk[0]*press + coeffsbulk[1] + # + return pdens, pbulk + + # + # ======================= + # Fluid properties end + # ======================= + # + + # + # ========================= + # Solid properties start + # ========================= + # + def _solidprops_Johansen(self): + # + # Calculate bulk and shear solid rock (mineral) + # moduli by averaging Hashin-Shtrikman bounds + # + # + # Input + # poro -porosity + # + # Output + # denss - solid rock density (kg/m³) + # bulks - solid rock bulk modulus (unit MPa) + # shears - solid rock shear modulus (unit MPa) + # + # ----------------------------------------------- + # + # + # Solid rock (mineral) density. (Note + # that this is often termed \rho_dry, and not + # \rho_s) + + denss = 2650 # Density of mineral/solid rock kg/m3 + + # + bulks = 37 # (GPa) + shears = 44 # (GPa) + bulks *= 1000 # Convert from GPa to MPa + shears *= 1000 + # + return denss, bulks, shears + # + # + # ======================= + # Solid properties end + # ======================= + # + def _coordination_number(self): + # Applies for granular media + # Average number of contacts that each grain has with surrounding grains + # Coordnumber = 6; simple cubic packing + # Coordnumber = 12; hexagonal close packing + # Needed for Hertz-Mindlin model + # Smeaheia number (Tuhin) + coordnumber = 9 + + return coordnumber + # + def _critical_porosity(self): + # For most porous media there exists a critical porosity + # phi_critical, that seperates their mechanical and acoustic behaviour into two domains. + # For porosities below phi_critical the mineral grains are oad bearing, for values above the grains are + # suspended in the fluids which are load-bearing + # Needed for Hertz-Mindlin model + # Smeaheia number (Tuhin) + phicritical = 0.36 + + return phicritical + # + def _effective_pressure(self, poverb, pfluid): + + # Input + # poverb - overburden pressure (MPa) + # pfluid - fluid pressure (MPa) + + peff = poverb - pfluid + + if peff < 0: + print("\nError in _hertzmindlin method") + print("Negative effective pressure (" + str(peff) + + "). Setting effective pressure to 0.01") + peff = 0.01 + + + + return peff + + # ============================ + # Dry rock properties start + # ============================ + # + def _dryrockmoduli_Smeaheia(self, coordnumber, phicritical, poro, peff, bulks, shears): + # + # + # Calculate bulk and shear dry rock moduli, + + # + # Input + # poro - porosity + # peff - effective pressure overburden - fluid pressure (MPa) + # bulks - bulk solid (mineral) rock bulk + # modulus (MPa) + # shears - solid rock (mineral) shear + # modulus (MPa) + # + # Output + # bulkd - dry rock bulk modulus (unit + # inherited from hertzmindlin and + # variable; bulks) + # sheard - dry rock shear modulus (unit + # inherited from hertzmindlin and + # variable; shears) + # + # ----------------------------------------------- + # + # Calculate Hertz-Mindlin moduli + # + bulkhm, shearhm = self._hertzmindlin_Mavko(peff, bulks, shears, coordnumber, phicritical) + # + bulkd = 1 / ((poro / phicritical) / (bulkhm + 4 / 3 * shearhm) + + (1 - poro / phicritical) / (bulks + 4 / 3 * shearhm)) - 4 / 3 * shearhm + + psi = (9 * bulkhm + 8 * shearhm) / (bulkhm + 2 * shearhm) + + sheard = 1 / ((poro / phicritical) / (shearhm + 1 / 6 * psi * shearhm) + + (1 - poro / phicritical) / (shears + 1 / 6 * psi * shearhm)) - 1 / 6 * psi * shearhm + + #return K_dry, G_dry + return bulkd, sheard + + + # + # --------------------------------------------------- + # + + def _hertzmindlin_Mavko(self, peff, bulks, shears, coordnumber, phicritical): + # + # Calculate bulk and shear Hertz-Mindlin moduli + # adapted from Tuhins kode and "The rock physics handbook", pp247 + # + # + # Input + # p_eff - effective pressure + # bulks - bulk solid (mineral) rock bulk + # modulus (MPa) + # shears - solid rock (mineral) shear + # modulus (MPa) + # coordnumber - average number of contacts that each grain has with surrounding grains + # phicritical - critical porosity + # + # Output + # bulkhm - Hertz-Mindlin bulk modulus + # (MPa) + # shearhm - Hertz-Mindlin shear modulus + # (MPa) + # + # ----------------------------------------------- + # + + + poisson = (3 * bulks - 2 * shears) / (6 * bulks + 2 * shears) + + bulkhm = ((coordnumber ** 2 * (1 - phicritical) ** 2 * shears ** 2 * peff) / + (18 * np.pi ** 2 * (1 - poisson) ** 2)) ** (1 / 3) + shearhm = (5 - 4 * poisson) / (10 - 5 * poisson) * \ + ((3 * coordnumber ** 2 * (1 - phicritical) ** 2 * shears ** 2 * peff) / + (2 * np.pi ** 2 * (1 - poisson) ** 2)) ** (1 / 3) + + + + # + return bulkhm, shearhm + + + # =========================== + # Dry rock properties end + # =========================== + + +if __name__ == '__main__': + # + # Example input with two phases and three grid cells + # + porosity = [0.34999999, 0.34999999, 0.34999999] +# pressure = [ 29.29150963, 29.14003944, 28.88845444] + pressure = [29.3558, 29.2625, 29.3558] +# pressure = [ 25.0, 25.0, 25.0] + phases = ["Oil", "Wat"] +# saturations = [[0.72783828, 0.66568458, 0.58033288], +# [0.27216172, 0.33431542, 0.41966712]] + saturations = [[0.6358, 0.5755, 0.6358], + [0.3641, 0.4245, 0.3641]] +# saturations = [[0.4, 0.5, 0.6], +# [0.6, 0.5, 0.4]] + petElProp = "bulk velocity" + input_dict = {} + input_dict['overburden'] = 'overb.npz' + + print("\nInput:") + print("porosity, pressure:", porosity, pressure) + print("phases, saturations:", phases, saturations) + print("petElProp:", petElProp) + print("input_dict:", input_dict) + + satrock = elasticproperties(input_dict) + + print("overburden:", satrock.overburden) + + satrock.calc_props(phases, saturations, pressure, + porosity) + + print("\nOutput from calc_props:") + print("Density:", satrock.getDens()) + print("Bulk modulus:", satrock.getBulkMod()) + print("Shear modulus:", satrock.getShearMod()) + print("Bulk velocity:", satrock.getBulkVel()) + print("Shear velocity:", satrock.getShearVel()) + print("Bulk impedance:", satrock.getBulkImp()) + print("Shear impedance:", satrock.getShearImp()) + + satrock.getMatchProp(petElProp) + + print("\nOutput from getMatchProp:") + print("Model output selected for data match:", + satrock.match_prop) diff --git a/simulator/rockphysics/standardrp.py b/simulator/rockphysics/standardrp.py index f847ed0..316fd04 100644 --- a/simulator/rockphysics/standardrp.py +++ b/simulator/rockphysics/standardrp.py @@ -135,6 +135,7 @@ def calc_props(self, phases, saturations, pressure, # Load "overburden" into local variable to # comply with remaining code parts overburden = self.overburden + if press_init is None: p_init = self.p_init else: @@ -209,7 +210,7 @@ def calc_props(self, phases, saturations, pressure, # Density # self.dens[i] = (porosity[i]*densf + - (1-porosity[i])*denss)*0.001 + (1-porosity[i])*denss) # # Moduli # @@ -225,10 +226,10 @@ def calc_props(self, phases, saturations, pressure, # instead of km/s) # self.bulkvel[i] = \ - 100*np.sqrt((abs(self.bulkmod[i]) + + 1000*np.sqrt((abs(self.bulkmod[i]) + 4*self.shearmod[i]/3)/(self.dens[i])) self.shearvel[i] = \ - 100*np.sqrt(self.shearmod[i] / + 1000*np.sqrt(self.shearmod[i] / (self.dens[i])) # # Impedances (m/s)*(Kg/m3) From 9bb0d9ccbb74b7d4f4ea1e003a30042856f90773 Mon Sep 17 00:00:00 2001 From: Rolf Johan Lorentzen Date: Tue, 21 Jan 2025 16:06:02 +0100 Subject: [PATCH 03/15] Rename keys variable --- ensemble/ensemble.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ensemble/ensemble.py b/ensemble/ensemble.py index 676a059..565f659 100644 --- a/ensemble/ensemble.py +++ b/ensemble/ensemble.py @@ -477,7 +477,7 @@ def get_list_assim_steps(self): List of total assimilation steps. """ # Get list of assim. steps. from ASSIMINDEX - list_assim = list(range(len(self.keys_da['assimindex']))) + list_assim = list(range(len(self.keys_en['assimindex']))) # If it is a restart run, we only list the assimilation steps we have not done if self.restart is True: @@ -555,7 +555,7 @@ def calc_prediction(self, input_state=None, save_prediction=None): # modified by xluo, for including the simulation of the mean reservoir model # as used in the RLM-MAC algorithm - if self.keys_da['daalg'][1] == 'gies': + if self.keys_en['daalg'][1] == 'gies': list_state.append({}) list_member_index.append(self.ne) From 97c1aa30c17c78ea8537a44f4f7ecaa8ea8403d6 Mon Sep 17 00:00:00 2001 From: mlienorce Date: Tue, 28 Jan 2025 12:42:44 +0100 Subject: [PATCH 04/15] Ramon co pet (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * GIES and AVO Smeaheia in progress * avo updted, grav included, method for joint grav and avo. calc_pem method in flow_rock that is read by flow_avo * softsand.py represents soft sand rock physics model, added 4D gravity and 4D avo * give overburden velocity values to inactive cells * minor edits * Compute reflection coefficients for active cells --------- Co-authored-by: Martha Økland Lien Co-authored-by: Rolf J. Lorentzen --- simulator/flow_rock.py | 152 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 135 insertions(+), 17 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index ef7f03c..92b63f5 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -27,6 +27,7 @@ from geostat.decomp import Cholesky from simulator.eclipse import ecl_100 + class flow_rock(flow): """ Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic @@ -167,16 +168,21 @@ def calc_pem(self, time): #for key in self.all_data_types: - # if 'grav' in key: - # for var in phases: - # # fluid densities - # dens = [var + '_DEN'] - # tmp = self.ecl_case.cell_data(dens, time) - # pem_input[dens] = np.array(tmp[~tmp.mask], dtype=float) - - # # pore volumes at each assimilation step - # tmp = self.ecl_case.cell_data('RPORV', time) - # pem_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + #if 'grav' in key: + for var in phases: + # fluid densities + dens = [var + '_DEN'] + try: + tmp = self.ecl_case.cell_data(dens, time) + pem_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + except: + pem_input[dens] = None + # pore volumes at each assimilation step + try: + tmp = self.ecl_case.cell_data('RPORV', time) + pem_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + except: + pem_input['RPORV'] = None # Get elastic parameters if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ @@ -649,7 +655,6 @@ def extract_data(self, member): class flow_avo(flow_rock): - def __init__(self, input_dict=None, filename=None, options=None, **kwargs): super().__init__(input_dict, filename, options) @@ -774,7 +779,8 @@ def get_avo_result(self, folder, save_folder): self.calc_velocities(folder, save_folder, grid, v) # avo data - self._calc_avo_props() + #self._calc_avo_props() + self._calc_avo_props_active_cells(grid) avo = self.avo_data.flatten(order="F") @@ -787,8 +793,9 @@ def get_avo_result(self, folder, save_folder): self.calc_velocities(folder, save_folder, grid, -1) # avo data - self._calc_avo_props() - + #self._calc_avo_props() + self._calc_avo_props_active_cells(grid) + avo_baseline = self.avo_data.flatten(order="F") avo = avo - avo_baseline @@ -974,6 +981,7 @@ def _calc_avo_props(self, dt=0.0005): vs_sample[m, l, k] = vs[m, l, idx] rho_sample[m, l, k] = rho[m, l, idx] + # Ricker wavelet wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), f0=self.avo_config['frequency']) @@ -1015,6 +1023,114 @@ def _calc_avo_props(self, dt=0.0005): self.avo_data = avo_data + + def _calc_avo_props_active_cells(self, grid, dt=0.005): + # dt is the fine resolution sampling rate + # convert properties in reservoir model to time domain + vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) + vs_shale = self.avo_config['vs_shale'] # scalar value + rho_shale = self.avo_config['den_shale'] # scalar value + + # Two-way travel time of the top of the reservoir + # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer + top_res = 2 * self.TOPS[:, :, 0] / vp_shale + + # Cumulative traveling time through the reservoir in vertical direction + cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] + + # assumes underburden to be constant. No reflections from underburden. Hence set traveltime to underburden very large + underburden = top_res + np.max(cum_time_res) + + # total travel time + # cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res), axis=2) + cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, underburden[:, :, np.newaxis]), axis=2) + + # add overburden and underburden of Vp, Vs and Density + vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), + self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), + self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) + #rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 + # self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) + rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)), + self.rho, rho_shale * np.ones((self.NX, self.NY, 1))), axis=2) + + # get indices of active cells + actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) + indices = np.where(actnum) + a, b, c = indices + # Combine a and b into a 2D array (each column represents a vector) + ab = np.column_stack((a, b)) + + # Extract unique rows and get the indices of those unique rows + unique_rows, indices = np.unique(ab, axis=0, return_index=True) + + + # search for the lowest grid cell thickness and sample the time according to + # that grid thickness to preserve the thin layer effect + time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) + if time_sample.shape[0] == 1: + time_sample = time_sample.reshape(-1) + time_sample = np.tile(time_sample, (len(indices), 1)) + + vp_sample = np.zeros((len(indices), time_sample.shape[1])) + vs_sample = np.zeros((len(indices), time_sample.shape[1])) + rho_sample = np.zeros((len(indices), time_sample.shape[1])) + + + for ind in range(len(indices)): + for k in range(time_sample.shape[1]): + # find the right interval of time_sample[m, l, k] belonging to, and use + # this information to allocate vp, vs, rho + idx = np.searchsorted(cum_time[a[indices[ind]], b[indices[ind]], :], time_sample[ind, k], side='left') + idx = idx if idx < len(cum_time[a[indices[ind]], b[indices[ind]], :]) else len(cum_time[a[indices[ind]], b[indices[ind]], :]) - 1 + vp_sample[ind, k] = vp[a[indices[ind]], b[indices[ind]], idx] + vs_sample[ind, k] = vs[a[indices[ind]], b[indices[ind]], idx] + rho_sample[ind, k] = rho[a[indices[ind]], b[indices[ind]], idx] + + + # Ricker wavelet + wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), + f0=self.avo_config['frequency']) + + # Travel time corresponds to reflectivity series + t = time_sample[:, 0:-1] + + # interpolation time + t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) + trace_interp = np.zeros((len(indices), len(t_interp))) + + # number of pp reflection coefficients in the vertical direction + nz_rpp = vp_sample.shape[1] - 1 + conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + + for i in range(len(self.avo_config['angle'])): + angle = self.avo_config['angle'][i] + Rpp = self.pp_func(vp_sample[:, :-1], vs_sample[:, :-1], rho_sample[:, :-1], + vp_sample[:, 1:], vs_sample[:, 1:], rho_sample[:, 1:], angle) + + + + for ind in range(len(indices)): + # convolution with the Ricker wavelet + + w_trace = conv_op * Rpp[ind, :] + + # Sample the trace into regular time interval + f = interp1d(np.squeeze(t[ind, :]), np.squeeze(w_trace), + kind='nearest', fill_value='extrapolate') + trace_interp[ind, :] = f(t_interp) + + if i == 0: + avo_data = trace_interp # 3D + elif i == 1: + avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D + else: + avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D + + self.avo_data = avo_data + + @classmethod def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): """ @@ -1323,8 +1439,9 @@ def measurement_locations(self, grid): def find_cell_centers(self, grid): # Find indices where the boolean array is True - indices = np.where(grid['ACTNUM']) - + #indices = np.where(grid['ACTNUM']) + actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) + indices = np.where(actnum) # `indices` will be a tuple of arrays: (x_indices, y_indices, z_indices) #nactive = len(actind) # Number of active cells @@ -1335,7 +1452,7 @@ def find_cell_centers(self, grid): #N1, N2, N3 = grid['DIMENS'] - c, a, b = indices + b, a, c = indices # Calculate xt, yt, zt xb = 0.25 * (coord[a, b, 0, 0] + coord[a, b + 1, 0, 0] + coord[a + 1, b, 0, 0] + coord[a + 1, b + 1, 0, 0]) yb = 0.25 * (coord[a, b, 0, 1] + coord[a, b + 1, 0, 1] + coord[a + 1, b, 0, 1] + coord[a + 1, b + 1, 0, 1]) @@ -1466,6 +1583,7 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): folder = self.folder # run flow simulator + #success = True #super(flow_rock, self).call_sim(folder, True) success = super(flow_rock, self).call_sim(folder, True) # use output from flow simulator to forward model gravity response From f78ed3c07ba2f5025e4b26d1e124a0b4210bba3b Mon Sep 17 00:00:00 2001 From: mlienorce Date: Wed, 12 Feb 2025 15:28:12 +0100 Subject: [PATCH 05/15] Bug fix of avo and gravity (#77) * Bug fix of avo and gravity * Bug fix --- ensemble/ensemble.py | 2 +- simulator/flow_rock.py | 203 ++++++++++++++++++++-------- simulator/rockphysics/softsandrp.py | 145 ++++++++++++++++++-- 3 files changed, 283 insertions(+), 67 deletions(-) diff --git a/ensemble/ensemble.py b/ensemble/ensemble.py index 565f659..e71cc84 100644 --- a/ensemble/ensemble.py +++ b/ensemble/ensemble.py @@ -555,7 +555,7 @@ def calc_prediction(self, input_state=None, save_prediction=None): # modified by xluo, for including the simulation of the mean reservoir model # as used in the RLM-MAC algorithm - if self.keys_en['daalg'][1] == 'gies': + if 'daalg' in self.keys_en and self.keys_en['daalg'][1] == 'gies': list_state.append({}) list_member_index.append(self.ne) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 92b63f5..64d64c8 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -4,6 +4,7 @@ import datetime as dt import numpy as np import os +import pandas as pd from misc import ecl, grdecl import shutil import glob @@ -19,7 +20,7 @@ from pylops.utils.wavelets import ricker from pylops.signalprocessing import Convolve1D import sys -#sys.path.append("/home/AD.NORCERESEARCH.NO/mlie/") +sys.path.append("/home/AD.NORCERESEARCH.NO/mlie/") from PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master from scipy.interpolate import interp1d from scipy.interpolate import griddata @@ -166,33 +167,35 @@ def calc_pem(self, time): tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} self.sats.extend([tmp_s]) - + keywords = self.ecl_case.arrays(time) + keywords = [s.strip() for s in keywords] # Remove leading/trailing spaces #for key in self.all_data_types: #if 'grav' in key: + densities = [] for var in phases: # fluid densities - dens = [var + '_DEN'] - try: + dens = var + '_DEN' + if dens in keywords: tmp = self.ecl_case.cell_data(dens, time) pem_input[dens] = np.array(tmp[~tmp.mask], dtype=float) - except: - pem_input[dens] = None + # extract densities + densities.append(pem_input[dens]) + else: + densities = None # pore volumes at each assimilation step - try: + if 'RPORV' in keywords: tmp = self.ecl_case.cell_data('RPORV', time) pem_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) - except: - pem_input['RPORV'] = None # Get elastic parameters if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ (self.ensemble_member >= 0): self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, + dens = densities, ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, ensembleMember=self.ensemble_member) else: self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) + dens = densities, ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) def setup_fwd_run(self, redund_sim): super().setup_fwd_run(redund_sim=redund_sim) @@ -764,9 +767,28 @@ def get_avo_result(self, folder, save_folder): self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ else ecl.EclipseCase(folder + self.file + '.DATA') grid = self.ecl_case.grid() - + #ecl_init = ecl.EclipseInit(ecl_case) + f_dim = [self.ecl_case.init.nk, self.ecl_case.init.nj, self.ecl_case.init.ni] # phases = self.ecl_case.init.phases self.sats = [] + + if 'baseline' in self.pem_input: # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem_input['baseline']) + self.calc_pem(base_time) + # vp, vs, density in reservoir + self.calc_velocities(folder, save_folder, grid, -1, f_dim) + + # avo data + # self._calc_avo_props() + self._calc_avo_props_active_cells(grid) + + avo_baseline = self.avo_data.flatten(order="F") + Rpp_baseline = self.Rpp + vs_baseline = self.vs_sample + vp_baseline = self.vp_sample + rho_baseline = self.rho_sample + vintage = [] # loop over seismic vintages for v, assim_time in enumerate(self.pem_input['vintage']): @@ -776,7 +798,7 @@ def get_avo_result(self, folder, save_folder): self.calc_pem(time) # vp, vs, density in reservoir - self.calc_velocities(folder, save_folder, grid, v) + self.calc_velocities(folder, save_folder, grid, v, f_dim) # avo data #self._calc_avo_props() @@ -786,18 +808,16 @@ def get_avo_result(self, folder, save_folder): # MLIE: implement 4D avo if 'baseline' in self.pem_input: # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem_input['baseline']) - self.calc_pem(base_time) - # vp, vs, density in reservoir - self.calc_velocities(folder, save_folder, grid, -1) - - # avo data - #self._calc_avo_props() - self._calc_avo_props_active_cells(grid) - - avo_baseline = self.avo_data.flatten(order="F") avo = avo - avo_baseline + Rpp = self.Rpp - Rpp_baseline + Vs = self.vs_sample - vs_baseline + Vp = self.vp_sample - vp_baseline + rho = self.rho_sample - rho_baseline + else: + Rpp = self.Rpp + Vs = self.vs_sample + Vp = self.vp_sample + rho = self.rho_sample # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies @@ -811,7 +831,9 @@ def get_avo_result(self, folder, save_folder): else: noise_std = 0.0 # simulated data don't contain noise - save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} + #save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} + save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': Vs, 'Vp': Vp, 'rho': rho, **self.avo_config} + if save_folder is not None: file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"avo_vint{v}.npz" @@ -823,30 +845,50 @@ def get_avo_result(self, folder, save_folder): # dump(**save_dic, f) np.savez(file_name, **save_dic) - def calc_velocities(self, folder, save_folder, grid, v): + def calc_velocities(self, folder, save_folder, grid, v, f_dim): # The properties in pem are only given in the active cells # indices of active cells: - if grid['ACTNUM'].shape[0] == self.NX: - true_indices = np.where(grid['ACTNUM']) - elif grid['ACTNUM'].shape[0] == self.NZ: - actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) - true_indices = np.where(actnum) - else: - print('warning: dimension mismatch in line 750 flow_rock.py') + + true_indices = np.where(grid['ACTNUM']) + + + + # Alt 2 if len(self.pem.getBulkVel()) == len(true_indices[0]): - self.vp = np.full(grid['DIMENS'], self.avo_config['vp_shale']) + #self.vp = np.full(f_dim, self.avo_config['vp_shale']) + self.vp = np.full(f_dim, np.nan) self.vp[true_indices] = (self.pem.getBulkVel()) - self.vs = np.full(grid['DIMENS'], self.avo_config['vs_shale']) + #self.vs = np.full(f_dim, self.avo_config['vs_shale']) + self.vs = np.full(f_dim, np.nan) self.vs[true_indices] = (self.pem.getShearVel()) - self.rho = np.full(grid['DIMENS'], self.avo_config['den_shale']) + #self.rho = np.full(f_dim, self.avo_config['den_shale']) + self.rho = np.full(f_dim, np.nan) self.rho[true_indices] = (self.pem.getDens()) + else: self.vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ), order='F') self.vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ), order='F') self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} + ## Debug + self.bulkmod = np.full(f_dim, np.nan) + self.bulkmod[true_indices] = self.pem.getBulkMod() + self.shearmod = np.full(f_dim, np.nan) + self.shearmod[true_indices] = self.pem.getShearMod() + self.poverburden = np.full(f_dim, np.nan) + self.poverburden[true_indices] = self.pem.getOverburdenP() + self.pressure = np.full(f_dim, np.nan) + self.pressure[true_indices] = self.pem.getPressure() + self.peff = np.full(f_dim, np.nan) + self.peff[true_indices] = self.pem.getPeff() + self.porosity = np.full(f_dim, np.nan) + self.porosity[true_indices] = self.pem.getPorosity() + + # + + + save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.rho, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, 'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': self.porosity} if save_folder is not None: file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"vp_vs_rho_vint{v}.npz" @@ -982,11 +1024,14 @@ def _calc_avo_props(self, dt=0.0005): rho_sample[m, l, k] = rho[m, l, idx] + + # Ricker wavelet wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), f0=self.avo_config['frequency']) + # Travel time corresponds to reflectivity series t = time_sample[:, :, 0:-1] @@ -1031,10 +1076,22 @@ def _calc_avo_props_active_cells(self, grid, dt=0.005): vs_shale = self.avo_config['vs_shale'] # scalar value rho_shale = self.avo_config['den_shale'] # scalar value + # check if Nz, is at axis = 0, then transpose to dimensions, Nx, ny, Nz + if grid['ACTNUM'].shape[0] == self.NZ: + self.vp = np.transpose(self.vp, (2, 1, 0)) + self.vs = np.transpose(self.vs, (2, 1, 0)) + self.rho = np.transpose(self.rho, (2, 1, 0)) + actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) + else: + actnum = grid['ACTNUM'] + # # # + # Two-way travel time of the top of the reservoir # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer top_res = 2 * self.TOPS[:, :, 0] / vp_shale + + # Cumulative traveling time through the reservoir in vertical direction cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] @@ -1056,7 +1113,7 @@ def _calc_avo_props_active_cells(self, grid, dt=0.005): self.rho, rho_shale * np.ones((self.NX, self.NY, 1))), axis=2) # get indices of active cells - actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) + indices = np.where(actnum) a, b, c = indices # Combine a and b into a 2D array (each column represents a vector) @@ -1129,6 +1186,10 @@ def _calc_avo_props_active_cells(self, grid, dt=0.005): avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D self.avo_data = avo_data + self.Rpp = Rpp + self.vp_sample = vp_sample + self.vs_sample = vs_sample + self.rho_sample = rho_sample @classmethod @@ -1256,13 +1317,7 @@ def get_grav_result(self, folder, save_folder): # porosity, saturation, densities, and fluid mass at individual time-steps grav_struct[v] = self.calc_mass(time) # calculate the mass of each fluid in each grid cell - # TODO save densities, saturation and mass for each vintage for plotting? - # grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { - # 'Vs': self.pem.getShearVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - # grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v+1}.grdecl', { - # 'Vp': self.pem.getBulkVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - # grdecl.write(f'En_{str(self.ensemble_member)}/rho{v+1}.grdecl', - # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) + if 'baseline' in self.grav_config: # 4D measurement base_time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + dt.timedelta(days=self.grav_config['baseline']) @@ -1272,7 +1327,7 @@ def get_grav_result(self, folder, save_folder): else: # seafloor gravity only work in 4D mode - print('Need to specify Baseline survey in pipt file') + print('Need to specify Baseline survey for gravity in pipt file') vintage = [] @@ -1292,6 +1347,21 @@ def get_grav_result(self, folder, save_folder): # dump(**save_dic, f) np.savez(file_name, **save_dic) + # fluid masses + save_dic = {key: grav_struct[v][key] - grav_base[key] for key in grav_struct[v].keys()} + if save_folder is not None: + file_name = save_folder + os.sep + f"fluid_mass_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"fluid_mass_vint{v}.npz" + else: + if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ + (self.ensemble_member >= 0): + file_name = folder + os.sep + f"fluid_mass_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"fluid_mass_vint{v}.npz" + else: + file_name = os.getcwd() + os.sep + f"fluid_mass_vint{v}.npz" + np.savez(file_name, **save_dic) + + # 4D response self.grav_result = [] for i, elem in enumerate(vintage): @@ -1359,6 +1429,7 @@ def calc_grav(self, grid, grav_base, grav_repeat): # assumes the length of each vector gives the total number of measurement points N_meas = (len(pos['x'])) dg = np.zeros(N_meas) # 1D array for dg + dg[:] = np.nan # total fluid mass at this time phases = self.ecl_case.init.phases @@ -1375,7 +1446,7 @@ def calc_grav(self, grid, grav_base, grav_repeat): for j in range(N_meas): # Calculate dg for the current measurement location (j, i) - dg_tmp = (z - pos['z'][j]) / ((x[j] - pos['x'][j]) ** 2 + (y[j] - pos['y'][j]) ** 2 + ( + dg_tmp = (z - pos['z'][j]) / ((x - pos['x'][j]) ** 2 + (y - pos['y'][j]) ** 2 + ( z - pos['z'][j]) ** 2) ** (3 / 2) dg[j] = np.dot(dg_tmp, dm) @@ -1397,9 +1468,9 @@ def measurement_locations(self, grid): ymax = np.max(cell_centre[1]) # Make a mesh of the area - pad = self.grav_config.get('padding_reservoir', 3000) # 3 km padding around the reservoir + pad = self.grav_config.get('padding_reservoir', 1500) # 3 km padding around the reservoir if 'padding_reservoir' not in self.grav_config: - print('Please specify extent of measurement locations (Padding in pipt file), using 3 km as default') + print('Please specify extent of measurement locations (Padding in pipt file), using 1.5 km as default') xmin -= pad xmax += pad @@ -1425,8 +1496,11 @@ def measurement_locations(self, grid): # Handle seabed map or water depth scalar if defined in pipt if 'seabed' in self.grav_config and self.grav_config['seabed'] is not None: - pos['z'] = griddata((self.grav_config['seabed']['x'], self.grav_config['seabed']['y']), - self.grav_config['seabed']['z'], (pos['x'], pos['y']), method='nearest') + # read seabed depths from file + water_depths = self.get_seabed_depths() + # get water depths at measurement locations + pos['z'] = griddata((water_depths['x'], water_depths['y']), + np.abs(water_depths['z']), (pos['x'], pos['y']), method='nearest') # z is positive downwards else: pos['z'] = np.ones_like(pos['x']) * self.grav_config.get('water_depth', 300) @@ -1436,12 +1510,26 @@ def measurement_locations(self, grid): #return pos self.grav_config['meas_location'] = pos + def get_seabed_depths(self): + # Path to your CSV file + file_path = self.grav_config['seabed'] # Replace with your actual file path + + # Read the data while skipping the header comments + # We'll assume the header data ends before the numerical data + # The 'delim_whitespace' keyword in pd.read_csv is deprecated and will be removed in a future version. Use ``sep='\s+'`` instead + water_depths = pd.read_csv(file_path, comment='#', delim_whitespace=True, header=None) + + # Give meaningful column names: + water_depths.columns = ['x', 'y', 'z', 'column', 'row'] + + return water_depths + def find_cell_centers(self, grid): # Find indices where the boolean array is True - #indices = np.where(grid['ACTNUM']) - actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) - indices = np.where(actnum) + indices = np.where(grid['ACTNUM']) + #actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) + #indices = np.where(actnum) # `indices` will be a tuple of arrays: (x_indices, y_indices, z_indices) #nactive = len(actind) # Number of active cells @@ -1452,7 +1540,8 @@ def find_cell_centers(self, grid): #N1, N2, N3 = grid['DIMENS'] - b, a, c = indices + #b, a, c = indices + c, a, b = indices # Calculate xt, yt, zt xb = 0.25 * (coord[a, b, 0, 0] + coord[a, b + 1, 0, 0] + coord[a + 1, b, 0, 0] + coord[a + 1, b + 1, 0, 0]) yb = 0.25 * (coord[a, b, 0, 1] + coord[a, b + 1, 0, 1] + coord[a + 1, b, 0, 1] + coord[a + 1, b + 1, 0, 1]) @@ -1478,7 +1567,7 @@ def _get_grav_info(self, grav_config=None): GRAV configuration """ # list of configuration parameters in the "Grav" section of teh pipt file - config_para_list = ['baseline', 'vintage', 'water_depth', 'padding', 'grid_spacing'] + config_para_list = ['baseline', 'vintage', 'water_depth', 'padding', 'grid_spacing', 'seabed'] if 'grav' in self.input_dict: self.grav_config = {} @@ -1583,7 +1672,7 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): folder = self.folder # run flow simulator - #success = True #super(flow_rock, self).call_sim(folder, True) + #success = True success = super(flow_rock, self).call_sim(folder, True) # use output from flow simulator to forward model gravity response diff --git a/simulator/rockphysics/softsandrp.py b/simulator/rockphysics/softsandrp.py index 974e2e5..762209d 100644 --- a/simulator/rockphysics/softsandrp.py +++ b/simulator/rockphysics/softsandrp.py @@ -102,7 +102,7 @@ def setup_fwd_run(self, state): def calc_props(self, phases, saturations, pressure, porosity, dens = None, wait_for_proc=None, ntg=None, Rs=None, press_init=None, ensembleMember=None): ### - # TODO add fluid densities here -needs to be added as optional input + # if not isinstance(phases, list): phases = [phases] @@ -118,6 +118,12 @@ def calc_props(self, phases, saturations, pressure, # Load "overburden" pressures into local variable to # comply with remaining code parts poverburden = self.overburden + + # debug + self.pressure = pressure + self.peff = poverburden - pressure + self.porosity = porosity + if press_init is None: p_init = self.p_init else: @@ -172,21 +178,31 @@ def calc_props(self, phases, saturations, pressure, if p_init is None: p_init = [None for _ in range(len(saturations[:, 0]))] + + if dens is not None: + assert (len(dens) == len(phases)) + # Transpose makes it a no. grid cells x phases array + dens = np.array(dens).T + + # + denss, bulks, shears = self._solidprops_Johansen() + for i in range(len(saturations[:, 0])): # # Calculate fluid properties # - # set Rs if needed - densf, bulkf = \ - self._fluidprops(self.phases, - saturations[i, :], pressure[i], Rs[i]) + if dens is None: + densf, bulkf = \ + self._fluidprops_Wood(self.phases, + saturations[i, :], pressure[i], Rs[i]) + else: + densf = self._fluid_dens(saturations[i, :], dens[i, :]) + + bulkf = self._fluidprops_Brie(self.phases, saturations[i, :], pressure[i]) # #denss, bulks, shears = \ # self._solidprops(porosity[i], ntg[i], i) - # - denss, bulks, shears = \ - self._solidprops_Johansen() # # Calculate dry rock moduli # @@ -242,6 +258,8 @@ def calc_props(self, phases, saturations, pressure, self.shearimp[i] = self.dens[i] * \ self.shearvel[i] + + def getMatchProp(self, petElProp): if petElProp.lower() == 'density': self.match_prop = self.getDens() @@ -291,12 +309,23 @@ def getBulkImp(self): def getShearImp(self): return self.shearimp + def getOverburdenP(self): + return self.overburden + + def getPressure(self): + return self.pressure + + def getPeff(self): + return self.peff + + def getPorosity(self): + return self.porosity # # =================================================== # Fluid properties start # =================================================== # - def _fluidprops(self, fphases, fsats, fpress, Rs=None): + def _fluidprops_Wood(self, fphases, fsats, fpress, Rs=None): # # Calculate fluid density and bulk modulus # @@ -339,6 +368,104 @@ def _fluidprops(self, fphases, fsats, fpress, Rs=None): # --------------------------------------------------- # + def _fluid_dens(self, fsatsp, fdensp): + fdens = sum(fsatsp * fdensp) + return fdens + + def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): + # + # Calculate fluid density and bulk modulus BRIE et al. 1995 + # Assumes two phases liquid and gas + # + # Input + # fphases - fluid phases present; Oil + # and/or Water and/or Gas + # fsats - fluid saturation values for + # fluid phases in "fphases" + # fpress - fluid pressure value (MPa) + # Rs - Gas oil ratio. Default value None + # e - Brie's exponent (e= 5 Utsira sand filled with brine and CO2 + # Figure 7 in Carcione et al. 2006 "Physics and Seismic Modeling + # for Monitoring CO 2 Storage" + + # + # Output + # fbulk - bulk modulus of fluid mixture for + # pressure value "fpress" (unit + # inherited from phaseprops) + # + # ----------------------------------------------- + # + + + for i in range(len(fphases)): + # + if fphases[i].lower() in ["oil", "wat"]: + fsatsl = fsats[i] + pbulkl = self._phaseprops_Smeaheia(fphases[i], fpress, Rs) + elif fphases[i].lower() in ["gas"]: + pbulkg = self._phaseprops_Smeaheia(fphases[i], fpress, Rs) + + + fbulk = (pbulkl - pbulkg) * (fsatsl)**e + pbulkg + + # + return fbulk + # + # --------------------------------------------------- + # + def _phaseprops_Smeaheia(self, fphase, press, Rs=None): + # + # Calculate properties for a single fluid phase + # + # + # Input + # fphase - fluid phase; Oil, Water or Gas + # press - fluid pressure value (MPa) + # + # Output + # pbulk - bulk modulus of fluid phase + # "fphase" for pressure value + # "press" (MPa) + # + # ----------------------------------------------- + # + if fphase.lower() == "oil": # refers to water in Smeaheia + press_range = np.array([0.10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) + # Bo values assume Rs = 0 + Bo_values = np.array( + [1.00469, 1.00430, 1.00387, 1.00345, 1.00302, 1.00260, 1.00218, 1.00176, 1.00134, 1.00092, 1.00050, + 1.00008, 0.99967, 0.99925, 0.99884, 0.99843, 0.99802, 0.99761, 0.99720, 0.99679, 0.99638]) + + + elif fphase.lower() == "gas": + # Values from .DATA file for Smeaheia (converted to MPa) + press_range = np.array( + [0.101, 0.885, 1.669, 2.453, 3.238, 4.022, 4.806, 5.590, 6.2098, 7.0899, 7.6765, 8.2630, 8.8495, 9.4359, + 10.0222, 10.6084, 11.1945, 14.7087, 17.6334, 20.856, 23.4695, 27.5419]) # Example pressures in MPa + Bo_values = np.array( + [1.07365, 0.11758, 0.05962, 0.03863, 0.02773, 0.02100, 0.01639, 0.01298, 0.010286, 0.007578, 0.005521, + 0.003314, 0.003034, 0.002919, 0.002851, 0.002802, 0.002766, 0.002648, 0.002599, 0.002566, 0.002546, + 0.002525]) # Example formation volume factors in m^3/kg + + # Calculate numerical derivative of Bo with respect to Pressure + dBo_dP = - np.gradient(Bo_values, press_range) + # Calculate isothermal compressibility (van der Waals) + compressibility = (1 / Bo_values) * dBo_dP # Resulting array of compressibility values + bulk_mod = 1 / compressibility + + # + # Find the index of the closest pressure value in b + closest_index = (np.abs(press_range - press)).argmin() + + # Extract the corresponding value from a + pbulk = bulk_mod[closest_index] + + # + return pbulk + + # + def _phaseprops(self, fphase, press, Rs=None): # # Calculate properties for a single fluid phase From 8ba330849617771d285c03b0f96cef7874f0054e Mon Sep 17 00:00:00 2001 From: mlienorce Date: Fri, 21 Feb 2025 15:29:35 +0100 Subject: [PATCH 06/15] Add functionality for updating pressure and saturations directly (#80) --- simulator/flow_rock.py | 137 ++++++++++++---------------- simulator/rockphysics/standardrp.py | 19 +++- 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 64d64c8..0d80fc1 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -57,6 +57,10 @@ def __init__(self, input_dict=None, filename=None, options=None): self.scale = [] + # Store dynamic variables in case they are provided in the state + self.state = None + self.no_flow = False + def _getpeminfo(self, input_dict): """ Get, and return, flow and PEM modules @@ -86,6 +90,8 @@ def _getpeminfo(self, input_dict): self.pem_input['overburden'] = elem[1] if elem[0] == 'percentile': # use for scaling self.pem_input['percentile'] = elem[1] + if elem[0] == 'phases': # get the fluid phases + self.pem_input['phases'] = elem[1] pem = getattr(import_module('simulator.rockphysics.' + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) @@ -95,16 +101,29 @@ def _getpeminfo(self, input_dict): else: self.pem = None + def _get_pem_input(self, type, time=None): + if self.no_flow: # get variable from state + if type in self.state.keys(): + return self.state[type+'_'+str(time)] + else: # read parameter from file + param_file = self.input_dict['param_file'] + npzfile = np.load(param_file) + parameter = npzfile[type] + npzfile.close() + return parameter + else: # get variable of parameter from flow simulation + return self.ecl_case.cell_data(type,time) + def calc_pem(self, time): - # fluid phases written to restart file from simulator run - phases = self.ecl_case.init.phases + + # fluid phases written given as input + phases = self.pem_input['phases'] pem_input = {} # get active porosity - tmp = self.ecl_case.cell_data('PORO') + tmp = self._get_pem_input('PORO') # self.ecl_case.cell_data('PORO') if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - + multfactor = self._get_pem_input('PORV_RC', time) pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) else: pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) @@ -114,29 +133,26 @@ def calc_pem(self, time): if self.pem_input['ntg'] == 'no': pem_input['NTG'] = None else: - tmp = self.ecl_case.cell_data('NTG') + tmp = self._get_pem_input('NTG') pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) else: - tmp = self.ecl_case.cell_data('NTG') + tmp = self._get_pem_input('NTG') pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - if 'RS' in self.pem_input: #ecl_case.cell_data: # to be more robust! - tmp = self.ecl_case.cell_data('RS', time) + tmp = self._get_pem_input('RS', time) pem_input['RS'] = np.array(tmp[~tmp.mask], dtype=float) else: pem_input['RS'] = None print('RS is not a variable in the ecl_case') # extract pressure - tmp = self.ecl_case.cell_data('PRESSURE', time) + tmp = self._get_pem_input('PRESSURE', time) pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) if 'press_conv' in self.pem_input: pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] - if hasattr(self.pem, 'p_init'): P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] else: @@ -149,7 +165,7 @@ def calc_pem(self, time): if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended for var in phases: if var in ['WAT', 'GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] @@ -157,7 +173,7 @@ def calc_pem(self, time): elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model for var in phases: if var in ['GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) saturations = [1 - (pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] else: @@ -176,7 +192,7 @@ def calc_pem(self, time): # fluid densities dens = var + '_DEN' if dens in keywords: - tmp = self.ecl_case.cell_data(dens, time) + tmp = self._get_pem_input(dens, time) pem_input[dens] = np.array(tmp[~tmp.mask], dtype=float) # extract densities densities.append(pem_input[dens]) @@ -184,7 +200,7 @@ def calc_pem(self, time): densities = None # pore volumes at each assimilation step if 'RPORV' in keywords: - tmp = self.ecl_case.cell_data('RPORV', time) + tmp = self._get_pem_input('RPORV', time) pem_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) # Get elastic parameters @@ -203,6 +219,14 @@ def setup_fwd_run(self, redund_sim): def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i + + # Check if dynamic variables are provided in the state. If that is the case, do not run flow simulator + if 'SATURATION' in state.keys() and 'PRESSURE' in state.keys(): + self.state = {} + for key in state.keys(): + self.state[key] = state[key][:,member_i] + self.no_flow = True + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) return self.pred_data @@ -210,7 +234,10 @@ def run_fwd_sim(self, state, member_i, del_folder=True): def call_sim(self, folder=None, wait_for_proc=False): # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) + if not self.no_flow: + success = super().call_sim(folder, wait_for_proc) + else: + success = True if success: self.ecl_case = ecl.EclipseCase( @@ -220,8 +247,11 @@ def call_sim(self, folder=None, wait_for_proc=False): vintage = [] # loop over seismic vintages for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) + if not self.no_flow: + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + else: + time = int(v + 1) self.calc_pem(time) @@ -235,8 +265,11 @@ def call_sim(self, folder=None, wait_for_proc=False): vintage.append(deepcopy(self.pem.bulkimp)) if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + if not self.no_flow: + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + else: + v = 0 # self.calc_pem(base_time) @@ -867,9 +900,9 @@ def calc_velocities(self, folder, save_folder, grid, v, f_dim): self.rho[true_indices] = (self.pem.getDens()) else: - self.vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ), order='F') - self.vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ), order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 + self.vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') + self.vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') + self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ))#, order='F') ## Debug self.bulkmod = np.full(f_dim, np.nan) @@ -1069,7 +1102,7 @@ def _calc_avo_props(self, dt=0.0005): self.avo_data = avo_data - def _calc_avo_props_active_cells(self, grid, dt=0.005): + def _calc_avo_props_active_cells(self, grid, dt=0.0005): # dt is the fine resolution sampling rate # convert properties in reservoir model to time domain vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) @@ -1610,60 +1643,10 @@ def setup_fwd_run(self, **kwargs): def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. - #self.ensemble_member = member_i - #self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - #return self.pred_data - - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False + return self.pred_data def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. diff --git a/simulator/rockphysics/standardrp.py b/simulator/rockphysics/standardrp.py index 316fd04..7aa4dce 100644 --- a/simulator/rockphysics/standardrp.py +++ b/simulator/rockphysics/standardrp.py @@ -90,7 +90,7 @@ def setup_fwd_run(self, state): pass def calc_props(self, phases, saturations, pressure, - porosity, wait_for_proc=None, ntg=None, Rs=None, press_init=None, ensembleMember=None): + porosity, dens = None, wait_for_proc=None, ntg=None, Rs=None, press_init=None, ensembleMember=None): ### # This doesn't initialize for models with no uncertainty ### @@ -141,6 +141,13 @@ def calc_props(self, phases, saturations, pressure, else: p_init = press_init # + poverburden = self.overburden + + # debug + self.pressure = pressure + self.peff = poverburden - pressure + self.porosity = porosity + # Check that no. of phases is equal to no. of # entries in saturations list # @@ -287,7 +294,17 @@ def getBulkImp(self): def getShearImp(self): return self.shearimp + def getOverburdenP(self): + return self.overburden + + def getPressure(self): + return self.pressure + + def getPeff(self): + return self.peff + def getPorosity(self): + return self.porosity # # =================================================== # Fluid properties start From 3a0d70dcdf393fbce5b53ea47c084b20c686e1af Mon Sep 17 00:00:00 2001 From: mlienorce Date: Fri, 28 Mar 2025 14:57:30 +0100 Subject: [PATCH 07/15] invert for dynamic variables first iteration (#94) --- simulator/flow_rock.py | 247 +++++++++++++--------------- simulator/rockphysics/softsandrp.py | 2 +- simulator/rockphysics/standardrp.py | 1 + 3 files changed, 117 insertions(+), 133 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 0d80fc1..8fe5d13 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -1,4 +1,6 @@ """Descriptive description.""" +from selectors import SelectSelector + from simulator.opm import flow from importlib import import_module import datetime as dt @@ -92,6 +94,11 @@ def _getpeminfo(self, input_dict): self.pem_input['percentile'] = elem[1] if elem[0] == 'phases': # get the fluid phases self.pem_input['phases'] = elem[1] + if elem[0] == 'grid': # get the model grid + self.pem_input['grid'] = elem[1] + if elem[0] == 'param_file': # get model parameters required for pem + self.pem_input['param_file'] = elem[1] + pem = getattr(import_module('simulator.rockphysics.' + self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) @@ -103,27 +110,39 @@ def _getpeminfo(self, input_dict): def _get_pem_input(self, type, time=None): if self.no_flow: # get variable from state - if type in self.state.keys(): - return self.state[type+'_'+str(time)] + if any(type.lower() in key for key in self.state.keys()) and time > 0: + data = self.state[type.lower()+'_'+str(time)] + mask = np.zeros(data.shape, dtype=bool) + return np.ma.array(data=data, dtype=data.dtype, + mask=mask) else: # read parameter from file - param_file = self.input_dict['param_file'] + param_file = self.pem_input['param_file'] npzfile = np.load(param_file) parameter = npzfile[type] npzfile.close() - return parameter + data = parameter[:,self.ensemble_member] + mask = np.zeros(data.shape, dtype=bool) + return np.ma.array(data=data, dtype=data.dtype, + mask=mask) else: # get variable of parameter from flow simulation return self.ecl_case.cell_data(type,time) - def calc_pem(self, time): + def calc_pem(self, time, time_index=None): + + if self.no_flow: + time_input = time_index + else: + time_input = time # fluid phases written given as input - phases = self.pem_input['phases'] + phases = str.upper(self.pem_input['phases']) + phases = phases.split() pem_input = {} # get active porosity tmp = self._get_pem_input('PORO') # self.ecl_case.cell_data('PORO') if 'compaction' in self.pem_input: - multfactor = self._get_pem_input('PORV_RC', time) + multfactor = self._get_pem_input('PORV_RC', time_input) pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) else: pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) @@ -140,14 +159,14 @@ def calc_pem(self, time): pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) if 'RS' in self.pem_input: #ecl_case.cell_data: # to be more robust! - tmp = self._get_pem_input('RS', time) + tmp = self._get_pem_input('RS', time_input) pem_input['RS'] = np.array(tmp[~tmp.mask], dtype=float) else: pem_input['RS'] = None print('RS is not a variable in the ecl_case') # extract pressure - tmp = self._get_pem_input('PRESSURE', time) + tmp = self._get_pem_input('PRESSURE', time_input) pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) if 'press_conv' in self.pem_input: @@ -165,43 +184,47 @@ def calc_pem(self, time): if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended for var in phases: if var in ['WAT', 'GAS']: - tmp = self._get_pem_input('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time_input) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model for var in phases: if var in ['GAS']: - tmp = self._get_pem_input('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time_input) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) - saturations = [1 - (pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] + saturations = [1 - (pem_input['SGAS']) if ph == 'WAT' else pem_input['S{}'.format(ph)] for ph in phases] else: print('Type and number of fluids are unspecified in calc_pem') # fluid saturations in dictionary - tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} - self.sats.extend([tmp_s]) + tmp_dyn_var = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + tmp_dyn_var['PRESSURE'] = pem_input['PRESSURE'] + self.dyn_var.extend([tmp_dyn_var]) - keywords = self.ecl_case.arrays(time) - keywords = [s.strip() for s in keywords] # Remove leading/trailing spaces - #for key in self.all_data_types: - #if 'grav' in key: - densities = [] - for var in phases: - # fluid densities - dens = var + '_DEN' - if dens in keywords: - tmp = self._get_pem_input(dens, time) - pem_input[dens] = np.array(tmp[~tmp.mask], dtype=float) - # extract densities - densities.append(pem_input[dens]) - else: - densities = None - # pore volumes at each assimilation step - if 'RPORV' in keywords: - tmp = self._get_pem_input('RPORV', time) - pem_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + if not self.no_flow: + keywords = self.ecl_case.arrays(time) + keywords = [s.strip() for s in keywords] # Remove leading/trailing spaces + #for key in self.all_data_types: + #if 'grav' in key: + densities = [] + for var in phases: + # fluid densities + dens = var + '_DEN' + if dens in keywords: + tmp = self._get_pem_input(dens, time_input) + pem_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + # extract densities + densities.append(pem_input[dens]) + else: + densities = None + # pore volumes at each assimilation step + if 'RPORV' in keywords: + tmp = self._get_pem_input('RPORV', time_input) + pem_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + else: + densities = None # Get elastic parameters if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ @@ -221,13 +244,14 @@ def run_fwd_sim(self, state, member_i, del_folder=True): self.ensemble_member = member_i # Check if dynamic variables are provided in the state. If that is the case, do not run flow simulator - if 'SATURATION' in state.keys() and 'PRESSURE' in state.keys(): + if any('sgas' in key for key in state.keys()) or any('swat' in key for key in state.keys()) or any('pressure' in key for key in state.keys()): self.state = {} for key in state.keys(): - self.state[key] = state[key][:,member_i] + self.state[key] = state[key] self.no_flow = True - - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + #self.pred_data = self.extract_data(member_i) + #else: + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=del_folder) return self.pred_data @@ -243,17 +267,14 @@ def call_sim(self, folder=None, wait_for_proc=False): self.ecl_case = ecl.EclipseCase( 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') phases = self.ecl_case.init.phases - self.sats = [] + self.dyn_var = [] vintage = [] # loop over seismic vintages for v, assim_time in enumerate(self.pem_input['vintage']): - if not self.no_flow: - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - else: - time = int(v + 1) + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) - self.calc_pem(time) + self.calc_pem(time, v+1) # mask the bulk imp. to get proper dimensions tmp_value = np.zeros(self.ecl_case.init.shape) @@ -265,13 +286,10 @@ def call_sim(self, folder=None, wait_for_proc=False): vintage.append(deepcopy(self.pem.bulkimp)) if hasattr(self.pem, 'baseline'): # 4D measurement - if not self.no_flow: - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - else: - v = 0 - # - self.calc_pem(base_time) + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.pem.baseline) + + self.calc_pem(base_time, 0) # mask the bulk imp. to get proper dimensions tmp_value = np.zeros(self.ecl_case.init.shape) @@ -402,7 +420,7 @@ def call_sim(self, folder=None, wait_for_proc=False): grid = self.ecl_case.grid() phases = self.ecl_case.init.phases - self.sats = [] + self.dyn_var = [] vintage = [] # loop over seismic vintages for v, assim_time in enumerate(self.pem_input['vintage']): @@ -718,76 +736,21 @@ def run_fwd_sim(self, state, member_i, del_folder=True): Boolean to determine if the ensemble folder should be deleted. Default is False. """ - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder + # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i - - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=del_folder) def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): # replace the sim2seis part (which is unusable) by avo based on Pylops if folder is None: folder = self.folder + else: + self.folder = folder - # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" - if hasattr(self, 'run_reservoir_model'): - run_reservoir_model = self.run_reservoir_model - - if run_reservoir_model is None: - run_reservoir_model = True - - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) + if not self.no_flow: + # call call_sim in flow class (skip flow_rock, go directly to flow which is a parent of flow_rock) success = super(flow_rock, self).call_sim(folder, wait_for_proc) - #ecl = ecl_100(filename=self.file) - #ecl.options = self.options - #success = ecl.call_sim(folder, wait_for_proc) else: success = True @@ -797,18 +760,27 @@ def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, s return success def get_avo_result(self, folder, save_folder): - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - grid = self.ecl_case.grid() - #ecl_init = ecl.EclipseInit(ecl_case) - f_dim = [self.ecl_case.init.nk, self.ecl_case.init.nj, self.ecl_case.init.ni] + + if self.no_flow: + grid_file = self.pem_input['grid'] + grid = np.load(grid_file) + else: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + + # ecl_init = ecl.EclipseInit(ecl_case) + # f_dim = [self.ecl_case.init.nk, self.ecl_case.init.nj, self.ecl_case.init.ni] + f_dim = [self.NZ, self.NY, self.NX] # phases = self.ecl_case.init.phases - self.sats = [] + self.dyn_var = [] if 'baseline' in self.pem_input: # 4D measurement base_time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + dt.timedelta(days=self.pem_input['baseline']) - self.calc_pem(base_time) + + self.calc_pem(base_time,0) # vp, vs, density in reservoir self.calc_velocities(folder, save_folder, grid, -1, f_dim) @@ -826,9 +798,10 @@ def get_avo_result(self, folder, save_folder): # loop over seismic vintages for v, assim_time in enumerate(self.pem_input['vintage']): time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) + dt.timedelta(days=assim_time) + # extract dynamic variables from simulation run - self.calc_pem(time) + self.calc_pem(time, v+1) # vp, vs, density in reservoir self.calc_velocities(folder, save_folder, grid, v, f_dim) @@ -1405,11 +1378,21 @@ def calc_mass(self, time): phases = self.ecl_case.init.phases grav_input = {} - # get active porosity - # pore volumes at each assimilation step - tmp = self.ecl_case.cell_data('RPORV', time) - grav_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + keywords = self.ecl_case.arrays(time) + keywords = [s.strip() for s in keywords] # Remove leading/trailing spaces + # for key in self.all_data_types: + # if 'grav' in key: + # pore volumes at each assimilation step + if 'RPORV' in keywords: + #tmp = self._get_pem_input('RPORV', time) + #grav_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + # get active porosity + # pore volumes at each assimilation step + tmp = self.ecl_case.cell_data('RPORV', time) + grav_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) + else: + print('Keyword RPORV missing from simulation output, need pdated porevolumes at each assimilation step') # extract saturation if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended for var in phases: @@ -1419,13 +1402,13 @@ def calc_mass(self, time): grav_input['SOIL'] = 1 - (grav_input['SWAT'] + grav_input['SGAS']) - elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model for var in phases: if var in ['GAS']: tmp = self.ecl_case.cell_data('S{}'.format(var), time) grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) - grav_input['SOIL'] = 1 - (grav_input['SGAS']) + grav_input['SWAT'] = 1 - (grav_input['SGAS']) else: print('Type and number of fluids are unspecified in calc_mass') @@ -1469,8 +1452,8 @@ def calc_grav(self, grid, grav_base, grav_repeat): if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: dm = grav_repeat['OIL_mass'] + grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['WAT_mass'] + grav_base['GAS_mass']) - elif 'OIL' in phases and 'GAS' in phases: # Smeaheia model - dm = grav_repeat['OIL_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['GAS_mass']) + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model + dm = grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['WAT_mass'] + grav_base['GAS_mass']) else: print('Type and number of fluids are unspecified in calc_grav') @@ -1655,7 +1638,7 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): folder = self.folder # run flow simulator - #success = True + # success = True success = super(flow_rock, self).call_sim(folder, True) # use output from flow simulator to forward model gravity response diff --git a/simulator/rockphysics/softsandrp.py b/simulator/rockphysics/softsandrp.py index 762209d..7b7f652 100644 --- a/simulator/rockphysics/softsandrp.py +++ b/simulator/rockphysics/softsandrp.py @@ -430,7 +430,7 @@ def _phaseprops_Smeaheia(self, fphase, press, Rs=None): # # ----------------------------------------------- # - if fphase.lower() == "oil": # refers to water in Smeaheia + if fphase.lower() == "wat": # refers to water in Smeaheia press_range = np.array([0.10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) # Bo values assume Rs = 0 Bo_values = np.array( diff --git a/simulator/rockphysics/standardrp.py b/simulator/rockphysics/standardrp.py index 7aa4dce..e05afd3 100644 --- a/simulator/rockphysics/standardrp.py +++ b/simulator/rockphysics/standardrp.py @@ -294,6 +294,7 @@ def getBulkImp(self): def getShearImp(self): return self.shearimp + def getOverburdenP(self): return self.overburden From 3c4d0c95e4bd57541e619f08f0500c06e20dd9e6 Mon Sep 17 00:00:00 2001 From: Kjersti <48013553+kjei@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:30:58 +0200 Subject: [PATCH 08/15] NE in pipt file is now used instead of the whole loaded ensemble if this number is smaller+bug fix (#95) --- ensemble/ensemble.py | 20 +++++++++++++++----- simulator/flow_rock.py | 11 +---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/ensemble/ensemble.py b/ensemble/ensemble.py index e71cc84..130b96a 100644 --- a/ensemble/ensemble.py +++ b/ensemble/ensemble.py @@ -134,14 +134,24 @@ def __init__(self, keys_en, sim, redund_sim=None): # individually). self.state = {key: val for key, val in tmp_load.items()} - # Find the number of ensemble members from state variable + # Find the number of ensemble members from loaded state variables tmp_ne = [] for tmp_state in self.state.keys(): tmp_ne.extend([self.state[tmp_state].shape[1]]) - if max(tmp_ne) != min(tmp_ne): - print('\033[1;33mInput states have different ensemble size\033[1;m') - sys.exit(1) - self.ne = min(tmp_ne) + + if 'ne' not in self.keys_en: # NE not specified in input file + if max(tmp_ne) != min(tmp_ne): #Check loaded ensembles are the same size (if more than one state variable) + print('\033[1;33mInput states have different ensemble size\033[1;m') + sys.exit(1) + self.ne = min(tmp_ne) # Use the number of ensemble members in loaded ensemble + else: + # Use the number of ensemble members specified in input file (may be fewer than loaded) + self.ne = int(self.keys_en['ne']) + if self.ne < min(tmp_ne): + # pick correct number of ensemble members + self.state = {key: val[:,:self.ne] for key, val in self.state.items()} + else: + print('\033[1;33mInput states are smaller than NE\033[1;m') self._ext_ml_info() def _ext_ml_info(self): diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 8fe5d13..e55b37d 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -22,7 +22,6 @@ from pylops.utils.wavelets import ricker from pylops.signalprocessing import Convolve1D import sys -sys.path.append("/home/AD.NORCERESEARCH.NO/mlie/") from PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master from scipy.interpolate import interp1d from scipy.interpolate import griddata @@ -41,8 +40,6 @@ def __init__(self, input_dict=None, filename=None, options=None): super().__init__(input_dict, filename, options) self._getpeminfo(input_dict) - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) self.date_slack = None if 'date_slack' in input_dict: self.date_slack = int(input_dict['date_slack']) @@ -738,7 +735,7 @@ def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=del_folder) + return super().run_fwd_sim(state, member_i, del_folder=del_folder) def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): # replace the sim2seis part (which is unusable) by avo based on Pylops @@ -855,11 +852,8 @@ def calc_velocities(self, folder, save_folder, grid, v, f_dim): # The properties in pem are only given in the active cells # indices of active cells: - true_indices = np.where(grid['ACTNUM']) - - # Alt 2 if len(self.pem.getBulkVel()) == len(true_indices[0]): #self.vp = np.full(f_dim, self.avo_config['vp_shale']) @@ -1037,7 +1031,6 @@ def _calc_avo_props(self, dt=0.0005): f0=self.avo_config['frequency']) - # Travel time corresponds to reflectivity series t = time_sample[:, :, 0:-1] @@ -1096,8 +1089,6 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer top_res = 2 * self.TOPS[:, :, 0] / vp_shale - - # Cumulative traveling time through the reservoir in vertical direction cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] From 80392463164064952ff82df237e43cd33194ae03 Mon Sep 17 00:00:00 2001 From: mlie Date: Tue, 20 May 2025 15:42:26 +0200 Subject: [PATCH 09/15] subsidence --- ensemble/ensemble.py | 8 +- simulator/flow_rock.py | 1262 +++++++++++++++++++++++---- simulator/flow_rock_backup.py | 1120 ------------------------ simulator/flow_rock_mali.py | 1130 ------------------------ simulator/rockphysics/softsandrp.py | 195 ++++- simulator/rockphysics/standardrp.py | 3 +- 6 files changed, 1243 insertions(+), 2475 deletions(-) delete mode 100644 simulator/flow_rock_backup.py delete mode 100644 simulator/flow_rock_mali.py diff --git a/ensemble/ensemble.py b/ensemble/ensemble.py index 130b96a..66bd3ce 100644 --- a/ensemble/ensemble.py +++ b/ensemble/ensemble.py @@ -376,16 +376,16 @@ def _ext_prior_info(self): if isinstance(limits[0], list) and len(limits) < nz or \ not isinstance(limits[0], list) and len(limits) < 2 * nz: # Check if it is more than one entry and give error - assert (isinstance(limits[0], list) and len(limits) == 1), \ - 'Information from LIMITS has been given for {0} layers, whereas {1} is needed!' \ - .format(len(limits), nz) + #assert (isinstance(limits[0], list) and len(limits) == 1), \ + # 'Information from LIMITS has been given for {0} layers, whereas {1} is needed!' \ + # .format(len(limits), nz) assert (not isinstance(limits[0], list) and len(limits) == 2), \ 'Information from LIMITS has been given for {0} layers, whereas {1} is needed!' \ .format(len(limits) / 2, nz) # Only 1 entry; copy this to all layers print( - '\033[1;33mSingle entry for RANGE will be copied to all {0} layers\033[1;m'.format(nz)) + '\033[1;33mSingle entry for LIMITS will be copied to all {0} layers\033[1;m'.format(nz)) self.prior_info[name]['limits'] = [limits] * nz else: # 2D grid only, or optimization case diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index e55b37d..9bec3ad 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -15,6 +15,10 @@ from copy import deepcopy from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler +from scipy.optimize import fsolve +from scipy.special import jv # Bessel function of the first kind +from scipy.integrate import quad +from scipy.special import j0 from mako.lookup import TemplateLookup from mako.runtime import Context @@ -28,7 +32,7 @@ from pipt.misc_tools.analysis_tools import store_ensemble_sim_information from geostat.decomp import Cholesky from simulator.eclipse import ecl_100 - +from CoolProp.CoolProp import PropsSI # http://coolprop.org/#high-level-interface-example class flow_rock(flow): """ @@ -136,6 +140,7 @@ def calc_pem(self, time, time_index=None): phases = phases.split() pem_input = {} + tmp_dyn_var = {} # get active porosity tmp = self._get_pem_input('PORO') # self.ecl_case.cell_data('PORO') if 'compaction' in self.pem_input: @@ -166,7 +171,8 @@ def calc_pem(self, time, time_index=None): tmp = self._get_pem_input('PRESSURE', time_input) pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) - if 'press_conv' in self.pem_input: + # convert pressure from Bar to MPa + if 'press_conv' in self.pem_input and time_input == time: pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] if hasattr(self.pem, 'p_init'): @@ -174,7 +180,7 @@ def calc_pem(self, time, time_index=None): else: P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first - if 'press_conv' in self.pem_input: + if 'press_conv' in self.pem_input and time_input == time: P_init = P_init * self.pem_input['press_conv'] # extract saturations @@ -183,20 +189,38 @@ def calc_pem(self, time, time_index=None): if var in ['WAT', 'GAS']: tmp = self._get_pem_input('S{}'.format(var), time_input) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + pem_input['S{}'.format(var)] = np.clip(pem_input['S{}'.format(var)], 0, 1) - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + pem_input['SOIL'] = np.clip(1 - (pem_input['SWAT'] + pem_input['SGAS']), 0, 1) + saturations = [ np.clip(1 - (pem_input['SWAT'] + pem_input['SGAS']), 0, 1) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model using OPM CO2Store for var in phases: if var in ['GAS']: tmp = self._get_pem_input('S{}'.format(var), time_input) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + pem_input['S{}'.format(var)] = np.clip(pem_input['S{}'.format(var)] , 0, 1) + pem_input['SWAT'] = 1 - pem_input['SGAS'] saturations = [1 - (pem_input['SGAS']) if ph == 'WAT' else pem_input['S{}'.format(ph)] for ph in phases] + + elif 'OIL' in phases and 'GAS' in phases: # Original Smeaheia model + for var in phases: + if var in ['GAS']: + tmp = self._get_pem_input('S{}'.format(var), time_input) + pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + pem_input['S{}'.format(var)] = np.clip(pem_input['S{}'.format(var)], 0, 1) + pem_input['SOIL'] = 1 - pem_input['SGAS'] + saturations = [1 - (pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] + else: + print('Type and number of fluids are unspecified in calc_pem') # fluid saturations in dictionary - tmp_dyn_var = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + # tmp_dyn_var = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + for var in phases: + tmp_dyn_var[f'S{var}'] = pem_input[f'S{var}'] + tmp_dyn_var['PRESSURE'] = pem_input['PRESSURE'] self.dyn_var.extend([tmp_dyn_var]) @@ -735,8 +759,14 @@ def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i +<<<<<<< Updated upstream return super().run_fwd_sim(state, member_i, del_folder=del_folder) +======= + #return super().run_fwd_sim(state, member_i, del_folder=del_folder) + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=del_folder) + return self.pred_data +>>>>>>> Stashed changes def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): # replace the sim2seis part (which is unusable) by avo based on Pylops @@ -748,6 +778,7 @@ def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, s if not self.no_flow: # call call_sim in flow class (skip flow_rock, go directly to flow which is a parent of flow_rock) success = super(flow_rock, self).call_sim(folder, wait_for_proc) + #success = True else: success = True @@ -777,19 +808,32 @@ def get_avo_result(self, folder, save_folder): base_time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + dt.timedelta(days=self.pem_input['baseline']) + self.calc_pem(base_time,0) - # vp, vs, density in reservoir - self.calc_velocities(folder, save_folder, grid, -1, f_dim) - # avo data - # self._calc_avo_props() - self._calc_avo_props_active_cells(grid) + if not self.no_flow: + # vp, vs, density in reservoir + vp, vs, rho = self.calc_velocities(folder, save_folder, grid, 0, f_dim) - avo_baseline = self.avo_data.flatten(order="F") - Rpp_baseline = self.Rpp - vs_baseline = self.vs_sample - vp_baseline = self.vp_sample - rho_baseline = self.rho_sample + # avo data + # self._calc_avo_props() + avo_data, Rpp, vp_sample, vs_sample, rho_sample = self._calc_avo_props_active_cells(grid, vp, vs, rho) + + avo_baseline = avo_data.flatten(order="F") + Rpp_baseline = Rpp + vs_baseline = vs_sample + vp_baseline = vp_sample + rho_baseline = rho_sample + print('OPM flow is used') + else: + file_name = f"avo_vint0_{folder}.npz" if folder[-1] != os.sep \ + else f"avo_vint0_{folder[:-1]}.npz" + + avo_baseline = np.load(file_name, allow_pickle=True)['avo_bl'] + Rpp_baseline = np.load(file_name, allow_pickle=True)['Rpp_bl'] + vs_baseline = np.load(file_name, allow_pickle=True)['vs_bl'] + vp_baseline = np.load(file_name, allow_pickle=True)['vp_bl'] + rho_baseline = np.load(file_name, allow_pickle=True)['rho_bl'] vintage = [] # loop over seismic vintages @@ -801,26 +845,28 @@ def get_avo_result(self, folder, save_folder): self.calc_pem(time, v+1) # vp, vs, density in reservoir - self.calc_velocities(folder, save_folder, grid, v, f_dim) + vp, vs, rho = self.calc_velocities(folder, save_folder, grid, v+1, f_dim) # avo data #self._calc_avo_props() - self._calc_avo_props_active_cells(grid) + avo_data, Rpp, vp_sample, vs_sample, rho_sample = self._calc_avo_props_active_cells(grid, vp, vs, rho) - avo = self.avo_data.flatten(order="F") + avo = avo_data.flatten(order="F") # MLIE: implement 4D avo if 'baseline' in self.pem_input: # 4D measurement avo = avo - avo_baseline - Rpp = self.Rpp - Rpp_baseline - Vs = self.vs_sample - vs_baseline - Vp = self.vp_sample - vp_baseline - rho = self.rho_sample - rho_baseline - else: - Rpp = self.Rpp - Vs = self.vs_sample - Vp = self.vp_sample - rho = self.rho_sample + #Rpp = self.Rpp - Rpp_baseline + #Vs = self.vs_sample - vs_baseline + #Vp = self.vp_sample - vp_baseline + #rho = self.rho_sample - rho_baseline + print('Time-lapse avo') + #else: + # Rpp = self.Rpp + # Vs = self.vs_sample + # Vp = self.vp_sample + # rho = self.rho_sample + # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies @@ -834,82 +880,103 @@ def get_avo_result(self, folder, save_folder): else: noise_std = 0.0 # simulated data don't contain noise + vintage.append(deepcopy(avo)) + + #save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': Vs, 'Vp': Vp, 'rho': rho, **self.avo_config} + save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, 'rho': rho_sample, #**self.avo_config, + 'vs_bl': vs_baseline, 'vp_bl': vp_baseline, 'avo_bl': avo_baseline, 'Rpp_bl': Rpp_baseline, 'rho_bl': rho_baseline, **self.avo_config} if save_folder is not None: file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"avo_vint{v}.npz" + #np.savez(file_name, **save_dic) else: file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ else folder + f"avo_vint{v}.npz" - + file_name_rec = 'Ensemble_results/' + f"avo_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"avo_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) # with open(file_name, "wb") as f: # dump(**save_dic, f) np.savez(file_name, **save_dic) + # 4D response + self.avo_result = [] + for i, elem in enumerate(vintage): + self.avo_result.append(elem) + def calc_velocities(self, folder, save_folder, grid, v, f_dim): # The properties in pem are only given in the active cells # indices of active cells: +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes true_indices = np.where(grid['ACTNUM']) # Alt 2 if len(self.pem.getBulkVel()) == len(true_indices[0]): #self.vp = np.full(f_dim, self.avo_config['vp_shale']) - self.vp = np.full(f_dim, np.nan) - self.vp[true_indices] = (self.pem.getBulkVel()) + vp = np.full(f_dim, np.nan) + vp[true_indices] = (self.pem.getBulkVel()) #self.vs = np.full(f_dim, self.avo_config['vs_shale']) - self.vs = np.full(f_dim, np.nan) - self.vs[true_indices] = (self.pem.getShearVel()) + vs = np.full(f_dim, np.nan) + vs[true_indices] = (self.pem.getShearVel()) #self.rho = np.full(f_dim, self.avo_config['den_shale']) - self.rho = np.full(f_dim, np.nan) - self.rho[true_indices] = (self.pem.getDens()) + rho = np.full(f_dim, np.nan) + rho[true_indices] = (self.pem.getDens()) else: - self.vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') - self.vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ))#, order='F') + # option not used for Box or smeaheia--needs to be tested + vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') + vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') + rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ))#, order='F') ## Debug - self.bulkmod = np.full(f_dim, np.nan) - self.bulkmod[true_indices] = self.pem.getBulkMod() - self.shearmod = np.full(f_dim, np.nan) - self.shearmod[true_indices] = self.pem.getShearMod() - self.poverburden = np.full(f_dim, np.nan) - self.poverburden[true_indices] = self.pem.getOverburdenP() - self.pressure = np.full(f_dim, np.nan) - self.pressure[true_indices] = self.pem.getPressure() - self.peff = np.full(f_dim, np.nan) - self.peff[true_indices] = self.pem.getPeff() - self.porosity = np.full(f_dim, np.nan) - self.porosity[true_indices] = self.pem.getPorosity() - + #self.bulkmod = np.full(f_dim, np.nan) + #self.bulkmod[true_indices] = self.pem.getBulkMod() + #self.shearmod = np.full(f_dim, np.nan) + #self.shearmod[true_indices] = self.pem.getShearMod() + #self.poverburden = np.full(f_dim, np.nan) + #self.poverburden[true_indices] = self.pem.getOverburdenP() + #self.pressure = np.full(f_dim, np.nan) + #self.pressure[true_indices] = self.pem.getPressure() + #self.peff = np.full(f_dim, np.nan) + #self.peff[true_indices] = self.pem.getPeff() + porosity = np.full(f_dim, np.nan) + porosity[true_indices] = self.pem.getPorosity() + if self.dyn_var: + sgas = np.full(f_dim, np.nan) + sgas[true_indices] = self.dyn_var[v]['SGAS'] + #soil = np.full(f_dim, np.nan) + #soil[true_indices] = self.dyn_var[v]['SOIL'] + pdyn = np.full(f_dim, np.nan) + pdyn[true_indices] = self.dyn_var[v]['PRESSURE'] # + if self.dyn_var is None: + save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, + #'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': porosity} # for debugging + else: + save_dic = {'vp': vp, 'vs': vs, 'rho': rho, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.rho, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, 'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': self.porosity} if save_folder is not None: file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"vp_vs_rho_vint{v}.npz" + np.savez(file_name, **save_dic) else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"vp_vs_rho_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" - - # with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - + file_name_rec = 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) + # for debugging + return vp, vs, rho def extract_data(self, member): # start by getting the data from the flow simulator super(flow_rock, self).extract_data(member) - # get the sim2seis from file + # get the avo from file for prim_ind in self.l_prim: # Loop over all keys in pred_data (all data types) for key in self.all_data_types: @@ -920,6 +987,9 @@ def extract_data(self, member): else self.folder + key + '_vint' + str(idx) + '.npz' with np.load(filename) as f: self.pred_data[prim_ind][key] = f[key] + # + #v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + #self.pred_data[prim_ind][key] = self.avo_result[v].flatten() def _get_avo_info(self, avo_config=None): """ @@ -932,6 +1002,8 @@ def _get_avo_info(self, avo_config=None): self.avo_config = {} for elem in self.input_dict['avo']: assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + if elem[0] == 'vintage' and not isinstance(elem[1], list): + elem[1] = [elem[1]] self.avo_config[elem[0]] = elem[1] # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later @@ -1067,8 +1139,131 @@ def _calc_avo_props(self, dt=0.0005): self.avo_data = avo_data + def _calc_avo_props_active_cells(self, grid, vp, vs, rho, dt=0.0005): + # dt is the fine resolution sampling rate + # convert properties in reservoir model to time domain + vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) + vs_shale = self.avo_config['vs_shale'] # scalar value + rho_shale = self.avo_config['den_shale'] # scalar value + + + actnum = grid['ACTNUM'] + # Find indices where the boolean array is True + active_indices = np.where(actnum) + # # # + + # Two-way travel time tp the top of the reservoir + # Use cell depths of top layer + zcorn = grid['ZCORN'] + + c, a, b = active_indices + + # Two-way travel time tp the top of the reservoir + top_res = 2 * zcorn[0, 0, :, 0, :, 0] / vp_shale + + # depth difference between cells in z-direction: + depth_differences = np.diff(zcorn[:, 0, :, 0, :, 0] , axis=0) + # Extract the last layer + last_layer = depth_differences[-1, :, :] + # Reshape to ensure it has the same number of dimensions + last_layer = last_layer.reshape(1, depth_differences.shape[1], depth_differences.shape[2]) + # Concatenate to the original array along the first axis + extended_differences = np.concatenate([depth_differences, last_layer], axis=0) + + # Cumulative traveling time through the reservoir in vertical direction + #cum_time_res = 2 * zcorn[:, 0, :, 0, :, 0] / self.vp + top_res[np.newaxis, :, :] + cum_time_res = np.cumsum(2 * extended_differences / vp, axis=0) + top_res[np.newaxis, :, :] + # assumes under burden to be constant. No reflections from under burden. Hence set travel time to under burden very large + underburden = top_res + np.nanmax(cum_time_res) + + # total travel time + # cum_time = np.concat enate((top_res[:, :, np.newaxis], cum_time_res), axis=2) + cum_time = np.concatenate((top_res[np.newaxis, :, :], cum_time_res, underburden[np.newaxis, :, :]), axis=0) + + # add overburden and underburden values for Vp, Vs and Density + vp = np.concatenate((vp_shale * np.ones((1, self.NY, self.NX)), + vp, vp_shale * np.ones((1, self.NY, self.NX))), axis=0) + vs = np.concatenate((vs_shale * np.ones((1, self.NY, self.NX)), + vs, vs_shale * np.ones((1, self.NY, self.NX))), axis=0) + rho = np.concatenate((rho_shale * np.ones((1, self.NY, self.NX)), + rho, rho_shale * np.ones((1, self.NY, self.NX))), axis=0) + + + # Combine a and b into a 2D array (each column represents a vector) + ab = np.column_stack((a, b)) + + # Extract unique rows and get the indices of those unique rows + unique_rows, indices = np.unique(ab, axis=0, return_index=True) + + # search for the lowest grid cell thickness and sample the time according to + # that grid thickness to preserve the thin layer effect + time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) + if time_sample.shape[0] == 1: + time_sample = time_sample.reshape(-1) + time_sample = np.tile(time_sample, (len(indices), 1)) + + vp_sample = np.zeros((len(indices), time_sample.shape[1])) + vs_sample = np.zeros((len(indices), time_sample.shape[1])) + rho_sample = np.zeros((len(indices), time_sample.shape[1])) + + for ind in range(len(indices)): + for k in range(time_sample.shape[1]): + # find the right interval of time_sample[m, l, k] belonging to, and use + # this information to allocate vp, vs, rho + idx = np.searchsorted(cum_time[:, a[indices[ind]], b[indices[ind]]], time_sample[ind, k], side='left') + idx = idx if idx < len(cum_time[:, a[indices[ind]], b[indices[ind]]]) else len( + cum_time[:,a[indices[ind]], b[indices[ind]]]) - 1 + vp_sample[ind, k] = vp[idx, a[indices[ind]], b[indices[ind]]] + vs_sample[ind, k] = vs[idx, a[indices[ind]], b[indices[ind]]] + rho_sample[ind, k] = rho[idx, a[indices[ind]], b[indices[ind]]] + + # Ricker wavelet + wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len']-dt, dt), + f0=self.avo_config['frequency']) + + # Travel time corresponds to reflectivity series + t = time_sample[:, 0:-1] + + # interpolation time + t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) + trace_interp = np.zeros((len(indices), len(t_interp))) + + # number of pp reflection coefficients in the vertical direction + nz_rpp = vp_sample.shape[1] - 1 + conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + + avo_data = [] + Rpp = [] + for i in range(len(self.avo_config['angle'])): + angle = self.avo_config['angle'][i] + Rpp = self.pp_func(vp_sample[:, :-1], vs_sample[:, :-1], rho_sample[:, :-1], + vp_sample[:, 1:], vs_sample[:, 1:], rho_sample[:, 1:], angle) - def _calc_avo_props_active_cells(self, grid, dt=0.0005): + for ind in range(len(indices)): + # convolution with the Ricker wavelet + + w_trace = conv_op * Rpp[ind, :] + + # Sample the trace into regular time interval + f = interp1d(np.squeeze(t[ind, :]), np.squeeze(w_trace), + kind='nearest', fill_value='extrapolate') + trace_interp[ind, :] = f(t_interp) + + if i == 0: + avo_data = trace_interp # 3D + elif i == 1: + avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D + else: + avo_data = np.concatenate((avo_data, trace_interp[:, :, np.newaxis]), axis=2) # 4D + + return avo_data, Rpp, vp_sample, vs_sample, rho_sample + #self.avo_data = avo_data + #self.Rpp = Rpp + #self.vp_sample = vp_sample + #self.vs_sample = vs_sample + #self.rho_sample = rho_sample + + def _calc_avo_props_active_cells_org(self, grid, dt=0.0005): # dt is the fine resolution sampling rate # convert properties in reservoir model to time domain vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) @@ -1077,12 +1272,16 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): # check if Nz, is at axis = 0, then transpose to dimensions, Nx, ny, Nz if grid['ACTNUM'].shape[0] == self.NZ: - self.vp = np.transpose(self.vp, (2, 1, 0)) - self.vs = np.transpose(self.vs, (2, 1, 0)) - self.rho = np.transpose(self.rho, (2, 1, 0)) + vp = np.transpose(self.vp, (2, 1, 0)) + + vs = np.transpose(self.vs, (2, 1, 0)) + rho = np.transpose(self.rho, (2, 1, 0)) actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) else: actnum = grid['ACTNUM'] + vp = self.vp + vs = self.vs + rho = self.rho # # # # Two-way travel time of the top of the reservoir @@ -1090,9 +1289,9 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): top_res = 2 * self.TOPS[:, :, 0] / vp_shale # Cumulative traveling time through the reservoir in vertical direction - cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] + cum_time_res = np.nancumsum(2 * self.DZ / vp, axis=2) + top_res[:, :, np.newaxis] - # assumes underburden to be constant. No reflections from underburden. Hence set traveltime to underburden very large + # assumes under burden to be constant. No reflections from under burden. Hence set travel time to under burden very large underburden = top_res + np.max(cum_time_res) # total travel time @@ -1101,13 +1300,13 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): # add overburden and underburden of Vp, Vs and Density vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), - self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), - self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) #rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 # self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)), - self.rho, rho_shale * np.ones((self.NX, self.NY, 1))), axis=2) + rho, rho_shale * np.ones((self.NX, self.NY, 1))), axis=2) # get indices of active cells @@ -1158,6 +1357,8 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): nz_rpp = vp_sample.shape[1] - 1 conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + avo_data = [] + Rpp = [] for i in range(len(self.avo_config['angle'])): angle = self.avo_config['angle'][i] Rpp = self.pp_func(vp_sample[:, :-1], vs_sample[:, :-1], rho_sample[:, :-1], @@ -1197,7 +1398,7 @@ def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): """ array = np.array(array) if len(array.shape) != 1: # if array is a 1D array, then do nothing - assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" + assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy array are supported" # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] new_array = np.transpose(array, axes=[2, 1, 0]) @@ -1223,60 +1424,11 @@ def setup_fwd_run(self, **kwargs): def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. - #self.ensemble_member = member_i - #self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - #return self.pred_data - - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder self.ensemble_member = member_i + #return super().run_fwd_sim(state, member_i, del_folder=del_folder) + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) + return self.pred_data - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. @@ -1284,8 +1436,11 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): if folder is None: folder = self.folder - # run flow simulator - success = super(flow_rock, self).call_sim(folder, True) + if not self.no_flow: + # call call_sim in flow class (skip flow_rock, go directly to flow which is a parent of flow_rock) + success = super(flow_rock, self).call_sim(folder, True) + else: + success = True # # use output from flow simulator to forward model gravity response if success: @@ -1294,9 +1449,18 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): return success def get_grav_result(self, folder, save_folder): - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - grid = self.ecl_case.grid() + if self.no_flow: + grid_file = self.pem_input['grid'] + grid = np.load(grid_file) + else: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + + #f_dim = [self.NZ, self.NY, self.NX] + + self.dyn_var = [] # cell centers self.find_cell_centers(grid) @@ -1307,56 +1471,53 @@ def get_grav_result(self, folder, save_folder): # loop over vintages with gravity acquisitions grav_struct = {} - for v, assim_time in enumerate(self.grav_config['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - - # porosity, saturation, densities, and fluid mass at individual time-steps - grav_struct[v] = self.calc_mass(time) # calculate the mass of each fluid in each grid cell - - if 'baseline' in self.grav_config: # 4D measurement base_time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + dt.timedelta(days=self.grav_config['baseline']) # porosity, saturation, densities, and fluid mass at time of baseline survey - grav_base = self.calc_mass(base_time) + grav_base = self.calc_mass(base_time, 0) else: # seafloor gravity only work in 4D mode + grav_base = None print('Need to specify Baseline survey for gravity in pipt file') + for v, assim_time in enumerate(self.grav_config['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + + # porosity, saturation, densities, and fluid mass at individual time-steps + grav_struct[v] = self.calc_mass(time, v+1) # calculate the mass of each fluid in each grid cell + + + vintage = [] for v, assim_time in enumerate(self.grav_config['vintage']): dg = self.calc_grav(grid, grav_base, grav_struct[v]) vintage.append(deepcopy(dg)) - save_dic = {'grav': dg, **self.grav_config} + #save_dic = {'grav': dg, **self.grav_config} + save_dic = { + 'grav': dg, 'P_vint': grav_struct[v]['PRESSURE'], 'rho_gas_vint':grav_struct[v]['GAS_DEN'], + **self.grav_config, + **{key: grav_struct[v][key] - grav_base[key] for key in grav_struct[v].keys()} + } if save_folder is not None: file_name = save_folder + os.sep + f"grav_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"grav_vint{v}.npz" else: file_name = folder + os.sep + f"grav_vint{v}.npz" if folder[-1] != os.sep \ else folder + f"grav_vint{v}.npz" + file_name_rec = 'Ensemble_results/' + f"grav_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"grav_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) # with open(file_name, "wb") as f: # dump(**save_dic, f) np.savez(file_name, **save_dic) - # fluid masses - save_dic = {key: grav_struct[v][key] - grav_base[key] for key in grav_struct[v].keys()} - if save_folder is not None: - file_name = save_folder + os.sep + f"fluid_mass_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"fluid_mass_vint{v}.npz" - else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"fluid_mass_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"fluid_mass_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"fluid_mass_vint{v}.npz" - np.savez(file_name, **save_dic) # 4D response @@ -1364,66 +1525,148 @@ def get_grav_result(self, folder, save_folder): for i, elem in enumerate(vintage): self.grav_result.append(elem) - def calc_mass(self, time): - # fluid phases written to restart file from simulator run - phases = self.ecl_case.init.phases + def calc_mass(self, time, time_index = None): + + if self.no_flow: + time_input = time_index + else: + time_input = time + # fluid phases given as input + phases = str.upper(self.pem_input['phases']) + phases = phases.split() + # grav_input = {} + tmp_dyn_var = {} - keywords = self.ecl_case.arrays(time) - keywords = [s.strip() for s in keywords] # Remove leading/trailing spaces - # for key in self.all_data_types: - # if 'grav' in key: - # pore volumes at each assimilation step - if 'RPORV' in keywords: - #tmp = self._get_pem_input('RPORV', time) - #grav_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) - # get active porosity - # pore volumes at each assimilation step - tmp = self.ecl_case.cell_data('RPORV', time) - grav_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - else: - print('Keyword RPORV missing from simulation output, need pdated porevolumes at each assimilation step') + + tmp = self._get_pem_input('RPORV', time_input) + grav_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + + tmp = self._get_pem_input('PRESSURE', time_input) + #if time_input == time_index and time_index > 0: # to be activiated in case on inverts for Delta Pressure + # # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('PRESSURE', 0) + # tmp = tmp + tmp_baseline + grav_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) + # convert pressure from Bar to MPa + if 'press_conv' in self.pem_input and time_input == time: + grav_input['PRESSURE'] = grav_input['PRESSURE'] * self.pem_input['press_conv'] + #else: + # print('Keyword RPORV missing from simulation output, need updated pore volumes at each assimilation step') # extract saturation if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended for var in phases: if var in ['WAT', 'GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time_input) + #if time_input == time_index and time_index > 0: # to be activated in case on inverts for Delta S + # # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('S{}'.format(var), 0) + # tmp = tmp + tmp_baseline + #tmp = self.ecl_case.cell_data('S{}'.format(var), time) grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] > 1] = 1 + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] < 0] = 0 grav_input['SOIL'] = 1 - (grav_input['SWAT'] + grav_input['SGAS']) + grav_input['SOIL'][grav_input['SOIL'] > 1] = 1 + grav_input['SOIL'][grav_input['SOIL'] < 0] = 0 + + + tmp_dyn_var['SWAT'] = grav_input['SWAT'] # = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + tmp_dyn_var['SGAS'] = grav_input['SGAS'] + tmp_dyn_var['SOIL'] = grav_input['SOIL'] + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model for var in phases: if var in ['GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time_input) + #if time_input == time_index and time_index > 0: # to be activated in case on inverts for Delta S + # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('S{}'.format(var), 0) + # tmp = tmp + tmp_baseline + #tmp = self.ecl_case.cell_data('S{}'.format(var), time) grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] > 1] = 1 + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] < 0] = 0 grav_input['SWAT'] = 1 - (grav_input['SGAS']) + # fluid saturation + tmp_dyn_var['SWAT'] = grav_input['SWAT'] #= {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + tmp_dyn_var['SGAS'] = grav_input['SGAS'] + + elif 'OIL' in phases and 'GAS' in phases: # Original Smeaheia model + for var in phases: + if var in ['GAS']: + tmp = self._get_pem_input('S{}'.format(var), time_input) + #if time_input == time_index and time_index > 0: # to be activated in case on inverts for Delta S + # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('S{}'.format(var), 0) + # tmp = tmp + tmp_baseline + #tmp = self.ecl_case.cell_data('S{}'.format(var), time) + grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] > 1] = 1 + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] < 0] = 0 + + grav_input['SOIL'] = 1 - (grav_input['SGAS']) + + # fluid saturation + tmp_dyn_var['SOIL'] = grav_input['SOIL'] #= {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + tmp_dyn_var['SGAS'] = grav_input['SGAS'] + else: print('Type and number of fluids are unspecified in calc_mass') - # fluid densities for var in phases: dens = var + '_DEN' - tmp = self.ecl_case.cell_data(dens, time) - grav_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + #tmp = self.ecl_case.cell_data(dens, time) + if self.no_flow: + if any('pressure' in key for key in self.state.keys()): + if 'press_conv' in self.pem_input: + conv2pa = 1e6 #MPa to Pa + else: + conv2pa = 1e5 # Bar to Pa + + if var == 'GAS': + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: + tmp = PropsSI('D', 'T', 298.15, 'P', grav_input['PRESSURE']*conv2pa, 'Methane') + elif 'WAT' in phases and 'GAS' in grav_input['PRESSURE']: # Smeaheia model T = 37 C + tmp = PropsSI('D', 'T', 310.15, 'P', grav_input['PRESSURE']*conv2pa, 'CO2') + mask = np.zeros(tmp.shape, dtype=bool) + tmp = np.ma.array(data=tmp, dtype=tmp.dtype, mask=mask) + elif var == 'WAT': + tmp = PropsSI('D', 'T|liquid', 298.15, 'P', grav_input['PRESSURE']*conv2pa, 'Water') + mask = np.zeros(tmp.shape, dtype=bool) + tmp = np.ma.array(data=tmp, dtype=tmp.dtype, mask=mask) + else: + tmp = self._get_pem_input(dens, time_input) + else: + tmp = self._get_pem_input(dens, time_input) + grav_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + tmp_dyn_var[dens] = grav_input[dens] + else: + tmp = self._get_pem_input(dens, time_input) + grav_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + tmp_dyn_var[dens] = grav_input[dens] - #fluid masses + tmp_dyn_var['PRESSURE'] = grav_input['PRESSURE'] + tmp_dyn_var['RPORV'] = grav_input['RPORV'] + self.dyn_var.extend([tmp_dyn_var]) + + #fluid masses for var in phases: mass = var + '_mass' - grav_input[mass] = grav_input[var + '_DEN'] * grav_input['S' + var] * grav_input['PORO'] + grav_input[mass] = grav_input[var + '_DEN'] * grav_input['S' + var] * grav_input['RPORV'] return grav_input def calc_grav(self, grid, grav_base, grav_repeat): - - #cell_centre = [x, y, z] cell_centre = self.grav_config['cell_centre'] x = cell_centre[0] @@ -1438,15 +1681,22 @@ def calc_grav(self, grid, grav_base, grav_repeat): dg = np.zeros(N_meas) # 1D array for dg dg[:] = np.nan - # total fluid mass at this time - phases = self.ecl_case.init.phases + # fluid phases given as input + phases = str.upper(self.pem_input['phases']) + phases = phases.split() if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: dm = grav_repeat['OIL_mass'] + grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['WAT_mass'] + grav_base['GAS_mass']) + elif 'OIL' in phases and 'GAS' in phases: # Original Smeaheia model + dm = grav_repeat['OIL_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['GAS_mass']) + # dm = grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['WAT_mass'] + grav_base['GAS_mass']) + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model dm = grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['WAT_mass'] + grav_base['GAS_mass']) + #dm = grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['WAT_mass'] + grav_base['GAS_mass']) else: + dm = None print('Type and number of fluids are unspecified in calc_grav') @@ -1457,7 +1707,7 @@ def calc_grav(self, grid, grav_base, grav_repeat): z - pos['z'][j]) ** 2) ** (3 / 2) dg[j] = np.dot(dg_tmp, dm) - print(f'Progress: {j + 1}/{N_meas}') # Mimicking waitbar + #print(f'Progress: {j + 1}/{N_meas}') # Mimicking wait bar # Scale dg by the constant dg *= 6.67e-3 @@ -1475,8 +1725,8 @@ def measurement_locations(self, grid): ymax = np.max(cell_centre[1]) # Make a mesh of the area - pad = self.grav_config.get('padding_reservoir', 1500) # 3 km padding around the reservoir - if 'padding_reservoir' not in self.grav_config: + pad = self.grav_config.get('padding', 1500) # 3 km padding around the reservoir + if 'padding' not in self.grav_config: print('Please specify extent of measurement locations (Padding in pipt file), using 1.5 km as default') xmin -= pad @@ -1524,7 +1774,7 @@ def get_seabed_depths(self): # Read the data while skipping the header comments # We'll assume the header data ends before the numerical data # The 'delim_whitespace' keyword in pd.read_csv is deprecated and will be removed in a future version. Use ``sep='\s+'`` instead - water_depths = pd.read_csv(file_path, comment='#', delim_whitespace=True, header=None) + water_depths = pd.read_csv(file_path, comment='#', sep=r'\s+', header=None)#delim_whitespace=True, header=None) # Give meaningful column names: water_depths.columns = ['x', 'y', 'z', 'column', 'row'] @@ -1580,9 +1830,9 @@ def _get_grav_info(self, grav_config=None): self.grav_config = {} for elem in self.input_dict['grav']: assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + if elem[0] == 'vintage' and not isinstance(elem[1], list): + elem[1] = [elem[1]] self.grav_config[elem[0]] = elem[1] - - else: self.grav_config = None @@ -1618,7 +1868,7 @@ def setup_fwd_run(self, **kwargs): def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) return self.pred_data @@ -1627,6 +1877,8 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): # Then, get the pem. if folder is None: folder = self.folder + else: + self.folder = folder # run flow simulator # success = True @@ -1662,4 +1914,634 @@ def extract_data(self, member): else self.folder + key + '_vint' + str(idx) + '.npz' with np.load(filename) as f: self.pred_data[prim_ind][key] = f[key] + #v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + #self.pred_data[prim_ind][key] = self.avo_result[v].flatten() + +class flow_seafloor_disp(flow_grav): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + self.grav_input = {} + assert 'sea_disp' in input_dict, 'To do subsidence/uplift simulation, please specify an "SEA_DISP" section in the pipt file' + self._get_disp_info() + + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run(redund_sim=None) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if folder is None: + folder = self.folder + + # run flow simulator + # success = True + success = super(flow_rock, self).call_sim(folder, True) + + # use output from flow simulator to forward model gravity response + if success: + # calculate gravity data based on flow simulation output + self.get_displacement_result(folder, save_folder) + + + return success + + def get_displacement_result(self, folder, save_folder): + if self.no_flow: + grid_file = self.pem_input['grid'] + grid = np.load(grid_file) + else: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + self.dyn_var = [] + + # cell centers + self.find_cell_centers(grid) + + # receiver locations + self.measurement_locations(grid) + + # loop over vintages with gravity acquisitions + disp_struct = {} + + if 'baseline' in self.disp_config: # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.grav_config['baseline']) + # porosity, saturation, densities, and fluid mass at time of baseline survey + disp_base = self.get_pore_volume(base_time, 0) + + + else: + # seafloor displacement only work in 4D mode + disp_base = None + print('Need to specify Baseline survey for displacement modelling in pipt file') + + for v, assim_time in enumerate(self.disp_config['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + + # porosity, saturation, densities, and fluid mass at individual time-steps + disp_struct[v] = self.get_pore_volume(time, v+1) # calculate the mass of each fluid in each grid cell + + + + vintage = [] + + for v, assim_time in enumerate(self.disp_config['vintage']): + # calculate subsidence and uplift + dz = self.map_z_response(disp_base, disp_struct[v], grid) + vintage.append(deepcopy(dz)) + + save_dic = {'disp': dz, **self.disp_config} + if save_folder is not None: + file_name = save_folder + os.sep + f"sea_disp_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"sea_disp_vint{v}.npz" + else: + file_name = folder + os.sep + f"sea_disp_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"sea_disp_vint{v}.npz" + file_name_rec = f"sea_disp_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else f"sea_disp_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) + + # with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + + # 4D response + self.disp_result = [] + for i, elem in enumerate(vintage): + self.disp_result.append(elem) + + def get_pore_volume(self, time, time_index = None): + + if self.no_flow: + time_input = time_index + else: + time_input = time + + # fluid phases given as input + phases = str.upper(self.pem_input['phases']) + phases = phases.split() + # + disp_input = {} + tmp_dyn_var = {} + + + tmp = self._get_pem_input('RPORV', time_input) + disp_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + + tmp = self._get_pem_input('PRESSURE', time_input) + #if time_input == time_index and time_index > 0: # to be activiated in case on inverts for Delta Pressure + # # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('PRESSURE', 0) + # tmp = tmp + tmp_baseline + disp_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) + # convert pressure from Bar to MPa + if 'press_conv' in self.pem_input and time_input == time: + disp_input['PRESSURE'] = disp_input['PRESSURE'] * self.pem_input['press_conv'] + #else: + # print('Keyword RPORV missing from simulation output, need pdated porevolumes at each assimilation step') + + + tmp_dyn_var['PRESSURE'] = disp_input['PRESSURE'] + tmp_dyn_var['RPORV'] = disp_input['RPORV'] + self.dyn_var.extend([tmp_dyn_var]) + + return disp_input + + def compute_horizontal_distance(self, pos, x, y): + dx = pos['x'][:, np.newaxis] - x + dy = pos['y'][:, np.newaxis] - y + rho = np.sqrt(dx ** 2 + dy ** 2).flatten() + return rho + + def map_z_response(self, base, repeat, grid): + """ + Maps out subsidence and uplift based either on the simulation + model pressure drop (method = 'pressure') or simulated change in pore volume + using either the van Opstal or Geertsma forward model + + Arguments: + base -- A dictionary containing baseline pressures and pore volumes. + repeat -- A dictionary containing pressures and pore volumes at repeat measurements. + + compute subsidence at position 'pos', b + + Output is modeled subsidence in cm. + + """ + + # Method to compute pore volume change + method = self.disp_config['method'].lower() + + # Forward model to compute subsidence/uplift response + model = self.disp_config['model'].lower() + + if self.disp_config['poisson'] > 0.5: + poisson = 0.5 + print('Poisson\'s ratio exceeds physical limits, setting it to 0.5') + else: + poisson = self.disp_config['poisson'] + + # Depth of rigid basement + z_base = self.disp_config['z_base'] + + compressibility = self.disp_config['compressibility'] # 1/MPa + + E = ((1 + poisson) * (1 - 2 * poisson)) / ((1 - poisson) * compressibility) + + # coordinates of cell centres + cell_centre = self.grav_config['cell_centre'] + + # measurement locations + pos = self.grav_config['meas_location'] + + + # compute pore volume change between baseline and repeat survey + # based on the reservoir pore volumes in the individual vintages + if method == 'pressure': + dV = base['RPORV'] * (base['PRESSURE'] - repeat['PRESSURE']) * compressibility + else: + dV = base['RPORV'] - repeat['RPORV'] + + # coordinates of cell centres + x = cell_centre[0] + y = cell_centre[1] + z = cell_centre[2] + + # Depth range of reservoir plus vertical span in seafloor measurement positions + # z_res = np.linspace(np.min(z) - np.max(pos['z']) - 1, np.max(z) - np.min(pos['z']) + 1) + # Maximum horizontal span between seafloor measurement location and reservoir boundary + #rho_x_max = max(np.max(x) - np.min(pos['x']), np.max(pos['x']) - np.min(x)) + #rho_y_max = max(np.max(y) - np.min(pos['y']), np.max(pos['y']) - np.min(y)) + #rho = np.linspace(0, np.sqrt(rho_x_max ** 2 + rho_y_max ** 2) + 1) + #rho_mesh, z_res_mesh = np.meshgrid(rho, z_res) + #t_van_opstal, t_geertsma = self.compute_van_opstal_transfer_function(z_res, z_base, rho, poisson) + + if model == 'van_Opstal': + # Represents a signal change for subsidence/uplift. + #trans_func = t_van_opstal + component = ["Geertsma_vertical", "System_3_vertical"] + else: # Use Geertsma + #trans_func = t_geertsma + component = ["Geertsma_vertical"] + # Initialization + dz_1_2 = 0 + dz_3 = 0 + + # indices of active cells: + true_indices = np.where(grid['ACTNUM']) + # number of active gridcells + Nn = len(true_indices[0]) + + for j in range(Nn): + rho = self.compute_horizontal_distance(pos, x[j], y[j]) + THH, TRB = self.compute_deformation_transfer(pos['z'], z[j], z_base, rho, poisson, E, dV[j], component) + dz_1_2 = dz_1_2 + THH + dz_3 = dz_3 + TRB + + if model == 'van_Opstal': + # Represents a signal change for subsidence/uplift. + dz = dz_1_2 + dz_3 + else: # Use Geertsma + dz = dz_1_2 + + #ny, nx = pos['x'].shape + #dz = np.zeros((ny, nx)) + + # Compute subsidence and uplift + #for j in range(ny): + # for i in range(nx): + # r = np.sqrt((x - pos['x'][j, i]) ** 2 + (y - pos['y'][j, i]) ** 2) + # dz[j, i] = np.sum( + # dV * griddata((rho_mesh.flatten(), z_res_mesh.flatten()), trans_func, (r, z[j, i] - pos['z'][j, i]), + # method='linear')) + + # Normalize + #dz = dz * (1 - poisson) / (2 * np.pi) + + # Convert from meters to centimeters + dz *= 100 + + return dz + + def compute_van_opstal_transfer_function(self, z_res, z_base, rho, poisson): + """ + Compute the Van Opstal transfer function. + + Args: + z_res -- Numpy array of depths to reservoir cells [m]. + z_base -- Distance to the basement [m]. + rho -- Numpy array of horizontal distances in the field [m]. + poisson -- Poisson's ratio. + + Returns: + T -- Numpy array of the transfer function values. + T_geertsma -- Numpy array of the Geertsma transfer function values. + """ + + # Change to km scale + rho = rho / 1e3 + z_res = z_res / 1e3 + z_base = z_base / 1e3 + + + # Find lambda max (to optimize Hilbert transform) + cutoff = 1e-10 # Function value at max lambda + try: + lambda_max = fsolve(lambda x: 4 * (2 * x * z_base + 1) / (3 - 4 * poisson) * np.exp( + x * (np.max(z_res) - 2 * z_base)) - cutoff, 10)[0] + except: + lambda_max = 10 # Default value if unable to solve for max lambda + + lambda_vals = np.linspace(0, lambda_max, 100) + # range of lateral distances between measurement location and reservoir cells + nj = len(rho) + # range of vertical distances between measurement location and reservoir cells + ni = len(z_res) + # initialize + t_van_opstal = np.zeros((ni, nj)) + + # input function to make a hankel transform of order 0 of + c_t = self.van_opstal(lambda_vals, z_res[0], z_base, poisson) + + h_t, i_t = self.h_t(c_t, lambda_vals, rho) # Extract integrand + t_van_opstal[0, :] = (2 * z_res[0] / (rho ** 2 + z_res[0] ** 2) ** (3 / 2)) + h_t / (2 * np.pi) + + for i in range(1, ni): + C = self.van_opstal(lambda_vals, z_res[i], z_base, poisson) + h_t = self.h_t(C, lambda_vals, rho, i_t) + t_van_opstal[i, :] = (2 * z_res[i] / (rho ** 2 + z_res[i] ** 2) ** (3 / 2)) + h_t / (2 * np.pi) + + t_van_opstal *= 1e-6 # Convert back to meters + + t_geertsma = (2 * z_res[:, np.newaxis] / ((np.ones((ni, 1)) * rho) ** 2 + (z_res[:, np.newaxis]) ** 2) ** ( + 3 / 2)) * 1e-6 + + return t_van_opstal, t_geertsma + + def van_opstal(self, lambda_vals, z_res, z_base, poisson): + """ + Compute the Van Opstal transfer function. + + Args: + lambda_vals -- Numpy array of lambda values. + z_res -- Depth to reservoir [m]. + z_base -- Distance to the basement [m]. + poisson -- Poisson's ratio. + + Returns: + value -- Numpy array of computed values. + """ + + term1 = np.exp(lambda_vals * z_res) * (2 * lambda_vals * z_base + 1) + term2 = np.exp(-lambda_vals * z_res) * ( + 4 * lambda_vals ** 2 * z_base ** 2 + 2 * lambda_vals * z_base + (3 - 4 * poisson) ** 2) + + term3_numer = (3 - 4 * poisson) * ( + np.exp(-lambda_vals * (2 * z_base + z_res)) - np.exp(-lambda_vals * (2 * z_base - z_res))) + term3_denom = 2 * ((1 - 2 * poisson) ** 2 + lambda_vals ** 2 * z_base ** 2 + (3 - 4 * poisson) * np.cosh( + lambda_vals * z_base) ** 2) + + value = term1 - term2 - (term3_numer / term3_denom) + + return value + + def van_opstal_org(self, lambda_vals, z_res, z_base, poisson): + """ + Compute the Van Opstal transfer function. + + Args: + lambda_vals -- Numpy array of lambda values. + z_res -- Depth to reservoir [m]. + z_base -- Distance to the basement [m]. + poisson -- Poisson's ratio. + + Returns: + value -- Numpy array of computed values. + """ + + term1 = np.exp(lambda_vals * z_res) * (2 * lambda_vals * z_base + 1) + term2 = np.exp(-lambda_vals * z_res) * ( + 4 * lambda_vals ** 2 * z_base ** 2 + 2 * lambda_vals * z_base + (3 - 4 * poisson) ** 2) + + term3_numer = (3 - 4 * poisson) * ( + np.exp(-lambda_vals * (2 * z_base + z_res)) - np.exp(-lambda_vals * (2 * z_base - z_res))) + term3_denom = 2 * ((1 - 2 * poisson) ** 2 + lambda_vals ** 2 * z_base ** 2 + (3 - 4 * poisson) * np.cosh( + lambda_vals * z_base) ** 2) + + value = term1 - term2 - (term3_numer / term3_denom) + + return value + + def hankel_transform_order_0(f, r_max, num_points=1000): + """ + Computes the Hankel transform of order 0 of a function f(r). + + Parameters: + - f: callable, the function to transform, f(r) + - r_max: float, upper limit of the integral (approximate infinity) + - num_points: int, number of points for numerical integration + + Returns: + - k_values: array of k values + - H_k: array of Hankel transform evaluated at k_values + """ + r = np.linspace(0, r_max, num_points) + dr = r[1] - r[0] + f_r = f(r) + + def integrand(r, k): + return f(r) * j0(k * r) * r + + # Define a range of k values to evaluate + k_min, k_max = 0, 10 # adjust as needed + k_values = np.linspace(k_min, k_max, 100) + + H_k = [] + + for k in k_values: + # Perform numerical integration over r + result, _ = quad(integrand, 0, r_max, args=(k,)) + H_k.append(result) + + return k_values, np.array(H_k) + + def makeL(self, poisson, k, c, A_g, eps, lambda_): + L = A_g * ( + (4 * poisson - 3 + 2 * k * lambda_) * np.exp(-lambda_ * (k + c)) + - np.exp(lambda_ * eps * (k - c)) + ) + return L + + def makeM(self, poisson, k, c, A_g, eps, lambda_): + M = A_g * ( + (4 * poisson - 3 - 2 * k * lambda_) * np.exp(-lambda_ * (k + c)) + - eps * np.exp(lambda_ * eps * (k - c)) + ) + return M + + def makeDelta(self, poisson, k, lambda_): + Delta = ( + (4 * poisson - 3) * np.cosh(k * lambda_) ** 2 + - (k * lambda_) ** 2 + - (1 - 2 * poisson) ** 2 + ) + return Delta + + def makeB(self, poisson, k, c, A_g, eps, lambda_): + L = self.makeL(poisson, k, c, A_g, eps, lambda_) + M = self.makeM(poisson, k, c, A_g, eps, lambda_) + Delta = self.makeDelta(poisson, k, lambda_) + + numerator = ( + lambda_ * L * (2 * (1 - poisson) * np.cosh(k * lambda_) - lambda_ * k * np.sinh(k * lambda_)) + + lambda_ * M * ((1 - 2 * poisson) * np.sinh(k * lambda_) + k * lambda_ * np.cosh(k * lambda_)) + ) + + B = numerator / Delta + return B + + def makeC(self,poisson, k, c, A_g, eps, lambda_): + L = self.makeL(poisson, k, c, A_g, eps, lambda_) + M = self.makeM(poisson, k, c, A_g, eps, lambda_) + Delta = self.makeDelta(poisson, k, lambda_) + + numerator = ( + lambda_ * L * ((1 - 2 * poisson) * np.sinh(k * lambda_) - lambda_ * k * np.cosh(k * lambda_)) + + lambda_ * M * (2 * (1 - poisson) * np.cosh(k * lambda_) + k * lambda_ * np.sinh(k * lambda_)) + ) + + C = numerator / Delta + return C + + def compute_deformation_transfer(self, z, c, k, rho, poisson, E, dV, component): + # Convert inputs to numpy arrays if they are not already + z = np.array(z) + #x = np.array(x) + rho = np.array(rho) + component = list(component) + + Ni = len(z) + Nj = len(rho) + + # Initialize output arrays + THH = np.zeros((Ni, Nj)) + #THHr = np.zeros((Ni, Nj)) + TRB = np.zeros((Ni, Nj)) + #TRBr = np.zeros((Ni, Nj)) + + # Constants + A_g = -dV * E / (4 * np.pi * (1 + poisson)) + uHH_outside_intregral = -(A_g * (1 + poisson)) / E + uRB_outside_intregral = (1 + poisson) / E + lambda_max = min([0.1, 500 / max(z)]) # Avoid index errors if z is empty + # Setup your lambda grid + num_points = 500 + lambda_grid = np.linspace(0, lambda_max, num_points) + + for c_n in component: + if c_n == 'Geertsma_vertical': + for j in range(Nj): + for i in range(Ni): + eps = np.sign(c - z[i]) + z_ratio = z[i] - c + z_sum = z[i] + c + + # Evaluate the integrand over the grid + integrand_vals = lambda lam: lam * ( + eps * np.exp(lam * eps * z_ratio) + + (3 - 4 * poisson + 2 * z[i] * lam) * np.exp(-lam * z_sum) + ) * j0(lam * rho[j]) + + values = integrand_vals(lambda_grid) + + #def uHH_integrand(lambda_): + # val = lambda_ * (eps * np.exp(lambda_ * eps * (z[i] - c)) + + # (3 - 4 * poisson + 2 * z[i] * lambda_) * + # np.exp(-lambda_ * (z[i] + c))) + # return val * j0(lambda_ * rho[j]) + + THH[i, j] = np.trapz(values, lambda_grid) * uHH_outside_intregral + #THH[i, j] = quad(uHH_integrand, 0, lambda_max)[0] * uHH_outside_intregral + + elif c_n == 'System_3_vertical': + sinh_z = np.sinh(z[:, np.newaxis] * lambda_grid) + cosh_z = np.cosh(z[:, np.newaxis] * lambda_grid) + J0_rho = j0(lambda_grid * rho) + + for j in range(Nj): + rho_j = rho[j] + J0_rho_j = J0_rho[:, j] + for i in range(Ni): + z_i = z[i] + sinh_z_i = sinh_z[i] # precomputed sinh values + cosh_z_i = cosh_z[i] # precomputed cosh values + + # Use vectorized operations over lambda_grid + b_values = self.makeB(poisson, k, c, A_g, -1, lambda_grid) + c_values = self.makeC(poisson, k, c, A_g, -1, lambda_grid) + + part1 = b_values * (lambda_grid * z_i * cosh_z_i - (1 - 2 * poisson) * sinh_z_i) + part2 = c_values * ((2 * (1 - poisson) * cosh_z_i) - lambda_grid * z_i * sinh_z_i) + + values = (part1 + part2) * J0_rho_j + + integral_result = np.trapz(values, lambda_grid) + TRB[i, j] = integral_result * uRB_outside_intregral + + elif c_n == 'System_3_vertical_original': + for j in range(Nj): + for i in range(Ni): + # Assume makeB and makeC are implemented similarly + def uRB_integrand(lambda_): + b_val = self.makeB(poisson, k, c, A_g, -1, lambda_) + c_val = self.makeC(poisson, k, c, A_g, -1, lambda_) + # Assuming makeB and makeC return scalars. Replace with actual functions. + part1 = b_val * (lambda_ * z[i] * np.cosh(z[i] * lambda_) - (1 - 2 * poisson) * np.sinh( + z[i] * lambda_)) + part2 = c_val * (2 * (1 - poisson) * np.cosh(z[i] * lambda_) + (-lambda_) * z[i] * np.sinh( + z[i] * lambda_)) + return (part1 + part2) * j0(lambda_ * rho[j]) + + values = uRB_integrand(lambda_grid) + integral_result = np.trapz(values, lambda_grid) + TRB[i, j] = integral_result * uRB_outside_intregral + #TRB[i, j] = quad(uRB_integrand, 0, lambda_max)[0] * uRB_outside_intregral + + return THH, TRB + + def h_t(self, h, r=None, k=None, i_k=None): + """ + Hankel transform of order 0. + + Args: + h -- Signal h(r). + r -- Radial positions [m] (optional). + k -- Spatial frequencies [rad/m] (optional). + I -- Integration kernel (optional). + + Returns: + h_t -- Spectrum H(k). + I -- Integration kernel. + """ + + # Check if h is a vector + if h.ndim > 1: + raise ValueError('Signal must be a vector.') + + if r is None or len(r) == 0: + r = np.arange(len(h)) # Default to 0:numel(h)-1 + else: + r = np.sort(r) + h = h[np.argsort(r)] # Sort h according to sorted r + + if k is None or len(k) == 0: + k = np.pi / len(h) * np.arange(len(h)) # Default spatial frequencies + + if i_k is None: + # Create integration kernel I + r = np.concatenate([(r[:-1] + r[1:]) / 2, [r[-1]]]) # Midpoints plus last point + i_k = (2 * np.pi / k[:, np.newaxis]) * r * jv(1, k[:, np.newaxis] * r) # Bessel function + i_k[k == 0, :] = np.pi * r * r + i_k = i_k - np.hstack([np.zeros((len(k), 1)), i_k[:, :-1]]) # Shift integration kernel + else: + # Ensure I is sorted based on r + i_k = i_k[:, np.argsort(r)] + + # Compute Hankel Transform + h_t = np.reshape(i_k @ h.flatten(), k.shape) + + + + return h_t, i_k + + def _get_disp_info(self, grav_config=None): + """ + seafloor displacement (uplift/subsidence) configuration + """ + # list of configuration parameters in the "Grav" section of teh pipt file + config_para_list = ['baseline', 'vintage', 'method', 'model', 'poisson', 'compressibility', 'z_base'] + + if 'sea_disp' in self.input_dict: + self.disp_config = {} + for elem in self.input_dict['sea_disp']: + assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + if elem[0] == 'vintage' and not isinstance(elem[1], list): + elem[1] = [elem[1]] + self.disp_config[elem[0]] = elem[1] + else: + self.disp_config = None + + def extract_data(self, member): + # start by getting the data from the flow simulator i.e. prod. and inj. data + super(flow_rock, self).extract_data(member) + + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'grav' in key: + if self.true_prim[1][prim_ind] in self.grav_config['vintage']: + v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.grav_result[v].flatten() + + + if 'subs_uplift' in key: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.disp_result[v].flatten() diff --git a/simulator/flow_rock_backup.py b/simulator/flow_rock_backup.py deleted file mode 100644 index 7eb2e03..0000000 --- a/simulator/flow_rock_backup.py +++ /dev/null @@ -1,1120 +0,0 @@ -from simulator.opm import flow -from importlib import import_module -import datetime as dt -import numpy as np -import os -from misc import ecl, grdecl -import shutil -import glob -from subprocess import Popen, PIPE -import mat73 -from copy import deepcopy -from sklearn.cluster import KMeans -from sklearn.preprocessing import StandardScaler -from mako.lookup import TemplateLookup -from mako.runtime import Context - -# from pylops import avo -from pylops.utils.wavelets import ricker -from pylops.signalprocessing import Convolve1D -from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master -from scipy.interpolate import interp1d -from pipt.misc_tools.analysis_tools import store_ensemble_sim_information -from geostat.decomp import Cholesky -from simulator.eclipse import ecl_100 - -class flow_sim2seis(flow): - """ - Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurement - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self): - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - # need a if to check that we have correct sim2seis - # copy relevant sim2seis files into folder. - for file in glob.glob('sim2seis_config/*'): - shutil.copy(file, 'En_' + str(self.ensemble_member) + os.sep) - - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - grid = self.ecl_case.grid() - - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - tmp = self.ecl_case.cell_data(var, time) - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - - grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { - 'Vs': self.pem.getShearVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v+1}.grdecl', { - 'Vp': self.pem.getBulkVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/rho{v+1}.grdecl', - {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - current_folder = os.getcwd() - run_folder = current_folder + os.sep + 'En_' + str(self.ensemble_member) - # The sim2seis is invoked via a shell script. The simulations provides outputs. Run, and get all output. Search - # for Done. If not finished in reasonable time -> kill - p = Popen(['./sim2seis.sh', run_folder], stdout=PIPE) - start = time - while b'done' not in p.stdout.readline(): - pass - - # Todo: handle sim2seis or pem error - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['sim2seis']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - result = mat73.loadmat(f'En_{member}/Data_conv.mat')['data_conv'] - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = np.sum( - np.abs(result[:, :, :, v]), axis=0).flatten() - -class flow_rock(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - self.pem_result = [] - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['bulkimp']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() - -class flow_barycenter(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the - barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are - identified using k-means clustering, and the number of objects are determined using and elbow strategy. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - self.pem_result = [] - self.bar_result = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - if elem[0] == 'clusters': # number of clusters for each barycenter - self.pem_input['clusters'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - # Extract k-means centers and interias for each element in pem_result - if 'clusters' in self.pem_input: - npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) - n_clusters_list = npzfile['n_clusters_list'] - npzfile.close() - else: - n_clusters_list = len(self.pem_result)*[2] - kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} - for i, bulkimp in enumerate(self.pem_result): - std = np.std(bulkimp) - features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) - scaler = StandardScaler() - scaled_features = scaler.fit_transform(features) - kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) - kmeans.fit(scaled_features) - kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements - self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the barycenters and inertias - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['barycenter']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.bar_result[v].flatten() - -class flow_avo(flow_sim2seis): - def __init__(self, input_dict=None, filename=None, options=None, **kwargs): - super().__init__(input_dict, filename, options) - - assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' - self._get_avo_info() - - def setup_fwd_run(self, **kwargs): - self.__dict__.update(kwargs) - - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - """ - Setup and run the AVO forward simulator. - - Parameters - ---------- - state : dict - Dictionary containing the ensemble state. - - member_i : int - Index of the ensemble member. any index < 0 (e.g., -1) means the ground truth in synthetic case studies - - del_folder : bool, optional - Boolean to determine if the ensemble folder should be deleted. Default is False. - """ - - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder - self.ensemble_member = member_i - - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False - - - def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): - # replace the sim2seis part (which is unusable) by avo based on Pylops - - if folder is None: - folder = self.folder - - # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" - if hasattr(self, 'run_reservoir_model'): - run_reservoir_model = self.run_reservoir_model - - if run_reservoir_model is None: - run_reservoir_model = True - - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) - success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) - #ecl = ecl_100(filename=self.file) - #ecl.options = self.options - #success = ecl.call_sim(folder, wait_for_proc) - else: - success = True - - if success: - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - grid = self.ecl_case.grid() - - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(multfactor * tmp), dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - tmp = self.ecl_case.cell_data(var, time) - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input[var] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - #pem_input['PRESSURE'] = self._reformat3D_then_flatten(pem_input['PRESSURE'] * - # self.pem_input['press_conv']) - - tmp = self.ecl_case.cell_data('PRESSURE', 0) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] - #P_init = self._reformat3D_then_flatten(self.pem.p_init.reshape(tmp.shape) * np.ones(tmp.shape)) - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - #P_init = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init * self.pem_input['press_conv'] - #P_init = self._reformat3D_then_flatten(P_init * self.pem_input['press_conv']) - - saturations = [ - 1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - #saturations = [self._reformat3D_then_flatten(1 - (pem_input['SWAT'] + pem_input['SGAS'])) - # if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - - # Get the pressure - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - else: - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) - - #grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { - # 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { - # 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', - # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - # vp, vs, density - self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 - - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} - if save_folder is not None: - file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"vp_vs_rho_vint{v}.npz" - else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"vp_vs_rho_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - # avo data - self._calc_avo_props() - - avo = self.avo_data.flatten(order="F") - - # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies - # the corresonding (noisy) data are observations in data assimilation - if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: - non_nan_idx = np.argwhere(~np.isnan(avo)) - data_std = np.std(avo[non_nan_idx]) - if self.input_dict['add_synthetic_noise'][0] == 'snr': - noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std - avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) - else: - noise_std = 0.0 # simulated data don't contain noise - - save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - if save_folder is not None: - file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"avo_vint{v}.npz" - else: - # if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - # (self.ensemble_member >= 0): - # file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - # else folder + f"avo_vint{v}.npz" - # else: - # file_name = os.getcwd() + os.sep + f"avo_vint{v}.npz" - file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"avo_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super(flow_sim2seis, self).extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if 'avo' in key: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ - else self.folder + key + '_vint' + str(idx) + '.npz' - with np.load(filename) as f: - self.pred_data[prim_ind][key] = f[key] - - def _runMako(self, folder, state, addfiles=['properties']): - """ - Hard coding, maybe a better way possible - addfiles: additional files that need to be included into ECLIPSE/OPM DATA file - """ - super()._runMako(folder, state) - - lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') - for file in addfiles: - if os.path.exists(file + '.mako'): - tmpl = lkup.get_template('%s.mako' % file) - - # use a context and render onto a file - with open('{0}'.format(folder + file), 'w') as f: - ctx = Context(f, **state) - tmpl.render_context(ctx) - - def calc_pem(self, time): - - - def _get_avo_info(self, avo_config=None): - """ - AVO configuration - """ - # list of configuration parameters in the "AVO" section - config_para_list = ['dz', 'tops', 'angle', 'frequency', 'wave_len', 'vp_shale', 'vs_shale', - 'den_shale', 't_min', 't_max', 't_sampling', 'pp_func'] - if 'avo' in self.input_dict: - self.avo_config = {} - for elem in self.input_dict['avo']: - assert elem[0] in config_para_list, f'Property {elem[0]} not supported' - self.avo_config[elem[0]] = elem[1] - - # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later - if isinstance(self.avo_config['angle'], float): - self.avo_config['angle'] = [self.avo_config['angle']] - - # self._get_DZ(file=self.avo_config['dz']) # =>self.DZ - kw_file = {'DZ': self.avo_config['dz'], 'TOPS': self.avo_config['tops']} - self._get_props(kw_file) - self.overburden = self.pem_input['overburden'] - - # make sure that the "pylops" package is installed - # See https://github.com/PyLops/pylops - self.pp_func = getattr(import_module('pylops.avo.avo'), self.avo_config['pp_func']) - - else: - self.avo_config = None - - def _get_props(self, kw_file): - # extract properties (specified by keywords) in (possibly) different files - # kw_file: a dictionary contains "keyword: file" pairs - # Note that all properties are reshaped into the reservoir model dimension (NX, NY, NZ) - # using the "F" order - for kw in kw_file: - file = kw_file[kw] - if file.endswith('.npz'): - with np.load(file) as f: - exec(f'self.{kw} = f[ "{kw}" ]') - self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] - else: - reader = GRDECL_Parser(filename=file) - reader.read_GRDECL() - exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") - self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ - eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') - - def _calc_avo_props(self, dt=0.0005): - # dt is the fine resolution sampling rate - # convert properties in reservoir model to time domain - vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) - vs_shale = self.avo_config['vs_shale'] # scalar value - rho_shale = self.avo_config['den_shale'] # scalar value - - # Two-way travel time of the top of the reservoir - # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer - top_res = 2 * self.TOPS[:, :, 0] / vp_shale - - # Cumulative traveling time trough the reservoir in vertical direction - cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] - # total travel time - cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) - - # add overburden and underburden of Vp, Vs and Density - vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), - self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) - vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), - self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) - rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 - self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) - - # search for the lowest grid cell thickness and sample the time according to - # that grid thickness to preserve the thin layer effect - time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) - if time_sample.shape[0] == 1: - time_sample = time_sample.reshape(-1) - time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) - - vp_sample = np.tile(vp[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - vs_sample = np.tile(vs[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - rho_sample = np.tile(rho[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - - for m in range(self.NX): - for l in range(self.NY): - for k in range(time_sample.shape[2]): - # find the right interval of time_sample[m, l, k] belonging to, and use - # this information to allocate vp, vs, rho - idx = np.searchsorted(cum_time[m, l, :], time_sample[m, l, k], side='left') - idx = idx if idx < len(cum_time[m, l, :]) else len(cum_time[m, l, :]) - 1 - vp_sample[m, l, k] = vp[m, l, idx] - vs_sample[m, l, k] = vs[m, l, idx] - rho_sample[m, l, k] = rho[m, l, idx] - - - # from matplotlib import pyplot as plt - # plt.plot(vp_sample[0, 0, :]) - # plt.show() - - #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) - #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) - #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) - - #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] - #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] - #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] - - #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) - #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg - - # PP reflection coefficients, see, e.g., - # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" - # So far, it seems that "approx_zoeppritz_pp" is the only available option - # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) - avo_data_list = [] - - # Ricker wavelet - wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), - f0=self.avo_config['frequency']) - - # Travel time corresponds to reflectivity sereis - t = time_sample[:, :, 0:-1] - - # interpolation time - t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) - trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) - - # number of pp reflection coefficients in the vertial direction - nz_rpp = vp_sample.shape[2] - 1 - - for i in range(len(self.avo_config['angle'])): - angle = self.avo_config['angle'][i] - Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], - vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) - - for m in range(self.NX): - for l in range(self.NY): - # convolution with the Ricker wavelet - conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") - w_trace = conv_op * Rpp[m, l, :] - - # Sample the trace into regular time interval - f = interp1d(np.squeeze(t[m, l, :]), np.squeeze(w_trace), - kind='nearest', fill_value='extrapolate') - trace_interp[m, l, :] = f(t_interp) - - if i == 0: - avo_data = trace_interp # 3D - elif i == 1: - avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D - else: - avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D - - self.avo_data = avo_data - - @classmethod - def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): - """ - XILU: Quantities read by "EclipseData.cell_data" are put in the axis order of [nz, ny, nx]. To be consisent with - ECLIPSE/OPM custom, we need to change the axis order. We further flatten the array according to the specified order - """ - array = np.array(array) - if len(array.shape) != 1: # if array is a 1D array, then do nothing - assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" - - # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] - new_array = np.transpose(array, axes=[2, 1, 0]) - if flatten: - new_array = new_array.flatten(order=order) - - return new_array - else: - return array \ No newline at end of file diff --git a/simulator/flow_rock_mali.py b/simulator/flow_rock_mali.py deleted file mode 100644 index 6abb95d..0000000 --- a/simulator/flow_rock_mali.py +++ /dev/null @@ -1,1130 +0,0 @@ -from simulator.opm import flow -from importlib import import_module -import datetime as dt -import numpy as np -import os -from misc import ecl, grdecl -import shutil -import glob -from subprocess import Popen, PIPE -import mat73 -from copy import deepcopy -from sklearn.cluster import KMeans -from sklearn.preprocessing import StandardScaler -from mako.lookup import TemplateLookup -from mako.runtime import Context - -# from pylops import avo -from pylops.utils.wavelets import ricker -from pylops.signalprocessing import Convolve1D -from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master -from scipy.interpolate import interp1d -from pipt.misc_tools.analysis_tools import store_ensemble_sim_information -from geostat.decomp import Cholesky -from simulator.eclipse import ecl_100 - -class flow_sim2seis(flow): - """ - Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - self.sats = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurement - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def calc_pem(self, v, time, phases): - - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - for var in ['PRESSURE', 'RS', 'SWAT', 'SGAS']: - tmp = self.ecl_case.cell_data(var, time) - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) # only active, and conv. to float - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] - else: - P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first - - if 'press_conv' in self.pem_input: - P_init = P_init * self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - - # fluid saturations in dictionary - tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} - self.sats.extend([tmp_s]) - - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - - - def setup_fwd_run(self): - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - # need a if to check that we have correct sim2seis - # copy relevant sim2seis files into folder. - for file in glob.glob('sim2seis_config/*'): - shutil.copy(file, 'En_' + str(self.ensemble_member) + os.sep) - - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - - grid = self.ecl_case.grid() - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - self.sats = [] - # loop over seismic vintages - for v, assim_time in enumerate([0.0] + self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - - self.calc_pem(v, time, phases) - - grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { - 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { - 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', - {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - current_folder = os.getcwd() - run_folder = current_folder + os.sep + 'En_' + str(self.ensemble_member) - # The sim2seis is invoked via a shell script. The simulations provides outputs. Run, and get all output. Search - # for Done. If not finished in reasonable time -> kill - p = Popen(['./sim2seis.sh', run_folder], stdout=PIPE) - start = time - while b'done' not in p.stdout.readline(): - pass - - # Todo: handle sim2seis or pem error - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['sim2seis']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - result = mat73.loadmat(f'En_{member}/Data_conv.mat')['data_conv'] - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = np.sum( - np.abs(result[:, :, :, v]), axis=0).flatten() - -class flow_rock(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - self.pem_result = [] - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['bulkimp']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() - -class flow_barycenter(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the - barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are - identified using k-means clustering, and the number of objects are determined using and elbow strategy. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - self.pem_result = [] - self.bar_result = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - if elem[0] == 'clusters': # number of clusters for each barycenter - self.pem_input['clusters'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - # Extract k-means centers and interias for each element in pem_result - if 'clusters' in self.pem_input: - npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) - n_clusters_list = npzfile['n_clusters_list'] - npzfile.close() - else: - n_clusters_list = len(self.pem_result)*[2] - kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} - for i, bulkimp in enumerate(self.pem_result): - std = np.std(bulkimp) - features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) - scaler = StandardScaler() - scaled_features = scaler.fit_transform(features) - kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) - kmeans.fit(scaled_features) - kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements - self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the barycenters and inertias - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['barycenter']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.bar_result[v].flatten() - -class flow_avo(flow_sim2seis): - def __init__(self, input_dict=None, filename=None, options=None, **kwargs): - super().__init__(input_dict, filename, options) - - assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' - self._get_avo_info() - - def setup_fwd_run(self, **kwargs): - self.__dict__.update(kwargs) - - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - """ - Setup and run the AVO forward simulator. - - Parameters - ---------- - state : dict - Dictionary containing the ensemble state. - - member_i : int - Index of the ensemble member. any index < 0 (e.g., -1) means the ground truth in synthetic case studies - - del_folder : bool, optional - Boolean to determine if the ensemble folder should be deleted. Default is False. - """ - - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder - self.ensemble_member = member_i - - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False - - - def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): - # replace the sim2seis part (which is unusable) by avo based on Pylops - - if folder is None: - folder = self.folder - - # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" - if hasattr(self, 'run_reservoir_model'): - run_reservoir_model = self.run_reservoir_model - - if run_reservoir_model is None: - run_reservoir_model = True - - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) - success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) - #ecl = ecl_100(filename=self.file) - #ecl.options = self.options - #success = ecl.call_sim(folder, wait_for_proc) - else: - success = True - - if success: - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - - self.calc_pem() - - grid = self.ecl_case.grid() - - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(multfactor * tmp), dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - tmp = self.ecl_case.cell_data(var, time) - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input[var] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - #pem_input['PRESSURE'] = self._reformat3D_then_flatten(pem_input['PRESSURE'] * - # self.pem_input['press_conv']) - - tmp = self.ecl_case.cell_data('PRESSURE', 0) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] - #P_init = self._reformat3D_then_flatten(self.pem.p_init.reshape(tmp.shape) * np.ones(tmp.shape)) - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - #P_init = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init * self.pem_input['press_conv'] - #P_init = self._reformat3D_then_flatten(P_init * self.pem_input['press_conv']) - - saturations = [ - 1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - #saturations = [self._reformat3D_then_flatten(1 - (pem_input['SWAT'] + pem_input['SGAS'])) - # if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - - # Get the pressure - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - else: - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) - - #grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { - # 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { - # 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', - # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - # vp, vs, density - self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 - - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} - if save_folder is not None: - file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"vp_vs_rho_vint{v}.npz" - else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"vp_vs_rho_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - # avo data - self._calc_avo_props() - - avo = self.avo_data.flatten(order="F") - - # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies - # the corresonding (noisy) data are observations in data assimilation - if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: - non_nan_idx = np.argwhere(~np.isnan(avo)) - data_std = np.std(avo[non_nan_idx]) - if self.input_dict['add_synthetic_noise'][0] == 'snr': - noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std - avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) - else: - noise_std = 0.0 # simulated data don't contain noise - - save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - if save_folder is not None: - file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"avo_vint{v}.npz" - else: - # if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - # (self.ensemble_member >= 0): - # file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - # else folder + f"avo_vint{v}.npz" - # else: - # file_name = os.getcwd() + os.sep + f"avo_vint{v}.npz" - file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"avo_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super(flow_sim2seis, self).extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if 'avo' in key: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ - else self.folder + key + '_vint' + str(idx) + '.npz' - with np.load(filename) as f: - self.pred_data[prim_ind][key] = f[key] - - def _runMako(self, folder, state, addfiles=['properties']): - """ - Hard coding, maybe a better way possible - addfiles: additional files that need to be included into ECLIPSE/OPM DATA file - """ - super()._runMako(folder, state) - - lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') - for file in addfiles: - tmpl = lkup.get_template('%s.mako' % file) - - # use a context and render onto a file - with open('{0}'.format(folder + file), 'w') as f: - ctx = Context(f, **state) - tmpl.render_context(ctx) - - def _get_avo_info(self, avo_config=None): - """ - AVO configuration - """ - # list of configuration parameters in the "AVO" section - config_para_list = ['dz', 'tops', 'angle', 'frequency', 'wave_len', 'vp_shale', 'vs_shale', - 'den_shale', 't_min', 't_max', 't_sampling', 'pp_func'] - if 'avo' in self.input_dict: - self.avo_config = {} - for elem in self.input_dict['avo']: - assert elem[0] in config_para_list, f'Property {elem[0]} not supported' - self.avo_config[elem[0]] = elem[1] - - # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later - if isinstance(self.avo_config['angle'], float): - self.avo_config['angle'] = [self.avo_config['angle']] - - # self._get_DZ(file=self.avo_config['dz']) # =>self.DZ - kw_file = {'DZ': self.avo_config['dz'], 'TOPS': self.avo_config['tops']} - self._get_props(kw_file) - self.overburden = self.pem_input['overburden'] - - # make sure that the "pylops" package is installed - # See https://github.com/PyLops/pylops - self.pp_func = getattr(import_module('pylops.avo.avo'), self.avo_config['pp_func']) - - else: - self.avo_config = None - - def _get_props(self, kw_file): - # extract properties (specified by keywords) in (possibly) different files - # kw_file: a dictionary contains "keyword: file" pairs - # Note that all properties are reshaped into the reservoir model dimension (NX, NY, NZ) - # using the "F" order - for kw in kw_file: - file = kw_file[kw] - if file.endswith('.npz'): - with np.load(file) as f: - exec(f'self.{kw} = f[ "{kw}" ]') - self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] - else: - reader = GRDECL_Parser(filename=file) - reader.read_GRDECL() - exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") - self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ - eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') - - def _calc_avo_props(self, dt=0.0005): - # dt is the fine resolution sampling rate - # convert properties in reservoir model to time domain - vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) - vs_shale = self.avo_config['vs_shale'] # scalar value - rho_shale = self.avo_config['den_shale'] # scalar value - - # Two-way travel time of the top of the reservoir - # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer - top_res = 2 * self.TOPS[:, :, 0] / vp_shale - - # Cumulative traveling time trough the reservoir in vertical direction - cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] - cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) - - # add overburden and underburden of Vp, Vs and Density - vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), - self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) - vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), - self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) - rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 - self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) - - # search for the lowest grid cell thickness and sample the time according to - # that grid thickness to preserve the thin layer effect - time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) - if time_sample.shape[0] == 1: - time_sample = time_sample.reshape(-1) - time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) - - vp_sample = np.tile(vp[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - vs_sample = np.tile(vs[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - rho_sample = np.tile(rho[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - - for m in range(self.NX): - for l in range(self.NY): - for k in range(time_sample.shape[2]): - # find the right interval of time_sample[m, l, k] belonging to, and use - # this information to allocate vp, vs, rho - idx = np.searchsorted(cum_time[m, l, :], time_sample[m, l, k], side='left') - idx = idx if idx < len(cum_time[m, l, :]) else len(cum_time[m, l, :]) - 1 - vp_sample[m, l, k] = vp[m, l, idx] - vs_sample[m, l, k] = vs[m, l, idx] - rho_sample[m, l, k] = rho[m, l, idx] - - - # from matplotlib import pyplot as plt - # plt.plot(vp_sample[0, 0, :]) - # plt.show() - - #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) - #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) - #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) - - #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] - #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] - #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] - - #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) - #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg - - # PP reflection coefficients, see, e.g., - # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" - # So far, it seems that "approx_zoeppritz_pp" is the only available option - # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) - avo_data_list = [] - - # Ricker wavelet - wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), - f0=self.avo_config['frequency']) - - # Travel time corresponds to reflectivity sereis - t = time_sample[:, :, 0:-1] - - # interpolation time - t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) - trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) - - # number of pp reflection coefficients in the vertial direction - nz_rpp = vp_sample.shape[2] - 1 - - for i in range(len(self.avo_config['angle'])): - angle = self.avo_config['angle'][i] - Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], - vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) - - for m in range(self.NX): - for l in range(self.NY): - # convolution with the Ricker wavelet - conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") - w_trace = conv_op * Rpp[m, l, :] - - # Sample the trace into regular time interval - f = interp1d(np.squeeze(t[m, l, :]), np.squeeze(w_trace), - kind='nearest', fill_value='extrapolate') - trace_interp[m, l, :] = f(t_interp) - - if i == 0: - avo_data = trace_interp # 3D - elif i == 1: - avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D - else: - avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D - - self.avo_data = avo_data - - @classmethod - def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): - """ - XILU: Quantities read by "EclipseData.cell_data" are put in the axis order of [nz, ny, nx]. To be consisent with - ECLIPSE/OPM custom, we need to change the axis order. We further flatten the array according to the specified order - """ - array = np.array(array) - if len(array.shape) != 1: # if array is a 1D array, then do nothing - assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" - - # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] - new_array = np.transpose(array, axes=[2, 1, 0]) - if flatten: - new_array = new_array.flatten(order=order) - - return new_array - else: - return array \ No newline at end of file diff --git a/simulator/rockphysics/softsandrp.py b/simulator/rockphysics/softsandrp.py index 7b7f652..207237c 100644 --- a/simulator/rockphysics/softsandrp.py +++ b/simulator/rockphysics/softsandrp.py @@ -6,7 +6,10 @@ import numpy as np import sys import multiprocessing as mp - +from CoolProp.CoolProp import PropsSI # http://coolprop.org/#high-level-interface-example +import CoolProp.CoolProp as CP +# Density of carbon dioxide at 100 bar and 25C # Smeaheia 37 degrees C +#rho_co2 = PropsSI('D', 'T', 298.15, 'P', 100e5, 'CO2') from numpy.random import poisson # internal load @@ -192,13 +195,15 @@ def calc_props(self, phases, saturations, pressure, # Calculate fluid properties # if dens is None: + densf_SI = self._fluid_densSIprop(self.phases, + saturations[i, :], pressure[i]) densf, bulkf = \ self._fluidprops_Wood(self.phases, saturations[i, :], pressure[i], Rs[i]) else: densf = self._fluid_dens(saturations[i, :], dens[i, :]) - bulkf = self._fluidprops_Brie(self.phases, saturations[i, :], pressure[i]) + bulkf = self._fluidprops_Brie(self.phases, saturations[i, :], pressure[i], densf) # #denss, bulks, shears = \ # self._solidprops(porosity[i], ntg[i], i) @@ -325,6 +330,35 @@ def getPorosity(self): # Fluid properties start # =================================================== # + def _fluid_densSIprop(self, phases, fsats, press, t= 37, CO2 = None): + + conv2Pa = 1e6 # MPa to Pa + ta = t + 273.15 # absolute temp in K + # fluid densities + fdens = 0.0 + + for i in range(len(phases)): + # + # Calculate mixture properties by summing + # over individual phase properties + # + + var = phases[i] + if var == 'GAS' and CO2 is None: + pdens = PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'Methane') + elif var == 'GAS' and CO2 is True: + pdens = PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'CO2') + elif var == 'OIL': + CP.get_global_param_string('predefined_mixtures').split(',')[0:6] + #pdens = CP.PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'Ekofisk.mix') + pdens = CP.PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'butane') + elif var == 'WAT': + pdens = PropsSI('D', 'T|liquid', ta, 'P', press * conv2Pa, 'Water') + + fdens = fdens + fsats[i] * abs(pdens) + + return fdens + def _fluidprops_Wood(self, fphases, fsats, fpress, Rs=None): # # Calculate fluid density and bulk modulus @@ -372,7 +406,7 @@ def _fluid_dens(self, fsatsp, fdensp): fdens = sum(fsatsp * fdensp) return fdens - def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): + def _fluidprops_Brie(self, fphases, fsats, fpress, fdens, Rs=None, e = 5): # # Calculate fluid density and bulk modulus BRIE et al. 1995 # Assumes two phases liquid and gas @@ -382,6 +416,7 @@ def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): # and/or Water and/or Gas # fsats - fluid saturation values for # fluid phases in "fphases" + # fdens - fluid density for given pressure and temperature # fpress - fluid pressure value (MPa) # Rs - Gas oil ratio. Default value None # e - Brie's exponent (e= 5 Utsira sand filled with brine and CO2 @@ -402,9 +437,9 @@ def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): # if fphases[i].lower() in ["oil", "wat"]: fsatsl = fsats[i] - pbulkl = self._phaseprops_Smeaheia(fphases[i], fpress, Rs) + pbulkl = self._phaseprops_Smeaheia(fphases[i], fpress, fdens, Rs) elif fphases[i].lower() in ["gas"]: - pbulkg = self._phaseprops_Smeaheia(fphases[i], fpress, Rs) + pbulkg = self._phaseprops_Smeaheia(fphases[i], fpress, fdens, Rs) fbulk = (pbulkl - pbulkg) * (fsatsl)**e + pbulkg @@ -414,7 +449,53 @@ def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): # # --------------------------------------------------- # - def _phaseprops_Smeaheia(self, fphase, press, Rs=None): + @staticmethod + def pseudo_p_t(pres, t, gs): + """Calculate the pseudoreduced temperature and pressure according to Thomas et al. 1970. + + Parameters + ---------- + pres : float or array-like + Pressure in MPa + t : float or array-like + Temperature in °C + gs : float + Gas gravity + + Returns + ------- + float or array-like + Ta: absolute temperature + Ppr:pseudoreduced pressure + Tpr:pseudoreduced temperature + """ + + # convert the temperature to absolute temperature + ta = t + 273.15 + p_pr = pres / (4.892 - 0.4048 * gs) + t_pr = ta / (94.72 + 170.75 * gs) + return ta, p_pr, t_pr + # + # --------------------------------------------------- + # + @staticmethod + def dz_dp(p_pr, t_pr): + """Values for dZ/dPpr obtained from equation 10b in Batzle and Wang (1992). + """ + # analytic + dz_dp = (0.03 + 0.00527 * (3.5 - t_pr) ** 3) + 0.109 * (3.85 - t_pr) ** 2 * 1.2 * p_pr ** 0.2 * -( + 0.45 + 8 * (0.56 - 1 / t_pr) ** 2) / t_pr * np.exp( + -(0.45 + 8 * (0.56 - 1 / t_pr) ** 2) * p_pr ** 1.2 / t_pr) + + # numerical approximation + # dzdp= 1.938783*P_pr**0.2*(1 - 0.25974025974026*T_pr)**2*(-8*(0.56 - 1/T_pr)**2 - 0.45)* + # np.exp(P_pr**1.2*(-8*(0.56 - 1/T_pr)**2 - 0.45)/T_pr)/T_pr + 0.22595125*(1 - 0.285714285714286*T_pr)**3 + # + 0.03 + return dz_dp + # + #----------------------------------------------------------- + # + def _phaseprops_Smeaheia(self, fphase, press, fdens, Rs=None, t = 37, CO2 = None): # # Calculate properties for a single fluid phase # @@ -422,6 +503,8 @@ def _phaseprops_Smeaheia(self, fphase, press, Rs=None): # Input # fphase - fluid phase; Oil, Water or Gas # press - fluid pressure value (MPa) + # fdens - fluid density (kg/m3) + # t - temperature in degrees C # # Output # pbulk - bulk modulus of fluid phase @@ -429,42 +512,96 @@ def _phaseprops_Smeaheia(self, fphase, press, Rs=None): # "press" (MPa) # # ----------------------------------------------- - # - if fphase.lower() == "wat": # refers to water in Smeaheia - press_range = np.array([0.10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) - # Bo values assume Rs = 0 - Bo_values = np.array( - [1.00469, 1.00430, 1.00387, 1.00345, 1.00302, 1.00260, 1.00218, 1.00176, 1.00134, 1.00092, 1.00050, - 1.00008, 0.99967, 0.99925, 0.99884, 0.99843, 0.99802, 0.99761, 0.99720, 0.99679, 0.99638]) + # References + # ---------- + # Xu, H. (2006). Calculation of CO2 acoustic properties using Batzle-Wang equations. Geophysics, 71(2), F21-F23. + # """ + + if fphase.lower() == "wat": # refers to pure water or brine + #Compute the bulk modulus of pure water as a function of temperature and pressure + #using Batzle and Wang (1992). + if np.any(press > 100): + print('pressures above about 100 MPa-> inaccurate estimations of water velocity') + w = np.array([[1.40285e+03, 1.52400e+00, 3.43700e-03, -1.19700e-05], + [4.87100e+00, -1.11000e-02, 1.73900e-04, -1.62800e-06], + [-4.78300e-02, 2.74700e-04, -2.13500e-06, 1.23700e-08], + [1.48700e-04, -6.50300e-07, -1.45500e-08, 1.32700e-10], + [-2.19700e-07, 7.98700e-10, 5.23000e-11, -4.61400e-13]]) + v_w = sum(w[i, j] * t ** i * press ** j for i in range(5) for j in range(4)) # m/s + K_w = fdens * v_w ** 2 * 1e-6 + if CO2 is True: # refers to brine + salinity = 35000 / 1000000 + s1 = 1170 - 9.6 * t + 0.055 * t ** 2 - 8.5e-5 * t ** 3 + 2.6 * press - 0.0029 * t * press - 0.0476 * press ** 2 + s15 = 780 - 10 * press + 0.16 * press ** 2 + s2 = -820 + v_b = v_w + s1 * salinity + s15 * salinity ** 1.5 + s2 * salinity ** 2 + x = 300 * press - 2400 * press * salinity + t * (80 + 3 * t - 3300 * salinity - 13 * press + 47 * press * salinity) + rho_b = fdens + salinity * (0.668 + 0.44 * salinity + 1e-6 * x) + pbulk = rho_b * v_b ** 2 * 1e-6 + else: + pbulk = K_w + elif fphase.lower() == "gas" and CO2 is True: # refers to CO2 + R = 8.3145 # J.mol-1K-1 gas constant for CO2 + gs = 1.5189 # Specific gravity #https://www.engineeringtoolbox.com/specific-gravities-gases-d_334.html + ta, p_pr, t_pr = self.pseudo_p_t(press, t, gs) + + E = 0.109 * (3.85 - t_pr) ** 2 * np.exp(-(0.45 + 8 * (0.56 - 1 / t_pr) ** 2) * p_pr ** 1.2 / t_pr) + Z = (0.03 + 0.00527 * (3.5 - t_pr) ** 3) * p_pr + (0.642 * t_pr - 0.007 * t_pr ** 4 - 0.52) + E + rho = 28.8 * gs * press / (Z * R * ta) # g/cm3 + + r_0 = 0.85 + 5.6 / (p_pr + 2) + 27.1 / (p_pr + 3.5) ** 2 - 8.7 * np.exp(-0.65 * (p_pr + 1)) + dz_dp = self.dz_dp(p_pr, t_pr) + pbulk = press / (1 - p_pr * dz_dp / Z) * r_0 + + pbulk_test = self.test_new_implementation(press) + print(np.max(pbulk-pbulk_test)) + + elif fphase.lower() == "gas": # refers to Methane + gs = 0.5537 #https://www.engineeringtoolbox.com/specific-gravities-gases-d_334.html + R = 8.3145 # J.mol-1K-1 gas constant + ta, p_pr, t_pr = self.pseudo_p_t(press, t, gs) + E = 0.109 * (3.85 - t_pr) ** 2 * np.exp(-(0.45 + 8 * (0.56 - 1 / t_pr) ** 2) * p_pr ** 1.2 / t_pr) + Z = (0.03 + 0.00527 * (3.5 - t_pr) ** 3) * p_pr + (0.642 * t_pr - 0.007 * t_pr ** 4 - 0.52) + E + rho = 28.8 * gs * press / (Z * R * ta) # g/cm3 + + r_0 = 0.85 + 5.6 / (p_pr + 2) + 27.1 / (p_pr + 3.5) ** 2 - 8.7 * np.exp(-0.65 * (p_pr + 1)) + dz_dp = self.dz_dp(p_pr, t_pr) + pbulk = press / (1 - p_pr * dz_dp / Z) * r_0 + + elif fphase.lower() == "oil": #pure oil + # Estimate the oil bulk modulus at specific temperature and pressure. + v = 2096 * (fdens / (2600 - fdens)) ** 0.5 - 3.7 * t + 4.64 * press + 0.0115 * ( + 4.12 * (1080 / fdens - 1) ** 0.5 - 1) * t * press + pbulk = fdens * v ** 2 - elif fphase.lower() == "gas": - # Values from .DATA file for Smeaheia (converted to MPa) - press_range = np.array( - [0.101, 0.885, 1.669, 2.453, 3.238, 4.022, 4.806, 5.590, 6.2098, 7.0899, 7.6765, 8.2630, 8.8495, 9.4359, - 10.0222, 10.6084, 11.1945, 14.7087, 17.6334, 20.856, 23.4695, 27.5419]) # Example pressures in MPa - Bo_values = np.array( - [1.07365, 0.11758, 0.05962, 0.03863, 0.02773, 0.02100, 0.01639, 0.01298, 0.010286, 0.007578, 0.005521, - 0.003314, 0.003034, 0.002919, 0.002851, 0.002802, 0.002766, 0.002648, 0.002599, 0.002566, 0.002546, - 0.002525]) # Example formation volume factors in m^3/kg + + # + return pbulk + + # + def test_new_implementation(self, press): + # Values from .DATA file for Smeaheia (converted to MPa) + press_range = np.array( + [0.101, 0.885, 1.669, 2.453, 3.238, 4.022, 4.806, 5.590, 6.2098, 7.0899, 7.6765, 8.2630, 8.8495, 9.4359, + 10.0222, 10.6084, 11.1945, 14.7087, 17.6334, 20.856, 23.4695, 27.5419]) # Example pressures in MPa + Bo_values = np.array( + [1.07365, 0.11758, 0.05962, 0.03863, 0.02773, 0.02100, 0.01639, 0.01298, 0.010286, 0.007578, 0.005521, + 0.003314, 0.003034, 0.002919, 0.002851, 0.002802, 0.002766, 0.002648, 0.002599, 0.002566, 0.002546, + 0.002525]) # Example formation volume factors in m^3/kg # Calculate numerical derivative of Bo with respect to Pressure dBo_dP = - np.gradient(Bo_values, press_range) # Calculate isothermal compressibility (van der Waals) compressibility = (1 / Bo_values) * dBo_dP # Resulting array of compressibility values bulk_mod = 1 / compressibility - # # Find the index of the closest pressure value in b closest_index = (np.abs(press_range - press)).argmin() # Extract the corresponding value from a - pbulk = bulk_mod[closest_index] - - # - return pbulk - - # + pbulk_test = bulk_mod[closest_index] + return pbulk_test def _phaseprops(self, fphase, press, Rs=None): # diff --git a/simulator/rockphysics/standardrp.py b/simulator/rockphysics/standardrp.py index e05afd3..dde7b09 100644 --- a/simulator/rockphysics/standardrp.py +++ b/simulator/rockphysics/standardrp.py @@ -216,8 +216,7 @@ def calc_props(self, phases, saturations, pressure, # # Density # - self.dens[i] = (porosity[i]*densf + - (1-porosity[i])*denss) + self.dens[i] = (porosity[i]*densf + (1-porosity[i])*denss) # # Moduli # From afb2155ed27a8fc122283760427a44d447cdb8a0 Mon Sep 17 00:00:00 2001 From: mlienorce Date: Tue, 20 May 2025 16:10:34 +0200 Subject: [PATCH 10/15] subsidence (#96) --- ensemble/ensemble.py | 8 +- simulator/flow_rock.py | 1262 +++++++++++++++++++++++---- simulator/flow_rock_backup.py | 1120 ------------------------ simulator/flow_rock_mali.py | 1130 ------------------------ simulator/rockphysics/softsandrp.py | 195 ++++- simulator/rockphysics/standardrp.py | 3 +- 6 files changed, 1243 insertions(+), 2475 deletions(-) delete mode 100644 simulator/flow_rock_backup.py delete mode 100644 simulator/flow_rock_mali.py diff --git a/ensemble/ensemble.py b/ensemble/ensemble.py index 130b96a..66bd3ce 100644 --- a/ensemble/ensemble.py +++ b/ensemble/ensemble.py @@ -376,16 +376,16 @@ def _ext_prior_info(self): if isinstance(limits[0], list) and len(limits) < nz or \ not isinstance(limits[0], list) and len(limits) < 2 * nz: # Check if it is more than one entry and give error - assert (isinstance(limits[0], list) and len(limits) == 1), \ - 'Information from LIMITS has been given for {0} layers, whereas {1} is needed!' \ - .format(len(limits), nz) + #assert (isinstance(limits[0], list) and len(limits) == 1), \ + # 'Information from LIMITS has been given for {0} layers, whereas {1} is needed!' \ + # .format(len(limits), nz) assert (not isinstance(limits[0], list) and len(limits) == 2), \ 'Information from LIMITS has been given for {0} layers, whereas {1} is needed!' \ .format(len(limits) / 2, nz) # Only 1 entry; copy this to all layers print( - '\033[1;33mSingle entry for RANGE will be copied to all {0} layers\033[1;m'.format(nz)) + '\033[1;33mSingle entry for LIMITS will be copied to all {0} layers\033[1;m'.format(nz)) self.prior_info[name]['limits'] = [limits] * nz else: # 2D grid only, or optimization case diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index e55b37d..9bec3ad 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -15,6 +15,10 @@ from copy import deepcopy from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler +from scipy.optimize import fsolve +from scipy.special import jv # Bessel function of the first kind +from scipy.integrate import quad +from scipy.special import j0 from mako.lookup import TemplateLookup from mako.runtime import Context @@ -28,7 +32,7 @@ from pipt.misc_tools.analysis_tools import store_ensemble_sim_information from geostat.decomp import Cholesky from simulator.eclipse import ecl_100 - +from CoolProp.CoolProp import PropsSI # http://coolprop.org/#high-level-interface-example class flow_rock(flow): """ @@ -136,6 +140,7 @@ def calc_pem(self, time, time_index=None): phases = phases.split() pem_input = {} + tmp_dyn_var = {} # get active porosity tmp = self._get_pem_input('PORO') # self.ecl_case.cell_data('PORO') if 'compaction' in self.pem_input: @@ -166,7 +171,8 @@ def calc_pem(self, time, time_index=None): tmp = self._get_pem_input('PRESSURE', time_input) pem_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) - if 'press_conv' in self.pem_input: + # convert pressure from Bar to MPa + if 'press_conv' in self.pem_input and time_input == time: pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] if hasattr(self.pem, 'p_init'): @@ -174,7 +180,7 @@ def calc_pem(self, time, time_index=None): else: P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first - if 'press_conv' in self.pem_input: + if 'press_conv' in self.pem_input and time_input == time: P_init = P_init * self.pem_input['press_conv'] # extract saturations @@ -183,20 +189,38 @@ def calc_pem(self, time, time_index=None): if var in ['WAT', 'GAS']: tmp = self._get_pem_input('S{}'.format(var), time_input) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + pem_input['S{}'.format(var)] = np.clip(pem_input['S{}'.format(var)], 0, 1) - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] + pem_input['SOIL'] = np.clip(1 - (pem_input['SWAT'] + pem_input['SGAS']), 0, 1) + saturations = [ np.clip(1 - (pem_input['SWAT'] + pem_input['SGAS']), 0, 1) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model using OPM CO2Store for var in phases: if var in ['GAS']: tmp = self._get_pem_input('S{}'.format(var), time_input) pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + pem_input['S{}'.format(var)] = np.clip(pem_input['S{}'.format(var)] , 0, 1) + pem_input['SWAT'] = 1 - pem_input['SGAS'] saturations = [1 - (pem_input['SGAS']) if ph == 'WAT' else pem_input['S{}'.format(ph)] for ph in phases] + + elif 'OIL' in phases and 'GAS' in phases: # Original Smeaheia model + for var in phases: + if var in ['GAS']: + tmp = self._get_pem_input('S{}'.format(var), time_input) + pem_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + pem_input['S{}'.format(var)] = np.clip(pem_input['S{}'.format(var)], 0, 1) + pem_input['SOIL'] = 1 - pem_input['SGAS'] + saturations = [1 - (pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] + else: + print('Type and number of fluids are unspecified in calc_pem') # fluid saturations in dictionary - tmp_dyn_var = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + # tmp_dyn_var = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + for var in phases: + tmp_dyn_var[f'S{var}'] = pem_input[f'S{var}'] + tmp_dyn_var['PRESSURE'] = pem_input['PRESSURE'] self.dyn_var.extend([tmp_dyn_var]) @@ -735,8 +759,14 @@ def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i +<<<<<<< Updated upstream return super().run_fwd_sim(state, member_i, del_folder=del_folder) +======= + #return super().run_fwd_sim(state, member_i, del_folder=del_folder) + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=del_folder) + return self.pred_data +>>>>>>> Stashed changes def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): # replace the sim2seis part (which is unusable) by avo based on Pylops @@ -748,6 +778,7 @@ def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, s if not self.no_flow: # call call_sim in flow class (skip flow_rock, go directly to flow which is a parent of flow_rock) success = super(flow_rock, self).call_sim(folder, wait_for_proc) + #success = True else: success = True @@ -777,19 +808,32 @@ def get_avo_result(self, folder, save_folder): base_time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + dt.timedelta(days=self.pem_input['baseline']) + self.calc_pem(base_time,0) - # vp, vs, density in reservoir - self.calc_velocities(folder, save_folder, grid, -1, f_dim) - # avo data - # self._calc_avo_props() - self._calc_avo_props_active_cells(grid) + if not self.no_flow: + # vp, vs, density in reservoir + vp, vs, rho = self.calc_velocities(folder, save_folder, grid, 0, f_dim) - avo_baseline = self.avo_data.flatten(order="F") - Rpp_baseline = self.Rpp - vs_baseline = self.vs_sample - vp_baseline = self.vp_sample - rho_baseline = self.rho_sample + # avo data + # self._calc_avo_props() + avo_data, Rpp, vp_sample, vs_sample, rho_sample = self._calc_avo_props_active_cells(grid, vp, vs, rho) + + avo_baseline = avo_data.flatten(order="F") + Rpp_baseline = Rpp + vs_baseline = vs_sample + vp_baseline = vp_sample + rho_baseline = rho_sample + print('OPM flow is used') + else: + file_name = f"avo_vint0_{folder}.npz" if folder[-1] != os.sep \ + else f"avo_vint0_{folder[:-1]}.npz" + + avo_baseline = np.load(file_name, allow_pickle=True)['avo_bl'] + Rpp_baseline = np.load(file_name, allow_pickle=True)['Rpp_bl'] + vs_baseline = np.load(file_name, allow_pickle=True)['vs_bl'] + vp_baseline = np.load(file_name, allow_pickle=True)['vp_bl'] + rho_baseline = np.load(file_name, allow_pickle=True)['rho_bl'] vintage = [] # loop over seismic vintages @@ -801,26 +845,28 @@ def get_avo_result(self, folder, save_folder): self.calc_pem(time, v+1) # vp, vs, density in reservoir - self.calc_velocities(folder, save_folder, grid, v, f_dim) + vp, vs, rho = self.calc_velocities(folder, save_folder, grid, v+1, f_dim) # avo data #self._calc_avo_props() - self._calc_avo_props_active_cells(grid) + avo_data, Rpp, vp_sample, vs_sample, rho_sample = self._calc_avo_props_active_cells(grid, vp, vs, rho) - avo = self.avo_data.flatten(order="F") + avo = avo_data.flatten(order="F") # MLIE: implement 4D avo if 'baseline' in self.pem_input: # 4D measurement avo = avo - avo_baseline - Rpp = self.Rpp - Rpp_baseline - Vs = self.vs_sample - vs_baseline - Vp = self.vp_sample - vp_baseline - rho = self.rho_sample - rho_baseline - else: - Rpp = self.Rpp - Vs = self.vs_sample - Vp = self.vp_sample - rho = self.rho_sample + #Rpp = self.Rpp - Rpp_baseline + #Vs = self.vs_sample - vs_baseline + #Vp = self.vp_sample - vp_baseline + #rho = self.rho_sample - rho_baseline + print('Time-lapse avo') + #else: + # Rpp = self.Rpp + # Vs = self.vs_sample + # Vp = self.vp_sample + # rho = self.rho_sample + # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies @@ -834,82 +880,103 @@ def get_avo_result(self, folder, save_folder): else: noise_std = 0.0 # simulated data don't contain noise + vintage.append(deepcopy(avo)) + + #save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': Vs, 'Vp': Vp, 'rho': rho, **self.avo_config} + save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, 'rho': rho_sample, #**self.avo_config, + 'vs_bl': vs_baseline, 'vp_bl': vp_baseline, 'avo_bl': avo_baseline, 'Rpp_bl': Rpp_baseline, 'rho_bl': rho_baseline, **self.avo_config} if save_folder is not None: file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"avo_vint{v}.npz" + #np.savez(file_name, **save_dic) else: file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ else folder + f"avo_vint{v}.npz" - + file_name_rec = 'Ensemble_results/' + f"avo_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"avo_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) # with open(file_name, "wb") as f: # dump(**save_dic, f) np.savez(file_name, **save_dic) + # 4D response + self.avo_result = [] + for i, elem in enumerate(vintage): + self.avo_result.append(elem) + def calc_velocities(self, folder, save_folder, grid, v, f_dim): # The properties in pem are only given in the active cells # indices of active cells: +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes true_indices = np.where(grid['ACTNUM']) # Alt 2 if len(self.pem.getBulkVel()) == len(true_indices[0]): #self.vp = np.full(f_dim, self.avo_config['vp_shale']) - self.vp = np.full(f_dim, np.nan) - self.vp[true_indices] = (self.pem.getBulkVel()) + vp = np.full(f_dim, np.nan) + vp[true_indices] = (self.pem.getBulkVel()) #self.vs = np.full(f_dim, self.avo_config['vs_shale']) - self.vs = np.full(f_dim, np.nan) - self.vs[true_indices] = (self.pem.getShearVel()) + vs = np.full(f_dim, np.nan) + vs[true_indices] = (self.pem.getShearVel()) #self.rho = np.full(f_dim, self.avo_config['den_shale']) - self.rho = np.full(f_dim, np.nan) - self.rho[true_indices] = (self.pem.getDens()) + rho = np.full(f_dim, np.nan) + rho[true_indices] = (self.pem.getDens()) else: - self.vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') - self.vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ))#, order='F') + # option not used for Box or smeaheia--needs to be tested + vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') + vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') + rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ))#, order='F') ## Debug - self.bulkmod = np.full(f_dim, np.nan) - self.bulkmod[true_indices] = self.pem.getBulkMod() - self.shearmod = np.full(f_dim, np.nan) - self.shearmod[true_indices] = self.pem.getShearMod() - self.poverburden = np.full(f_dim, np.nan) - self.poverburden[true_indices] = self.pem.getOverburdenP() - self.pressure = np.full(f_dim, np.nan) - self.pressure[true_indices] = self.pem.getPressure() - self.peff = np.full(f_dim, np.nan) - self.peff[true_indices] = self.pem.getPeff() - self.porosity = np.full(f_dim, np.nan) - self.porosity[true_indices] = self.pem.getPorosity() - + #self.bulkmod = np.full(f_dim, np.nan) + #self.bulkmod[true_indices] = self.pem.getBulkMod() + #self.shearmod = np.full(f_dim, np.nan) + #self.shearmod[true_indices] = self.pem.getShearMod() + #self.poverburden = np.full(f_dim, np.nan) + #self.poverburden[true_indices] = self.pem.getOverburdenP() + #self.pressure = np.full(f_dim, np.nan) + #self.pressure[true_indices] = self.pem.getPressure() + #self.peff = np.full(f_dim, np.nan) + #self.peff[true_indices] = self.pem.getPeff() + porosity = np.full(f_dim, np.nan) + porosity[true_indices] = self.pem.getPorosity() + if self.dyn_var: + sgas = np.full(f_dim, np.nan) + sgas[true_indices] = self.dyn_var[v]['SGAS'] + #soil = np.full(f_dim, np.nan) + #soil[true_indices] = self.dyn_var[v]['SOIL'] + pdyn = np.full(f_dim, np.nan) + pdyn[true_indices] = self.dyn_var[v]['PRESSURE'] # + if self.dyn_var is None: + save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, + #'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': porosity} # for debugging + else: + save_dic = {'vp': vp, 'vs': vs, 'rho': rho, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.rho, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, 'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': self.porosity} if save_folder is not None: file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"vp_vs_rho_vint{v}.npz" + np.savez(file_name, **save_dic) else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"vp_vs_rho_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" - - # with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - + file_name_rec = 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) + # for debugging + return vp, vs, rho def extract_data(self, member): # start by getting the data from the flow simulator super(flow_rock, self).extract_data(member) - # get the sim2seis from file + # get the avo from file for prim_ind in self.l_prim: # Loop over all keys in pred_data (all data types) for key in self.all_data_types: @@ -920,6 +987,9 @@ def extract_data(self, member): else self.folder + key + '_vint' + str(idx) + '.npz' with np.load(filename) as f: self.pred_data[prim_ind][key] = f[key] + # + #v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + #self.pred_data[prim_ind][key] = self.avo_result[v].flatten() def _get_avo_info(self, avo_config=None): """ @@ -932,6 +1002,8 @@ def _get_avo_info(self, avo_config=None): self.avo_config = {} for elem in self.input_dict['avo']: assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + if elem[0] == 'vintage' and not isinstance(elem[1], list): + elem[1] = [elem[1]] self.avo_config[elem[0]] = elem[1] # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later @@ -1067,8 +1139,131 @@ def _calc_avo_props(self, dt=0.0005): self.avo_data = avo_data + def _calc_avo_props_active_cells(self, grid, vp, vs, rho, dt=0.0005): + # dt is the fine resolution sampling rate + # convert properties in reservoir model to time domain + vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) + vs_shale = self.avo_config['vs_shale'] # scalar value + rho_shale = self.avo_config['den_shale'] # scalar value + + + actnum = grid['ACTNUM'] + # Find indices where the boolean array is True + active_indices = np.where(actnum) + # # # + + # Two-way travel time tp the top of the reservoir + # Use cell depths of top layer + zcorn = grid['ZCORN'] + + c, a, b = active_indices + + # Two-way travel time tp the top of the reservoir + top_res = 2 * zcorn[0, 0, :, 0, :, 0] / vp_shale + + # depth difference between cells in z-direction: + depth_differences = np.diff(zcorn[:, 0, :, 0, :, 0] , axis=0) + # Extract the last layer + last_layer = depth_differences[-1, :, :] + # Reshape to ensure it has the same number of dimensions + last_layer = last_layer.reshape(1, depth_differences.shape[1], depth_differences.shape[2]) + # Concatenate to the original array along the first axis + extended_differences = np.concatenate([depth_differences, last_layer], axis=0) + + # Cumulative traveling time through the reservoir in vertical direction + #cum_time_res = 2 * zcorn[:, 0, :, 0, :, 0] / self.vp + top_res[np.newaxis, :, :] + cum_time_res = np.cumsum(2 * extended_differences / vp, axis=0) + top_res[np.newaxis, :, :] + # assumes under burden to be constant. No reflections from under burden. Hence set travel time to under burden very large + underburden = top_res + np.nanmax(cum_time_res) + + # total travel time + # cum_time = np.concat enate((top_res[:, :, np.newaxis], cum_time_res), axis=2) + cum_time = np.concatenate((top_res[np.newaxis, :, :], cum_time_res, underburden[np.newaxis, :, :]), axis=0) + + # add overburden and underburden values for Vp, Vs and Density + vp = np.concatenate((vp_shale * np.ones((1, self.NY, self.NX)), + vp, vp_shale * np.ones((1, self.NY, self.NX))), axis=0) + vs = np.concatenate((vs_shale * np.ones((1, self.NY, self.NX)), + vs, vs_shale * np.ones((1, self.NY, self.NX))), axis=0) + rho = np.concatenate((rho_shale * np.ones((1, self.NY, self.NX)), + rho, rho_shale * np.ones((1, self.NY, self.NX))), axis=0) + + + # Combine a and b into a 2D array (each column represents a vector) + ab = np.column_stack((a, b)) + + # Extract unique rows and get the indices of those unique rows + unique_rows, indices = np.unique(ab, axis=0, return_index=True) + + # search for the lowest grid cell thickness and sample the time according to + # that grid thickness to preserve the thin layer effect + time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) + if time_sample.shape[0] == 1: + time_sample = time_sample.reshape(-1) + time_sample = np.tile(time_sample, (len(indices), 1)) + + vp_sample = np.zeros((len(indices), time_sample.shape[1])) + vs_sample = np.zeros((len(indices), time_sample.shape[1])) + rho_sample = np.zeros((len(indices), time_sample.shape[1])) + + for ind in range(len(indices)): + for k in range(time_sample.shape[1]): + # find the right interval of time_sample[m, l, k] belonging to, and use + # this information to allocate vp, vs, rho + idx = np.searchsorted(cum_time[:, a[indices[ind]], b[indices[ind]]], time_sample[ind, k], side='left') + idx = idx if idx < len(cum_time[:, a[indices[ind]], b[indices[ind]]]) else len( + cum_time[:,a[indices[ind]], b[indices[ind]]]) - 1 + vp_sample[ind, k] = vp[idx, a[indices[ind]], b[indices[ind]]] + vs_sample[ind, k] = vs[idx, a[indices[ind]], b[indices[ind]]] + rho_sample[ind, k] = rho[idx, a[indices[ind]], b[indices[ind]]] + + # Ricker wavelet + wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len']-dt, dt), + f0=self.avo_config['frequency']) + + # Travel time corresponds to reflectivity series + t = time_sample[:, 0:-1] + + # interpolation time + t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) + trace_interp = np.zeros((len(indices), len(t_interp))) + + # number of pp reflection coefficients in the vertical direction + nz_rpp = vp_sample.shape[1] - 1 + conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + + avo_data = [] + Rpp = [] + for i in range(len(self.avo_config['angle'])): + angle = self.avo_config['angle'][i] + Rpp = self.pp_func(vp_sample[:, :-1], vs_sample[:, :-1], rho_sample[:, :-1], + vp_sample[:, 1:], vs_sample[:, 1:], rho_sample[:, 1:], angle) - def _calc_avo_props_active_cells(self, grid, dt=0.0005): + for ind in range(len(indices)): + # convolution with the Ricker wavelet + + w_trace = conv_op * Rpp[ind, :] + + # Sample the trace into regular time interval + f = interp1d(np.squeeze(t[ind, :]), np.squeeze(w_trace), + kind='nearest', fill_value='extrapolate') + trace_interp[ind, :] = f(t_interp) + + if i == 0: + avo_data = trace_interp # 3D + elif i == 1: + avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D + else: + avo_data = np.concatenate((avo_data, trace_interp[:, :, np.newaxis]), axis=2) # 4D + + return avo_data, Rpp, vp_sample, vs_sample, rho_sample + #self.avo_data = avo_data + #self.Rpp = Rpp + #self.vp_sample = vp_sample + #self.vs_sample = vs_sample + #self.rho_sample = rho_sample + + def _calc_avo_props_active_cells_org(self, grid, dt=0.0005): # dt is the fine resolution sampling rate # convert properties in reservoir model to time domain vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) @@ -1077,12 +1272,16 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): # check if Nz, is at axis = 0, then transpose to dimensions, Nx, ny, Nz if grid['ACTNUM'].shape[0] == self.NZ: - self.vp = np.transpose(self.vp, (2, 1, 0)) - self.vs = np.transpose(self.vs, (2, 1, 0)) - self.rho = np.transpose(self.rho, (2, 1, 0)) + vp = np.transpose(self.vp, (2, 1, 0)) + + vs = np.transpose(self.vs, (2, 1, 0)) + rho = np.transpose(self.rho, (2, 1, 0)) actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) else: actnum = grid['ACTNUM'] + vp = self.vp + vs = self.vs + rho = self.rho # # # # Two-way travel time of the top of the reservoir @@ -1090,9 +1289,9 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): top_res = 2 * self.TOPS[:, :, 0] / vp_shale # Cumulative traveling time through the reservoir in vertical direction - cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] + cum_time_res = np.nancumsum(2 * self.DZ / vp, axis=2) + top_res[:, :, np.newaxis] - # assumes underburden to be constant. No reflections from underburden. Hence set traveltime to underburden very large + # assumes under burden to be constant. No reflections from under burden. Hence set travel time to under burden very large underburden = top_res + np.max(cum_time_res) # total travel time @@ -1101,13 +1300,13 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): # add overburden and underburden of Vp, Vs and Density vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), - self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), - self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) + vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) #rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 # self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)), - self.rho, rho_shale * np.ones((self.NX, self.NY, 1))), axis=2) + rho, rho_shale * np.ones((self.NX, self.NY, 1))), axis=2) # get indices of active cells @@ -1158,6 +1357,8 @@ def _calc_avo_props_active_cells(self, grid, dt=0.0005): nz_rpp = vp_sample.shape[1] - 1 conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + avo_data = [] + Rpp = [] for i in range(len(self.avo_config['angle'])): angle = self.avo_config['angle'][i] Rpp = self.pp_func(vp_sample[:, :-1], vs_sample[:, :-1], rho_sample[:, :-1], @@ -1197,7 +1398,7 @@ def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): """ array = np.array(array) if len(array.shape) != 1: # if array is a 1D array, then do nothing - assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" + assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy array are supported" # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] new_array = np.transpose(array, axes=[2, 1, 0]) @@ -1223,60 +1424,11 @@ def setup_fwd_run(self, **kwargs): def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. - #self.ensemble_member = member_i - #self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - #return self.pred_data - - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder self.ensemble_member = member_i + #return super().run_fwd_sim(state, member_i, del_folder=del_folder) + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) + return self.pred_data - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. @@ -1284,8 +1436,11 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): if folder is None: folder = self.folder - # run flow simulator - success = super(flow_rock, self).call_sim(folder, True) + if not self.no_flow: + # call call_sim in flow class (skip flow_rock, go directly to flow which is a parent of flow_rock) + success = super(flow_rock, self).call_sim(folder, True) + else: + success = True # # use output from flow simulator to forward model gravity response if success: @@ -1294,9 +1449,18 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): return success def get_grav_result(self, folder, save_folder): - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - grid = self.ecl_case.grid() + if self.no_flow: + grid_file = self.pem_input['grid'] + grid = np.load(grid_file) + else: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + + #f_dim = [self.NZ, self.NY, self.NX] + + self.dyn_var = [] # cell centers self.find_cell_centers(grid) @@ -1307,56 +1471,53 @@ def get_grav_result(self, folder, save_folder): # loop over vintages with gravity acquisitions grav_struct = {} - for v, assim_time in enumerate(self.grav_config['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - - # porosity, saturation, densities, and fluid mass at individual time-steps - grav_struct[v] = self.calc_mass(time) # calculate the mass of each fluid in each grid cell - - if 'baseline' in self.grav_config: # 4D measurement base_time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + dt.timedelta(days=self.grav_config['baseline']) # porosity, saturation, densities, and fluid mass at time of baseline survey - grav_base = self.calc_mass(base_time) + grav_base = self.calc_mass(base_time, 0) else: # seafloor gravity only work in 4D mode + grav_base = None print('Need to specify Baseline survey for gravity in pipt file') + for v, assim_time in enumerate(self.grav_config['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + + # porosity, saturation, densities, and fluid mass at individual time-steps + grav_struct[v] = self.calc_mass(time, v+1) # calculate the mass of each fluid in each grid cell + + + vintage = [] for v, assim_time in enumerate(self.grav_config['vintage']): dg = self.calc_grav(grid, grav_base, grav_struct[v]) vintage.append(deepcopy(dg)) - save_dic = {'grav': dg, **self.grav_config} + #save_dic = {'grav': dg, **self.grav_config} + save_dic = { + 'grav': dg, 'P_vint': grav_struct[v]['PRESSURE'], 'rho_gas_vint':grav_struct[v]['GAS_DEN'], + **self.grav_config, + **{key: grav_struct[v][key] - grav_base[key] for key in grav_struct[v].keys()} + } if save_folder is not None: file_name = save_folder + os.sep + f"grav_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"grav_vint{v}.npz" else: file_name = folder + os.sep + f"grav_vint{v}.npz" if folder[-1] != os.sep \ else folder + f"grav_vint{v}.npz" + file_name_rec = 'Ensemble_results/' + f"grav_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"grav_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) # with open(file_name, "wb") as f: # dump(**save_dic, f) np.savez(file_name, **save_dic) - # fluid masses - save_dic = {key: grav_struct[v][key] - grav_base[key] for key in grav_struct[v].keys()} - if save_folder is not None: - file_name = save_folder + os.sep + f"fluid_mass_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"fluid_mass_vint{v}.npz" - else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"fluid_mass_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"fluid_mass_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"fluid_mass_vint{v}.npz" - np.savez(file_name, **save_dic) # 4D response @@ -1364,66 +1525,148 @@ def get_grav_result(self, folder, save_folder): for i, elem in enumerate(vintage): self.grav_result.append(elem) - def calc_mass(self, time): - # fluid phases written to restart file from simulator run - phases = self.ecl_case.init.phases + def calc_mass(self, time, time_index = None): + + if self.no_flow: + time_input = time_index + else: + time_input = time + # fluid phases given as input + phases = str.upper(self.pem_input['phases']) + phases = phases.split() + # grav_input = {} + tmp_dyn_var = {} - keywords = self.ecl_case.arrays(time) - keywords = [s.strip() for s in keywords] # Remove leading/trailing spaces - # for key in self.all_data_types: - # if 'grav' in key: - # pore volumes at each assimilation step - if 'RPORV' in keywords: - #tmp = self._get_pem_input('RPORV', time) - #grav_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) - # get active porosity - # pore volumes at each assimilation step - tmp = self.ecl_case.cell_data('RPORV', time) - grav_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - else: - print('Keyword RPORV missing from simulation output, need pdated porevolumes at each assimilation step') + + tmp = self._get_pem_input('RPORV', time_input) + grav_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + + tmp = self._get_pem_input('PRESSURE', time_input) + #if time_input == time_index and time_index > 0: # to be activiated in case on inverts for Delta Pressure + # # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('PRESSURE', 0) + # tmp = tmp + tmp_baseline + grav_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) + # convert pressure from Bar to MPa + if 'press_conv' in self.pem_input and time_input == time: + grav_input['PRESSURE'] = grav_input['PRESSURE'] * self.pem_input['press_conv'] + #else: + # print('Keyword RPORV missing from simulation output, need updated pore volumes at each assimilation step') # extract saturation if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended for var in phases: if var in ['WAT', 'GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time_input) + #if time_input == time_index and time_index > 0: # to be activated in case on inverts for Delta S + # # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('S{}'.format(var), 0) + # tmp = tmp + tmp_baseline + #tmp = self.ecl_case.cell_data('S{}'.format(var), time) grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] > 1] = 1 + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] < 0] = 0 grav_input['SOIL'] = 1 - (grav_input['SWAT'] + grav_input['SGAS']) + grav_input['SOIL'][grav_input['SOIL'] > 1] = 1 + grav_input['SOIL'][grav_input['SOIL'] < 0] = 0 + + + tmp_dyn_var['SWAT'] = grav_input['SWAT'] # = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + tmp_dyn_var['SGAS'] = grav_input['SGAS'] + tmp_dyn_var['SOIL'] = grav_input['SOIL'] + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model for var in phases: if var in ['GAS']: - tmp = self.ecl_case.cell_data('S{}'.format(var), time) + tmp = self._get_pem_input('S{}'.format(var), time_input) + #if time_input == time_index and time_index > 0: # to be activated in case on inverts for Delta S + # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('S{}'.format(var), 0) + # tmp = tmp + tmp_baseline + #tmp = self.ecl_case.cell_data('S{}'.format(var), time) grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] > 1] = 1 + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] < 0] = 0 grav_input['SWAT'] = 1 - (grav_input['SGAS']) + # fluid saturation + tmp_dyn_var['SWAT'] = grav_input['SWAT'] #= {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + tmp_dyn_var['SGAS'] = grav_input['SGAS'] + + elif 'OIL' in phases and 'GAS' in phases: # Original Smeaheia model + for var in phases: + if var in ['GAS']: + tmp = self._get_pem_input('S{}'.format(var), time_input) + #if time_input == time_index and time_index > 0: # to be activated in case on inverts for Delta S + # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('S{}'.format(var), 0) + # tmp = tmp + tmp_baseline + #tmp = self.ecl_case.cell_data('S{}'.format(var), time) + grav_input['S{}'.format(var)] = np.array(tmp[~tmp.mask], dtype=float) + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] > 1] = 1 + grav_input['S{}'.format(var)][grav_input['S{}'.format(var)] < 0] = 0 + + grav_input['SOIL'] = 1 - (grav_input['SGAS']) + + # fluid saturation + tmp_dyn_var['SOIL'] = grav_input['SOIL'] #= {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} + tmp_dyn_var['SGAS'] = grav_input['SGAS'] + else: print('Type and number of fluids are unspecified in calc_mass') - # fluid densities for var in phases: dens = var + '_DEN' - tmp = self.ecl_case.cell_data(dens, time) - grav_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + #tmp = self.ecl_case.cell_data(dens, time) + if self.no_flow: + if any('pressure' in key for key in self.state.keys()): + if 'press_conv' in self.pem_input: + conv2pa = 1e6 #MPa to Pa + else: + conv2pa = 1e5 # Bar to Pa + + if var == 'GAS': + if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: + tmp = PropsSI('D', 'T', 298.15, 'P', grav_input['PRESSURE']*conv2pa, 'Methane') + elif 'WAT' in phases and 'GAS' in grav_input['PRESSURE']: # Smeaheia model T = 37 C + tmp = PropsSI('D', 'T', 310.15, 'P', grav_input['PRESSURE']*conv2pa, 'CO2') + mask = np.zeros(tmp.shape, dtype=bool) + tmp = np.ma.array(data=tmp, dtype=tmp.dtype, mask=mask) + elif var == 'WAT': + tmp = PropsSI('D', 'T|liquid', 298.15, 'P', grav_input['PRESSURE']*conv2pa, 'Water') + mask = np.zeros(tmp.shape, dtype=bool) + tmp = np.ma.array(data=tmp, dtype=tmp.dtype, mask=mask) + else: + tmp = self._get_pem_input(dens, time_input) + else: + tmp = self._get_pem_input(dens, time_input) + grav_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + tmp_dyn_var[dens] = grav_input[dens] + else: + tmp = self._get_pem_input(dens, time_input) + grav_input[dens] = np.array(tmp[~tmp.mask], dtype=float) + tmp_dyn_var[dens] = grav_input[dens] - #fluid masses + tmp_dyn_var['PRESSURE'] = grav_input['PRESSURE'] + tmp_dyn_var['RPORV'] = grav_input['RPORV'] + self.dyn_var.extend([tmp_dyn_var]) + + #fluid masses for var in phases: mass = var + '_mass' - grav_input[mass] = grav_input[var + '_DEN'] * grav_input['S' + var] * grav_input['PORO'] + grav_input[mass] = grav_input[var + '_DEN'] * grav_input['S' + var] * grav_input['RPORV'] return grav_input def calc_grav(self, grid, grav_base, grav_repeat): - - #cell_centre = [x, y, z] cell_centre = self.grav_config['cell_centre'] x = cell_centre[0] @@ -1438,15 +1681,22 @@ def calc_grav(self, grid, grav_base, grav_repeat): dg = np.zeros(N_meas) # 1D array for dg dg[:] = np.nan - # total fluid mass at this time - phases = self.ecl_case.init.phases + # fluid phases given as input + phases = str.upper(self.pem_input['phases']) + phases = phases.split() if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: dm = grav_repeat['OIL_mass'] + grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['WAT_mass'] + grav_base['GAS_mass']) + elif 'OIL' in phases and 'GAS' in phases: # Original Smeaheia model + dm = grav_repeat['OIL_mass'] + grav_repeat['GAS_mass'] - (grav_base['OIL_mass'] + grav_base['GAS_mass']) + # dm = grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['WAT_mass'] + grav_base['GAS_mass']) + elif 'WAT' in phases and 'GAS' in phases: # Smeaheia model dm = grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['WAT_mass'] + grav_base['GAS_mass']) + #dm = grav_repeat['WAT_mass'] + grav_repeat['GAS_mass'] - (grav_base['WAT_mass'] + grav_base['GAS_mass']) else: + dm = None print('Type and number of fluids are unspecified in calc_grav') @@ -1457,7 +1707,7 @@ def calc_grav(self, grid, grav_base, grav_repeat): z - pos['z'][j]) ** 2) ** (3 / 2) dg[j] = np.dot(dg_tmp, dm) - print(f'Progress: {j + 1}/{N_meas}') # Mimicking waitbar + #print(f'Progress: {j + 1}/{N_meas}') # Mimicking wait bar # Scale dg by the constant dg *= 6.67e-3 @@ -1475,8 +1725,8 @@ def measurement_locations(self, grid): ymax = np.max(cell_centre[1]) # Make a mesh of the area - pad = self.grav_config.get('padding_reservoir', 1500) # 3 km padding around the reservoir - if 'padding_reservoir' not in self.grav_config: + pad = self.grav_config.get('padding', 1500) # 3 km padding around the reservoir + if 'padding' not in self.grav_config: print('Please specify extent of measurement locations (Padding in pipt file), using 1.5 km as default') xmin -= pad @@ -1524,7 +1774,7 @@ def get_seabed_depths(self): # Read the data while skipping the header comments # We'll assume the header data ends before the numerical data # The 'delim_whitespace' keyword in pd.read_csv is deprecated and will be removed in a future version. Use ``sep='\s+'`` instead - water_depths = pd.read_csv(file_path, comment='#', delim_whitespace=True, header=None) + water_depths = pd.read_csv(file_path, comment='#', sep=r'\s+', header=None)#delim_whitespace=True, header=None) # Give meaningful column names: water_depths.columns = ['x', 'y', 'z', 'column', 'row'] @@ -1580,9 +1830,9 @@ def _get_grav_info(self, grav_config=None): self.grav_config = {} for elem in self.input_dict['grav']: assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + if elem[0] == 'vintage' and not isinstance(elem[1], list): + elem[1] = [elem[1]] self.grav_config[elem[0]] = elem[1] - - else: self.grav_config = None @@ -1618,7 +1868,7 @@ def setup_fwd_run(self, **kwargs): def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) return self.pred_data @@ -1627,6 +1877,8 @@ def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): # Then, get the pem. if folder is None: folder = self.folder + else: + self.folder = folder # run flow simulator # success = True @@ -1662,4 +1914,634 @@ def extract_data(self, member): else self.folder + key + '_vint' + str(idx) + '.npz' with np.load(filename) as f: self.pred_data[prim_ind][key] = f[key] + #v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + #self.pred_data[prim_ind][key] = self.avo_result[v].flatten() + +class flow_seafloor_disp(flow_grav): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + self.grav_input = {} + assert 'sea_disp' in input_dict, 'To do subsidence/uplift simulation, please specify an "SEA_DISP" section in the pipt file' + self._get_disp_info() + + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run(redund_sim=None) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if folder is None: + folder = self.folder + + # run flow simulator + # success = True + success = super(flow_rock, self).call_sim(folder, True) + + # use output from flow simulator to forward model gravity response + if success: + # calculate gravity data based on flow simulation output + self.get_displacement_result(folder, save_folder) + + + return success + + def get_displacement_result(self, folder, save_folder): + if self.no_flow: + grid_file = self.pem_input['grid'] + grid = np.load(grid_file) + else: + self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + grid = self.ecl_case.grid() + + self.dyn_var = [] + + # cell centers + self.find_cell_centers(grid) + + # receiver locations + self.measurement_locations(grid) + + # loop over vintages with gravity acquisitions + disp_struct = {} + + if 'baseline' in self.disp_config: # 4D measurement + base_time = dt.datetime(self.startDate['year'], self.startDate['month'], + self.startDate['day']) + dt.timedelta(days=self.grav_config['baseline']) + # porosity, saturation, densities, and fluid mass at time of baseline survey + disp_base = self.get_pore_volume(base_time, 0) + + + else: + # seafloor displacement only work in 4D mode + disp_base = None + print('Need to specify Baseline survey for displacement modelling in pipt file') + + for v, assim_time in enumerate(self.disp_config['vintage']): + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ + dt.timedelta(days=assim_time) + + # porosity, saturation, densities, and fluid mass at individual time-steps + disp_struct[v] = self.get_pore_volume(time, v+1) # calculate the mass of each fluid in each grid cell + + + + vintage = [] + + for v, assim_time in enumerate(self.disp_config['vintage']): + # calculate subsidence and uplift + dz = self.map_z_response(disp_base, disp_struct[v], grid) + vintage.append(deepcopy(dz)) + + save_dic = {'disp': dz, **self.disp_config} + if save_folder is not None: + file_name = save_folder + os.sep + f"sea_disp_vint{v}.npz" if save_folder[-1] != os.sep \ + else save_folder + f"sea_disp_vint{v}.npz" + else: + file_name = folder + os.sep + f"sea_disp_vint{v}.npz" if folder[-1] != os.sep \ + else folder + f"sea_disp_vint{v}.npz" + file_name_rec = f"sea_disp_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else f"sea_disp_vint{v}_{folder[:-1]}.npz" + np.savez(file_name_rec, **save_dic) + + # with open(file_name, "wb") as f: + # dump(**save_dic, f) + np.savez(file_name, **save_dic) + + + # 4D response + self.disp_result = [] + for i, elem in enumerate(vintage): + self.disp_result.append(elem) + + def get_pore_volume(self, time, time_index = None): + + if self.no_flow: + time_input = time_index + else: + time_input = time + + # fluid phases given as input + phases = str.upper(self.pem_input['phases']) + phases = phases.split() + # + disp_input = {} + tmp_dyn_var = {} + + + tmp = self._get_pem_input('RPORV', time_input) + disp_input['RPORV'] = np.array(tmp[~tmp.mask], dtype=float) + + tmp = self._get_pem_input('PRESSURE', time_input) + #if time_input == time_index and time_index > 0: # to be activiated in case on inverts for Delta Pressure + # # Inverts for changes in dynamic variables using time-lapse data + # tmp_baseline = self._get_pem_input('PRESSURE', 0) + # tmp = tmp + tmp_baseline + disp_input['PRESSURE'] = np.array(tmp[~tmp.mask], dtype=float) + # convert pressure from Bar to MPa + if 'press_conv' in self.pem_input and time_input == time: + disp_input['PRESSURE'] = disp_input['PRESSURE'] * self.pem_input['press_conv'] + #else: + # print('Keyword RPORV missing from simulation output, need pdated porevolumes at each assimilation step') + + + tmp_dyn_var['PRESSURE'] = disp_input['PRESSURE'] + tmp_dyn_var['RPORV'] = disp_input['RPORV'] + self.dyn_var.extend([tmp_dyn_var]) + + return disp_input + + def compute_horizontal_distance(self, pos, x, y): + dx = pos['x'][:, np.newaxis] - x + dy = pos['y'][:, np.newaxis] - y + rho = np.sqrt(dx ** 2 + dy ** 2).flatten() + return rho + + def map_z_response(self, base, repeat, grid): + """ + Maps out subsidence and uplift based either on the simulation + model pressure drop (method = 'pressure') or simulated change in pore volume + using either the van Opstal or Geertsma forward model + + Arguments: + base -- A dictionary containing baseline pressures and pore volumes. + repeat -- A dictionary containing pressures and pore volumes at repeat measurements. + + compute subsidence at position 'pos', b + + Output is modeled subsidence in cm. + + """ + + # Method to compute pore volume change + method = self.disp_config['method'].lower() + + # Forward model to compute subsidence/uplift response + model = self.disp_config['model'].lower() + + if self.disp_config['poisson'] > 0.5: + poisson = 0.5 + print('Poisson\'s ratio exceeds physical limits, setting it to 0.5') + else: + poisson = self.disp_config['poisson'] + + # Depth of rigid basement + z_base = self.disp_config['z_base'] + + compressibility = self.disp_config['compressibility'] # 1/MPa + + E = ((1 + poisson) * (1 - 2 * poisson)) / ((1 - poisson) * compressibility) + + # coordinates of cell centres + cell_centre = self.grav_config['cell_centre'] + + # measurement locations + pos = self.grav_config['meas_location'] + + + # compute pore volume change between baseline and repeat survey + # based on the reservoir pore volumes in the individual vintages + if method == 'pressure': + dV = base['RPORV'] * (base['PRESSURE'] - repeat['PRESSURE']) * compressibility + else: + dV = base['RPORV'] - repeat['RPORV'] + + # coordinates of cell centres + x = cell_centre[0] + y = cell_centre[1] + z = cell_centre[2] + + # Depth range of reservoir plus vertical span in seafloor measurement positions + # z_res = np.linspace(np.min(z) - np.max(pos['z']) - 1, np.max(z) - np.min(pos['z']) + 1) + # Maximum horizontal span between seafloor measurement location and reservoir boundary + #rho_x_max = max(np.max(x) - np.min(pos['x']), np.max(pos['x']) - np.min(x)) + #rho_y_max = max(np.max(y) - np.min(pos['y']), np.max(pos['y']) - np.min(y)) + #rho = np.linspace(0, np.sqrt(rho_x_max ** 2 + rho_y_max ** 2) + 1) + #rho_mesh, z_res_mesh = np.meshgrid(rho, z_res) + #t_van_opstal, t_geertsma = self.compute_van_opstal_transfer_function(z_res, z_base, rho, poisson) + + if model == 'van_Opstal': + # Represents a signal change for subsidence/uplift. + #trans_func = t_van_opstal + component = ["Geertsma_vertical", "System_3_vertical"] + else: # Use Geertsma + #trans_func = t_geertsma + component = ["Geertsma_vertical"] + # Initialization + dz_1_2 = 0 + dz_3 = 0 + + # indices of active cells: + true_indices = np.where(grid['ACTNUM']) + # number of active gridcells + Nn = len(true_indices[0]) + + for j in range(Nn): + rho = self.compute_horizontal_distance(pos, x[j], y[j]) + THH, TRB = self.compute_deformation_transfer(pos['z'], z[j], z_base, rho, poisson, E, dV[j], component) + dz_1_2 = dz_1_2 + THH + dz_3 = dz_3 + TRB + + if model == 'van_Opstal': + # Represents a signal change for subsidence/uplift. + dz = dz_1_2 + dz_3 + else: # Use Geertsma + dz = dz_1_2 + + #ny, nx = pos['x'].shape + #dz = np.zeros((ny, nx)) + + # Compute subsidence and uplift + #for j in range(ny): + # for i in range(nx): + # r = np.sqrt((x - pos['x'][j, i]) ** 2 + (y - pos['y'][j, i]) ** 2) + # dz[j, i] = np.sum( + # dV * griddata((rho_mesh.flatten(), z_res_mesh.flatten()), trans_func, (r, z[j, i] - pos['z'][j, i]), + # method='linear')) + + # Normalize + #dz = dz * (1 - poisson) / (2 * np.pi) + + # Convert from meters to centimeters + dz *= 100 + + return dz + + def compute_van_opstal_transfer_function(self, z_res, z_base, rho, poisson): + """ + Compute the Van Opstal transfer function. + + Args: + z_res -- Numpy array of depths to reservoir cells [m]. + z_base -- Distance to the basement [m]. + rho -- Numpy array of horizontal distances in the field [m]. + poisson -- Poisson's ratio. + + Returns: + T -- Numpy array of the transfer function values. + T_geertsma -- Numpy array of the Geertsma transfer function values. + """ + + # Change to km scale + rho = rho / 1e3 + z_res = z_res / 1e3 + z_base = z_base / 1e3 + + + # Find lambda max (to optimize Hilbert transform) + cutoff = 1e-10 # Function value at max lambda + try: + lambda_max = fsolve(lambda x: 4 * (2 * x * z_base + 1) / (3 - 4 * poisson) * np.exp( + x * (np.max(z_res) - 2 * z_base)) - cutoff, 10)[0] + except: + lambda_max = 10 # Default value if unable to solve for max lambda + + lambda_vals = np.linspace(0, lambda_max, 100) + # range of lateral distances between measurement location and reservoir cells + nj = len(rho) + # range of vertical distances between measurement location and reservoir cells + ni = len(z_res) + # initialize + t_van_opstal = np.zeros((ni, nj)) + + # input function to make a hankel transform of order 0 of + c_t = self.van_opstal(lambda_vals, z_res[0], z_base, poisson) + + h_t, i_t = self.h_t(c_t, lambda_vals, rho) # Extract integrand + t_van_opstal[0, :] = (2 * z_res[0] / (rho ** 2 + z_res[0] ** 2) ** (3 / 2)) + h_t / (2 * np.pi) + + for i in range(1, ni): + C = self.van_opstal(lambda_vals, z_res[i], z_base, poisson) + h_t = self.h_t(C, lambda_vals, rho, i_t) + t_van_opstal[i, :] = (2 * z_res[i] / (rho ** 2 + z_res[i] ** 2) ** (3 / 2)) + h_t / (2 * np.pi) + + t_van_opstal *= 1e-6 # Convert back to meters + + t_geertsma = (2 * z_res[:, np.newaxis] / ((np.ones((ni, 1)) * rho) ** 2 + (z_res[:, np.newaxis]) ** 2) ** ( + 3 / 2)) * 1e-6 + + return t_van_opstal, t_geertsma + + def van_opstal(self, lambda_vals, z_res, z_base, poisson): + """ + Compute the Van Opstal transfer function. + + Args: + lambda_vals -- Numpy array of lambda values. + z_res -- Depth to reservoir [m]. + z_base -- Distance to the basement [m]. + poisson -- Poisson's ratio. + + Returns: + value -- Numpy array of computed values. + """ + + term1 = np.exp(lambda_vals * z_res) * (2 * lambda_vals * z_base + 1) + term2 = np.exp(-lambda_vals * z_res) * ( + 4 * lambda_vals ** 2 * z_base ** 2 + 2 * lambda_vals * z_base + (3 - 4 * poisson) ** 2) + + term3_numer = (3 - 4 * poisson) * ( + np.exp(-lambda_vals * (2 * z_base + z_res)) - np.exp(-lambda_vals * (2 * z_base - z_res))) + term3_denom = 2 * ((1 - 2 * poisson) ** 2 + lambda_vals ** 2 * z_base ** 2 + (3 - 4 * poisson) * np.cosh( + lambda_vals * z_base) ** 2) + + value = term1 - term2 - (term3_numer / term3_denom) + + return value + + def van_opstal_org(self, lambda_vals, z_res, z_base, poisson): + """ + Compute the Van Opstal transfer function. + + Args: + lambda_vals -- Numpy array of lambda values. + z_res -- Depth to reservoir [m]. + z_base -- Distance to the basement [m]. + poisson -- Poisson's ratio. + + Returns: + value -- Numpy array of computed values. + """ + + term1 = np.exp(lambda_vals * z_res) * (2 * lambda_vals * z_base + 1) + term2 = np.exp(-lambda_vals * z_res) * ( + 4 * lambda_vals ** 2 * z_base ** 2 + 2 * lambda_vals * z_base + (3 - 4 * poisson) ** 2) + + term3_numer = (3 - 4 * poisson) * ( + np.exp(-lambda_vals * (2 * z_base + z_res)) - np.exp(-lambda_vals * (2 * z_base - z_res))) + term3_denom = 2 * ((1 - 2 * poisson) ** 2 + lambda_vals ** 2 * z_base ** 2 + (3 - 4 * poisson) * np.cosh( + lambda_vals * z_base) ** 2) + + value = term1 - term2 - (term3_numer / term3_denom) + + return value + + def hankel_transform_order_0(f, r_max, num_points=1000): + """ + Computes the Hankel transform of order 0 of a function f(r). + + Parameters: + - f: callable, the function to transform, f(r) + - r_max: float, upper limit of the integral (approximate infinity) + - num_points: int, number of points for numerical integration + + Returns: + - k_values: array of k values + - H_k: array of Hankel transform evaluated at k_values + """ + r = np.linspace(0, r_max, num_points) + dr = r[1] - r[0] + f_r = f(r) + + def integrand(r, k): + return f(r) * j0(k * r) * r + + # Define a range of k values to evaluate + k_min, k_max = 0, 10 # adjust as needed + k_values = np.linspace(k_min, k_max, 100) + + H_k = [] + + for k in k_values: + # Perform numerical integration over r + result, _ = quad(integrand, 0, r_max, args=(k,)) + H_k.append(result) + + return k_values, np.array(H_k) + + def makeL(self, poisson, k, c, A_g, eps, lambda_): + L = A_g * ( + (4 * poisson - 3 + 2 * k * lambda_) * np.exp(-lambda_ * (k + c)) + - np.exp(lambda_ * eps * (k - c)) + ) + return L + + def makeM(self, poisson, k, c, A_g, eps, lambda_): + M = A_g * ( + (4 * poisson - 3 - 2 * k * lambda_) * np.exp(-lambda_ * (k + c)) + - eps * np.exp(lambda_ * eps * (k - c)) + ) + return M + + def makeDelta(self, poisson, k, lambda_): + Delta = ( + (4 * poisson - 3) * np.cosh(k * lambda_) ** 2 + - (k * lambda_) ** 2 + - (1 - 2 * poisson) ** 2 + ) + return Delta + + def makeB(self, poisson, k, c, A_g, eps, lambda_): + L = self.makeL(poisson, k, c, A_g, eps, lambda_) + M = self.makeM(poisson, k, c, A_g, eps, lambda_) + Delta = self.makeDelta(poisson, k, lambda_) + + numerator = ( + lambda_ * L * (2 * (1 - poisson) * np.cosh(k * lambda_) - lambda_ * k * np.sinh(k * lambda_)) + + lambda_ * M * ((1 - 2 * poisson) * np.sinh(k * lambda_) + k * lambda_ * np.cosh(k * lambda_)) + ) + + B = numerator / Delta + return B + + def makeC(self,poisson, k, c, A_g, eps, lambda_): + L = self.makeL(poisson, k, c, A_g, eps, lambda_) + M = self.makeM(poisson, k, c, A_g, eps, lambda_) + Delta = self.makeDelta(poisson, k, lambda_) + + numerator = ( + lambda_ * L * ((1 - 2 * poisson) * np.sinh(k * lambda_) - lambda_ * k * np.cosh(k * lambda_)) + + lambda_ * M * (2 * (1 - poisson) * np.cosh(k * lambda_) + k * lambda_ * np.sinh(k * lambda_)) + ) + + C = numerator / Delta + return C + + def compute_deformation_transfer(self, z, c, k, rho, poisson, E, dV, component): + # Convert inputs to numpy arrays if they are not already + z = np.array(z) + #x = np.array(x) + rho = np.array(rho) + component = list(component) + + Ni = len(z) + Nj = len(rho) + + # Initialize output arrays + THH = np.zeros((Ni, Nj)) + #THHr = np.zeros((Ni, Nj)) + TRB = np.zeros((Ni, Nj)) + #TRBr = np.zeros((Ni, Nj)) + + # Constants + A_g = -dV * E / (4 * np.pi * (1 + poisson)) + uHH_outside_intregral = -(A_g * (1 + poisson)) / E + uRB_outside_intregral = (1 + poisson) / E + lambda_max = min([0.1, 500 / max(z)]) # Avoid index errors if z is empty + # Setup your lambda grid + num_points = 500 + lambda_grid = np.linspace(0, lambda_max, num_points) + + for c_n in component: + if c_n == 'Geertsma_vertical': + for j in range(Nj): + for i in range(Ni): + eps = np.sign(c - z[i]) + z_ratio = z[i] - c + z_sum = z[i] + c + + # Evaluate the integrand over the grid + integrand_vals = lambda lam: lam * ( + eps * np.exp(lam * eps * z_ratio) + + (3 - 4 * poisson + 2 * z[i] * lam) * np.exp(-lam * z_sum) + ) * j0(lam * rho[j]) + + values = integrand_vals(lambda_grid) + + #def uHH_integrand(lambda_): + # val = lambda_ * (eps * np.exp(lambda_ * eps * (z[i] - c)) + + # (3 - 4 * poisson + 2 * z[i] * lambda_) * + # np.exp(-lambda_ * (z[i] + c))) + # return val * j0(lambda_ * rho[j]) + + THH[i, j] = np.trapz(values, lambda_grid) * uHH_outside_intregral + #THH[i, j] = quad(uHH_integrand, 0, lambda_max)[0] * uHH_outside_intregral + + elif c_n == 'System_3_vertical': + sinh_z = np.sinh(z[:, np.newaxis] * lambda_grid) + cosh_z = np.cosh(z[:, np.newaxis] * lambda_grid) + J0_rho = j0(lambda_grid * rho) + + for j in range(Nj): + rho_j = rho[j] + J0_rho_j = J0_rho[:, j] + for i in range(Ni): + z_i = z[i] + sinh_z_i = sinh_z[i] # precomputed sinh values + cosh_z_i = cosh_z[i] # precomputed cosh values + + # Use vectorized operations over lambda_grid + b_values = self.makeB(poisson, k, c, A_g, -1, lambda_grid) + c_values = self.makeC(poisson, k, c, A_g, -1, lambda_grid) + + part1 = b_values * (lambda_grid * z_i * cosh_z_i - (1 - 2 * poisson) * sinh_z_i) + part2 = c_values * ((2 * (1 - poisson) * cosh_z_i) - lambda_grid * z_i * sinh_z_i) + + values = (part1 + part2) * J0_rho_j + + integral_result = np.trapz(values, lambda_grid) + TRB[i, j] = integral_result * uRB_outside_intregral + + elif c_n == 'System_3_vertical_original': + for j in range(Nj): + for i in range(Ni): + # Assume makeB and makeC are implemented similarly + def uRB_integrand(lambda_): + b_val = self.makeB(poisson, k, c, A_g, -1, lambda_) + c_val = self.makeC(poisson, k, c, A_g, -1, lambda_) + # Assuming makeB and makeC return scalars. Replace with actual functions. + part1 = b_val * (lambda_ * z[i] * np.cosh(z[i] * lambda_) - (1 - 2 * poisson) * np.sinh( + z[i] * lambda_)) + part2 = c_val * (2 * (1 - poisson) * np.cosh(z[i] * lambda_) + (-lambda_) * z[i] * np.sinh( + z[i] * lambda_)) + return (part1 + part2) * j0(lambda_ * rho[j]) + + values = uRB_integrand(lambda_grid) + integral_result = np.trapz(values, lambda_grid) + TRB[i, j] = integral_result * uRB_outside_intregral + #TRB[i, j] = quad(uRB_integrand, 0, lambda_max)[0] * uRB_outside_intregral + + return THH, TRB + + def h_t(self, h, r=None, k=None, i_k=None): + """ + Hankel transform of order 0. + + Args: + h -- Signal h(r). + r -- Radial positions [m] (optional). + k -- Spatial frequencies [rad/m] (optional). + I -- Integration kernel (optional). + + Returns: + h_t -- Spectrum H(k). + I -- Integration kernel. + """ + + # Check if h is a vector + if h.ndim > 1: + raise ValueError('Signal must be a vector.') + + if r is None or len(r) == 0: + r = np.arange(len(h)) # Default to 0:numel(h)-1 + else: + r = np.sort(r) + h = h[np.argsort(r)] # Sort h according to sorted r + + if k is None or len(k) == 0: + k = np.pi / len(h) * np.arange(len(h)) # Default spatial frequencies + + if i_k is None: + # Create integration kernel I + r = np.concatenate([(r[:-1] + r[1:]) / 2, [r[-1]]]) # Midpoints plus last point + i_k = (2 * np.pi / k[:, np.newaxis]) * r * jv(1, k[:, np.newaxis] * r) # Bessel function + i_k[k == 0, :] = np.pi * r * r + i_k = i_k - np.hstack([np.zeros((len(k), 1)), i_k[:, :-1]]) # Shift integration kernel + else: + # Ensure I is sorted based on r + i_k = i_k[:, np.argsort(r)] + + # Compute Hankel Transform + h_t = np.reshape(i_k @ h.flatten(), k.shape) + + + + return h_t, i_k + + def _get_disp_info(self, grav_config=None): + """ + seafloor displacement (uplift/subsidence) configuration + """ + # list of configuration parameters in the "Grav" section of teh pipt file + config_para_list = ['baseline', 'vintage', 'method', 'model', 'poisson', 'compressibility', 'z_base'] + + if 'sea_disp' in self.input_dict: + self.disp_config = {} + for elem in self.input_dict['sea_disp']: + assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + if elem[0] == 'vintage' and not isinstance(elem[1], list): + elem[1] = [elem[1]] + self.disp_config[elem[0]] = elem[1] + else: + self.disp_config = None + + def extract_data(self, member): + # start by getting the data from the flow simulator i.e. prod. and inj. data + super(flow_rock, self).extract_data(member) + + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'grav' in key: + if self.true_prim[1][prim_ind] in self.grav_config['vintage']: + v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.grav_result[v].flatten() + + + if 'subs_uplift' in key: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.disp_result[v].flatten() diff --git a/simulator/flow_rock_backup.py b/simulator/flow_rock_backup.py deleted file mode 100644 index 7eb2e03..0000000 --- a/simulator/flow_rock_backup.py +++ /dev/null @@ -1,1120 +0,0 @@ -from simulator.opm import flow -from importlib import import_module -import datetime as dt -import numpy as np -import os -from misc import ecl, grdecl -import shutil -import glob -from subprocess import Popen, PIPE -import mat73 -from copy import deepcopy -from sklearn.cluster import KMeans -from sklearn.preprocessing import StandardScaler -from mako.lookup import TemplateLookup -from mako.runtime import Context - -# from pylops import avo -from pylops.utils.wavelets import ricker -from pylops.signalprocessing import Convolve1D -from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master -from scipy.interpolate import interp1d -from pipt.misc_tools.analysis_tools import store_ensemble_sim_information -from geostat.decomp import Cholesky -from simulator.eclipse import ecl_100 - -class flow_sim2seis(flow): - """ - Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurement - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self): - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - # need a if to check that we have correct sim2seis - # copy relevant sim2seis files into folder. - for file in glob.glob('sim2seis_config/*'): - shutil.copy(file, 'En_' + str(self.ensemble_member) + os.sep) - - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - grid = self.ecl_case.grid() - - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - tmp = self.ecl_case.cell_data(var, time) - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - - grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v+1}.grdecl', { - 'Vs': self.pem.getShearVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v+1}.grdecl', { - 'Vp': self.pem.getBulkVel()*.1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/rho{v+1}.grdecl', - {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - current_folder = os.getcwd() - run_folder = current_folder + os.sep + 'En_' + str(self.ensemble_member) - # The sim2seis is invoked via a shell script. The simulations provides outputs. Run, and get all output. Search - # for Done. If not finished in reasonable time -> kill - p = Popen(['./sim2seis.sh', run_folder], stdout=PIPE) - start = time - while b'done' not in p.stdout.readline(): - pass - - # Todo: handle sim2seis or pem error - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['sim2seis']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - result = mat73.loadmat(f'En_{member}/Data_conv.mat')['data_conv'] - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = np.sum( - np.abs(result[:, :, :, v]), axis=0).flatten() - -class flow_rock(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - self.pem_result = [] - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['bulkimp']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() - -class flow_barycenter(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the - barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are - identified using k-means clustering, and the number of objects are determined using and elbow strategy. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - self.pem_result = [] - self.bar_result = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - if elem[0] == 'clusters': # number of clusters for each barycenter - self.pem_input['clusters'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - # Extract k-means centers and interias for each element in pem_result - if 'clusters' in self.pem_input: - npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) - n_clusters_list = npzfile['n_clusters_list'] - npzfile.close() - else: - n_clusters_list = len(self.pem_result)*[2] - kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} - for i, bulkimp in enumerate(self.pem_result): - std = np.std(bulkimp) - features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) - scaler = StandardScaler() - scaled_features = scaler.fit_transform(features) - kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) - kmeans.fit(scaled_features) - kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements - self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the barycenters and inertias - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['barycenter']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.bar_result[v].flatten() - -class flow_avo(flow_sim2seis): - def __init__(self, input_dict=None, filename=None, options=None, **kwargs): - super().__init__(input_dict, filename, options) - - assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' - self._get_avo_info() - - def setup_fwd_run(self, **kwargs): - self.__dict__.update(kwargs) - - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - """ - Setup and run the AVO forward simulator. - - Parameters - ---------- - state : dict - Dictionary containing the ensemble state. - - member_i : int - Index of the ensemble member. any index < 0 (e.g., -1) means the ground truth in synthetic case studies - - del_folder : bool, optional - Boolean to determine if the ensemble folder should be deleted. Default is False. - """ - - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder - self.ensemble_member = member_i - - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False - - - def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): - # replace the sim2seis part (which is unusable) by avo based on Pylops - - if folder is None: - folder = self.folder - - # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" - if hasattr(self, 'run_reservoir_model'): - run_reservoir_model = self.run_reservoir_model - - if run_reservoir_model is None: - run_reservoir_model = True - - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) - success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) - #ecl = ecl_100(filename=self.file) - #ecl.options = self.options - #success = ecl.call_sim(folder, wait_for_proc) - else: - success = True - - if success: - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - grid = self.ecl_case.grid() - - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(multfactor * tmp), dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - tmp = self.ecl_case.cell_data(var, time) - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input[var] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - #pem_input['PRESSURE'] = self._reformat3D_then_flatten(pem_input['PRESSURE'] * - # self.pem_input['press_conv']) - - tmp = self.ecl_case.cell_data('PRESSURE', 0) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] - #P_init = self._reformat3D_then_flatten(self.pem.p_init.reshape(tmp.shape) * np.ones(tmp.shape)) - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - #P_init = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init * self.pem_input['press_conv'] - #P_init = self._reformat3D_then_flatten(P_init * self.pem_input['press_conv']) - - saturations = [ - 1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - #saturations = [self._reformat3D_then_flatten(1 - (pem_input['SWAT'] + pem_input['SGAS'])) - # if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - - # Get the pressure - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - else: - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) - - #grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { - # 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { - # 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', - # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - # vp, vs, density - self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 - - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} - if save_folder is not None: - file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"vp_vs_rho_vint{v}.npz" - else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"vp_vs_rho_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - # avo data - self._calc_avo_props() - - avo = self.avo_data.flatten(order="F") - - # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies - # the corresonding (noisy) data are observations in data assimilation - if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: - non_nan_idx = np.argwhere(~np.isnan(avo)) - data_std = np.std(avo[non_nan_idx]) - if self.input_dict['add_synthetic_noise'][0] == 'snr': - noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std - avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) - else: - noise_std = 0.0 # simulated data don't contain noise - - save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - if save_folder is not None: - file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"avo_vint{v}.npz" - else: - # if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - # (self.ensemble_member >= 0): - # file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - # else folder + f"avo_vint{v}.npz" - # else: - # file_name = os.getcwd() + os.sep + f"avo_vint{v}.npz" - file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"avo_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super(flow_sim2seis, self).extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if 'avo' in key: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ - else self.folder + key + '_vint' + str(idx) + '.npz' - with np.load(filename) as f: - self.pred_data[prim_ind][key] = f[key] - - def _runMako(self, folder, state, addfiles=['properties']): - """ - Hard coding, maybe a better way possible - addfiles: additional files that need to be included into ECLIPSE/OPM DATA file - """ - super()._runMako(folder, state) - - lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') - for file in addfiles: - if os.path.exists(file + '.mako'): - tmpl = lkup.get_template('%s.mako' % file) - - # use a context and render onto a file - with open('{0}'.format(folder + file), 'w') as f: - ctx = Context(f, **state) - tmpl.render_context(ctx) - - def calc_pem(self, time): - - - def _get_avo_info(self, avo_config=None): - """ - AVO configuration - """ - # list of configuration parameters in the "AVO" section - config_para_list = ['dz', 'tops', 'angle', 'frequency', 'wave_len', 'vp_shale', 'vs_shale', - 'den_shale', 't_min', 't_max', 't_sampling', 'pp_func'] - if 'avo' in self.input_dict: - self.avo_config = {} - for elem in self.input_dict['avo']: - assert elem[0] in config_para_list, f'Property {elem[0]} not supported' - self.avo_config[elem[0]] = elem[1] - - # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later - if isinstance(self.avo_config['angle'], float): - self.avo_config['angle'] = [self.avo_config['angle']] - - # self._get_DZ(file=self.avo_config['dz']) # =>self.DZ - kw_file = {'DZ': self.avo_config['dz'], 'TOPS': self.avo_config['tops']} - self._get_props(kw_file) - self.overburden = self.pem_input['overburden'] - - # make sure that the "pylops" package is installed - # See https://github.com/PyLops/pylops - self.pp_func = getattr(import_module('pylops.avo.avo'), self.avo_config['pp_func']) - - else: - self.avo_config = None - - def _get_props(self, kw_file): - # extract properties (specified by keywords) in (possibly) different files - # kw_file: a dictionary contains "keyword: file" pairs - # Note that all properties are reshaped into the reservoir model dimension (NX, NY, NZ) - # using the "F" order - for kw in kw_file: - file = kw_file[kw] - if file.endswith('.npz'): - with np.load(file) as f: - exec(f'self.{kw} = f[ "{kw}" ]') - self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] - else: - reader = GRDECL_Parser(filename=file) - reader.read_GRDECL() - exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") - self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ - eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') - - def _calc_avo_props(self, dt=0.0005): - # dt is the fine resolution sampling rate - # convert properties in reservoir model to time domain - vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) - vs_shale = self.avo_config['vs_shale'] # scalar value - rho_shale = self.avo_config['den_shale'] # scalar value - - # Two-way travel time of the top of the reservoir - # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer - top_res = 2 * self.TOPS[:, :, 0] / vp_shale - - # Cumulative traveling time trough the reservoir in vertical direction - cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] - # total travel time - cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) - - # add overburden and underburden of Vp, Vs and Density - vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), - self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) - vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), - self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) - rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 - self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) - - # search for the lowest grid cell thickness and sample the time according to - # that grid thickness to preserve the thin layer effect - time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) - if time_sample.shape[0] == 1: - time_sample = time_sample.reshape(-1) - time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) - - vp_sample = np.tile(vp[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - vs_sample = np.tile(vs[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - rho_sample = np.tile(rho[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - - for m in range(self.NX): - for l in range(self.NY): - for k in range(time_sample.shape[2]): - # find the right interval of time_sample[m, l, k] belonging to, and use - # this information to allocate vp, vs, rho - idx = np.searchsorted(cum_time[m, l, :], time_sample[m, l, k], side='left') - idx = idx if idx < len(cum_time[m, l, :]) else len(cum_time[m, l, :]) - 1 - vp_sample[m, l, k] = vp[m, l, idx] - vs_sample[m, l, k] = vs[m, l, idx] - rho_sample[m, l, k] = rho[m, l, idx] - - - # from matplotlib import pyplot as plt - # plt.plot(vp_sample[0, 0, :]) - # plt.show() - - #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) - #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) - #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) - - #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] - #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] - #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] - - #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) - #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg - - # PP reflection coefficients, see, e.g., - # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" - # So far, it seems that "approx_zoeppritz_pp" is the only available option - # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) - avo_data_list = [] - - # Ricker wavelet - wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), - f0=self.avo_config['frequency']) - - # Travel time corresponds to reflectivity sereis - t = time_sample[:, :, 0:-1] - - # interpolation time - t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) - trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) - - # number of pp reflection coefficients in the vertial direction - nz_rpp = vp_sample.shape[2] - 1 - - for i in range(len(self.avo_config['angle'])): - angle = self.avo_config['angle'][i] - Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], - vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) - - for m in range(self.NX): - for l in range(self.NY): - # convolution with the Ricker wavelet - conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") - w_trace = conv_op * Rpp[m, l, :] - - # Sample the trace into regular time interval - f = interp1d(np.squeeze(t[m, l, :]), np.squeeze(w_trace), - kind='nearest', fill_value='extrapolate') - trace_interp[m, l, :] = f(t_interp) - - if i == 0: - avo_data = trace_interp # 3D - elif i == 1: - avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D - else: - avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D - - self.avo_data = avo_data - - @classmethod - def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): - """ - XILU: Quantities read by "EclipseData.cell_data" are put in the axis order of [nz, ny, nx]. To be consisent with - ECLIPSE/OPM custom, we need to change the axis order. We further flatten the array according to the specified order - """ - array = np.array(array) - if len(array.shape) != 1: # if array is a 1D array, then do nothing - assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" - - # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] - new_array = np.transpose(array, axes=[2, 1, 0]) - if flatten: - new_array = new_array.flatten(order=order) - - return new_array - else: - return array \ No newline at end of file diff --git a/simulator/flow_rock_mali.py b/simulator/flow_rock_mali.py deleted file mode 100644 index 6abb95d..0000000 --- a/simulator/flow_rock_mali.py +++ /dev/null @@ -1,1130 +0,0 @@ -from simulator.opm import flow -from importlib import import_module -import datetime as dt -import numpy as np -import os -from misc import ecl, grdecl -import shutil -import glob -from subprocess import Popen, PIPE -import mat73 -from copy import deepcopy -from sklearn.cluster import KMeans -from sklearn.preprocessing import StandardScaler -from mako.lookup import TemplateLookup -from mako.runtime import Context - -# from pylops import avo -from pylops.utils.wavelets import ricker -from pylops.signalprocessing import Convolve1D -from misc.PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master -from scipy.interpolate import interp1d -from pipt.misc_tools.analysis_tools import store_ensemble_sim_information -from geostat.decomp import Cholesky -from simulator.eclipse import ecl_100 - -class flow_sim2seis(flow): - """ - Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - self.sats = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurement - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def calc_pem(self, v, time, phases): - - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - for var in ['PRESSURE', 'RS', 'SWAT', 'SGAS']: - tmp = self.ecl_case.cell_data(var, time) - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) # only active, and conv. to float - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] - else: - P_init = np.array(tmp[~tmp.mask], dtype=float) # initial pressure is first - - if 'press_conv' in self.pem_input: - P_init = P_init * self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - - # fluid saturations in dictionary - tmp_s = {f'S{ph}': saturations[i] for i, ph in enumerate(phases)} - self.sats.extend([tmp_s]) - - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - - - def setup_fwd_run(self): - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - # need a if to check that we have correct sim2seis - # copy relevant sim2seis files into folder. - for file in glob.glob('sim2seis_config/*'): - shutil.copy(file, 'En_' + str(self.ensemble_member) + os.sep) - - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - - grid = self.ecl_case.grid() - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - self.sats = [] - # loop over seismic vintages - for v, assim_time in enumerate([0.0] + self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - - self.calc_pem(v, time, phases) - - grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { - 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { - 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', - {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - current_folder = os.getcwd() - run_folder = current_folder + os.sep + 'En_' + str(self.ensemble_member) - # The sim2seis is invoked via a shell script. The simulations provides outputs. Run, and get all output. Search - # for Done. If not finished in reasonable time -> kill - p = Popen(['./sim2seis.sh', run_folder], stdout=PIPE) - start = time - while b'done' not in p.stdout.readline(): - pass - - # Todo: handle sim2seis or pem error - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['sim2seis']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - result = mat73.loadmat(f'En_{member}/Data_conv.mat')['data_conv'] - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = np.sum( - np.abs(result[:, :, :, v]), axis=0).flatten() - -class flow_rock(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - self.pem_result = [] - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['bulkimp']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() - -class flow_barycenter(flow): - """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic - quantities can be calculated. Inherit the flow class, and use super to call similar functions. In the end, the - barycenter and moment of interia for the bulkimpedance objects, are returned as observations. The objects are - identified using k-means clustering, and the number of objects are determined using and elbow strategy. - """ - - def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) - self._getpeminfo(input_dict) - - self.dum_file_root = 'dummy.txt' - self.dum_entry = str(0) - self.date_slack = None - if 'date_slack' in input_dict: - self.date_slack = int(input_dict['date_slack']) - - # If we want to extract, or evaluate, something uniquely from the ensemble specific run we can - # run a user defined code to do this. - self.saveinfo = None - if 'savesiminfo' in input_dict: - # Make sure "ANALYSISDEBUG" gives a list - if isinstance(input_dict['savesiminfo'], list): - self.saveinfo = input_dict['savesiminfo'] - else: - self.saveinfo = [input_dict['savesiminfo']] - - self.scale = [] - self.pem_result = [] - self.bar_result = [] - - def _getpeminfo(self, input_dict): - """ - Get, and return, flow and PEM modules - """ - if 'pem' in input_dict: - self.pem_input = {} - for elem in input_dict['pem']: - if elem[0] == 'model': # Set the petro-elastic model - self.pem_input['model'] = elem[1] - if elem[0] == 'depth': # provide the npz of depth values - self.pem_input['depth'] = elem[1] - if elem[0] == 'actnum': # the npz of actnum values - self.pem_input['actnum'] = elem[1] - if elem[0] == 'baseline': # the time for the baseline 4D measurment - self.pem_input['baseline'] = elem[1] - if elem[0] == 'vintage': - self.pem_input['vintage'] = elem[1] - if not type(self.pem_input['vintage']) == list: - self.pem_input['vintage'] = [elem[1]] - if elem[0] == 'ntg': - self.pem_input['ntg'] = elem[1] - if elem[0] == 'press_conv': - self.pem_input['press_conv'] = elem[1] - if elem[0] == 'compaction': - self.pem_input['compaction'] = True - if elem[0] == 'overburden': # the npz of overburden values - self.pem_input['overburden'] = elem[1] - if elem[0] == 'percentile': # use for scaling - self.pem_input['percentile'] = elem[1] - if elem[0] == 'clusters': # number of clusters for each barycenter - self.pem_input['clusters'] = elem[1] - - pem = getattr(import_module('simulator.rockphysics.' + - self.pem_input['model'].split()[0]), self.pem_input['model'].split()[1]) - - self.pem = pem(self.pem_input) - - else: - self.pem = None - - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder=True) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - success = super().call_sim(folder, wait_for_proc) - - if success: - self.ecl_case = ecl.EclipseCase( - 'En_' + str(self.ensemble_member) + os.sep + self.file + '.DATA') - phases = self.ecl_case.init.phases - #if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - if 'WAT' in phases and 'GAS' in phases: - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask]*tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - tmp = self.ecl_case.cell_data('PRESSURE', 1) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init*np.ones(tmp.shape)[~tmp.mask] - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init*self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - # run filter - self.pem._filter() - vintage.append(deepcopy(self.pem.bulkimp)) - - if hasattr(self.pem, 'baseline'): # 4D measurement - base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.pem.baseline) - # pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', base_time) - - pem_input['PORO'] = np.array( - multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - - pem_input['RS'] = None - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - try: - tmp = self.ecl_case.cell_data(var, base_time) - except: - pass - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - - saturations = [1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - # Get the pressure - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=None) - - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) - - tmp_value[self.ecl_case.init.actnum] = self.pem.bulkimp - # kill if values are inf or nan - assert not np.isnan(tmp_value).any() - assert not np.isinf(tmp_value).any() - self.pem.bulkimp = np.ma.array(data=tmp_value, dtype=float, - mask=deepcopy(self.ecl_case.init.mask)) - self.pem._filter() - - # 4D response - for i, elem in enumerate(vintage): - self.pem_result.append(elem - deepcopy(self.pem.bulkimp)) - else: - for i, elem in enumerate(vintage): - self.pem_result.append(elem) - - # Extract k-means centers and interias for each element in pem_result - if 'clusters' in self.pem_input: - npzfile = np.load(self.pem_input['clusters'], allow_pickle=True) - n_clusters_list = npzfile['n_clusters_list'] - npzfile.close() - else: - n_clusters_list = len(self.pem_result)*[2] - kmeans_kwargs = {"init": "random", "n_init": 10, "max_iter": 300, "random_state": 42} - for i, bulkimp in enumerate(self.pem_result): - std = np.std(bulkimp) - features = np.argwhere(np.squeeze(np.reshape(np.abs(bulkimp), self.ecl_case.init.shape,)) > 3 * std) - scaler = StandardScaler() - scaled_features = scaler.fit_transform(features) - kmeans = KMeans(n_clusters=n_clusters_list[i], **kmeans_kwargs) - kmeans.fit(scaled_features) - kmeans_center = np.squeeze(scaler.inverse_transform(kmeans.cluster_centers_)) # data / measurements - self.bar_result.append(np.append(kmeans_center, kmeans.inertia_)) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super().extract_data(member) - - # get the barycenters and inertias - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if key in ['barycenter']: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.bar_result[v].flatten() - -class flow_avo(flow_sim2seis): - def __init__(self, input_dict=None, filename=None, options=None, **kwargs): - super().__init__(input_dict, filename, options) - - assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' - self._get_avo_info() - - def setup_fwd_run(self, **kwargs): - self.__dict__.update(kwargs) - - super().setup_fwd_run() - - def run_fwd_sim(self, state, member_i, del_folder=True): - """ - Setup and run the AVO forward simulator. - - Parameters - ---------- - state : dict - Dictionary containing the ensemble state. - - member_i : int - Index of the ensemble member. any index < 0 (e.g., -1) means the ground truth in synthetic case studies - - del_folder : bool, optional - Boolean to determine if the ensemble folder should be deleted. Default is False. - """ - - if member_i >= 0: - folder = 'En_' + str(member_i) + os.sep - if not os.path.exists(folder): - os.mkdir(folder) - else: # XLUO: any negative member_i is considered as the index for the true model - assert 'truth_folder' in self.input_dict, "ensemble member index is negative, please specify " \ - "the folder containing the true model" - if not os.path.exists(self.input_dict['truth_folder']): - os.mkdir(self.input_dict['truth_folder']) - folder = self.input_dict['truth_folder'] + os.sep if self.input_dict['truth_folder'][-1] != os.sep \ - else self.input_dict['truth_folder'] - del_folder = False # never delete this folder - self.folder = folder - self.ensemble_member = member_i - - state['member'] = member_i - - # start by generating the .DATA file, using the .mako template situated in ../folder - self._runMako(folder, state) - success = False - rerun = self.rerun - while rerun >= 0 and not success: - success = self.call_sim(folder, True) - rerun -= 1 - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if hasattr(self, 'redund_sim') and self.redund_sim is not None: - success = self.redund_sim.call_sim(folder, True) - if success: - self.extract_data(member_i) - if del_folder: - if self.saveinfo is not None: # Try to save information - store_ensemble_sim_information(self.saveinfo, member_i) - self.remove_folder(member_i) - return self.pred_data - else: - if del_folder: - self.remove_folder(member_i) - return False - else: - if del_folder: - self.remove_folder(member_i) - return False - - - def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): - # replace the sim2seis part (which is unusable) by avo based on Pylops - - if folder is None: - folder = self.folder - - # The field 'run_reservoir_model' can be passed from the method "setup_fwd_run" - if hasattr(self, 'run_reservoir_model'): - run_reservoir_model = self.run_reservoir_model - - if run_reservoir_model is None: - run_reservoir_model = True - - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - if run_reservoir_model: # in case that simulation has already done (e.g., for the true reservoir model) - success = super(flow_sim2seis, self).call_sim(folder, wait_for_proc) - #ecl = ecl_100(filename=self.file) - #ecl.options = self.options - #success = ecl.call_sim(folder, wait_for_proc) - else: - success = True - - if success: - self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ - else ecl.EclipseCase(folder + self.file + '.DATA') - - self.calc_pem() - - grid = self.ecl_case.grid() - - phases = self.ecl_case.init.phases - if 'OIL' in phases and 'WAT' in phases and 'GAS' in phases: # This should be extended - vintage = [] - # loop over seismic vintages - for v, assim_time in enumerate(self.pem_input['vintage']): - time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ - dt.timedelta(days=assim_time) - pem_input = {} - # get active porosity - tmp = self.ecl_case.cell_data('PORO') - if 'compaction' in self.pem_input: - multfactor = self.ecl_case.cell_data('PORV_RC', time) - - pem_input['PORO'] = np.array(multfactor[~tmp.mask] * tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(multfactor * tmp), dtype=float) - else: - pem_input['PORO'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['PORO'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - # get active NTG if needed - if 'ntg' in self.pem_input: - if self.pem_input['ntg'] == 'no': - pem_input['NTG'] = None - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - else: - tmp = self.ecl_case.cell_data('NTG') - pem_input['NTG'] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input['NTG'] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - for var in ['SWAT', 'SGAS', 'PRESSURE', 'RS']: - tmp = self.ecl_case.cell_data(var, time) - # only active, and conv. to float - pem_input[var] = np.array(tmp[~tmp.mask], dtype=float) - #pem_input[var] = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - pem_input['PRESSURE'] = pem_input['PRESSURE'] * \ - self.pem_input['press_conv'] - #pem_input['PRESSURE'] = self._reformat3D_then_flatten(pem_input['PRESSURE'] * - # self.pem_input['press_conv']) - - tmp = self.ecl_case.cell_data('PRESSURE', 0) - if hasattr(self.pem, 'p_init'): - P_init = self.pem.p_init * np.ones(tmp.shape)[~tmp.mask] - #P_init = self._reformat3D_then_flatten(self.pem.p_init.reshape(tmp.shape) * np.ones(tmp.shape)) - else: - # initial pressure is first - P_init = np.array(tmp[~tmp.mask], dtype=float) - #P_init = np.array(self._reformat3D_then_flatten(tmp), dtype=float) - - if 'press_conv' in self.pem_input: - P_init = P_init * self.pem_input['press_conv'] - #P_init = self._reformat3D_then_flatten(P_init * self.pem_input['press_conv']) - - saturations = [ - 1 - (pem_input['SWAT'] + pem_input['SGAS']) if ph == 'OIL' else pem_input['S{}'.format(ph)] - for ph in phases] - #saturations = [self._reformat3D_then_flatten(1 - (pem_input['SWAT'] + pem_input['SGAS'])) - # if ph == 'OIL' else pem_input['S{}'.format(ph)] for ph in phases] - - # Get the pressure - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init, - ensembleMember=self.ensemble_member) - else: - self.pem.calc_props(phases, saturations, pem_input['PRESSURE'], pem_input['PORO'], - ntg=pem_input['NTG'], Rs=pem_input['RS'], press_init=P_init) - - #grdecl.write(f'En_{str(self.ensemble_member)}/Vs{v + 1}.grdecl', { - # 'Vs': self.pem.getShearVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/Vp{v + 1}.grdecl', { - # 'Vp': self.pem.getBulkVel() * .1, 'DIMENS': grid['DIMENS']}, multi_file=False) - #grdecl.write(f'En_{str(self.ensemble_member)}/rho{v + 1}.grdecl', - # {'rho': self.pem.getDens(), 'DIMENS': grid['DIMENS']}, multi_file=False) - - # vp, vs, density - self.vp = (self.pem.getBulkVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.vs = (self.pem.getShearVel() * .1).reshape((self.NX, self.NY, self.NZ), order='F') - self.rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ), order='F') # in the unit of g/cm^3 - - save_dic = {'vp': self.vp, 'vs': self.vs, 'rho': self.vp} - if save_folder is not None: - file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"vp_vs_rho_vint{v}.npz" - else: - if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - (self.ensemble_member >= 0): - file_name = folder + os.sep + f"vp_vs_rho_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"vp_vs_rho_vint{v}.npz" - else: - file_name = os.getcwd() + os.sep + f"vp_vs_rho_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - # avo data - self._calc_avo_props() - - avo = self.avo_data.flatten(order="F") - - # XLUO: self.ensemble_member < 0 => reference reservoir model in synthetic case studies - # the corresonding (noisy) data are observations in data assimilation - if 'add_synthetic_noise' in self.input_dict and self.ensemble_member < 0: - non_nan_idx = np.argwhere(~np.isnan(avo)) - data_std = np.std(avo[non_nan_idx]) - if self.input_dict['add_synthetic_noise'][0] == 'snr': - noise_std = np.sqrt(self.input_dict['add_synthetic_noise'][1]) * data_std - avo[non_nan_idx] += noise_std * np.random.randn(avo[non_nan_idx].size, 1) - else: - noise_std = 0.0 # simulated data don't contain noise - - save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - if save_folder is not None: - file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"avo_vint{v}.npz" - else: - # if hasattr(self, 'ensemble_member') and (self.ensemble_member is not None) and \ - # (self.ensemble_member >= 0): - # file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - # else folder + f"avo_vint{v}.npz" - # else: - # file_name = os.getcwd() + os.sep + f"avo_vint{v}.npz" - file_name = folder + os.sep + f"avo_vint{v}.npz" if folder[-1] != os.sep \ - else folder + f"avo_vint{v}.npz" - - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) - - return success - - def extract_data(self, member): - # start by getting the data from the flow simulator - super(flow_sim2seis, self).extract_data(member) - - # get the sim2seis from file - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if 'avo' in key: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ - else self.folder + key + '_vint' + str(idx) + '.npz' - with np.load(filename) as f: - self.pred_data[prim_ind][key] = f[key] - - def _runMako(self, folder, state, addfiles=['properties']): - """ - Hard coding, maybe a better way possible - addfiles: additional files that need to be included into ECLIPSE/OPM DATA file - """ - super()._runMako(folder, state) - - lkup = TemplateLookup(directories=os.getcwd(), input_encoding='utf-8') - for file in addfiles: - tmpl = lkup.get_template('%s.mako' % file) - - # use a context and render onto a file - with open('{0}'.format(folder + file), 'w') as f: - ctx = Context(f, **state) - tmpl.render_context(ctx) - - def _get_avo_info(self, avo_config=None): - """ - AVO configuration - """ - # list of configuration parameters in the "AVO" section - config_para_list = ['dz', 'tops', 'angle', 'frequency', 'wave_len', 'vp_shale', 'vs_shale', - 'den_shale', 't_min', 't_max', 't_sampling', 'pp_func'] - if 'avo' in self.input_dict: - self.avo_config = {} - for elem in self.input_dict['avo']: - assert elem[0] in config_para_list, f'Property {elem[0]} not supported' - self.avo_config[elem[0]] = elem[1] - - # if only one angle is considered, convert self.avo_config['angle'] into a list, as required later - if isinstance(self.avo_config['angle'], float): - self.avo_config['angle'] = [self.avo_config['angle']] - - # self._get_DZ(file=self.avo_config['dz']) # =>self.DZ - kw_file = {'DZ': self.avo_config['dz'], 'TOPS': self.avo_config['tops']} - self._get_props(kw_file) - self.overburden = self.pem_input['overburden'] - - # make sure that the "pylops" package is installed - # See https://github.com/PyLops/pylops - self.pp_func = getattr(import_module('pylops.avo.avo'), self.avo_config['pp_func']) - - else: - self.avo_config = None - - def _get_props(self, kw_file): - # extract properties (specified by keywords) in (possibly) different files - # kw_file: a dictionary contains "keyword: file" pairs - # Note that all properties are reshaped into the reservoir model dimension (NX, NY, NZ) - # using the "F" order - for kw in kw_file: - file = kw_file[kw] - if file.endswith('.npz'): - with np.load(file) as f: - exec(f'self.{kw} = f[ "{kw}" ]') - self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] - else: - reader = GRDECL_Parser(filename=file) - reader.read_GRDECL() - exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") - self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ - eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') - - def _calc_avo_props(self, dt=0.0005): - # dt is the fine resolution sampling rate - # convert properties in reservoir model to time domain - vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) - vs_shale = self.avo_config['vs_shale'] # scalar value - rho_shale = self.avo_config['den_shale'] # scalar value - - # Two-way travel time of the top of the reservoir - # TOPS[:, :, 0] corresponds to the depth profile of the reservoir top on the first layer - top_res = 2 * self.TOPS[:, :, 0] / vp_shale - - # Cumulative traveling time trough the reservoir in vertical direction - cum_time_res = np.cumsum(2 * self.DZ / self.vp, axis=2) + top_res[:, :, np.newaxis] - cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) - - # add overburden and underburden of Vp, Vs and Density - vp = np.concatenate((vp_shale * np.ones((self.NX, self.NY, 1)), - self.vp, vp_shale * np.ones((self.NX, self.NY, 1))), axis=2) - vs = np.concatenate((vs_shale * np.ones((self.NX, self.NY, 1)), - self.vs, vs_shale * np.ones((self.NX, self.NY, 1))), axis=2) - rho = np.concatenate((rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001, # kg/m^3 -> k/cm^3 - self.rho, rho_shale * np.ones((self.NX, self.NY, 1)) * 0.001), axis=2) - - # search for the lowest grid cell thickness and sample the time according to - # that grid thickness to preserve the thin layer effect - time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) - if time_sample.shape[0] == 1: - time_sample = time_sample.reshape(-1) - time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) - - vp_sample = np.tile(vp[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - vs_sample = np.tile(vs[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - rho_sample = np.tile(rho[:, :, 1][..., np.newaxis], (1, 1, time_sample.shape[2])) - - for m in range(self.NX): - for l in range(self.NY): - for k in range(time_sample.shape[2]): - # find the right interval of time_sample[m, l, k] belonging to, and use - # this information to allocate vp, vs, rho - idx = np.searchsorted(cum_time[m, l, :], time_sample[m, l, k], side='left') - idx = idx if idx < len(cum_time[m, l, :]) else len(cum_time[m, l, :]) - 1 - vp_sample[m, l, k] = vp[m, l, idx] - vs_sample[m, l, k] = vs[m, l, idx] - rho_sample[m, l, k] = rho[m, l, idx] - - - # from matplotlib import pyplot as plt - # plt.plot(vp_sample[0, 0, :]) - # plt.show() - - #vp_avg = 0.5 * (vp_sample[:, :, 1:] + vp_sample[:, :, :-1]) - #vs_avg = 0.5 * (vs_sample[:, :, 1:] + vs_sample[:, :, :-1]) - #rho_avg = 0.5 * (rho_sample[:, :, 1:] + rho_sample[:, :, :-1]) - - #vp_diff = vp_sample[:, :, 1:] - vp_sample[:, :, :-1] - #vs_diff = vs_sample[:, :, 1:] - vs_sample[:, :, :-1] - #rho_diff = rho_sample[:, :, 1:] - rho_sample[:, :, :-1] - - #R0_smith = 0.5 * (vp_diff / vp_avg + rho_diff / rho_avg) - #G_smith = -2.0 * (vs_avg / vp_avg) ** 2 * (2.0 * vs_diff / vs_avg + rho_diff / rho_avg) + 0.5 * vp_diff / vp_avg - - # PP reflection coefficients, see, e.g., - # "https://pylops.readthedocs.io/en/latest/api/generated/pylops.avo.avo.approx_zoeppritz_pp.html" - # So far, it seems that "approx_zoeppritz_pp" is the only available option - # approx_zoeppritz_pp(vp1, vs1, rho1, vp0, vs0, rho0, theta1) - avo_data_list = [] - - # Ricker wavelet - wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len'], dt), - f0=self.avo_config['frequency']) - - # Travel time corresponds to reflectivity sereis - t = time_sample[:, :, 0:-1] - - # interpolation time - t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) - trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) - - # number of pp reflection coefficients in the vertial direction - nz_rpp = vp_sample.shape[2] - 1 - - for i in range(len(self.avo_config['angle'])): - angle = self.avo_config['angle'][i] - Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], - vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) - - for m in range(self.NX): - for l in range(self.NY): - # convolution with the Ricker wavelet - conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") - w_trace = conv_op * Rpp[m, l, :] - - # Sample the trace into regular time interval - f = interp1d(np.squeeze(t[m, l, :]), np.squeeze(w_trace), - kind='nearest', fill_value='extrapolate') - trace_interp[m, l, :] = f(t_interp) - - if i == 0: - avo_data = trace_interp # 3D - elif i == 1: - avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D - else: - avo_data = np.concatenate((avo_data, trace_interp[:, :, :, np.newaxis]), axis=3) # 4D - - self.avo_data = avo_data - - @classmethod - def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): - """ - XILU: Quantities read by "EclipseData.cell_data" are put in the axis order of [nz, ny, nx]. To be consisent with - ECLIPSE/OPM custom, we need to change the axis order. We further flatten the array according to the specified order - """ - array = np.array(array) - if len(array.shape) != 1: # if array is a 1D array, then do nothing - assert isinstance(array, np.ndarray) and len(array.shape) == 3, "Only 3D numpy arraies are supported" - - # axis [0 (nz), 1 (ny), 2 (nx)] -> [2 (nx), 1 (ny), 0 (nz)] - new_array = np.transpose(array, axes=[2, 1, 0]) - if flatten: - new_array = new_array.flatten(order=order) - - return new_array - else: - return array \ No newline at end of file diff --git a/simulator/rockphysics/softsandrp.py b/simulator/rockphysics/softsandrp.py index 7b7f652..207237c 100644 --- a/simulator/rockphysics/softsandrp.py +++ b/simulator/rockphysics/softsandrp.py @@ -6,7 +6,10 @@ import numpy as np import sys import multiprocessing as mp - +from CoolProp.CoolProp import PropsSI # http://coolprop.org/#high-level-interface-example +import CoolProp.CoolProp as CP +# Density of carbon dioxide at 100 bar and 25C # Smeaheia 37 degrees C +#rho_co2 = PropsSI('D', 'T', 298.15, 'P', 100e5, 'CO2') from numpy.random import poisson # internal load @@ -192,13 +195,15 @@ def calc_props(self, phases, saturations, pressure, # Calculate fluid properties # if dens is None: + densf_SI = self._fluid_densSIprop(self.phases, + saturations[i, :], pressure[i]) densf, bulkf = \ self._fluidprops_Wood(self.phases, saturations[i, :], pressure[i], Rs[i]) else: densf = self._fluid_dens(saturations[i, :], dens[i, :]) - bulkf = self._fluidprops_Brie(self.phases, saturations[i, :], pressure[i]) + bulkf = self._fluidprops_Brie(self.phases, saturations[i, :], pressure[i], densf) # #denss, bulks, shears = \ # self._solidprops(porosity[i], ntg[i], i) @@ -325,6 +330,35 @@ def getPorosity(self): # Fluid properties start # =================================================== # + def _fluid_densSIprop(self, phases, fsats, press, t= 37, CO2 = None): + + conv2Pa = 1e6 # MPa to Pa + ta = t + 273.15 # absolute temp in K + # fluid densities + fdens = 0.0 + + for i in range(len(phases)): + # + # Calculate mixture properties by summing + # over individual phase properties + # + + var = phases[i] + if var == 'GAS' and CO2 is None: + pdens = PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'Methane') + elif var == 'GAS' and CO2 is True: + pdens = PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'CO2') + elif var == 'OIL': + CP.get_global_param_string('predefined_mixtures').split(',')[0:6] + #pdens = CP.PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'Ekofisk.mix') + pdens = CP.PropsSI('D', 'T', ta, 'P', press * conv2Pa, 'butane') + elif var == 'WAT': + pdens = PropsSI('D', 'T|liquid', ta, 'P', press * conv2Pa, 'Water') + + fdens = fdens + fsats[i] * abs(pdens) + + return fdens + def _fluidprops_Wood(self, fphases, fsats, fpress, Rs=None): # # Calculate fluid density and bulk modulus @@ -372,7 +406,7 @@ def _fluid_dens(self, fsatsp, fdensp): fdens = sum(fsatsp * fdensp) return fdens - def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): + def _fluidprops_Brie(self, fphases, fsats, fpress, fdens, Rs=None, e = 5): # # Calculate fluid density and bulk modulus BRIE et al. 1995 # Assumes two phases liquid and gas @@ -382,6 +416,7 @@ def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): # and/or Water and/or Gas # fsats - fluid saturation values for # fluid phases in "fphases" + # fdens - fluid density for given pressure and temperature # fpress - fluid pressure value (MPa) # Rs - Gas oil ratio. Default value None # e - Brie's exponent (e= 5 Utsira sand filled with brine and CO2 @@ -402,9 +437,9 @@ def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): # if fphases[i].lower() in ["oil", "wat"]: fsatsl = fsats[i] - pbulkl = self._phaseprops_Smeaheia(fphases[i], fpress, Rs) + pbulkl = self._phaseprops_Smeaheia(fphases[i], fpress, fdens, Rs) elif fphases[i].lower() in ["gas"]: - pbulkg = self._phaseprops_Smeaheia(fphases[i], fpress, Rs) + pbulkg = self._phaseprops_Smeaheia(fphases[i], fpress, fdens, Rs) fbulk = (pbulkl - pbulkg) * (fsatsl)**e + pbulkg @@ -414,7 +449,53 @@ def _fluidprops_Brie(self, fphases, fsats, fpress, Rs=None, e = 5): # # --------------------------------------------------- # - def _phaseprops_Smeaheia(self, fphase, press, Rs=None): + @staticmethod + def pseudo_p_t(pres, t, gs): + """Calculate the pseudoreduced temperature and pressure according to Thomas et al. 1970. + + Parameters + ---------- + pres : float or array-like + Pressure in MPa + t : float or array-like + Temperature in °C + gs : float + Gas gravity + + Returns + ------- + float or array-like + Ta: absolute temperature + Ppr:pseudoreduced pressure + Tpr:pseudoreduced temperature + """ + + # convert the temperature to absolute temperature + ta = t + 273.15 + p_pr = pres / (4.892 - 0.4048 * gs) + t_pr = ta / (94.72 + 170.75 * gs) + return ta, p_pr, t_pr + # + # --------------------------------------------------- + # + @staticmethod + def dz_dp(p_pr, t_pr): + """Values for dZ/dPpr obtained from equation 10b in Batzle and Wang (1992). + """ + # analytic + dz_dp = (0.03 + 0.00527 * (3.5 - t_pr) ** 3) + 0.109 * (3.85 - t_pr) ** 2 * 1.2 * p_pr ** 0.2 * -( + 0.45 + 8 * (0.56 - 1 / t_pr) ** 2) / t_pr * np.exp( + -(0.45 + 8 * (0.56 - 1 / t_pr) ** 2) * p_pr ** 1.2 / t_pr) + + # numerical approximation + # dzdp= 1.938783*P_pr**0.2*(1 - 0.25974025974026*T_pr)**2*(-8*(0.56 - 1/T_pr)**2 - 0.45)* + # np.exp(P_pr**1.2*(-8*(0.56 - 1/T_pr)**2 - 0.45)/T_pr)/T_pr + 0.22595125*(1 - 0.285714285714286*T_pr)**3 + # + 0.03 + return dz_dp + # + #----------------------------------------------------------- + # + def _phaseprops_Smeaheia(self, fphase, press, fdens, Rs=None, t = 37, CO2 = None): # # Calculate properties for a single fluid phase # @@ -422,6 +503,8 @@ def _phaseprops_Smeaheia(self, fphase, press, Rs=None): # Input # fphase - fluid phase; Oil, Water or Gas # press - fluid pressure value (MPa) + # fdens - fluid density (kg/m3) + # t - temperature in degrees C # # Output # pbulk - bulk modulus of fluid phase @@ -429,42 +512,96 @@ def _phaseprops_Smeaheia(self, fphase, press, Rs=None): # "press" (MPa) # # ----------------------------------------------- - # - if fphase.lower() == "wat": # refers to water in Smeaheia - press_range = np.array([0.10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) - # Bo values assume Rs = 0 - Bo_values = np.array( - [1.00469, 1.00430, 1.00387, 1.00345, 1.00302, 1.00260, 1.00218, 1.00176, 1.00134, 1.00092, 1.00050, - 1.00008, 0.99967, 0.99925, 0.99884, 0.99843, 0.99802, 0.99761, 0.99720, 0.99679, 0.99638]) + # References + # ---------- + # Xu, H. (2006). Calculation of CO2 acoustic properties using Batzle-Wang equations. Geophysics, 71(2), F21-F23. + # """ + + if fphase.lower() == "wat": # refers to pure water or brine + #Compute the bulk modulus of pure water as a function of temperature and pressure + #using Batzle and Wang (1992). + if np.any(press > 100): + print('pressures above about 100 MPa-> inaccurate estimations of water velocity') + w = np.array([[1.40285e+03, 1.52400e+00, 3.43700e-03, -1.19700e-05], + [4.87100e+00, -1.11000e-02, 1.73900e-04, -1.62800e-06], + [-4.78300e-02, 2.74700e-04, -2.13500e-06, 1.23700e-08], + [1.48700e-04, -6.50300e-07, -1.45500e-08, 1.32700e-10], + [-2.19700e-07, 7.98700e-10, 5.23000e-11, -4.61400e-13]]) + v_w = sum(w[i, j] * t ** i * press ** j for i in range(5) for j in range(4)) # m/s + K_w = fdens * v_w ** 2 * 1e-6 + if CO2 is True: # refers to brine + salinity = 35000 / 1000000 + s1 = 1170 - 9.6 * t + 0.055 * t ** 2 - 8.5e-5 * t ** 3 + 2.6 * press - 0.0029 * t * press - 0.0476 * press ** 2 + s15 = 780 - 10 * press + 0.16 * press ** 2 + s2 = -820 + v_b = v_w + s1 * salinity + s15 * salinity ** 1.5 + s2 * salinity ** 2 + x = 300 * press - 2400 * press * salinity + t * (80 + 3 * t - 3300 * salinity - 13 * press + 47 * press * salinity) + rho_b = fdens + salinity * (0.668 + 0.44 * salinity + 1e-6 * x) + pbulk = rho_b * v_b ** 2 * 1e-6 + else: + pbulk = K_w + elif fphase.lower() == "gas" and CO2 is True: # refers to CO2 + R = 8.3145 # J.mol-1K-1 gas constant for CO2 + gs = 1.5189 # Specific gravity #https://www.engineeringtoolbox.com/specific-gravities-gases-d_334.html + ta, p_pr, t_pr = self.pseudo_p_t(press, t, gs) + + E = 0.109 * (3.85 - t_pr) ** 2 * np.exp(-(0.45 + 8 * (0.56 - 1 / t_pr) ** 2) * p_pr ** 1.2 / t_pr) + Z = (0.03 + 0.00527 * (3.5 - t_pr) ** 3) * p_pr + (0.642 * t_pr - 0.007 * t_pr ** 4 - 0.52) + E + rho = 28.8 * gs * press / (Z * R * ta) # g/cm3 + + r_0 = 0.85 + 5.6 / (p_pr + 2) + 27.1 / (p_pr + 3.5) ** 2 - 8.7 * np.exp(-0.65 * (p_pr + 1)) + dz_dp = self.dz_dp(p_pr, t_pr) + pbulk = press / (1 - p_pr * dz_dp / Z) * r_0 + + pbulk_test = self.test_new_implementation(press) + print(np.max(pbulk-pbulk_test)) + + elif fphase.lower() == "gas": # refers to Methane + gs = 0.5537 #https://www.engineeringtoolbox.com/specific-gravities-gases-d_334.html + R = 8.3145 # J.mol-1K-1 gas constant + ta, p_pr, t_pr = self.pseudo_p_t(press, t, gs) + E = 0.109 * (3.85 - t_pr) ** 2 * np.exp(-(0.45 + 8 * (0.56 - 1 / t_pr) ** 2) * p_pr ** 1.2 / t_pr) + Z = (0.03 + 0.00527 * (3.5 - t_pr) ** 3) * p_pr + (0.642 * t_pr - 0.007 * t_pr ** 4 - 0.52) + E + rho = 28.8 * gs * press / (Z * R * ta) # g/cm3 + + r_0 = 0.85 + 5.6 / (p_pr + 2) + 27.1 / (p_pr + 3.5) ** 2 - 8.7 * np.exp(-0.65 * (p_pr + 1)) + dz_dp = self.dz_dp(p_pr, t_pr) + pbulk = press / (1 - p_pr * dz_dp / Z) * r_0 + + elif fphase.lower() == "oil": #pure oil + # Estimate the oil bulk modulus at specific temperature and pressure. + v = 2096 * (fdens / (2600 - fdens)) ** 0.5 - 3.7 * t + 4.64 * press + 0.0115 * ( + 4.12 * (1080 / fdens - 1) ** 0.5 - 1) * t * press + pbulk = fdens * v ** 2 - elif fphase.lower() == "gas": - # Values from .DATA file for Smeaheia (converted to MPa) - press_range = np.array( - [0.101, 0.885, 1.669, 2.453, 3.238, 4.022, 4.806, 5.590, 6.2098, 7.0899, 7.6765, 8.2630, 8.8495, 9.4359, - 10.0222, 10.6084, 11.1945, 14.7087, 17.6334, 20.856, 23.4695, 27.5419]) # Example pressures in MPa - Bo_values = np.array( - [1.07365, 0.11758, 0.05962, 0.03863, 0.02773, 0.02100, 0.01639, 0.01298, 0.010286, 0.007578, 0.005521, - 0.003314, 0.003034, 0.002919, 0.002851, 0.002802, 0.002766, 0.002648, 0.002599, 0.002566, 0.002546, - 0.002525]) # Example formation volume factors in m^3/kg + + # + return pbulk + + # + def test_new_implementation(self, press): + # Values from .DATA file for Smeaheia (converted to MPa) + press_range = np.array( + [0.101, 0.885, 1.669, 2.453, 3.238, 4.022, 4.806, 5.590, 6.2098, 7.0899, 7.6765, 8.2630, 8.8495, 9.4359, + 10.0222, 10.6084, 11.1945, 14.7087, 17.6334, 20.856, 23.4695, 27.5419]) # Example pressures in MPa + Bo_values = np.array( + [1.07365, 0.11758, 0.05962, 0.03863, 0.02773, 0.02100, 0.01639, 0.01298, 0.010286, 0.007578, 0.005521, + 0.003314, 0.003034, 0.002919, 0.002851, 0.002802, 0.002766, 0.002648, 0.002599, 0.002566, 0.002546, + 0.002525]) # Example formation volume factors in m^3/kg # Calculate numerical derivative of Bo with respect to Pressure dBo_dP = - np.gradient(Bo_values, press_range) # Calculate isothermal compressibility (van der Waals) compressibility = (1 / Bo_values) * dBo_dP # Resulting array of compressibility values bulk_mod = 1 / compressibility - # # Find the index of the closest pressure value in b closest_index = (np.abs(press_range - press)).argmin() # Extract the corresponding value from a - pbulk = bulk_mod[closest_index] - - # - return pbulk - - # + pbulk_test = bulk_mod[closest_index] + return pbulk_test def _phaseprops(self, fphase, press, Rs=None): # diff --git a/simulator/rockphysics/standardrp.py b/simulator/rockphysics/standardrp.py index e05afd3..dde7b09 100644 --- a/simulator/rockphysics/standardrp.py +++ b/simulator/rockphysics/standardrp.py @@ -216,8 +216,7 @@ def calc_props(self, phases, saturations, pressure, # # Density # - self.dens[i] = (porosity[i]*densf + - (1-porosity[i])*denss) + self.dens[i] = (porosity[i]*densf + (1-porosity[i])*denss) # # Moduli # From 00295b703140876465f8467c7a33924bb489be53 Mon Sep 17 00:00:00 2001 From: mlie Date: Thu, 22 May 2025 14:49:30 +0200 Subject: [PATCH 11/15] stash issue resolve --- simulator/flow_rock.py | 56 +++++++++--------------------------------- 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 9bec3ad..605160d 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -26,7 +26,7 @@ from pylops.utils.wavelets import ricker from pylops.signalprocessing import Convolve1D import sys -from PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master +#from PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master from scipy.interpolate import interp1d from scipy.interpolate import griddata from pipt.misc_tools.analysis_tools import store_ensemble_sim_information @@ -759,14 +759,12 @@ def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i -<<<<<<< Updated upstream - return super().run_fwd_sim(state, member_i, del_folder=del_folder) - -======= #return super().run_fwd_sim(state, member_i, del_folder=del_folder) + + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=del_folder) return self.pred_data ->>>>>>> Stashed changes + def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): # replace the sim2seis part (which is unusable) by avo based on Pylops @@ -909,10 +907,7 @@ def get_avo_result(self, folder, save_folder): def calc_velocities(self, folder, save_folder, grid, v, f_dim): # The properties in pem are only given in the active cells # indices of active cells: -<<<<<<< Updated upstream -======= ->>>>>>> Stashed changes true_indices = np.where(grid['ACTNUM']) # Alt 2 @@ -959,7 +954,7 @@ def calc_velocities(self, folder, save_folder, grid, v, f_dim): save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, #'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': porosity} # for debugging else: - save_dic = {'vp': vp, 'vs': vs, 'rho': rho, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} + save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} if save_folder is not None: file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ @@ -1033,12 +1028,12 @@ def _get_props(self, kw_file): with np.load(file) as f: exec(f'self.{kw} = f[ "{kw}" ]') self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] - else: - reader = GRDECL_Parser(filename=file) - reader.read_GRDECL() - exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") - self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ - eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') + #else: + # reader = GRDECL_Parser(filename=file) + # reader.read_GRDECL() + # exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") + # self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ + # eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') def _calc_avo_props(self, dt=0.0005): # dt is the fine resolution sampling rate @@ -2260,34 +2255,7 @@ def van_opstal(self, lambda_vals, z_res, z_base, poisson): return value - def van_opstal_org(self, lambda_vals, z_res, z_base, poisson): - """ - Compute the Van Opstal transfer function. - - Args: - lambda_vals -- Numpy array of lambda values. - z_res -- Depth to reservoir [m]. - z_base -- Distance to the basement [m]. - poisson -- Poisson's ratio. - - Returns: - value -- Numpy array of computed values. - """ - - term1 = np.exp(lambda_vals * z_res) * (2 * lambda_vals * z_base + 1) - term2 = np.exp(-lambda_vals * z_res) * ( - 4 * lambda_vals ** 2 * z_base ** 2 + 2 * lambda_vals * z_base + (3 - 4 * poisson) ** 2) - - term3_numer = (3 - 4 * poisson) * ( - np.exp(-lambda_vals * (2 * z_base + z_res)) - np.exp(-lambda_vals * (2 * z_base - z_res))) - term3_denom = 2 * ((1 - 2 * poisson) ** 2 + lambda_vals ** 2 * z_base ** 2 + (3 - 4 * poisson) * np.cosh( - lambda_vals * z_base) ** 2) - - value = term1 - term2 - (term3_numer / term3_denom) - - return value - - def hankel_transform_order_0(f, r_max, num_points=1000): + def hankel_transform_order_0(self, f, r_max, num_points=1000): """ Computes the Hankel transform of order 0 of a function f(r). From 9d5a8ba6c22e1f2bc5fd67b331cb83593eca9f23 Mon Sep 17 00:00:00 2001 From: mlienorce Date: Fri, 23 May 2025 13:30:56 +0200 Subject: [PATCH 12/15] Subsidence (#97) * subsidence * stash issue resolve --- simulator/flow_rock.py | 55 ++++++++++-------------------------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 9bec3ad..5b12db9 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -26,7 +26,7 @@ from pylops.utils.wavelets import ricker from pylops.signalprocessing import Convolve1D import sys -from PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master +#from PyGRDECL.GRDECL_Parser import GRDECL_Parser # https://github.com/BinWang0213/PyGRDECL/tree/master from scipy.interpolate import interp1d from scipy.interpolate import griddata from pipt.misc_tools.analysis_tools import store_ensemble_sim_information @@ -759,14 +759,13 @@ def run_fwd_sim(self, state, member_i, del_folder=True): # The inherited simulator also has a run_fwd_sim. Call this. self.ensemble_member = member_i -<<<<<<< Updated upstream - return super().run_fwd_sim(state, member_i, del_folder=del_folder) -======= #return super().run_fwd_sim(state, member_i, del_folder=del_folder) + + self.pred_data = super().run_fwd_sim(state, member_i, del_folder=del_folder) return self.pred_data ->>>>>>> Stashed changes + def call_sim(self, folder=None, wait_for_proc=False, run_reservoir_model=None, save_folder=None): # replace the sim2seis part (which is unusable) by avo based on Pylops @@ -909,10 +908,7 @@ def get_avo_result(self, folder, save_folder): def calc_velocities(self, folder, save_folder, grid, v, f_dim): # The properties in pem are only given in the active cells # indices of active cells: -<<<<<<< Updated upstream -======= ->>>>>>> Stashed changes true_indices = np.where(grid['ACTNUM']) # Alt 2 @@ -959,7 +955,7 @@ def calc_velocities(self, folder, save_folder, grid, v, f_dim): save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, #'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': porosity} # for debugging else: - save_dic = {'vp': vp, 'vs': vs, 'rho': rho, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} + save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} if save_folder is not None: file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ @@ -1033,12 +1029,12 @@ def _get_props(self, kw_file): with np.load(file) as f: exec(f'self.{kw} = f[ "{kw}" ]') self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] - else: - reader = GRDECL_Parser(filename=file) - reader.read_GRDECL() - exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") - self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ - eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') + #else: + # reader = GRDECL_Parser(filename=file) + # reader.read_GRDECL() + # exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") + # self.NX, self.NY, self.NZ = reader.NX, reader.NY, reader.NZ + # eval(f'np.savez("./{kw}.npz", {kw}=self.{kw}, NX=self.NX, NY=self.NY, NZ=self.NZ)') def _calc_avo_props(self, dt=0.0005): # dt is the fine resolution sampling rate @@ -1249,6 +1245,7 @@ def _calc_avo_props_active_cells(self, grid, vp, vs, rho, dt=0.0005): kind='nearest', fill_value='extrapolate') trace_interp[ind, :] = f(t_interp) + if i == 0: avo_data = trace_interp # 3D elif i == 1: @@ -2260,34 +2257,8 @@ def van_opstal(self, lambda_vals, z_res, z_base, poisson): return value - def van_opstal_org(self, lambda_vals, z_res, z_base, poisson): - """ - Compute the Van Opstal transfer function. - - Args: - lambda_vals -- Numpy array of lambda values. - z_res -- Depth to reservoir [m]. - z_base -- Distance to the basement [m]. - poisson -- Poisson's ratio. - - Returns: - value -- Numpy array of computed values. - """ - - term1 = np.exp(lambda_vals * z_res) * (2 * lambda_vals * z_base + 1) - term2 = np.exp(-lambda_vals * z_res) * ( - 4 * lambda_vals ** 2 * z_base ** 2 + 2 * lambda_vals * z_base + (3 - 4 * poisson) ** 2) - - term3_numer = (3 - 4 * poisson) * ( - np.exp(-lambda_vals * (2 * z_base + z_res)) - np.exp(-lambda_vals * (2 * z_base - z_res))) - term3_denom = 2 * ((1 - 2 * poisson) ** 2 + lambda_vals ** 2 * z_base ** 2 + (3 - 4 * poisson) * np.cosh( - lambda_vals * z_base) ** 2) - - value = term1 - term2 - (term3_numer / term3_denom) - - return value + def hankel_transform_order_0(self, f, r_max, num_points=1000): - def hankel_transform_order_0(f, r_max, num_points=1000): """ Computes the Hankel transform of order 0 of a function f(r). From 406f0cfd75dcfaf2e98d199fe808f02d7917b622 Mon Sep 17 00:00:00 2001 From: Kjersti <48013553+kjei@users.noreply.github.com> Date: Thu, 2 Oct 2025 13:57:57 +0200 Subject: [PATCH 13/15] Compression also for 2D data (#107) --- pipt/misc_tools/wavelet_tools.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pipt/misc_tools/wavelet_tools.py b/pipt/misc_tools/wavelet_tools.py index a907cb6..ac206de 100644 --- a/pipt/misc_tools/wavelet_tools.py +++ b/pipt/misc_tools/wavelet_tools.py @@ -16,7 +16,6 @@ class SparseRepresentation: def __init__(self, options): # options: dim, actnum, level, wname, colored_noise, threshold_rule, th_mult, # use_hard_th, keep_ca, inactive_value - # dim must be given as (nz,ny,nx) self.options = options self.num_grid = np.prod(self.options['dim']) @@ -30,7 +29,7 @@ def __init__(self, options): self.ca_leading_index = None self.ca_leading_coeff = None - # Function to doing image compression. If the function is called without threshold, then the leading indices must + # Function for image compression. If the function is called without threshold, then the leading indices must # be defined in the class. Typically, this is done by running the compression on true data with a given threshold. def compress(self, data, th_mult=None): if ('inactive_value' not in self.options) or (self.options['inactive_value'] is None): @@ -43,13 +42,10 @@ def compress(self, data, th_mult=None): if 'min_noise' not in self.options: self.options['min_noise'] = 1.0e-9 signal = signal.reshape(self.options['dim'], order=self.options['order']) - # get the signal back into its original shape (nx,ny,nz) - signal = signal.transpose((2, 1, 0)) - # pywt throws a warning in case of single-dimentional entries in the shape of the signal. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - wdec = pywt.wavedecn( - signal, self.options['wname'], 'symmetric', int(self.options['level'])) + + # Wavelet decomposition + wdec = pywt.wavedecn(signal, self.options['wname'], 'symmetric', int(self.options['level'])) + wdec_rec = deepcopy(wdec) # Perform thresholding if the threshold is given as input. @@ -65,7 +61,12 @@ def compress(self, data, th_mult=None): # Initialize # Note: the keys below are organized the same way as in Matlab. - keys = ['daa', 'ada', 'dda', 'aad', 'dad', 'add', 'ddd'] + if signal.ndim == 3: + keys = ['daa', 'ada', 'dda', 'aad', 'dad', 'add', 'ddd'] + details = 'ddd' + elif signal.ndim == 2: + keys = ['da', 'ad', 'dd'] + details = 'dd' if true_data: for level in range(0, int(self.options['level'])+1): num_subband = 1 if level == 0 else len(keys) @@ -85,7 +86,7 @@ def compress(self, data, th_mult=None): # In the white noise case estimated std is based on the high (hhh) subband only if true_data and not self.options['colored_noise']: - subband_hhh = wdec[-1]['ddd'].flatten() + subband_hhh = wdec[-1][details].flatten() est_noise_level = np.median( np.abs(subband_hhh - np.median(subband_hhh))) / 0.6745 # estimated noise std est_noise_level = np.maximum(est_noise_level, self.options['min_noise']) @@ -224,10 +225,10 @@ def reconstruct(self, wdec_rec): print('No signal to reconstruct') sys.exit(1) + # reconstruct from wavelet coefficients data_rec = pywt.waverecn(wdec_rec, self.options['wname'], 'symmetric') - data_rec = data_rec.transpose((2, 1, 0)) # flip the axes - dim = self.options['dim'] - data_rec = data_rec[0:dim[0], 0:dim[1], 0:dim[2]] # severe issure here + data_rec = data_rec[tuple(slice(0, s) for s in self.options['dim'])] + data_rec = data_rec.flatten(order=self.options['order']) data_rec = data_rec[self.options['mask']] From 50eceb58ae71fb07673cb63389880f98633bcbb4 Mon Sep 17 00:00:00 2001 From: mlie Date: Wed, 5 Nov 2025 09:54:11 +0100 Subject: [PATCH 14/15] Smeaheia update and GIES modifications --- pipt/misc_tools/qaqc_tools.py | 1013 ++++++++++++++++++++++-- simulator/flow_rock.py | 1125 ++++++++++++++++----------- simulator/rockphysics/softsandrp.py | 7 +- 3 files changed, 1617 insertions(+), 528 deletions(-) diff --git a/pipt/misc_tools/qaqc_tools.py b/pipt/misc_tools/qaqc_tools.py index 5ee3da4..adddf01 100644 --- a/pipt/misc_tools/qaqc_tools.py +++ b/pipt/misc_tools/qaqc_tools.py @@ -1,4 +1,5 @@ """Quality Assurance of the forecast (QA) and analysis (QC) step.""" +import copy import numpy as np import os # import matplotlib as mpl @@ -12,20 +13,19 @@ from pipt.misc_tools import cov_regularization from scipy.interpolate import interp1d from scipy.io import loadmat -# import cv2 +import cv2 # Define the class for qa/qc tools. class QAQC: """ Perform Quality Assurance of the forecast (QA) and analysis (QC) step. - Available functions (developed in 4DSEIS project and not available yet): - - - `calc_coverage`: check forecast data coverage - - `calc_mahalanobis`: evaluate "higher-order" data coverage - - `calc_kg`: check/write individual gain for parameters; - - flag data which have conflicting updates - - `calc_da_stat`: compute statistics for updated parameters + Available functions: + 1) calc_coverage: check forecast data coverage + 2) calc_mahalanobis: evaluate "higher-order" data coverage + 3) calc_kg: check/write individual gain for parameters; + flag data which have conflicting updates + 4) calc_da_stat: compute statistics for updated parameters Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS """ @@ -42,20 +42,40 @@ def __init__(self, keys, obs_data, datavar, logger=None, prior_info=None, sim=No format='%(asctime)s : %(levelname)s : %(name)s : %(message)s') self.logger = logging.getLogger('QAQC') else: - self.logger = logging.getLogger('PET.PIPT.QCQA') + self.logger = logger self.prior_info = prior_info # prior info for the different parameter types - # this class contains potential writing functions (this class can be saved to debug_analysis) - self.sim = sim + self.sim = sim # this class contains potential writing functions (this class can be saved to debug_analysis) self.ini_state = ini_state # the first state; used to compute statistics self.ne = 0 - if self.ini_state is not None: - # get the ensemble size from here - self.ne = self.ini_state[list(self.ini_state.keys())[0]].shape[1] + if 'multilevel' in keys: + self.multilevel = keys['multilevel'] + for i, opt in enumerate(list(zip(*self.multilevel))[0]): + if opt == 'levels': + self.tot_level = int(self.multilevel[i][1]) + if opt == 'en_size': + self.ml_ne = [int(el) for el in self.multilevel[i][1]] + if opt == 'cov_wgt': + try: + cov_mat_wgt = [float(elem) for elem in [item for item in self.multilevel[i][1]]] + except: + cov_mat_wgt = [float(item) for item in self.multilevel[i][1]] + Sum = 0 + for i in range(len(cov_mat_wgt)): + Sum += cov_mat_wgt[i] + for i in range(len(cov_mat_wgt)): + cov_mat_wgt[i] /= Sum + self.cov_wgt = cov_mat_wgt + self.list_state = list(self.ini_state[0].keys()) + else: + if self.ini_state is not None: + self.ne = self.ini_state[list(self.ini_state.keys())[0]].shape[1] # get the ensemble size from here + self.list_state = list(self.ini_state.keys()) - assim_step = 0 # Assume simultaneous assimiation - assim_ind = [keys['obsname'], keys['assimindex'][assim_step]] + #assim_step = 0 # Assume simultaneous assimiation + #assim_ind = [keys['obsname'], keys['assimindex'][assim_step]] + assim_ind = [keys['obsname'], keys['assimindex']] if isinstance(assim_ind[1], list): # Check if prim. ind. is a list - self.l_prim = [int(x) for x in assim_ind[1]] + self.l_prim = [int(x[0]) for x in assim_ind[1]] else: # Float self.l_prim = [int(assim_ind[1])] @@ -67,8 +87,9 @@ def __init__(self, keys, obs_data, datavar, logger=None, prior_info=None, sim=No for typ in self.data_types: self.en_obs[typ] = np.array( [self.obs_data[ind][typ].flatten() for ind in self.l_prim if self.obs_data[ind][typ] - is not None and self.obs_data[ind][typ].shape == (1,)]) + is not None and sum(np.isnan(self.obs_data[ind][typ])) == 0 and self.obs_data[ind][typ].shape == (1,)]) l = [self.obs_data[ind][typ].flatten() for ind in self.l_prim if self.obs_data[ind][typ] is not None + and sum(np.isnan(self.obs_data[ind][typ])) == 0 and self.obs_data[ind][typ].shape[0] > 1] if l: self.en_obs_vec[typ] = np.expand_dims(np.concatenate(l), 1) @@ -93,6 +114,8 @@ def __init__(self, keys, obs_data, datavar, logger=None, prior_info=None, sim=No self.pred_data = None self.state = None self.en_fcst = {} + self.en_ml_fcst = {} + self.en_ml_fcst_vec = {} self.en_fcst_vec = {} self.lam = None @@ -100,32 +123,338 @@ def __init__(self, keys, obs_data, datavar, logger=None, prior_info=None, sim=No def set(self, pred_data, state=None, lam=None): self.pred_data = pred_data for typ in self.data_types: - self.en_fcst[typ] = np.array( - [self.pred_data[ind][typ].flatten() for ind in self.l_prim if self.obs_data[ind][typ] - is not None and self.obs_data[ind][typ].shape == (1,)]) - l = [self.pred_data[ind][typ] for ind in self.l_prim if self.obs_data[ind][typ] is not None - and self.obs_data[ind][typ].shape[0] > 1] - if l: - self.en_fcst_vec[typ] = np.concatenate(l) + if hasattr(self, 'multilevel'): + self.en_ml_fcst[typ] = [np.array([self.pred_data[ind][l][typ].flatten() + for ind in self.l_prim if sum(np.isnan(self.obs_data[ind][typ])) == 0 + and self.obs_data[ind][typ].shape == (1,)]) for l in + range(self.tot_level)] + # todo: for vector data + + self.en_fcst[typ] = np.concatenate(self.en_ml_fcst[typ], axis=1) # merge all levels + else: + self.en_fcst[typ] = np.array( + [self.pred_data[ind][typ].flatten() for ind in self.l_prim if + self.obs_data[ind][typ] is not None and + sum(np.isnan(self.obs_data[ind][typ])) == 0 + and self.obs_data[ind][typ].shape == (1,)]) + l = [self.pred_data[ind][typ] for ind in self.l_prim if + self.obs_data[ind][typ] is not None + and sum(np.isnan(self.obs_data[ind][typ])) == 0 + and self.obs_data[ind][typ].shape[0] > 1] + if l: + self.en_fcst_vec[typ] = np.concatenate(l) self.state = state self.lam = lam - def calc_coverage(self, line=None): + def calc_coverage(self, line=None, field_dim=None, uxl = None, uil = None, contours = None, uxl_c = None, uil_c = None): """ Calculate the Data coverage for production and seismic data. For seismic data the plotting is based on the importance-scaled coverage developed by Espen O. Lie from GeoCore. - Parameters - ---------- - line : array-like, optional - If not None, plot 1D coverage. + Input: + line: if not None, plot 1d coverage + field_dim: if None, must import utm coordinates. Else give the grid - Notes - ----- - - Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS - - Not available in current version of PIPT + Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS """ + def _colorline(x, y, z=None, cmap='copper', norm=plt.Normalize(0.0, 1.0), + linewidth=3, alpha=1.0): + """ + http://nbviewer.ipython.org/github/dpsanders/matplotlib-examples/blob/master/colorline.ipynb + http://matplotlib.org/examples/pylab_examples/multicolored_line.html + Plot a colored line with coordinates x and y + Optionally specify colors in the array z + Optionally specify a colormap, a norm function and a line width + """ + + # Default colors equally spaced on [0,1]: + if z is None: + z = np.linspace(0.0, 1.0, len(x)) + + # Special case if a single number: + # to check for numerical input -- this is a hack + if not hasattr(z, "__iter__"): + z = np.array([z]) + + z = np.asarray(z) + + segments = _make_segments(x, y) + lc = mcoll.LineCollection(segments, array=z, cmap=cmap, norm=norm, + linewidth=linewidth, alpha=alpha) + + ax = plt.gca() + ax.add_collection(lc) + + return lc + + def _make_segments(x, y): + """ + Create list of line segments from x and y coordinates, in the correct format + for LineCollection: an array of the form numlines x (points per line) x 2 (x + and y) array + """ + + points = np.array([x, y]).T.reshape(-1, 1, 2) + segments = np.concatenate([points[:-1], points[1:]], axis=1) + return segments + + def _plot_coverage_1D(line, field_dim): + x = np.array([-1, -np.finfo(float).eps, 0, .5, 1, 1 + np.finfo(float).eps, 2]) + d_ens = np.squeeze(data_reg[:, int(line), :]) + d_real = np.squeeze(data_real_reg[:, int(line)]) + scale = max(d_real) # 2.5 + + r = np.array([0.1, 0.3, 0.8, 1.0, 0.8, 0.7, 0.5]) + f = interp1d(x, r) + ri = f(3 * np.arange(256) / 255 - 1) + g = np.array([0.1, 0.3, 0.9, 1.0, 0.9, 0.4, 0.2]) + f = interp1d(x, g) + gi = f(3 * np.arange(256) / 255 - 1) + b = np.array([0.4, 0.6, 0.8, 1.0, 0.8, 0.4, 0.2]) + f = interp1d(x, b) + bi = f(3 * np.arange(256) / 255 - 1) + + d_min = np.min(d_ens, axis=1) + d_max = np.max(d_ens, axis=1) + nl + sat = 2 * np.minimum((d_max + d_real) / scale, 0.5) + sat = (sat - nl) / (1 - nl) + sc = d_max - d_min + + attr = (d_real - d_min) / sc + attr = np.minimum(np.maximum(attr, -1), 2) + + try: + uxl = loadmat('seglines.mat')['uxl'].flatten() + except: + uxl = [0, field_dim[0]] + + uxl = np.arange(uxl[0], uxl[-1], (uxl[-1] - uxl[0]) / data_real_reg.shape[0]) + x = np.concatenate((uxl, np.flip(uxl))) + y = np.concatenate((d_min, np.flip(d_max))) + + # plot not scaled by importance + fig = plt.figure() + ax = fig.add_subplot() + right_side = ax.spines["right"] + right_side.set_visible(False) + top_side = ax.spines["top"] + top_side.set_visible(False) + poly = pat.Polygon(np.column_stack((x, y)), closed=False, edgecolor='k', facecolor=np.array([.7, .7, .7])) + ax.add_patch(poly) + ln = _colorline(uxl, d_real, attr, None, plt.Normalize(-1, 2)) + c = np.column_stack((ri, gi, bi)) + cm = ListedColormap(c) + ln.set_cmap(cm) + plt.colorbar(ln) + plt.xlim(uxl[0] - np.finfo(float).eps, uxl[-1] + np.finfo(float).eps) + plt.ylim(0, scale) + plt.title('1D coverage plot not scaled by Importance') + filename = self.folder + 'coverage_1d_vint_' + str(vint) + plt.savefig(filename) + os.system('convert ' + filename + '.png' + ' -trim ' + filename + '.png') + + # plot scaled by importance + fig = plt.figure() + ax = fig.add_subplot() + right_side = ax.spines["right"] + right_side.set_visible(False) + top_side = ax.spines["top"] + top_side.set_visible(False) + poly = pat.Polygon(np.column_stack((x, y)), closed=False, edgecolor='k', facecolor=np.array([.7, .7, .7])) + ax.add_patch(poly) + ln = _colorline(uxl, d_real, attr, None, plt.Normalize(-1, 2)) + # y0 = np.column_stack((np.zeros(uxl.shape)+np.minimum(np.min(d_min), np.min(d_real)), + # np.zeros(uxl.shape)+np.maximum(np.max(d_max), np.max(d_real)))) + alpha = 1 - sat + alpha = np.minimum(alpha, 1.0) + alpha = np.maximum(alpha, 0.0) + cw = ListedColormap(['White']) + for l in range(len(uxl)): + ln_imp = _colorline(uxl[l] * np.ones(2), np.array([d_min[l], d_max[l]]), alpha=alpha[l]) + ln_imp.set_cmap(cw) + c = np.column_stack((ri, gi, bi)) + cm = ListedColormap(c) + ln.set_cmap(cm) + plt.colorbar(ln) + plt.xlim(uxl[0] - np.finfo(float).eps, uxl[-1] + np.finfo(float).eps) + plt.ylim(0, scale) + plt.title('1D coverage plot scaled by Importance') + filename = self.folder + 'coverage_1d_importance_vint_' + str(vint) + plt.savefig(filename) + os.system('convert ' + filename + '.png' + ' -trim ' + filename + '.png') + + for typ in [dat for dat in self.data_types if not dat in ['bulkimp', 'sim2seis', 'avo', 'grav']]: # Only well data + if hasattr(self, 'multilevel'): # calc for each level + plt.figure() + cover_low = [True for _ in self.en_obs[typ]] + cover_high = [True for _ in self.en_obs[typ]] + for l in range(self.tot_level): + # Check coverage + level_cover_low = [(el < self.en_ml_fcst[typ][l][ind]).all() for ind, el in + enumerate(self.en_obs[typ])] + level_cover_high = [(el > self.en_ml_fcst[typ][l][ind]).all() for ind, el in + enumerate(self.en_obs[typ])] + for ind, el in enumerate(level_cover_low): + if not el: + cover_low[ind] = False + if not level_cover_high[ind]: + cover_high[ind] = False + plt.plot(self.en_time[typ], self.en_ml_fcst[typ][l], c=f'{l / self.tot_level}', label=f'Level {l}') + plt.plot(self.en_time[typ], self.en_obs[typ], 'g*') + plt.plot([self.en_time[typ][ind] for ind, el in enumerate(cover_high) if el], + self.en_obs[typ][cover_high], 'r*') + plt.plot([self.en_time[typ][ind] for ind, el in enumerate(cover_low) if el], + self.en_obs[typ][cover_low], 'r*') + # remove duplicate labels + handles, labels = plt.gca().get_legend_handles_labels() + labels, ids = np.unique(labels, return_index=True) + handles = [handles[i] for i in ids] + plt.legend(handles, labels, loc='best') + ###### + plt.savefig(self.folder + typ.replace(' ', '_')) + plt.close() + else: + # Check coverage + cover_low = [(el < self.en_fcst[typ][ind]).all() for ind, el in enumerate(self.en_obs[typ])] + cover_high = [(el > self.en_fcst[typ][ind]).all() for ind, el in enumerate(self.en_obs[typ])] + # if sum(cover_low) > 1 or sum(cover_high) > 1: # not covered + # TODO: log this with some text + # plot the missing coverage + plt.figure() + plt.plot(self.en_time[typ], self.en_fcst[typ], c='0.35') + plt.plot(self.en_time[typ], self.en_obs[typ], 'g*') + plt.plot([self.en_time[typ][ind] for ind, el in enumerate(cover_high) if el], + self.en_obs[typ][cover_high], 'r*') + plt.plot([self.en_time[typ][ind] for ind, el in enumerate(cover_low) if el], + self.en_obs[typ][cover_low], 'r*') + plt.savefig(self.folder + typ.replace(' ', '_')) + plt.close() + + # Plot the seismic data + data_sim = [] + data = [] + supported_data = ['sim2seis', 'bulkimp', 'avo', 'grav'] + my_data = [dat for dat in supported_data if dat in self.data_types] + if len(my_data) == 0: + return + else: + my_data = my_data[1] + + # get the data + seis_scaling = 1.0 + if 'scale' in self.keys: + seis_scaling = self.keys['scale'][1] + for ind, t in enumerate(self.l_prim): + if self.obs_data[t][my_data] is not None and sum(np.isnan(self.obs_data[t][my_data])) == 0: + data_sim.append(self.obs_data[t][my_data] / seis_scaling) + data.append(self.pred_data[t][my_data] / seis_scaling) + + # loop through all vintages + for vint in range(len(data_sim)): + + # map to 2D + if not len(data_sim): + return + try: + mask = loadmat('mask_20.mat')[f'mask_{vint + 1}'] + mask = mask.astype(bool).transpose() + data_real_reg = np.zeros(mask.shape) + except: + mask = np.ones(field_dim, dtype=bool) + data_real_reg = np.zeros(mask.shape) + data_real_reg[mask] = data_sim[vint] + ne = data[vint].shape[1] + data_reg = np.zeros(mask.shape + (ne,)) + for member in range(ne): + data_reg[mask, member] = data[vint][:, member] + + # generate coverage and plot + nl = 0.25 + x = np.array([-1, -np.finfo(float).eps, 0, .5, 1, 1 + np.finfo(float).eps, 2]) + + r = np.array([0.1, 0.3, 0.8, 1.0, 0.8, 0.7, 0.5]) + g = np.array([0.1, 0.3, 0.9, 1.0, 0.9, 0.4, 0.2]) + b = np.array([0.4, 0.6, 0.8, 1.0, 0.8, 0.4, 0.2]) + + d_min = np.min(data_reg, axis=2) + d_max = np.max(data_reg, axis=2) + nl + sat = 2 * np.minimum((d_max + data_real_reg) / np.max(d_max.flatten() + data_real_reg.flatten()), + 0.5) + sc = d_max - d_min + + attr = (data_real_reg - d_min) / sc + attr = np.minimum(np.maximum(attr, -1), 2) + + rgb = [] + f = interp1d(x, r) + rgb.append(f(attr)) + f = interp1d(x, g) + rgb.append(f(attr)) + f = interp1d(x, b) + rgb.append(f(attr)) + rgb = np.dstack(rgb) + + if uxl is None and uil is None: + try: + uxl = loadmat('seglines.mat')['uxl'].flatten() + uil = loadmat('seglines.mat')['uil'].flatten() + except: + uxl = [0, field_dim[0]] + uil = [0, field_dim[1]] + + extent = (uxl[0], uxl[-1], uil[-1], uil[0]) + plt.figure() + plt.imshow(rgb, extent=extent) + if contours is not None and uil_c is not None and uxl_c is not None: + plt.contour(uxl_c, uil_c, contours[::-1, :], levels=1, colors='black') + plt.xlim(uxl[0], uxl[-1]) + plt.ylim(uil[-1], uil[0]) + plt.xlabel('Easting (km)') + plt.ylabel('Northing (km)') + plt.title('Coverage - not scaled by Importance - epsilon=' + str(nl)) + filename = self.folder + 'coverage_vint_' + str(vint) + plt.savefig(filename) + os.system('convert ' + filename + '.png' + ' -trim ' + filename + '.png') + + plt.figure() + rgb_scaled = np.uint8(rgb * 255) + hls = cv2.cvtColor(rgb_scaled, cv2.COLOR_RGB2HLS) + hls = hls / np.array([180, 255, 255]) + hls[:, :, 1] = hls[:, :, 1] / (np.abs(sat - nl) / (1 - nl) * 1.5) + hls[:, :, 1] = np.minimum(hls[:, :, 1], 1.0) + hls = np.uint8(hls * np.array([180, 255, 255])) + rgb_scaled = cv2.cvtColor(hls, cv2.COLOR_HLS2RGB) + rgb = rgb_scaled / 255 + plt.imshow(rgb, extent=extent) + if contours is not None and uil_c is not None and uxl_c is not None: + plt.contour(uxl_c, uil_c, contours[::-1, :], levels=1, colors='black', extent=extent) + plt.xlim(uxl[0], uxl[-1]) + plt.ylim(uil[-1], uil[0]) + plt.xlabel('Easting (km)') + plt.ylabel('Northing (km)') + plt.title('Coverage - scaled by Importance - epsilon=' + str(nl)) + filename = self.folder + 'coverage_importance_vint_' + str(vint) + plt.savefig(filename) + os.system('convert ' + filename + '.png' + ' -trim ' + filename + '.png') + #plt.close() + + plt.figure() + plt.imshow(sat[::-1,:], extent=extent) + if contours is not None and uil_c is not None and uxl_c is not None: + plt.contour(uxl_c, uil_c, contours[::-1, :], levels=1, colors='black', extent=extent) + plt.xlim(uxl[0], uxl[-1]) + plt.ylim(uil[-1], uil[0]) + plt.xlabel('Easting (km)') + plt.ylabel('Northing (km)') + plt.title('Importance - epsilon=' + str(nl)) + filename = self.folder + 'importance_vint_' + str(vint) + plt.savefig(filename) + os.system('convert ' + filename + '.png' + ' -trim ' + filename + '.png') + + if line: + _plot_coverage_1D(line, field_dim) + def calc_kg(self, options=None): """ Check/write individual gain for parameters. @@ -135,67 +464,587 @@ def calc_kg(self, options=None): to write this to the simulation grid. While for other applications, one might want other visualization. Hence, the method also depends on a simulator specific writer. - Parameters - ---------- - options : dict - Settings for the Kalman gain computations. - - - 'num_store' : int, optional - Number of elements to store. Default is 10. - - 'unique_time' : bool, optional - Calculate for each time instance. Default is False. - - 'plot_all_kg' : bool, optional - Plot all the Kalman gains for the field parameters. If False, plot the num_store. Default is False. - - 'only_log' : bool, optional - Only write to the logger; no plotting. Default is True. - - 'auto_ada_loc' : bool, optional - Use localization in computations. Default is True. - - 'write_to_resinsight' : bool, optional - Pipe results to ResInsight. Default is False. - - !!! note "requires that ResInsight is open on the computer." - - Notes - ----- - - Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS - - Not available in current version of PIPT + Input: + options: Settings for the kalman gain computations + - num_store: number of elements to store (default 10) + - unique_time: calculate for each time instance (default False) + - plot_all_kg: plot all the kalman gains for the field parameters, if not plot the num_store (default False) + - only_log: only write to logger; no plotting (default True) + - auto_ada_loc: use localization in computations (default True) + - write_to_resinsight: pipe results to ResInsight (default False) + (Note: this requires that ResInsight is open on the computer) + + Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS """ + # Stuff which needs to be defined in the initialization + # number of elements to store + if options is not None and 'num_store' in options: + num_store = options['num_store'] + else: + num_store = 10 + # calculate for each time instance + if options is not None and 'unique_time' in options: + unique_time = options['unique_time'] + else: + unique_time = False + # plot all the kalman gains for the field parameters, if not plot the num_store + if options is not None and 'plot_all_kg' in options: + plot_all_kg = options['plot_all_kg'] + else: + plot_all_kg = False + # only write to logger; no plotting + if options is not None and 'only_log' in options: + only_log = options['only_log'] + else: + only_log = True + # use localization in computations + if 'localization' not in self.keys: + auto_ada_loc = False + elif options is not None and 'auto_ada_loc' in options: + auto_ada_loc = options['auto_ada_loc'] + else: + auto_ada_loc = True + # write to resinsight + if options is not None and 'write_to_resinsight' in options: + write_to_resinsight = options['write_to_resinsight'] + else: + write_to_resinsight = False + + # check that we have prior info and sim class + if self.prior_info is None: + raise NameError('prior_info must be defined') + if self.lam is None: + raise NameError('lam must be defined') + if self.state is None: + raise NameError('state must be defined') + + # initialize + max_kg_update = [0 for _ in range(num_store)] + max_mean_kg_update = [0 for _ in range(num_store)] + kg_max_max = [tuple() for _ in range(num_store)] + kg_max_mean = [tuple() for _ in range(num_store)] + + # function to compute projection + def _calc_proj(): + # do subspace inversion + u, s, v = np.linalg.svd(pert_pred, full_matrices=False) + # store 99 % of energy + ti = (np.cumsum(s) / sum(s)) <= 0.99 + if sum(ti) == 0: + ti[0] = True + u, s, v = u[:, ti].copy(), s[ti].copy(), v[ti, :].copy() + _X2 = None + if sum(s): + ps_inv = np.diag([el_s ** (-1) for el_s in s]) + X0 = (self.ne - 1) * np.dot(ps_inv, np.dot(u.T, (np.concatenate(t_var) * + np.dot(u, ps_inv).T).T)) + Lamb, Z = np.linalg.eig(X0) + _X1 = np.dot(u, np.dot(ps_inv, Z)) + _X2 = np.dot(np.dot(pert_pred.T, _X1), np.dot(np.linalg.inv((self.lam + 1) * + np.eye(Lamb.shape[0]) + Lamb), _X1.T)) + return _X2 + + # function to compute kalman gain + def _calc_kalman_gain(): + if num_cell > 1: + if actnum is None: + idx = np.ones(self.state[param].shape[0], dtype=bool) + else: + if num_cell == np.sum(actnum): + idx = actnum # 3d-parameter fields + else: + if self.prior_info: + num_act_layer = int(self.prior_info[param]['nx'] * self.prior_info[param]['ny']) + idx = actnum[:num_act_layer] # this occurs for 2d-parameter fields + else: + raise NameError('prior_info must be defined') + _kg = np.zeros(idx.shape) + if auto_ada_loc and num_cell == np.sum(idx): + proj_pred_data = np.dot(X2, delta_d) + step = self.localization.auto_ada_loc(self.state[param], proj_pred_data, + [param], **{'prior_info': self.prior_info}) + _kg[idx] = np.mean(step, axis=1) + else: + _kg[idx] = np.dot(self.state[param], np.dot(X2, mean_residual)).flatten() + else: # scalar + _kg = np.dot(np.dot(self.state[param], X2), mean_residual).flatten() + + return _kg + + # function to compute max values + def _populate_kg(): + if actnum is None: + idx = np.ones(self.state[param].shape[0], dtype=bool) + else: + if num_cell == np.sum(actnum): + idx = actnum # 3d-parameter fields + else: + if self.prior_info: + num_act_layer = int(self.prior_info[param]['nx'] * self.prior_info[param]['ny']) + idx = actnum[:num_act_layer] # this occurs for 2d-parameter fields + else: + raise NameError('prior_info must be defined') + if len(np.where(abs(tmp[idx]).max() > np.array(max_kg_update))[0]): + indx = np.where(abs(tmp[idx]).max() > np.array(max_kg_update))[0][0] + max_kg_update.insert(indx, abs(tmp[idx]).max()) + max_kg_update.pop() + kg_max_max.insert(indx, (typ, param, time)) + kg_max_max.pop() + if len(np.where(abs(tmp[idx].mean()) > np.array(max_mean_kg_update))[0]): + indx = np.where(abs(tmp[idx].mean()) > np.array(max_mean_kg_update))[0][0] + max_mean_kg_update.insert(indx, abs(tmp[idx].mean())) + max_mean_kg_update.pop() + kg_max_mean.insert(indx, (typ, param, time)) + kg_max_mean.pop() + + # function to write to grid + def _plot_kg(_field=None): + if _field is None: # assume scalar plot + plt.figure() + plt.plot(self.en_time[typ], kg_single) + plt.savefig(self.folder + f'Kg_{param}_{typ}') + plt.close() + else: + if self.sim is None: + raise NameError('sim must be defined') + if actnum is None: + idx = np.ones(self.state[param].shape[0], dtype=bool) + else: + if num_cell != np.sum(actnum): + return # TODO: implement plotting of surfaces + if os.path.exists('actnum_ref.npz'): + idx = np.load('actnum_ref.npz')['actnum'] + else: + idx = actnum + kg = np.ma.array(data=tmp, mask=~idx) + #dim = (self.prior_info[param]['nx'], self.prior_info[param]['ny'], self.prior_info[param]['nz']) + dim = next((item[1] for item in self.prior_info[param] if item[0] == 'grid'), None) + input_time = None + if write_to_resinsight: + if time is None: + input_time = len(self.l_prim) + else: + input_time = time + deblank_typ = typ.replace(' ', '_') + if hasattr(self.sim, 'write_to_grid'): + self.sim.write_to_grid(kg, f'{_field}_{param}_{deblank_typ}_{time}', self.folder, dim, input_time) + elif hasattr(self.sim.flow, 'write_to_grid'): + self.sim.flow.write_to_grid(kg, f'{_field}_{param}_{deblank_typ}_{time}', self.folder, dim, + input_time) + else: + print('You need to implement a writer in you simulator class!! \n') + + # -- Main function -- + # need actnum + actnum = None + if os.path.exists('actnum.npz'): + actnum = np.load('actnum.npz')['actnum'] + if unique_time: + en_fcst = self.en_fcst + en_ml_fcst = self.en_ml_fcst + en_obs = self.en_obs + en_time = self.en_time + else: # second dict overwrites the first if the same key is present + en_fcst = {**self.en_fcst, **self.en_fcst_vec} + en_ml_fcst = {**self.en_ml_fcst, **self.en_ml_fcst_vec} + en_obs = {**self.en_obs, **self.en_obs_vec} + en_time = {**self.en_time, **self.en_time_vec} + for typ in self.data_types: # ['sim2seis', 'WOPR A-11']: + if unique_time: + for param in self.list_state: + kg_single = [] + for ind, time in enumerate(en_time[typ]): + t_var = np.array(max([el[typ] for el in self.datavar if el[typ] is not None]))[ + np.newaxis] # to be able to concantenate + if not len(t_var): # [self.datavar[ind][typ]] + t_var = [1] + if hasattr(self, 'multilevel'): + self.ML_state = copy.deepcopy(self.state) + delattr(self, 'state') + tmp_kg = [] + for l in range(self.tot_level): + pert_pred = (en_ml_fcst[typ][l][ind, :] - en_ml_fcst[typ][l][ind, :].mean())[np.newaxis, + :] + mean_residual = (en_obs[typ][ind] - en_ml_fcst[typ][l][ind, :]).mean() + mean_residual = mean_residual[np.newaxis, np.newaxis].flatten() + delta_d = (en_obs[typ][ind] - en_ml_fcst[typ][l][ind, :self.ne])[np.newaxis, :] + X2 = _calc_proj() + self.state = self.ML_state[l] + num_cell = self.state[param].shape[0] + if X2 is None: # cases with full collapse in one level + tmp_kg.append(np.zeros(num_cell)) + else: + tmp_kg.append(_calc_kalman_gain()) + tmp = sum([self.cov_wgt[i] * el for i, el in enumerate(tmp_kg)]) / sum(self.cov_wgt) + num_cell = self.state[param].shape[0] + self.state = copy.deepcopy(self.ML_state) + delattr(self, 'ML_state') + else: + pert_pred = (en_fcst[typ][ind, :self.ne] - en_fcst[typ][ind, :self.ne].mean())[np.newaxis, :] + mean_residual = (en_obs[typ][ind] - en_fcst[typ][ind, :self.ne]).mean() + mean_residual = mean_residual[np.newaxis, np.newaxis].flatten() + delta_d = (en_obs[typ][ind] - en_fcst[typ][ind, :self.ne])[np.newaxis, :] + X2 = _calc_proj() + num_cell = self.state[param].shape[0] + tmp = _calc_kalman_gain() + num_cell = self.state[param].shape[0] + + if num_cell == 1: + kg_single.append(tmp) + else: + _populate_kg() + if not only_log and plot_all_kg: + _plot_kg('Kg') + + if len(kg_single): + _plot_kg() + + else: + t_var = [self.datavar[ind][typ] for ind in en_time[typ] if self.datavar[ind][typ] is not None] + if len(t_var) == 0: + continue + if hasattr(self, 'multilevel'): + self.ML_state = copy.deepcopy(self.state) + delattr(self, 'state') + for param in self.list_state: + tmp_kg = [] + for l in range(self.tot_level): + if len(en_ml_fcst[typ][l].shape) == 2: + pert_pred = en_ml_fcst[typ][l] - np.dot(en_ml_fcst[typ][l].mean(axis=1)[:, np.newaxis], + np.ones((1, self.ml_ne[l]))) + delta_d = en_obs[typ] - en_ml_fcst[typ][l][:,:self.ne] + mean_residual = (en_obs[typ] - en_ml_fcst[typ][l]).mean(axis=1) + X2 = _calc_proj() + self.state = self.ML_state[l] + num_cell = self.state[param].shape[0] + if num_cell > 1: + time = None + if X2 is None: # cases with full collapse in one level + tmp_kg.append(np.zeros(num_cell)) + else: + tmp_kg.append(_calc_kalman_gain()) + + tmp = sum([self.cov_wgt[i] * el for i, el in enumerate(tmp_kg)]) / sum(self.cov_wgt) + _populate_kg() + if not only_log and plot_all_kg: + _plot_kg('Kg-lump_vector') + self.state = copy.deepcopy(self.ML_state) + delattr(self, 'ML_state') + else: + # combine time instances + if len(en_fcst[typ].shape) == 2: + pert_pred = en_fcst[typ][:, :self.ne] - np.dot(en_fcst[typ][:, :self.ne].mean(axis=1)[:, np.newaxis], + np.ones((1, self.ne))) + delta_d = en_obs[typ] - en_fcst[typ][:, :self.ne] + mean_residual = (en_obs[typ] - en_fcst[typ][:, :self.ne]).mean(axis=1) + X2 = _calc_proj() + for param in self.list_state: + num_cell = self.state[param].shape[0] + if num_cell > 1: + time = None + tmp = _calc_kalman_gain() + _populate_kg() + if not only_log and plot_all_kg: + _plot_kg('Kg-lump_vector') + + # write top 10 values to the log + newline = "\n" + self.logger.info('Calculations complete. 10 largest Kg mean values are:' + newline + + f'{newline.join(f"{el}" for el in kg_max_mean if el)}') + self.logger.info('Calculations complete. 10 largest Kg max values are:' + newline + + f'{newline.join(f"{el}" for el in kg_max_max if el)}') + if not only_log and not plot_all_kg: + # need to form and plot/write the gains from kg_max_mean and kg_max_max + # start with kg_max_mean + for el_ind, el in enumerate(itertools.chain(kg_max_mean, kg_max_max)): + # add filter if there are not 10 values + if len(el): + # test if we have some time-dependece + if el[2] is not None: + typ = el[0] + param = el[1] + time = el[2] + time_str = '-' + str(time) + ind = en_time[typ].index(time) + pert_pred = (en_fcst[typ][ind, :] - en_fcst[typ][ind, :].mean())[np.newaxis, :] + mean_residual = (en_obs[typ][ind] - en_fcst[typ][ind, :]).mean()[np.newaxis, np.newaxis] + t_var = [self.datavar[ind][typ]] + else: + typ = el[0] + param = el[1] + time = len(self.l_prim) + time_str = '-' + pert_pred = en_fcst[typ][:, :self.ne] - np.dot(en_fcst[typ][:, :self.ne].mean(axis=1)[:, np.newaxis], + np.ones((1, self.ne))) + mean_residual = (en_obs[typ] - en_fcst[typ]).mean(axis=1) + t_var = [self.datavar[ind][typ] for ind in en_time[typ] if self.datavar[ind][typ] is not None] + X2 = _calc_proj() + delta_d = en_obs[typ] - en_fcst[typ][:, :self.ne] + num_cell = self.state[param].shape[0] + tmp = _calc_kalman_gain() + if el_ind < len(kg_max_mean): + _plot_kg('Kg-mean' + time_str) + else: + _plot_kg('Kg-max' + time_str) + def calc_mahalanobis(self, combi_list=(1, None)): """ Calculate the mahalanobis distance as described in "Oliver, D. S. (2020). Diagnosing reservoir model deficiency for model improvement. Journal of Petroleum Science and Engineering, 193(February). https://doi.org/10.1016/j.petrol.2020.107367" - Parameters - ---------- - combi_list : list - List of levels and possible combinations of datatypes. The list must be given as a tuple with pairs: - - level : int - Defines which level. Default is 1. - - combi_typ : str - Defines how data are combined. Default is no combine. - - Notes - ----- - - Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS - - Not available in current version of PIPT + Input: + combi_list: list of levels and possible combination of datatypes. The list must be given as a tuple with pairs: + level int: defines which level. default = 1 + combi_typ: defines how data are combined: Default is no combine. + + Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS """ + for combo in range(0, len(combi_list), 2): + level = combi_list[combo] + if len(combi_list) > combo: + combi_type = combi_list[combo + 1] + else: + combi_type = None + + self.logger.info(f'Starting level {level} calculations of Mahalanobis distance') + + # start by generating correct vectors and fixind the seed + np.random.seed(50) + en_fcst_pert = [] + filt_data = [] + if combi_type is None: # look at all data individually + en_fcst = np.concatenate([self.en_fcst[typ] for typ in self.data_types if self.en_fcst[typ].size], + axis=0) + filt_data = [(typ, ind) for typ in self.data_types for ind in self.l_prim + if self.obs_data[ind][typ] is not None and sum(np.isnan(self.obs_data[ind][typ])) == 0 + and self.obs_data[ind][typ].shape == (1,)] + en_obs = np.concatenate([self.en_obs[typ] for typ in self.data_types if self.en_obs[typ].size], axis=0) + en_var = np.array([self.datavar[ind][typ].flatten() for typ in self.data_types for ind in self.l_prim + if + self.obs_data[ind][typ] is not None and sum(np.isnan(self.obs_data[ind][typ])) == 0 + and self.obs_data[ind][typ].shape == (1,)]) + + en_fcst_pert = en_fcst + np.sqrt(en_var[:, 0])[:, np.newaxis] * \ + np.random.randn(en_fcst.shape[0], en_fcst.shape[1]) + + else: # some data should be defined as blocks. To get the correct measure we project the data onto the subspace + # spanned by the first principal component. The level 1, 2 and 3. Difference is then calculated in + # similar fashion as for the full data-space. have simple rules for generating combinations. All data are + # aquired at some time, at some position, and there might be multiple data types at the same time and + # position. + en_obs = [] + if 'time' in combi_type or 'vector' in combi_type: + tmp_fcst = [] + for typ in self.data_types: + tmp_fcst.append([self.en_fcst[typ][ind, :self.ne][np.newaxis, :self.ne] for ind in self.l_prim + if self.obs_data[ind][typ] is not None and sum( + np.isnan(self.obs_data[ind][typ])) == 0]) + filt_fcst = [x for x in tmp_fcst if len(x)] # remove all empty lists + filt_data = [list(self.data_types)[i] for i, x in enumerate(tmp_fcst) if len(x)] + en_fcst_pert = [] + for i, dat in enumerate(filt_data): + tmp_enfcst = np.concatenate(filt_fcst[i], axis=0) + tmp_var = np.concatenate([self.datavar[ind][dat].flatten() for ind in self.l_prim + if self.obs_data[ind][dat] is not None and sum( + np.isnan(self.obs_data[ind][dat])) == 0]) + tmp_var = np.expand_dims(tmp_var, 1) + tmp_fcst_pert = tmp_enfcst + np.sqrt(tmp_var[:, 0])[:, np.newaxis] * \ + np.random.randn(tmp_enfcst.shape[0], tmp_enfcst.shape[1]) + X = tmp_fcst_pert - tmp_fcst_pert.mean(axis=1)[:, np.newaxis] + u, s, v = np.linalg.svd(X.T, full_matrices=False) + v_sing = v[:1, :] + en_fcst_pert.append(np.dot(v_sing, tmp_fcst_pert).flatten()) + tmp_obs = np.concatenate([self.obs_data[ind][dat] for ind in self.l_prim if + self.obs_data[ind][dat] is not None and + sum(np.isnan(self.obs_data[ind][dat])) == 0]) + tmp_obs = np.expand_dims(tmp_obs, 1) + en_obs.append(np.dot(v_sing, tmp_obs).flatten()) + + en_fcst_pert = np.array(en_fcst_pert) + en_obs = np.array(en_obs) + + if level == 1: + nD = len(en_fcst_pert) + scores = np.zeros(nD) + for i in range(nD): + mean_fcst = np.mean(en_fcst_pert[i, :]) + ivar = 1. / np.var(en_fcst_pert[i, :]) + scores[i] = ivar * (en_obs[i, :] - mean_fcst) ** 2 + + num_scores = min(10, len(scores.flatten())) # if there is less than 10 data + unsort_top10 = np.argpartition(scores.flatten(), -num_scores)[ + -num_scores:] # this is fast but not sorted. Get 10 highest values + top10 = unsort_top10[np.argsort(scores[unsort_top10])[::-1]] # sort in descending order + newline = "\n" + if combi_type is None: + self.logger.info(f'Calculations complete. {num_scores} largest values are:' + newline + + f'{newline.join(f" data type: {filt_data[ind][0]} time: {filt_data[ind][1]} Score: {scores[ind]}" for ind in top10)}') + + # make cross-plot + i1 = [top10[3], top10[3]] + i2 = [top10[2], top10[0]] + for ind in range(len(i1)): + plt.figure() + plt.plot(en_fcst_pert[i1[ind], :], en_fcst_pert[i2[ind], :], '.b') + plt.plot(en_obs[i1[ind], :], en_obs[i2[ind], :], '.r') + plt.xlabel(str(filt_data[i1[ind]][0]) + ', time ' + str(filt_data[i1[ind]][1])) + plt.ylabel(str(filt_data[i2[ind]][0]) + ', time ' + str(filt_data[i2[ind]][1])) + plt.savefig( + self.folder + 'crossplot_' + filt_data[i1[ind]][0].replace(' ', '_') + '_t' + + str(filt_data[i1[ind]][1]) + '-' + filt_data[i2[ind]][0].replace( + ' ', '_') + '_t' + str(filt_data[i2[ind]][1])) + plt.close() + else: + self.logger.info(f'Calculations complete. {num_scores} largest values are:' + newline + + f'{newline.join(f" data type: {filt_data[ind]} Score: {scores[ind]}" for ind in top10)}') + + # make cross-plot + i1 = [top10[0], top10[1]] + i2 = [top10[1], top10[3]] + for ind in range(len(i1)): + plt.figure() + plt.plot(en_fcst_pert[i1[ind], :], en_fcst_pert[i2[ind], :], '.b') + plt.plot(en_obs[i1[ind], :], en_obs[i2[ind], :], '.r') + plt.xlabel(str(filt_data[i1[ind]]) + ' (proj)') + plt.ylabel(str(filt_data[i2[ind]]) + ' (proj)') + plt.savefig( + self.folder + 'crossplot_' + str(filt_data[i1[ind]]).replace(' ', '_') + '-' + + str(filt_data[i2[ind]]).replace(' ', '_')) + plt.close() + + elif level == 2: + nD = len(en_fcst_pert) + scores = np.zeros((nD, nD)) + for i in range(nD): + for j in range(nD): + if i != j: + ne = en_fcst_pert.shape[1] + z = np.concatenate((en_obs[i, :], en_obs[j, :]), axis=0) + X = np.vstack((en_fcst_pert[i, :], en_fcst_pert[j, :])) + mean_fcst = np.mean(X, axis=1) + diff_fcst = X - mean_fcst[:, np.newaxis] + C_fcst = np.dot(diff_fcst, diff_fcst.T) / (ne - 1) + inv_C = np.linalg.inv(C_fcst) + res = z - mean_fcst + term1 = np.dot(res, inv_C) + scores[i, j] = np.dot(term1, res) / 2 + else: + mean_fcst = np.mean(en_fcst_pert[i, :]) + ivar = 1. / np.var(en_fcst_pert[i, :]) + scores[i, j] = ivar * (en_obs[i, :] - mean_fcst) ** 2 + + num_scores = min(20, len(scores.flatten())) + unsort_top10 = np.argpartition(scores.flatten(), -num_scores)[ + -num_scores:] # this is fast but not sorted. Get 20 highest values, select every other. + top10 = unsort_top10[np.argsort(scores.flatten()[unsort_top10])[ + ::-2]] # sort in descending order. Will be duplicates select every other. + newline = "\n" + if combi_type is None: + self.logger.info(f'Calculations complete. {int(num_scores / 2)} largest values are:' + newline + + f'{newline.join(f" data type 1: {filt_data[np.where(scores == scores.flatten()[ind])[0][0]][0]} time 1: {filt_data[np.where(scores == scores.flatten()[ind])[0][0]][1]} data type 2: {filt_data[np.where(scores == scores.flatten()[ind])[1][0]][0]} time 2: {filt_data[np.where(scores == scores.flatten()[ind])[1][0]][1]} Score: {scores.flatten()[ind]}" for ind in top10)}') + + else: + self.logger.info(f'Calculations complete. {int(num_scores / 2)} largest values are:' + newline + + f'{newline.join(f" data type 1: {filt_data[np.where(scores == scores.flatten()[ind])[0][0]]} data type 2: {filt_data[np.where(scores == scores.flatten()[ind])[1][0]]} Score: {scores.flatten()[ind]}" for ind in top10)}') + + elif level == 3: + nD = len(en_fcst_pert) + scores = np.zeros((nD, nD, nD)) + for i in range(nD): + for j in range(nD): + for k in range(nD): + if i != j != k: + ne = en_fcst_pert.shape[1] + z = np.concatenate((self.en_obs[i, :], self.en_obs[j, :], self.en_obs[k, :]), axis=0) + X = np.vstack((en_fcst_pert[i, :], en_fcst_pert[j, :], en_fcst_pert[k, :])) + mean_fcst = np.mean(X, axis=1) + diff_fcst = X - mean_fcst[:, np.newaxis] + C_fcst = np.dot(diff_fcst, diff_fcst.T) / (ne - 1) + inv_C = np.linalg.inv(C_fcst) + res = z - mean_fcst + term1 = np.dot(res, inv_C) + scores[i, j] = np.dot(term1, res) / 2 + else: + mean_fcst = np.mean(en_fcst_pert[i, :]) + ivar = 1. / np.var(en_fcst_pert[i, :]) + scores[i, j] = ivar * (self.en_obs[i, :] - mean_fcst) ** 2 + else: + print('Current level is not implemented') + def calc_da_stat(self, options=None): """ Calculate statistics for the updated parameters. The persentage of parameters that have updates larger than one, two and three standard deviations (calculated from the initial ensemble) are flagged. - Parameters - ---------- - options : dict - Settings for statistics. - write_to_file : bool, optional - Whether to write results to a .grdecl file. Defaults to False. - - Notes - ----- - - Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS - - Not available in current version of PIPT + Input: + options: Settings for statistics + - write_to_file: write results to .grdecl file (default False) + + Copyright (c) 2019-2022 NORCE, All Rights Reserved. 4DSEIS """ + + if options is not None and 'write_to_file' in options: + write_to_file = options['write_to_file'] + else: + write_to_file = False + + actnum = None + if os.path.exists('actnum.npz'): + actnum = np.load('actnum.npz')['actnum'] + + newline = '\n' + log_str = 'Statistics for updated parameters. Initial and final std, and percent larger than 1,2,3 initial std:' + for key in self.list_state: + if hasattr(self, 'multilevel'): + tot_init_state = np.concatenate([el[key] for el in self.ini_state], axis=1) + tot_state = np.concatenate([el[key] for el in self.state], axis=1) + initial_mean = np.mean(tot_init_state, axis=1) + final_mean = np.mean(tot_state, axis=1) + S = np.std(tot_init_state, axis=1) + ES = np.append(np.mean(S), np.mean(np.std(tot_state, axis=1))) + else: + initial_mean = np.mean(self.ini_state[key], axis=1) + final_mean = np.mean(self.state[key], axis=1) + S = np.std(self.ini_state[key], axis=1) + ES = np.append(np.mean(S), np.mean(np.std(self.state[key], axis=1))) + M = final_mean - initial_mean + N = np.zeros(3) + N[0] = np.sum(np.abs(M) > S) + N[1] = np.sum(np.abs(M) > 2 * S) + N[2] = np.sum(np.abs(M) > 3 * S) + P = N * 100 / len(M) + log_str += newline + 'Group ' + key + ' ' + str(ES) + ', ' + str(P) + + if write_to_file: + if actnum is None: + if hasattr(self, 'multilevel'): + idx = np.ones(self.state[0][key].shape[0], dtype=bool) + else: + idx = np.ones(self.state[key].shape[0], dtype=bool) + else: + idx = actnum + if M.size == np.sum(idx) and M.size > 1: # we have a grid parameter + tmp = np.zeros(M.shape) + tmp[M > S] = 1 + tmp[M > 2 * S] = 2 + tmp[M > 3 * S] = 3 + tmp[M < -S] = -1 + tmp[M < -2 * S] = -2 + tmp[M < -3 * S] = -3 + data = np.zeros(idx.shape) + data[idx] = tmp + field = np.ma.array(data=data, mask=~idx) + #dim = (self.prior_info[key]['nx'], self.prior_info[key]['ny'], self.prior_info[key]['nz']) + dim = next((item[1] for item in self.prior_info[key] if item[0] == 'grid'), None) + input_time = None + if hasattr(self.sim, 'write_to_grid'): + self.sim.write_to_grid(field, f'da_stat_{key}', self.folder, dim, input_time) + elif hasattr(self.sim.flow, 'write_to_grid'): + self.sim.flow.write_to_grid(field, f'da_stat_{key}', self.folder, dim, input_time) + else: + print('You need to implement a writer in you simulator class!! \n') + + self.logger.info(log_str) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 605160d..31200b6 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -21,6 +21,8 @@ from scipy.special import j0 from mako.lookup import TemplateLookup from mako.runtime import Context +#import cProfile +#import pstats # from pylops import avo from pylops.utils.wavelets import ricker @@ -31,9 +33,88 @@ from scipy.interpolate import griddata from pipt.misc_tools.analysis_tools import store_ensemble_sim_information from geostat.decomp import Cholesky -from simulator.eclipse import ecl_100 +#from simulator.eclipse import ecl_100 from CoolProp.CoolProp import PropsSI # http://coolprop.org/#high-level-interface-example +class mixIn_multi_data(): + + def find_cell_centre(self, grid): + # Find indices where the boolean array is True + indices = np.where(grid['ACTNUM']) + + coord = grid['COORD'] + zcorn = grid['ZCORN'] + + c, a, b = indices + # Calculate xt, yt, zt + xb = 0.25 * (coord[a, b, 0, 0] + coord[a, b + 1, 0, 0] + coord[a + 1, b, 0, 0] + coord[a + 1, b + 1, 0, 0]) + yb = 0.25 * (coord[a, b, 0, 1] + coord[a, b + 1, 0, 1] + coord[a + 1, b, 0, 1] + coord[a + 1, b + 1, 0, 1]) + zb = 0.25 * (coord[a, b, 0, 2] + coord[a, b + 1, 0, 2] + coord[a + 1, b, 0, 2] + coord[a + 1, b + 1, 0, 2]) + + xt = 0.25 * (coord[a, b, 1, 0] + coord[a, b + 1, 1, 0] + coord[a + 1, b, 1, 0] + coord[a + 1, b + 1, 1, 0]) + yt = 0.25 * (coord[a, b, 1, 1] + coord[a, b + 1, 1, 1] + coord[a + 1, b, 1, 1] + coord[a + 1, b + 1, 1, 1]) + zt = 0.25 * (coord[a, b, 1, 2] + coord[a, b + 1, 1, 2] + coord[a + 1, b, 1, 2] + coord[a + 1, b + 1, 1, 2]) + + # Calculate z, x, and y positions + z = (zcorn[c, 0, a, 0, b, 0] + zcorn[c, 0, a, 1, b, 0] + zcorn[c, 0, a, 0, b, 1] + zcorn[c, 0, a, 1, b, 1] + + zcorn[c, 1, a, 0, b, 0] + zcorn[c, 1, a, 1, b, 0] + zcorn[c, 1, a, 0, b, 1] + zcorn[c, 1, a, 1, b, 1]) / 8 + + x = xb + (xt - xb) * (z - zb) / (zt - zb) + y = yb + (yt - yb) * (z - zb) / (zt - zb) + + cell_centre = [x, y, z] + return cell_centre + + + def get_seabed_depths(self, file_path): + # Read the data while skipping the header comments + # We'll assume the header data ends before the numerical data + # The 'delim_whitespace' keyword in pd.read_csv is deprecated and will be removed in a future version. Use ``sep='\s+'`` instead + water_depths = pd.read_csv(file_path, comment='#', sep=r'\s+', + header=None) # delim_whitespace=True, header=None) + + # Give meaningful column names: + water_depths.columns = ['x', 'y', 'z', 'column', 'row'] + + return water_depths + + def measurement_locations(self, grid, water_depth, pad=1500, dxy=1500): + + # Determine the size of the measurement area as defined by the field extent + cell_centre = self.find_cell_centre(grid) + xmin = np.min(cell_centre[0]) + xmax = np.max(cell_centre[0]) + ymin = np.min(cell_centre[1]) + ymax = np.max(cell_centre[1]) + + xmin -= pad + xmax += pad + ymin -= pad + ymax += pad + + xspan = xmax - xmin + yspan = ymax - ymin + + Nx = int(np.ceil(xspan / dxy)) + Ny = int(np.ceil(yspan / dxy)) + + xvec = np.linspace(xmin, xmax, Nx) + yvec = np.linspace(ymin, ymax, Ny) + + x, y = np.meshgrid(xvec, yvec) + + pos = {'x': x.flatten(), 'y': y.flatten()} + + # Seabed map or water depth scalar depending on input + if isinstance(water_depth, float): + pos['z'] = np.ones_like(pos['x']) * water_depth + else: + pos['z'] = griddata((water_depth['x'], water_depth['y']), + np.abs(water_depth['z']), (pos['x'], pos['y']), + method='nearest') # z is positive downwards + return pos + + class flow_rock(flow): """ Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic @@ -346,9 +427,6 @@ def extract_data(self, member): v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() - - - class flow_sim2seis(flow): """ Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic @@ -728,8 +806,7 @@ def extract_data(self, member): v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) self.pred_data[prim_ind][key] = self.bar_result[v].flatten() - -class flow_avo(flow_rock): +class flow_avo(flow_rock, mixIn_multi_data): def __init__(self, input_dict=None, filename=None, options=None, **kwargs): super().__init__(input_dict, filename, options) @@ -790,18 +867,31 @@ def get_avo_result(self, folder, save_folder): if self.no_flow: grid_file = self.pem_input['grid'] grid = np.load(grid_file) + zcorn = grid['ZCORN'] + dz = np.diff(zcorn[:, 0, :, 0, :, 0], axis=0) + # Extract the last layer + last_layer = dz[-1, :, :] + # Reshape to ensure it has the same number of dimensions + last_layer = last_layer.reshape(1, dz.shape[1], dz.shape[2]) + # Concatenate to the original array along the first axis + dz = np.concatenate([dz, last_layer], axis=0) + f_dim = [grid['DIMENS'][2], grid['DIMENS'][1], grid['DIMENS'][0]] else: self.ecl_case = ecl.EclipseCase(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ else ecl.EclipseCase(folder + self.file + '.DATA') grid = self.ecl_case.grid() + zcorn = grid['ZCORN'] + ecl_init = ecl.EclipseInit(folder + os.sep + self.file + '.DATA') if folder[-1] != os.sep \ + else ecl.EclipseCase(folder + self.file + '.DATA') + dz = ecl_init.cell_data('DZ') + f_dim = [ecl_init.init.nk, ecl_init.init.nj, ecl_init.init.ni] - # ecl_init = ecl.EclipseInit(ecl_case) - # f_dim = [self.ecl_case.init.nk, self.ecl_case.init.nj, self.ecl_case.init.ni] - f_dim = [self.NZ, self.NY, self.NX] - # phases = self.ecl_case.init.phases - self.dyn_var = [] + self.dyn_var = [] + # coarsening of avo data - should be given as input in pipt + step_x = 2 + step_y = 3 if 'baseline' in self.pem_input: # 4D measurement base_time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + dt.timedelta(days=self.pem_input['baseline']) @@ -815,23 +905,27 @@ def get_avo_result(self, folder, save_folder): # avo data # self._calc_avo_props() - avo_data, Rpp, vp_sample, vs_sample, rho_sample = self._calc_avo_props_active_cells(grid, vp, vs, rho) - - avo_baseline = avo_data.flatten(order="F") - Rpp_baseline = Rpp - vs_baseline = vs_sample - vp_baseline = vp_sample - rho_baseline = rho_sample + avo_data_baseline, Rpp_baseline, vp_baseline, vs_baseline = self._calc_avo_props_active_cells(grid, vp, vs, rho, dz, zcorn) + kept_data = avo_data_baseline[::step_x, ::step_y, :].copy() + avo_data_baseline[:] = np.nan + avo_data_baseline[::step_x, ::step_y, :] = kept_data + avo_baseline = avo_data_baseline.flatten(order="C") + avo_baseline = avo_baseline[~np.isnan(avo_baseline)] + #rho_baseline = rho_sample + tmp = self._get_pem_input('PRESSURE', base_time) + PRESSURE_baseline = np.array(tmp[~tmp.mask], dtype=float) + tmp = self._get_pem_input('SGAS', base_time) + SGAS_baseline = np.array(tmp[~tmp.mask], dtype=float) print('OPM flow is used') else: file_name = f"avo_vint0_{folder}.npz" if folder[-1] != os.sep \ else f"avo_vint0_{folder[:-1]}.npz" avo_baseline = np.load(file_name, allow_pickle=True)['avo_bl'] - Rpp_baseline = np.load(file_name, allow_pickle=True)['Rpp_bl'] - vs_baseline = np.load(file_name, allow_pickle=True)['vs_bl'] - vp_baseline = np.load(file_name, allow_pickle=True)['vp_bl'] - rho_baseline = np.load(file_name, allow_pickle=True)['rho_bl'] + #Rpp_baseline = np.load(file_name, allow_pickle=True)['Rpp_bl'] + #vs_baseline = np.load(file_name, allow_pickle=True)['Vs_bl'] + #vp_baseline = np.load(file_name, allow_pickle=True)['Vp_bl'] + #rho_baseline = np.load(file_name, allow_pickle=True)['Rho_bl'] vintage = [] # loop over seismic vintages @@ -847,16 +941,33 @@ def get_avo_result(self, folder, save_folder): # avo data #self._calc_avo_props() - avo_data, Rpp, vp_sample, vs_sample, rho_sample = self._calc_avo_props_active_cells(grid, vp, vs, rho) + avo_data, Rpp, vp_sample, vs_sample = self._calc_avo_props_active_cells(grid, vp, vs, rho, dz, zcorn) + #avo_data = avo_data[::step_x, ::step_y, :] + # make mask for wavelet decomposition + kept_data = avo_data[::step_x, ::step_y, :].copy() + avo_data[:] = np.nan + avo_data[::step_x, ::step_y, :] = kept_data + mask = np.ones(np.shape(avo_data), dtype=bool) + mask[np.isnan(avo_data)]=False + np.savez(f'mask_{v}.npz', mask=mask) + + avo = avo_data.flatten(order="C") + avo = avo[~np.isnan(avo)] + + tmp = self._get_pem_input('PRESSURE', time) + PRESSURE = np.array(tmp[~tmp.mask], dtype=float) + tmp = self._get_pem_input('SGAS', time) + SGAS = np.array(tmp[~tmp.mask], dtype=float) - avo = avo_data.flatten(order="F") # MLIE: implement 4D avo if 'baseline' in self.pem_input: # 4D measurement avo = avo - avo_baseline - #Rpp = self.Rpp - Rpp_baseline - #Vs = self.vs_sample - vs_baseline - #Vp = self.vp_sample - vp_baseline + Rpp = Rpp - Rpp_baseline + vs_sample = vs_sample - vs_baseline + vp_sample = vp_sample - vp_baseline + PRESSURE = PRESSURE - PRESSURE_baseline + SGAS = SGAS - SGAS_baseline #rho = self.rho_sample - rho_baseline print('Time-lapse avo') #else: @@ -880,10 +991,15 @@ def get_avo_result(self, folder, save_folder): vintage.append(deepcopy(avo)) - - #save_dic = {'avo': avo, 'noise_std': noise_std, **self.avo_config} - save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, 'rho': rho_sample, #**self.avo_config, - 'vs_bl': vs_baseline, 'vp_bl': vp_baseline, 'avo_bl': avo_baseline, 'Rpp_bl': Rpp_baseline, 'rho_bl': rho_baseline, **self.avo_config} + if v == 0: + save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, 'PRESSURE': PRESSURE, 'SGAS': SGAS, **self.avo_config} + #save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, 'rho': rho_sample, #**self.avo_config, + # 'Vs_bl': vs_baseline, 'Vp_bl': vp_baseline, 'avo_bl': avo_baseline, 'Rpp_bl': Rpp_baseline, 'rho_bl': rho_baseline, **self.avo_config} + #save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, + # **self.avo_config} + else: + save_dic = {'avo': avo, 'noise_std': noise_std, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, 'PRESSURE': PRESSURE, 'SGAS': SGAS}#, 'Rpp': Rpp, 'Vs': vs_sample, 'Vp': vp_sample, + #'rho': rho_sample, **self.avo_config} if save_folder is not None: file_name = save_folder + os.sep + f"avo_vint{v}.npz" if save_folder[-1] != os.sep \ @@ -903,34 +1019,24 @@ def get_avo_result(self, folder, save_folder): for i, elem in enumerate(vintage): self.avo_result.append(elem) - def calc_velocities(self, folder, save_folder, grid, v, f_dim): # The properties in pem are only given in the active cells # indices of active cells: true_indices = np.where(grid['ACTNUM']) - # Alt 2 - if len(self.pem.getBulkVel()) == len(true_indices[0]): - #self.vp = np.full(f_dim, self.avo_config['vp_shale']) - vp = np.full(f_dim, np.nan) - vp[true_indices] = (self.pem.getBulkVel()) - #self.vs = np.full(f_dim, self.avo_config['vs_shale']) - vs = np.full(f_dim, np.nan) - vs[true_indices] = (self.pem.getShearVel()) - #self.rho = np.full(f_dim, self.avo_config['den_shale']) - rho = np.full(f_dim, np.nan) - rho[true_indices] = (self.pem.getDens()) + vp = np.full(f_dim, np.nan) + vp[true_indices] = (self.pem.getBulkVel()) + vs = np.full(f_dim, np.nan) + vs[true_indices] = (self.pem.getShearVel()) + rho = np.full(f_dim, np.nan) + rho[true_indices] = (self.pem.getDens()) + - else: - # option not used for Box or smeaheia--needs to be tested - vp = (self.pem.getBulkVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') - vs = (self.pem.getShearVel()).reshape((self.NX, self.NY, self.NZ))#, order='F') - rho = (self.pem.getDens()).reshape((self.NX, self.NY, self.NZ))#, order='F') ## Debug - #self.bulkmod = np.full(f_dim, np.nan) - #self.bulkmod[true_indices] = self.pem.getBulkMod() + #bulkmod = np.full(f_dim, np.nan) + #bulkmod[true_indices] = self.pem.getBulkMod() #self.shearmod = np.full(f_dim, np.nan) #self.shearmod[true_indices] = self.pem.getShearMod() #self.poverburden = np.full(f_dim, np.nan) @@ -939,31 +1045,30 @@ def calc_velocities(self, folder, save_folder, grid, v, f_dim): #self.pressure[true_indices] = self.pem.getPressure() #self.peff = np.full(f_dim, np.nan) #self.peff[true_indices] = self.pem.getPeff() - porosity = np.full(f_dim, np.nan) - porosity[true_indices] = self.pem.getPorosity() - if self.dyn_var: - sgas = np.full(f_dim, np.nan) - sgas[true_indices] = self.dyn_var[v]['SGAS'] - #soil = np.full(f_dim, np.nan) - #soil[true_indices] = self.dyn_var[v]['SOIL'] - pdyn = np.full(f_dim, np.nan) - pdyn[true_indices] = self.dyn_var[v]['PRESSURE'] + #porosity = np.full(f_dim, np.nan) + #porosity[true_indices] = self.pem.getPorosity() + #if self.dyn_var: + # sgas = np.full(f_dim, np.nan) + # sgas[true_indices] = self.dyn_var[v]['SGAS'] + # soil = np.full(f_dim, np.nan) + # soil[true_indices] = self.dyn_var[v]['SOIL'] + # pdyn = np.full(f_dim, np.nan) + # pdyn[true_indices] = self.dyn_var[v]['PRESSURE'] # + #if self.dyn_var is None: + # save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, + # #'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': porosity} # for debugging + #else: + # save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} - if self.dyn_var is None: - save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'bulkmod': self.bulkmod, 'shearmod': self.shearmod, - #'Pov': self.poverburden, 'P': self.pressure, 'Peff': self.peff, 'por': porosity} # for debugging - else: - save_dic = {'vp': vp, 'vs': vs, 'rho': rho}#, 'por': porosity, 'sgas': sgas, 'Pd': pdyn} - - if save_folder is not None: - file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ - else save_folder + f"vp_vs_rho_vint{v}.npz" - np.savez(file_name, **save_dic) - else: - file_name_rec = 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder}.npz" if folder[-1] != os.sep \ - else 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder[:-1]}.npz" - np.savez(file_name_rec, **save_dic) + #if save_folder is not None: + # file_name = save_folder + os.sep + f"vp_vs_rho_vint{v}.npz" if save_folder[-1] != os.sep \ + # else save_folder + f"vp_vs_rho_vint{v}.npz" + # np.savez(file_name, **save_dic) + #else: + # file_name_rec = 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + # else 'Ensemble_results/' + f"vp_vs_rho_vint{v}_{folder[:-1]}.npz" + # np.savez(file_name_rec, **save_dic) # for debugging return vp, vs, rho @@ -1028,7 +1133,19 @@ def _get_props(self, kw_file): with np.load(file) as f: exec(f'self.{kw} = f[ "{kw}" ]') self.NX, self.NY, self.NZ = f['NX'], f['NY'], f['NZ'] - #else: + else: + try: + self.NX = int(self.input_dict['dimension'][0]) + self.NY = int(self.input_dict['dimension'][1]) + self.NZ = int(self.input_dict['dimension'][2]) + except: + for item in self.input_dict['pem']: + if item[0] == 'dimension': + dimension = item[1] + break + self.NX = int(dimension[0]) + self.NY = int(dimension[1]) + self.NZ = int(dimension[2]) # reader = GRDECL_Parser(filename=file) # reader.read_GRDECL() # exec(f"self.{kw} = reader.{kw}.reshape((reader.NX, reader.NY, reader.NZ), order='F')") @@ -1134,7 +1251,130 @@ def _calc_avo_props(self, dt=0.0005): self.avo_data = avo_data - def _calc_avo_props_active_cells(self, grid, vp, vs, rho, dt=0.0005): + def _calc_avo_props_active_cells(self, grid, vp, vs, rho, dz, zcorn, dt=0.0005): + # dt is the fine resolution sampling rate + # convert properties in reservoir model to time domain + vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) + vs_shale = self.avo_config['vs_shale'] # scalar value + rho_shale = self.avo_config['den_shale'] # scalar value + + + actnum = grid['ACTNUM'] + # Find indices where the boolean array is True + active_indices = np.where(actnum) + c, a, b = active_indices + + # Two-way travel time tp the top of the reservoir + top_res = 2 * zcorn[0, 0, :, 0, :, 0] / vp_shale + + # depth difference between cells in z-direction: + depth_differences = dz#(zcorn[:, 0, :, 0, :, 0] , axis=0) + + + # Cumulative traveling time through the reservoir in vertical direction + #cum_time_res = 2 * zcorn[:, 0, :, 0, :, 0] / self.vp + top_res[np.newaxis, :, :] + cum_time_res = np.cumsum(2 * depth_differences / vp, axis=0) + top_res[np.newaxis, :, :] + # assumes under burden to be constant. No reflections from under burden. Hence set travel time to under burden very large + underburden = top_res + np.nanmax(cum_time_res) + + # total travel time + # cum_time = np.concat enate((top_res[:, :, np.newaxis], cum_time_res), axis=2) + cum_time = np.concatenate((top_res[np.newaxis, :, :], cum_time_res, underburden[np.newaxis, :, :]), axis=0) + + # add overburden and underburden values for Vp, Vs and Density + vp = np.concatenate((vp_shale * np.ones((1, self.NY, self.NX)), + vp, vp_shale * np.ones((1, self.NY, self.NX))), axis=0) + vs = np.concatenate((vs_shale * np.ones((1, self.NY, self.NX)), + vs, vs_shale * np.ones((1, self.NY, self.NX))), axis=0) + rho = np.concatenate((rho_shale * np.ones((1, self.NY, self.NX)), + rho, rho_shale * np.ones((1, self.NY, self.NX))), axis=0) + + + # Combine a and b into a 2D array (each column represents a vector) + ab = np.column_stack((a, b)) + + # Extract unique rows and get the indices of those unique rows + unique_rows, indices = np.unique(ab, axis=0, return_index=True) + + # search for the lowest grid cell thickness and sample the time according to + # that grid thickness to preserve the thin layer effect + time_sample = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], dt) + if time_sample.shape[0] == 1: + time_sample = time_sample.reshape(-1) + #time_sample = np.tile(time_sample, (len(indices), 1)) + time_sample = np.tile(time_sample, (self.NX, self.NY, 1)) + + #vp_sample = vp_shale * np.ones((self.NX, self.NY, time_sample.shape[2])) + #vs_sample = vs_shale * np.ones((self.NX, self.NY, time_sample.shape[2])) + #rho_sample = rho_shale * np.ones((self.NX, self.NY, time_sample.shape[2])) + vp_sample = np.full([self.NX, self.NY, time_sample.shape[2]], np.nan) + vs_sample = np.full([self.NX, self.NY, time_sample.shape[2]], np.nan) + rho_sample = np.full([self.NX, self.NY, time_sample.shape[2]], np.nan) + + + for ind in range(len(indices)): + for k in range(time_sample.shape[2]): + # find the right interval of time_sample[m, l, k] belonging to, and use + # this information to allocate vp, vs, rho + idx = np.searchsorted(cum_time[:, a[indices[ind]], b[indices[ind]]], time_sample[b[indices[ind]], a[indices[ind]], k], side='left') + idx = idx if idx < len(cum_time[:, a[indices[ind]], b[indices[ind]]]) else len( + cum_time[:,a[indices[ind]], b[indices[ind]]]) - 1 + vp_sample[b[indices[ind]], a[indices[ind]], k] = vp[idx, a[indices[ind]], b[indices[ind]]] + vs_sample[b[indices[ind]], a[indices[ind]], k] = vs[idx, a[indices[ind]], b[indices[ind]]] + rho_sample[b[indices[ind]], a[indices[ind]], k] = rho[idx, a[indices[ind]], b[indices[ind]]] + + # Ricker wavelet + wavelet, t_axis, wav_center = ricker(np.arange(0, self.avo_config['wave_len']-dt, dt), + f0=self.avo_config['frequency']) + + # Travel time corresponds to reflectivity series + #t = time_sample[:, 0:-1] + t = time_sample[:, :, 0:-1] + + # interpolation time + t_interp = np.arange(self.avo_config['t_min'], self.avo_config['t_max'], self.avo_config['t_sampling']) + #trace_interp = np.zeros((len(indices), len(t_interp))) + #trace_interp = np.zeros((self.NX, self.NY, len(t_interp))) + trace_interp = np.full([self.NX, self.NY, len(t_interp)], np.nan) + + # number of pp reflection coefficients in the vertical direction + nz_rpp = vp_sample.shape[2] - 1 + conv_op = Convolve1D(nz_rpp, h=wavelet, offset=wav_center, dtype="float32") + + avo_data = [] + Rpp = [] + for i in range(len(self.avo_config['angle'])): + angle = self.avo_config['angle'][i] + Rpp = self.pp_func(vp_sample[:, :, :-1], vs_sample[:, :, :-1], rho_sample[:, :, :-1], + vp_sample[:, :, 1:], vs_sample[:, :, 1:], rho_sample[:, :, 1:], angle) + + for ind in range(len(indices)): + # convolution with the Ricker wavelet + + w_trace = conv_op * Rpp[b[indices[ind]], a[indices[ind]], :] + + # Sample the trace into regular time interval + f = interp1d(np.squeeze(t[b[indices[ind]], a[indices[ind]], :]), np.squeeze(w_trace), + kind='nearest', fill_value='extrapolate') + trace_interp[b[indices[ind]], a[indices[ind]], :] = f(t_interp) + + if i == 0: + avo_data = trace_interp # 3D + elif i == 1: + avo_data = np.stack((avo_data, trace_interp), axis=-1) # 4D + else: + avo_data = np.concatenate((avo_data, trace_interp[:, :, np.newaxis]), axis=2) # 4D + + return avo_data, Rpp, vp_sample, vs_sample + #self.avo_data = avo_data + #self.Rpp = Rpp + #self.vp_sample = vp_sample + #self.vs_sample = vs_sample + #self.rho_sample = rho_sample + + + + def _calc_avo_props_active_cells_org(self, grid, vp, vs, rho, dz, zcorn, dt=0.0005): # dt is the fine resolution sampling rate # convert properties in reservoir model to time domain vp_shale = self.avo_config['vp_shale'] # scalar value (code may not work for matrix value) @@ -1148,8 +1388,7 @@ def _calc_avo_props_active_cells(self, grid, vp, vs, rho, dt=0.0005): # # # # Two-way travel time tp the top of the reservoir - # Use cell depths of top layer - zcorn = grid['ZCORN'] + c, a, b = active_indices @@ -1157,17 +1396,12 @@ def _calc_avo_props_active_cells(self, grid, vp, vs, rho, dt=0.0005): top_res = 2 * zcorn[0, 0, :, 0, :, 0] / vp_shale # depth difference between cells in z-direction: - depth_differences = np.diff(zcorn[:, 0, :, 0, :, 0] , axis=0) - # Extract the last layer - last_layer = depth_differences[-1, :, :] - # Reshape to ensure it has the same number of dimensions - last_layer = last_layer.reshape(1, depth_differences.shape[1], depth_differences.shape[2]) - # Concatenate to the original array along the first axis - extended_differences = np.concatenate([depth_differences, last_layer], axis=0) + depth_differences = dz#(zcorn[:, 0, :, 0, :, 0] , axis=0) + # Cumulative traveling time through the reservoir in vertical direction #cum_time_res = 2 * zcorn[:, 0, :, 0, :, 0] / self.vp + top_res[np.newaxis, :, :] - cum_time_res = np.cumsum(2 * extended_differences / vp, axis=0) + top_res[np.newaxis, :, :] + cum_time_res = np.cumsum(2 * depth_differences / vp, axis=0) + top_res[np.newaxis, :, :] # assumes under burden to be constant. No reflections from under burden. Hence set travel time to under burden very large underburden = top_res + np.nanmax(cum_time_res) @@ -1404,7 +1638,7 @@ def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): else: return array -class flow_grav(flow_rock): +class flow_grav(flow_rock, mixIn_multi_data): def __init__(self, input_dict=None, filename=None, options=None, **kwargs): super().__init__(input_dict, filename, options) @@ -1424,7 +1658,6 @@ def run_fwd_sim(self, state, member_i, del_folder=True): self.pred_data = super().run_fwd_sim(state, member_i, del_folder) return self.pred_data - def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. # Then, get the pem. @@ -1453,15 +1686,27 @@ def get_grav_result(self, folder, save_folder): grid = self.ecl_case.grid() - #f_dim = [self.NZ, self.NY, self.NX] - self.dyn_var = [] # cell centers - self.find_cell_centers(grid) + #cell_centre = self.find_cell_centre(grid) # receiver locations - self.measurement_locations(grid) + # Make a mesh of the area + pad = self.grav_config.get('padding', 1500) # 3 km padding around the reservoir + if 'padding' not in self.grav_config: + print('Please specify extent of measurement locations (Padding in input file), using 1.5 km as default') + dxy = self.grav_config.get('grid_spacing', 1500) # + if 'grid_spacing' not in self.grav_config: + print('Please specify grid spacing in input file, using 1.5 km as default') + if 'seabed' in self.grav_config and self.grav_config['seabed'] is not None: + file_path = self.grav_config['seabed'] + water_depth = self.get_seabed_depths(file_path) + else: + water_depth = self.grav_config.get('water_depth', 300) + if 'water_depth' not in self.grav_config: + print('Please specify water depths in input file, using 300 m as default') + pos = self.measurement_locations(grid, water_depth, pad, dxy) # loop over vintages with gravity acquisitions grav_struct = {} @@ -1474,9 +1719,9 @@ def get_grav_result(self, folder, save_folder): else: - # seafloor gravity only work in 4D mode + # seafloor gravity only works in 4D mode grav_base = None - print('Need to specify Baseline survey for gravity in pipt file') + print('Need to specify Baseline survey for gravity in input file') for v, assim_time in enumerate(self.grav_config['vintage']): time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ @@ -1489,14 +1734,15 @@ def get_grav_result(self, folder, save_folder): vintage = [] + for v, assim_time in enumerate(self.grav_config['vintage']): - dg = self.calc_grav(grid, grav_base, grav_struct[v]) + dg = self.calc_grav(grid, grav_base, grav_struct[v], pos) vintage.append(deepcopy(dg)) #save_dic = {'grav': dg, **self.grav_config} save_dic = { 'grav': dg, 'P_vint': grav_struct[v]['PRESSURE'], 'rho_gas_vint':grav_struct[v]['GAS_DEN'], - **self.grav_config, + 'meas_location': pos, **self.grav_config, **{key: grav_struct[v][key] - grav_base[key] for key in grav_struct[v].keys()} } if save_folder is not None: @@ -1505,12 +1751,23 @@ def get_grav_result(self, folder, save_folder): else: file_name = folder + os.sep + f"grav_vint{v}.npz" if folder[-1] != os.sep \ else folder + f"grav_vint{v}.npz" - file_name_rec = 'Ensemble_results/' + f"grav_vint{v}_{folder}.npz" if folder[-1] != os.sep \ - else 'Ensemble_results/' + f"grav_vint{v}_{folder[:-1]}.npz" + prior_folder = 'Prior_ensemble_results' + try: + files = os.listdir(prior_folder) + filename_to_check = f"grav_vint{v}_{folder}.npz" + + if filename_to_check in files: + file_name_rec = 'Ensemble_results/' + f"grav_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"grav_vint{v}_{folder[:-1]}.npz" + else: + file_name_rec = 'Prior_ensemble_results/' + f"grav_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Prior_ensemble_results/' + f"grav_vint{v}_{folder[:-1]}.npz" + + except: + file_name_rec = 'Ensemble_results/' + f"grav_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"grav_vint{v}_{folder[:-1]}.npz" np.savez(file_name_rec, **save_dic) - # with open(file_name, "wb") as f: - # dump(**save_dic, f) np.savez(file_name, **save_dic) @@ -1660,20 +1917,17 @@ def calc_mass(self, time, time_index = None): return grav_input - def calc_grav(self, grid, grav_base, grav_repeat): + def calc_grav(self, grid, grav_base, grav_repeat, pos): - #cell_centre = [x, y, z] - cell_centre = self.grav_config['cell_centre'] + cell_centre = self.find_cell_centre(grid) x = cell_centre[0] y = cell_centre[1] z = cell_centre[2] - pos = self.grav_config['meas_location'] - # Initialize dg as a zero array, with shape depending on the condition # assumes the length of each vector gives the total number of measurement points - N_meas = (len(pos['x'])) - dg = np.zeros(N_meas) # 1D array for dg + n_meas = (len(pos['x'])) + dg = np.zeros(n_meas) # 1D array for dg dg[:] = np.nan # fluid phases given as input @@ -1695,243 +1949,69 @@ def calc_grav(self, grid, grav_base, grav_repeat): print('Type and number of fluids are unspecified in calc_grav') - for j in range(N_meas): + for j in range(n_meas): # Calculate dg for the current measurement location (j, i) dg_tmp = (z - pos['z'][j]) / ((x - pos['x'][j]) ** 2 + (y - pos['y'][j]) ** 2 + ( z - pos['z'][j]) ** 2) ** (3 / 2) dg[j] = np.dot(dg_tmp, dm) - #print(f'Progress: {j + 1}/{N_meas}') # Mimicking wait bar + #print(f'Progress: {j + 1}/{n_meas}') # Mimicking wait bar # Scale dg by the constant dg *= 6.67e-3 return dg - def measurement_locations(self, grid): - # Determine the size of the target area as defined by the reservoir area + def _get_grav_info(self, grav_config=None): + """ + GRAV configuration + """ + # list of configuration parameters in the "Grav" section of teh pipt file + config_para_list = ['baseline', 'vintage', 'water_depth', 'padding', 'grid_spacing', 'seabed'] - #cell_centre = [x, y, z] - cell_centre = self.grav_config['cell_centre'] - xmin = np.min(cell_centre[0]) - xmax = np.max(cell_centre[0]) - ymin = np.min(cell_centre[1]) - ymax = np.max(cell_centre[1]) + if 'grav' in self.input_dict: + self.grav_config = {} + for elem in self.input_dict['grav']: + assert elem[0] in config_para_list, f'Property {elem[0]} not supported' + if elem[0] == 'vintage' and not isinstance(elem[1], list): + elem[1] = [elem[1]] + self.grav_config[elem[0]] = elem[1] + else: + self.grav_config = None - # Make a mesh of the area - pad = self.grav_config.get('padding', 1500) # 3 km padding around the reservoir - if 'padding' not in self.grav_config: - print('Please specify extent of measurement locations (Padding in pipt file), using 1.5 km as default') + def extract_data(self, member): + # start by getting the data from the flow simulator + super(flow_rock, self).extract_data(member) - xmin -= pad - xmax += pad - ymin -= pad - ymax += pad + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'grav' in key: + if self.true_prim[1][prim_ind] in self.grav_config['vintage']: + v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.grav_result[v].flatten() - xspan = xmax - xmin - yspan = ymax - ymin +class flow_seafloor_disp(flow_rock, mixIn_multi_data): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) - dxy = self.grav_config.get('grid_spacing', 1500) # - if 'grid_spacing' not in self.grav_config: - print('Please specify grid spacing in pipt file, using 1.5 km as default') + assert 'sea_disp' in input_dict, 'To do subsidence/uplift simulation, please specify an "SEA_DISP" section in the pipt file' + self._get_disp_info() - Nx = int(np.ceil(xspan / dxy)) - Ny = int(np.ceil(yspan / dxy)) - xvec = np.linspace(xmin, xmax, Nx) - yvec = np.linspace(ymin, ymax, Ny) + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) - x, y = np.meshgrid(xvec, yvec) + super().setup_fwd_run(redund_sim=None) - pos = {'x': x.flatten(), 'y': y.flatten()} + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) - # Handle seabed map or water depth scalar if defined in pipt - if 'seabed' in self.grav_config and self.grav_config['seabed'] is not None: - # read seabed depths from file - water_depths = self.get_seabed_depths() - # get water depths at measurement locations - pos['z'] = griddata((water_depths['x'], water_depths['y']), - np.abs(water_depths['z']), (pos['x'], pos['y']), method='nearest') # z is positive downwards - else: - pos['z'] = np.ones_like(pos['x']) * self.grav_config.get('water_depth', 300) - - if 'water_depth' not in self.grav_config: - print('Please specify water depths in pipt file, using 300 m as default') - - #return pos - self.grav_config['meas_location'] = pos - - def get_seabed_depths(self): - # Path to your CSV file - file_path = self.grav_config['seabed'] # Replace with your actual file path - - # Read the data while skipping the header comments - # We'll assume the header data ends before the numerical data - # The 'delim_whitespace' keyword in pd.read_csv is deprecated and will be removed in a future version. Use ``sep='\s+'`` instead - water_depths = pd.read_csv(file_path, comment='#', sep=r'\s+', header=None)#delim_whitespace=True, header=None) - - # Give meaningful column names: - water_depths.columns = ['x', 'y', 'z', 'column', 'row'] - - return water_depths - - def find_cell_centers(self, grid): - - # Find indices where the boolean array is True - indices = np.where(grid['ACTNUM']) - #actnum = np.transpose(grid['ACTNUM'], (2, 1, 0)) - #indices = np.where(actnum) - # `indices` will be a tuple of arrays: (x_indices, y_indices, z_indices) - #nactive = len(actind) # Number of active cells - - coord = grid['COORD'] - zcorn = grid['ZCORN'] - - # Unpack dimensions - #N1, N2, N3 = grid['DIMENS'] - - - #b, a, c = indices - c, a, b = indices - # Calculate xt, yt, zt - xb = 0.25 * (coord[a, b, 0, 0] + coord[a, b + 1, 0, 0] + coord[a + 1, b, 0, 0] + coord[a + 1, b + 1, 0, 0]) - yb = 0.25 * (coord[a, b, 0, 1] + coord[a, b + 1, 0, 1] + coord[a + 1, b, 0, 1] + coord[a + 1, b + 1, 0, 1]) - zb = 0.25 * (coord[a, b, 0, 2] + coord[a, b + 1, 0, 2] + coord[a + 1, b, 0, 2] + coord[a + 1, b + 1, 0, 2]) - - xt = 0.25 * (coord[a, b, 1, 0] + coord[a, b + 1, 1, 0] + coord[a + 1, b, 1, 0] + coord[a + 1, b + 1, 1, 0]) - yt = 0.25 * (coord[a, b, 1, 1] + coord[a, b + 1, 1, 1] + coord[a + 1, b, 1, 1] + coord[a + 1, b + 1, 1, 1]) - zt = 0.25 * (coord[a, b, 1, 2] + coord[a, b + 1, 1, 2] + coord[a + 1, b, 1, 2] + coord[a + 1, b + 1, 1, 2]) - - # Calculate z, x, and y positions - z = (zcorn[c, 0, a, 0, b, 0] + zcorn[c, 0, a, 1, b, 0] + zcorn[c, 0, a, 0, b, 1] + zcorn[c, 0, a, 1, b, 1] + - zcorn[c, 1, a, 0, b, 0] + zcorn[c, 1, a, 1, b, 0] + zcorn[c, 1, a, 0, b, 1] + zcorn[c, 1, a, 1, b, 1]) / 8 - - x = xb + (xt - xb) * (z - zb) / (zt - zb) - y = yb + (yt - yb) * (z - zb) / (zt - zb) - - - cell_centre = [x, y, z] - self.grav_config['cell_centre'] = cell_centre - - def _get_grav_info(self, grav_config=None): - """ - GRAV configuration - """ - # list of configuration parameters in the "Grav" section of teh pipt file - config_para_list = ['baseline', 'vintage', 'water_depth', 'padding', 'grid_spacing', 'seabed'] - - if 'grav' in self.input_dict: - self.grav_config = {} - for elem in self.input_dict['grav']: - assert elem[0] in config_para_list, f'Property {elem[0]} not supported' - if elem[0] == 'vintage' and not isinstance(elem[1], list): - elem[1] = [elem[1]] - self.grav_config[elem[0]] = elem[1] - else: - self.grav_config = None - - def extract_data(self, member): - # start by getting the data from the flow simulator - super(flow_rock, self).extract_data(member) - - # get the gravity data from results - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if 'grav' in key: - if self.true_prim[1][prim_ind] in self.grav_config['vintage']: - v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.grav_result[v].flatten() - -class flow_grav_and_avo(flow_avo, flow_grav): - def __init__(self, input_dict=None, filename=None, options=None, **kwargs): - super().__init__(input_dict, filename, options) - - self.grav_input = {} - assert 'grav' in input_dict, 'To do GRAV simulation, please specify an "GRAV" section in the "FWDSIM" part' - self._get_grav_info() - - assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' - self._get_avo_info() - - def setup_fwd_run(self, **kwargs): - self.__dict__.update(kwargs) - - super().setup_fwd_run(redund_sim=None) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder) - - return self.pred_data - - def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): - # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. - # Then, get the pem. - if folder is None: - folder = self.folder - else: - self.folder = folder - - # run flow simulator - # success = True - success = super(flow_rock, self).call_sim(folder, True) - - # use output from flow simulator to forward model gravity response - if success: - # calculate gravity data based on flow simulation output - self.get_grav_result(folder, save_folder) - # calculate avo data based on flow simulation output - self.get_avo_result(folder, save_folder) - - return success - - - def extract_data(self, member): - # start by getting the data from the flow simulator i.e. prod. and inj. data - super(flow_rock, self).extract_data(member) - - # get the gravity data from results - for prim_ind in self.l_prim: - # Loop over all keys in pred_data (all data types) - for key in self.all_data_types: - if 'grav' in key: - if self.true_prim[1][prim_ind] in self.grav_config['vintage']: - v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) - self.pred_data[prim_ind][key] = self.grav_result[v].flatten() - - if 'avo' in key: - if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ - else self.folder + key + '_vint' + str(idx) + '.npz' - with np.load(filename) as f: - self.pred_data[prim_ind][key] = f[key] - #v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) - #self.pred_data[prim_ind][key] = self.avo_result[v].flatten() - -class flow_seafloor_disp(flow_grav): - def __init__(self, input_dict=None, filename=None, options=None, **kwargs): - super().__init__(input_dict, filename, options) - - self.grav_input = {} - assert 'sea_disp' in input_dict, 'To do subsidence/uplift simulation, please specify an "SEA_DISP" section in the pipt file' - self._get_disp_info() - - - def setup_fwd_run(self, **kwargs): - self.__dict__.update(kwargs) - - super().setup_fwd_run(redund_sim=None) - - def run_fwd_sim(self, state, member_i, del_folder=True): - # The inherited simulator also has a run_fwd_sim. Call this. - self.ensemble_member = member_i - self.pred_data = super().run_fwd_sim(state, member_i, del_folder) - - return self.pred_data + return self.pred_data def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. @@ -1962,56 +2042,61 @@ def get_displacement_result(self, folder, save_folder): self.dyn_var = [] - # cell centers - self.find_cell_centers(grid) - # receiver locations - self.measurement_locations(grid) + pad = self.disp_config.get('padding', 1500) # 3 km padding around the reservoir + if 'padding' not in self.disp_config: + print('Please specify extent of measurement locations, padding in input file, using 1.5 km as default') + dxy = self.disp_config.get('grid_spacing', 1500) # + if 'grid_spacing' not in self.disp_config: + print('Please specify grid spacing in input file, using 1.5 km as default') + if 'seabed' in self.disp_config and self.disp_config['seabed'] is not None: + file_path = self.disp_config['seabed'] + water_depth = self.get_seabed_depths(file_path) + else: + water_depth = self.disp_config.get('water_depth', 300) + if 'water_depth' not in self.disp_config: + print('Please specify water depths in input file, using 300 m as default') + pos = self.measurement_locations(grid, water_depth, pad, dxy) # loop over vintages with gravity acquisitions disp_struct = {} if 'baseline' in self.disp_config: # 4D measurement base_time = dt.datetime(self.startDate['year'], self.startDate['month'], - self.startDate['day']) + dt.timedelta(days=self.grav_config['baseline']) - # porosity, saturation, densities, and fluid mass at time of baseline survey + self.startDate['day']) + dt.timedelta(days=self.disp_config['baseline']) + # pore volume at time of baseline survey disp_base = self.get_pore_volume(base_time, 0) - else: # seafloor displacement only work in 4D mode disp_base = None - print('Need to specify Baseline survey for displacement modelling in pipt file') + print('Need to specify Baseline survey for displacement modelling in input file') for v, assim_time in enumerate(self.disp_config['vintage']): time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ dt.timedelta(days=assim_time) - # porosity, saturation, densities, and fluid mass at individual time-steps + # pore volume and pressure at individual time-steps disp_struct[v] = self.get_pore_volume(time, v+1) # calculate the mass of each fluid in each grid cell - - vintage = [] for v, assim_time in enumerate(self.disp_config['vintage']): # calculate subsidence and uplift - dz = self.map_z_response(disp_base, disp_struct[v], grid) - vintage.append(deepcopy(dz)) + dz_seafloor = self.map_z_response(disp_base, disp_struct[v], grid, pos) + vintage.append(deepcopy(dz_seafloor)) - save_dic = {'disp': dz, **self.disp_config} + save_dic = {'sea_disp': dz_seafloor, 'meas_location': pos, **self.disp_config} if save_folder is not None: file_name = save_folder + os.sep + f"sea_disp_vint{v}.npz" if save_folder[-1] != os.sep \ else save_folder + f"sea_disp_vint{v}.npz" else: file_name = folder + os.sep + f"sea_disp_vint{v}.npz" if folder[-1] != os.sep \ else folder + f"sea_disp_vint{v}.npz" - file_name_rec = f"sea_disp_vint{v}_{folder}.npz" if folder[-1] != os.sep \ - else f"sea_disp_vint{v}_{folder[:-1]}.npz" + file_name_rec = 'Ensemble_results/' + f"sea_disp_vint{v}_{folder}.npz" if folder[-1] != os.sep \ + else 'Ensemble_results/' + f"sea_disp_vint{v}_{folder[:-1]}.npz" np.savez(file_name_rec, **save_dic) - # with open(file_name, "wb") as f: - # dump(**save_dic, f) np.savez(file_name, **save_dic) @@ -2027,9 +2112,6 @@ def get_pore_volume(self, time, time_index = None): else: time_input = time - # fluid phases given as input - phases = str.upper(self.pem_input['phases']) - phases = phases.split() # disp_input = {} tmp_dyn_var = {} @@ -2063,7 +2145,7 @@ def compute_horizontal_distance(self, pos, x, y): rho = np.sqrt(dx ** 2 + dy ** 2).flatten() return rho - def map_z_response(self, base, repeat, grid): + def map_z_response(self, base, repeat, grid, pos): """ Maps out subsidence and uplift based either on the simulation model pressure drop (method = 'pressure') or simulated change in pore volume @@ -2099,11 +2181,7 @@ def map_z_response(self, base, repeat, grid): E = ((1 + poisson) * (1 - 2 * poisson)) / ((1 - poisson) * compressibility) # coordinates of cell centres - cell_centre = self.grav_config['cell_centre'] - - # measurement locations - pos = self.grav_config['meas_location'] - + cell_centre = self.find_cell_centre(grid) # compute pore volume change between baseline and repeat survey # based on the reservoir pore volumes in the individual vintages @@ -2112,19 +2190,11 @@ def map_z_response(self, base, repeat, grid): else: dV = base['RPORV'] - repeat['RPORV'] - # coordinates of cell centres + # coordinates of active cell centres x = cell_centre[0] y = cell_centre[1] z = cell_centre[2] - # Depth range of reservoir plus vertical span in seafloor measurement positions - # z_res = np.linspace(np.min(z) - np.max(pos['z']) - 1, np.max(z) - np.min(pos['z']) + 1) - # Maximum horizontal span between seafloor measurement location and reservoir boundary - #rho_x_max = max(np.max(x) - np.min(pos['x']), np.max(pos['x']) - np.min(x)) - #rho_y_max = max(np.max(y) - np.min(pos['y']), np.max(pos['y']) - np.min(y)) - #rho = np.linspace(0, np.sqrt(rho_x_max ** 2 + rho_y_max ** 2) + 1) - #rho_mesh, z_res_mesh = np.meshgrid(rho, z_res) - #t_van_opstal, t_geertsma = self.compute_van_opstal_transfer_function(z_res, z_base, rho, poisson) if model == 'van_Opstal': # Represents a signal change for subsidence/uplift. @@ -2140,34 +2210,31 @@ def map_z_response(self, base, repeat, grid): # indices of active cells: true_indices = np.where(grid['ACTNUM']) # number of active gridcells - Nn = len(true_indices[0]) + n_nucleous = len(true_indices[0]) + assert n_nucleous == len(x) + #pr = cProfile.Profile() + #pr.enable() - for j in range(Nn): + for j in range(n_nucleous): rho = self.compute_horizontal_distance(pos, x[j], y[j]) THH, TRB = self.compute_deformation_transfer(pos['z'], z[j], z_base, rho, poisson, E, dV[j], component) dz_1_2 = dz_1_2 + THH dz_3 = dz_3 + TRB + #pr.disable() + + # Print profiling results + #stats = pstats.Stats(pr) + #stats.strip_dirs() + #stats.sort_stats('cumulative') + #stats.print_stats() + if model == 'van_Opstal': # Represents a signal change for subsidence/uplift. dz = dz_1_2 + dz_3 else: # Use Geertsma dz = dz_1_2 - #ny, nx = pos['x'].shape - #dz = np.zeros((ny, nx)) - - # Compute subsidence and uplift - #for j in range(ny): - # for i in range(nx): - # r = np.sqrt((x - pos['x'][j, i]) ** 2 + (y - pos['y'][j, i]) ** 2) - # dz[j, i] = np.sum( - # dV * griddata((rho_mesh.flatten(), z_res_mesh.flatten()), trans_func, (r, z[j, i] - pos['z'][j, i]), - # method='linear')) - - # Normalize - #dz = dz * (1 - poisson) / (2 * np.pi) - # Convert from meters to centimeters dz *= 100 @@ -2336,99 +2403,72 @@ def makeC(self,poisson, k, c, A_g, eps, lambda_): C = numerator / Delta return C + def uHH_integrand(self, lambda_, z, rho, eps, c, poisson): + val = lambda_ * (eps * np.exp(lambda_ * eps * (z - c)) + + (3 - 4 * poisson + 2 * z * lambda_) * + np.exp(-lambda_ * (z + c))) + return val * j0(lambda_ * rho) + def compute_deformation_transfer(self, z, c, k, rho, poisson, E, dV, component): - # Convert inputs to numpy arrays if they are not already - z = np.array(z) - #x = np.array(x) - rho = np.array(rho) + scale = 1000 # convert to km + # depth of receiver positions + z = np.array(z)/scale + rho = np.array(rho)/scale + # depth of reservoir cell + c = c/scale + k = k/scale component = list(component) - - Ni = len(z) - Nj = len(rho) - - # Initialize output arrays - THH = np.zeros((Ni, Nj)) - #THHr = np.zeros((Ni, Nj)) - TRB = np.zeros((Ni, Nj)) - #TRBr = np.zeros((Ni, Nj)) + # number of measurement locations + n_rec = len(z) + assert len(rho) == n_rec + THH = np.zeros(n_rec) + TRB = np.zeros(n_rec) # Constants A_g = -dV * E / (4 * np.pi * (1 + poisson)) uHH_outside_intregral = -(A_g * (1 + poisson)) / E uRB_outside_intregral = (1 + poisson) / E - lambda_max = min([0.1, 500 / max(z)]) # Avoid index errors if z is empty - # Setup your lambda grid - num_points = 500 - lambda_grid = np.linspace(0, lambda_max, num_points) for c_n in component: if c_n == 'Geertsma_vertical': - for j in range(Nj): - for i in range(Ni): + lambda_max = 15 / np.max(rho).item() + for i in range(n_rec): + if rho[i] > np.abs(c-z[i])*3: + THH[i] = 0 + else: eps = np.sign(c - z[i]) - z_ratio = z[i] - c - z_sum = z[i] + c - - # Evaluate the integrand over the grid - integrand_vals = lambda lam: lam * ( - eps * np.exp(lam * eps * z_ratio) + - (3 - 4 * poisson + 2 * z[i] * lam) * np.exp(-lam * z_sum) - ) * j0(lam * rho[j]) - - values = integrand_vals(lambda_grid) - - #def uHH_integrand(lambda_): - # val = lambda_ * (eps * np.exp(lambda_ * eps * (z[i] - c)) + - # (3 - 4 * poisson + 2 * z[i] * lambda_) * - # np.exp(-lambda_ * (z[i] + c))) - # return val * j0(lambda_ * rho[j]) - - THH[i, j] = np.trapz(values, lambda_grid) * uHH_outside_intregral - #THH[i, j] = quad(uHH_integrand, 0, lambda_max)[0] * uHH_outside_intregral + THH[i] = quad(lambda lambda_var: self.uHH_integrand(lambda_var, z[i], rho[i], eps, c, poisson), 0, lambda_max)[0] * uHH_outside_intregral + THH[i] = THH[i]*scale**-2 elif c_n == 'System_3_vertical': + lambda_max = 30 / np.max(rho).item() + num_points = 500 + lambda_grid = np.linspace(0, lambda_max, num_points) + sinh_z = np.sinh(z[:, np.newaxis] * lambda_grid) cosh_z = np.cosh(z[:, np.newaxis] * lambda_grid) J0_rho = j0(lambda_grid * rho) - for j in range(Nj): - rho_j = rho[j] - J0_rho_j = J0_rho[:, j] - for i in range(Ni): + # + for i in range(n_rec): + if rho[i] > np.abs(c-z[i])*3: + TRB[i] = 0 + else: z_i = z[i] - sinh_z_i = sinh_z[i] # precomputed sinh values - cosh_z_i = cosh_z[i] # precomputed cosh values + sinh_z_i = sinh_z[i] + cosh_z_i = cosh_z[i] - # Use vectorized operations over lambda_grid b_values = self.makeB(poisson, k, c, A_g, -1, lambda_grid) c_values = self.makeC(poisson, k, c, A_g, -1, lambda_grid) part1 = b_values * (lambda_grid * z_i * cosh_z_i - (1 - 2 * poisson) * sinh_z_i) part2 = c_values * ((2 * (1 - poisson) * cosh_z_i) - lambda_grid * z_i * sinh_z_i) - values = (part1 + part2) * J0_rho_j + values = (part1 + part2) * J0_rho[:, i]#J0_rho_j integral_result = np.trapz(values, lambda_grid) - TRB[i, j] = integral_result * uRB_outside_intregral - - elif c_n == 'System_3_vertical_original': - for j in range(Nj): - for i in range(Ni): - # Assume makeB and makeC are implemented similarly - def uRB_integrand(lambda_): - b_val = self.makeB(poisson, k, c, A_g, -1, lambda_) - c_val = self.makeC(poisson, k, c, A_g, -1, lambda_) - # Assuming makeB and makeC return scalars. Replace with actual functions. - part1 = b_val * (lambda_ * z[i] * np.cosh(z[i] * lambda_) - (1 - 2 * poisson) * np.sinh( - z[i] * lambda_)) - part2 = c_val * (2 * (1 - poisson) * np.cosh(z[i] * lambda_) + (-lambda_) * z[i] * np.sinh( - z[i] * lambda_)) - return (part1 + part2) * j0(lambda_ * rho[j]) - - values = uRB_integrand(lambda_grid) - integral_result = np.trapz(values, lambda_grid) - TRB[i, j] = integral_result * uRB_outside_intregral - #TRB[i, j] = quad(uRB_integrand, 0, lambda_max)[0] * uRB_outside_intregral + TRB[i] = integral_result * uRB_outside_intregral + TRB[i] = TRB[i]*scale**-2 return THH, TRB @@ -2477,12 +2517,13 @@ def h_t(self, h, r=None, k=None, i_k=None): return h_t, i_k - def _get_disp_info(self, grav_config=None): + def _get_disp_info(self, disp_config=None): """ seafloor displacement (uplift/subsidence) configuration """ # list of configuration parameters in the "Grav" section of teh pipt file - config_para_list = ['baseline', 'vintage', 'method', 'model', 'poisson', 'compressibility', 'z_base'] + config_para_list = ['baseline', 'vintage', 'method', 'model', 'poisson', 'compressibility', + 'z_base', 'grid_spacing', 'padding', 'seabed', 'water_depth'] if 'sea_disp' in self.input_dict: self.disp_config = {} @@ -2494,6 +2535,64 @@ def _get_disp_info(self, grav_config=None): else: self.disp_config = None + def extract_data(self, member): + # start by getting the data from the flow simulator i.e. prod. and inj. data + super(flow_rock, self).extract_data(member) + + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'sea_disp' in key: + if self.true_prim[1][prim_ind] in self.disp_config['vintage']: + v = self.disp_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.disp_result[v].flatten() + +class flow_grav_and_avo(flow_avo, flow_grav): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + self.grav_input = {} + assert 'grav' in input_dict, 'To do GRAV simulation, please specify an "GRAV" section in the "FWDSIM" part' + self._get_grav_info() + + assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' + self._get_avo_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run(redund_sim=None) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if folder is None: + folder = self.folder + else: + self.folder = folder + + # run flow simulator + # success = True + success = super(flow_rock, self).call_sim(folder, True) + + # use output from flow simulator to forward model gravity response + if success: + # calculate gravity data based on flow simulation output + self.get_grav_result(folder, save_folder) + # calculate avo data based on flow simulation output + self.get_avo_result(folder, save_folder) + + return success + + def extract_data(self, member): # start by getting the data from the flow simulator i.e. prod. and inj. data super(flow_rock, self).extract_data(member) @@ -2507,9 +2606,149 @@ def extract_data(self, member): v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) self.pred_data[prim_ind][key] = self.grav_result[v].flatten() + if 'avo' in key: + if self.true_prim[1][prim_ind] in self.pem_input['vintage']: + idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ + else self.folder + key + '_vint' + str(idx) + '.npz' + with np.load(filename) as f: + self.pred_data[prim_ind][key] = f[key] + #v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + #self.pred_data[prim_ind][key] = self.avo_result[v].flatten() + +class flow_grav_seafloor_disp_and_avo(flow_avo, flow_grav, flow_seafloor_disp): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + assert 'grav' in input_dict, 'To do GRAV simulation, please specify an "GRAV" section in the "FWDSIM" part' + self._get_grav_info() - if 'subs_uplift' in key: + assert 'avo' in input_dict, 'To do AVO simulation, please specify an "AVO" section in the "FWDSIM" part' + self._get_avo_info() + + assert 'sea_disp' in input_dict, 'To do subsidence/uplift simulation, please specify an "SEA_DISP" section in the "FWDSIM" part' + self._get_disp_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run(redund_sim=None) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if folder is None: + folder = self.folder + else: + self.folder = folder + + # run flow simulator + # success = True + success = super(flow_rock, self).call_sim(folder, True) + + # use output from flow simulator to forward model gravity response + if success: + # calculate gravity data based on flow simulation output + self.get_grav_result(folder, save_folder) + # calculate avo data based on flow simulation output + self.get_avo_result(folder, save_folder) + # calculate gravity data based on flow simulation output + self.get_displacement_result(folder, save_folder) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator i.e. prod. and inj. data + super(flow_rock, self).extract_data(member) + + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'grav' in key: + if self.true_prim[1][prim_ind] in self.grav_config['vintage']: + v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.grav_result[v].flatten() + + if 'avo' in key: if self.true_prim[1][prim_ind] in self.pem_input['vintage']: - v = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + idx = self.pem_input['vintage'].index(self.true_prim[1][prim_ind]) + filename = self.folder + os.sep + key + '_vint' + str(idx) + '.npz' if self.folder[-1] != os.sep \ + else self.folder + key + '_vint' + str(idx) + '.npz' + with np.load(filename) as f: + self.pred_data[prim_ind][key] = f[key] + + if 'sea_disp' in key: + if self.true_prim[1][prim_ind] in self.disp_config['vintage']: + v = self.disp_config['vintage'].index(self.true_prim[1][prim_ind]) self.pred_data[prim_ind][key] = self.disp_result[v].flatten() +class flow_grav_seafloor_disp(flow_grav, flow_seafloor_disp): + def __init__(self, input_dict=None, filename=None, options=None, **kwargs): + super().__init__(input_dict, filename, options) + + assert 'grav' in input_dict, 'To do GRAV simulation, please specify an "GRAV" section in the "FWDSIM" part' + self._get_grav_info() + + assert 'sea_disp' in input_dict, 'To do subsidence/uplift simulation, please specify an "SEA_DISP" section in the "FWDSIM" part' + self._get_disp_info() + + def setup_fwd_run(self, **kwargs): + self.__dict__.update(kwargs) + + super().setup_fwd_run(redund_sim=None) + + def run_fwd_sim(self, state, member_i, del_folder=True): + # The inherited simulator also has a run_fwd_sim. Call this. + self.ensemble_member = member_i + self.pred_data = super().run_fwd_sim(state, member_i, del_folder) + + return self.pred_data + + def call_sim(self, folder=None, wait_for_proc=False, save_folder=None): + # the super run_fwd_sim will invoke call_sim. Modify this such that the fluid simulator is run first. + # Then, get the pem. + if folder is None: + folder = self.folder + else: + self.folder = folder + + # run flow simulator + # success = True + success = super(flow_rock, self).call_sim(folder, True) + + # use output from flow simulator to forward model gravity response + if success: + # calculate gravity data based on flow simulation output + self.get_grav_result(folder, save_folder) + # calculate avo data based on flow simulation output + #self.get_avo_result(folder, save_folder) + # calculate gravity data based on flow simulation output + self.get_displacement_result(folder, save_folder) + + return success + + def extract_data(self, member): + # start by getting the data from the flow simulator i.e. prod. and inj. data + super(flow_rock, self).extract_data(member) + + # get the gravity data from results + for prim_ind in self.l_prim: + # Loop over all keys in pred_data (all data types) + for key in self.all_data_types: + if 'grav' in key: + if self.true_prim[1][prim_ind] in self.grav_config['vintage']: + v = self.grav_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.grav_result[v].flatten() + + if 'sea_disp' in key: + if self.true_prim[1][prim_ind] in self.disp_config['vintage']: + v = self.disp_config['vintage'].index(self.true_prim[1][prim_ind]) + self.pred_data[prim_ind][key] = self.disp_result[v].flatten() \ No newline at end of file diff --git a/simulator/rockphysics/softsandrp.py b/simulator/rockphysics/softsandrp.py index 207237c..5011049 100644 --- a/simulator/rockphysics/softsandrp.py +++ b/simulator/rockphysics/softsandrp.py @@ -197,6 +197,7 @@ def calc_props(self, phases, saturations, pressure, if dens is None: densf_SI = self._fluid_densSIprop(self.phases, saturations[i, :], pressure[i]) + bulkf_Brie = self._fluidprops_Brie(self.phases, saturations[i, :], pressure[i], densf_SI) densf, bulkf = \ self._fluidprops_Wood(self.phases, saturations[i, :], pressure[i], Rs[i]) @@ -495,7 +496,7 @@ def dz_dp(p_pr, t_pr): # #----------------------------------------------------------- # - def _phaseprops_Smeaheia(self, fphase, press, fdens, Rs=None, t = 37, CO2 = None): + def _phaseprops_Smeaheia(self, fphase, press, fdens, Rs=None, t = 37, CO2 = True): # # Calculate properties for a single fluid phase # @@ -554,8 +555,8 @@ def _phaseprops_Smeaheia(self, fphase, press, fdens, Rs=None, t = 37, CO2 = None dz_dp = self.dz_dp(p_pr, t_pr) pbulk = press / (1 - p_pr * dz_dp / Z) * r_0 - pbulk_test = self.test_new_implementation(press) - print(np.max(pbulk-pbulk_test)) + #pbulk_test = self.test_new_implementation(press) + #print(np.max(pbulk-pbulk_test)) elif fphase.lower() == "gas": # refers to Methane gs = 0.5537 #https://www.engineeringtoolbox.com/specific-gravities-gases-d_334.html From 582f1040ec8582052baed483addb386276b9eded Mon Sep 17 00:00:00 2001 From: mlie Date: Thu, 6 Nov 2025 11:25:03 +0100 Subject: [PATCH 15/15] More updates after merge of Ramonco branch --- ensemble/ensemble.py | 2 +- pipt/loop/ensemble.py | 19 +++++- pipt/misc_tools/extract_tools.py | 8 ++- pipt/update_schemes/gies/gies_base.py | 2 +- simulator/flow_rock.py | 90 ++++++++++++++++++++------- 5 files changed, 94 insertions(+), 27 deletions(-) diff --git a/ensemble/ensemble.py b/ensemble/ensemble.py index 74ce404..48cd716 100644 --- a/ensemble/ensemble.py +++ b/ensemble/ensemble.py @@ -152,7 +152,7 @@ def __init__(self, keys_en, sim, redund_sim=None): else: # Use the number of ensemble members specified in input file (may be fewer than loaded) self.ne = int(self.keys_en['ne']) - if self.ne < min(tmp_ne): + if self.ne <= min(tmp_ne): # pick correct number of ensemble members self.state = {key: val[:,:self.ne] for key, val in self.state.items()} else: diff --git a/pipt/loop/ensemble.py b/pipt/loop/ensemble.py index 2c82f0f..b986a76 100644 --- a/pipt/loop/ensemble.py +++ b/pipt/loop/ensemble.py @@ -540,7 +540,24 @@ def _ext_scaling(self): self.state_scaling = at.calc_scaling( self.prior_state, self.list_states, self.prior_info) - self.Am = None + delta_scaled_prior = self.state_scaling[:, None] * \ + np.dot(at.aug_state(self.prior_state, self.list_states), self.proj) + + u_d, s_d, v_d = np.linalg.svd(delta_scaled_prior, full_matrices=False) + + # remove the last singular value/vector. This is because numpy returns all ne values, while the last is actually + # zero. This part is a good place to include eventual additional truncation. + energy = 0 + trunc_index = len(s_d) - 1 # inititallize + for c, elem in enumerate(s_d): + energy += elem + if energy / sum(s_d) >= self.trunc_energy: + trunc_index = c # take the index where all energy is preserved + break + u_d, s_d, v_d = u_d[:, :trunc_index + + 1], s_d[:trunc_index + 1], v_d[:trunc_index + 1, :] + self.Am = np.dot(u_d, np.eye(trunc_index+1) * + ((s_d**(-1))[:, None])) # notation from paper def save_temp_state_assim(self, ind_save): """ diff --git a/pipt/misc_tools/extract_tools.py b/pipt/misc_tools/extract_tools.py index 8f25616..c59d2d5 100644 --- a/pipt/misc_tools/extract_tools.py +++ b/pipt/misc_tools/extract_tools.py @@ -271,8 +271,12 @@ def organize_sparse_representation(info: Union[dict,list]) -> dict: sparse['dim'] = [dim[2], dim[1], dim[0]] # Read mask_files - sparse['mask'] = [] - for idx, filename in enumerate(info['mask'], start=1): + sparse['mask'] = [] + m_info = info['mask'] + # allow for one mask with filename given as string + if isinstance(m_info, str): + m_info = [m_info] + for idx, filename in enumerate(m_info, start=1): if not os.path.exists(filename): mask = np.ones(sparse['dim'], dtype=bool) np.savez(f'mask_{idx}.npz', mask=mask) diff --git a/pipt/update_schemes/gies/gies_base.py b/pipt/update_schemes/gies/gies_base.py index f967f63..fc96ee5 100644 --- a/pipt/update_schemes/gies/gies_base.py +++ b/pipt/update_schemes/gies/gies_base.py @@ -70,7 +70,7 @@ def __init__(self, keys_da, keys_fwd, sim): self.data_random_state = cp.deepcopy(np.random.get_state()) self._ext_obs() # Get state scaling and svd of scaled prior - self._ext_state() + self._ext_scaling() self.current_state = cp.deepcopy(self.state) def calc_analysis(self): diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 851871c..d2b6857 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -78,30 +78,76 @@ def get_seabed_depths(self, file_path): return water_depths - def measurement_locations(self, grid, water_depth, pad=1500, dxy=1500): + def measurement_locations(self, grid, water_depth, pad=1500, dxy=3000, well_coord = None, dxy_fine = 1500, r0 = 5000): # Determine the size of the measurement area as defined by the field extent - cell_centre = self.find_cell_centre(grid) - xmin = np.min(cell_centre[0]) - xmax = np.max(cell_centre[0]) - ymin = np.min(cell_centre[1]) - ymax = np.max(cell_centre[1]) - - xmin -= pad - xmax += pad - ymin -= pad - ymax += pad - - xspan = xmax - xmin - yspan = ymax - ymin - - Nx = int(np.ceil(xspan / dxy)) - Ny = int(np.ceil(yspan / dxy)) - - xvec = np.linspace(xmin, xmax, Nx) - yvec = np.linspace(ymin, ymax, Ny) + cell_centre = self.find_cell_centre(grid) + x_min = np.min(cell_centre[0]) + x_max = np.max(cell_centre[0]) + y_min = np.min(cell_centre[1]) + y_max = np.max(cell_centre[1]) + + x_min -= pad + x_max += pad + y_min -= pad + y_max += pad + + x_span = x_max - x_min + y_span = y_max - y_min + + nx = int(np.ceil(x_span / dxy)) + ny = int(np.ceil(y_span / dxy)) + + x_vec = np.linspace(x_min, x_max, nx) + y_vec = np.linspace(y_min, y_max, ny) + x, y = np.meshgrid(x_vec, y_vec) + + # allow for finer measurement grid around injection well + if well_coord is not None: + # choose center point and radius for area with finer measurement grid + # y = 64, x = 34 # alpha well position Smeaheia + xc = cell_centre[well_coord[0]] + yc = cell_centre[well_coord[1]] + + # Fine grid covering bounding box of the circle (clamped to domain) + fx_min = max(x_min, xc - r0) + fx_max = min(x_max, xc + r0) + fy_min = max(y_min, yc - r0) + fy_max = min(y_max, yc + r0) + + pts_coarse = np.column_stack((x.ravel(), y.ravel())) + + if fx_max > fx_min and fy_max > fy_min: + nfx = int(np.ceil((fx_max - fx_min) / dxy_fine)) + 1 + nfy = int(np.ceil((fy_max - fy_min) / dxy_fine)) + 1 + x_fine = np.linspace(fx_min, fx_max, nfx) + y_fine = np.linspace(fy_min, fy_max, nfy) + xf, yf = np.meshgrid(x_fine, y_fine) + pts_fine = np.column_stack((xf.ravel(), yf.ravel())) + + # Keep only fine points inside the circle + d2 = (pts_fine[:, 0] - xc) ** 2 + (pts_fine[:, 1] - yc) ** 2 + mask_inside = d2 <= r0 ** 2 + pts_fine_inside = pts_fine[mask_inside] + + # remove the coarse points inside the circle + d2 = (pts_coarse[:, 0] - xc) ** 2 + (pts_coarse[:, 1] - yc) ** 2 + mask_inside = d2 <= r0 ** 2 + pts_coarse = pts_coarse[~mask_inside] + + # Combine and remove duplicates by rounding to a tolerance or using a structured array + # Use tolerance based on the smaller spacing + tol = min(dxy, dxy_fine) * 1e-3 + all_pts = np.vstack((pts_coarse, pts_fine_inside)) + + # Round coordinates to avoid floating point duplicates then use np.unique + # Determine digits to round so differences smaller than tol collapse + digits = max(0, int(-np.floor(np.log10(tol)))) + all_pts_rounded = np.round(all_pts, digits) + uniq_pts = np.unique(all_pts_rounded, axis=0) + x = uniq_pts[:, 0] + y = uniq_pts[:, 1] - x, y = np.meshgrid(xvec, yvec) pos = {'x': x.flatten(), 'y': y.flatten()} @@ -122,7 +168,7 @@ class flow_rock(flow): """ def __init__(self, input_dict=None, filename=None, options=None): - super().__init__(input_dict, filename, options) + super().__init__(input_dict) self._getpeminfo(input_dict) self.date_slack = None