From fa95545bf17a00ffa9b90be22b8634d62834f0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martha=20=C3=98kland=20Lien?= Date: Wed, 13 Nov 2024 11:30:16 +0100 Subject: [PATCH 1/6] GIES and AVO Smeaheia in progress --- 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 | 551 +++++++++- simulator/flow_rock_backup.py | 1120 ++++++++++++++++++++ simulator/flow_rock_mali.py | 1130 +++++++++++++++++++++ 12 files changed, 3382 insertions(+), 63 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 4309abe..6317392 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 88b413f..ee5fc47 100644 --- a/pipt/loop/assimilation.py +++ b/pipt/loop/assimilation.py @@ -586,8 +586,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 f43efb4..3a49236 100644 --- a/pipt/loop/ensemble.py +++ b/pipt/loop/ensemble.py @@ -725,7 +725,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( @@ -734,7 +734,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 e8a21e3..8f68951 100644 --- a/pipt/misc_tools/analysis_tools.py +++ b/pipt/misc_tools/analysis_tools.py @@ -547,8 +547,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 8528776..4168f5b 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ 'tomli-w', 'pyyaml', 'libecalc', - 'scikit-learn' + 'scikit-learn', + 'pylops' ], ) 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 5bbce85..de08e02 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -11,7 +11,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): """ @@ -54,7 +64,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] @@ -105,64 +115,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() @@ -654,4 +620,477 @@ def extract_data(self, member): 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() \ No newline at end of file + 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 \ No newline at end of file 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 ad276625d374796dd823ce744f3fb43042ed9e9d Mon Sep 17 00:00:00 2001 From: mlie Date: Tue, 21 Jan 2025 09:40:25 +0100 Subject: [PATCH 2/6] avo updted, grav included, method for joint grav and avo. calc_pem method in flow_rock that is read by flow_avo --- simulator/flow_rock_mali.py | 1076 +++++++++++++++++++++++------------ 1 file changed, 721 insertions(+), 355 deletions(-) diff --git a/simulator/flow_rock_mali.py b/simulator/flow_rock_mali.py index 6abb95d..6470aa1 100644 --- a/simulator/flow_rock_mali.py +++ b/simulator/flow_rock_mali.py @@ -19,13 +19,15 @@ 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 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_sim2seis(flow): + +class flow_rock(flow): """ - Couple the OPM-flow simulator with a sim2seis simulator such that both reservoir quantities and petro-elastic + 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. """ @@ -50,7 +52,6 @@ def __init__(self, input_dict=None, filename=None, options=None): self.saveinfo = [input_dict['savesiminfo']] self.scale = [] - self.sats = [] def _getpeminfo(self, input_dict): """ @@ -65,7 +66,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 measurement + 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] @@ -90,7 +91,9 @@ def _getpeminfo(self, input_dict): else: self.pem = None - def calc_pem(self, v, time, phases): + 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 @@ -112,14 +115,23 @@ def calc_pem(self, v, time, phases): 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 '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'] - 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] @@ -129,22 +141,53 @@ def calc_pem(self, v, time, phases): 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] + # 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 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) - + #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): - super().setup_fwd_run() + 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. @@ -159,43 +202,54 @@ 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 - # 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']) + \ + 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) + time = dt.datetime(self.startDate['year'], self.startDate['month'], self.startDate['day']) + \ dt.timedelta(days=assim_time) - self.calc_pem(v, time, phases) + self.calc_pem(time) - 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 + # 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)) - # Todo: handle sim2seis or pem error + 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 @@ -207,16 +261,17 @@ def extract_data(self, member): 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 key in ['bulkimp']: 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() + self.pred_data[prim_ind][key] = self.pem_result[v].data.flatten() -class flow_rock(flow): + + + +class flow_sim2seis(flow): """ - Couple the OPM-flow simulator with a rock-physics simulator such that both reservoir quantities and petro-elastic + 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. """ @@ -242,7 +297,6 @@ def __init__(self, input_dict=None, filename=None, options=None): self.scale = [] - def _getpeminfo(self, input_dict): """ Get, and return, flow and PEM modules @@ -256,7 +310,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] @@ -281,8 +335,8 @@ def _getpeminfo(self, input_dict): else: self.pem = None - def setup_fwd_run(self, redund_sim): - super().setup_fwd_run(redund_sim=redund_sim) + 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. @@ -297,128 +351,42 @@ def call_sim(self, folder=None, wait_for_proc=False): success = super().call_sim(folder, wait_for_proc) if success: + # 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) + 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 - 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']) + \ + 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) - - 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) + self.calc_pem(time) #mali: update class inherent in flow_rock. Include calc_pem as method in flow_rock - # mask the bulkimp to get proper dimensions - tmp_value = np.zeros(self.ecl_case.init.shape) + 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) - 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() + 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 - # 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) + # Todo: handle sim2seis or pem error return success @@ -430,10 +398,12 @@ def extract_data(self, member): 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 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] = self.pem_result[v].data.flatten() + self.pred_data[prim_ind][key] = np.sum( + np.abs(result[:, :, :, v]), axis=0).flatten() class flow_barycenter(flow): """ @@ -678,7 +648,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_sim2seis): +class flow_avo(flow_rock): def __init__(self, input_dict=None, filename=None, options=None, **kwargs): super().__init__(input_dict, filename, options) @@ -688,7 +658,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): """ @@ -756,7 +726,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 @@ -773,7 +742,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) @@ -781,153 +750,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') + self.get_avo_result(folder, save_folder) - self.calc_pem() + return success - grid = self.ecl_case.grid() + 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 - 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) + # 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) - 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) + # vp, vs, density in reservoir + self.calc_velocities(folder, save_folder, grid, v) - # 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) + # avo data + self._calc_avo_props() - 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) + avo = self.avo_data.flatten(order="F") - 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']) + # 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 - 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: + 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.zeros(grid['DIMENS']) + self.vp[true_indices] = (self.pem.getBulkVel()) + self.vs = np.zeros(grid['DIMENS']) + self.vs[true_indices] = (self.pem.getShearVel()) + self.rho = np.zeros(grid['DIMENS']) + 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" - 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) + # 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: @@ -941,22 +871,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: - 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 @@ -1015,17 +929,25 @@ 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] - cum_time = np.concatenate((top_res[:, :, np.newaxis], cum_time_res, top_res[:, :, np.newaxis]), axis=2) + + # 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)) * 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 @@ -1050,39 +972,18 @@ def _calc_avo_props(self, dt=0.0005): 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'])): @@ -1127,4 +1028,469 @@ def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): return new_array else: - return array \ No newline at end of file + 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] \ No newline at end of file From aa5437a0ddb2478af61af9507427c84631956a51 Mon Sep 17 00:00:00 2001 From: mlie Date: Tue, 21 Jan 2025 12:28:41 +0100 Subject: [PATCH 3/6] softsand.py represents soft sand rock physics model, added 4D gravity and 4D avo --- simulator/flow_rock.py | 1290 +++++++++++++++-------- simulator/flow_rock_backup.py | 1120 -------------------- simulator/flow_rock_mali.py | 1496 --------------------------- simulator/rockphysics/softsandrp.py | 616 +++++++++++ simulator/rockphysics/standardrp.py | 7 +- 5 files changed, 1466 insertions(+), 3063 deletions(-) delete mode 100644 simulator/flow_rock_backup.py delete mode 100644 simulator/flow_rock_mali.py create mode 100644 simulator/rockphysics/softsandrp.py diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index de08e02..272d052 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -17,12 +17,260 @@ # 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 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 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) + 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 @@ -105,7 +353,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) @@ -122,7 +370,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) @@ -159,10 +407,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): @@ -186,6 +436,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): """ @@ -216,232 +468,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] - - 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] + 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]) @@ -622,7 +650,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_sim2seis): +class flow_avo(flow_rock): def __init__(self, input_dict=None, filename=None, options=None, **kwargs): super().__init__(input_dict, filename, options) @@ -632,7 +660,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): """ @@ -700,7 +728,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 @@ -717,7 +744,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) @@ -725,91 +752,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 = self.avo_data.flatten(order="F") + 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 - 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" + # 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 - #with open(file_name, "wb") as f: - # dump(**save_dic, f) - np.savez(file_name, **save_dic) + 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.zeros(grid['DIMENS']) + self.vp[true_indices] = (self.pem.getBulkVel()) + self.vs = np.zeros(grid['DIMENS']) + self.vs[true_indices] = (self.pem.getShearVel()) + self.rho = np.zeros(grid['DIMENS']) + 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) - 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: @@ -823,117 +873,18 @@ 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']): + def _get_avo_info(self, avo_config=None): """ - Hard coding, maybe a better way possible - addfiles: additional files that need to be included into ECLIPSE/OPM DATA file + AVO configuration """ - 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] + # 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): @@ -980,18 +931,25 @@ 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 @@ -1016,39 +974,18 @@ def _calc_avo_props(self, dt=0.0005): 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'])): @@ -1093,4 +1030,469 @@ def _reformat3D_then_flatten(cls, array, flatten=True, order="F"): return new_array else: - return array \ No newline at end of file + 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] \ No newline at end of file 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 6470aa1..0000000 --- a/simulator/flow_rock_mali.py +++ /dev/null @@ -1,1496 +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 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 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 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) - 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 - 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 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) - - 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 - 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) #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) - 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_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_rock): - 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(redund_sim=None) - - 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_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 - - if success: - self.get_avo_result(folder, save_folder) - - 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() - - # 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 - - 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.zeros(grid['DIMENS']) - self.vp[true_indices] = (self.pem.getBulkVel()) - self.vs = np.zeros(grid['DIMENS']) - self.vs[true_indices] = (self.pem.getShearVel()) - self.rho = np.zeros(grid['DIMENS']) - 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) - - - 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 - 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 _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 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) - - # 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] - - - # 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((self.NX, self.NY, len(t_interp))) - - # 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'])): - 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 - -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] \ No newline at end of file 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 d47b2bf..40a555f 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 5bef1f1433aa39271d267f508dd766c743309066 Mon Sep 17 00:00:00 2001 From: mlie Date: Tue, 21 Jan 2025 13:19:44 +0100 Subject: [PATCH 4/6] give overburden velocity values to inactive cells --- simulator/flow_rock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index 272d052..ed898e6 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -829,11 +829,11 @@ def calc_velocities(self, folder, save_folder, grid, v): 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 = np.full(grid['DIMENS'], self.avo_config['vp_shale']) self.vp[true_indices] = (self.pem.getBulkVel()) - self.vs = np.zeros(grid['DIMENS']) + self.vs = np.full(grid['DIMENS'], self.avo_config['vs_shale']) self.vs[true_indices] = (self.pem.getShearVel()) - self.rho = np.zeros(grid['DIMENS']) + 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') From 71c98dc21d46e225c83e8adba58517f76f0c7f81 Mon Sep 17 00:00:00 2001 From: mlie Date: Tue, 21 Jan 2025 13:22:58 +0100 Subject: [PATCH 5/6] minor edits --- simulator/flow_rock.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index ed898e6..a1f9acf 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -18,7 +18,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 @@ -213,12 +213,9 @@ def call_sim(self, folder=None, wait_for_proc=False): 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) - 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 From 54c0f3512ac9ccb31128c3e32365ead511a1dc7f Mon Sep 17 00:00:00 2001 From: mlie Date: Tue, 28 Jan 2025 10:28:10 +0100 Subject: [PATCH 6/6] Compute reflection coefficients for active cells --- simulator/flow_rock.py | 149 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 16 deletions(-) diff --git a/simulator/flow_rock.py b/simulator/flow_rock.py index a1f9acf..d072154 100644 --- a/simulator/flow_rock.py +++ b/simulator/flow_rock.py @@ -18,7 +18,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 @@ -167,16 +167,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 \ @@ -772,7 +777,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") @@ -785,7 +791,8 @@ 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 @@ -1010,6 +1017,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"): """ @@ -1318,8 +1433,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 @@ -1330,7 +1446,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]) @@ -1461,6 +1577,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