void VertEqUpscaler::gather ( int col, double* buf, const double* data, int stride, int offset) const { // index into the fine grid for all columns const rlw_int col_cells (ts.number_of_cells, ts.col_cellpos, ts.col_cells); // get the indices for this particular column const int* fine_ndx = col_cells[col]; // loop through each block in the column and fetch the property for (int row = 0; row < col_cells.size (col); ++row) { // index in the fine grid for this block const int block_ndx = fine_ndx[row]; // calculate position in the data array const int pos = block_ndx * stride + offset; // move the data buf[row] = data[pos]; } }
int VertEqUpscaler::num_rows ( int col) const { // use this helper object to query about the size of the column // (the compiler should be able to optimize most of it away) const rlw_int pos (ts.number_of_cells, ts.col_cellpos, ts.col_cells); return pos.size (col); }
virtual void upscale_pressure (const double* coarseSaturation, const double* finePressure, double* coarsePressure) { // pressure locations we'll have to relate to static const int FIRST_BLOCK = 0; // relative index in the column static const double HALFWAY = 0.5; // center of the block // incompressible means that the density is the same everywhere // we can thus cache the phase properties outside of the loop const double gas_dens = density ()[GAS]; const double wat_dens = density ()[WAT]; // helper object to get the index (into the pressure array) and // the height of elements in a column const rlw_double ts_dz (ts.number_of_cells, ts.col_cellpos, ts.dz); const rlw_int col_cells (ts.number_of_cells, ts.col_cellpos, ts.col_cells); // upscale each column separately. assume that something like the // EQUIL keyword has been used in the Eclipse file and that the // pressures are already in equilibrium. thus, we only need to // extract the pressure at the reference point (top surface) for (int col = 0; col < col_cells.cols (); ++col) { // location of the brine-co2 phase contact const double gas_sat = coarseSaturation[col * NUM_PHASES + GAS]; const Elevation& intf_lvl = intf_elev (col, gas_sat); // what fraction of the first block from the pressure point (halfway) // up to the top surface is of each of the phases? if the interface // is below the first block, or if it is further down than halfway, // then everything, otherwise the fraction (less than 0.5) double gas_frac = (intf_lvl.block () == FIRST_BLOCK) ? min (HALFWAY, intf_lvl.fraction ()) : HALFWAY; // id of the upper-most block of this column. if there is no // blocks, then the TopSurf object wouldn't generate a column. const int block_id = col_cells[col][FIRST_BLOCK]; // height of the uppermost block (twice the distance from the top // to the center const double hgt = ts_dz[col][FIRST_BLOCK]; // get the pressure in the middle of this block const double mid_pres = finePressure[block_id]; // pressure at the reference point; adjust hydrostatically for // those phases that are on the way up from the center of the first // block in the column. const double ref_pres = mid_pres - gravity * hgt * (gas_frac * gas_dens + (HALFWAY - gas_frac) * wat_dens); // Eclipse uses non-aquous pressure (see Variable Sets in Formulation // of the Equations in the Technical Description) as the main unknown // in the pressure equation; there is assumed continuity at the // contact, so the pressure at the top should always be a CO2 pressure coarsePressure[col] = ref_pres; } }
double VertEqUpscaler::sum ( int col, const double *val) const { // index into the fine grid for all columns const rlw_int col_cells (ts.number_of_cells, ts.col_cellpos, ts.col_cells); // get the indices for this particular column const int* fine_ndx = col_cells[col]; // loop through each block in the column, accumulating as we go double the_sum = 0.; for (int row = 0; row < col_cells.size (col); ++row) { the_sum += val[fine_ndx[row]]; } return the_sum; }
virtual void downscale_pressure (const double* coarseSaturation, const double* coarsePressure, double* finePressure) { // pressure locations we'll have to relate to static const double HALFWAY = 0.5; // center of the block // helper object to get the index (into the pressure array) and // the height of elements in a column const rlw_double ts_h (ts.number_of_cells, ts.col_cellpos, ts.h); const rlw_double ts_dz (ts.number_of_cells, ts.col_cellpos, ts.dz); const rlw_int col_cells (ts.number_of_cells, ts.col_cellpos, ts.col_cells); // incompressible means that the density is the same everywhere // we can thus cache the phase properties outside of the loop const double gas_dens = density ()[GAS]; const double wat_dens = density ()[WAT]; for (int col = 0; col < col_cells.cols (); ++col) { // location of the brine-co2 phase contact const double gas_sat = coarseSaturation[col * NUM_PHASES + GAS]; const Elevation& intf_lvl = intf_elev (col, gas_sat); // get the pressure difference between the phases at top of this column const double sat[NUM_PHASES] = { gas_sat, 1-gas_sat }; double pres_diff; capPress (1, sat, &col, &pres_diff, 0); // get the reference phase pressure at the top; notice that the CO2 // pressure is the largest so we subtract the difference const double gas_ref = coarsePressure[col]; const double wat_ref = gas_ref - pres_diff; // are we going to include the block with the interface const int incl_intf = intf_lvl.fraction () >= HALFWAY ? 1 : 0; const int num_gas_rows = intf_lvl.block () + incl_intf; // write all CO2 pressure blocks for (int row = 0; row < num_gas_rows; ++row) { // height of block center const double hgt = ts_h[col][row] + HALFWAY * ts_dz[col][row]; // hydrostatically get the pressure for this block const double gas_pres = gas_ref + gravity * hgt * gas_dens; const int block = col_cells[col][row]; // (scatter) write to output array finePressure[block] = gas_pres; } // then write the brine blocks, starting where we left off for (int row = num_gas_rows; row < col_cells.size (col); ++row) { // height of block center const double hgt = ts_h[col][row] + HALFWAY * ts_dz[col][row]; // hydrostatically get the pressure for this block const double wat_pres = wat_ref + gravity * hgt * wat_dens; const int block = col_cells[col][row]; // (scatter) write to output array finePressure[block] = wat_pres; } } }
virtual void downscale_saturation (const double* coarseSaturation, double* fineSaturation) { // scratch vectors that will hold the minimum and maximum, resp. // CO2 saturation. we could get these from res_xxx_vol, but then // we would have to dig up the porosity for each cell and divide // which is not necessarily faster. if we save this data in the // object itself, it may add to memory pressure; I assume instead // that it is not expensive for the underlaying properties object // to deliver these values on-demand. vector <double> sgr (ts.max_vert_res * NUM_PHASES, 0.); // residual CO2 vector <double> l_swr (ts.max_vert_res * NUM_PHASES, 0.); // 1 - residual brine // indexing object that helps us find the cell in a particular column const rlw_int col_cells (ts.number_of_cells, ts.col_cellpos, ts.col_cells); // downscale each column individually for (int col = 0; col < ts.number_of_cells; ++col) { // current height of mobile CO2 const double gas_hgt = coarseSaturation[col * NUM_PHASES + GAS]; // height of the interface of residual and mobile CO2, resp. const Elevation res_gas = res_elev (col, gas_hgt); // zeta_R const Elevation mob_gas = intf_elev (col, gas_hgt); // zeta_M // query the fine properties for the residual saturations; notice // that only every other item holds the value for CO2 const int* ids = col_cells[col]; fp.satRange (col_cells.size (col), ids, &sgr[0], &l_swr[0]); // fill the number of whole blocks which contain mobile CO2 and // only residual water (maximum CO2) for (int row = 0; row < mob_gas.block (); ++row) { const double gas_sat = l_swr[row * NUM_PHASES + GAS]; const int block = ids[row]; fineSaturation[block * NUM_PHASES + GAS] = gas_sat; fineSaturation[block * NUM_PHASES + WAT] = 1 - gas_sat; } // then fill the number of *whole* blocks which contain only // residual CO2. we start out in the block that was not filled // with mobile CO2, i.e. these only fill the *extra* blocks // where the plume once was but is not anymore for (int row = mob_gas.block(); row < res_gas.block(); ++row) { const double gas_sat = sgr[row * NUM_PHASES + GAS]; const int block = ids[row]; fineSaturation[block * NUM_PHASES + GAS] = gas_sat; fineSaturation[block * NUM_PHASES + WAT] = 1 - gas_sat; } // fill the remaining of the blocks in the column with pure brine for (int row = res_gas.block(); row < col_cells.size (col); ++row) { const int block = ids[row]; fineSaturation[block * NUM_PHASES + GAS] = 0.; fineSaturation[block * NUM_PHASES + WAT] = 1.; } // adjust the block with the mobile/residual interface with its // fraction of mobile CO2. since we only have a resolution of one // block this sharp interface will only be seen on the visualization // as a slightly differently colored block. only do this if there // actually is a partially filled block. const int intf_block = ids[mob_gas.block ()]; if (intf_block != col_cells.size(col)) { // there will already be residual gas in this block thanks to the // loop above; we must only fill a fraction of it with mobile gas, // which is the difference between the maximum and minimum filling const double intf_gas_sat_incr = mob_gas.fraction () * (l_swr[intf_block * NUM_PHASES + GAS] - sgr[intf_block * NUM_PHASES + GAS]); fineSaturation[intf_block * NUM_PHASES + GAS] += intf_gas_sat_incr; // we could have written at the brine saturations afterwards to // avoid this extra adjustment, but the data locality will be bad fineSaturation[intf_block * NUM_PHASES + WAT] -= intf_gas_sat_incr; } // do the same drill, but with the fraction of where the residual // zone ends (the outermost historical edge of the plume) const int res_block = ids[res_gas.block()]; if (res_block != col_cells.size(col)) { const double res_gas_sat_incr = res_gas.fraction() * sgr[res_block * NUM_PHASES + GAS]; fineSaturation[res_block * NUM_PHASES + GAS] += res_gas_sat_incr; fineSaturation[res_block * NUM_PHASES + WAT] -= res_gas_sat_incr; } } }
VertEqPropsImpl (const IncompPropertiesInterface& fineProps, const TopSurf& topSurf, const double* grav_vec) : fp (fineProps) , ts (topSurf) , up (ts) // assign which phase is which (e.g. CO2 is first, brine is second) // a basic assumption of the vertical equilibrium is that the CO2 is // the lightest phase and thus rise to the top of the reservoir , GAS (fp.density()[0] < fp.density()[1] ? 0 : 1) , WAT (1 - GAS) , phase_sign (GAS < WAT ? +1. : -1.) // allocate memory for intermediate integrals , res_gas_vol (ts.number_of_cells, ts.col_cellpos) , mob_mix_vol (ts.number_of_cells, ts.col_cellpos) , res_wat_vol (ts.number_of_cells, ts.col_cellpos) , res_gas_dpt (ts.number_of_cells, ts.col_cellpos) , mob_mix_dpt (ts.number_of_cells, ts.col_cellpos) , res_wat_dpt (ts.number_of_cells, ts.col_cellpos) // assume that there is no initial plume; first notification will // trigger an update of all columns where there actually is CO2 , max_gas_sat (ts.number_of_cells, 0.) , prm_gas (ts.number_of_cells, ts.col_cellpos) , prm_gas_int (ts.number_of_cells, ts.col_cellpos) , prm_res (ts.number_of_cells, ts.col_cellpos) , prm_res_int (ts.number_of_cells, ts.col_cellpos) , prm_wat (ts.number_of_cells, ts.col_cellpos) , prm_wat_int (ts.number_of_cells, ts.col_cellpos) , gravity (grav_vec[THREE_DIMS - 1]) { // check that we only have two phases if (fp.numPhases () != NUM_PHASES) { throw OPM_EXC ("Expected %d phases, but got %d", NUM_PHASES, fp.numPhases ()); } // allocate memory to store results for faster lookup later upscaled_poro.resize (ts.number_of_cells); upscaled_absperm.resize (ts.number_of_cells * PERM_MATRIX_2D); // buffers that holds intermediate values for each column; // pre-allocate to avoid doing that inside the loop vector <double> poro (ts.max_vert_res, 0.); // porosity vector <double> kxx (ts.max_vert_res, 0.); // abs.perm. vector <double> kxy (ts.max_vert_res, 0.); vector <double> kyy (ts.max_vert_res, 0.); vector <double> sgr (ts.max_vert_res * NUM_PHASES, 0.); // residual CO2 vector <double> l_swr (ts.max_vert_res * NUM_PHASES, 0.); // 1 - residual brine vector <double> lkl (ts.max_vert_res); // magnitude of abs.perm.; k_|| // saturations and rel.perms. of each phase, assuming maximum filling of... vector <double> wat_sat (ts.max_vert_res * NUM_PHASES, 0.); // brine; res. CO2 vector <double> gas_sat (ts.max_vert_res * NUM_PHASES, 0.); // CO2; res. brine vector <double> wat_mob (ts.max_vert_res * NUM_PHASES, 0.); // k_r(S_c=S_{c,r}) vector <double> gas_mob (ts.max_vert_res * NUM_PHASES, 0.); // k_r(S_c=1-S_{b,r}) // pointer to all porosities in the fine grid const double* fine_poro = fp.porosity (); const double* fine_perm = fp.permeability (); // upscale each column separately for (int col = 0; col < ts.number_of_cells; ++col) { // retrieve the fine porosities for this column only up.gather (col, &poro[0], fine_poro, 1, 0); // compute the depth-averaged value and store upscaled_poro[col] = up.dpt_avg (col, &poro[0]); // retrieve the fine abs. perm. for this column only up.gather (col, &kxx[0], fine_perm, PERM_MATRIX_3D, KXX_OFS_3D); up.gather (col, &kxy[0], fine_perm, PERM_MATRIX_3D, KXY_OFS_3D); up.gather (col, &kyy[0], fine_perm, PERM_MATRIX_3D, KYY_OFS_3D); // compute upscaled values for each dimension separately const double up_kxx = up.dpt_avg (col, &kxx[0]); const double up_kxy = up.dpt_avg (col, &kxy[0]); const double up_kyy = up.dpt_avg (col, &kyy[0]); // store back into the interleaved format required by the 2D // simulator code (fetching a tensor at the time, probably) // notice that we take advantage of the tensor being symmetric // at the third line below upscaled_absperm[PERM_MATRIX_2D * col + KXX_OFS_2D] = up_kxx; upscaled_absperm[PERM_MATRIX_2D * col + KXY_OFS_2D] = up_kxy; upscaled_absperm[PERM_MATRIX_2D * col + KYX_OFS_2D] = up_kxy; upscaled_absperm[PERM_MATRIX_2D * col + KYY_OFS_2D] = up_kyy; // contract each fine perm. to a scalar, used for weight later for (int row = 0; row < up.num_rows (col); ++row) { lkl[row] = magnitude (kxx[row], kxy[row], kyy[row]); } // we only need the relative weight, so get the depth-averaged // total weight, which we'll use to scale the weights below const double tot_lkl = up.dpt_avg (col, &lkl[0]); // 1/K^{-1} // query the fine properties for the residual saturations; // notice that we implicitly get the brine saturation as the maximum // allowable co2 saturation; now we've got the values we need, but // only every other item (due to that both phases are stored) const rlw_int col_cells (ts.number_of_cells, ts.col_cellpos, ts.col_cells); fp.satRange (col_cells.size (col), col_cells[col], &sgr[0], &l_swr[0]); // cache pointers to this particular column to avoid recomputing // the starting point for each and every item double* res_gas_col = res_gas_vol[col]; double* mob_mix_col = mob_mix_vol[col]; double* res_wat_col = res_wat_vol[col]; for (int row = 0; row < col_cells.size (col); ++row) { // multiply with num_phases because the saturations for *both* // phases are store consequtively (as a record); we only need // the residuals framed as co2 saturations const double sgr_ = sgr[row * NUM_PHASES + GAS]; const double l_swr_ = l_swr[row * NUM_PHASES + GAS]; // portions of the block that are filled with: residual co2, // mobile fluid and residual brine, respectively res_gas_col[row] = poro[row] * sgr_; // \phi*S_{n,r} mob_mix_col[row] = poro[row] * (l_swr_ - sgr_); // \phi*(1-S_{w,r}-S_{n_r}) res_wat_col[row] = poro[row] * l_swr_; // \phi*(1-S_{w,r} } // weight the relative depth factor (how close are we towards a // completely filled column) with the volume portions. this call // to up.wgt_dpt is the same as 1/H int_{h}^{\Zeta_T} ... dz up.wgt_dpt (col, &res_gas_col[0], res_gas_dpt); up.wgt_dpt (col, &mob_mix_col[0], mob_mix_dpt); up.wgt_dpt (col, &res_wat_col[0], res_wat_dpt); // now, when we queried the saturation ranges, we got back the min. // and max. sat., and when there is min. of one, then there should // be max. of the other; however, these data are in different arrays! // cross-pick such that we get (min CO2, max brine), (max CO2, min brine) // instead of (min CO2, min brine), (max CO2, max brine). this code // has no other effect than to satisfy the ordering of items required // for the relperm() call for (int row = 0; row < col_cells.size (col); ++row) { wat_sat[row * NUM_PHASES + GAS] = sgr[row * NUM_PHASES + GAS]; wat_sat[row * NUM_PHASES + WAT] = l_swr[row * NUM_PHASES + WAT]; gas_sat[row * NUM_PHASES + GAS] = l_swr[row * NUM_PHASES + GAS]; gas_sat[row * NUM_PHASES + WAT] = sgr[row * NUM_PHASES + WAT]; } // get rel.perm. for those cases where one phase is (maximally) mobile // and the other one is immobile (at residual saturation); we get back // rel.perm. for both phases, although only one of them is of interest // for us (the other one should be zero). we have no interest in the // derivative of the fine-scale rel.perm. fp.relperm (col_cells.size (col), &wat_sat[0], col_cells[col], &wat_mob[0], 0); fp.relperm (col_cells.size (col), &gas_sat[0], col_cells[col], &gas_mob[0], 0); // cache the pointers here to avoid indexing in the loop double* prm_gas_col = prm_gas[col]; double* prm_res_col = prm_res[col]; double* prm_wat_col = prm_wat[col]; for (int row = 0; row < up.num_rows (col); ++row) { // rel.perm. for CO2 when having maximal sat. (only residual brine); this // is the rel.perm. for the CO2 that is in the plume const double kr_plume = gas_mob[row * NUM_PHASES + GAS]; // rel.perm. of brine, when residual CO2 const double kr_brine = wat_mob[row + NUM_PHASES + WAT]; // upscaled rel. perm. change for this block; we'll use this to weight // the depth fractions when we integrate to get the upscaled rel. perm. const double k_factor = lkl[row] / tot_lkl; prm_gas_col[row] = k_factor * kr_plume; prm_wat_col[row] = k_factor * kr_brine; prm_res_col[row] = k_factor * (1 - kr_brine); } // integrate the derivate to get the upscaled rel. perm. up.wgt_dpt (col, prm_gas_col, prm_gas_int); up.wgt_dpt (col, prm_wat_col, prm_wat_int); up.wgt_dpt (col, prm_res_col, prm_res_int); } }