book: Book examples

Module containing all the scripts used for the examples, exercises, and figures in the book “An introduction to reservoir simulation using MATLAB” by Knut-Andreas Lie.

Examples

load mrst-logo.mat;
p = .05 + .3*(K(G.cells.indexMap)-30)/600;
rock = makeRock(G, p.^3.*(1e-5)^2./(0.81*72*(1-p).^2), p);
figure('Position',[440 290 860 500]);
plotCellData(G,rock.poro,'EdgeAlpha',.1); view(74,74);
clear p K;
_images/mrstLogo_01.png
mrstModule add libgeometry incomp;
rad = @(x,y) sum(bsxfun(@minus,x(:,1:2),y).^2,2);
G = mcomputeGeometry(G);
pv = sum(poreVolume(G,rock));
W = addWell([],G,rock, find(rad(G.cells.centroids,[3.5,180.5])<0.6), ...
            'Type', 'rate','Val', -pv/(20*year));
W = addWell(W,G,rock, find(rad(G.cells.centroids,[3.5,134.5])<0.6), ...
           'Type', 'rate','Val', -pv/(20*year));
W = addWell(W,G,rock, find(rad(G.cells.centroids,[3.5,90.5])<0.6), ...
           'Type', 'rate','Val', -pv/(20*year));
W = addWell(W,G,rock, find(rad(G.cells.centroids,[3.5,22.5])<0.6), ...
           'Type', 'rate','Val', -pv/(20*year));
W = addWell(W,G,rock, find(rad(G.cells.centroids,[42.5,45.5])<0.6), ...
            'Type', 'rate','Val', pv/(15*year));
W = addWell(W,G,rock, find(rad(G.cells.centroids,[42.5,90.5])<0.6), ...
            'Type', 'rate','Val', pv/(15*year));
W = addWell(W,G,rock, find(rad(G.cells.centroids,[42.5,130.5])<0.6), ...
            'Type', 'rate','Val', pv/(15*year));
T = computeTrans(G, rock);

fluid   = initSingleFluid('mu' , 1*centi*poise, ...
                          'rho', 1000*kilogram/meter^3);
state = initState(G,W,100*barsa);
state = incompTPFA(state, G, T, fluid, 'wells', W);
mrstModule add diagnostics
D = computeTOFandTracer(state,G,rock,'wells',W);
figure('Position',[440 290 860 500]);
plotTracerBlend(G, D.ipart, max(D.itracer, [], 2)),
plotWell(G,W,'Color','k','FontSize',10); view(96,76)
plotGrid(G,'EdgeAlpha',.1,'FaceColor','none'); axis off
_images/mrstLogo_02.png
mrstModule add coarsegrid streamlines
cla
p = partitionUI(G,[1 1 G.cartDims(3)]);
plotCellData(G,D.ppart,p>1,'EdgeAlpha',.1,'EdgeColor','k','FaceAlpha',.6);
ip = find( (abs(G.cells.centroids(:,1)-15.5)<.6) & (p==1) & ...
   ((G.cells.centroids(:,2)<35) | (G.cells.centroids(:,2)>65)));
h=streamline(pollock(G,state,ip(1:1:end),'reverse',true));
set(h,'Color',.4*[1 1 1],'LineWidth',.5);
h=streamline(pollock(G,state,ip(1:1:end)));
set(h,'Color',.4*[1 1 1],'LineWidth',.5);
plotWell(G,W,'Color','k','FontSize',0,'height',12,'radius',2);
set(gca,'dataasp',[1.25 2 .4]); zoom(1.2)
_images/mrstLogo_03.png

Example: specification of boundary conditions

Generated from boundaryConditions.m

In this example, we will show how to set boundary conditions to create pressure drop across a reservoir. For a rectangular model with homogeneous properties, this will create a linear pressure drop. The user can choose between different setups:

mrstModule add incomp

addpath('src');
setup = 3;
Warning: Name is nonexistent or not a directory:
/home/francesca/mrstReleaseCleanRepo/mrst-bitbucket/src

Define geometry

[nx,ny,nz] = deal(20, 20, 5);
[Lx,Ly,Lz] = deal(1000, 1000, 50);
switch setup
   case 1,
      gravity reset off
      G = cartGrid([nx ny nz], [Lx Ly Lz]);
   case 2,
      gravity reset on
      G = cartGrid([nx ny nz], [Lx Ly Lz]);
   case 3,
      gravity reset on
      G = processGRDECL(makeModel3([nx ny nz], [Lx Ly Lz/5]));
      G.nodes.coords(:,3) = 5*(G.nodes.coords(:,3) ...
                               - min(G.nodes.coords(:,3)));
end
G.nodes.coords(:,3) = G.nodes.coords(:,3) + 500;
G = computeGeometry(G);
Adding 800 artificial cells at top/bottom

Processing regular i-faces
 Found 2304 new regular faces
Elapsed time is 0.002980 seconds.

Processing i-faces on faults
 Found 23 faulted stacks
...

Set rock and fluid data

rock.poro = repmat(.2, [G.cells.num, 1]);
rock.perm = repmat([1000, 300, 10].* milli*darcy(), [G.cells.num, 1]);
fluid = initSingleFluid('mu', 1*centi*poise, 'rho', 1014*kilogram/meter^3);

Compute transmissibility and initialize reservoir state

hT = simpleComputeTrans(G, rock);
[mu,rho] = fluid.properties();
state = initResSol(G, G.cells.centroids(:,3)*rho*norm(gravity), 1.0);
Warning:
      4 negative transmissibilities.
      Replaced by absolute values...

Impose boundary conditions

Our flow solvers automatically assume no-flow conditions on all outer (and inner) boundaries; other type of boundary conditions need to be specified explicitly.

[src,bc] = deal([]);
switch setup
   case 1,
      bc = fluxside(bc, G, 'EAST', 5e3*meter^3/day);
      bc = pside   (bc, G, 'WEST', 50*barsa);

      clf, plotGrid(G,'FaceColor', 'none'); view(3);
      plotFaces(G, bc.face(strcmp(bc.type,'flux')), 'b');
      plotFaces(G, bc.face(strcmp(bc.type,'pressure')), 'r');

   case 2,
      % Alternative 1: use psideh
      % bc = psideh(bc, G, 'EAST', fluid);
      % bc = psideh(bc, G, 'WEST', fluid);
      % bc = psideh(bc, G, 'SOUTH', fluid);
      % bc = psideh(bc, G, 'NORTH', fluid);
      %
      % Alternative 2: compute using face centroids
      % f = boundaryFaces(G);
      % f = f(abs(G.faces.normals(f,3))<eps);
      % bc = addBC(bc,f,'pressure',G.faces.centroids(f,3)*rho*norm(gravity));
      %
      % Alternative 3: sample from initialized state object
      f = boundaryFaces(G);
      f = f(abs(G.faces.normals(f,3))<eps);  % vertical faces only
      cif = sum(G.faces.neighbors(f,:),2);   % cells adjacent to face
      bc = addBC(bc, f, 'pressure', state.pressure(cif));

      clf, plotGrid(G,'FaceColor', 'none'); view(-40,40)
      plotFaces(G, f, state.pressure(cif)/barsa);

      ci = round(.5*(nx*ny-nx));
      ci = [ci; ci+nx*ny];
      src = addSource(src, ci, repmat(-1e3*meter^3/day,numel(ci),1));
      plotGrid(G, ci, 'FaceColor', 'y'); axis tight
      h=colorbar; set(h,'XTick', 1, 'XTickLabel','[bar]');

   case 3,
      clf,
      show = false(nz,1); show([1 nz])=true;
      k = repmat((1:nz),nx*ny,1); k = k(G.cells.indexMap);
      plotGrid(G,'FaceColor','none');
      plotGrid(G, show(k), 'FaceColor', [1 1 .7]);
      view(40,20);

      % First attempt: use pside/fluxside
      bcf = fluxside([], G, 'EAST', 5e3*meter^3/day);
      bcp = pside   ([], G, 'WEST', 50*barsa);
      hf  = plotFaces(G, bcf.face, 'b');
      hp  = plotFaces(G, bcp.face, 'r');

      % Second attempt: count faces
      bcf = fluxside([], G, 'EAST', 5e3*meter^3/day, 4:15, 1:5);
      bcp = pside   ([], G, 'WEST', 50*barsa,        7:17, []);
      delete([hf hp]);
      hf  = plotFaces(G, bcf.face, 'b');
      hp  = plotFaces(G, bcp.face, 'r');

      % Third attempt: do it the hard way
      f = boundaryFaces(G);
      f = f(abs(G.faces.normals(f,3))<eps);  % vertical faces only

      x = G.faces.centroids(f,1);
      [xm,xM] = deal(min(x), max(x));
      ff = f(x>xM-1e-5);
      bc = addBC(bc, ff, 'flux', (5e3*meter^3/day) ...
                 * G.faces.areas(ff)/ sum(G.faces.areas(ff)));
      fp = f(x<xm+1e-5);
      bc = addBC(bc, fp, 'pressure', repmat(50*barsa, numel(fp), 1));

      delete([hf hp]);
      plotFaces(G, ff, 'b'); plotFaces(G, fp, 'r');

end
_images/boundaryConditions_01.png

Solve the linear system and plot result

tate = simpleIncompTPFA(state, G, hT, fluid, 'bc', bc, 'src', src);
clf
plotCellData(G, convertTo(state.pressure, barsa()), 'EdgeColor', 'k');
xlabel('x'), ylabel('y'), zlabel('Depth');
view(3); axis tight;
h=colorbar; set(h,'XTick', 1, 'XTickLabel','[bar]');
_images/boundaryConditions_02.png

Use of Peacemann well models

Generated from firstWellExample.m

In this example we will demonstrate how to set up a flow problems with two wells, one rate-controlled, vertical well and one horizontal well controlled by bottom-hole pressure. The reservoir is a regular box with homogeneous petrophysical properties.

mrstModule add incomp

Set up reservoir model

[nx,ny,nz] = deal(20,20,5);
G = computeGeometry( cartGrid([nx,ny,nz], [500 500 25]) );
rock  = makeRock(G, 100.*milli*darcy, .2);
fluid = initSingleFluid('mu', 1*centi*poise,'rho', 1014*kilogram/meter^3);
hT    = computeTrans(G, rock);

Add wells wells

W = verticalWell([], G, rock, 1, 1, 1:nz, 'Type', 'rate', 'Comp_i', 1,...
                'Val', 3e3/day, 'Radius', .12*meter, 'name', 'I');
disp('Well #1: '); display(W(1));

W = addWell(W, G, rock, nx : ny : nx*ny, 'Type', 'bhp', 'Comp_i', 1, ...
            'Val', 1.0e5, 'Radius', .12*meter, 'Dir', 'y', 'name', 'P');
disp('Well #2: '); display(W(2));

state = initState(G, W, 0);
Well #1:
        cells: [5×1 double]
         type: 'rate'
          val: 0.0347
            r: [5×1 double]
          dir: [5×1 char]
           rR: [5×1 double]
           WI: [5×1 double]
...

We plot the wells to check if the wells are placed as we wanted them. (The plot will later be moved to subplot(2,2,1), hence we first find the corresponding axes position before generating the handle graphics).

subplot(2,2,1), pos = get(gca,'Position'); clf
plotGrid(G, 'FaceColor', 'none');
view(3), camproj perspective, axis tight off,
plotWell(G, W(1), 'radius',  1, 'height', 5, 'color', 'r');
plotWell(G, W(2), 'radius', .5, 'height', 5, 'color', 'b');
_images/firstWellExample_01.png

Assemble and solve system

gravity reset on;
state = incompTPFA(state, G, hT, fluid, 'wells', W);

Report results

We move the plot of the grids and wells to the upper-left subplot. The producer inflow profile is shown in the upper-right and the cell pressures in the lower-left subplot. In the lower-right subplot, we show the flux intensity, which must be constructed by averaging over cell faces

subplot(2,2,1)
   set(gca, 'Position', pos);  % move the current plot

subplot(2,2,2)
   plot(convertTo(-state.wellSol(2).flux, meter^3/day),'o')
   title('Producer inflow profile [m^3/d]');

subplot(2,2,3)
   plotCellData(G, convertTo(state.pressure(1:G.cells.num), barsa),'EdgeAlpha',.1);
   title('Pressure [bar]')
   view(3), camproj perspective, axis tight off

subplot(2,2,4)
   [i j k] = ind2sub(G.cartDims, 1:G.cells.num);
   I = false(nx,1); I([1 end])=true;
   J = false(ny,1); J(end)=true;
   K = false(nz,1); K([1 end]) = true;
   cf = accumarray(getCellNoFaces(G), ...
      abs(faceFlux2cellFlux(G, state.flux)));
   plotCellData(G, convertTo(cf, meter^3/day), I(i) | J(j) | K(k),'EdgeAlpha',.1);
   title('Flux intensity [m^3/day]')
   view(-40,20), camproj perspective, axis tight, box on
   set(gca,'XTick',[],'YTick',[],'ZTick',[]);
_images/firstWellExample_02.png

Gravity Column

Generated from gravityColumn.m

In this example, we introduce a simple pressure solver and use it to solve the single-phase pressure equation

\nabla\cdot v = q, \qquad
   v=\textbf{--}\frac{K}{\mu} \bigl[\nabla p+\rho g\nabla z\bigr],

within the domain [0,1]x[0,1]x[0,30] using a Cartesian grid with homogeneous isotropic permeability of 100 mD. The fluid has density 1000 kg/m^3 and viscosity 1 cP and the pressure is 100 bar at the top of the structure. The purpose of the example is to show the basic steps for setting up, solving, and visualizing a flow problem. More details on the grid structure, the structure used to hold the solutions, and so on, are given in the basic flow-solver tutorial.

addpath('src');
mrstModule add incomp
Warning: Name is nonexistent or not a directory:
/home/francesca/mrstReleaseCleanRepo/mrst-bitbucket/src

Define the model

To set up a model, we need: a grid, rock properties (permeability), a fluid object with density and viscosity, and boundary conditions.

gravity reset on
G     = cartGrid([1, 1, 30], [1, 1, 30]);
G     = computeGeometry(G);
rock  = makeRock(G, 0.1*darcy(), 0.2);
fluid = initSingleFluid('mu' ,    1*centi*poise, ...
                             'rho', 1014*kilogram/meter^3);
bc  = pside([], G, 'TOP', 100.*barsa());
Processing Cells  1 : 30 of 30 ... done (0.00 [s], 1.46e+04 cells/second)
Total 3D Geometry Processing Time = 0.002 [s]

Assemble and solve the linear system

To solve the flow problem, we use the standard two-point flux-approximation method (TPFA), which for a Cartesian grid is the same as a classical seven-point finite-difference scheme for Poisson’s equation. This is done in two steps: first we compute the transmissibilities and then we assemble and solve the corresponding discrete system.

T   = simpleComputeTrans(G, rock);
sol = simpleIncompTPFA(initResSol(G, 0.0), G, T, fluid, 'bc', bc);

Plot the face pressures

ewplot;
plotFaces(G, 1:G.faces.num, convertTo(sol.facePressure, barsa()));
set(gca, 'ZDir', 'reverse'), title('Pressure [bar]')
view(3), colorbar
set(gca,'DataAspect',[1 1 10]);
_images/gravityColumn_01.png

Compare grid-orientation effects for TPFA/mimetic schemes

Generated from gridOrientationError.m

mrstModule add incomp mimetic streamlines diagnostics

% Rectangular reservoir with a skew grid.
G = cartGrid([41,20],[2,1]);
makeSkew = @(c) c(:,1) + .4*(1-(c(:,1)-1).^2).*(1-c(:,2));
G.nodes.coords(:,1) = 2*makeSkew(G.nodes.coords);
% G.nodes.coords = twister(G.nodes.coords);
% G.nodes.coords(:,1) = 2*G.nodes.coords(:,1);
G = computeGeometry(G);

% Homogeneous reservoir properties
rock = makeRock(G, 100*milli*darcy, .2);
pv   = sum(poreVolume(G,rock));

% Symmetric well pattern
srcCells = findEnclosingCell(G,[2 .975; .5 .025; 3.5 .025]);
src = addSource([], srcCells, [pv; -.5*pv; -.5*pv]);

% Single-phase fluid
fluid = initSingleFluid('mu', 1*centi*poise,'rho', 1000*kilogram/meter^3);

Figure and figure settings

figure('Position',[400 460 900 350]);
parg = {'EdgeColor','k','EdgeAlpha',.05};
_images/gridOrientationError_01.png

TPFA solution

hT   = computeTrans(G, rock);
s_tp = initState(G,[], 0);
s_tp = incompTPFA(s_tp, G, hT, fluid, 'src', src);
subplot(2,2,1);
plotCellData(G, s_tp.pressure, 'EdgeColor', 'k', 'EdgeAlpha', .05);
_images/gridOrientationError_02.png

Mimetic solution

S = computeMimeticIP(G, rock);
s_mi = initState(G, [], 0);
s_mi = incompMimetic(s_mi, G, S, fluid, 'src', src);
subplot(2,2,2);
plotCellData(G, s_mi.pressure, 'EdgeColor', 'k', 'EdgeAlpha', .05);
_images/gridOrientationError_03.png

Compare time-of-flight

ubplot(2,2,3);
tof_tp = computeTimeOfFlight(s_tp, G, rock, 'src', src);
plotCellData(G, tof_tp, tof_tp<.2,'EdgeColor','none'); caxis([0 .2]); box on
seed = floor(G.cells.num/5)+(1:G.cartDims(1))';
hf = streamline(pollock(G, s_tp, seed, 'substeps', 1) );
hb = streamline(pollock(G, s_tp, seed, 'substeps', 1, 'reverse' , true));
set ([ hf ; hb ], 'Color' , 'k' );

subplot(2,2,4);
tof_mi = computeTimeOfFlight(s_mi, G, rock, 'src', src);
plotCellData(G, tof_mi, tof_mi<.2,'EdgeColor','none'); caxis([0 .2]); box on
hf = streamline(pollock(G, s_mi, seed, 'substeps', 1) );
hb = streamline(pollock(G, s_mi, seed, 'substeps', 1, 'reverse' , true));
set ([ hf ; hb ], 'Color' , 'k' );
_images/gridOrientationError_04.png

Quarter five spot

Generated from quarterFiveSpot.m

The purpose of this example is to give an overview of how to set up and use the single-phase TPFA pressure solver to solve the pressure equation

\nabla\cdot v = q, \qquad v=\textbf{--}\frac{K}{\mu}\nabla p,

with no-flow boundary conditions and two source terms at diagonally opposite corners of a 2D Cartesian grid. This setup mimics a quarter five-spot well pattern, which is a standard test in reservoir simulation. In addition to computing pressure, we will also compute streamlines from the flux field as well as the corresponding time-of-flight, i.e., the time it takes for a neutral particle to travel from a fluid inlet (source term, well, inflow boundary, etc) to a given point in the reservoir.

mrstModule add incomp

Set up grid and petrophysical data

We use a Cartesian grid of size nx-by-ny with homogeneous petrophysical data: permeability of 100 mD and porosity of 0.2.

[nx,ny] = deal(32);
G = cartGrid([nx,ny],[500,500]);
G = computeGeometry(G);
rock = makeRock(G, 100*milli*darcy, .2);
Computing normals, areas, and centroids...    Elapsed time is 0.000321 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.001712 seconds.

Compute half transmissibilities

All we need to know to develop the spatial discretization is the reservoir geometry and the petrophysical properties. This means that we can compute the half transmissibilities without knowing any details about the fluid properties and the boundary conditions and/or sources/sinks that will drive the global flow:

hT = simpleComputeTrans(G, rock);

Fluid model

When gravity forces are absent, the only fluid property we need in the incompressible, single-phase flow equation is the viscosity. However, the flow solver is written for general incompressible flow and requires the evaluation of a fluid object that can be expanded to represent more advanced fluid models. Here, however, we only use a simple fluid object that requires a viscosity and a density (the latter is needed when gravity is present)

gravity reset off
fluid = initSingleFluid('mu' , 1*centi*poise, ...
                        'rho', 1014*kilogram/meter^3);
display(fluid)
fluid =

  struct with fields:

    properties: @(varargin)properties(opt,varargin{:})
    saturation: @(x,varargin)x.s
       relperm: @(s,varargin)relperm(s,opt,varargin{:})

Add source terms

To drive the flow, we will use a fluid source at the SW corner and a fluid sink at the NE corner. The time scale of the problem is defined by the strength of the source term. In our case, we set the source terms such that a unit time corresponds to the injection of one pore volume of fluids. All flow solvers in MRST automatically assume no-flow conditions on all outer (and inner) boundaries if no other conditions are specified explicitly.

pv  = sum(poreVolume(G,rock));
src = addSource([], 1, pv);
src = addSource(src, G.cells.num, -pv);
display(src)
src =

  struct with fields:

    cell: [2×1 double]
    rate: [2×1 double]
     sat: []

Construct reservoir state object

To simplify communication among different flow and transport solvers, all unknowns (reservoir states) are collected in a structure. Strictly speaking, this structure need not be initialized for an incompressible model in which none of the fluid properties depend on the reservoir states. However, to avoid treatment of special cases, MRST requires that the structure is initialized and passed as argument to the pressure solver. We therefore initialize it with a dummy pressure value of zero and a unit fluid saturation since we only have a single fluid

state = initResSol(G, 0.0, 1.0);
display(state)
state =

  struct with fields:

    pressure: [1024×1 double]
        flux: [2112×1 double]
           s: [1024×1 double]

Solve pressure and show the result

To solve for the pressure, we simply pass the reservoir state, grid model, half transsmisibilities, fluid model, and driving forces to the flow solver that assembles and solves the incompressible flow equation.

state = simpleIncompTPFA(state, G, hT, fluid, 'src', src);
display(state)

clf,
plotCellData(G, state.pressure);
plotGrid(G, src.cell, 'FaceColor', 'w');
axis equal tight; colormap(jet(128));
state =

  struct with fields:

        pressure: [1024×1 double]
            flux: [2112×1 double]
               s: [1024×1 double]
...
_images/quarterFiveSpot_01.png

Trace streamlines

To visualize the flow field, we show streamlines. To this end, we will use Pollock’s method which is implemented in the ‘streamlines’ add-on module to MRST. Starting at the midpoint of all cells along the NW–SE diagonal in the grid, we trace streamlines forward and backward using ‘pollock’ and plot them using Matlab’s builtin ‘streamline’ routine

mrstModule add streamlines;
seed = (nx:nx-1:nx*ny).';
Sf = pollock(G, state, seed, 'substeps', 1);
Sb = pollock(G, state, seed, 'substeps', 1, 'reverse', true);
hf=streamline(Sf);
hb=streamline(Sb);
set([hf; hb],'Color','k');
_images/quarterFiveSpot_02.png

Compute time-of-flight

Finally, we will compute time-of-flight, i.e., the time it takes a neutral particle to travel from the fluid source to a given point in the reservoir. Isocontours of the time-of-flight define natural time lines in the reservoir, and to emphasize this fact, we plot the time-of-flight using only a few colors.

mrstModule add diagnostics
tof = computeTimeOfFlight(state, G, rock, 'src', src);

clf,
plotCellData(G, tof);
plotGrid(G,src.cell,'FaceColor','w');
axis equal tight;
colormap(jet(16)); caxis([0,1]);
Forward  maximal TOF set to  0.00 years.
_images/quarterFiveSpot_03.png

Visualize high-flow and stagnant regions

We can also compute the backward time-of-flight, i.e., the time it takes a neutral particle to travel from a given point in the reservoir to the fluid sink. The sum of the forward and backward time-of-flights give the total travel time in the reservoir, which can be used to visualize high-flow and stagnant regions

tofb   = computeTimeOfFlight(state, G, rock, 'src', src, 'reverse', true);

clf,
plotCellData(G, tof+tofb);
plotGrid(G,src.cell,'FaceColor','w');
axis equal tight;
colormap(jet(128));
Backward maximal TOF set to  0.00 years.
_images/quarterFiveSpot_04.png

Exercises

Compute a quarter five-spot setup using - perturbed grids, e.g., as computed by ‘twister’ - heterogeneous permeability and porosity, e.g., from a Karman-Cozeny relationship or as subsamples from SPE10

%{

Pressure Solver: Example of a realistic Field Model

Generated from saigupWithWells.m

In the example, we will solve the single-phase, incompressible pressure equation using the corner-point geometry from synthetic reservoir model from the SAIGUP study. The purpose of this example is to demonstrate how the two-point flow solver can be applied to compute flow on a real grid model that has degenerate cell geometries and non-neighbouring connections arising from a number of faults, and inactive cells.

Check for existence of input model data

The model can be downloaded from the the MRST page

grdecl = fullfile(getDatasetPath('SAIGUP'), 'SAIGUP.GRDECL');
if ~exist(grdecl, 'file'),
   error('SAIGUP model data is not available.')
end

Load and process grid model

The model data is provided as an ECLIPSE input file. MRST uses the strict SI conventions in all of its internal calculations. The SAIGUP model, however, is provided using the ECLIPSE ‘METRIC’ conventions (permeabilities in mD and so on) and we must therefore convert the input data to MRST’s internal unit conventions.

grdecl = readGRDECL(grdecl);
usys   = getUnitSystem('METRIC');
grdecl = convertInputUnits(grdecl, usys);
G = processGRDECL(grdecl);
G = computeGeometry(G);

% To speed up the processing, one can use a C-accelerated versions of the
% gridprocessing routines (provided you have a compatible compiler):
% mrstModule add libgeometry opm_gridprocessing
% G = mcomputeGeometry(processgrid(grdecl));

Get petrophysical data

The input data of the permeability in the SAIGUP realisation is an anisotropic tensor with zero vertical permeability in a number of cells. As a result some parts of the reservoir to be completely sealed from the wells. This will cause problems for the time-of-flight solver, which requires that all cells in the model must be flooded after some finite time that can be arbitrarily large. We work around this issue by assigning a small constant times the minimum positive vertical (cross-layer) permeability to the grid blocks that have zero cross-layer permeability.

rock = grdecl2Rock(grdecl, G.cells.indexMap);
is_pos = rock.perm(:, 3) > 0;
rock.perm(~is_pos, 3) = 1e-6*min(rock.perm(is_pos, 3));

hT   = computeTrans(G, rock);

Set fluid data

mrstModule add incomp
gravity reset on
fluid = initSingleFluid('mu',1*centi*poise,'rho', 1000*kilogram/meter^3);

Introduce wells

The reservoir is produced using a set of production wells controlled by bottom-hole pressure and rate-controlled injectors. Wells are described using a Peacemann model, giving an extra set of equations that need to be assembled. For simplicity, all wells are assumed to be vertical and are assigned using the logical (i,j) subindex.

% Plot grid outline
figure('position',[440 317 866 480]);
plotCellData(G,log10(rock.perm(:,1)), ...
   'EdgeColor','k','EdgeAlpha',.1,'FaceAlpha',.5);
axis tight off, view(-100,20)

% Set eight vertical injectors around the perimeter of the model, completed
% in each layer.
nz = G.cartDims(3);
I = [ 3, 20,  3, 25,  3, 30,  5, 29];
J = [ 4,  3, 35, 35, 70, 70,113,113];
R = [ 1,  3,  3,  3,  2,  4,  2,  3]*500*meter^3/day;
W = [];
for i = 1 : numel(I),
   W = verticalWell(W, G, rock, I(i), J(i), 1:nz, 'Type', 'rate', ...
      'Val', R(i), 'Radius', .1*meter, 'Comp_i', 1, ...
      'name', ['I$_{', int2str(i), '}$']);
end
plotWell(G, W, 'height', 30, 'color', 'k');
in = numel(W);

% Set six vertical producers, completed in each layer.
I = [15, 12, 25, 21, 29, 12];
J = [25, 51, 51, 60, 95, 90];
for i = 1 : numel(I),
   W = verticalWell(W, G, rock, I(i), J(i), 1:nz, 'Type', 'bhp', ...
      'Val', 200*barsa(), 'Radius', .1*meter, ...
      'name', ['P$_{', int2str(i), '}$'], 'Comp_i',1);
end
plotWell(G,W(in+1:end),'height',30,'color','b');
_images/saigupWithWells_01.png

Assemble and solve system, plot results

state = initState(G, W, 350*barsa, 1);
state = incompTPFA(state, G, hT, fluid, 'wells', W);

figure('position',[440 317 866 480]);
plotCellData(G, convertTo(state.pressure(1:G.cells.num), barsa), ...
   'EdgeColor','k','EdgeAlpha',0.1);
plotWell(G, W(1:in),     'height', 100, 'color', 'b');
plotWell(G, W(in+1:end), 'height', 100, 'color', 'k');
axis off; view(-80,36)
h=colorbar; set(h,'Position',[0.88 0.15 0.03 0.67]);
_images/saigupWithWells_02.png

Time-of-flight analysis

rstModule add diagnostics
tf = computeTimeOfFlight(state, G, rock, 'wells', W)/year;
tb = computeTimeOfFlight(state, G, rock, 'wells', W, 'reverse', true)/year;

figure('position',[440 317 866 480]);
plotCellData(G,tf+tb,tf+tb<50,'EdgeColor','k','EdgeAlpha',0.1);
plotWell(G, W(1:in),     'height', 100, 'color', 'b');
plotWell(G, W(in+1:end), 'height', 100, 'color', 'k');
plotGrid(G,'FaceColor','none','edgealpha',.05);
axis off; view(-80,36)
caxis([0 50]);
h=colorbar; set(h,'Position',[0.88 0.15 0.03 0.67]);
_images/saigupWithWells_03.png

Grid-orientation and anisotropy effects

Generated from showAnisotropyErrors.m

This script contains two examples that originate from the first MRST paper: Lie et al, “Open source MATLAB implementation of consistent discretisations on complex grids”. Comput. Geosci., 16(2):297-322, 2012. DOI: 10.1007/s10596-011-9244-4

addpath(fullfile(fileparts(mfilename('fullpath')), 'src'))
mrstModule add incomp mimetic mpfa

First example

This example corresponds to Figure 7 in the paper, which illustrates grid-orientation effects for the TPFA scheme and reproduction of linear flow for the mimetic and the MPFA-O method on a perturbed grid for a homogeneous, diagonal permeability tensor with entries Kx=1 and Ky=1000.

% Grid and permeability
G = cartGrid([51, 51]);
G.nodes.coords = twister(G.nodes.coords, 0.03);

% Permeability
K = diag([1, 1000]);

% Seed for streamline tracing
seed = (ceil(G.cartDims(1)/2):4*G.cartDims(1):prod(G.cartDims))';

% Run example
showMonotonicityExample(G, K, seed, true);
colormap(.75*jet(32) + .25*ones(32,3));
Computing normals, areas, and centroids...    Elapsed time is 0.000495 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.002857 seconds.
Setting up linear system...                   Elapsed time is 0.029076 seconds.
Solving linear system...                      Elapsed time is 0.003518 seconds.
Computing fluxes, face pressures etc...               Elapsed time is 0.007400 seconds.
Using inner product: 'ip_quasitpf'.
Computing cell inner products ...             Elapsed time is 0.129290 seconds.
Assembling global inner product matrix ...    Elapsed time is 0.000998 seconds.
...
_images/showAnisotropyErrors_01.png

Second example

This example corresponds to Figure 8 in the paper, which illustrates montonicity effects for the TPFA, mimetic, and MPFA schemes. Same setup as in the first example, but now with the anisotropy ration of 1:1000 making 30 degree angle with the grid directions.

% Grid and permeability
G = cartGrid([21, 21]);
G.nodes.coords = twister(G.nodes.coords, 0.03);

% Permeability
t  =  30*pi/180;
U  = [ cos(t), sin(t); -sin(t), cos(t)];
Kd = diag([1,1000]);
K  =  U'*Kd*U;

% Start points for streamline tracing
seed = (ceil(G.cartDims(1)/2):G.cartDims(1):prod(G.cartDims))';

% Run example
showMonotonicityExample(G, K, seed, true);
Computing normals, areas, and centroids...    Elapsed time is 0.000119 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.000869 seconds.
Setting up linear system...                   Elapsed time is 0.002672 seconds.
Solving linear system...                      Elapsed time is 0.000494 seconds.
Computing fluxes, face pressures etc...               Elapsed time is 0.000236 seconds.
Using inner product: 'ip_quasitpf'.
Computing cell inner products ...             Elapsed time is 0.028684 seconds.
Assembling global inner product matrix ...    Elapsed time is 0.001015 seconds.
...
_images/showAnisotropyErrors_02.png

Demonstrate lack of convergence for the TPFA scheme

Generated from showInconsistentTPFA.m

We consider a homogeneous, rectangular reservoir with a symmetric well pattern consisting of one injector and two producers. Because of the symmetry, the travel times from the injector to each producer should be equal. When using a skew grid that is not K-orhtogonal, the travel times will not be equal and the flow pattern will differ quite a lot from being symmetric. In particular, since our discretization method is not consistent, the dissymmetry does not decay with increasing grid resolution and hence the method does not converge.

rstModule add incomp diagnostics streamlines

figure('Position', [440 450 865 351]);
T = nan(30,2);
disp('Convergence study:');
for i=1:1:30
   % Rectangular reservoir with a skew grid.
   G = cartGrid([i*20+1,i*10],[2,1]);
   makeSkew = @(c) c(:,1) + .4*(1-(c(:,1)-1).^2).*(1-c(:,2));
   G.nodes.coords(:,1) = 2*makeSkew(G.nodes.coords);
   G = computeGeometry(G);
   disp(['  Grid: ' num2str(G.cartDims)]);

   % Homogeneous reservoir properties
   rock = makeRock(G, 100*milli*darcy, .2);
   pv   = sum(poreVolume(G,rock));

   % Symmetric well pattern
   srcCells = findEnclosingCell(G,[2 .975; .5 .025; 3.5 .025]);
   src = addSource([], srcCells, [pv; -.5*pv; -.5*pv]);

   % Single-phase fluid
   fluid = initSingleFluid('mu', 1*centi*poise,'rho', 1000*kilogram/meter^3);

   % Solve flow problem
   hT     = computeTrans(G, rock);
   state  = initState(G,[], 0);
   state  = incompTPFA(state, G, hT, fluid, 'src', src);
   tof    = computeTimeOfFlight(state, G, rock, 'src', src);
   T(i,:) = tof(srcCells(2:3))';

   % Plot second solution
   if i==2
      subplot(2,3,1:2);
      plotCellData(G, state.pressure, 'EdgeColor', 'k', 'EdgeAlpha', .05);
      hold on
      plot([.5 2 3.5], [.025 .975 .025],'.','Color',[.9 .9 .9],'MarkerSize',16);
      hold off

      subplot(2,3,4:5);
      plotCellData(G, tof, tof<.2, 'EdgeColor','none'); caxis([0 .2]); box on
      seed = floor(G.cells.num/5)+(1:G.cartDims(1))';
      hf = streamline(pollock(G, state, seed, 'substeps', 1) );
      hb = streamline(pollock(G, state, seed, 'substeps', 1, 'reverse' , true));
      set ([ hf ; hb ], 'Color' , 'k' );
      drawnow;
   end
end
subplot(2,3,[3 6]);
bar(T')
hold on, plot([.5 2.5],[1 1], '--r','LineWidth',2); hold off
set(gca,'XTickLabel', {'L','R'});
axis tight
Convergence study:
  Grid: 21  10
  Grid: 41  20
  Grid: 61  30
  Grid: 81  40
  Grid: 101   50
  Grid: 121   60
  Grid: 141   70
...
_images/showInconsistentTPFA_01.png
_images/showInconsistentTPFA_02.png

Solve the Poisson problem

Generated from solvePoisson.m

In the first example we use a small Cartesian grid

G = computeGeometry(cartGrid([5 5],[1 1]));
Computing normals, areas, and centroids...    Elapsed time is 0.000135 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.000583 seconds.

Define discrete operators

Since we impose no-flow boundary conditions, we restrict the connections to the interior faces only

N = G.faces.neighbors;
N = N(all(N ~= 0, 2), :);
nf = size(N,1);
nc = G.cells.num;
C = sparse([(1:nf)'; (1:nf)'], N, ones(nf,1)*[-1 1], nf, nc);
grad = @(x) C*x;
div  = @(x) -C'*x;
figure; spy(C);
_images/solvePoisson_01.png

Set up and solve the problem

p = initVariablesADI(zeros(nc,1));
q = zeros(nc, 1);               % source term
q(1) = 1; q(nc) = -1;           % -> quarter five-spot

eq    = div(grad(p))+q;         % equation
eq(1) = eq(1) + p(1);           % make solution unique
p     = -eq.jac{1}\eq.val;      % solve equation
clf, plotCellData(G, p);
_images/solvePoisson_02.png

Grid

Generated from solvePoissonCircle.m

G = cartGrid([20 20],[1 1]);
G = computeGeometry(G);
r1 = sum(bsxfun(@minus,G.cells.centroids,[0.5 1]).^2,2);
r2 = sum(bsxfun(@minus,G.cells.centroids,[0.5 0]).^2,2);
clf, plotCellData(G, double((r1>0.16) & (r2>0.16)) );

G = extractSubgrid(G, (r1>0.16) & (r2>0.16));
Computing normals, areas, and centroids...    Elapsed time is 0.000134 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.000813 seconds.
_images/solvePoissonCircle_01.png

Grid information

N = G.faces.neighbors;
N = N(all(N ~= 0, 2), :);
nf = size(N,1);
nc = G.cells.num;

Operators

C = sparse([(1:nf)'; (1:nf)'], N, ...
    ones(nf,1)*[-1 1], nf, nc);
grad = @(x) C*x;
div  = @(x) -C'*x;
spy(C); set(gca,'XTick',[],'YTick',[]); xlabel([]);
_images/solvePoissonCircle_02.png

Assemble and solve equations

p = initVariablesADI(zeros(nc,1));
q = zeros(nc, 1);             % source term
q(1) = 1; q(nc) = -1;         % -> quarter five-spot

eq    = div(grad(p))+q;       % equation
eq(1) = eq(1) + p(1);         % make solution unique
p     = -eq.jac{1}\eq.val;    % solve equation

Plot result

clf, plotCellData(G,p);
_images/solvePoissonCircle_03.png

Grid

Generated from solvePoissonSeamount.m

load seamount
G = pebi(triangleGrid([x(:) y(:)], delaunay(x,y)));
G = computeGeometry(G);
clf, plotGrid(G);
axis tight off; set(gca,'XLim',[210.6 211.7]);
Computing normals, areas, and centroids...    Elapsed time is 0.000129 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.000711 seconds.
_images/solvePoissonSeamount_01.png

Grid information

N  = G.faces.neighbors;
N  = N(all(N ~= 0, 2), :);
nf = size(N,1);
nc = G.cells.num;

Operators

C = sparse([(1:nf)'; (1:nf)'], N, ...
    ones(nf,1)*[-1 1], nf, nc);
grad = @(x) C*x;
div  = @(x) -C'*x;
spy(C); set(gca,'XTick',[],'YTick',[]); xlabel([]);
_images/solvePoissonSeamount_02.png

Transmissibility

hT = computeTrans(G, struct('perm', ones(nc,1)));
cf = G.cells.faces(:,1);
T  = 1 ./ accumarray(cf, 1 ./ hT, [G.faces.num, 1]);
T  = T(all(N~=0,2),:);

Assemble and solve equations

p = initVariablesADI(zeros(nc,1));
q = zeros(nc, 1);
q([135 282 17]) = [-1 .5 .5];
eq    = div(T.*grad(p))+q;
eq(1) = eq(1) + p(1);
p     = -eq.jac{1}\eq.val;

Plot result

clf, plotCellData(G,p);
axis tight off; set(gca,'XLim',[210.6 211.7]);
_images/solvePoissonSeamount_03.png

Triangular grid

Generated from stencilComparison.m

load seamount
T = triangleGrid([x(:) y(:)], delaunay(x,y));
[Tmin,Tmax] = deal(min(T.nodes.coords), max(T.nodes.coords));
T.nodes.coords = bsxfun(@times, ...
   bsxfun(@minus, T.nodes.coords, Tmin), 1000./(Tmax - Tmin));
T = computeGeometry(T);
clear x y z Tmin Tmax;
Computing normals, areas, and centroids...    Elapsed time is 0.000162 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.000859 seconds.

Cartesian grids

G = computeGeometry(cartGrid([25 25], [1000 1000]));
inside = isPointInsideGrid(T, G.cells.centroids);
G = removeCells(G, ~inside);

Gr = computeGeometry(cartGrid([250 250], [1000 1000]));
inside = isPointInsideGrid(T, Gr.cells.centroids);
Gr = removeCells(Gr, ~inside);
Computing normals, areas, and centroids...    Elapsed time is 0.000152 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.001048 seconds.
Computing normals, areas, and centroids...    Elapsed time is 0.003370 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.036560 seconds.

Radial grid

P = [];
for r = exp([-3.5:.2:0, 0, .1]),
   [x,y] = cylinder(r,25); P = [P [x(1,:); y(1,:)]]; %#ok<AGROW>
end
P = unique([P'; 0 0],'rows');
[Pmin,Pmax] = deal(min(P), max(P));
P = bsxfun(@minus, bsxfun(@times, ...
   bsxfun(@minus, P, Pmin), 1200./(Pmax-Pmin)), [150 100]);
inside = isPointInsideGrid(T, P);
V = pebi( triangleGrid(P(inside,:)) );
V = computeGeometry(V);
clear P* x y;
Computing normals, areas, and centroids...    Elapsed time is 0.000146 seconds.
Computing cell volumes and centroids...               Elapsed time is 0.000950 seconds.

Simulation loop

mrstModule add incomp diagnostics
state = cell(4,1);
src   = cell(4,1);
bc    = cell(4,1);
A     = cell(4,1);
tof   = cell(4,1);
fluid = initSingleFluid('mu', 1*centi*poise, 'rho', 1014*kilogram/meter^3);
g     = {G, T, V, Gr};
for i=1:4
   rock.poro = repmat(0.2, g{i}.cells.num, 1);
   rock.perm = repmat(100*milli*darcy, g{i}.cells.num, 1);
   hT = simpleComputeTrans(g{i}, rock);
   pv = sum(poreVolume(g{i}, rock));

   tmp = (g{i}.cells.centroids - repmat([450, 500],g{i}.cells.num,1)).^2;
   [~,ind] = min(sum(tmp,2));
   src{i} = addSource(src{i}, ind, -.02*pv/year);

   f = boundaryFaces(g{i});
   bc{i} = addBC([], f, 'pressure', 50*barsa);

   state{i} = incompTPFA(initResSol(g{i},0,1), ...
      g{i}, hT, fluid, 'src', src{i}, 'bc', bc{i}, 'MatrixOutput', true);

   [tof{i},A{i}] = computeTimeOfFlight(state{i}, g{i}, rock,...
      'src', src{i},'bc',bc{i}, 'reverse', true);
end
Setting up linear system...                   Elapsed time is 0.003491 seconds.
Solving linear system...                      Elapsed time is 0.000790 seconds.
Computing fluxes, face pressures etc...               Elapsed time is 0.000249 seconds.
Backward maximal TOF set to 2500.00 years.
Setting up linear system...                   Elapsed time is 0.001687 seconds.
Solving linear system...                      Elapsed time is 0.000618 seconds.
Computing fluxes, face pressures etc...               Elapsed time is 0.000210 seconds.
Backward maximal TOF set to 2500.00 years.
...

Plot solutions

figure(1), clf, set(gcf,'Position', [400 420 925 400]);
ttext = {'Cartesian','Triangular','Radial','Reference'};
for i=1:4
   subplot(1,4,i),
   plotCellData(g{i},state{i}.pressure/barsa,'EdgeColor','k', 'EdgeAlpha', .1);
   plotGrid(g{i},src{i}.cell, 'FaceColor', 'w');
   title([ttext{i} ': ' num2str(g{i}.cells.num) ' cells']);
   caxis([40 50]); axis tight off
end
set(get(gca,'Children'),'EdgeColor','none');
h=colorbar('Location','South');
set(h,'position',[0.13 0.01 0.8 0.03],'YTick',1,'YTickLabel','[bar]');
set(gcf,'PaperPositionMode', 'auto');
% print -dpng stencil-p.png;
_images/stencilComparison_01.png

Plot radial solutions

col = 'rbgk';
ms  = [12 8 8 2];
figure(2), clf, hold on
for i=[4 1:3]
  d = g{i}.cells.centroids - repmat(g{i}.cells.centroids(src{i}.cell,:),g{i}.cells.num,1);
  r = (sum(d.^2,2)).^.5;
  plot(r, state{i}.pressure/barsa, [col(i) '.'],'MarkerSize',ms(i));
end
axis([0 300 40 50]);
h=legend(ttext{[4 1:3]},'Location','SouthEast'); set(h,'FontSize',14);
chld = get(h,'Children');
set(chld(1:3:end),'MarkerSize',20);
% print -depsc2 stencil-rad.eps;
_images/stencilComparison_02.png

Plot matrix structures: TPFA matrix

figure(3); clf, set(gcf,'Position', [400 420 925 400]);
for i=1:3
   subplot(1,3,i),
   spy(state{i}.A);
   title(ttext{i});
end
set(gcf,'PaperPositionMode', 'auto');
% print -depsc2 stencil-A.eps;
_images/stencilComparison_03.png

Plot solutions

figure(4), clf, set(gcf,'Position', [400 420 925 400]);
ttext = {'Cartesian','Triangular','Radial','Reference'};
for i=1:4
   subplot(1,4,i),
   plotCellData(g{i},tof{i}/year,'EdgeColor','k', 'EdgeAlpha', .1);
   plotGrid(g{i},src{i}.cell, 'FaceColor', 'w');
   title([ttext{i} ': ' num2str(g{i}.cells.num) ' cells']);
   axis tight off
end
set(get(gca,'Children'),'EdgeColor','none');
h=colorbar('Location','South');
set(h,'position',[0.13 0.01 0.8 0.03],'YTick',1,'YTickLabel','[year]');
set(gcf,'PaperPositionMode', 'auto');
% print -dpng stencil-tof.png;
_images/stencilComparison_04.png

Plot matrix structures: TPFA matrix

igure(5); clf, set(gcf,'Position', [400 420 925 500]);
for i=1:3
   subplot(2,3,i),
   spy(A{i});
   title(ttext{i});
   subplot(2,3,i+3)
   [~,q] = sort(state{i}.pressure);
   spy(A{i}(q,q));
   l = triu(A{i}(q,q),1);
   if sum(l(:))
      disp(['Discretization matrix: ' ttext{i} ', *not* lower triangular']);
   else
      disp(['Discretization matrix: ' ttext{i} ', lower triangular']);
   end
end
set(gcf,'PaperPositionMode', 'auto');
% print -depsc2 stencil-A-tof.eps;
Discretization matrix: Cartesian, lower triangular
Discretization matrix: Triangular, lower triangular
Discretization matrix: Radial, lower triangular
_images/stencilComparison_05.png

Non-Newtonian fluid

Generated from nonNewtonianCell.m

In this example, we will demonstrate how one can easily extend the compressible single-phase pressure solver to include the effect of non-Newtonian fluids modelled using a simple power law in which the effective viscosity depends on the norm of the velocity

Define geometric quantitites

Grid that represents the reservoir geometry

[nx,ny,nz] = deal( 10,  10, 10);
[Lx,Ly,Lz] = deal(200, 200, 50);
G = cartGrid([nx, ny, nz], [Lx, Ly, Lz]);
G = computeGeometry(G);

% Discrete operators
N  = double(G.faces.neighbors);
intInx = all(N ~= 0, 2);
N  = N(intInx, :);
n = size(N,1);
C = sparse( [(1:n)'; (1:n)'], N, ones(n,1)*[-1 1], n, G.cells.num);
aC = bsxfun(@rdivide,0.5*abs(C),G.faces.areas(intInx))';
grad = @(x) C*x;
div  = @(x) -C'*x;
cavg = @(x) aC*x;
favg = @(x) 0.5 * (x(N(:,1)) + x(N(:,2)));
clear aC C N;

Rock model and transmissibilities

rock = makeRock(G, 30*milli*darcy, 0.3);

cr   = 1e-6/barsa;
p_r  = 200*barsa;
pv_r = poreVolume(G, rock);
pv   = @(p) pv_r .* exp( cr * (p - p_r) );
clear pv_r;

hT = computeTrans(G, rock);
cf = G.cells.faces(:,1);
nf = G.faces.num;
T  = 1 ./ accumarray(cf, 1 ./ hT, [nf, 1]);
T  = T(intInx);
clear hT;

Basic fluid model

c     = 1e-3/barsa;
rho_r = 850*kilogram/meter^3;
rhoS  = 750*kilogram/meter^3;
rho   = @(p) rho_r .* exp( c * (p - p_r) );
if exist('fluidModel', 'var')
   mu0 = fluidModel.mu0;
   nmu = fluidModel.nmu;
   Kc  = fluidModel.Kc;
   Kbc = (Kc/mu0)^(2/(nmu-1))*36*((3*nmu+1)/(4*nmu))^(2*nmu/(nmu-1));
   if nmu==1, Kbc = 0; end
else
   mu0 = 100*centi*poise;
   nmu = 0.25;
   Kc  = .1;
   Kbc = (Kc/mu0)^(2/(nmu-1))*36*((3*nmu+1)/(4*nmu))^(2*nmu/(nmu-1));
end

Initial vertical equilibrium

gravity reset on, g = norm(gravity);
[z_0, z_max] = deal(0, max(G.cells.centroids(:,3)));
equil  = ode23(@(z,p) g .* rho(p), [z_0, z_max], p_r);
p_init = reshape(deval(equil, G.cells.centroids(:,3)), [], 1);
clear equil z_0 z_max;

Constant for the simulation

numSteps = 52;
totTime  = 365*day;
dt       = totTime / numSteps;
tol      = 1e-5;
maxits   = 100;

Flow equations

phiK  = rock.perm.*rock.poro;
gradz = grad(G.cells.centroids(:,3));
v     = @(p, eta) ...
        -(T./(mu0*favg(eta))).*( grad(p) - g*favg(rho(p)).*gradz );
etaEq = @(p, eta) ...
        eta - ( 1 + Kbc* cavg(v(p,eta)).^2 ./phiK ).^((nmu-1)/2);
presEq= @(p, p0, eta, dt)  ...
        (1/dt)*(pv(p).*rho(p) - pv(p0).*rho(p0)) + div(favg(rho(p)).*v(p, eta));

Well model

nperf = 8;
I = repmat(2, [nperf, 1]);
J = (1 : nperf).' + 1;
K = repmat(5, [nperf, 1]);
cellInx = sub2ind(G.cartDims, I, J, K);
W = addWell([ ], G, rock, cellInx, 'Name', 'P1', 'Dir', 'x' );

% Define well equations
wc = W(1).cells; % connection grid cells
WI = W(1).WI;    % well-indices
dz = W(1).dZ;    % connection depth relative to bottom-hole

p_conn = @(bhp) ...
   bhp + g*dz.*rho(bhp);
q_conn = @(p, eta, bhp) ...
   WI .* (rho(p(wc)) ./ (mu0*eta(wc))) .* (p_conn(bhp) - p(wc));
rateEq = @(p, eta, bhp, qS) ...
   qS - sum(q_conn(p, eta, bhp))/rhoS;
ctrlEq = @(bhp) ...
   bhp - 300*barsa;

Initialize for solution loop

nc = G.cells.num;
[p_ad, eta_ad, bhp_ad, qS_ad] = ...
   initVariablesADI(p_init, ones(nc,1), p_init(wc(1)), 0);
[pIx, etaIx, bhpIx, qSIx] = ...
   deal(1:nc, nc+1:2*nc, 2*nc+1, 2*nc+2);
sol = repmat(struct('time',[],'pressure',[],'eta',[], ...
                    'bhp',[],'qS',[]), [numSteps+1,1]);
sol(1) = struct('time', 0, 'pressure', value(p_ad), ...
                'eta', value(eta_ad), ...
                'bhp', value(bhp_ad), 'qS', value(qS_ad));
[etamin, etawmin, etamean] = deal(zeros(numSteps,1));

Time loop

t = 0; step = 0;
while t < totTime

   % Increment time
   t = t + dt;
   step = step + 1;
   fprintf('Time step %d: Time %.2f -> %.2f days\n', ...
      step, convertTo(t - dt, day), convertTo(t, day));

   % Main Newton loop
   p0  = value(p_ad); % Previous step pressure
   [resNorm,nit] = deal(1e99, 0);
   while (resNorm > tol) && (nit < maxits)

      % Newton loop for eta (effective viscosity)
      [resNorm2,nit2] = deal(1e99, 0);
      eta_ad2 = initVariablesADI(eta_ad.val);
      while (resNorm2 > tol) && (nit2 <= maxits)
         eeq = etaEq(p_ad.val, eta_ad2);
         res = eeq.val;
         eta_ad2.val = eta_ad2.val - (eeq.jac{1} \ res);

         resNorm2 = norm(res);
         nit2     = nit2+1;
      end
      if nit2 > maxits
         error('Local Newton solves did not converge')
      else
         eta_ad.val = eta_ad2.val;
      end

      % Add source terms to homogeneous pressure equation:
      eq1     = presEq(p_ad, p0, eta_ad, dt);
      eq1(wc) = eq1(wc) - q_conn(p_ad, eta_ad, bhp_ad);

      % Collect all equations
      eqs = {eq1, etaEq(p_ad, eta_ad), ...
         rateEq(p_ad, eta_ad, bhp_ad, qS_ad), ctrlEq(bhp_ad)};

      % Concatenate equations and solve for update:
      eq  = cat(eqs{:});
      J   = eq.jac{1};  % Jacobian
      res = eq.val;     % residual
      upd = -(J \ res); % Newton update
      % Update variables
      p_ad.val   = p_ad.val   + upd(pIx);
      eta_ad.val = eta_ad.val + upd(etaIx);
      bhp_ad.val = bhp_ad.val + upd(bhpIx);
      qS_ad.val  = qS_ad.val  + upd(qSIx);

      resNorm = norm(res);
      nit     = nit + 1;
   end

 %  clf,
 %  plotCellData(G,eta_ad.val,'FaceAlpha',.3,'EdgeAlpha', .1);
 %  view(3); colorbar; drawnow

   if nit > maxits
      error('Newton solves did not converge')
   else % store solution
      sol(step+1)  = struct('time', t, 'pressure', value(p_ad), ...
         'eta', value(eta_ad), ...
         'bhp', value(bhp_ad), 'qS', value(qS_ad));
   end
   etamin (step) = min(eta_ad.val);
   etawmin(step) = min(eta_ad.val(wc));
   etamean(step) = mean(eta_ad.val);

end
Time step 1: Time 0.00 -> 7.02 days
Time step 2: Time 7.02 -> 14.04 days
Time step 3: Time 14.04 -> 21.06 days
Time step 4: Time 21.06 -> 28.08 days
Time step 5: Time 28.08 -> 35.10 days
Time step 6: Time 35.10 -> 42.12 days
Time step 7: Time 42.12 -> 49.13 days
Time step 8: Time 49.13 -> 56.15 days
...
lf
[ha,hr,hp] = ...
   plotyy([sol(2:end).time]/day, [sol(2:end).qS]*day, ...
          [sol(2:end).time]/day, mean([sol(2:end).pressure]/barsa), ...
          'stairs', 'plot');
%set(ha,'FontSize',16);
set(hr,'LineWidth', 2);
set(hp,'LineStyle','none','Marker','o','LineWidth', 1);
xlabel('time [days]');
ylabel(ha(1), 'rate [m^3/day]');
p=get(gca,'Position'); p(1)=p(1)-.01; p(2)=p(2)+.02; set(gca,'Position',p);
ylabel(ha(2), 'avg pressure [bar]');
_images/nonNewtonianCell_01.png

Non-Newtonian fluid

Generated from nonNewtonianFace.m

In this example, we will demonstrate how one can easily extend the compressible single-phase pressure solver to include the effect of non-Newtonian fluids modelled using a simple power law in which the effective viscosity depends on the norm of the velocity

Define geometric quantitites

Grid that represents the reservoir geometry

[nx,ny,nz] = deal( 10,  10, 10);
[Lx,Ly,Lz] = deal(200, 200, 50);
G = cartGrid([nx, ny, nz], [Lx, Ly, Lz]);
G = computeGeometry(G);

% Discrete operators
N  = double(G.faces.neighbors);
intInx = all(N ~= 0, 2);
N  = N(intInx, :);
n = size(N,1);
C = sparse( [(1:n)'; (1:n)'], N, ones(n,1)*[-1 1], n, G.cells.num);
grad = @(x)C*x;
div  = @(x)-C'*x;
avg  = @(x) 0.5 * (x(N(:,1)) + x(N(:,2)));

Rock model and transmissibilities

rock = makeRock(G, 30*milli*darcy, 0.3);

cr   = 1e-6/barsa;
p_r  = 200*barsa;
pv_r = poreVolume(G, rock);
pv   = @(p) pv_r .* exp( cr * (p - p_r) );

hT = computeTrans(G, rock);
cf = G.cells.faces(:,1);
nf = G.faces.num;
T  = 1 ./ accumarray(cf, 1 ./ hT, [nf, 1]);
T  = T(intInx);
fa = G.faces.areas(intInx);

Basic fluid model

c     = 1e-3/barsa;
rho_r = 850*kilogram/meter^3;
rhoS  = 750*kilogram/meter^3;
rho   = @(p) rho_r .* exp( c * (p - p_r) );
if exist('fluidModel', 'var')
   mu0 = fluidModel.mu0;
   nmu = fluidModel.nmu;
   Kc  = fluidModel.Kc;
   Kbc = (Kc/mu0)^(2/(nmu-1))*36*((3*nmu+1)/(4*nmu))^(2*nmu/(nmu-1));
   if nmu==1, Kbc = 0; end
else
   mu0 = 100*centi*poise;
   nmu = 0.25;
   Kc  = .1;
   Kbc = (Kc/mu0)^(2/(nmu-1))*36*((3*nmu+1)/(4*nmu))^(2*nmu/(nmu-1));
end

Initial vertical equilibrium

gravity reset on, g = norm(gravity);
[z_0, z_max] = deal(0, max(G.cells.centroids(:,3)));
equil  = ode23(@(z,p) g .* rho(p), [z_0, z_max], p_r);
p_init = reshape(deval(equil, G.cells.centroids(:,3)), [], 1);  clear equil

Constant for the simulation

numSteps = 52;
totTime  = 365*day;
dt       = totTime / numSteps;
tol      = 1e-5;
maxits   = 100;

Flow equations

phiK   = avg(rock.perm.*rock.poro).*G.faces.areas(intInx).^2;
gradz  = grad(G.cells.centroids(:,3));
v      = @(p, eta)   -(T./(mu0*eta)).*( grad(p) - g*avg(rho(p)).*gradz );
etaEq  = @(p, eta)   eta - (1 + Kbc*v(p,eta).^2./phiK).^((nmu-1)/2);
presEq = @(p, p0, eta, dt)  ...
   (1/dt)*(pv(p).*rho(p) - pv(p0).*rho(p0)) + div(avg(rho(p)).*v(p, eta));

Well model

nperf = 8;
I = repmat(2, [nperf, 1]);
J = (1 : nperf).' + 1;
K = repmat(5, [nperf, 1]);
cellInx = sub2ind(G.cartDims, I, J, K);
W = addWell([ ], G, rock, cellInx, 'Name', 'P1', 'Dir', 'x' );
if exist('wellAvg', 'var') && wellAvg
   wavg = @(eta) 1/6*abs(C(:,W.cells))'*eta;
else
   wavg = @(eta) ones(8,1);
end

% Define well equations
wc = W(1).cells;
WI = W(1).WI;
dz = W(1).dZ;

p_conn = @(bhp) ...
   bhp + g*dz.*rho(bhp);
q_conn = @(p, eta, bhp) ...
   WI .* (rho(p(wc)) ./ (mu0*wavg(eta))) .* (p_conn(bhp) - p(wc));
rateEq = @(p, eta, bhp, qS) ...
   qS - sum(q_conn(p, eta, bhp))/rhoS;
ctrlEq = @(bhp) ...
   bhp - 300*barsa;

Initialize for solution loop

nc = G.cells.num;
nf = numel(T);
[p_ad, eta_ad, bhp_ad, qS_ad] = ...
   initVariablesADI(p_init, ones(nf,1), p_init(wc(1)), 0);
[pIx, etaIx, bhpIx, qSIx] = ...
   deal(1:nc, nc+1:nc+nf, nc+nf+1, nc+nf+2);
sol = repmat(struct('time',[],'pressure',[],'eta',[], ...
                    'bhp',[],'qS',[]), [numSteps+1,1]);
sol(1) = struct('time', 0, 'pressure', value(p_ad), ...
                'eta', value(eta_ad), ...
                'bhp', value(bhp_ad), 'qS', value(qS_ad));
[etamin, etawmin, etamean] = deal(zeros(numSteps,1));

Time loop

t = 0; step = 0;
while t < totTime

   % Increment time
   t = t + dt;
   step = step + 1;
   fprintf('Time step %d: Time %.2f -> %.2f days\n', ...
      step, convertTo(t - dt, day), convertTo(t, day));

   % Main Newton loop
   p0  = value(p_ad); % Previous step pressure
   [resNorm,nit] = deal(1e99, 0);
   while (resNorm > tol) && (nit < maxits)

      % Newton loop for eta (effective viscosity)
      [resNorm2,nit2] = deal(1e99, 0);
      eta_ad2 = initVariablesADI(eta_ad.val);
      while (resNorm2 > tol) && (nit2 <= maxits)
         eeq = etaEq(p_ad.val, eta_ad2);
         res = eeq.val;
         eta_ad2.val = eta_ad2.val - (eeq.jac{1} \ res);

         resNorm2 = norm(res);
         nit2     = nit2+1;
      end
      if nit2 > maxits
         error('Local Newton solves did not converge')
      else
         eta_ad.val = eta_ad2.val;
      end

      % Add source terms to homogeneous pressure equation:
      eq1     = presEq(p_ad, p0, eta_ad, dt);
      eq1(wc) = eq1(wc) - q_conn(p_ad, eta_ad, bhp_ad);

      % Collect all equations
      eqs = {eq1, etaEq(p_ad, eta_ad), ...
         rateEq(p_ad, eta_ad, bhp_ad, qS_ad), ctrlEq(bhp_ad)};

      % Concatenate equations and solve for update:
      eq  = cat(eqs{:});
      J   = eq.jac{1};  % Jacobian
      res = eq.val;     % residual
      upd = -(J \ res); % Newton update
      % Update variables
      p_ad.val   = p_ad.val   + upd(pIx);
      eta_ad.val = eta_ad.val + upd(etaIx);
      bhp_ad.val = bhp_ad.val + upd(bhpIx);
      qS_ad.val  = qS_ad.val  + upd(qSIx);

      resNorm = norm(res);
      nit     = nit + 1;
   end

%   clf,
%   plotFaces(G,intInx, eta_ad.val,'FaceAlpha',.3,'EdgeAlpha', .1);
%   view(3); colorbar; drawnow

   if nit > maxits
      error('Newton solves did not converge')
   else % store solution
      sol(step+1)  = struct('time', t, 'pressure', value(p_ad), ...
         'eta', value(eta_ad), ...
         'bhp', value(bhp_ad), 'qS', value(qS_ad));
   end
   etamin (step) = min(eta_ad.val);
   etawmin(step) = min(wavg(eta_ad.val));
   etamean(step) = mean(eta_ad.val);
end
Time step 1: Time 0.00 -> 7.02 days
Time step 2: Time 7.02 -> 14.04 days
Time step 3: Time 14.04 -> 21.06 days
Time step 4: Time 21.06 -> 28.08 days
Time step 5: Time 28.08 -> 35.10 days
Time step 6: Time 35.10 -> 42.12 days
Time step 7: Time 42.12 -> 49.13 days
Time step 8: Time 49.13 -> 56.15 days
...
lf
[ha,hr,hp] = ...
   plotyy([sol(2:end).time]/day, [sol(2:end).qS]*day, ...
          [sol(2:end).time]/day, mean([sol(2:end).pressure]/barsa), ...
          'stairs', 'plot');
%set(ha,'FontSize',16);
set(hr,'LineWidth', 2);
set(hp,'LineStyle','none','Marker','o','LineWidth', 1);
xlabel('time [days]');
ylabel(ha(1), 'rate [m^3/day]');
p=get(gca,'Position'); p(1)=p(1)-.01; p(2)=p(2)+.02; set(gca,'Position',p);
ylabel(ha(2), 'avg pressure [bar]');
_images/nonNewtonianFace_01.png
mu0 = 100*centi*poise;
for nsim=1:4
   switch nsim
      case 1
         fluidModel = struct('mu0', mu0, 'nmu',  1, 'Kc', .1);
         nonNewtonianCell;
      case 2
         fluidModel = struct('mu0', mu0, 'nmu', .3, 'Kc', .1);
         nonNewtonianCell;
      case 3
         fluidModel = struct('mu0', mu0, 'nmu', .3, 'Kc', .1);
         wellAvg = true;
         nonNewtonianFace;
      case 4
         fluidModel = struct('mu0', mu0, 'nmu', .3, 'Kc', .1);
         wellAvg = false;
         nonNewtonianFace;
   end
   avgpres(:,nsim) = mean([sol(2:end).pressure]/barsa);        %#ok<SAGROW>
   rate   (:,nsim) = [sol(2:end).qS]*day;                      %#ok<SAGROW>
   time   (:,nsim) = [sol(2:end).time]/day;                    %#ok<SAGROW>
   mineta (:,nsim) = etamin;                                   %#ok<SAGROW>
   minweta(:,nsim) = etawmin;                                  %#ok<SAGROW>
   meaneta(:,nsim) = etamean;                                  %#ok<SAGROW>
end
Time step 1: Time 0.00 -> 7.02 days
Time step 2: Time 7.02 -> 14.04 days
Time step 3: Time 14.04 -> 21.06 days
Time step 4: Time 21.06 -> 28.08 days
Time step 5: Time 28.08 -> 35.10 days
Time step 6: Time 35.10 -> 42.12 days
Time step 7: Time 42.12 -> 49.13 days
Time step 8: Time 49.13 -> 56.15 days
...
_images/nonNewtonianFigure_01.png
figure('Position', [440 375 840 420]);
subplot(1,2,1);
stairs(time, rate, 'LineWidth', 2);
set(gca,'FontSize',12);
xlabel('time [days]'); ylabel('rate [m^3/day]');
legend('Newtonian', 'Cell-based', 'Face-based', 'Face-based (not well)', ...
       'Location', 'NorthEast');
subplot(1,2,2);
plot(time, avgpres,'o','LineWidth', 2);
set(gca,'FontSize',12,'YAxisLocation','right');
xlabel('time [days]'); ylabel('avg pressure [bar]');
legend('Newtonian', 'Cell-based', 'Face-based', 'Face-based (not well)', ...
       'Location', 'SouthEast');
set(gcf,'PaperPositionMode','auto');
_images/nonNewtonianFigure_02.png
ubplot(1,2,2), cla
plot(time,mineta,'-','LineWidth',2);
hold on
plot(time,minweta,'--','LineWidth',2);
plot(time,meaneta,'-.','LineWidth',2);
hold off
set(gca,'FontSize',12,'YAxisLocation','right');
xlabel('time [days]'); ylabel('shear multiplicator [1]');
_images/nonNewtonianFigure_03.png

Single-phase compressible AD solver

Generated from singlePhaseAD.m

The purpose of the example is to give the first introduction to how one can use the automatic differentiation (AD) class in MRST to write a flow simulator for a compressible single-phase model. For simplicity, the reservoir is assumed to be a rectangular box with homogeneous properties and no-flow boundaries. Starting from a hydrostatic initial state, the reservoir is produced from a horizontal well that will create a zone of pressure draw-down. As more fluids are produced, the average pressure in the reservoir drops, causing a gradual decay in the production rate.

Set up model geometry

[nx,ny,nz] = deal( 10,  10, 10);
[Dx,Dy,Dz] = deal(200, 200, 50);
G = cartGrid([nx, ny, nz], [Dx, Dy, Dz]);
G = computeGeometry(G);

plotGrid(G); view(3); axis tight
_images/singlePhaseAD_01.png

Define rock model

rock = makeRock(G, 30*milli*darcy, 0.3);

cr   = 1e-6/barsa;
p_r  = 200*barsa;
pv_r = poreVolume(G, rock);
pv   = @(p) pv_r .* exp( cr * (p - p_r) );

p = linspace(100*barsa,220*barsa,50);
plot(p/barsa, pv_r(1).*exp(cr*(p-p_r)),'LineWidth',2);
_images/singlePhaseAD_02.png

Define model for compressible fluid

mu    = 5*centi*poise;
c     = 1e-3/barsa;
rho_r = 850*kilogram/meter^3;
rhoS  = 750*kilogram/meter^3;
rho   = @(p) rho_r .* exp( c * (p - p_r) );

plot(p/barsa,rho(p),'LineWidth',2);
_images/singlePhaseAD_03.png

Assume a single horizontal well

nperf = 8;
I = repmat(2, [nperf, 1]);
J = (1 : nperf).' + 1;
K = repmat(5, [nperf, 1]);
cellInx = sub2ind(G.cartDims, I, J, K);
W = addWell([ ], G, rock, cellInx, 'Name', 'P1', 'Dir', 'y' );

Impose vertical equilibrium

gravity reset on, g = norm(gravity);
[z_0, z_max] = deal(0, max(G.cells.centroids(:,3)));
equil  = ode23(@(z,p) g .* rho(p), [z_0, z_max], p_r);
p_init = reshape(deval(equil, G.cells.centroids(:,3)), [], 1);  clear equil

Plot well and initial pressure

clf
show = true(G.cells.num,1);
cellInx = sub2ind(G.cartDims, ...
   [I-1; I-1; I; I;   I(1:2)-1], ...
   [J  ; J;   J; J;   nperf+[2;2]], ...
   [K-1; K;   K; K-1; K(1:2)-[0; 1]]);
show(cellInx) = false;
plotCellData(G,p_init/barsa, show,'EdgeColor','k');
plotWell(G,W, 'height',10);
view(-125,20), camproj perspective
_images/singlePhaseAD_04.png

Compute transmissibilities

N  = double(G.faces.neighbors);
intInx = all(N ~= 0, 2);
N  = N(intInx, :);                          % Interior neighbors
hT = computeTrans(G, rock);                 % Half-transmissibilities
cf = G.cells.faces(:,1);
nf = G.faces.num;
T  = 1 ./ accumarray(cf, 1 ./ hT, [nf, 1]); % Harmonic average
T  = T(intInx);                             % Restricted to interior

Define discrete operators

n = size(N,1);
C = sparse( [(1:n)'; (1:n)'], N, ones(n,1)*[-1 1], n, G.cells.num);
grad = @(x)C*x;
div  = @(x)-C'*x;
avg  = @(x) 0.5 * (x(N(:,1)) + x(N(:,2)));
spy(C)
_images/singlePhaseAD_05.png

Define flow equations

gradz  = grad(G.cells.centroids(:,3));
v      = @(p)  -(T/mu).*( grad(p) - g*avg(rho(p)).*gradz );
presEq = @(p,p0,dt) (1/dt)*(pv(p).*rho(p) - pv(p0).*rho(p0)) ...
                      + div( avg(rho(p)).*v(p) );

Define well equations

wc = W(1).cells; % connection grid cells
WI = W(1).WI;    % well-indices
dz = W(1).dZ;    % connection depth relative to bottom-hole

p_conn  = @(bhp)  bhp + g*dz.*rho(bhp); %connection pressures
q_conn  = @(p,bhp) WI .* (rho(p(wc)) / mu) .* (p_conn(bhp) - p(wc));

rateEq = @(p,bhp,qS)  qS-sum(q_conn(p, bhp))/rhoS;
ctrlEq = @(bhp)       bhp-100*barsa;

Initialize for solution loop

[p_ad, bhp_ad, qS_ad] = initVariablesADI(p_init, p_init(wc(1)), 0);
nc = G.cells.num;
[pIx, bhpIx, qSIx] = deal(1:nc, nc+1, nc+2);

numSteps = 52;                  % number of time-steps
totTime  = 365*day;             % total simulation time
dt       = totTime / numSteps;  % constant time step
tol      = 1e-5;                % Newton tolerance
maxits   = 10;                  % max number of Newton its

sol = repmat(struct('time',[],'pressure',[],'bhp',[],'qS',[]),[numSteps+1,1]);
sol(1)  = struct('time', 0, 'pressure', value(p_ad), ...
   'bhp', value(bhp_ad), 'qS', value(qS_ad));

Main loop

t = 0; step = 0;
hwb = waitbar(t,'Simulation ..');
while t < totTime
   t = t + dt;
   step = step + 1;
   fprintf('\nTime step %d: Time %.2f -> %.2f days\n', ...
      step, convertTo(t - dt, day), convertTo(t, day));
   % Newton loop
   resNorm = 1e99;
   p0  = value(p_ad); % Previous step pressure
   nit = 0;
   while (resNorm > tol) && (nit <= maxits)
      % Add source terms to homogeneous pressure equation:
      eq1     = presEq(p_ad, p0, dt);
      eq1(wc) = eq1(wc) - q_conn(p_ad, bhp_ad);
      % Collect all equations
      eqs = {eq1, rateEq(p_ad, bhp_ad, qS_ad), ctrlEq(bhp_ad)};
      % Concatenate equations and solve for update:
      eq  = cat(eqs{:});
      J   = eq.jac{1};  % Jacobian
      res = eq.val;     % residual
      upd = -(J \ res); % Newton update
      % Update variables
      p_ad.val   = p_ad.val   + upd(pIx);
      bhp_ad.val = bhp_ad.val + upd(bhpIx);
      qS_ad.val  = qS_ad.val  + upd(qSIx);

      resNorm = norm(res);
      nit     = nit + 1;
      fprintf('  Iteration %3d:  Res = %.4e\n', nit, resNorm);
   end

   if nit > maxits
      error('Newton solves did not converge')
   else % store solution
      sol(step+1)  = struct('time', t, 'pressure', value(p_ad), ...
                            'bhp', value(bhp_ad), 'qS', value(qS_ad));
      waitbar(t/totTime,hwb);
   end
end
close(hwb);
Time step 1: Time 0.00 -> 7.02 days
  Iteration   1:  Res = 1.0188e+07
  Iteration   2:  Res = 3.1032e-02
  Iteration   3:  Res = 1.8547e-05
  Iteration   4:  Res = 5.4513e-12

Time step 2: Time 7.02 -> 14.04 days
...

Plot production rate and pressure decay

clf,
[ha,hr,hp] = plotyy(...
   [sol(2:end).time]/day, -[sol(2:end).qS]*day, ...
   [sol(2:end).time]/day, mean([sol(2:end).pressure]/barsa), 'stairs', 'plot');
set(ha,'FontSize',16);
set(hr,'LineWidth', 2);
set(hp,'LineStyle','none','Marker','o','LineWidth', 1);
set(ha(2),'YLim',[100 210],'YTick',100:50:200);
xlabel('time [days]');
ylabel(ha(1), 'rate [m^3/day]');
ylabel(ha(2), 'avg pressure [bar]');
_images/singlePhaseAD_06.png

Plot pressure evolution

lf;
steps = [2 5 10 20];
for i=1:4
   subplot(2,2,i);
   set(gca,'Clipping','off');
   plotCellData(G, sol(steps(i)).pressure/barsa, show,'EdgeColor',.5*[1 1 1]);
   plotWell(G,W);
   view(-125,20), camproj perspective
   caxis([115 205]);
   axis tight off; zoom(1.1)
   text(200,170,-8,[num2str(round(steps(i)*dt/day)) ' days'],'FontSize',14);
end
h=colorbar('horiz','Position',[.1 .05 .8 .025]);
colormap(jet(55));
_images/singlePhaseAD_07.png

Pressure-dependent viscosity

Generated from singlePhaseMuP.m

In this example, we will demonstrate how one can easily extend the compressible single-phase pressure solver to include the effect of pressure-dependent viscosity using either arithmetic averaging of the viscosity or harmonic averaging of the fluid mobility.

Define geometric quantitites

Grid that represents the reservoir geometry

[nx,ny,nz] = deal( 10,  10, 10);
[Lx,Ly,Lz] = deal(200, 200, 50);
G = cartGrid([nx, ny, nz], [Lx, Ly, Lz]);
G = computeGeometry(G);

% Discrete operators
N  = double(G.faces.neighbors);
intInx = all(N ~= 0, 2);
N  = N(intInx, :);
n = size(N,1);
C = sparse( [(1:n)'; (1:n)'], N, ones(n,1)*[-1 1], n, G.cells.num);
grad = @(x)C*x;
div  = @(x)-C'*x;
avg  = @(x) 0.5 * (x(N(:,1)) + x(N(:,2)));

Rock model and transmissibilities

rock = makeRock(G, 30*milli*darcy, 0.3);

mrstModule add spe10
rock = getSPE10rock(41:50,101:110,1:10);

cr   = 1e-6/barsa;
p_r  = 200*barsa;
pv_r = poreVolume(G, rock);
pv   = @(p) pv_r .* exp( cr * (p - p_r) );

hT = computeTrans(G, rock);
cf = G.cells.faces(:,1);
nf = G.faces.num;
T  = 1 ./ accumarray(cf, 1 ./ hT, [nf, 1]);
T  = T(intInx);

Fluid model

mu0   = 5*centi*poise;
c     = 1e-3/barsa;
rho_r = 850*kilogram/meter^3;
rhoS  = 750*kilogram/meter^3;
rho   = @(p) rho_r .* exp( c * (p - p_r) );

gravity reset on, g = norm(gravity);
[z_0, z_max] = deal(0, max(G.cells.centroids(:,3)));
equil  = ode23(@(z,p) g .* rho(p), [z_0, z_max], p_r);
p_init = reshape(deval(equil, G.cells.centroids(:,3)), [], 1);  clear equil

Assume a single horizontal well

nperf = 8;
I = repmat(2, [nperf, 1]);
J = (1 : nperf).' + 1;
K = repmat(5, [nperf, 1]);
cellInx = sub2ind(G.cartDims, I, J, K);
W = addWell([ ], G, rock, cellInx, 'Name', 'P1', 'Dir', 'x' );

Main loop

numSteps = 52;
totTime  = 365*day;
dt       = totTime / numSteps;
tol      = 1e-5;
maxits   = 10;
mu_const = [0 2e-3 5e-3]/barsa;
[mypres,myrate] = deal(nan(numSteps+1,2*numel(mu_const)));

n = 1;
for method=1:2

   for i=1:numel(mu_const)

      mu = @(p) mu0*(1+mu_const(i)*(p-p_r));

      gradz = grad(G.cells.centroids(:,3));
      switch method
         case 1
            v  = @(p)  -(T./mu(avg(p))).*( grad(p) - g*avg(rho(p)).*gradz );
         case 2
            hf2cn = getCellNoFaces(G);
            nhf = numel(hf2cn);
            hf2f  = sparse(double(G.cells.faces(:,1)),(1:nhf)',1);
            hf2if = hf2f(intInx,:);
            hlam = @(mu,p) 1./(hf2if*(mu(p(hf2cn))./hT));
            %
            v  = @(p) -hlam(mu,p).*( grad(p) - g*avg(rho(p)).*gradz );
      end

      presEq = @(p, p0, dt) (1/dt)*(pv(p).*rho(p) - pv(p0).*rho(p0)) ...
         + div( avg(rho(p)).*v(p) );

      % Define well equations
      wc = W(1).cells; % connection grid cells
      WI = W(1).WI;    % well-indices
      dz = W(1).dZ;    % connection depth relative to bottom-hole

      p_conn  = @(bhp)  bhp + g*dz.*rho(bhp); %connection pressures
      q_conn  = @(p,bhp) WI .* (rho(p(wc)) ./ mu(p(wc))) .* (p_conn(bhp) - p(wc));

      rateEq = @(p,bhp,qS)  qS-sum(q_conn(p, bhp))/rhoS;
      ctrlEq = @(bhp)       bhp-100*barsa;

      % Initialize for solution loop
      [p_ad, bhp_ad, qS_ad] = initVariablesADI(p_init, p_init(wc(1)), 0);
      nc = G.cells.num;
      [pIx, bhpIx, qSIx] = deal(1:nc, nc+1, nc+2);
      sol = repmat(struct('time',[],'pressure',[],'bhp',[],'qS',[]),[numSteps+1,1]);
      sol(1)  = struct('time', 0, 'pressure', value(p_ad), ...
         'bhp', value(bhp_ad), 'qS', value(qS_ad));

      % Time loop
      t = 0; step = 0;
      hwb = waitbar(0,'Simulation ..');
      while t < totTime
         t = t + dt;
         step = step + 1;
         fprintf('Time step %d: Time %.2f -> %.2f days\n', ...
            step, convertTo(t - dt, day), convertTo(t, day));
         % Newton loop
         resNorm = 1e99;
         p0  = value(p_ad); % Previous step pressure
         nit = 0;
         while (resNorm > tol) && (nit <= maxits)
            % Add source terms to homogeneous pressure equation:
            eq1     = presEq(p_ad, p0, dt);
            eq1(wc) = eq1(wc) - q_conn(p_ad, bhp_ad);
            % Collect all equations
            eqs = {eq1, rateEq(p_ad, bhp_ad, qS_ad), ctrlEq(bhp_ad)};
            % Concatenate equations and solve for update:
            eq  = cat(eqs{:});
            J   = eq.jac{1};  % Jacobian
            res = eq.val;     % residual
            upd = -(J \ res); % Newton update
            % Update variables
            p_ad.val   = p_ad.val   + upd(pIx);
            bhp_ad.val = bhp_ad.val + upd(bhpIx);
            qS_ad.val  = qS_ad.val  + upd(qSIx);

            resNorm = norm(res);
            nit     = nit + 1;
          end

         if nit > maxits
            error('Newton solves did not converge')
         else % store solution
            sol(step+1)  = struct('time', t, 'pressure', value(p_ad), ...
               'bhp', value(bhp_ad), 'qS', value(qS_ad));
           waitbar(t/totTime,hwb)
         end
      end
      close(hwb)
      myrate(2:end,n) = -[sol(2:end).qS]*day;
      mypres(:,n) = mean([sol(:).pressure]/barsa);
      mytime = [sol(1:end).time]/day;
      n = n+1;
   end
end
Time step 1: Time 0.00 -> 7.02 days
Time step 2: Time 7.02 -> 14.04 days
Time step 3: Time 14.04 -> 21.06 days
Time step 4: Time 21.06 -> 28.08 days
Time step 5: Time 28.08 -> 35.10 days
Time step 6: Time 35.10 -> 42.12 days
Time step 7: Time 42.12 -> 49.13 days
Time step 8: Time 49.13 -> 56.15 days
...
igure(1);
myrate(1,:)=NaN;
stairs(mytime, myrate(:,1:3),'LineWidth',2);
set(gca,'FontSize',16);
xlabel('time [days]'); ylabel('rate [m^3/day]');
legend('c_\mu=0', 'c_\mu=0.002', 'c_\mu=0.005');

figure(2);
plot(mytime, mypres(:,1:3),'o');
set(gca,'FontSize',16);
xlabel('time [days]'); ylabel('avg pressure [bar]');
legend('c_\mu=0', 'c_\mu=0.002', 'c_\mu=0.005');

figure(3);
myrate(1,:)=0;
plot(mytime, cumsum(myrate(:,1:3)),'LineWidth',2);
set(gca,'FontSize',16);
xlabel('time [days]'); ylabel('cummulative product [m^3]');
legend('c_\mu=0', 'c_\mu=0.002', 'c_\mu=0.005', 'Location', 'SouthEast');

figure(4);
p = [100*barsa,205*barsa];
mup = zeros(numel(p), numel(mu_const));
for i=1:numel(mu_const)
   mu = @(p) mu0*(1+mu_const(i)*(p-p_r));
   mup(:,i) = mu(p);
end
plot(convertTo(p,barsa),convertTo(mup,centi*poise),'LineWidth',2);
set(gca,'FontSize',16);
xlabel('pressure [bar]'); ylabel('viscosity [cP]');
legend('c_\mu=0', 'c_\mu=0.002', 'c_\mu=0.005', 'Location', 'SouthEast');
axis tight
_images/singlePhaseMuP_01.png
_images/singlePhaseMuP_02.png
_images/singlePhaseMuP_03.png
_images/singlePhaseMuP_04.png

Single-phase compressible AD solver with thermal effects

Generated from singlePhaseThermal.m

The purpose of the example is to give demonstrate rapid prototying in MRST using the automatic differentiation (AD) class. To this end, we extend the flow simulator for compressible single-phase flow to include temperature effects, which are modeled by incorporating a second conservation equation for energy. Except for this, the computational setup is the same with a single horizontal well draining fluids from a simple box-geometry reservoir.

mrstModule add ad-core

Set up model: grid, perm and poro

[nx,ny,nz] = deal( 10,  10, 10);
[Dx,Dy,Dz] = deal(200, 200, 50);
G = cartGrid([nx, ny, nz], [Dx, Dy, Dz]);
G = computeGeometry(G);

Define rock model

rock = makeRock(G, 30*milli*darcy, 0.3);

cr   = 1e-6/barsa;
p_r  = 200*barsa;
pv_r = poreVolume(G, rock);
pv   = @(p) pv_r .* exp( cr * (p - p_r) );
sv   = @(p) G.cells.volumes - pv(p);

Define model for compressible fluid

mu0   = 5*centi*poise;
cmup  = 2e-3/barsa;
cmut  = 1e-3;
T_r   = 300;
mu    = @(p,T)  mu0*(1+cmup*(p-p_r)).*exp(-cmut*(T-T_r));

beta  = 1e-3/barsa;
alpha = 5e-3;
rho_r = 850*kilogram/meter^3;
rho_S = 750*kilogram/meter^3;
rho   = @(p,T) rho_r .* (1+beta*(p-p_r)).*exp(-alpha*(T-T_r) );

Quantities for energy equation

Cp = 4e3;
Cr = 2*Cp;
Hf = @(p,T) Cp*T+(1-T_r*alpha).*(p-p_r)./rho(p,T);
Ef = @(p,T) Hf(p,T) - p./rho(p,T);
Er = @(T)   Cp*T;

Assume a single horizontal well

nperf = floor(G.cartDims(2)*(ny-2)/ny);
I = repmat(2, [nperf, 1]);
J = (1 : nperf).' + 1;
K = repmat(nz/2, [nperf, 1]);
cellInx = sub2ind(G.cartDims, I, J, K);
W = addWell([ ], G, rock, cellInx, 'Name', 'P1', 'Dir', 'y');

Impose vertical equilibrium

gravity reset on; g = norm(gravity);
[z_0, z_max] = deal(0, max(G.cells.centroids(:,3)));
equil  = ode23(@(z,p) g.* rho(p,T_r), [z_0, z_max], p_r);
p_init = reshape(deval(equil, G.cells.centroids(:,3)), [], 1);  clear equil
T_init = ones(G.cells.num,1)*T_r;

Plot well and initial pressure

figure
show = true(G.cells.num,1);
cellInx = sub2ind(G.cartDims, ...
   [I-1; I-1; I; I;   I(1:2)-1], ...
   [J  ; J;   J; J;   nperf+[2;2]], ...
   [K-1; K;   K; K-1; K(1:2)-[0; 1]]);
show(cellInx) = false;
plotCellData(G,p_init/barsa, show,'EdgeColor','k');
plotWell(G,W, 'height',10);
view(-125,20), camproj perspective
_images/singlePhaseThermal_01.png

Compute transmissibilities

N   = double(G.faces.neighbors);
intInx = all(N ~= 0, 2);
cf  = G.cells.faces(:,1);
nf  = G.faces.num;
N   = N(intInx, :);                           % Interior neighbors

hT  = computeTrans(G, rock);                  % Half-transmissibilities
Tp  = 1 ./ accumarray(cf, 1 ./ hT, [nf, 1]);  % Harmonic average
Tp  = Tp(intInx);                             % Restricted to interior

kap = 4;                                      % Heat conduction of granite
tmp = struct('perm',kap*ones(G.cells.num,1)); % Temporary rock object
hT  = computeTrans(G, tmp);                   % Half-transmissibilities
Th  = 1 ./ accumarray(cf, 1 ./ hT, [nf, 1]);  % Harmonic average
Th  = Th(intInx);                             % Restricted to interior

Define discrete operators

n    = size(N,1);
C    = sparse( [(1:n)'; (1:n)'], N, ones(n,1)*[-1 1], n, G.cells.num);
grad = @(x) C*x;
div  = @(x) -C'*x;
avg  = @(x) 0.5 * (x(N(:,1)) + x(N(:,2)));
upw  = @(x,flag) x(N(:,1)).*double(flag)+x(N(:,2)).*double(~flag);

Define flow equations

Writing in functional form means that v(p,T) is evaluated two times more than strictly needed if the whole definition of discrete equations written as a single function

gdz = grad(G.cells.centroids)*gravity()';
v   = @(p,T)  -(Tp./mu(avg(p),avg(T))).*(grad(p) - avg(rho(p,T)).*gdz);
pEq = @(p,T, p0,T0, dt) ...
     (1/dt)*(pv(p).*rho(p,T) - pv(p0).*rho(p0,T0)) ...
      + div( avg(rho(p,T)).*v(p,T) );
hEq = @(p, T, p0, T0, dt) ...
     (1/dt)*(pv(p ).*rho(p, T ).*Ef(p ,T ) + sv(p ).*Er(T ) ...
           - pv(p0).*rho(p0,T0).*Ef(p0,T0) - sv(p0).*Er(T0)) ...
     + div( upw(Hf(p,T),v(p,T)>0).*avg(rho(p,T)).*v(p,T) ) ...
     + div( -Th.*grad(T));

Define well equations.

wc = W(1).cells; % connection grid cells
assert(numel(wc)==numel(unique(wc))); % each cell should only appear once
WI  = W(1).WI;    % well-indices
dz  = W(1).dZ;    % connection depth relative to bottom-hole
bhT = ones(size(wc))*200; % temperature of wells not used if production

p_conn  = @(bhp,bhT) bhp + g*dz.*rho(bhp,bhT); %connection pressures
q_conn  = @(p,T, bhp) ...
    WI .* (rho(p(wc),T(wc))./mu(p(wc),T(wc))) .* (p_conn(bhp,bhT)-p(wc));

rateEq = @(p,T, bhp, qS)  qS-sum(q_conn(p, T, bhp))/rho_S;
ctrlEq = @(bhp)           bhp-100*barsa;

Initialize for solution loop

[p_ad, T_ad, bhp_ad, qS_ad] = ...
    initVariablesADI(p_init,T_init, p_init(wc(1)), 0);
nc = G.cells.num;
[pIx, TIx, bhpIx, qSIx] = deal(1:nc, nc+1:2*nc, 2*nc+1, 2*nc+2);

numSteps = 78;                  % number of time-steps
totTime  = 365*day*1.5;         % total simulation time
dt       = totTime / numSteps;  % constant time step
tol      = 1e-5;                % Newton tolerance
maxits   = 10;                  % max number of Newton its

sol = repmat(struct('time',[], 'pressure',[], 'bhp',[], 'qS',[], ...
    'T',[], 'qH',[]),[numSteps+1,1]);
sol(1)  = struct('time', 0, 'pressure', value(p_ad), ...
    'bhp', value(bhp_ad), 'qS', value(qS_ad), 'T', value(T_ad),'qH', 0);

Main loop

t = 0; step = 0;
hwb = waitbar(0,'Simulation..');
while t < totTime
   t = t + dt;
   step = step + 1;
   fprintf('\nTime step %d: Time %.2f -> %.2f days\n', ...
      step, convertTo(t - dt, day), convertTo(t, day));

   % Newton loop
   resNorm = 1e99;
   p0  = value(p_ad); % Previous step pressure
   T0  = value(T_ad);
   nit = 0;
   while (resNorm > tol) && (nit < maxits)

      % Add source terms to homogeneous pressure equation:
      eq1      = pEq(p_ad,T_ad, p0, T0,dt);
      qw       = q_conn(p_ad,T_ad, bhp_ad);
      eq1(wc)  = eq1(wc) - qw;
      hq       = Hf(bhp_ad,bhT).*qw;    %inflow not in this example
      Hcells   = Hf(p_ad,T_ad);
      hq(qw<0) = Hcells(wc(qw<0)).*qw(qw<0);
      eq2      = hEq(p_ad,T_ad, p0, T0,dt);
      eq2(wc)  = eq2(wc) - hq;

      % Collect all equations. Scale residual of energy equation
      eqs = {eq1, eq2/Cp, rateEq(p_ad,T_ad, bhp_ad, qS_ad), ctrlEq(bhp_ad)};

      % Concatenate equations and solve for update:
      eq  = cat(eqs{:});
      J   = eq.jac{1};  % Jacobian
      res = eq.val;     % residual
      upd = -(J \ res); % Newton update

      % Update variables
      p_ad.val   = p_ad.val   + upd(pIx);
      T_ad.val   = T_ad.val   + upd(TIx);
      bhp_ad.val = bhp_ad.val + upd(bhpIx);
      qS_ad.val  = qS_ad.val  + upd(qSIx);

      resNorm = norm(res);
      nit     = nit + 1;
      fprintf('  Iteration %3d:  Res = %.4e\n', nit, resNorm);
   end

   if nit > maxits
      error('Newton solves did not converge')
   else % store solution
      sol(step+1)  = struct('time', t, 'pressure', value(p_ad), ...
                            'bhp', value(bhp_ad), 'qS', value(qS_ad), ...
                          'T', value(T_ad), 'qH', sum(value(hq)));
      waitbar(t/totTime,hwb);
   end
end
close(hwb)
Time step 1: Time 0.00 -> 7.02 days
  Iteration   1:  Res = 1.0188e+07
  Iteration   2:  Res = 2.9454e+02
  Iteration   3:  Res = 3.9794e+02
  Iteration   4:  Res = 5.8886e+01
  Iteration   5:  Res = 1.3027e+00
  Iteration   6:  Res = 5.9547e-04
...

Plot production rate

clf, set(gca,'FontSize',20);
stairs([sol(2:end).time]/day,-[sol(2:end).qS]*day,'LineWidth',2);
xlabel('days');ylabel('m^3/day')
_images/singlePhaseThermal_02.png

Plot pressure evolution

figure; clf;
steps = [2 5 10 25];
%steps = floor(1:(numel(sol)/6-eps):numel(sol));
p = vertcat(sol(:).pressure);
cax=[min(p) max(p)]./barsa;
for i=1:4
   subplot(2,2,i);
   set(gca,'Clipping','off');
   plotCellData(G, sol(steps(i)).pressure/barsa, show, 'EdgeColor',.5*[1 1 1]);
   plotWell(G, W, 'FontSize',12);
   view(-125,20), camproj perspective
   caxis(cax);
   axis tight off; zoom(1.1)
   text(200,170,-8,[num2str(round(steps(i)*dt/day)) ' days'],'FontSize',12);
end
colorbar('horiz','Position',[.1 .05 .8 .025]);
colormap(jet(55));
_images/singlePhaseThermal_03.png
figure(),clf;
steps = [2 5 10 numel(sol)];
T = vertcat(sol(:).T); %-T_r;
cax=[min(T) max(T)];
for i=1:numel(steps)
   subplot(2,2,i);
   set(gca,'Clipping','off');
   plotCellData(G, sol(steps(i)).T, show, 'EdgeColor',.5*[1 1 1]);
   plotWell(G, W, 'FontSize',12);
   view(-125,20), camproj perspective
   caxis(cax);
   axis tight off; zoom(1.1)
   text(200,170,-8,[num2str(round(steps(i)*dt/day)) ' days'],'FontSize',12);
end
h=colorbar('horiz','Position',[.1 .05 .8 .025]);
colormap(jet(55));
_images/singlePhaseThermal_04.png
ns = numel(sol);
Tw = nan(ns,numel(wc));
Pw = nan(ns,numel(wc));
[t,Pm,PM,Pa,Tm,TM,Ta] = deal(nan(ns,1));
for i=1:numel(sol)
   t(i) = sol(i).time/day;
   Tw(i,:)=sol(i).T(wc);
   Tm(i) = min(sol(i).T);
   TM(i) = max(sol(i).T);
   Ta(i) = mean(sol(i).T);
   Pw(i,:)=sol(i).pressure(wc)./barsa;
   Pm(i) = min(sol(i).pressure./barsa);
   PM(i) = max(sol(i).pressure./barsa);
   Pa(i) = mean(sol(i).pressure./barsa);
end
figure; plot(t,Pm,t,Pa,t,PM,t,Pw,'.k','LineWidth',2);
legend('min(p)', 'avg(p)', 'max(p)', 'wells');
figure; plot(t,Tm,t,Ta,t,TM,t,Tw,'.k','LineWidth',2);
legend('min(T)', 'avg(T)', 'max(T)', 'wells');
_images/singlePhaseThermal_05.png
_images/singlePhaseThermal_06.png

Compute the three different expansion temperatures

[p,T] = initVariablesADI(p_r,T_r);
dp   = Pm(end)*barsa-p_r;

% Joule-Thomson
hf   = Hf(p,T);
dHdp = hf.jac{1};
dHdT = hf.jac{2};
Tjt  = T_r - dHdp*dp/dHdT;
hold on, plot(t([1 end]), [Tjt Tjt],'--k'); hold off
text(t(5), Tjt+.25, 'Joule-Tompson');

% linearized adiabatic temperature
hf   = Ef(p,T) + value(p)./rho(p,T);
dHdp = hf.jac{1};
dHdT = hf.jac{2};
Tab  = T_r - dHdp*dp/dHdT;
hold on, plot(t([1 end]), [Tab Tab],'--k'); hold off
text(t(2), Tab-.35, 'Adiabatic expansion');

% free expansion
hf = Ef(p,T);
dHdp = hf.jac{1};
dHdT = hf.jac{2};
Tfr  = T_r - dHdp*dp/dHdT;
hold on, plot(t([1 end]), [Tfr Tfr],'--k'); hold off
text(t(end/2), Tfr+.25, 'Free expansion');
set(gca,'XLim',t([1 end]));
_images/singlePhaseThermal_07.png
%{

Lorenz coefficient for layers of SPE 10, Model 2

Generated from computeLorenzSPE10.m

In this example, we first compute the Lorenz coefficient for all layers of the SPE10 model subject to a five-spot well pattern. We then pick one of the layers and show how we can balance the well allocation and improve the Lorenz coefficient and the areal sweep by moving some of the wells to regions with better sand quality.

mrstModule add diagnostics spe10 incomp

Base model

We set up a grid. Later, we will assign petrophysical properties from one single layer at the time to compute Lorenz coefficients inside the main loop

cartDims = [  60,  220,  1];
physDims = [1200, 2200, 2*cartDims(end)] .* ft();
G  = cartGrid(cartDims, physDims);
G  = computeGeometry(G);

% Set parameters describing the
wtype    = {'bhp', 'bhp', 'bhp', 'bhp', 'bhp'};
wtarget  = [200,   200,   200,   200,   500] .* barsa();
wrad     = [0.125, 0.125, 0.125, 0.125, 0.125] .* meter;
wloc     = [  1,   60,     1,   60,  30;
              1,    1,   220,  220, 111];
wname    = {'P1', 'P2', 'P3', 'P4', 'I'};

% Set fluid model: Here, we use a simple fluid model with properties that
% are typical for water. Replace this by a multiphase fluid object if you
% also want to include fluid effects in the calculation
fluid = initSingleFluid('mu', 1*centi*poise, ...
                        'rho', 1014*kilogram/meter^3);

Compute Lorenz coefficient for each layer

Lc = zeros(85,1);
h = waitbar(0,'Computing Lorenz coefficients ...');
for n=1:85

    % --- Set petrophysical data for this particular layer
    % To avoid problems with very small porosity values, we explicitly
    % impose a lower threshold of 1e-4
    rock = getSPE10rock(1:cartDims(1),1:cartDims(2),n);
    rock.poro = max(rock.poro, 1e-4);

    % --- Set up well model
    % To ensure that we get the correct well index when updating the
    % petrophysical data, we simply regenerate the well objects.
    W = [];
    for w = 1 : numel(wtype)
        W = verticalWell(W, G, rock, wloc(1,w), wloc(2,w), 1, ...
            'Type', wtype{w}, 'Val', wtarget(w), ...
            'Radius', wrad(w), 'Name', wname{w}, ...
            'InnerProduct', 'ip_tpf', 'Comp_i', 1);
    end

    % --- Initiate and solve flow problem
    rS = initState(G, W, 0, 0.0);
    T  = computeTrans(G, rock);
    rS = incompTPFA(rS, G, T, fluid, 'wells', W);

    % --- Compute flow diagnostics
    D       = computeTOFandTracer(rS, G, rock, 'wells', W, 'maxTOF', inf);
    [F,Phi] = computeFandPhi(poreVolume(G,rock), D.tof);
    Lc(n)   = computeLorenz(F,Phi);
    waitbar(n/85);
end
close(h);
clf; set(gcf,'Position',[470 420 900 250]);
h=bar(Lc,'hist');
axis tight; set(h,'FaceColor',[.95 .95 1],'EdgeColor',[0 0 .7]);
hold on, h=plot([35.5 35.5],[0 .75],'--k','LineWidth',2); hold off;
_images/computeLorenzSPE10_01.png

Improve Lorenz/sweep by moving wells

Here, we have first computed Lorenz coefficient, sweep and well-pair connections for the layer with lowest/highest Lorenz coefficient and then tried to move the wells having small allocation factors to the nearest high-poro region that seems reasonably well connected with the injector.

minCase = false;  %#ok<*UNRCH>
if minCase
    [~,n]=min(Lc);
else
    [~,n]=max(Lc);
end
rock = getSPE10rock(1:cartDims(1),1:cartDims(2),n);
rock.poro = max(rock.poro, 1e-4);
pv = poreVolume(G, rock);

nwloc = wloc;
fig1=figure('Position',[250 490 750 300]); col = {'b','g'};
for nstep=1:2
    % Set well conditions
    W = [];
    for w = 1 : numel(wtype)
        W = verticalWell(W, G, rock, nwloc(1,w), nwloc(2,w), 1, ...
            'Type', wtype{w}, 'Val', wtarget(w), ...
            'Radius', wrad(w), 'Name', wname{w}, ...
            'InnerProduct', 'ip_tpf');
    end

    % Compute flow field and diagnostics
    rS = initState(G, W, 0);
    T  = computeTrans(G, rock);
    rS = incompTPFA(rS, G, T, fluid, 'wells', W);
    D  = computeTOFandTracer(rS, G, rock, 'wells', W, 'maxTOF', inf);
    WP = computeWellPairs(rS, G, rock, W, D);
    [F,Phi] = computeFandPhi(pv, D.tof);
    computeLorenz(F,Phi)
    [Ev,tD] = computeSweep(F, Phi);

    % Plot F-Phi and sweep diagram
    % To reduce the number of points, we resample the data
    figure(fig1);
    subplot(1,2,1); hold on;
    xq = linspace(0,1,100);
    vq = interp1(Phi,F,xq);
    plot(xq,vq,['-',col{nstep}],'LineWidth',2); hold off;

    subplot(1,2,2); hold on;
    [T,ia] = unique(tD);
    E = Ev(ia);
    xq = linspace(0,5+(1-minCase)*20,100);
    vq = interp1(T,E,xq);
    plot(xq,vq,['-',col{nstep}],'LineWidth',2); hold off;

    % Plot porosity map and well-pair connections
    figure('Position',[710   420   720   400]);
    plotCellData(G,rock.poro,'EdgeColor','none');
    plotWell(G,W,'height',10,'LineWidth',4);
    plotWellPairConnections(G,WP,D,W,pv,1e-4);
    cmap=jet(128); colormap(.4*cmap + .6*ones(size(cmap))); clear cmap;
    view(0,70); set(gca,'DataAspectRatio',[1 1 .5]); axis off
    cax = caxis;

    % Plot zoom around each producer in separate axes
    pos = [.05 .05 .25 .35;  .725 .05 .25 .35; ...
        .05 .6 .25 .35; .725 .6 .25 .35];
    for i=1:4
        axes('position', pos(i,:));
        if nstep==1
            rad(:,i) = sum(bsxfun(@minus,G.cells.centroids,....
                G.cells.centroids(W(i).cells,:)).^2,2); %#ok<SAGROW>
        end
        plotCellData(G,rock.poro,rad(:,i)<3500,'EdgeColor','k','EdgeAlpha',.1);
        plotWell(G,W(i),'height',10,'LineWidth',10);
        caxis(cax);
        view(0,70); set(gca,'DataAspectRatio',[1 1 .5]); axis off tight;
    end

    % Impose manually improved well positions for next pass
    if minCase
        nwloc     = [  1,   53,     1,   59,  30;
                       1,    2,   218,  220, 111];
    else
        nwloc     = [  1,   60,     7,   60,  30;
                       6,   11,   220,  219, 111];
    end
    vertcat(rS.wellSol.flux)
end
figure(fig1);
subplot(1,2,1); hold on; plot([0 1],[0 1],'k'); hold off; title('Lorenz');
subplot(1,2,2); set(gca,'XLim',[0 5+(1-minCase)*20]); title('Sweep');
Warning: Inconsistent Number of Phases.  Using 1 Phase (=min([3, 1, 1])).

ans =

    0.7820


ans =
...
_images/computeLorenzSPE10_02.png
_images/computeLorenzSPE10_03.png
_images/computeLorenzSPE10_04.png

Example: Interactive Diagnostics Tool for the SAIGUP Model

Generated from interactiveSAIGUP.m

In this example, we will use the SAIGUP model as set up in the ‘saigupWithWells’ example to show how to launch an interactive session for doing flow diagnostics.

mrstModule add book diagnostics;

Set up model

We start by re-running this case study to set up the simulation model and compute a flow field. Henceforth, we only need the geological model, the description of the wells, and the reservoir state. Hence, we clear all other variables and close all plots produced by the script.

saigupWithWells;
close all;
clearvars -except G rock W state

% Get rid of LaTeX notation in well names
% In the original example, we used LaTeX syntax to specify well names of
% the form $I_1$, $P_1$, etc. Unfortunately, this does not play very well
% along with MATLAB's tools for  graphical interfaces, and we therefore
% post-process the well names to be on the form I1, P1, etc.
for i=1:numel(W)
    W(i).name = W(i).name([1 5]);
end

Launch flow diagnostics

interactiveDiagnostics(G, rock, W, 'state', state, 'computeFlux', false);
set(gcf,'position',[440 317 866 480]); axis normal
view(-85,73)
New state encountered, computing diagnostics...
_images/interactiveSAIGUP_01.png
_images/interactiveSAIGUP_02.png

Flux allocation plots

To produce the flux and well allocation figure in the book: - in the ‘region selection’ tab: set maxTOF to zero to remove plotting of volumetric quantities - in the ‘advanced’ tab: select ‘show grid’, ‘well pairs’, and ‘plot all wells’. This will produce the overview of the reservoir - use your pointer (mouse) to click on any of the wells to bring up the allocation plots

Snapshots of swept regions

In the ‘advanced’ tab, deselect ‘show grid’, ‘well pairs’, and ‘plot all wells’. Then, in the ‘region selection’ tab: - set ‘selection’ to ‘intersection’ - set ‘display’ to ‘tracer selected injector’ - select wells (mark I6 and P2 to P6, or I4 and P1 to P4) - type ‘shading faceted’ on the command line - rotation, zoom, and translate the plot to get an acceptable view

Simple 2D reservoir with two injectors and three producers

Generated from interactiveSimple.m

This is an examples discussed in the MRST book, which is a continuation of the ‘showDiagnostBasics’ example, in which we have added two low-porosity regions as a simple representation of two sealing faults, moved the two injectors slightly, and switched all wells from rate to bottom-hole pressure control. In the following, we give instructions for how you can use the diagnostic tool to manipulate the well controls to improve the overall sweep of the reservoir.

Set up the geomodel and specify wells

[nx,ny] = deal(64);
G = cartGrid([nx,ny,1],[5000,2500,10]);
G = computeGeometry(G);
p = gaussianField(G.cartDims(1:2), [0.2 0.4], [11 3], 2.5);
p(round(2*end/3):end,round(end/3)) = 1e-3;
p(1:round(end/3),round(2*end/3)) = 1e-3;
K = p.^3.*(1.5e-5)^2./(0.81*72*(1-p).^2);
rock = makeRock(G, K(:), p(:));

Set up and solve flow problem, compute diagnostics

n = 12;
W = addWell([],  G, rock, nx*(n-6)+n/2+1, ...
    'Type', 'bhp', 'Comp_i', 1, 'name', 'I1', 'Val', 200*barsa);
W = addWell(W, G, rock, nx*(n-6)+n/2+1+nx-n, ...
    'Type','bhp',  'Comp_i', 1, 'name', 'I2', 'Val', 200*barsa);
W = addWell(W, G, rock, round(G.cells.num-(n/2-.5)*nx-.3*nx), ...
    'Type','bhp',  'Comp_i', 0, 'name', 'P1', 'Val', 100*barsa);
W = addWell(W, G, rock, G.cells.num-(n-.5)*nx, ...
    'Type','bhp',  'Comp_i', 0, 'name', 'P2', 'Val', 100*barsa);
W = addWell(W, G, rock, round(G.cells.num-(n/2-.5)*nx+.3*nx), ...
    'Type','bhp',  'Comp_i', 0, 'name', 'P3', 'Val', 100*barsa);
close all;
mrstModule add diagnostics
interactiveDiagnostics(G, rock, W, 'showGrid', true);
axis normal tight; view(0,90);
New state encountered, computing diagnostics...
_images/interactiveSimple_01.png
_images/interactiveSimple_02.png

Plot of reservoir model:

Plot of time evolution

  • Set display to be ‘forward TOF’, and selection to be ‘Flood volumes’ - set ‘Max TOF’ to be 25, 75, 125, and 175 - or use the ‘Play TOF’ button to play the evolution of neutral displacement fronts

Manual optimization of flow pattern

First, change the colormap to

colormap(gray.^10);
% and then switch the displayed property to be 'sum of tofs'

% From the 'plots' tab, you can first plot the F-Phi diagram, and then say
figure(3); hold on
% to ensure that if you push this button subsequently, the new F-Phi
% diagram will be added on top of the preexisting one(s) rather than
% replacing it. (Depending upon what you have done before you come to this
% point, the number of the figure may be different. Check the window header
% to get the right number.)

% Case 1:
% Use the 'edit wells' button from the 'plots' tab to edit the various
% wells. We can start by increasing the pressure of 'P2' from 100 bar to
% 130 bar to decrease the flow rate in the I1-P2 region. Once you have
% entered the new value (13000000), you push 'apply' and a new flow field
% with flow diagnostics will be computed. You can now change the displayed
% property to be 'sum of TOFs', and use the 'F/Phi diagram' button from the
% 'plots' tab to plot the F-Phi diagram and compute the Lorenz coefficient
% of the new well setup. (To distinguish the curves, you may want to
% manually change the color of the new graph using the 'Edit plot' tool
% from the plotting window.)

% Case 2:
% To increase the flow in the I2-P3 region, we can repeat the exercise and
% set P3 to operate at 80 bar.

% Case 3:
% To also sweep the large unswept region east of P3, we can introduce a new
% well in the south-east of this region, just north of the sealing fault.
% To do this, we use the 'add' button from the 'edit wells' workflow. This
% will bring up a new window in which you can click the cell where you want
% to place the well. When you click a cell, a new window will pop up; this
% can be ignored. Once you are happy with the placement, you close the
% editing window, and then you can sett the correct value in the new well.
% In the book, we propose to set the new well to operate at 140 bar, and
% similarly increase the pressure of I2 to 220 bar. Then you push 'apply',
% go back to view the 'sum of TOFs' and produce a new F-Phi diagram
_images/interactiveSimple_03.png
_images/interactiveSimple_04.png

Plot of time evolution

  • Set display to be ‘forward TOF’, and selection to be ‘Flood volumes’ - set ‘Max TOF’ to be 25, 75, 125, and 175 - or use the ‘Play TOF’ button to play the evolution of neutral displacement fronts

Exercise:

Can you make any suggestions for further improvements?

Simple Model of an Anticline

Generated from makeAnticlineModel.m

mrstModule add spe10 coarsegrid

Cartesian grid and rock parameters

[nx,ny,nz] = deal(30,30,15);
G = cartGrid([nx ny nz], [nx ny nz].*[20 10 2]*ft);

rock = getSPE10rock(1:nx,1:ny,nz:-1:1);
rock.poro(rock.poro==0) = 1e-5;
%rock.perm = 10*milli*darcy*ones(G.cells.num,1);
%rock.poro = 0.2*ones(G.cells.num,1);

Make anticline structure

x = G.nodes.coords(:,1);
y = G.nodes.coords(:,2);

normalize = @(x) (x-min(x))/(max(x)-min(x));
x = 2*normalize(x)-1;
y = 2*normalize(y)-1;
r = min(sqrt(x.^2 + y.^2),1);
dz = r.^2 ./ (r.^2 + (1-r).^4);

G.nodes.coords(:,3) = G.nodes.coords(:,3) + dz*(nz+1)*2*ft;
G = computeGeometry(G);

Set initial saturation

s = zeros(G.nodes.num,1);
s(G.nodes.coords(:,3)>=nz*2*ft) = 1;
[nodes,pos] = gridCellNodes(G, (1:G.cells.num).');
c = rldecode(1:G.cells.num, diff(pos), 2).';
A = sparse(c,nodes,1)*[s, ones([G.nodes.num,1])];
s = bsxfun(@rdivide, A(:,1:(end-1)), A(:,end));

Set wells

producers

args = {'Type', 'bhp', 'Val', 100*barsa, 'Comp_i', [0 1]};
W = verticalWell([], G, rock, 14, 15, [], args{:}, 'name', 'P1');
W = verticalWell(W,  G, rock, 17, 16, [], args{:}, 'name', 'P2');

% injectors
args = {'Type', 'bhp', 'Val', 150*barsa, 'Comp_i', [1 0]};
W = verticalWell(W,  G, rock,  9,  9, [], args{:}, 'name', 'I1');
W = verticalWell(W,  G, rock,  9, 22, [], args{:}, 'name', 'I2');
W = verticalWell(W,  G, rock, 22,  9, [], args{:}, 'name', 'I3');
W = verticalWell(W,  G, rock, 22, 22, [], args{:}, 'name', 'I4');
mrstModule add diagnostics incomp streamlines;

Set up and solve flow problem

Generated from showDiagnostBasics.m

[nx,ny] = deal(64);
G = cartGrid([nx,ny,1],[500,250,10]);
G = computeGeometry(G);
p = gaussianField(G.cartDims(1:2), [0.2 0.4], [11 3], 2.5);
K = p.^3.*(1.5e-5)^2./(0.81*72*(1-p).^2);
rock = makeRock(G, K(:), p(:));
hT = computeTrans(G, rock);

Set up and solve flow problem, compute diagnostics

gravity reset off
fluid = initSingleFluid('mu', 1*centi*poise, 'rho', 1014*kilogram/meter^3);
pv  = sum(poreVolume(G,rock));

n = 12;
W = addWell([],  G, rock, nx*n+n+1, ...
    'Type', 'rate', 'Comp_i', 1, 'name', 'I1', 'Val', pv/2);
W = addWell(W, G, rock, nx*n+n+1+nx-2*n, ...
    'Type','rate',  'Comp_i', 1, 'name', 'I2', 'Val', pv/2);
W = addWell(W, G, rock, round(G.cells.num-(n/2-.5)*nx-.3*nx), ...
    'Type','rate',  'Comp_i', 0, 'name', 'P1', 'Val', -pv/4);
W = addWell(W, G, rock, G.cells.num-(n-.5)*nx, ...
    'Type','rate',  'Comp_i', 0, 'name', 'P2', 'Val', -pv/2);
W = addWell(W, G, rock, round(G.cells.num-(n/2-.5)*nx+.3*nx), ...
    'Type','rate',  'Comp_i', 0, 'name', 'P3', 'Val', -pv/4);
state = initState(G, W, 0.0, 1.0);
state = incompTPFA(state, G, hT, fluid, 'wells', W);
D = computeTOFandTracer(state, G, rock, 'wells', W);
Warning: Well rates and flux bc must sum up to 0
when there are no bhp constrained wells or pressure bc.Results may not be
reliable.

Trace streamlines

seed = (nx*ny/2 + (1:nx)).';
Sf = pollock(G, state, seed, 'substeps', 1);
Sb = pollock(G, state, seed, 'substeps', 1, 'reverse', true);

Forward time-of-flight

clf
hf=streamline(Sf);
hb=streamline(Sb);
plotGrid(G,vertcat(W.cells),'FaceColor','none','EdgeColor','r','LineWidth',1.5);
plotWell(G,W,'FontSize',20);
set([hf; hb],'Color','w','LineWidth',1.5);
hd=plotCellData(G,D.tof(:,1),'EdgeColor','none','FaceAlpha',.6);
colormap(parula(32));
axis equal off;
% print -dpng diagnost-ftof.png;
_images/showDiagnostBasics_01.png

Backward time-of-flight

delete(hd);
hd=plotCellData(G,D.tof(:,2),'EdgeColor','none','FaceAlpha',.6);
% print -dpng diagnost-btof.png;
_images/showDiagnostBasics_02.png

Total travel time/residence time

delete(hd);
hd=plotCellData(G,sum(D.tof,2),'EdgeColor','none','FaceAlpha',.6);
% print -dpng diagnost-ttof.png;
_images/showDiagnostBasics_03.png

Tracer from I1

delete(hd);
t = D.itracer(:,1);
hd = plotCellData(G, t, t>1e-6, 'EdgeColor','none','FaceAlpha',.6);
% print -dpng diagnost-C-I1.png;
_images/showDiagnostBasics_04.png

Tracer from P2

delete(hd);
t = D.ptracer(:,2);
hd = plotCellData(G, t, t>1e-6, 'EdgeColor','none','FaceAlpha',.6);
% print -dpng diagnost-C-P2.png;
_images/showDiagnostBasics_05.png

Flooded regions

delete(hd);
hd = plotCellData(G,D.ipart,'EdgeColor','none','FaceAlpha',.6);
% print -dpng diagnost-ipart.png;
_images/showDiagnostBasics_06.png

Drainage regions

delete(hd);
hd = plotCellData(G,D.ppart,'EdgeColor','none','FaceAlpha',.6);
% print -dpng diagnost-ppart.png;
_images/showDiagnostBasics_07.png

Well regions: I1<->P1, I2<->P3

delete(hd);
hd = plotCellData(G,D.ipart, D.ppart~=2, 'EdgeColor','none','FaceAlpha',.6);
% print -dpng diagnost-wreg.png;
_images/showDiagnostBasics_08.png

Compute time-of-flights inside each well region

T = computeTimeOfFlight(state, G, rock, 'wells', W, ...
    'tracer',{W(D.inj).cells},'computeWellTOFs', true);

F-Phi diagram

[F,Phi] = computeFandPhi(poreVolume(G,rock), D.tof);
clf,
plot(Phi,F,'.',[0 1],[0 1],'--'); set(gca,'FontSize',20);
% print -depsc2 diagnost-FPhi.eps;
_images/showDiagnostBasics_09.png

Lorenz coefficient

computeLorenz(F,Phi)
ans =

    0.2604

Sweep effciency diagram

[Ev,tD] = computeSweep(F,Phi);
clf
plot(tD,Ev,'.'); set(gca,'FontSize',20);
% print -depsc2 diagnost-sweep.eps;
_images/showDiagnostBasics_10.png

Compute F-Phi per well-pair region

n = 1; cmap=lines;
pv = poreVolume(G,rock);
leg = {};
clf, hold on
for i=1:numel(D.inj),
    for p=1:numel(D.prod)
        I = (D.ipart==i) & (D.ppart==p);
        if sum(I)==0, continue, end;
        [F,Phi] = computeFandPhi(pv(I),D.tof(I,:));
        plot(Phi(1:4:end),F(1:4:end),'.','Color',cmap(n,:));
        n = n+1;
        leg{n} = sprintf('I%d -> P%d', i, p);
    end
end
hold off
legend(leg{:});
_images/showDiagnostBasics_11.png

Illustrate the use of residence-time distributions

Generated from showRTD.m

In this example, we compute the residence-time distribution for two different layers of the SPE 10 benchmark test. We compare distributions obtained by tracing a delta pulse through the reservoir and obtained directly from the averaged TOF values.

mrstModule add diagnostics spe10 incomp

Base model

We set up a grid. Later, we will assign petrophysical properties from one single layer at the time

cartDims = [  60,  220,  1];
physDims = [1200, 2200, 2*cartDims(end)] .* ft();
G  = cartGrid(cartDims, physDims);
G  = computeGeometry(G);

% Set fluid model: Here, we use a simple fluid model with properties that
% are typical for water. Replace this by a multiphase fluid object if you
% also want to include fluid effects in the calculation
fluid = initSingleFluid('mu', 1*centi*poise, ...
                        'rho', 1014*kilogram/meter^3);

Compute RTD for a single well

[T,layer] = deal(10*year, [23, 75]);
for n=1:2
    % --- Set petrophysical data for this particular layer
    % To avoid problems with very small porosity values, we explicitly
    % impose a lower threshold of 1e-4
    rock = getSPE10rock(1:cartDims(1),1:cartDims(2),layer(n));
    rock.poro = max(rock.poro, 1e-4);

    % --- Set up well model
    pv  = poreVolume(G, rock);
    W = addWell([], G, rock, 1:60, ...
            'Type', 'rate', 'Val', 3*sum(pv)/T, ...
            'Radius', 0.125, 'Name', 'I', ...
            'InnerProduct', 'ip_tpf', 'Comp_i', 1);
    W = addWell(W, G, rock, (60*219+1):(60*220), ...
            'Type', 'bhp', 'Val', 200*barsa, ...
            'Radius', 0.125, 'Name', 'P', ...
            'InnerProduct', 'ip_tpf', 'Comp_i', 1);

    % --- Initiate and solve flow problem
    rS = initState(G, W, 0, 0.0);
    hT = computeTrans(G, rock);
    rS = incompTPFA(rS, G, hT, fluid, 'wells', W);
    rS.wellSol(1).sign = 1;
    rS.wellSol(2).sign =-1;


    % --- Compute RTD
    D   = computeTOFandTracer(rS, G, rock, 'wells', W, ...
        'maxTOF', inf,'computeWellTOFs', true, 'firstArrival', true);
    WP  = computeWellPairs(rS, G, rock, W, D);
    rtd = computeRTD(rS, G, pv, D, WP, W, 'nsteps',5000);
    RTD = estimateRTD(pv, D, WP);

    % --- Plot RTD
    figure, hold on
    plot(rtd.t/year, rtd.values, '-',  'LineWidth',2);
    plot(RTD.t/year, RTD.values, '--', 'LineWidth',2);
    tfa = min(D.ifa(W(2).cells))/year;      % first arrival
    tm  = RTD.volumes/RTD.allocations/year; % mean time
    vm  = max(RTD.values);
    plot([tfa tfa], [0 vm], ':k', [tm tm], [0 vm], '-k');
    legend('Simulated pulse','From averaged TOF');
    set(gca,'XLim',[0 10],'YLim',[0 5e-8],'FontSize',14);
    xlabel('Time [years]')
    hold off

    axes('Position',[.6 .4 .3 .35]);
    plotCellData(G,log10(rock.perm(:,1)),'EdgeColor','none');
    view(2); axis equal off, colormap(parula)

    % --- F-Phi diagram
    [F, Phi] = computeFandPhi(pv, D.tof);
    [f, phi] = computeFandPhi(rtd, 'sum', true);

    figure
    plot(phi,f, '-', Phi,F, '--', 'LineWidth',2);
    legend(['Simulated pulse: L=',num2str(computeLorenz(f,phi))],...
        ['From averaged TOF: L=', num2str(computeLorenz(F,Phi))], ...
        'Location','SouthEast');
    axis([0 1 0 1]); set(gca,'FontSize',14);
end
_images/showRTD_01.png
_images/showRTD_02.png
_images/showRTD_03.png
_images/showRTD_04.png

RTD for two well pairs

Set parameters describing the wells

wtype    = {'bhp', 'bhp', 'bhp'};
wtarget  = [200,   200,   600] .* barsa();
wrad     = [0.125, 0.125, 0.125] .* meter;
wloc     = [ 10,   50,    25;
            220,   220,    1];
wname    = {'P1', 'P2', 'I'};
sgn      = [-1 -1 1];

[T,layer] = deal(10*year, [23, 75]);
for n=1:2

    % --- Set petrophysical data for this particular layer
    % To avoid problems with very small porosity values, we explicitly
    % impose a lower threshold of 1e-4
    rock = getSPE10rock(1:cartDims(1),1:cartDims(2),layer(n));
    rock.poro = max(rock.poro, 1e-4);

    % --- Set up well model
    % To ensure that we get the correct well index when updating the
    % petrophysical data, we simply regenerate the well objects.
    W = [];
    for w = 1 : numel(wtype)
       W = verticalWell(W, G, rock, wloc(1,w), wloc(2,w), 1, ...
           'Type', wtype{w}, 'Val', wtarget(w), ...
           'Radius', wrad(w), 'Name', wname{w}, ...
           'InnerProduct', 'ip_tpf', 'Comp_i', 1);
    end

    % --- Initiate and solve flow problem
    pv = poreVolume(G, rock);
    rS = initState(G, W, 0, 0.0);
    hT = computeTrans(G, rock);
    rS = incompTPFA(rS, G, hT, fluid, 'wells', W);
    for i=1:numel(rS.wellSol), rS.wellSol(i).sign = sgn(i); end

    % --- Compute RTD
    D   = computeTOFandTracer(rS, G, rock, 'wells', W, ...
        'maxTOF', inf, 'computeWellTOFs', true, 'firstArrival', true);
    WP  = computeWellPairs(rS, G, rock, W, D);
    rtd = computeRTD(rS, G, pv, D, WP, W, 'nsteps',2500);
    RTD = estimateRTD(pv, D, WP);

    % --- Plot RTD
    figure, hold on
    plot(rtd.t/year, rtd.values, '-', 'LineWidth', 2); set(gca,'ColorOrderIndex',1)
    plot(RTD.t/year, RTD.values, '--', 'LineWidth',2); set(gca,'ColorOrderIndex',1)
    tfa = min(D.ifa(W(2).cells))/year;      % first arrival
    tm  = rtd.volumes./rtd.allocations/year; % mean time
    vm  = max(RTD.values(:));
    plot([1; 1]*tm',repmat([0; vm],1,numel(rtd.volumes)), '-');
    legend('Simulated pulse: P1', 'Simulated pulse: P2', ...
        'From averaged TOF: P1', 'From averaged TOF: P2');
    set(gca,'FontSize',14,'XLim',[0 130]);
    xlabel('Time [years]')
    hold off

    axes('Position',[.6 .3 .3 .35]);
    plotCellData(G,log10(rock.perm(:,1)),'EdgeColor','none');
    outlineCoarseGrid(G,D.ppart,'LineWidth',1);
    view(2); axis equal off, colormap(parula)
    x = G.cells.centroids(vertcat(W.cells),:);
    hold on
    plot(x(:,1),x(:,2),'ok','MarkerSize',8,'MarkerFaceColor','w');
    text(x(:,1)+40,x(:,2), {W.name});
    hold off

    % --- F-Phi diagram
    [f, phi ] = computeFandPhi(rtd);
    [tf,tphi] = computeFandPhi(rtd, 'sum', true);

    figure, hold on
    plot(phi,f, '-', tphi, tf, '-','LineWidth',2);
    legend('Simulated pulse: P1', 'Simulated pulse: P2', ...
        'Simulated pulse: whole field','Location','SouthEast');
    hold off
    axis([0 1 0 1]); set(gca,'FontSize',14);
end
_images/showRTD_05.png
_images/showRTD_06.png
_images/showRTD_07.png
_images/showRTD_08.png

Generate Coarse Grids with Near-Well Refinement

Generated from coarsenCaseB4.m

Pressure gradients and flow rates will typically be much larger near wells than inside the reservoir. The accuracy with which we represent the flow in and out of wells will to a large extent determine the accuracy of an overall simulation and as a result one therefore often desires to have higher grid resolution in the near-well zone than inside the reservoir. The example considers the smallest pillar grid from CaseB4, which can either be partitioned rectangularly in index space, or using METIS with transmissibilities as edge weights. The latter approach gives blocks with boundaries aligning to sharp media contrasts. On top of this, we split partitions across faults and use the function ‘refineNearWell’ to impose radial refinement in near-well regions.

mrstModule add coarsegrid incomp
useMetis = false;

Load grid model and create petrophysical data

file = fullfile(getDatasetPath('CaseB4'),'pillar_36x48.grdecl');
G = processGRDECL(readGRDECL(file));
G = computeGeometry(G);
layers = [1 2 8 12 13];
K = logNormLayers(G.cartDims, [50 300 100 20]*milli*darcy, ...
                  'sz', [51 3 3], 'indices', layers);
rock = makeRock(G, bsxfun(@times, K*milli*darcy,[1 1 .1]), 0.2);

W = verticalWell([], G, rock, G.cartDims(1), 1, [], 'InnerProduct', 'ip_tpf');
W = verticalWell(W, G, rock, 14, 36, [],'InnerProduct', 'ip_tpf');

clf
plotCellData(G, log10(rock.perm(:,1)),'EdgeAlpha',.1); view(3);
plotWell(G, W, 'height', 30,'FontSize',12);
colormap(jet), axis tight off, view(250, 50)
_images/coarsenCaseB4_01.png

Uniform/METIS partition

We consider two possible partitions. The first is a standard load-balanced partition in index space. The second is an unstructured partitioning by the METIS graph library that adapts to the underlying geology. To this end, we transmissibilities as edge-weights in the graph-partitioning algorithm of METIS so that it tries to make grid blocks having as homogeneous permeability as possible.

if useMetis
    hT = computeTrans(G, rock);
    p0 = partitionMETIS(G, hT, 7*9*4, 'useLog', true);
else
    p0 = partitionUI(G, [7 9 1]); max(p0)
end
figure, myPlotPartition(G, W, p0, 400);
ans =

    63
_images/coarsenCaseB4_02.png

Split fault faces

It may also be advantageous to split blocks across faults, even if this will introduce some very small blocks

if useMetis
    % hT(G.faces.tag==1) = 1e-10*min(hT);
    % pf = partitionMETIS(G, hT, 7*9*4, 'useLog', true, 'ufactor',5);
    pf = p0;
else
    pf = processPartition(G, p0, find(G.faces.tag==1)); max(pf)
end
figure, myPlotPartition(G, W, pf, 400);
ans =

    79
_images/coarsenCaseB4_03.png

Radial refinement

The function ‘refineNearWell’ takes a set of points and partitions these according to the distance in the xy-plane from a single well point. Here, we will this function to refine the coarse blocks that contains wells. The first well is placed in the corner and we partition the perforated well block into five radial sections. The width of the radial sections is set to decay as log(r). For the second well, we refine all the neighboring blocks surrounding the well block using a number of angular sectors that increases as we move radially out from the well point.

angSectors = {1, [1,4,6,9]};
radSectors = [4 4];
pw = pf;
for i=1:numel(W)
    wc    = W(i).cells(1);
    wpt   = G.cells.centroids(wc,:);
    pwv   = pw(wc);
    if i==1
        % Pick all cells inside the well block
        cells = (pw==pwv);
    else
        % Use adjacency matrix to compute nearest neighbors. We start with
        % a vector e which is equal one in the well block and zero
        % elsewhere. Multiplying by the adjacency matrix A will set the
        % value in each block equal the sum over the block and its
        % face-neighbors. After two multiplications, all blocks surrounding
        % the initial block should have value larger than one.
        CG    = generateCoarseGrid(G, pw);
        A     = getConnectivityMatrix(getNeighbourship(CG),true,CG.cells.num);
        rblk  = zeros(CG.cells.num,1); rblk(pwv)=1; rblk((A*A*rblk)>1) = 1;
        cells = rblk(pw)>0;
    end
    pts   = G.cells.centroids(cells,:);
    out   = refineNearWell(pts, wpt, 'angleBins', angSectors{i}, ...
        'radiusBins', radSectors(i), 'logbins', true, 'maxRadius', inf);
    pw(cells) = max(pw) + out;
end
pw = compressPartition(pw);
figure, myPlotPartition(G, W, pw, 400);
_images/coarsenCaseB4_04.png

Refine partition in the vertical direction

For the Cartesian partition, we end by refining the partition in the vertical direction so that it follows the layering in the petrophysical data, which we assume is known.

if ~useMetis
    pK = rldecode((1:numel(layers)-1)',diff(layers));
    [~,~,k]=gridLogicalIndices(G);
    pk = compressPartition((pK(k)-1)*max(pw)+pw);
    pk = processPartition(G, pk);
    figure, myPlotPartition(G, W, pk, 400);
end
_images/coarsenCaseB4_05.png

Coarsen Real Models: the Johansen Formation

Generated from coarsenJohansen.m

In this script we will look at how to coarsen real model: a sector model of the Johansen aquifer which was considered as a possible candidate for large-scale geological storage of CO2. The data can be downloaded from http://www.sintef.no/Projectweb/MatMorA/Downloads/Johansen/

mrstModule add coarsegrid;

dpath = getDatasetPath('johansen');
sector = fullfile(dpath, 'NPD5');
filename = [sector, '.grdecl'];
G = processGRDECL(readGRDECL(filename));
G = computeGeometry(G);
K = reshape(load([sector, '_Permeability.txt'])', prod(G.cartDims), []);
K = K(G.cells.indexMap);

First, we make a partition with a coarsening factor four in each lateral direction. In the vertical direction, the model consists of three different formations that can be distinguised by the permeability values: the Dunlin shale has K<=0.01mD, the Amundsen shale has K<0.1mD, whereas the Johansen sandstone has K>1mD. For illustration purposes, we only keep one block in the vertical direction for each of the formations.

pK = 2*ones(size(K));  % Johansen
pK(K<=0.1) = 3;        % Amundsen
pK(K<=0.01)= 1;        % Dunlin
pC = partitionUI(G, [G.cartDims(1:2)/4 1]);
[~,~,p] = unique([pK, pC], 'rows');
p = processPartition(G,p);

clf
plotCellData(G,log10(K),'EdgeColor','k','EdgeAlpha',.4); view(3)
outlineCoarseGrid(G,p,'FaceColor','none','EdgeColor','k','LineWidth',1.5);
axis tight off
colormap((2*jet(16)+ones(16,3))/3);
_images/coarsenJohansen_01.png

Make a new coarse grid in which we keep the vertical resolution, which is typically what one would do if the model is to be used to simulate CO2 storage. The resulting coarse model will have many coarse blocks that have more than six connections. We loop through the blocks in the top layer to show how they look like

pK = 2*ones(size(K)); pK(K<=0.1) = 3; pK(K<=0.01)= 1;
pC = partitionUI(G, G.cartDims./[4 4 1]);
[~,~,p] = unique([pK, pC], 'rows');
p = processPartition(G,p);
CG = generateCoarseGrid(G,p);
cn = diff(CG.cells.facePos);

figure('Position',[0 60 560 820])
subplot(2,1,1);
plotGrid(G,'FaceColor','none','EdgeAlpha',.1);
plotGrid(CG,cn>6);

h1=[];
val = cumsum(ones(G.cartDims),3); val=val(G.cells.indexMap);
kn = zeros(size(cn)); kn(p) = val;
ind = find(cn>6 & kn==1);
for i=1:numel(ind)
   subplot(2,1,1); delete(h1);
   h1 = plotGrid(CG,ind(i),'FaceColor','r');

   subplot(2,1,2); cla;
   plotGrid(G,p==ind(i));
   plotGrid(CG,ind(i),'FaceColor','none','LineWidth',2);
   title(['Block number: ' num2str(ind(i))]);
   view(3); drawnow;

end
_images/coarsenJohansen_02.png
lock = [89 139 143 195 286 466];
ind = [1 5:8 4];
c   = colorcube(9);
col = (2*c(1:6,:)+ones(6,3))/3;
figure('position',[0 60 1000 400]);
subplot(2,4,2:3)
plotGrid(G,'FaceColor','none','EdgeAlpha',.1);
axis off; zoom(1.3);
for i=1:6
   subplot(2,4,2:3);
   plotGrid(CG,block(i),'FaceColor',col(i,:));
   subplot(2,4,ind(i));
   plotGrid(G,p==block(i),'FaceColor',col(i,:));
   plotGrid(CG,block(i),'FaceColor','none','LineWidth',2);
   view(3); set(gca,'Clipping','off'), axis tight off
   zoom(1.3)
end
_images/coarsenJohansen_03.png

Coarsen Real Models: SAIGUP Shallow-Marine Reservoir Model

Generated from coarsenSAIGUP.m

The <http://www.fault-analysis-group.ucd.ie/Projects/SAIGUP.html>SAIGUP project is a systematic assessment of uncertainty in reserves and production estimates within an objectively defined geological parameterisation encompassing the majority of European clastic oil reservoirs. A broad suite of shallow marine sedimentological reservoir types are indexed to continuously varying 3D anisotropy and heterogeneity levels. Structural complexity ranges from unfaulted to compartmentalised, and fault types from transmissive to sealing. Several geostatistical realisations each for the geologically diverse reservoir types covering the pre-defined parameter-space are up-scaled, faulted and simulated with an appropriate production strategy for an approximately 20 year period. Herein, we will inspect in detail one instance of the model, which can be downloaded from the <http://www.sintef.no/Projectweb/MRST>MRST webpage

Load dataset

mrstModule add libgeometry coarsegrid
filename = fullfile(getDatasetPath('SAIGUP'), 'SAIGUP.GRDECL');
G = processGRDECL(readGRDECL(filename));
try
   G = mcomputeGeometry(G);
catch %#ok<CTCH>
   G = computeGeometry(G);
end

We construct a coarse grid by partitioning the grid uniformly as 6x12x3 coarse blocks in index space. This process partitions all cells in the logical 40x120x20 grid, including cells that are inactive. We therefore compress the partitioning to remove blocks that contain no active cells, and then renumber the overall partitioning. Some of the blocks may contain disconnected cells because of erosion, faults, etc. We therefore postprocess the grid in physical space and split disconnected blocks.

p = partitionUI(G,[6 12 3]);
p = compressPartition(p);
p = processPartition(G,p);

We have now obtained a partitioning consisting of 243 blocks, in which each coarse block consists of a set of connected cells in the fine grid. To show the partition, we plot the coarse blocks using the explosion view technique

figure('Position',[0 60 800 420]);
axes('Position',[.01 .11 .775 .81]);
explosionView(G, p, 'EdgeAlpha', .1);
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,55); zoom(1.4); camdolly(0,-0.2,0)
colormap(.3*(2*colorcube(max(p))+ones(max(p),3)));
set(gca,'Clipping','off')
_images/coarsenSAIGUP_01.png

Build the coarse-grid and inspect the distribution of block volumes. The original fine-scale model has the peculiar characteristic that all cells have the same size. In the coarsened model there is almost two orders difference between the smallest and largest blocks.

CG = generateCoarseGrid(G, p); P=p;
CG = coarsenGeometry(CG);

axes('Position',[.75 .12 .18 .8]);
h = barh(CG.cells.volumes,'b');
set(gca,'xdir','reverse','YAxisLocation','right',...
   'Fontsize',8,'XLim',[0 12]*1e6);
set(h,'FaceColor',[.7 .7 .7],'EdgeColor',[.6 .6 .6]);
_images/coarsenSAIGUP_02.png

To get a more even size distribution for the coarse blocks, we will remove some of the smallest blocks by merging them with the neighbor that has the smallest block volume. This is done repeatedly until the volumes of all blocks are above a certain lower threshold. First, we visualize all blocks that have volume less than ten percent of the mean block volume.

blockVols = CG.cells.volumes;
meanVol   = mean(blockVols);
show = blockVols<.1*meanVol;
figure
plotGrid(G,'FaceColor','none','EdgeAlpha',.05);
bcol = zeros(max(p),1); bcol(show)=1:sum(show);
plotCellData(CG,bcol, show);
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,55); zoom(1.4); camdolly(0,-0.2,0)
colormap(lines);
set(gca,'Clipping','off')
_images/coarsenSAIGUP_03.png

The merging algorithm is quite simple: we compute the block volumes, select the block with the smallest volume, and then merge this block with one of its neighbors, depending on some criterion (e.g., the neighbor with the largest or smallest block volume). Then we update the partition vector by relabling all cells in the block with the new block number, compress the partition vector to get rid of empty entries, regenerate a coarse grid, recompute block volumes, pick the block with the smallest volume in the new grid, and so on. In each iteration, we plot the selected block and its neighbors.

blockVols = CG.cells.volumes;
meanVol   = mean(blockVols);
[minVol, block] = min(blockVols);
while minVol<.1*meanVol
   % Find all neighbors of the block
   clist = any(CG.faces.neighbors==block,2);
   nlist = reshape(CG.faces.neighbors(clist,:),[],1);
   nlist = unique(nlist(nlist>0 & nlist~=block));

   % Alt 1: merge with neighbor having smallest volume
   % [~,merge] = min(blockVols(nlist));

   % Alt 2: sort neigbors by cell volume and merge with the one in the
   % middle
   % [~,ind] = sort(blockVols(nlist),1,'descend');
   % merge = ind(round(numel(ind)/2));

   % Alt 3: merge with neighbor having largest volume
   [~,merge] = max(blockVols(nlist));

   figure;
   plotBlockAndNeighbors(CG, block, ...
      'PlotFaults', [false, true], 'Alpha', [1 .8 .8 .8]);

   % Update partition vector
   p(p==block)  = nlist(merge);
   p = compressPartition(p);

   % Regenerate coarse grid and pick the block with the smallest volume
   CG = generateCoarseGrid(G, p);
   CG = coarsenGeometry(CG);
   blockVols = CG.cells.volumes;
   [minVol, block] = min(blockVols);
end
_images/coarsenSAIGUP_04.png
_images/coarsenSAIGUP_05.png
_images/coarsenSAIGUP_06.png
_images/coarsenSAIGUP_07.png
_images/coarsenSAIGUP_08.png
_images/coarsenSAIGUP_09.png
_images/coarsenSAIGUP_10.png
_images/coarsenSAIGUP_11.png
_images/coarsenSAIGUP_12.png
_images/coarsenSAIGUP_13.png
_images/coarsenSAIGUP_14.png
_images/coarsenSAIGUP_15.png
_images/coarsenSAIGUP_16.png
_images/coarsenSAIGUP_17.png
_images/coarsenSAIGUP_18.png
_images/coarsenSAIGUP_19.png
_images/coarsenSAIGUP_20.png
_images/coarsenSAIGUP_21.png
_images/coarsenSAIGUP_22.png
_images/coarsenSAIGUP_23.png
_images/coarsenSAIGUP_24.png
_images/coarsenSAIGUP_25.png
_images/coarsenSAIGUP_26.png

Make new plot with the revised coarse grid

figure('Position',[0 60 800 420]);
axes('Position',[.01 .11 .775 .81]);
explosionView(G, CG.partition, 'EdgeAlpha', .1);
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,55); zoom(1.4); camdolly(0,-0.2,0)
colormap(.3*(2*colorcube(CG.cells.num)+ones(CG.cells.num,3)));
set(gca,'Clipping','off')

axes('Position',[.75 .12 .18 .8]);
h = barh(CG.cells.volumes,'b');
set(gca,'xdir','reverse','YAxisLocation','right',...
   'Fontsize',8,'XLim',[0 12]*1e6);
set(h,'FaceColor',[.7 .7 .7],'EdgeColor',[.6 .6 .6]);
_images/coarsenSAIGUP_27.png

In the partition above, there were several blocks that were only connected through small face areas in their interior. To try to improve this problem, we recompute the the partitioning, but now disregard couplings across small faces when postprocessing the initial, uniform partition

p = partitionUI(G,[6 12 3]);
p = compressPartition(p);
p = processPartition(G, p, G.faces.areas<250);
CG1 = generateCoarseGrid(G,p);
CG1 = coarsenGeometry(CG1);

The next step is to redo the merging of small blocks. The algorithm used above was written to make the visualization of the changing grid as simple as possible. The implementation is not very efficient since we in each step regenerate the coarse grid and compute all the block volumes. This time, we will use another implementation that is a bit more involved, but also more efficient.

blockVols       = CG1.cells.volumes;
meanVol         = mean(blockVols);
newBlockList    = 1:max(p);
[minVol, block] = min(blockVols);
while minVol<.1*meanVol
   nlist = reshape(CG1.faces.neighbors(any(CG1.faces.neighbors==block,2),:),[],1);
   nlist = newBlockList(nlist(nlist>0));
   nlist = unique(nlist(nlist~=block));

   [nVol,merge] = min(blockVols(nlist));

   newBlockNo   = nlist(merge);
   p(p==block)  = newBlockNo;
   newBlockList(block)   = newBlockNo;
   blockVols(block)      = inf;
   blockVols(newBlockNo) = minVol + nVol;

   [minVol, block] = min(blockVols);
end
p = compressPartition(p);
CG1 = generateCoarseGrid(G, p);
CG1 = coarsenGeometry(CG1);

Make new plot with the revised coarse grid

igure('Position',[0 60 800 420]);
axes('Position',[.01 .11 .775 .81]);
explosionView(G, CG1.partition, 'EdgeAlpha', .1);
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,55); zoom(1.4); camdolly(0,-0.2,0)
colormap(.3*(2*colorcube(CG1.cells.num)+ones(CG1.cells.num,3)));
set(gca,'Clipping','off')

axes('Position',[.75 .12 .18 .8]);
h = barh(CG1.cells.volumes,'b');
set(gca,'xdir','reverse','YAxisLocation','right',...
   'Fontsize',8,'XLim',[0 12]*1e6);
set(h,'FaceColor',[.7 .7 .7],'EdgeColor',[.6 .6 .6]);
_images/coarsenSAIGUP_28.png

Example: three uses of simpleGrdecl

Generated from exampleGrids.m

grdecl = simpleGrdecl([20, 20, 5]);
G = processGRDECL(grdecl);
clf, plotGrid(G,'FaceColor',[.8 .8 .8]), view(3), axis tight off
_images/exampleGrids_01.png
grdecl = simpleGrdecl([20, 20, 5], @(x) 0.05 * (sin(2*pi*x) - 1.5));
G = processGRDECL(grdecl);
clf, plotGrid(G,'FaceColor',[.8 .8 .8]), view(3), axis tight off
_images/exampleGrids_02.png
grdecl = simpleGrdecl([20, 20, 5], @(x) 0.25*(x-0.5), 'flat', true);
G = processGRDECL(grdecl);
clf, plotGrid(G,'FaceColor',[.8 .8 .8]), view(3), axis tight off
_images/exampleGrids_03.png

Example: makeModel3

grdecl = makeModel3([30,20,5]);
G = processGRDECL(grdecl);
clf, plotGrid(G,'FaceColor',[.8 .8 .8]), view(130,30), axis tight off
plotFaces(G,find(G.faces.tag>0),'FaceColor',[.6 .6 .6]);
_images/exampleGrids_04.png

Example: extrudedTriangleGrid

G = extrudedTriangleGrid(50);
clf, plotGrid(G,'FaceColor',[.8 .8 .8]), view(-30,60), axis tight off
_images/exampleGrids_05.png
= extrudedTriangleGrid(50, true);
clf, plotGrid(G,'FaceColor',[.8 .8 .8]), view(-30,60), axis tight off
_images/exampleGrids_06.png
mrstModule add agglom coarsegrid diagnostics incomp spe10

CaseB4: merge small cells

Generated from showAgglomExamples.m

Load and set up model

file = fullfile(getDatasetPath('CaseB4'),'pillar_36x48.grdecl');
G = processGRDECL(readGRDECL(file));
G = computeGeometry(G);
layers = [1 2 8 12 13];
K = logNormLayers(G.cartDims, [50 300 100 20]*milli*darcy, ...
                  'sz', [51 3 3], 'indices', layers);
rock = makeRock(G, bsxfun(@times, K*milli*darcy,[1 1 .1]), 0.2);

W = verticalWell([], G, rock, G.cartDims(1), 1, [], 'InnerProduct', 'ip_tpf');
W = verticalWell(W, G, rock, 14, 36, [],'InnerProduct', 'ip_tpf');

% Partition topology and split across faults
p0 = partitionUI(G, [7 9 1]);
pf = processPartition(G, p0, find(G.faces.tag==1));
figure, myPlotPartition(G, W, pf, 400);
_images/showAgglomExamples_01.png

Merge small blocks To avoid merging across faults, we generate a static partition that splits the model into three different fault blocks

I  = poreVolume(G, rock)./G.cells.volumes;
pm = mergeBlocks(pf, G, I, I, 50, 'static_partition', ...
    processPartition(G,ones(G.cells.num,1),find(G.faces.tag==1)));
figure, myPlotPartition(G, W, pm, 400);
_images/showAgglomExamples_02.png

Adaptive Cartesian coarsening for layer of SPE10

We consider a subset of layer 25 from SPE 10 and study how recursive Cartesian refinement works for three different indicator functions: permeability, velocity, and time of flight.

% Setup model
[G, W, rock] = getSPE10setup(25);
for i = 1:numel(W), W(i).compi = 1; end
rock.poro = max(rock.poro, 1e-4);
fluid = initSingleFluid('mu', 1*centi*poise, 'rho', 1014*kilogram/meter^3);
hT = computeTrans(G, rock);
rS = incompTPFA(initState(G,W,0), G, hT, fluid, 'wells', W);
Tf = computeTimeOfFlight(rS, G, rock, 'wells', W);
Tb = computeTimeOfFlight(rS, G, rock, 'wells', W, 'reverse', true);
pv = poreVolume(G, rock);

% Construct indicators
iK = log10(rock.perm(:,1)); iK = iK - min(iK) + 1;
v  = sqrt(sum(faceFlux2cellVelocity(G, rS.flux).^2, 2));
iV = log10(v); iV = iV - min(iV) + 1;
iT = -log10(Tf+Tb); iT = iT - min(iT) + 1;

% Extract subset of model
p1  = partitionUI(G, [3, 11, 1]);
ind = find(p1<19);
G   = extractSubgrid(G, ind);
[p1,iK,iV,iT,pv] = deal(p1(ind),iK(ind),iV(ind),iT(ind),pv(ind));

% Recursively subdivide the blocks by a factor 2x2
pK = refineUniformShape(p1, G, iK, 25, 'CartDims', [2,2,1]);
pV = refineUniformShape(p1, G, iV, 25, 'CartDims', [2,2,1]);
pT = refineUniformShape(p1, G, iT, 25, 'CartDims', [2,2,1]);

% Plot the results
I = {iK, iV, iT}; p = {pK, pV, pT};
head = {'Permeability', 'Velocity', 'Time-of-flight'};
wc = vertcat(W.cells);
x = G.cells.centroids(wc(wc<G.cells.num),:);
clf; set(gcf,'Position',[510 420 850 400]);
for i=1:3
    subplot(1,3,i)
    plotCellData(G,I{i},'EdgeColor','none');
    hold on
    plot(x(:,1),x(:,2),'ok','MarkerSize',7,'MarkerFaceColor','w'); hold off
    outlineCoarseGrid(G, p{i}, 'EdgeColor','k'); axis tight off
    title([head{i} ' indicator']);
end
colormap(jet(128)*.7+.3*ones(128,3));
_images/showAgglomExamples_03.png

Non-uniform coarsening

We outline the main steps of the original NUC algorithm proposed by Aarnes et al. To this end, we extract a further subdivision of the model

ind = find(p1>9);
G   = extractSubgrid(G, ind);
[p1,iK,iV,iT,pv] = deal(p1(ind),iK(ind),iV(ind),iT(ind),pv(ind));
volI = pv./G.cells.volumes;
flwI = iV;
clf,set(gcf,'Position',[50 560 1400 240]);
subplot(1,6,1)
plotCellData(G, flwI, 'EdgeColor','none'); axis tight
set(gca,'XTick',[],'YTick',[]); title('Indicator');

subplot(1,6,2)
ps = segmentIndicator(G, flwI, 5);
plotCellData(G, ps, 'EdgeColor','none'); axis tight
outlineCoarseGrid(G, ps, 'EdgeColor','k','FaceColor','none');
set(gca,'XTick',[],'YTick',[]); title('segmentIndicator');
xlabel(['#blocks: ' num2str(max(ps))]);

subplot(1,6,3)
pm1 = mergeBlocks(ps, G, volI, flwI, 20);
plotCellData(G, pm1, 'EdgeColor','none'); axis tight
outlineCoarseGrid(G, pm1, 'EdgeColor','k','FaceColor','none');
set(gca,'XTick',[],'YTick',[]); title('mergeBlocks');
xlabel(['#blocks: ' num2str(max(pm1))]);

subplot(1,6,4)
pr = refineGreedy(pm1, G, flwI, 30);
plotCellData(G, pr, 'EdgeColor','none'); axis tight
outlineCoarseGrid(G, pr, 'EdgeColor','k','FaceColor','none');
set(gca,'XTick',[],'YTick',[]); title('refineGreedy');
xlabel(['#blocks: ' num2str(max(pr))]);

subplot(1,6,5)
pm2 = mergeBlocks(pr, G, volI, flwI, 20);
plotCellData(G, pm2, 'EdgeColor','none'); axis tight
outlineCoarseGrid(G, pm2, 'EdgeColor','k','FaceColor','none');
set(gca,'XTick',[],'YTick',[]); title('mergeBlocks');
xlabel(['#blocks: ' num2str(max(pm2))]);

subplot(1,6,6);
p = mergeBlocks2(ps, G, volI, flwI, 20, 30);
p = refineGreedy2(p, G, flwI, 30, 'nlevel',1);
p = mergeBlocks2(p, G, volI, flwI, 20, 30);
plotCellData(G, p, 'EdgeColor','none'); axis tight
outlineCoarseGrid(G, p, 'EdgeColor','k','FaceColor','none');
set(gca,'XTick',[],'YTick',[]); title('Version 2');
xlabel(['#blocks: ' num2str(max(p))]);

colormap(jet(128)*.6+.4*ones(128,3));
_images/showAgglomExamples_04.png
mrstModule add agglom coarsegrid

—— S E G M E N T A T I O N ——

Generated from showAgglomModule.m

Demonstrate segmentation: apply to CaseB4 model

rng(1,'twister');    % reset random generator to reproduce the same results
file = fullfile(getDatasetPath('CaseB4'),'pillar_36x48.grdecl');
G = processGRDECL(readGRDECL(file));
G = computeGeometry(G);
layers = [1 2 8 12 13];
K = logNormLayers(G.cartDims, [50 300 150 20],'sz', [51 3 3], 'indices', layers);

Plot permeability and outline initial bins We segment the permeability, but only out put the initial bins without splitting disconnected components

p = segmentIndicator(G, K, [0 30 80 205 inf],'split',false);

figure(1); clf
plotCellData(G, K,'EdgeAlpha',.1);
view(-120,15), axis tight, set(gca,'Projection','Perspective');
colormap(.8*jet+0.2*ones(size(jet)));
[~, hh]=colorbarHist(K, [0 450], 'South', 100);
h = outlineCoarseGrid(G,p,'EdgeColor','k','LineWidth',2,'FaceColor','none');
hold(hh,'on');
ym = .8*max(reshape(get(get(hh,'Children'),'YData'),[],1));
plot(hh,[30 80 205; 30 80 205], [0 0 0; ym ym ym],'Color',[.3 .3 .3],'LineWidth',2);
hold(hh,'off'); axis off
%set(hc,'FontSize',16);
_images/showAgglomModule_01.png

Plot bins resulting from the segmentation

figure(2); clf
plotCellData(G, p,'EdgeAlpha',.1);
mp = max(p);
caxis([.5 mp+.5]); colormap(.8*tatarizeMap(mp)+.2*ones(mp,3));
view(-120,15), axis tight off, set(gca,'Projection','Perspective');
[hc,hh] = colorbarHist(p, [.5 mp+.5], 'South', mp);
bar(hh,accumarray(p,1),'FaceColor','none');
set(hh,'XLim',[.5 mp+.5]);
axis(hh,'off');
set(hc,'XTick',1:4); % set(hc,'FontSize',16);
_images/showAgglomModule_02.png

—– S A N I T Y C H E C K S ——

Split connected components (done by initial segmentation)

p = segmentIndicator(G, K, [0 30 80 205 inf]);
figure(1); clf
plotCellData(G, p,'EdgeAlpha',.1);
mp = max(p);
caxis([.5 mp+.5]); colormap(.8*tatarizeMap(mp)+.2*ones(mp,3));
view(-120,15), axis tight off, set(gca,'Projection','Perspective');
[hc,hh] = colorbarHist(p, [.5 mp+.5], 'South', mp);
bar(hh,log10(accumarray(p,1)),'FaceColor','none');
set(hh,'XLim',[.5 mp+.5]);
axis(hh,'off');
set(hc,'FontSize',16);
_images/showAgglomModule_03.png

Detect confined blocks

cb = findConfinedBlocks(G, p);
flag=false(mp,1); flag(cb)=true;
plotGrid(G,flag(p),'FaceColor','k','EdgeColor','k');
delete([hc,hh]);
_images/showAgglomModule_04.png

Remove confined blocks

%[cb,p] = findConfinedBlocks(G,p);
figure(1); clf,
hp = plotCellData(G, p,'EdgeAlpha',.1);
mp = max(p);
caxis([.5 mp+.5]); colormap(.8*tatarizeMap(mp)+.2*ones(mp,3));
view(-120,15), axis tight off, set(gca,'Projection','Perspective');
[hc,hh] = colorbarHist(p, [.5 mp+.5], 'South', mp);
bar(hh,log10(accumarray(p,1)),'FaceColor','none');
set(hh,'XLim',[.5 mp+.5]);
axis(hh,'off');
set(hc,'FontSize',14);
_images/showAgglomModule_05.png

Remove recursively confined blocks

The implementation in findConfinedBlocks is relatively simple and does not work properly for recursivley confined blocks. For this, we can instead use graph algorithms from the MATLAB Boost Graph Library.

mrstModule add matlab_bgl
figure('Position',[450 540 940 260]);
G = computeGeometry(cartGrid([7 7]));
% p = ones(7,7); p(2:6,2:6)=2; p(3:5,3:5)=3;p(4,4)=4;
p = ones(7,7);p(:,4:7)=2;p(2:6,2:6)=3;p(3:5,3)=4;p(3:5,4)=5;p(3:5,5)=6;
pn = removeConfinedBlocks(G,p(:));
[~,pm] = findConfinedBlocks(G, p(:));
subplot(1,3,1), plotCellData(G,p(:)); axis equal off, caxis([1 7]), title('initial');
subplot(1,3,2), plotCellData(G,pm); axis equal off, caxis([1 7]), title('findConfinedBlocks');
subplot(1,3,3), plotCellData(G,pn);  axis equal off, caxis([1 7]), title('removeConfinedBlocks');
_images/showAgglomModule_06.png

—— M E R G I N G F U N C T I O N S ——

G = computeGeometry(cartGrid([5 4]));
I = [1 1 5 2 2; 1 5 1 1 1; 4 5 1 2 2; 6 1 3 3 3]'; I=I(:);
p = segmentIndicator(G, I, 6);
n = G.cells.num;

set(figure(1),'Position',[450 540 940 260]);

subplot(1,3,1), cla
plotCellData(G,I,'edgecolor','none');
outlineCoarseGrid(G, p, 'k', 'LineWidth',2); axis off
colormap(.6*tatarizeMap(6)+.4*ones(6,3));
hold on
for i=1:n,
    text(G.cells.centroids(i,1),G.cells.centroids(i,2), num2str(I(i)));
end
hold off

% mergeBlocks
p1 = mergeBlocks(p, G, ones(n,1), I, 2);
subplot(1,3,2), cla
plotCellData(G,I,'EdgeColor','none');
outlineCoarseGrid(G, p1, 'k', 'LineWidth', 2); axis off
xm = sparse(p1,1:n,1)*[G.cells.centroids, ones(n,1), I];
Ib = xm(:,end);
xm = bsxfun(@rdivide, xm(:,1:2), xm(:,3));
for i=1:size(xm,1)
    text(xm(i,1),xm(i,2),num2str(Ib(i,end)));
end

% mergeBlocks2
p2 = mergeBlocks2(p, G, ones(G.cells.num,1), I, 2, 3);
subplot(1,3,3), cla
plotCellData(G,I,'EdgeColor','none');
outlineCoarseGrid(G, p2, 'k', 'LineWidth', 2); axis off
xm = sparse(p2,1:n,1)*[G.cells.centroids, ones(n,1), I];
Ib = xm(:,end);
xm = bsxfun(@rdivide, xm(:,1:2), xm(:,3));
for i=1:size(xm,1)
    text(xm(i,1),xm(i,2),num2str(Ib(i,end)));
end
_images/showAgglomModule_07.png

—— R E F I N E M E N T M E T H O D S —–

pargs={'EdgeColor','none'};
blk = [5 13];
dim = {[4,4],[5,5]};
for n=1:2
    G = computeGeometry(cartGrid(dim{n}));
    I = ones(G.cells.num,1);

    figure('Position', [440 560 930 240]);

    subplot(1,6,1);
    plotGrid(G,'FaceColor','none'); axis equal off
    for i=1:G.cells.num,
        text(G.cells.centroids(i,1),G.cells.centroids(i,2),num2str(i),...
            'HorizontalAlignment','center','FontSize',8);
    end

    p = refineUniform(I,G,I,4,'CartDims',[2,2]);
    subplot(1,6,2); plotCellData(G,p,pargs{:}); axis equal off
    outlineCoarseGrid(G,p,'k'); caxis([.5 blk(n)+.5]);
    title('refineUniform','Position',[dim{n} 1].*[.5 1.02 1.02]);

    p = refineGreedy(I,G,I,4,'nlevel',n);
    subplot(1,6,3), plotCellData(G,p,pargs{:}); axis equal off,
    outlineCoarseGrid(G,p,'k');caxis([.5 blk(n)+.5]);
    title('refineGreedy','Position',[dim{n} 1].*[.5 1.02 1.02]);

    p = refineGreedy2(I,G,I,4,'nlevel',n);
    subplot(1,6,4), plotCellData(G,p,pargs{:}); axis equal off,
    outlineCoarseGrid(G,p,'k'); caxis([.5 blk(n)+.5]);
    title('refineGreedy2','Position',[dim{n} 1].*[.5 1.02 1.02]);

    p = refineGreedy3(I,G,I,4,'nlevel',n);
    subplot(1,6,5), plotCellData(G,p,pargs{:}); axis equal off,
    outlineCoarseGrid(G,p,'k');caxis([.5 blk(n)+.5]);
    title('refineGreedy3','Position',[dim{n} 1].*[.5 1.02 1.02]);

    p = refineGreedy4(I,G,I,4,'nlevel',n);
    subplot(1,6,6), plotCellData(G,p,pargs{:}); title('refineGreedy4');axis equal off,
    outlineCoarseGrid(G,p,'k');caxis([.5 blk(n)+.5]);
    title('refineGreedy4','Position',[dim{n} 1].*[.5 1.02 1.02]);

    colormap(.8*tatarizeMap(blk(n))+.2*ones(blk(n),3));
    h = colorbar('horiz');
    set(h,'Position',[0.29 0.13 0.455 0.1],'XTick',1:blk(n));
end
_images/showAgglomModule_08.png
_images/showAgglomModule_09.png

Simple Model of an Anticline

Generated from showAnticlineModel.m

Conceptual illustration of a reservoir model with aggressive coarsening in the aquifer zone, modest coarsening above the initial water contact, and original resolution for cells with low residence time

mrstModule add coarsegrid diagnostics incomp spe10

Cartesian grid and rock parameters

[nx,ny,nz] = deal(60,60,15);
G = cartGrid([nx ny nz], [nx ny nz].*[20 10 2]*ft);

rock = getSPE10rock(1:nx,1:ny,nz:-1:1);
rock.poro(rock.poro==0) = 1e-5;

Make anticline structure

x = G.nodes.coords(:,1);
y = G.nodes.coords(:,2);

normalize = @(x) (x-min(x))/(max(x)-min(x));
x = 2*normalize(x)-1;
y = 2*normalize(y)-1;
r = min(sqrt(x.^2 + y.^2),1);
dz = r.^2 ./ (r.^2 + (1-r).^4);

G.nodes.coords(:,3) = G.nodes.coords(:,3) + dz*(nz+1)*2*ft;
G = computeGeometry(G);
clf, set(gcf,'Position', [230 190 1200 630]);
subplot(1,3,1)
plotCellData(G,log10(rock.perm(:,1)),'EdgeColor','none');
view(-54,30); set(gca,'dataasp',[1 1 .4]), axis tight off
_images/showAnticlineModel_01.png

Set initial saturation

s = zeros(G.cells.num,1);
s(G.cells.centroids(:,3)>(nz+1)*2*ft) = 1;
%[nodes,pos] = gridCellNodes(G, (1:G.cells.num).');
%c = rldecode(1:G.cells.num, diff(pos), 2).';
%A = sparse(c,nodes,1)*[s, ones([G.nodes.num,1])];
%s = bsxfun(@rdivide, A(:,1:(end-1)), A(:,end));
subplot(1,3,2), cla
plotCellData(G,1-s,'EdgeAlpha',.1); caxis([0 1]);
view(-54,30); set(gca,'dataasp',[1 1 .4]), axis tight off
_images/showAnticlineModel_02.png

Set wells

producers

args = {'Type', 'bhp', 'Val', 100*barsa, 'Comp_i', [0 1], 'sign', -1};
W = verticalWell([], G, rock, 28, 30, [], args{:}, 'name', 'P1');
W = verticalWell(W,  G, rock, 34, 32, [], args{:}, 'name', 'P2');

% injectors
args = {'Type', 'bhp', 'Val', 150*barsa, 'Comp_i', [1 0], 'sign', 1};
W = verticalWell(W,  G, rock, 18, 18, [], args{:}, 'name', 'I1');
W = verticalWell(W,  G, rock, 18, 44, [], args{:}, 'name', 'I2');
W = verticalWell(W,  G, rock, 44, 44, [], args{:}, 'name', 'I4');
subplot(1,3,1)
plotWell(G,W,'Color','k','FontSize',12); axis tight off
_images/showAnticlineModel_03.png

Incompressible pressure solution and time-of-flight

hT = computeTrans(G, rock);
fluid = initSimpleFluid('mu' , [1 1]*centi*poise     , ...
                        'rho', [1014, 859]*kilogram/meter^3, ...
                        'n'  , [ 2, 2]);
state = initState(G, W, 100*barsa, [s 1-s]);
state = incompTPFA(state, G, hT, fluid, 'wells', W);

Tf = computeTimeOfFlight(state, G, rock, 'wells', W, 'reverse', false);
Tb = computeTimeOfFlight(state, G, rock, 'wells', W, 'reverse', true);
T  = Tf + Tb;

Extract the high-flow zones

Selection criterion: travel time less than two years (times magic factor)

tfac = median(T(vertcat(W.cells)));
I = T < 2*tfac;
subplot(1,3,3), cla
plotCellData(G,I+0,'EdgeAlpha',.1); caxis([-3 2]);
plotWell(G,W,'Color','w','FontSize',12);
view(-54,30); set(gca,'dataasp',[1 1 .4]), axis tight off
_images/showAnticlineModel_04.png

Coarsen model differently in aquifer, oil, and high-flow zones

Aquifer zone

pc = partitionCartGrid([nx,ny,nz],[10 10 5]);
p = compressPartition(pc);

% Zone above intial water contact
pf = partitionCartGrid([nx,ny,nz],[nx,ny,nz]/2);
p(s<1e-5) = pf(s<1e-5);

% Zone with low residence time
po = 1:G.cells.num;
p(I) = po(I);

% Near-well region
q = zeros(G.cells.num,1);
q(vertcat(W.cells))=1;
qc = accumarray(pc,q);
ind = qc(pc)>0;
p(ind) = po(ind);
subplot(1,3,1)
h = outlineCoarseGrid(G, p,'FaceColor','none','EdgeColor','k');
_images/showAnticlineModel_05.png
for i=1:3
    subplot(1,3,i),
    set(gca,'Clipping', 'off', ...
        'Position',get(gca,'Position')+[-.025 -.025 .05 .05]); zoom(1.3*1.1);
end
colormap(.8*jet(128)+.2*ones(128,3));
_images/showAnticlineModel_06.png

Stair-stepped grid

Generated from showCaseB.m

clf
grdecl = readGRDECL(fullfile(getDatasetPath('CaseB4'),'stairstep_36x48.grdecl'));
Gs = processGRDECL(grdecl);
Gs = computeGeometry(Gs);
Kx = logNormLayers(Gs.cartDims, [10 50 400 50 10], ...
   'indices', [1 2 5 8 12 13]); Kx = log10(Kx);

plotCellData(Gs,Kx(Gs.cells.indexMap),'EdgeColor','k');
set(gca,'dataa',[15 20 1])
axis tight off, view(150,30), zoom(1.2)
set(gca,'Clipping','off')
_images/showCaseB_01.png
cut_grdecl = cutGrdecl(grdecl, [12 20; 13 23; 1 12]);
k = reshape(Kx,Gs.cartDims);
kx = reshape(k(12:20,13:23,1:12),1,[]);
g = processGRDECL(cut_grdecl);

clf
plotCellData(g,kx(g.cells.indexMap)','EdgeColor','k'),
axis tight off, view(150,50), zoom(1.2)
set(gca,'Clipping','off')
_images/showCaseB_02.png

Corner-point grid

clf
grdecl = readGRDECL(fullfile(getDatasetPath('CaseB4'),'pillar_36x48.grdecl'));
Gp = processGRDECL(grdecl);
Gp = computeGeometry(Gp);
plotCellData(Gp,Kx(Gp.cells.indexMap),'EdgeColor','k');
set(gca,'dataa',[15 20 1])
axis tight off, view(150,30), zoom(1.2)
set(gca,'Clipping','off')
_images/showCaseB_03.png
cut_grdecl = cutGrdecl(grdecl, [12 20; 13 23; 1 12]);
g = processGRDECL(cut_grdecl);

clf
plotCellData(g,kx(g.cells.indexMap)','EdgeColor','k'),
axis tight off, view(150,50), zoom(1.2)
set(gca,'Clipping','off')
_images/showCaseB_04.png

Visualize results

lf
set(gcf,'Position', [300 450 1000 330]);
subplot(1,2,1);
plotCellData(Gs, log10(Kx(Gs.cells.indexMap)), 'EdgeColor', 'k', 'EdgeAlpha',0.1);
set(gca,'dataaspect',[32 44 2.5])
axis tight off, view(104,28);
%
subplot(1,2,2);
plotCellData(Gp, log10(Kx(Gp.cells.indexMap)),'EdgeColor', 'k', 'EdgeAlpha',0.1);
set(gca,'dataaspect',[32 44 2.5])
axis tight off, view(104,28);
_images/showCaseB_05.png

Examples of How to Generate Coarse Grids

Generated from showCoarseGrid.m

We show three examples: a 2x2 partition of a 4x4 Cartesian grid, a 2x2 partition of a facies model with subpartition of coarse faces, and a 3D model with subdivision of coarse faces

mrstModule add coarsegrid;

First example: 2x2 partition of a 4x4 Cartesian grid

G = computeGeometry(cartGrid([4,4]));
p = partitionUI(G, [2,2]);
CG = generateCoarseGrid(G, p);

clf
plotCellData(G,(1:G.cells.num)','LineStyle',':');
plotGrid(CG,'faceColor','none','LineWidth',3);
colormap((jet(G.cells.num)+repmat(2,G.cells.num,3))/3);
_images/showCoarseGrid_01.png

Show cell/block indices

CG = coarsenGeometry(CG);
tg = text(G.cells.centroids(:,1), G.cells.centroids(:,2), ...
   num2str((1:G.cells.num)'),'FontSize',20, 'HorizontalAlignment','center');
tcg = text(CG.cells.centroids(:,1), CG.cells.centroids(:,2), ...
   num2str((1:CG.cells.num)'),'FontSize',24, 'HorizontalAlignment','center');
axis off;
set(tcg,'BackgroundColor','w','EdgeColor','none');
_images/showCoarseGrid_02.png

Show indices of the faces in the fine/coarse grids

delete([tg; tcg]);
tg = text(G.faces.centroids(:,1), G.faces.centroids(:,2), ...
   num2str((1:G.faces.num)'),'FontSize',18, 'HorizontalAlignment','center');
tcg = text(CG.faces.centroids(:,1), CG.faces.centroids(:,2), ...
   num2str((1:CG.faces.num)'),'FontSize',24, 'HorizontalAlignment','center');
set(tcg,'BackgroundColor','w','EdgeColor','k');
_images/showCoarseGrid_03.png

Second example: 2D face partition

clf;
G  = computeGeometry(cartGrid([8, 8], [1 1]));
f  = @(c) sin(3*pi*(c(:,1)-c(:,2)));
pf = 1 + (f(G.cells.centroids) > 0);

plotCellData(G, pf,'edgecolor','none'); axis off;
colormap(.2*gray(2)+.8*ones(2,3));

pv = partitionCartGrid(G.cartDims, [2 2]);
pf = cellPartitionToFacePartition(G,pf);
pf = processFacePartition(G, pv, pf);
CG = generateCoarseGrid(G, pv, pf);

CG = coarsenGeometry(CG);
cmap = lines(CG.faces.num);
for i=1:CG.faces.num
    plotFaces(CG,i,'LineWidth',6,'EdgeColor', cmap(i,:));
end
text(CG.faces.centroids(:,1), CG.faces.centroids(:,2), ...
        num2str((1:CG.faces.num)'),'FontSize',20,'HorizontalAlignment','center');
_images/showCoarseGrid_04.png

Third example: 3D face partition

clf
G = computeGeometry(cartGrid([20 20 6]));
c = G.cells.centroids;
G = removeCells(G, ...
   (c(:,1)<10) & (c(:,2)<10) & (c(:,3)<3));
plotGrid(G,'FaceColor',[1 1 .7]); view(3); axis off
_images/showCoarseGrid_05.png
clf
p = partitionUI(G,[2, 2, 2]);
q = partitionUI(G,[4, 4, 2]);
CG = generateCoarseGrid(G, p, ...
   cellPartitionToFacePartition(G,q));

plotCellData(CG,(1:max(p))');
plotFaces(CG,1:CG.faces.num,...
   'FaceColor' , 'none' , 'LineWidth' ,2);
view(3); axis off
colormap(.5*(jet(128)+ones(128,3)));
_images/showCoarseGrid_06.png

Add face centroids

CG = coarsenGeometry(CG);
plotCentroids = @(pts, varargin) ...
   plot3(pts(:,1), pts(:,2), pts(:,3), varargin{:});
hold on
h=plotCentroids(CG.faces.centroids, 'k*');
hold off;
_images/showCoarseGrid_07.png

The coarse grid also contains lookup tables for mapping blocks and interfaces in the coarse grid to cells and faces in the fine grid. To demonstrate this, we visualize a single coarse face consisting of several fine faces along with the cells its block neighbors in red and blue respectively on the fine grid.

ace  = 66;
sub   = CG.faces.connPos(face):CG.faces.connPos(face+1)-1;
ff    = CG.faces.fconn(sub);
neigh = CG.faces.neighbors(face,:);

clf
show = false(1,CG.faces.num);
show(boundaryFaces(CG)) = true;
show(boundaryFaces(CG,neigh)) = false;
plotFaces(CG, show,'FaceColor',[1 1 .7]);
plotFaces(CG,boundaryFaces(CG,neigh),'FaceColor','none','LineWidth', 2);
plotFaces(G, ff, 'FaceColor', 'g')
plotGrid(G, p == neigh(1), 'FaceColor', 'none', 'EdgeColor', 'r')
plotGrid(G, p == neigh(2), 'FaceColor', 'none', 'EdgeColor', 'b')
view(3); axis off
_images/showCoarseGrid_08.png

Illustration of Geometry Computation for a Hexahedral Cell

Generated from showComputeGeometry.m

We go through the computation of geometry information, step by step, for a hexahedral cell with skewed geometry

Construct model

G = cartGrid([1 1 1]);
G.nodes.coords(1:end/2,:) = G.nodes.coords(1:end/2,:)*0.75;
G = computeGeometry(G);

Add face number, etc

clf
plotGrid(G,'FaceColor',[.7 .7 .7], 'FaceAlpha',.7);
hold on
plot3(G.faces.centroids(:,1),G.faces.centroids(:,2),G.faces.centroids(:,3),...
   'sr','MarkerSize', 24, 'LineWidth',1.5);
text(G.faces.centroids(:,1),G.faces.centroids(:,2),G.faces.centroids(:,3),...
   num2str((1:G.faces.num)'),'HorizontalAlignment','center',...
   'Color','r', 'FontWeight','demi','FontSize', 20);

plot3(G.nodes.coords(:,1),G.nodes.coords(:,2),G.nodes.coords(:,3),...
   'ob','MarkerSize', 24, 'LineWidth',1.5);
text(G.nodes.coords(:,1),G.nodes.coords(:,2),G.nodes.coords(:,3),...
   num2str((1:G.nodes.num)'),'HorizontalAlignment','center',...
   'Color','b', 'FontWeight','demi','FontSize', 20);
hold off
view(50,25), axis tight off
_images/showComputeGeometry_01.png

Computation of face areas, normals, and centroids

faceNo  = rldecode(1:G.faces.num, diff(G.faces.nodePos), 2) .';
nodes=[(1:numel(G.faces.nodes))' faceNo G.faces.nodes]; disp(nodes)
p = G.faces.nodePos;
n = (2:size(G.faces.nodes, 1)+1) .';
n(p(2 : end) - 1) = p(1 : end-1);
localEdge2Face = sparse(1 : numel(G.faces.nodes), faceNo, 1, ...
   numel(G.faces.nodes), G.faces.num);
pC = bsxfun(@rdivide, localEdge2Face.' * G.nodes.coords(G.faces.nodes,:), ...
   diff(double(G.faces.nodePos)));
pC = localEdge2Face * pC;
pt1 = G.nodes.coords(G.faces.nodes,:);
pt2 = G.nodes.coords(G.faces.nodes(n),:);
v1 = pt2 - pt1;
v2 = pC - pt1;
sN =  cross(v1,v2)./2;
sC = (pt1 + pt2 + pC)/3;
sA = sqrt(sum(sN.^2, 2));
N  = localEdge2Face.' * sN;
A  = localEdge2Face.' * sA;
C  = bsxfun(@rdivide, localEdge2Face.'* bsxfun(@times, sA, sC), A);
sNs = sign(sum(sN.*(localEdge2Face*N),2));
1     1     1
     2     1     3
     3     1     7
     4     1     5
     5     2     2
     6     2     4
     7     2     8
     8     2     6
...
clf,
T = [pt1 pt2 pC]; T = T(:,[1 4 7 2 5 8 3 6 9]);
patch(T(:,1:3)',T(:,4:6)',T(:,7:9)',[.7 .7 .7],'FaceAlpha',0.9);
i = 5:8;
hold on
quiver3(pt1(i,1),pt1(i,2),pt1(i,3),v1(i,1),v1(i,2),v1(i,3),'LineWidth',2);
quiver3(pt1(i,1),pt1(i,2),pt1(i,3),v2(i,1),v2(i,2),v2(i,3),'LineWidth',2);
quiver3(pt1(i,1),pt1(i,2),pt1(i,3),sN(i,1),sN(i,2),sN(i,3),'LineWidth',2);
plot3(sC(i,1)+.01,sC(i,2),sC(i,3),'or');
hold off;
view(50,25), axis tight off, zoom(1.2), set(gca,'zdir','reverse');
set(gca,'Clipping','off')
_images/showComputeGeometry_02.png
cla,
patch(T(:,1:3)',T(:,4:6)',T(:,7:9)',[.7 .7 .7],'FaceAlpha',0.9);
hold on
plot3(C(:,1),C(:,2),C(:,3),'.k','MarkerSize',20)
quiver3(C(2,1),C(2,2),C(2,3),N(2,1),N(2,2),N(2,3),'LineWidth',2);
hold off
_images/showComputeGeometry_03.png

Computing cell volumes and centroids

nF = G.cells.facePos(2)-G.cells.facePos(1);
inx = 1:nF;
faces = G.cells.faces(inx,1);
[triE, triF] = find(localEdge2Face(:,faces));
fC = C(faces,:);
cC = sum(fC)./double(nF);
relSubC = bsxfun(@minus, sC(triE,:),cC);
orient = 2*double(G.faces.neighbors(G.cells.faces(inx,1), 1) == 1) - 1;
oN = bsxfun(@times, sN(triE,:), sNs(triE).*orient(triF) );
lf
X = [G.nodes.coords; pC(G.faces.nodePos(1:end-1),:); cC];
T = [G.faces.nodes, G.faces.nodes(n), faceNo+G.nodes.num, ...
   repmat(G.nodes.num+G.faces.num+1,numel(faceNo),1)];
h=tetramesh(T,X); set(h,'FaceColor',[.7 .7 .7],'facealpha',.15);
hold on;
i = [5:12 17:20];
h=tetramesh(T([3 7 10 14],:),X); set(h,'FaceColor','y','facealpha',.9);
quiver3(sC(i,1),sC(i,2),sC(i,3),oN(i,1),oN(i,2),oN(i,3),'LineWidth',2);
quiver3(sC(i,1),sC(i,2),sC(i,3),...
   relSubC(i,1),relSubC(i,2),relSubC(i,3),'LineWidth',2);
hold off;
view(75,30), axis tight off, zoom(1.3), set(gca,'zdir','reverse');
set(gca,'Clipping','off')
_images/showComputeGeometry_04.png

Show Geometry Computation for a PEBI Cell

Generated from showComputeGeometryPEBI.m

We go through the computation of geometry information, step by step, for a polyhedral cell from a 2.5D PEBI model used in industry. The cell is not exactly the same one as shown in the book.

load data/pebi-cell.mat;

Add face number, etc

clf
plotGrid(G,'FaceColor',[.7 .7 .7], 'FaceAlpha',.7); set(gca,'zdir','normal');
hold on
plot3(G.faces.centroids(:,1),G.faces.centroids(:,2),G.faces.centroids(:,3),...
   'sr','MarkerSize', 30, 'LineWidth',1.5);
text(G.faces.centroids(:,1),G.faces.centroids(:,2),G.faces.centroids(:,3),...
   num2str((1:G.faces.num)'),'HorizontalAlignment','center',...
   'Color','r', 'FontWeight','demi','FontSize',24);

plot3(G.nodes.coords(:,1),G.nodes.coords(:,2),G.nodes.coords(:,3),...
   'ob','MarkerSize', 30, 'LineWidth',1.5);
text(G.nodes.coords(:,1),G.nodes.coords(:,2),G.nodes.coords(:,3),...
   num2str((1:G.nodes.num)'),'HorizontalAlignment','center',...
   'Color','b', 'FontWeight','demi','FontSize',24);
hold off
view(20,15), axis tight off, zoom(1.2)
set(gca,'Clipping','off')
_images/showComputeGeometryPEBI_01.png

Computation of face areas, normals, and centroids

faceNo  = rldecode(1:G.faces.num, diff(G.faces.nodePos), 2) .';
nodes=[(1:numel(G.faces.nodes))' faceNo G.faces.nodes]; disp(nodes)
p = G.faces.nodePos;
n = (2:size(G.faces.nodes, 1)+1) .';
n(p(2 : end) - 1) = p(1 : end-1);
localEdge2Face = sparse(1 : numel(G.faces.nodes), faceNo, 1, ...
   numel(G.faces.nodes), G.faces.num);
pC = bsxfun(@rdivide, localEdge2Face.' * G.nodes.coords(G.faces.nodes,:), ...
   diff(double(G.faces.nodePos)));
pC = localEdge2Face * pC;
pt1 = G.nodes.coords(G.faces.nodes,:);
pt2 = G.nodes.coords(G.faces.nodes(n),:);
v1 = pt2 - pt1;
v2 = pC - pt1;
sN =  cross(v1,v2)./2;
sC = (pt1 + pt2 + pC)/3;
sA = sqrt(sum(sN.^2, 2));
N  = localEdge2Face.' * sN;
A  = localEdge2Face.' * sA;
C  = bsxfun(@rdivide, localEdge2Face.'* bsxfun(@times, sA, sC), A);
sNs = sign(sum(sN.*(localEdge2Face*N),2));
1    1    6
    2    1   10
    3    1    8
    4    1    4
    5    1    2
    6    2    9
    7    2   10
    8    2    6
...
clf,
T = [pt1 pt2 pC]; T = T(:,[1 4 7 2 5 8 3 6 9]);
patch(T(:,1:3)',T(:,4:6)',T(:,7:9)',[.7 .7 .7],'FaceAlpha',0.9);
i = 1:5;
hold on
quiver3(pt1(i,1),pt1(i,2),pt1(i,3),v1(i,1),v1(i,2),v1(i,3),'LineWidth',2);
quiver3(pt1(i,1),pt1(i,2),pt1(i,3),v2(i,1),v2(i,2),v2(i,3),'LineWidth',2);
quiver3(pt1(i,1),pt1(i,2),pt1(i,3),sN(i,1),sN(i,2),sN(i,3),.003,'LineWidth',2);
plot3(sC(i,1)+.01,sC(i,2),sC(i,3),'or');
hold off;
view(20,15), axis tight off, zoom(1.3)
set(gca,'Clipping','off')
_images/showComputeGeometryPEBI_02.png
cla,
patch(T(:,1:3)',T(:,4:6)',T(:,7:9)',[.7 .7 .7],'FaceAlpha',0.9);
hold on
plot3(C(:,1),C(:,2),C(:,3),'.k','MarkerSize',20)
quiver3(C(1,1),C(1,2),C(1,3),N(1,1)/2e5,N(1,2)/2e5,N(1,3)/2e5,'LineWidth',2);
hold off
_images/showComputeGeometryPEBI_03.png

Computing cell volumes and centroids

nF = G.cells.facePos(2)-G.cells.facePos(1);
inx = 1:nF;
faces = G.cells.faces(inx,1);
[triE, triF] = find(localEdge2Face(:,faces));
fC = C(faces,:);
cC = sum(fC)./double(nF);
relSubC = bsxfun(@minus, sC(triE,:),cC);
orient = 2*double(G.faces.neighbors(G.cells.faces(inx,1), 1) == 1) - 1;
oN = bsxfun(@times, sN(triE,:), sNs(triE).*orient(triF) );
lf
X = [G.nodes.coords; pC(G.faces.nodePos(1:end-1),:); cC];
T = [G.faces.nodes, G.faces.nodes(n), faceNo+G.nodes.num, ...
   repmat(G.nodes.num+G.faces.num+1,numel(faceNo),1)];
h=tetramesh(T,X); set(h,'FaceColor',[.7 .7 .7],'facealpha',.15);
set(gca,'dataasp',[1 1 8e-3])
view(20,15), axis tight off, zoom(1.3), %set(gca,'zdir','reverse');
set(gca,'Clipping','off')
hold on;
ix = [2 18 19 20 21 27]
col = 'ywmrcgb';
for i=1:numel(ix)
   h=tetramesh(T(ix(i),:),X); set(h,'FaceColor',col(mod(i,7)+1),'FaceAlpha',1);
end
ix = [5 13 12 11 10 30]
for i=1:numel(ix)
   h=tetramesh(T(ix(i),:),X); set(h,'FaceColor',col(mod(i,7)+1),'FaceAlpha',1);
end
i=1:5;
h=quiver3(sC(i,1),sC(i,2),sC(i,3),oN(i,1),oN(i,2),oN(i,3),2e-2,'LineWidth',2);
quiver3(sC(i,1),sC(i,2),sC(i,3),...
   relSubC(i,1),relSubC(i,2),relSubC(i,3),.5,'LineWidth',2);
hold off
ix =

     2    18    19    20    21    27


ix =

...
_images/showComputeGeometryPEBI_04.png

Example of Realistic Corner-Point Geometry: SAIGUP

Generated from showGridSAIGUP.m

The <http://www.fault-analysis-group.ucd.ie/Projects/SAIGUP.html>SAIGUP project is a systematic assessment of uncertainty in reserves and production estimates within an objectively defined geological parameterisation encompassing the majority of European clastic oil reservoirs. A broad suite of shallow marine sedimentological reservoir types are indexed to continuously varying 3D anisotropy and heterogeneity levels. Structural complexity ranges from unfaulted to compartmentalised, and fault types from transmissive to sealing. Several geostatistical realisations each for the geologically diverse reservoir types covering the pre-defined parameter-space are up-scaled, faulted and simulated with an appropriate production strategy for an approximately 20 year period. Herein, we will inspect in detail one instance of the model, which can be downloaded from the <http://www.sintef.no/Projectweb/MRST>MRST webpage

Load data and convert units

We start by reading the model from a file in the Eclipse format (GRDECL) that can be read using the readGRDECL function. (If the model is not available the getDatasetPath function will download and install it for you).

grdecl = fullfile(getDatasetPath('SAIGUP'), 'SAIGUP.GRDECL');
grdecl = readGRDECL(grdecl);
usys   = getUnitSystem('METRIC');
grdecl = convertInputUnits(grdecl, usys);

Inspect full model

G = processGRDECL(grdecl, 'Verbose', true);
Adding 9600 artificial cells at top/bottom

Processing regular i-faces
 Found 79498 new regular faces
Elapsed time is 0.027229 seconds.

Processing i-faces on faults
 Found 521 faulted stacks
...
G = computeGeometry(G);
rock = grdecl2Rock(grdecl, G.cells.indexMap);

Inspect geometry

Construct pillars and corner-points to verify the resolution of the model

[X,Y,Z] = buildCornerPtPillars(grdecl,'Scale',true);            %#ok<NASGU>
dx = unique(diff(X)).'                                          %#ok<NOPTS>
[x,y,z] = buildCornerPtNodes(grdecl);                           %#ok<ASGLU>
dz = unique(reshape(diff(z,1,3),1,[]))                          %#ok<NOPTS>
clear x y z X Y Z
dx =

       -3000          75


dz =

...

Next, we look at small faces created to enforce a matching grid. Vertical faces that are not subdividided will be parallelograms with a 300 m^2 area. We therefore look at faces having smaller area

hist(G.faces.areas(G.faces.areas<300),100);
xlabel('Face area, m^2');
disp(['Area less than 0.01 m^2: ', num2str(sum(G.faces.areas<0.01))])
disp(['Area less than 0.10 m^2: ', num2str(sum(G.faces.areas<0.1))])
disp(['Area less than 1.00 m^2: ', num2str(sum(G.faces.areas<1))])
Area less than 0.01 m^2: 43
Area less than 0.10 m^2: 202
Area less than 1.00 m^2: 868
_images/showGridSAIGUP_01.png

Plot where these faces appear in the model

clf
plotGrid(G,'FaceColor','none','EdgeAlpha',0.1);
plotFaces(G,find(G.faces.tag>0 & G.faces.areas>=290),'y','edgea',0.1);
plotFaces(G,find(G.faces.areas<290),'r','edgea',0.1);
view(-105,45), axis tight off
_images/showGridSAIGUP_02.png

Process the model using a tolerance to get rid of the smallest faces

for a=[0.05 0.1 .25 0.5 1]
   G2 = processGRDECL(grdecl, 'Tolerance', a);
   G2 = computeGeometry(G2);
   disp([ a, min(G2.faces.areas)])
end
clear G2
0.0500    0.0318

    0.1000    0.0268

    0.2500    0.0966

    0.5000    0.6048

...

The layered structure

Show layered structure using a very simple trick: create a matrix with ones in all cells of the logical Cartesian grid and then do a cummulative summation in the vertical direction to get increasing values.

clf, args = {'EdgeColor','k','EdgeAlpha',0.1};
val = cumsum(ones(G.cartDims),3);
plotCellData(G,val(G.cells.indexMap),args{:});
view(-80,50), axis off tight
_images/showGridSAIGUP_03.png

Unfortunately, our attempt at visualizing the layered structure was not very successful. We therefore try to extract and visualize only the cells that are adjacent to a fault.

cellList = G.faces.neighbors(G.faces.tag>0, :);
cells    = unique(cellList(cellList>0));
cla,
plotCellData(G,val(G.cells.indexMap),cells,args{:});
view(-120,40)
_images/showGridSAIGUP_04.png

Restrict the plot to only parts of the grid using ismember, which uses O(n log n) operations

clear ijk
[ijk{1:3}] = ind2sub(G.cartDims, G.cells.indexMap); ijk = [ijk{:}];
[I,J,K] = meshgrid(1:9,1:30,1:20);
bndBox = find(ismember(ijk,[I(:), J(:), K(:)],'rows'));
inspect = cells(ismember(cells,bndBox));
cla,
plotCellData(G,val(G.cells.indexMap), inspect,'EdgeColor','k');
axis tight off
_images/showGridSAIGUP_05.png

Restrict the plot to only parts of the grid using logical indexing which uses O(n) operations

clear ijk
[ijk{1:3}] = ind2sub(G.cartDims, G.cells.indexMap);
I = false(G.cartDims(1),1); I(1:9)=true;
J = false(G.cartDims(2),1); J(1:30)=true;
K = false(G.cartDims(3),1); K(1:20)=true;

pick = I(ijk{1}) & J(ijk{2}) & K(ijk{3});
pick2 = false(G.cells.num,1); pick2(cells) = true;
inspect = find(pick & pick2);

cla,
plotCellData(G,val(G.cells.indexMap), inspect,'EdgeColor','k');
axis tight off
_images/showGridSAIGUP_06.png

Let us now try to also colour the faces of the model

cellno  = gridCellNo(G);
faces   = unique(G.cells.faces(pick(cellno), 1));
inspect = faces(G.faces.tag(faces)>0);
plotFaces(G,inspect,[.7 .7 .7],'edgec','r');
_images/showGridSAIGUP_07.png

Making a ribbon plot

lear ijk;
[ijk{1:3}] = ind2sub(G.cartDims, G.cells.indexMap);

% Ribbon in x-direction
I = false(G.cartDims(1),1); I(1:5:end)=true;
J = true(G.cartDims(2),1);
K = true(G.cartDims(3),1);

pickX = I(ijk{1}) & J(ijk{2}) & K(ijk{3});

% Ribbon in y-direction
I = true(G.cartDims(1),1);
J = false(G.cartDims(2),1); J(1:10:end) = true;
K = true(G.cartDims(3),1);

pickY = I(ijk{1}) & J(ijk{2}) & K(ijk{3});

clf,
plotCellData(G,rock.poro, pickX | pickY,'EdgeColor','k','EdgeAlpha',0.1);
view(-100,45), axis tight off
_images/showGridSAIGUP_08.png

Graphical View of the Grid Structure

Generated from showGridStructure.m

In this example, we will show details of the grid structures for three different grids: a Cartesian grid with one cell removed, a triangular grid, and a Voronoi grid. The examples will show cell numbers, node numbers, and face numbers.

REGULAR GRID

G = removeCells( cartGrid([3,2]), 2);
G = computeGeometry(G);

Plot cell, face, and node numbers

newplot;
plotGrid(G,'FaceColor',[0.95 0.95 0.95]); axis off;
hold on;
text(G.cells.centroids(:,1)-0.04, ...
   G.cells.centroids(:,2), num2str((1:G.cells.num)'),'FontSize',20);
plot(G.cells.centroids(:,1),...
   G.cells.centroids(:,2),'ok','MarkerSize',24);
text(G.faces.centroids(:,1)-0.045, G.faces.centroids(:,2), ...
   num2str((1:G.faces.num)'),'FontSize',16);
text(G.nodes.coords(:,1)-0.075, ...
   G.nodes.coords(:,2), num2str((1:G.nodes.num)'),'FontSize',18);
plot(G.nodes.coords(:,1),...
   G.nodes.coords(:,2),'sk','MarkerSize',24);
hold off;
_images/showGridStructure_01.png

Output content of cells.faces, faces.nodes, and faces.neighbors to file

faces =[ rldecode(1 : G.cells.num,diff(G.cells.facePos), 2).' G.cells.faces];
tag = {'East'; 'West'; 'South'; 'North'; 'Bottom'; 'Top'};
fp = fopen('G-structured.txt','w');
fprintf(fp,'cells.faces =\n');
for i=1:size(faces,1)
   fprintf(fp,' %3d %3d %3d [%s]\n', faces(i,1:3), tag{faces(i,3)});
end
nodes = [ rldecode(1:G.faces.num,diff(G.faces.nodePos), 2).' G.faces.nodes];
fprintf(fp,'\n\nfaces.nodes =\n');
fprintf(fp,' %3d %3d\n', nodes');
fprintf(fp,'\n\nfaces.neighbors =\n');
fprintf(fp,' %3d %3d\n', G.faces.neighbors');
fclose(fp);

UNSTRUCTURED TRIANGULAR GRID

clf;
p = sortrows([ 0.0, 1.0, 0.9, 0.1, 0.6, 0.3, 0.75; ...
               0.0, 0.0, 0.8, 0.9, 0.2, 0.6, 0.45]');
G = triangleGrid(p, delaunay(p(:,1),p(:,2)));
G = computeGeometry(G);

newplot;
plotGrid(G,'FaceColor',[0.95 0.95 0.95]); axis off;
hold on;
% centroids
text(G.cells.centroids(:,1)-0.01, G.cells.centroids(:,2)-0.01, ...
   num2str((1:G.cells.num)'),'FontSize',20);
plot(G.cells.centroids(:,1), G.cells.centroids(:,2),...
   'ok','MarkerSize',24);
% faces
text(G.faces.centroids(:,1)-0.02, G.faces.centroids(:,2)-0.01, ...
   num2str((1:G.faces.num)'),'FontSize',16);
% vertices
text(G.nodes.coords(:,1)-0.01, G.nodes.coords(:,2)-0.01, ...
   num2str((1:G.nodes.num)'),'FontSize',18);
plot(G.nodes.coords(:,1), G.nodes.coords(:,2),'sk','MarkerSize',24);
hold off;
_images/showGridStructure_02.png

Output content of cells.faces, faces.nodes, and faces.neighbors to file

fp = fopen('G-unstructured.txt','w');
faces =[ rldecode(1 : G.cells.num,diff(G.cells.facePos), 2).' G.cells.faces];
fprintf(fp,'cells.faces =\n');
fprintf(fp,' %3d %3d\n', faces');
nodes = [ rldecode(1:G.faces.num,diff(G.faces.nodePos), 2).' G.faces.nodes];
fprintf(fp,'\n\nfaces.nodes =\n');
fprintf(fp,' %3d %3d\n', nodes');
fprintf(fp,'\n\nfaces.neighbors =\n');
fprintf(fp,' %3d %3d\n', G.faces.neighbors');
fclose(fp);

UNSTRUCTURED VORONOI

clf;
p = [ 0.0, 1.0, 1.0, 0.0, 0.0; ...
      0.0, 0.0, 1.0, 1.0, 0.7]';
G = pebi(triangleGrid(p, delaunay(p(:,1),p(:,2))));
G = computeGeometry(G);
newplot;
plotGrid(G,'FaceColor',[0.95 0.95 0.95]); axis off;
%plotGrid(tri2grid( delaunay(p(:,1),p(:,2)), p), 'FaceColor','none','EdgeColor','r');
hold on;
% centroids
text(G.cells.centroids(:,1)-0.01, G.cells.centroids(:,2), ...
   num2str((1:G.cells.num)'),'FontSize',20);
plot(G.cells.centroids(:,1), G.cells.centroids(:,2),...
   'ok','MarkerSize',24);
% faces
text(G.faces.centroids(:,1)-0.02, G.faces.centroids(:,2)-0.01, ...
   num2str((1:G.faces.num)'),'FontSize',16);
% vertices
text(G.nodes.coords(:,1)-0.02, G.nodes.coords(:,2)-0.005, ...
   num2str((1:G.nodes.num)'),'FontSize',18);
plot(G.nodes.coords(:,1), G.nodes.coords(:,2),'sk','MarkerSize',24);
hold off;
_images/showGridStructure_03.png

Output content of cells.faces, faces.nodes, and faces.neighbors to file

p = fopen('G-voronoi.txt','w');
faces =[ rldecode(1 : G.cells.num,diff(G.cells.facePos), 2).' G.cells.faces];
fprintf(fp,'cells.faces =\n');
fprintf(fp,' %3d %3d\n', faces');
nodes = [ rldecode(1:G.faces.num,diff(G.faces.nodePos), 2).' G.faces.nodes];
fprintf(fp,'\n\nfaces.nodes =\n');
fprintf(fp,' %3d %3d\n', nodes');
fprintf(fp,'\n\nfaces.neighbors =\n');
fprintf(fp,' %3d %3d\n', G.faces.neighbors');
fclose(fp);
[y,x,z]  = peaks(15); z = z+8;
horizons = {struct('x',x,'y',y,'z',z),struct('x',x,'y',y,'z',z+8)};
grdecl   = convertHorizonsToGrid(horizons,'layers', 4);
G        = processGRDECL(grdecl);
figure, plotGrid(G); view(3); axis tight  off

figure
surf(horizons{1}.x,horizons{1}.y,horizons{1}.z, 'EdgeC','r','FaceC',[.8 .8 .8]),  hold on
mesh(horizons{2}.x,horizons{2}.y,horizons{2}.z, 'EdgeC','b','FaceC',[.7 .7 .7])
plot3([horizons{1}.x(:) horizons{2}.x(:)]',...
    [horizons{1}.y(:) horizons{2}.y(:)]',...
    [horizons{1}.z(:) horizons{2}.z(:)]',':k');
axis tight off, set(gca,'ZDir','reverse');
set(gca,'XLim',[-3.05 3.05]);
_images/showGriddedHorizons_01.png
_images/showGriddedHorizons_02.png
[n,m] = deal(30);
horizons = {struct('x',x,'y',y,'z',z),struct('x',x+.5,'y',2*y+1,'z',z+10)};
grdecl = convertHorizonsToGrid(horizons,'dims',[n m], 'layers', 3);
G = processGRDECL(grdecl);

figure
h1 = surf(horizons{1}.x,horizons{1}.y,horizons{1}.z-.1, ...
    'EdgeC','r','FaceC',[.8 .8 .8]);  hold on
h2 = mesh(horizons{2}.x,horizons{2}.y,horizons{2}.z, ...
    'EdgeC','b','FaceC',[.7 .7 .7]);

xmin = min(cellfun(@(h) min(h.x(:)), horizons));
xmax = max(cellfun(@(h) max(h.x(:)), horizons));
ymin = min(cellfun(@(h) min(h.y(:)), horizons));
ymax = max(cellfun(@(h) max(h.y(:)), horizons));

[xi,yi] = ndgrid(linspace(xmin,xmax,n+1), linspace(ymin,ymax,m+1));
hi = mesh(xi,yi,26*ones(size(xi)),'FaceC','none');
hg = plotGrid(G);
hb1 = plot3(...
    [-3  3  3 -3 -3 NaN -3 -3 NaN  3  3 NaN  3  3 NaN -3 -3], ...
    [-3 -3  3  3 -3 NaN -3 -3 NaN -3 -3 NaN  3  3 NaN  3  3],...
    [26 26 26 26 26 NaN 26  8 NaN 26  8 NaN 26  8 NaN 26  8],'r-','LineWidth',2);
hb2 = plot3(...
    [-2  4  4 -2 -2 NaN -2 -2 NaN  4  4 NaN  4  4 NaN -2 -2]-.5, ...
    [-5 -5  7  7 -5 NaN -5 -5 NaN -5 -5 NaN  7  7 NaN  7  7],...
    [26 26 26 26 26 NaN 26 18 NaN 26 18 NaN 26 18 NaN 26 18],'b-','LineWidth',2);
axis tight off, view(-140,30)
_images/showGriddedHorizons_03.png

Hierarchical Coarsening

Generated from showHierCoarsen.m

This is a relatively simple example that illustrates the basic idea behind hierarchical coarsening. The model has two geological properties, unit and lithofacies assemblage (LFA), that are used to generate permeability. These two partitions are applied recursively together with a nested set of uniform partitions with permeability as an indicator.

mrstModule add coarsegrid agglom

% Load facies data
exdir = fullfile(mrstPath('agglom'), 'examples');
imload = @(fn) ...
   flipud(double(sum(imread(fullfile(exdir, 'data', fn)), 3))) .';
f  = imload('facies1.png'); f(2,16)=max(f(:));
pl = 3-compressPartition(f(:) + 1);


% Create grid
G  = computeGeometry(cartGrid(size(f))); clear f;
x = G.cells.centroids;

% Make unit data
pu = (x(:,2)>15+0.25*x(:,1))+1;

% Make fault blocks
faults = [12:42:29*42 1692:41:2840 432:41:1630];
pf = processPartition(G, ones(G.cells.num,1), faults);

% Generate and plot a simple permeability model
rng(1000);
K  = 1 + (pl-1)*8+.3*pu.*randn(G.cells.num,1); I = K-min(K)+.1;
figure(1); clf,
plotCellData(G,I,'EdgeColor','none');
axis equal off; colormap(.8*jet(128)+.2*ones(128,3))
_images/showHierCoarsen_01.png

Apply the geological features recursively

In addition to the geological features, we create a hierarchy of refined partitions that will be used to further subdivide the high-permeable LFA

pc = ones(G.cells.num,3);
for i=1:4
    pc(:,i) = partitionCartGrid(G.cartDims,[5 5].*2^(i-1));
end
p = applySuccessivePart(processPartition(G,pu,faults),G, I, 8, [pl pc]);

% Visualize results
figure(2); clf, set(gcf,'Position',[670 490 760 470]);
subplot(2,3,1)
plotCellData(G, pl, 'EdgeColor','none'); axis equal off
subplot(2,3,2);
plotCellData(G, pu, 'EdgeColor','none'); axis equal off
subplot(2,3,3)
plotCellData(G, pl+2*(pu-1), 'EdgeColor','none'); axis equal off
plotFaces(G, faults,'EdgeColor','k','LineWidth',1)
subplot(2,3,5); cla
plotCellData(G, pl+2*(pu-1),'EdgeColor','none'); axis equal off
outlineCoarseGrid(G, p, 'k','LineWidth',1);
colormap([.5 .5 1; .2 .2 .8; .3 .6 .6; .1 .4 .4]);
_images/showHierCoarsen_02.png

Merge small blocks outside of high-permeable lithofacies

We merge all blocks that have three cells or less in the first LFA, which is assumed to have less permeability than the second lithofacies. To this end, we remove cell connections within LFA two as well as connections across different units, lithofacies, and faults.

subplot(2,3,6), cla
plotCellData(G, pl+2*(pu-1),'EdgeColor','none'); axis equal off

qb = [0; processPartition(G,pl+2*(pu-1), faults)];
ql = [0; pl];
T = 2*(qb(G.faces.neighbors(:,1)+1)==qb(G.faces.neighbors(:,2)+1))-1;
T(ql(G.faces.neighbors(:,1)+1)==2) = -1;
pm = mergeBlocksByConnections(G, p, T, 4);

outlineCoarseGrid(G,pm, 'k','LineWidth',1);

for i=[1:3 5:6],
    subplot(2,3,i),
    set(gca,'Position',get(gca,'Position')+[-.025 -.025 .05 .05]);
end
_images/showHierCoarsen_03.png

Example 1: Cartesian grid with radial refinement

Generated from showHybridGrids.m

Pw = [];
for r = exp(-3.5:.2:0),
    [x,y,z] = cylinder(r,28); Pw = [Pw [x(1,:); y(1,:)]];
end
Pw = [Pw [0; 0]];
Pw1 = bsxfun(@plus, Pw, [2; 2]);
Pw2 = bsxfun(@plus, Pw, [12; 6]);
[x,y] = meshgrid(0:.5:14, 0:.5:8);
P = unique([Pw1'; Pw2'; x(:) y(:)], 'rows');
G = pebi(triangleGrid(P));
plotGrid(G);
_images/showHybridGrids_01.png

Example 1: Cartesian grid with refinement in the middle

Generated from showMultiBlockGrid.m

clf
G1 = cartGrid([ 4  4],[1 1]);
G2 = cartGrid([ 8  8],[1 1]);
G3 = cartGrid([12  4],[3 1]);

% Glue middle part
G1 = translateGrid(G1,[0 1]); plotGrid(G1,'FaceColor',[1 .9 .9]);
G2 = translateGrid(G2,[1 1]); plotGrid(G2,'FaceColor',[.9 1 .9]);
G = glue2DGrid(G1, G2);
G1 = translateGrid(G1,[2 0]); plotGrid(G1,'FaceColor',[1 .9 .9]);
G = glue2DGrid(G, G1);

% Glue top and bottom
G = glue2DGrid(G, G3);        plotGrid(G3,'FaceColor',[.9 .9 1]);
G3 = translateGrid(G3,[0 2]); plotGrid(G3,'FaceColor',[.9 .9 1]);
G = glue2DGrid(G3, G); %#ok<NASGU>
%print -depsc2 multiBlock-1a.eps;
_images/showMultiBlockGrid_01.png

Repeat the whole process without intermediate plotting

G1 = cartGrid([ 5  5],[1 1]);
G2 = cartGrid([20 20],[1 1]);
G3 = cartGrid([15  5],[3 1]);

G = glue2DGrid(G1, translateGrid(G2,[1 0]));
G = glue2DGrid(G,  translateGrid(G1,[2 0]));
G = glue2DGrid(G3, translateGrid(G, [0 1]));
G = glue2DGrid(G,  translateGrid(G3,[0 2]));
G = twister(G);
clf
plotGrid(G,'FaceColor','none');
%print -depsc2 multiBlock-1b.eps;
_images/showMultiBlockGrid_02.png

Example 2: Cartesian grid with triangular/PEBI refinement

Construct unstructured grid

[N,M]=deal(10,15);
xv = linspace(0,1,N+1);
yv = linspace(0,1,M+1);
[x,y] = ndgrid(xv, yv);
x(2:N,2:M) = x(2:N,2:M) + 0.3*randn(N-1,M-1)*max(diff(xv));
y(2:N,2:M) = y(2:N,2:M) + 0.3*randn(N-1,M-1)*max(diff(yv));
G2 = computeGeometry(triangleGrid([x(:) y(:)]));

% Set orientation of faces
hf = G2.cells.faces(:,1);
hf2cn = gridCellNo(G2);
sgn = 2*(hf2cn == G2.faces.neighbors(hf, 1)) - 1;
N   = bsxfun(@times, sgn, G2.faces.normals(hf,:));
N   = bsxfun(@rdivide, N, G2.faces.areas(hf,:));
n   = zeros(numel(hf),2); n(:,1)=1;

% Add cell tags
G2.cells.faces(:,2) = zeros(size(hf));
i = sum(N.*n,2)==-1; G2.cells.faces(i,2) = 1;
i = sum(N.*n,2)== 1; G2.cells.faces(i,2) = 2;
n = n(:,[2 1]);
i = sum(N.*n,2)==-1; G2.cells.faces(i,2) = 3;
i = sum(N.*n,2)== 1; G2.cells.faces(i,2) = 4;

% Glue grids together
G = glue2DGrid(G1, translateGrid(G2,[1 0]));
G = glue2DGrid(G,  translateGrid(G1,[2 0]));
G = glue2DGrid(G3, translateGrid(G, [0 1]));
G = glue2DGrid(G,  translateGrid(G3,[0 2]));
G = twister(G);
clf
plotGrid(G,'FaceColor','none');
%print -depsc2 multiBlock-2a.eps;
_images/showMultiBlockGrid_03.png

Repeat with Voronoi grid instead

[N,M]=deal(8,12);
xv = linspace(0,1,N+1);
yv = linspace(0,1,M+1);
[x,y] = ndgrid(xv, yv);
x(2:N,2:M) = x(2:N,2:M) + 0.3*randn(N-1,M-1)*max(diff(xv));
y(2:N,2:M) = y(2:N,2:M) + 0.3*randn(N-1,M-1)*max(diff(yv));
G2 = computeGeometry(pebi(triangleGrid([x(:) y(:)])));

% Set orientation of faces
hf = G2.cells.faces(:,1);
hf2cn = gridCellNo(G2);
sgn = 2*(hf2cn == G2.faces.neighbors(hf, 1)) - 1;
N   = bsxfun(@times, sgn, G2.faces.normals(hf,:));
N   = bsxfun(@rdivide, N, G2.faces.areas(hf,:));
n   = zeros(numel(hf),2); n(:,1)=1;

G2.cells.faces(:,2) = zeros(size(hf));
i = sum(N.*n,2)==-1; G2.cells.faces(i,2) = 1;
i = sum(N.*n,2)== 1; G2.cells.faces(i,2) = 2;
n = n(:,[2 1]);
i = sum(N.*n,2)==-1; G2.cells.faces(i,2) = 3;
i = sum(N.*n,2)== 1; G2.cells.faces(i,2) = 4;

% Refine G3 in the x-direction and rescale in y-direction
G3 = cartGrid([24 5], [3 .25]);
G = glue2DGrid(G1, translateGrid(G2,[1 0]));
G = glue2DGrid(G,  translateGrid(G1,[2 0]));
G = glue2DGrid(G3, translateGrid(G, [0 .25]));
G = glue2DGrid(G,  translateGrid(G3,[0 1.25]));
G = twister(G);
clf
plotGrid(G,'FaceColor','none');
%print -depsc2 multiBlock-2b.eps;
_images/showMultiBlockGrid_04.png

Example 3: Extruded grid rotated

G = glue2DGrid(G1, translateGrid(G2,[0 1]));
G = glue2DGrid(G,  translateGrid(G1,[0 2]));
G = makeLayeredGrid(G, 5);
G.nodes.coords = G.nodes.coords(:,[3 1 2]);
clf, plotGrid(G,'FaceColor',[1 1 1]); view(115,20); axis off
%print -depsc2 multiBlock-3.eps;
_images/showMultiBlockGrid_05.png

Examples of Voronoi / Perpendicular Bisector (PEBI) grids

Generated from showPEBI.m

In this script, we go through several examples of polyhedral grids in 3D.

Plot of Voronoi grid + pillars

[x,y] = meshgrid((0:2)*2*cos(pi/6),0:2);
x = [x(:); x(:)+cos(pi/6)];
y = [y(:); y(:)+sin(pi/6)];
a = [0 4.3 0 2.5 0 1.5];

clf
plot3(x,y,zeros(size(x)),'o'); view(40,30); axis tight off;
T = triangleGrid([x(:),y(:)]);
T.nodes.coords(:,3) = zeros(size(T.nodes.coords(:,1)));
plotGrid(T,'FaceColor','none'); view(40,30);
axis(a); axis equal tight off;
set(gca,'zdir','normal');
_images/showPEBI_01.png
G = pebi(T);
G.nodes.coords(:,3) = zeros(size(G.nodes.coords(:,1)));
plotGrid(G,'FaceColor','none','EdgeColor','r'); set(gca,'zdir','normal');
_images/showPEBI_02.png
newplot
plotGrid(G, 'FaceColor',[.9 .4 .4]); axis(a); axis off
hold on

G1 = G;
G1.nodes.coords(:,3) = 1.5;
G1.nodes.coords(:,1:2) = 0.925*G.nodes.coords(:,1:2);
plot3([G.nodes.coords(:,1) G1.nodes.coords(:,1)]', ...
   [G.nodes.coords(:,2) G1.nodes.coords(:,2)]', ...
   [G.nodes.coords(:,3) G1.nodes.coords(:,3)]', '-k','LineWidth',1);
set(gca,'zdir','normal'); view(40,30),
axis(a), axis equal tight off
_images/showPEBI_03.png
G1 = G;
G1.nodes.coords(:,3) = 1;
G1.nodes.coords(:,1:2) = 0.95*G1.nodes.coords(:,1:2);
plotGrid(G1, 'FaceColor',[.4 .4 .9]); set(gca,'zdir','normal');
hold off
_images/showPEBI_04.png

Make a good radial grid

newplot
P = [];
for r = exp(-3.5:0.25:0),
   [x,y,z] = cylinder(r,10); P = [P [x(1,:); y(1,:)]];          %#ok<AGROW>
end
P = unique(P','rows');
G = makeLayeredGrid(pebi(triangleGrid(P)), 5);
plotGrid(G,'FaceColor',[.8 .8 .8]); view(30,50), axis tight off
_images/showPEBI_05.png

Make a grid draped over peaks

x,y] = meshgrid((0:6)*2*cos(pi/6),0:7);
x = [x(:); x(:)+cos(pi/6)]; x=(x - mean(x(:)))/2;
y = [y(:); y(:)+sin(pi/6)]; y=(y - mean(y(:)))/2;
G = pebi(triangleGrid([x(:),y(:)]));
G.nodes.coords(:,3) = -peaks(G.nodes.coords(:,1),G.nodes.coords(:,2));
clf,
plotGrid(G); view(25,50), axis tight off
_images/showPEBI_06.png

How to Partition Grids

Generated from showPartitions.m

In this script, we go through several examples that demonstrate various methods for partitioning grids using MRST

mrstModule add coarsegrid

Partition a Cartesian 2D grid

We use partitionUI which exploits the logical structure and creates a uniform grid in logical space.

figure
G = cartGrid([7,7]);
p = partitionUI(G, [2,2]);
plotCellData(G, p, 'EdgeColor', 'y');
outlineCoarseGrid(G, p, 'k');
axis tight off,
caxis([.5 max(p)+.5]);
colormap(0.5*(lines(max(p))+ones(max(p),3)));
set(colorbar,'YTick',1:max(p));
_images/showPartitions_01.png

Partition a 3D grid in much the same manner

figure
G = cartGrid([10,10,4]);
p = partitionUI(G, [3,3,2]);

plotCellData(G, p, 'Edgecolor', 'w');
outlineCoarseGrid(G, p, 'EdgeColor','k','lineWidth',4);
colormap(.5*(colorcube(max(p)) + ones(max(p),3)));
view(3);
axis off
_images/showPartitions_02.png

Partition according to polar coordinate

figure
G = cartGrid([11, 11],[2,2]);
G.nodes.coords = ...
   bsxfun(@minus, G.nodes.coords, 1);
G = computeGeometry(G);
c = G.cells.centroids;
[th,r] = cart2pol(c(:,1),c(:,2));
p = mod(round(th/pi*4)+4,4)+1;
p(r<.3) = max(p)+1;

% Plot partition
plotCellData(G,p,'EdgeColor',[.7 .7 .7]);
outlineCoarseGrid(G,p,'k');
caxis([.5 max(p)+.5]);
colormap(.5*(jet(max(p))+ones(max(p),3)));
set(colorbar,'YTick',1:max(p),'FontSize',16);
axis off

% Split blocks that are multiply connected into a set of singly connected
% blocks and plot the new partition
p = processPartition(G, p);
figure
plotCellData(G,p,'EdgeColor',[.7 .7 .7]);
outlineCoarseGrid(G,p,'k');
caxis([.5 max(p)+.5]);
colormap(.5*(jet(max(p))+ones(max(p),3)));
set(colorbar,'YTick',1:max(p),'FontSize',16);
axis off
_images/showPartitions_03.png
_images/showPartitions_04.png

Combine a facies and a Cartesian partition

clear
G = cartGrid([20, 20], [1 1]);
G = computeGeometry(G);

f  = @(c) sin(4*pi*(c(:,1)-c(:,2)));
pf = 1 + (f(G.cells.centroids) > 0);
pc = partitionCartGrid(G.cartDims, [4 4]);

% Alternative 1:
[b,~,p] = unique([pf, pc], 'rows' );

% Alternative 2:
q = compressPartition(pf + max(pf)*pc);                         %#ok<NASGU>

% Plot results
figure
plotCellData(G,p);
outlineCoarseGrid(G, p, 'k','LineWidth',2);
axis off
colormap(.5*(jet(64)+ones(64,3)))
_images/showPartitions_05.png

Partition a cup-formed grid

igure
x = linspace(-2,2,41);
G = tensorGrid(x,x,x);
G = computeGeometry(G);
c = G.cells.centroids;
r = c(:,1).^2 + c(:,2).^2+c(:,3).^2;
G = removeCells(G, (r>1) | (r<0.25) | (c(:,3)<0));
plotGrid(G); view(15,60); axis tight off

% Make the partition vector contiguous:
figure
p = partitionUI(G,[5 5 4]);
subplot(2,1,1); bar(accumarray(p,1)); shading flat
q = compressPartition(p);
subplot(2,1,2); bar(accumarray(q,1)); shading flat
set(gca,'XLim',[0 100]);

% Visualize the partition using an exploding view
figure
explosionView(G,q,.4);
view(15,60); axis tight off
colormap(colorcube(max(q)));
_images/showPartitions_06.png
_images/showPartitions_07.png
_images/showPartitions_08.png
grdecl = simpleGrdecl([4, 2, 3], .12, 'flat', true);
[X,Y,Z] = buildCornerPtPillars(grdecl,'Scale',true);
[x,y,z] = buildCornerPtNodes(grdecl);

Plot pillars

Generated from showPillarGrid.m

plot3(X',Y',Z','k'); drawAxisCross(.15); axis tight;
set(gca,'zdir','reverse'), view(35,35), axis off, zoom(1.2);
set(gca,'dataaspectRatio',[1.8,1.8,1])
set(gca,'Clipping','off')
% print -deps2 showCPgrid-pillars.eps;
_images/showPillarGrid_01.png

Plot points on pillars, mark pillars with faults red

hold on; I=[3 8 13];
hpr = plot3(X(I,:)',Y(I,:)',Z(I,:)','r','LineWidth',2);
hpt = plot3(x(:),y(:),z(:),'o');
hold off;
% print -depsc2 showCPgrid-pts.eps;
_images/showPillarGrid_02.png

Create grid and plot two stacks of cells

G = processGRDECL(grdecl);
args = {'FaceColor'; 'r'; 'EdgeColor'; 'k'};
hcst = plotGrid(G,[1:8:24 7:8:24],'FaceAlpha', .1, args{:});
% print -dpng showCPgrid-cells.png;
_images/showPillarGrid_03.png

Plot cells and fault surface

elete([hpt; hpr; hcst]);
plotGrid(G,'FaceAlpha', .15, args{:});
plotFaces(G, G.faces.tag>0,'FaceColor','b','FaceAlpha',.4);
% print -dpng showCPgrid-grid.png;
_images/showPillarGrid_04.png
load(fullfile('data','showProcessPartition.mat'));

Plot grid

Generated from showProcessPartition.m

figure, plotGrid(G,c,'FaceColor',0.7*col+[.3 .3 .3]);
text(G.cells.centroids(c,1), G.cells.centroids(c,2),...
    num2str((1:numel(c))'),'HorizontalAlignment','center','FontSize',20);
axis off tight
_images/showProcessPartition_01.png

Plot original adjacency matrix

figure,
val = ones(size(c));
val(p(1:r(2)-1))=1; val(p(r(2):r(3)-1))=2;
hold on;
i1 = val(ii)==1; plot(ii(i1),jj(i1),'or','MarkerSize',6,'MarkerFaceColor','r');
i2 = val(ii)==2; plot(ii(i2),jj(i2),'ob','MarkerSize',6,'MarkerFaceColor','b');
set(gca,'YDir','reverse','FontSize',14); axis square tight
hold off
_images/showProcessPartition_02.png

Plot permuted adjacency matrix

figure,
n(p) = 1:numel(c);
hold on;
plot(n(ii(i1)),n(jj(i1)),'or','MarkerSize',6,'MarkerFaceColor','r');
plot(n(ii(i2)),n(jj(i2)),'ob','MarkerSize',6,'MarkerFaceColor','b');
plot([12.5 12.5 NaN .5 24.5],[.5 24.5 NaN 12.5 12.5],'--k');
set(gca,'YDir','reverse','FontSize',14); axis square tight
hold off
_images/showProcessPartition_03.png

Simple 2D Cartesian grids

Generated from showStructGrids.m

show a graded grid

clf;
dx = 1-0.5*cos((-1:0.1:1)*pi); x = -1.15+0.1*cumsum(dx);
y = 0:0.05:1;
G = tensorGrid(x, sqrt(y));
plotGrid(G); axis([-1.05 1.05 -0.05 1.05]);
_images/showStructGrids_01.png

Random perturbations

clf;
nx = 6; ny=12;
G = cartGrid([nx, ny]); subplot(1,2,1); plotGrid(G);

c = G.nodes.coords;
I = or( or(any(c==0,2), any(c(:,1)==nx,2)), any(c(:,2)==ny,2));
G.nodes.coords(~I,:) = c(~I,:) + 0.6*rand(sum(~I),2)-0.3;
subplot(1,2,2); plotGrid(G);
_images/showStructGrids_02.png

Example grid: twister

clf;
G = cartGrid([30, 20]);
G.nodes.coords = twister(G.nodes.coords);
G = computeGeometry(G);
plotCellData(G, G.cells.volumes, 'EdgeColor', 'k'), colorbar
_images/showStructGrids_03.png

removeCells: Create an ellipsoid grid

lf;
x = linspace(-2,2,21);
G = tensorGrid(x,x,x);
subplot(1,2,1); plotGrid(G);view(3); axis equal

subplot(1,2,2); plotGrid(G,'FaceColor','none','LineWidth',0.01);
G = computeGeometry(G);
c = G.cells.centroids;
r = c(:,1).^2 + 0.25*c(:,2).^2+0.25*c(:,3).^2;
G = removeCells(G, find(r>1));
plotGrid(G); view(-70,70); axis equal
_images/showStructGrids_04.png

Tessellations of 2D space

Generated from showTessellation.m

We consider a set of different examples of tessellations of increasing complexity. The first is just a standard Cartesian grid based on a regular quadrilaterals. The next three are n-polygonal extensions of a regular triangular tessellation.

colormap(.6*parula(128)+.4*ones(128,3));
_images/showTessellation_01.png

Example 1: Cartesian grid

This grid is formed by laying out regular quadrilaterals

[nx,ny] = deal(15,10);
[x,y] = meshgrid(linspace(0,1,nx+1),linspace(0,1,ny+1));
p = [x(:) y(:)];
n = (nx+1)*(ny+1);
I = reshape(1:n,ny+1,nx+1);
T = [
    reshape(I(1:end-1,1:end-1),[],1)';
    reshape(I(1:end-1,2:end  ),[],1)';
    reshape(I(2:end,  2:end  ),[],1)';
    reshape(I(2:end,  1:end-1),[],1)'
    ]';

G = tessellationGrid(p, T);
clf, plotGrid(G);
_images/showTessellation_02.png

Example 2: convex/concave hexagonal tiles

We first generate the two hexagonal tiles. Then, we identify three symmetry lines (that together make up a triangle for each tile) and use these to fit the tiles together in space.

% Construct two symmetric convex/concave hexagonal tiles
[dx, dy, dPhi] = deal(cos(pi/3),sin(pi/3), pi*15/180);
v = pi/180*[0 120 240]';
dv = [cos(v-dPhi) sin(v-dPhi) cos(v+dPhi) sin(v+dPhi)]/2;
P = [ 0           0           0           0;
      0+dv(1,1)   0+dv(1,2)   0+dv(1,3)   0+dv(1,4);
      1           0           1           0;
      1+dv(2,1)   0+dv(2,2)   1+dv(2,3)   0+dv(2,4);
      dx          dy          dx          dy;
      dx+dv(3,1)  dy+dv(3,2)  dx+dv(3,3)  dy+dv(3,4)];
P1 = P(:,1:2);
P2 = [P([1 6:-1:2],3) -P([1 6:-1:2],4)];
clf
plotCellData(tessellationGrid([P1; P2], reshape(1:12,6,2)'),(1:2)',...
    'EdgeColor','k');

% Add help triangles that give the symmetry-directions we will use to fit
% the tiles together
P = [0 0; 1 0; dx dy];
Gt = triangleGrid([P; P([1 3 2],1), -P([1 3 2],2)],reshape(1:6,3,2)');
plotGrid(Gt,'FaceColor','none'); axis equal off;
plotGrid(Gt,'FaceColor','none','LineWidth',2);
axis equal off
% print -depsc2 convexConcave-1.eps;
_images/showTessellation_03.png

To make a full tiling, we make a basic pattern consisting of four triangles and then use this pattern to tile as much of space as we want

[p,t,n] = deal([],[],0);
T  = reshape(1:24,6,4)';
for j=0:1
    for i=0:3
        p = [p; bsxfun(@plus,P1,[i 2*j*dy])];                          %#ok<AGROW>
        p = [p; bsxfun(@plus,P2,[i-dx (2*j+1)*dy])];                   %#ok<AGROW>
        p = [p; bsxfun(@plus,P1,[i-dx (2*j+1)*dy])];                   %#ok<AGROW>
        p = [p; bsxfun(@plus,P2,[i 2*(j+1)*dy])];                      %#ok<AGROW>
        t = [t; T+n]; n=n+24;                                          %#ok<AGROW>
    end
end

[p,~,ic] = unique(round(p*1e5)/1e5,'rows');
G = tessellationGrid(p, ic(t));
i=repmat((1:2)',G.cells.num/2,1);
clf; plotCellData(G,i(:));
%plotFaces(G,find(any(G.faces.neighbors==0,2)),'EdgeColor','r','LineWidth',2);
axis equal tight off; drawnow
% print -depsc2 convexConcave-2.eps;
_images/showTessellation_04.png

Example 3: nonagonal tiles

Using the same approach as in the previous example, we can make a set of nonagonal tiles

[dx, dy, dPhi] = deal(cos(pi/3),sin(pi/3), pi*40/180);
v = pi/180*[0 180 120 -60 240 60]';
dv = [cos(v-dPhi) sin(v-dPhi) cos(v+dPhi) sin(v+dPhi)]/3.5;
P = [...
    0           0           0           0;
    0+dv(1,1)   0+dv(1,2)   0+dv(1,3)   0+dv(1,4);
    1+dv(2,1)   0+dv(2,2)   1+dv(2,3)   0+dv(2,4);
    1           0           1           0;
    1+dv(3,1)   0+dv(3,2)   1+dv(3,3)   0+dv(3,4);
    dx+dv(4,1)  dy+dv(4,2)  dx+dv(4,3)  dy+dv(4,4);
    dx          dy          dx          dy;
    dx+dv(5,1)  dy+dv(5,2)  dx+dv(5,3)  dy+dv(5,4);
     0+dv(6,1)  0+dv(6,2)   0+dv(6,3)   0+dv(6,4);
    ];
m  = size(P,1);
P1 = P(:,1:2);
P2 = [P([1 m:-1:2],3) -P([1 m:-1:2],4)];
T  = reshape(1:4*m,m,4)';

[p,t,n] = deal([],[],0);
for j=0:2
    for i=0:4
        p = [p; bsxfun(@plus,P1,[i 2*j*dy])];                              %#ok<AGROW>
        p = [p; bsxfun(@plus,P2,[i-dx (2*j+1)*dy])];                       %#ok<AGROW>
        p = [p; bsxfun(@plus,P1,[i-dx (2*j+1)*dy])];                       %#ok<AGROW>
        p = [p; bsxfun(@plus,P2,[i 2*(j+1)*dy])];                          %#ok<AGROW>
        t = [t; T+n]; n=n+4*m;                                             %#ok<AGROW>
    end
end

[p,ia,ic] = unique(round(p*1e5)/1e5,'rows');
G = tessellationGrid(p, ic(t));
i=repmat((1:2)',G.cells.num/2,1);
clf
plotCellData(G,i(:));
%plotFaces(G,find(any(G.faces.neighbors==0,2)),'LineWidth',2);
axis tight off;
_images/showTessellation_05.png

Example 4: pentadecagonal tiles

In this example, we use a slightly different approach. We first create a regular triangular tiling, and then we go through the line segments of the triangles and add one by one point

% Basic patterns consisting of uniform triangles
[dx, dy] = deal(cos(pi/3),sin(pi/3));
P1 = [0 0; 1 0; dx dy];
P2 = [P1([1 3 2],1) -P1([1 3 2],2)];
[p,t,n] = deal([],[],0);
for j=0:1
    for i=0:3
        p = [p; bsxfun(@plus,P1,[i 2*j*dy])];                              %#ok<AGROW>
        p = [p; bsxfun(@plus,P2,[i-dx (2*j+1)*dy])];                       %#ok<AGROW>
        p = [p; bsxfun(@plus,P1,[i-dx (2*j+1)*dy])];                       %#ok<AGROW>
        p = [p; bsxfun(@plus,P2,[i 2*(j+1)*dy])];                          %#ok<AGROW>
        t = [t; [1:3; 4:6; 7:9; 10:12]+n]; n=n+12;                         %#ok<AGROW>
    end
end

%{
 % In case you just want to show two polygons as in the book
 p = [P1; P2];
 t = reshape(1:6,3,2)';
%}

We then extract the end-points on each edge and compute the angle the corresponding line makes with the x-axis. We will use this information to perturb the points. By using this orientation of the lines, we can easily make sure that the perturbations are introduced in the correct direction.

dPhi   = pi*35/180;
e      = reshape(t(:,[1 2 2 3 3 1])',2,[])';
v      = p(e(:,2),:) - p(e(:,1),:);
phi    = atan2(v(:,2),v(:,1));
P      = p;

T         = reshape(e',6,[])';
T         = T(:,[1 3 5]);
[Pp,~,ic] = unique(round(P*1e5)/1e5, 'rows','stable');
G         = tessellationGrid(Pp,ic(T));
i=repmat((1:2)',G.cells.num/2,1);
clf
plotCellData(G,i(:));
axis equal off
% print -depsc2 trigon.eps;
_images/showTessellation_06.png

Add the first set of points, making an irregular hexagonal tile

pn     = p(e(:,1),:) + 1/3*[cos(phi-dPhi)  sin(phi-dPhi)];
e      = e(:,[1 1 2]);
e(:,2) = size(P,1)+(1:size(pn,1)).';
P      = [P; pn];

T         = reshape(e',9,[])';
T         = T(:,[1:2 4:5 7:8]);
[Pp,~,ic] = unique(round(P*1e5)/1e5, 'rows','stable');
G         = tessellationGrid(Pp,ic(T));
i=repmat((1:2)',G.cells.num/2,1);
clf
plotCellData(G,i(:));
axis equal off
% print -depsc2 hexagon.eps;
_images/showTessellation_07.png

Reshape into irregular nonagons

pn     = p(e(:,1),:) + 1/3*[cos(phi-.2*dPhi)  sin(phi-.2*dPhi)];
e      = e(:,[1:3 3]);
e(:,3) = size(P,1)+(1:size(pn,1)).';
P      = [P; pn];

T         = reshape(e',12,[])';
T         = T(:,[1:3 5:7 9:11]);
[Pp,~,ic] = unique(round(P*1e5)/1e5, 'rows','stable');
G         = tessellationGrid(Pp,ic(T));
i=repmat((1:2)',G.cells.num/2,1);
clf
plotCellData(G,i(:));
axis equal off
% print -depsc2 nonagon.eps;
_images/showTessellation_08.png

Reshape into irregular dodecagons

phi    = atan2(-v(:,2),-v(:,1));
pn     = p(e(:,4),:) + 1/3*[cos(phi-.2*dPhi)  sin(phi-.2*dPhi)];
e      = e(:,[1:4 4]);
e(:,4) = size(P,1)+(1:size(pn,1)).';
P      = [P; pn];

T         = reshape(e',15,[])';
T         = T(:,[1:4 6:9 10:14]);
[Pp,~,ic] = unique(round(P*1e5)/1e5, 'rows','stable');
G         = tessellationGrid(Pp,ic(T));
i=repmat((1:2)',G.cells.num/2,1);
clf
plotCellData(G,i(:));
axis equal off
% print -depsc2 dodecagon.eps;
_images/showTessellation_09.png

Reshape into irregular pentadecagon

e      = e(:,[1:5 5]);
pn     = p(e(:,5),:) + 1/3*[cos(phi-dPhi) sin(phi-dPhi)];
e(:,5) = size(P,1)+(1:size(pn,1)).';
P      = [P; pn];

T         = reshape(e',18,[])';
T         = T(:,[1:5 7:11 13:17]);
[P,~,ic] = unique(round(P*1e5)/1e5, 'rows','stable');
G         = tessellationGrid(P,ic(T));
i=repmat((1:2)',G.cells.num/2,1);
clf
plotCellData(G,i(:));
axis equal off;
% print -depsc2 pentadecagon.eps;
_images/showTessellation_10.png
%{

Triangulation of a set of random points

Generated from showTriangularGrids.m

clf
p = rand(10,2);
t = delaunayn(p);
G = triangleGrid(p,t);
plotGrid(G,'FaceColor','none');
hold on; plot(p(:,1),p(:,2),'o'); hold off; axis off;
_images/showTriangularGrids_01.png

Triangulation of a rectangular mesh

[x,y] = meshgrid(1:10,1:8);
t = delaunay(x(:),y(:));
G = triangleGrid([x(:) y(:)],t);
plot(x(:),y(:),'o','MarkerSize',8);
plotGrid(G,'FaceColor','none');
axis([.9 10 0.9 8]); axis off;
_images/showTriangularGrids_02.png
t = delaunayn([x(:) y(:)]);
G = triangleGrid([x(:) y(:)], t);
plot(x(:),y(:),'o','MarkerSize',8);
plotGrid(G,'FaceColor','none');
axis([.9 10 0.9 8]); axis off;
_images/showTriangularGrids_03.png

Triangulation of a set of perturbed mesh points

clf;
N=7; M=5; K=3;
[x,y,z] = ndgrid(0:N,0:M,0:K);
x(2:N,2:M,:) = x(2:N,2:M,:) + 0.3*randn(N-1,M-1,K+1);
y(2:N,2:M,:) = y(2:N,2:M,:) + 0.3*randn(N-1,M-1,K+1);
p = [x(:) y(:) z(:)];
t = delaunayn(p);
G = tetrahedralGrid(p,t);
plotGrid(G, 'FaceColor',[.8 .8 .8]); view(-40,60); axis tight off;
_images/showTriangularGrids_04.png

Seamount: A standard example from Matlab

lf
load seamount
t = delaunay(x,y);
G = triangleGrid([x(:) y(:)], t);
plot(x(:),y(:),'o');
plotGrid(G,'FaceColor','none'); axis off
_images/showTriangularGrids_05.png

Illustrate the correspondence between Voronoi and Delaunay

Generated from showVoronoiGrids.m

N = 7; M=5;
[x,y]=ndgrid(0:N,0:M);
x(2:N,2:M) = x(2:N,2:M) + 0.3*randn(N-1,M-1);
y(2:N,2:M) = y(2:N,2:M) + 0.2*randn(N-1,M-1);
%
subplot(2,2,1);
plot(x,y,'ok');
axis equal tight off;
%
subplot(2,2,2);
t = delaunay(x,y);
triplot(t,x,y);
hold on; plot(x,y,'ok'); hold off;
axis equal tight off;
%
subplot(2,2,3);
voronoi(x,y);
axis equal tight off;
p=get(gca,'position'); p(2)=p(2)+0.1; set(gca,'position',p);
%
subplot(2,2,4);
h = voronoi(x,y,'b'); set(h,'LineWidth',1);
hold on; triplot(t,x,y,'r','LineWidth',0.25); hold off;
axis equal tight off;
p=get(gca,'position'); p(2)=p(2)+0.1; set(gca,'position',p);
_images/showVoronoiGrids_01.png

Examples of Voronoi grids

clf;
%
% Regular grid
[x,y] = meshgrid(0:10,0:8);
voronoi(x,y);
axis([1 8 1 5]); axis equal off;
_images/showVoronoiGrids_02.png

Regular grid

[x,y] = meshgrid(0:10,0:8);
x = [x(:); x(:)+0.5];
y = [y(:); y(:)+0.5];
voronoi(x,y);
axis([1 8 1 5]); axis equal off;
_images/showVoronoiGrids_03.png

Honeycomb grids

[x,y] = meshgrid((0:10)*2*cos(pi/6),0:8);
x = [x(:); x(:)+cos(pi/6)];
y = [y(:); y(:)+sin(pi/6)];
voronoi(x,y);
axis([1 8 1 5]); axis equal off;
_images/showVoronoiGrids_04.png

Triangulation of a set of perturbed mesh points

clf;
N=7; M=5;
[x,y] = ndgrid(0:N,0:M);
x(2:N,2:M) = x(2:N,2:M) + 0.3*randn(N-1,M-1);
y(2:N,2:M) = y(2:N,2:M) + 0.3*randn(N-1,M-1);
p = [x(:) y(:)];
T = triangleGrid(p,delaunayn(p));
V = makeLayeredGrid(pebi(T), 3);
plotGrid(V, 'FaceColor',[.8 .8 .8]); view(-40,60); axis tight off;
_images/showVoronoiGrids_05.png

Seamount: A standard example from Matlab

clf
load seamount
V = pebi( triangleGrid([x(:) y(:)], delaunay(x,y)));
plotGrid(V,'FaceColor','none'); axis off;
_images/showVoronoiGrids_06.png

A honeycombed grid

lf
[x,y] = meshgrid((0:4)*2*cos(pi/6),0:3);
x = [x(:); x(:)+cos(pi/6)];
y = [y(:); y(:)+sin(pi/6)];
G = triangleGrid([x(:),y(:)],delaunay(x(:),y(:)));
plotGrid(pebi(G), 'FaceColor','none'); axis equal off
_images/showVoronoiGrids_07.png

Classic test case: 1D Buckley-Leverett

Generated from buckleyLeverett1D.m

We investigate the effect of time step on the accuracy and convergence of the implicit transport solver

mrstModule add incomp

G = computeGeometry(cartGrid([100,1]));
rock = makeRock(G, 100*milli*darcy, 0.2);

fluid = initSimpleFluid('mu' , [   1,    1] .* centi*poise     , ...
                        'rho', [1000, 1000] .* kilogram/meter^3, ...
                        'n'  , [   2,    2]);
bc = fluxside([], G, 'Left',   1, 'sat', [1 0]);
bc = fluxside(bc, G, 'Right', -1, 'sat', [0 1]);

hT = computeTrans(G, rock);
rSol = initState(G, [], 0, [0 1]);
rSol = incompTPFA(rSol, G, hT, fluid, 'bc', bc);

% Explicit tranport solver
rSole = explicitTransport(rSol, G, 10, rock, fluid, 'bc', bc, 'verbose', true);

% Implicit transport solver: try with one time step
[rSoli, report] = ...
    implicitTransport(rSol, G, 10, rock, fluid, 'bc', bc, 'Verbose', true);
explicitTransport: Computing transport step in 199 substeps

implicitTransport:
----------------------------------------------------------------------
Time interval (s)     iter  relax   residual          rate
----------------------------------------------------------------------
 [0.0e+00, 1.0e+01]:   1    1.00    7.82670e+00              NaN
 [0.0e+00, 1.0e+01]:   2    0.12    6.81721e+00             0.07
...

Load and display convergence history

Here we have cheated a little: That is, we have copied the screendump to a file and used a text editor to manipulate it so that it can easily be reloaded

figure;
d = load('screendump.dat');
i = isnan(d(:,1)); d(:,1) = 1; d(i,1)=0; d(:,1)=cumsum(d(:,1));
plot(d(:,3),d(:,1),'o-','MarkerFaceColor',[0.5,0.5,0.5]);
set(gca,'XScale','log','YDir','reverse','XDir','normal'); axis tight
set(gca,'XTick',[1e-10 1e-5 1],'FontSize',12);
hold on;
j = find(i);
hold on
plot(repmat([8e-11 10],numel(j),1)',[d(j,1)+.5 d(j,1)+.5]','--r');
hold off
set(gcf,'Position',[1120 55 230 760],'PaperPositionMode','auto');
_images/buckleyLeverett1D_01.png

Plot results with various number of time steps

figure
plot(G.cells.centroids(:,1), rSole.s(:,1),'k--','LineWidth',1.5);
leg = cell(7,1); leg{1} = 'Expl: 199 steps';

n   = [4 10 20 40 100 200];
its = [0 0 0 0 0 0 0];
col = 'rgbcmk';
hold on
for k=1:numel(n)
    rSolt = rSol;
    for i=1:n(k)
        [rSolt, report] = ...
            implicitTransport(rSolt, G, 10/n(k), rock, fluid, 'bc', bc);
        its(k) = its(k) + report.iterations + report.vasted_iterations;
    end
    plot(G.cells.centroids(:,1),rSolt.s(:,1), [col(k) '-'],'LineWidth',1.5);
    leg{k+1} = sprintf('n=%3d: %3d its',n(k),its(k));
end
hold off
legend(leg{:});
_images/buckleyLeverett1D_02.png

Two-Phase Flow in Inclined Gravity Column

Generated from buoyancyExample.m

In this example, we simulate the injection of a light fluid (CO2) into a heavier fluid (brine) inside an inclined sandbox.

mrstModule add incomp

Set up model

To get an inclined reservoir, we manipulate the gravity direction. Since gravity is a persistent and global variable, it is important that we reset the gravity at the end of the script

theta  = 40;
height = 40;
exmpl  = 2;
n      = [ 20,  2,  100];
box_sz = [100, 10, 200];

% Grid
G  = cartGrid(n, box_sz);
G  = computeGeometry(G);
CG = cartGrid([1 1 1],box_sz); % used to create outline of sandbox

% Petrophysical data
if exmpl == 1
    rock   = makeRock(G, 0.1*darcy, 0.3);
else
    load rndseed.mat; rng(S);
    b = log(milli*darcy);
    a = (log(darcy)-b)/(.4 - .05);
    p = gaussianField(G.cartDims, [0.05 0.4], [3 1 11], 4.5);
    K = exp(a*(p-.05)+b);
    rock = makeRock(G, K(:), p(:));
end
T  = computeTrans(G, rock, 'verbose', true);

% Fluid
fluid  = initSimpleFluid('mu' , [  0.307,   0.049] .* centi*poise     , ...
                         'rho', [973    , 617    ] .* kilogram/meter^3, ...
                         'n'  , [  2    ,   2    ]);

% Redefine gravity direction
R = makehgtform('yrotate',-pi*theta/180);
gravity reset on
gravity( R(1:3,1:3)*gravity().' );

% Create special colormap
s  = linspace(0, 1, 64).';
cm = [1-s.^(13/16), 1-s.^6, s.^6];
Computing one-sided transmissibilities...     Elapsed time is 0.004786 seconds.

Set initial data and compute pressure distribution

Put region of CO2 at bottom of reservoir.

xr = initResSol(G, 1*barsa, 1);
d  = gravity() ./ norm(gravity);
dc = G.cells.centroids * d.';
xr.s(dc>max(dc)-height) = 0;
xr = incompTPFA(xr, G, T, fluid);

Plot initial data

clf
h = plotGrid(CG, 'FaceColor', 'none', 'EdgeColor', 'k','LineWidth',2);
rotate(h,[0 1 0],theta);
view([0,0])
hs = plotCellData(G, xr.s, xr.s < .995, 'EdgeColor', 'none');
rotate(hs,[0 1 0],theta);
caxis([0 1]); colormap(cm), axis equal tight off

% dPlot = [20 100 250 500 1000 1500 2500 inf]*day;
% print('-dpng', '-r0', sprintf('buoy-%d-%02d.png',exmpl, 0));
_images/buoyancyExample_01.png

Run simulation

For accuracy, the time step is gradually ramped up

dT = [.5, .5, 1, 1, 1, 2, 2, 2, 5, 5, 10, 10, 15, 20, repmat(25,[1,97])].*day;
[t, ip] = deal(0,1);
for k = 1 : numel(dT)
   xr = implicitTransport(xr, G, dT(k), rock, fluid, 'Verbose', false);

   % Check for inconsistent saturations
   assert (max(xr.s) < 1+eps && min(xr.s) > -eps);

   % Increase time and plot saturation
   t = t + dT(k);
   delete(hs)
   hs = plotCellData(G, xr.s, xr.s <.995, 'EdgeColor', 'none');
   rotate(hs,[0 1 0],theta);
   title(sprintf('%.2f days', t/day));
   drawnow

   %{
   if t>=dPlot(ip)-eps,
       colorbar off; title([]);
       drawnow;
       print('-dpng', '-r0', sprintf('buoy-%d-%02d.png',exmpl, ip));
       colorbar, title(sprintf('%.2f days', t/day));
       ip = ip+1;
   end
   %}

   % Compute new flow field.
   xr = incompTPFA(xr, G, T, fluid);
end
_images/buoyancyExample_02.png

NB: RESET GRAVITY

Gravity is defined as a persistent and global variable and we therefore need to reset it to avoid messing with other examples

gravity reset on

Capillary Equilibrium within Vertical Columns

Generated from capillaryColumn.m

We watch a sharp interface form into a capillary fringe in the vertical direction

mrstModule add incomp

Grid, permeability, and fluid object

gravity reset on
exmpl = 1;
G  = computeGeometry(cartGrid([20, 1, 40], [100 1 100]));
if exmpl==1
   perm  = @(x) (350*x/100 + 50).*milli*darcy;
   rock  = makeRock(G, perm(G.cells.centroids(:,1)), .1);
   dT    = .01;
   histb = false;
else
   load rndseed.mat; rng(S);
   b = log(milli*darcy);
   a = (log(darcy)-b)/(.4 - .05);
   p = gaussianField(G.cartDims, [0.05 0.4], [3 1 11], 4.5);
   K = exp(a*(p-.05)+b);
   rock = makeRock(G, K(:), p(:));
   dT = .1;
   histb = true;
end
hT = computeTrans(G, rock);

fluid = initSimpleFluidJfunc('mu' , [0.30860, 0.056641]*centi*poise, ...
      'rho', [ 975.86,  686.54]*kilogram/meter^3, ...
      'n' , [      2,       2], ...
      'surf_tension',1*barsa/sqrt(mean(rock.poro)/(mean(rock.perm))),...
      'rock',rock);

Initial data

t = 0;
xr = initResSol(G, 100.0*barsa, 0.0);
xr.s(G.cells.centroids(:,3)>50) = 1.0;

Plot permeability and prepare for saturation

clf, set(gcf,'Position',[0   450  1280   370]);
cax1 = subplot(1,3,1);
colormap(cax1, parula);
if exmpl==2
    plotCellData(G,log10(rock.perm),'EdgeColor','none'); view(0,0), axis tight
    [h,az] = colorbarHist(log10(rock.perm),[-14 -12],'South');
    set(h,'XTick',-14:-12,'XTickLabel',{'10', '100', '1000'});
else
    K = convertTo(rock.perm,milli*darcy);
    plotCellData(G,K,'EdgeColor','none'); view(0,0), axis tight
    [h,az] = colorbarHist(K,[50 400],'South',40);
end
set(az,'Position',get(az,'Position')-[0 0 0 .02]);

cax2 = subplot(1,3,2);
colormap(cax2, [zeros(128,1) linspace(.8,0,128)' linspace(0,.7,128)']);
_images/capillaryColumn_01.png

Time loop

dt = dT*[1 1 2 2 3 3 4 4 repmat(5,[1,96])]*year;
dt = [dt(1).*sort(repmat(2.^-[1:5 5],1,1)) dt(2:end)];
s  = xr.s(:,1);
for k = 1 : numel(dt)
   xr = incompTPFA(xr, G, hT, fluid);
   xr = implicitTransport(xr, G, dt(k), rock, fluid);
   t  = t+dt(k);

   % Plot solution
   cla
   plotCellData(G,xr.s(:,1),'EdgeColor','none');
   view(0,0); axis tight; caxis([0 1]);
   title(sprintf('time: %.1f yrs', t/year));
   drawnow

   ds = norm(s - xr.s(:,1),inf);
   if ds<1e-4, break, end
   fprintf('%e\n',ds);
   s = xr.s(:,1);

end
2.621472e-01
1.036592e-01
8.716126e-02
1.040406e-01
1.202392e-01
1.399498e-01
1.539844e-01
1.686116e-01
...
_images/capillaryColumn_02.png

Draw plot of pc versus S

subplot(1,3,3)
plot(xr.s, fluid.pc(xr)/barsa,'o');
_images/capillaryColumn_03.png

Simulate model with water coning

Generated from coningExample.m

mrstModule add incomp coarsegrid ad-core

Make grid and assign petrophysical properties

G = cartGrid([60,40,10],[1500 1000 200]);
G.nodes.coords(:,3) = G.nodes.coords(:,3)+2050;
G = computeGeometry(G);

[x0,x1,z0,z1] = deal(675,1250,2050,2250);
flt = @(c) (c(:,1)-x0)*(z1-z0)/(x1-x0) + z0 - c(:,3);

rock = makeRock(G, 500*milli*darcy, .2);
rock.perm(flt(G.cells.centroids)>0) = 50*milli*darcy;
rock.poro(flt(G.cells.centroids)>0) = 0.1;

hT = computeTrans(G,rock);

pargs = {'EdgeAlpha',.1,'EdgeColor','k'};
clf, hs = plotCellData(G,rock.perm,pargs{:});
view(3), axis tight
_images/coningExample_01.png

Setup wells

x = G.cells.centroids(:,[1 3]);
W = addWell([], G, rock, find(sum(bsxfun(@minus,x,[67.5 2060]).^2,2)<320), ...
            'InnerProduct', 'ip_tpf', ...
            'Type', 'bhp', 'Val', 100*barsa, ...
            'Comp_i', [0 1], 'Name', 'P', 'Dir','y');

x = G.cells.centroids(:,1:2);
W = addWell(W,  G, rock, find(sum(bsxfun(@minus,x,[1437.5 487.5]).^2,2)<320),  ...
            'InnerProduct', 'ip_tpf',...
            'Type', 'bhp', 'Val', 700*barsa, ...
            'Comp_i', [1 0], 'Name', 'I', 'Dir','z');

plotWell(G,W,'height',50,'radius',.01);

CG = generateCoarseGrid(G,(flt(G.cells.centroids)>0)+1);
plotFaces(CG,1:CG.faces.num,'FaceColor','none','LineWidth',1);
plotFaces(CG,11,'FaceColor','y','FaceAlpha',.3);
set(hs,'FaceAlpha',.35);
% zoom(1.4);
set(gca,'dataasp',[2 2 1]); view(25,30);
_images/coningExample_02.png

Fluid model

fluid = initSimpleFluid('mu' , [   1,  10] .* centi*poise     , ...
                        'rho', [1000, 100] .* kilogram/meter^3, ...
                        'n'  , [   2,   2]);

Simulation loop

N  = 450;
T  = 4500*day();
dT = T/N*ones(N,1);
dT = [dT(1)*sort(2.^-[1:4 4])'; dT(2:end)];

gravity reset on
rSol = initState(G, W, 0, [0, 1]);
rSol = incompTPFA(rSol, G, hT, fluid, 'wells', W);

t = 0;
pv = poreVolume(G, rock);
oip = sum(rSol.s(:,2).*pv);
colormap(flipud(winter))
wellSols = cell(numel(dT),1);
set(gca,'XTick',[],'Ytick',[],'ZTick',[]);
%
for i=1:numel(dT)
   rSol = implicitTransport(rSol, G, dT(i), rock, fluid, 'wells', W);

   % Check for inconsistent saturations
   assert(max(rSol.s(:,1)) < 1+eps && min(rSol.s(:,1)) > -eps);

   % Update solution of pressure equation.
   rSol  = incompTPFA(rSol , G, hT, fluid, 'wells', W);

   % Measure water saturation in production cells in saturation
   wellSols{i} = getWellSol(W, rSol, fluid);

   % Increase time
   t = t + dT(i);

   % Plot saturation
   delete(hs);
   hs = plotCellData(G, rSol.s(:,1), (rSol.s(:,1)>.01), pargs{:});
   title([num2str(convertTo(t,day)),  ' days']),
   caxis([0 1]); drawnow

end
_images/coningExample_03.png

Oil rate, with peak production indicated by red line

figure,
[Ym,Tm] = meshgrid(G.cells.centroids(W(1).cells,2),cumsum(dT)/year);
p       = cellfun(@(x) abs(x(1).qO)', wellSols,'UniformOutput',false);
p       = vertcat(p{:})*day;
[~,j]   = max(p);
m       = sub2ind(size(p),j,1:numel(W(1).cells));
surf(Ym,Tm,p); shading interp; colormap(parula(20));
hold on; plot3(Ym(m),Tm(m),p(m), '-r','LineWidth',2); hold off
axis tight, view(100,35)
_images/coningExample_04.png

Water production, with breakthrough indicated by red line

figure,
p  = cellfun(@(x) abs(x(1).qW)', wellSols,'UniformOutput',false);
p  = vertcat(p{:})*day;
j  = sum(~(p>1e-3));
m  = sub2ind(size(p),j,1:numel(W(1).cells));
surf(Ym,Tm,p); shading interp; colormap(parula(20));
hold on; plot3(Ym(m),Tm(m),p(m), '-r','LineWidth',2); hold off
axis tight, view(45,35)
_images/coningExample_05.png

Plot surface rates etc using GUI

plotWellSols(wellSols, cumsum(dT));

%{
%% Oil surface rate
t = cumsum(dT)/day;
qOs = cellfun(@(x) abs(x(1).qOs), wellSols);
figure, set(gca,'Fontsize',24)
plot(t, qOs,'LineWidth',2);
xlabel('Time [days]'); title('Oil surface rate [m^3/s]');
axis tight

%%
figure, set(gca,'Fontsize',24)
plot(t,cumsum(qOs.*dT),'LineWidth',2);
hold on, plot(t([1 end]),[oip oip],'--r','LineWidth',2); hold off
xlabel('Time [days]'); title('Cumulative oil production [m^3]');
axis tight

%% Water surface rate
qWs(:,1) = cellfun(@(x) abs(x(1).qWs), wellSols);
qWs(:,2) = cellfun(@(x) abs(x(2).qWs), wellSols);
figure, set(gca,'Fontsize',24)
plot(t, qWs,'LineWidth',2);
xlabel('Time [days]'); title('Water surface rate [m^3/s]');
axis tight

%%
figure, set(gca,'Fontsize',24)
plot(t,[cumsum(qWs(:,1).*dT) cumsum(qWs(:,2).*dT)],'LineWidth',2);
hold on, plot(t([1 end]),[oip oip],'--r','LineWidth',2); hold off
xlabel('Time [days]'); title('Cumulative oil production [m^3]');
set(gca,'XLim',t([1 end]));
%}
_images/coningExample_06.png

Grid-orientation effects for the five-spot

Generated from gridOrientationQ5.m

With a two-point type discretization (and with many other discretizations as well), evolving displacement profiles will preferrentially move along the axial directions, i.e., in the direction of the normals to the cell faces. To illustrate this, we contrast approximate solutions for the repeated five-spot well pattern computed using the original quarter five-spot setup and with a rotated setup with injectors in the SW and NE corners and producers in the SE and NW corners. The two setups will have different preferrential flow directions and hence generally give different solutions.

mrstModule add incomp

Difference in grid-orientation effects with mobility ratio

This effect is typically associated with instable displacements. To show this we consider three different fluid models with unfavorable mobility ratio, equal viscosities, and favorable mobility ratio.

mu       = [1 10; 1 1; 10 1];
pvi      = [0.3 0.6 0.7];
cartDim  = [32 32 1];
nstep    = 16;
set(gcf,'Position',[250 450 1100 360]);
for n=1:3
    fluid = initSimpleFluid('mu', mu(n,:).*centi*poise, ...
        'rho', [1000,  850].* kilogram/meter^3, 'n', [2, 2]);
    subplot(1,3,n)
    runQ5DiagParal(cartDim, nstep, fluid, pvi(n));
    set(gca,'XTick',[],'YTick',[]);
    title(sprintf('Mobility ratio %d:%d. Time: %.1f PVI',...
        mu(n,1),mu(n,2),pvi(n)),'FontSize',12); drawnow
end
_images/gridOrientationQ5_01.png

Convergence with respect to time step

m = 1;
fluid = initSimpleFluid('mu', mu(m,:).*centi*poise, ...
    'rho', [1000,  850].* kilogram/meter^3, 'n', [2, 2]);
nsteps = [1 8 64];
for n=1:3
     subplot(1,3,n)
     runQ5DiagParal(cartDim, nsteps(n), fluid, pvi(m));
     set(gca,'XTick',[],'YTick',[]);
     title(sprintf('Grid: %d x %d. Steps: %d', ...
         cartDim(1:2), nsteps(n)),'FontSize',12); drawnow
end
_images/gridOrientationQ5_02.png

Convergence with respect to spatial resolution

m = 1;
fluid = initSimpleFluid('mu', mu(m,:).*centi*poise, ...
    'rho', [1000,  850].* kilogram/meter^3, 'n', [2, 2]);
cartDims = [16 16 1; 32 32 1; 64 64 1];
for n=1:3
     subplot(1,3,n)
     runQ5DiagParal(cartDims(n,:), nstep, fluid, pvi(m));
     set(gca,'XTick',[],'YTick',[]);
     title(sprintf('Grid: %d x %d. Steps: %d', ...
         cartDims(n,1:2), nstep),'FontSize',12); drawnow
end
_images/gridOrientationQ5_03.png

Inverted Gravity Column

Generated from invertedGravityColumn.m

We consider a setup with a heavier fluid placed on top of a lighter fluid. The lighter fluid will move upward and the heavier fluid will move downward and gradually we will approach a stable steady state.

mrstModule add incomp

Define the model

gravity reset on
G     = computeGeometry(cartGrid([1, 1, 40], [1, 1, 10]));
rock  = makeRock(G, 0.1*darcy, 1);
fluid = initCoreyFluid('mu' , [0.30860, 0.056641]*centi*poise     , ...
                       'rho', [ 975.86,  686.54]*kilogram/meter^3, ...
                        'n' , [      2,       2],...
                        'sr', [     .1,      .2],...
                        'kwm',[  .2142,    .85]);

Initialize problem

Set up the fluid distribution, compute pressure

hT = computeTrans(G, rock);
xr = initResSol(G, 100.0*barsa, 1.0); xr.s(end/2+1:end) = 0.0;
xr = incompTPFA(xr, G, hT, fluid); x0 = xr;

% Plot the 3D solution
figure(1);
subplot(1,3,1:2);
plotCellData(G,xr.s(:,1),'EdgeColor','none');
set(gca,'XTick',[],'YTick',[],'Ztick',[]); box on; view(3); caxis([0 1]);
title(sprintf('Time: %.2f years', 0));
colormap(flipud(parula));

% Plot 2D profile of the solution
subplot(1,3,3);
plot(xr.s(:,1),G.cells.centroids(:,3),'-o',...
    'MarkerSize',8,'MarkerFaceColor',[.5,.5,.5]);
set(gca,'XTick',[],'YTick',[],'YDir','reverse');

%n=0; print('-depsc2',['inv-column-' num2str(n) '.eps']);

figure(2);
col = jet(16); k=1;
plot(G.cells.centroids(:,3), xr.pressure,'-o','MarkerSize',2,'color',col(k,:),'LineWidth',1.5);
_images/invertedGravityColumn_01.png
_images/invertedGravityColumn_02.png

Solve the problem

We use a number of time steps to march the solution towards steady state. Since we do not have any movement in the lateral directions, we can use the explicit solver as a pure gravity segregation solver.

dt = 5*day; t=0;
S = zeros(G.cells.num,151); S(:,1) = xr.s(:,1);
for i=1:150

    % Saturation step
    xr = explicitTransport(xr, G, dt, rock, fluid, 'onlygrav', true);
    t = t+dt;

    % Plot 3D solution
    figure(1);
    subplot(1,3,1:2); cla
    plotCellData(G,xr.s(:,1),'EdgeColor','none'), box on; view(3);
    set(gca,'XTick',[],'YTick',[],'Ztick',[]); caxis([0 1]);
    title(sprintf('Time: %.2f years', t/year));

    % Plot 2D profile of the saturation
    subplot(1,3,3);
    plot(xr.s(:,1),G.cells.centroids(:,3),'-o',...
        'MarkerSize',8,'MarkerFaceColor',[.5,.5,.5]);
    set(gca,'XTick',[],'YTick',[],'YDir','reverse');
    drawnow; pause(0.1);

    %if any(i==[25 50 75 100 150])
    %  n=n+1; print('-depsc2',['inv-column-' num2str(n) '.eps']);
    %end

    if ~mod(i,10)
        k = k+1;
        figure(2); hold on;
        plot(G.cells.centroids(:,3), xr.pressure,'-','Color',col(k,:),'LineWidth',1.5);
        hold off
    end

    % Compute pressure for the next step
    xr = incompTPFA(xr, G, hT, fluid);
    S(:,i+1)=xr.s(:,1);
end
figure(2);
hold on
plot(G.cells.centroids(:,3), xr.pressure,'+','Color',col(k,:),'MarkerSize',6);
hold off
_images/invertedGravityColumn_03.png
_images/invertedGravityColumn_04.png
figure;
pcolor(S'); shading interp;
colormap(flipud(parula));
set(gca,'YDir','reverse');
box on; %set(gca,'XTick',[],'YTick',[]);
_images/invertedGravityColumn_05.png

Simulate model with gravity override

Generated from overrideExample.m

We consider a model consisting of two zones of different permeability. A light fluid of high mobility is injected into the lower zone by a vertical well placed near the east side. Fluids are produced from a well placed near the west side and perforated in the lower zone only.

mrstModule add incomp coarsegrid ad-core

Make grid and assign petrophysical properties

The grid has two zones with different permeabilities: high permeability on top and low below, or opposite if inequality sign is reversed in the definition of layer function

G = cartGrid([60,40,10],[1500 1000 200]);
G.nodes.coords(:,3) = G.nodes.coords(:,3)+2050;
G = computeGeometry(G);

[K1,K2,p1,p2] = deal(200,1000,.1,.3);
layer = @(c) (c(:,3)-2150)<0; % <0: high perm on top, >0: low on top

rock = makeRock(G, K1*milli*darcy, p1);
rock.poro(layer(G.cells.centroids)) = p2;
rock.perm(layer(G.cells.centroids)) = K2*milli*darcy;

hT = computeTrans(G,rock);

clf
pargs = {'EdgeAlpha',.1,'EdgeColor','k'};
hs = plotCellData(G,rock.perm,pargs{:});
view(3), axis tight
set(hs,'FaceAlpha',.35);
% zoom(1.4);
set(gca,'dataasp',[2 2 1]); view(27,-12);
_images/overrideExample_01.png

Setup wells

Both wells are perforated in the lower zone only.

T  = 1500*day();
x = G.cells.centroids(:,1:2);
W = addWell([], G, rock,...
            find( sum(bsxfun(@minus,x,[67.5 487.5]).^2,2)<320 ...
                  & G.cells.centroids(:,3)>2150),  ...
            'InnerProduct', 'ip_tpf', ...
            'Type', 'bhp', 'Val', 100*barsa, ...
            'Comp_i', [0 1], 'Name', 'P', 'Dir','z');

x = G.cells.centroids(:,1:2);
W = addWell(W,  G, rock, ...
            find( sum(bsxfun(@minus,x,[1437.5 487.5]).^2,2)<320 ...
                  & G.cells.centroids(:,3)>2150),  ...
            'InnerProduct', 'ip_tpf',...
            'Type', 'rate', 'Val', .8*sum(poreVolume(G,rock))/T, ...
            'Comp_i', [1 0], 'Name', 'I', 'Dir','z');

plotWell(G,W,'height',75,'radius',.01);

CG = generateCoarseGrid(G,(layer(G.cells.centroids)>0)+1);
plotFaces(CG,1:CG.faces.num,'FaceColor','none','LineWidth',1);
plotFaces(CG,11,'FaceColor','y','FaceAlpha',.3);
_images/overrideExample_02.png

Fluid model

Inject a light and mobilt fluid into a denser and less mobile fluid

fluid = initSimpleFluid('mu' , [  .1,   1] .* centi*poise     , ...
                        'rho', [ 700,1000] .* kilogram/meter^3, ...
                        'n'  , [   2,   2]);

Simulation loop

N  = 100;
dT = T/N*ones(N,1);
dT = [dT(1)*sort(2.^-[1:4 4])'; dT(2:end)];

gravity reset on
rSol = initState(G, W, 0, [0, 1]);
rSol = incompTPFA(rSol, G, hT, fluid, 'wells', W);

t = 0;
colormap(flipud(winter))
wellSols = cell(numel(dT),1);
set(gca,'XTick',[],'Ytick',[],'ZTick',[]);
%
for i=1:numel(dT)
   rSol = implicitTransport(rSol, G, dT(i), rock, fluid, 'wells', W);

   % Check for inconsistent saturations
   assert(max(rSol.s(:,1)) < 1+eps && min(rSol.s(:,1)) > -eps);

   % Update solution of pressure equation.
   rSol  = incompTPFA(rSol , G, hT, fluid, 'wells', W);

   % Measure water saturation in production cells in saturation
   wellSols{i} = getWellSol(W, rSol, fluid);

   % Increase time
   t = t + dT(i);

   % Plot saturation
   delete(hs);
   hs = plotCellData(G, rSol.s(:,1), (rSol.s(:,1)>.01), pargs{:});
   title([num2str(convertTo(t,day)),  ' days']),
   caxis([0 1]); drawnow

end
_images/overrideExample_03.png

Oil rate, with peak production indicated by red line

figure,
[Ym,Tm] = meshgrid(G.cells.centroids(W(1).cells,3),cumsum(dT)/year);
p       = cellfun(@(x) abs(x(1).qO)', wellSols,'UniformOutput',false);
po      = vertcat(p{:})*day;
[~,j]   = max(po);
m       = sub2ind(size(po),j,1:numel(W(1).cells));
surf(Ym,Tm,po); shading interp; colormap(parula(20));
hold on; plot3(Ym(m),Tm(m),po(m), '-r','LineWidth',2); hold off
axis tight, view(100,35)
_images/overrideExample_04.png

Water production, with breakthrough indicated by red line

figure,
p  = cellfun(@(x) abs(x(1).qW)', wellSols,'UniformOutput',false);
pw = vertcat(p{:})*day;
j  = sum(~(pw>1e-3));
m  = sub2ind(size(pw),j,1:numel(W(1).cells));
surf(Ym,Tm,pw); shading interp; colormap(parula(20));
hold on; plot3(Ym(m),Tm(m),pw(m), '-r','LineWidth',2); hold off
axis tight, view(45,35)
_images/overrideExample_05.png

Total rate, with breakthrough indicated by red line

figure,
p = po + pw;
surf(Ym,Tm,p); shading interp; colormap(parula(20));
hold on; plot3(Ym(m),Tm(m),p(m), '-r','LineWidth',2); hold off
axis tight, view(45,35)
_images/overrideExample_06.png

Plot surface rates etc using GUI

plotWellSols(wellSols, cumsum(dT));
_images/overrideExample_07.png

Homogeneous quarter five-spot

Generated from quarterFiveSpot2D.m

In this example, we compare and contrast incompressible single-phase and two-phase flow for a homogeneous quarter five-spot. Through the example, you will also be introduced to the way well solutions are represented in more advanced multiphase simulators based on the AD-OO framework.

mrstModule add incomp diagnostics

Set up model

Square domain with homogeneous rock properties, no flow across the boundaries, no gravity, injection in the southeast and production in the northwest corners, both operating at fixed bottom-hole pressure

gravity reset off
cartDim = [128 128 1];
fluid = initSimpleFluid('mu' , [   1,    1] .* centi*poise     , ...
                        'rho', [1000,  850] .* kilogram/meter^3, ...
                        'n'  , [   2,    2]);
domain = [250 250 20];
G      = computeGeometry(cartGrid(cartDim,domain));
rock   = makeRock(G, 100*milli*darcy, 0.2);
pv     = poreVolume(G, rock);

W = addWell([],G, rock, 1, 'Type', 'bhp', ...
    'Val', 100*barsa, 'name', 'I', 'radius', .1, 'Comp_i', [1 0]);
W = addWell(W,G, rock, G.cells.num, 'Type', 'bhp', ...
    'Val', 0, 'name', 'P', 'radius', .1, 'Comp_i', [0 1]);
hT = computeTrans(G, rock);

% Figure, colormap and contour values
figure('Position',[300 550 1100 650]);
nval = 20;
cval = linspace(0,1,nval+1); cval=.5*cval(1:end-1)+.5*cval(2:end);
colormap(flipud(.5*jet(nval)+.5*ones(nval,3)));
_images/quarterFiveSpot2D_01.png

Compute solution and plot saturation evolution

To study the solution, we will plot saturation profiles at four instances in time up to slightly beyond water breakthrough; this corresponds dimensionless times 0.2 to 0.8 PVI. To get the solution provies as accurate as possible, we use the explicit transport solver and M substeps to advance the solution 0.2 PVI forward in time. We continue computing the solution up to time 1.2 PVI, but do not show snapshots of the saturation field for the two last time intervals.

% Compute an initial single-phase pressure solution, from which we estimate
% the final time that corresponds to 1.2 PVI if this flow field remains
% unchanged. With quadratic relperm curves and equal viscosities, the
% multiphase displacement front will propagate at a speed of
% a=1/(2(sqrt(2)-1)) relative to the total velocity.
x  = initState(G,W,100*barsa, [0 1]);
x  = incompTPFA(x, G, hT, fluid, 'wells', W);
T  = 1.2*sum(pv)/x.wellSol(1).flux;
a  = 1/(2*(sqrt(2)-1));

% Compute time-of-flight for the single-phase flow field and record the
% corresponding breakthrough time in the producer.
tau = computeTimeOfFlight(x, G, rock,  'wells', W);
tbf = tau(W(2).cells,1);

% Initialize number of time intervals, cell array to hold well solutions,
% and array to hold the oil in place
[N,M]    = deal(6,10);
wellSols = cell(N*M+1,1);  wellSols{1} = getWellSol(W, x, fluid);
oip      = zeros(N*M+1,1); oip(1) = sum(x.s(:,2).*pv);
for n=1:N
    fprintf(1,'Main step %d: ',n);
    for m=1:M
       x  = incompTPFA(x, G, hT, fluid, 'wells', W);
       x  = explicitTransport(x, G, T/(N*M), rock, fluid, 'wells', W);

       wellSols{(n-1)*M+m+1} = getWellSol(W, x, fluid);
       oip((n-1)*M+m+1) = sum(x.s(:,2).*pv);
       if x.s(W(2).cells,1)<eps
           W(2).bt = (n-1)*M+m+1;
       end
       fprintf(1,'%d, ',m);
    end
    fprintf(1,'\n');
    if n>4, continue, end

    % Plot multiphase solution
    subplot(2,4,n);
    contourf(reshape(G.cells.centroids(:,1), G.cartDims),...
        reshape(G.cells.centroids(:,2), G.cartDims), ...
        reshape(x.s(:,1),G.cartDims), [0 cval 1], 'EdgeColor','none');
    hold on;

    % Plot corresponding time lines from single-phase solution
    contour(reshape(G.cells.centroids(:,1), G.cartDims),...
        reshape(G.cells.centroids(:,2), G.cartDims), ...
        reshape(tau/T,G.cartDims), a*n/N, '-k','LineWidth',1);
    caxis([0 1]);
    axis equal; axis([0 domain(1) 0 domain(2)]);
    title(sprintf('t=%.2f PVI',n*.2));
    set(gca,'XTick',[],'YTick',[]);

    % Plot multiphase solution as function of single-phase time-of-flight
    subplot(2,4,4+n)
    set(gca,'position',get(gca,'position')+[0 .12 0 0]);
    plot(tau(:,1)/tbf,x.s(:,1),'.k','MarkerSize',4);
    set(gca,'XLim',[0 2]); drawnow;
end
Main step 1: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
Main step 2: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
Main step 3: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
Main step 4: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
Main step 5: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
Main step 6: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
_images/quarterFiveSpot2D_02.png

Plot production curves

First we plot the saturation in the perforated cell and the corresponding water cut, i.e., the fractional flow evaluated in the completion

dt = [0; ones(N*M,1)*T/(N*M)]; t = 1.2*cumsum(dt)/T;
figure;
plot(t,cellfun(@(x) x(2).Sw, wellSols),'--', ...
    t,cellfun(@(x) x(2).wcut, wellSols),'-','LineWidth',1);
legend('Sw in completion','Water cut','Location','NorthWest');
axis([0 max(t) -.05 1.0]);
_images/quarterFiveSpot2D_03.png

Second, we plot the oil rate used in our simulation in units m^3/day

figure;
qOs = cellfun(@(x) abs(x(2).qOs), wellSols);
stairs(t, qOs([2:end end])*day,'LineWidth',1); axis([0 1.2 30 260]);
_images/quarterFiveSpot2D_04.png

Last, we plot the cumulative oil production computed from the well solution and compare this with the amount of extracted oil derived from a mass-balance computation (initial oil in place minus current oil in place). We also include a horizontal line indicating the initial oil in place, and a straight line showing the amount of oil we would have extracted if oil was produced at the constant initial rate

figure
plot(t,cumsum(bsxfun(@times, abs(cellfun(@(x) x(2).qOs, wellSols)), dt)));
hold on;
plot(t,oip(1)-oip,'-.','LineWidth',3);
plot([0 1.2],oip([1 1]),'-k',t,min(t*oip(1),oip(1)),'-k', ...
    t(W(2).bt+[0 0]),[0 oip(1)],'--k');
hold off; axis tight; axis([0 max(t) 0 1.05*oip(1)]);
_images/quarterFiveSpot2D_05.png

Plotting with GUI from ad-core

mrstModule add ad-core
plotWellSols(wellSols,cumsum(dt))
_images/quarterFiveSpot2D_06.png

Two-Phase Incompressible Simulation on the Norne Model

Generated from runNorneSynthetic.m

In this example, we will consider the grid and the petrophysical properties from the Norne simulation model to demonstrate some of the complexity one will see in a real simulation model. We set up a two-phase simulation that uses synthetic fluid model, well placement, and simulation schedule. Norne is an oil and gas field lies located in the Norwegian Sea. The reservoir is found in Jurrasic sandstone at a depth of 2500 meter below sea level. Operator Statoil and partners (ENI and Petoro) have agreed with NTNU to release large amounts of subsurface data from the Norne field for research and education purposes. The Norne Benchmark datasets are hosted and supported by the Center for Integrated Operations in the Petroleum Industry (IO Center) at NTNU. Recently, the OPM Initiative released the full simulation model as an open data set on GitHub.

mrstModule add deckformat incomp
gravity reset on

Read and process the model

As the Norne dataset is available from the OPM project’s public GitHub repository, we can download a suitable subset of the simulation model and process that subset. See the showNorne script for a more detailed walkthrough of the model.

if ~ (makeNorneSubsetAvailable() && makeNorneGRDECL())
   error('Unable to obtain simulation model subset');
end
grdecl = fullfile(getDatasetPath('norne'), 'NORNE.GRDECL');
grdecl = readGRDECL(grdecl);
grdecl = convertInputUnits(grdecl, getUnitSystem('METRIC'));

Extract the active part of the model

To get the whole grid, we need to reprocess the input data. This gives a grid with two components: the main reservoir and a detached stack of twelve cells, which we ignore henceforth.

G = processGRDECL(grdecl);
G = computeGeometry(G(1));

Set view parameters and plot grid

myview = struct(...
    'view',     [-110,50],   ...
 % view angle
    'zoom',     1.8,         ...
 % zoom
    'dataasp',  [1 1 .1],    ...
 % data aspect ratio
    'dolly',    [0 .1 0],    ...
 % camdolly
    'pargs',    {{'EdgeAlpha'; 0.1; 'EdgeColor'; 'k'}} ...
    );
clf
plotReservoirModel(G, [], [], myview);
_images/runNorneSynthetic_01.png

Outline petrophysical properties

The petrophysical properties are included in the simulation model subset represented by function makeNorneSubsetAvailable. Consequently, the necessary data has been read into the grdecl structure. We can then extract the petrophysical properties. Notice that here, the rock

rock = grdecl2Rock(grdecl, G.cells.indexMap);

Porosity and net-to gross

Show the porosities mapped onto the structural grid

figure,
plotReservoirModel(G, rock.poro, [], myview);
colorbarHist(rock.poro, [.05 .35],'South',100);
_images/runNorneSynthetic_02.png

show also the net-to-gross

figure
plotReservoirModel(G, rock.ntg, [], myview);
colorbarHist(rock.ntg,[.07 1],'South',100);
_images/runNorneSynthetic_03.png

Permeability

The permeability is generally a tridiagonal tensor K = diag(Kx, Ky, Kz). For the Norne model, the data file only specifies Kx, and then various manipulations are done to get the correct vertical permeability.

figure,
prms      = myview;
prms.cs   = [1 10 100 1000 10000];
prms.unit = milli*darcy;
prms.cb   = 'South';
plotReservoirModel(G, log10(rock.perm(:,1)), [], prms);
_images/runNorneSynthetic_04.png
figure,
prms.cs   = [0.1 1 10 100 1000 3000];
plotReservoirModel(G, log10(rock.perm(:,3)), [], prms);
%caxis(log10([.1 2500]*milli*darcy));
_images/runNorneSynthetic_05.png

Multipliers

The model has a number of multipliers that reduce the transmissibility in the z-direction

figure
prms = myview;
prms.pargs = {'FaceColor','none','EdgeAlpha',.1,'EdgeColor','k'};
plotReservoirModel(G, [], [], prms);
mz = grdecl.MULTZ(G.cells.indexMap);
plotCellData(G,mz,mz<1,myview.pargs{:});
mz(mz==1) = nan;
colorbarHist(mz, [-.05 .6], 'South', 100);
_images/runNorneSynthetic_06.png

Introduce wells

The reservoir is produced using a set production wells controlled by bottom-hole pressure and rate-controlled injectors. Wells are described using a Peacemann model, giving an extra set of equations that need to be assembled. For simplicity, all wells are assumed to be vertical and are assigned using the logical (i,j) subindex.

% Set vertical injectors, completed in the lowest 12 layers.
nz = G.cartDims(3);
I = [ 9, 26,  8, 25, 35, 10];
J = [14, 14, 35, 35, 68, 75];
R = [ 4,  4,  4,  4,  4,  4]*1000*meter^3/day;
nIW = 1:numel(I); W = [];
for i = 1 : numel(I)
   W = verticalWell(W, G, rock, I(i), J(i), nz-11:nz, 'Type', 'rate', ...
                    'InnerProduct', 'ip_tpf', ...
                    'Val', R(i), 'Radius', 0.1, 'Comp_i', [1, 0], ...
                    'name', ['I', int2str(i)], 'refDepth', 2500);
end

% Set vertical producers, completed in the upper 14 layers
I = [17, 12, 25, 35, 15];
J = [23, 51, 51, 95, 94];
P = [300, 300, 300, 200, 200];
nPW = (1:numel(I))+max(nIW);
for i = 1 : numel(I)
   W = verticalWell(W, G, rock, I(i), J(i), 1:14, 'Type', 'bhp', ...
                    'InnerProduct', 'ip_tpf', ...
                    'Val', 300*barsa(), 'Radius', 0.1, 'refDepth', 2500, ...
                    'name', ['P', int2str(i)], 'Comp_i', [0, 1]);
end


% Plot grid outline and the wells
figure
myview = struct(...
    'view',     [30,50],      ...
 % view angle
    'zoom',     1.8,          ...
 % zoom
    'dataasp',  [15 15 2],    ...
 % data aspect ratio
    'cb',      'horiz',       ...
 % colorbar location
    'dolly',   [0 .3 0],      ...
 % camdolly
    'wargs',   {{'height', 30, 'Color', 'k', 'FontSize', 10}},  ...
    'pargs',   {{'FaceColor', 'none', 'EdgeAlpha', .05}} ...
    );
plotReservoirModel(G, [], W, rmfield(myview,'cb'));
plotGrid(G, vertcat(W(nIW).cells), 'FaceColor', 'b');
plotGrid(G, vertcat(W(nPW).cells), 'FaceColor', 'r');
_images/runNorneSynthetic_07.png

Transmissibilities and initial state

Initialize solution structures and compute transmissibilities from input grid, rock properties, and well structure.

hT = computeTrans(G, rock, 'Verbose', true);
tmult = computeTranMult(G, grdecl);
hT = hT.*tmult;
rSol  = initState(G, W, 0, [0, 1]);
Computing one-sided transmissibilities...
Warning:
      5512 negative transmissibilities.
      Replaced by absolute values...
Elapsed time is 0.039221 seconds.

Fluid model

fluid      = initSimpleFluid('mu' , [   1,   5]*centi*poise     , ...
                             'rho', [1014, 859]*kilogram/meter^3, ...
                             'n'  , [   2,   2]);

Prepare plotting of saturations

figure
myview.zoom = 2.05; myview.dolly = [0,.2,0];
h = plotReservoirModel(G, [], W, myview);
colormap(flipud(winter))
set(h,'Position',[.13 .07 .77 .05]);
[hs,ha] = deal([]); caxis([0 1]);
drawnow
_images/runNorneSynthetic_08.png

Main loop

In the main loop, we alternate between solving the transport and the flow equations. The transport equation is solved using the standard implicit single-point upwind scheme with a simple Newton-Raphson nonlinear solver.

T    = 12*year();
dplt = .25*year;
pv   = poreVolume(G,rock);
t    = 0;  plotNo = 1;
dt   = diff(0:year/12:T);
dt   = [dt(1).*sort(repmat(2.^-[1:5 5],1,1)) dt(2:end)];
wSol = cell(numel(dt),1);
for n=1:numel(dt)

   rSol = incompTPFA(rSol, G, hT, fluid, 'wells', W);
   rSol = implicitTransport(rSol, G, dt(n), rock, fluid, 'wells', W);

   % Check for inconsistent saturations
   assert(max(rSol.s(:,1)) < 1+eps && min(rSol.s(:,1)) > -eps);

   % Extract well data
   wSol{n} = getWellSol(W, rSol, fluid);

   % Increase time and continue if we do not want to plot saturations
   t = t + dt(n);
   if ( t < plotNo*dplt && t <T), continue, end

   % Plot saturation
   delete([hs, ha])
   hs = plotCellData(G, rSol.s(:,1), find(rSol.s(:,1) > 0.01),'EdgeColor','none');
   ha = annotation('textbox', [0 0.93 0.35 0.07], 'String', ...
       sprintf('Water saturation at %5.2f years', convertTo(t,year)));
   drawnow
   plotNo = plotNo+1;
end
_images/runNorneSynthetic_09.png

Demonstrate various analytical fluid models

Generated from showFluidObjects.m

mrstModule add incomp

Simple fluid model

fluid = initSimpleFluid('mu' , [   1,  1]*centi*poise     , ...
                       'rho', [1000, 1000]*kilogram/meter^3, ...
                       'n'  , [   3,   3]);
s=linspace(0,1,20)';
kr = fluid.relperm(s);
plot(s,kr(:,1),'-s',s,kr(:,2),'-o');
_images/showFluidObjects_01.png

Corey fluid model

fluid = initCoreyFluid('mu' , [   1,  10]*centi*poise     , ...
                       'rho', [1014, 859]*kilogram/meter^3, ...
                       'n'  , [   3, 2.5]                 , ...
                       'sr' , [ 0.2, 0.15]                 , ...
                       'kwm', [   1, .85]);
s=linspace(0,1,50)';
[kr,dkr,ddkr] = fluid.relperm(s);
subplot(1,3,1),
plot(s(s<.85),kr(s<.85,1),s(s>.2),kr(s>.2,2),'LineWidth',1); axis([0 1 -.02 1]);
subplot(1,3,2),
plot(s(s<.85),dkr(s<.85,1),s(s>.2),dkr(s>.2,4),'LineWidth',1); axis([0 1 -.1 4.5]);
subplot(1,3,3),
plot(s(s<.85),ddkr(s<.85,1),s(s>.2),ddkr(s>.2,2),'LineWidth',1); axis([0 1 -.2 9.5]);
_images/showFluidObjects_02.png

Simple fluid with linear capillary term

fluid = initSimpleFluidPc('mu' , [   1,  10]*centi*poise     , ...
                          'rho', [1014, 859]*kilogram/meter^3, ...
                          'n'  , [   2,   2], ...
                          'pc_scale', 2*barsa);
s = linspace(0, 1, 101).'; kr = fluid.relperm(s);
subplot(1,2,1), plot(s, kr), legend('kr_1(S)', 'kr_2(S)')
x.s = [s 1-s]; pc = fluid.pc(x);
subplot(1,2,2), plot(s, pc); legend('P_c(S)');
_images/showFluidObjects_03.png

Simple model with Leverett-J function

G = computeGeometry(cartGrid([40,5,1],[1000 500 1]));
state.s = G.cells.centroids(:,1)/1000;
p = G.cells.centroids(:,2)*.001;
rock = makeRock(G, p.^3.*(1e-5)^2./(0.81*72*(1-p).^2), p);
fluid = initSimpleFluidJfunc('mu' , [   1,  10]*centi*poise     , ...
            'rho', [1014, 859]*kilogram/meter^3, ...
            'n'  , [   2,   2], ...
            'surf_tension',10*barsa/sqrt(0.1/(100*milli*darcy)),...
            'rock',rock);
plot(state.s, fluid.pc(state)/barsa,'o');
_images/showFluidObjects_04.png

Compare grid-orientation effects for skew grids

Generated from skewGridErrors.m

In this example we consider a symmetric well pattern on a skew grid to study the grid-orientation effects for the combined single-point upwind and TPFA/mimetic schemes. The script contains two different examples, both describing a 400 x 100 m^2 reservoir section. The first (exmpl=1) describes a horizontal reservoir cross-section in which water is injected from one well at the midpoint of the north perimeter and fluids are produced from two wells along the south perimeter, located 50 m from the SE and SW corners, respectively. The second case (exmpl=2) describes a vertical cross-section, where water is injected from two horizontal injectors at the bottom of the reservoir and fluids produced from a horizontal producer at the top of the reservoir. In both cases, the well operate under bottom-hole control.

mrstModule add incomp mimetic ad-core
gravity reset off;
exmpl = 1;        % type of cross-section: 1 - horizontal,  2 - vertical

% Final time and number of time steps
T = 1200*day;
nstep =120;

% Rectangular reservoir with a skew grid.
G = cartGrid([41,20],[2,1]);
makeSkew = @(c) c(:,1) + .4*(1-(c(:,1)-1).^2).*(1-c(:,2));
G.nodes.coords(:,1) = 200*makeSkew(G.nodes.coords);
G.nodes.coords(:,2) = 100*G.nodes.coords(:,2);
G = computeGeometry(G);

% Homogeneous reservoir properties
rock = makeRock(G, 100*milli*darcy, .2);
pv   = sum(poreVolume(G,rock));
hT   = computeTrans(G, rock);
IP   = computeMimeticIP(G, rock);

% Symmetric well pattern
wcells = findEnclosingCell(G,[200 97.5; 50 2.5; 350 2.5]);
rate   = sum(poreVolume(G,rock));
if exmpl==1
    W = addWell([], G, rock, wcells(1), 'Type', 'bhp', ...
        'Val', 200*barsa, 'name', 'I', 'radius', .1, 'Comp_i', [1 0]);
    W = addWell(W, G, rock, wcells(2), 'Type', 'bhp', ...
        'Val', 100*barsa, 'name', 'P1', 'radius', .1, 'Comp_i', [0 1]);
    W = addWell(W, G, rock, wcells(3), 'Type', 'bhp', ...
        'Val', 100*barsa, 'name', 'P2', 'radius', .1, 'Comp_i', [0 1]);
else
    gravity(norm(gravity())*[0 -1 0]);
    W = addWell([], G, rock, wcells(1), 'Type', 'bhp', ...
        'Val', 100*barsa, 'name', 'P', 'radius', .1, 'Comp_i', [0 1]);
    W = addWell(W, G, rock, wcells(2), 'Type', 'bhp', ...
        'Val', 200*barsa, 'name', 'I1', 'radius', .1, 'Comp_i', [1 0]);
    W = addWell(W, G, rock, wcells(3), 'Type', 'bhp', ...
        'Val', 200*barsa, 'name', 'I2', 'radius', .1, 'Comp_i', [1 0]);
end

% Two-phase fluid
fluid = initSimpleFluid('mu', [1 10].*centi*poise, ...
        'rho', [1000,  850].* kilogram/meter^3, 'n', [2, 2]);

Prepare plotting and allocate various objects

Figure and figure settings

igure('Position',[400 460 900 350]);
pargs = {'EdgeColor','k','EdgeAlpha',.05};

[xtp,xmi] = deal(initState(G,W,100*barsa, [0 1]));

dt = repmat(T/nstep,1,nstep);
dt = [dt(1).*sort(repmat(2.^-[1:5 5],1,1)) dt(2:end)];
N  = numel(dt);
wellSols = cell(N,2);
t  = 0;
for n=1:N
    t = t + dt(n);

    xtp  = incompTPFA(xtp, G, hT, fluid, 'wells', W);
    xtp  = implicitTransport(xtp, G, dt(n), rock, fluid, 'wells', W);
    wellSols{n,1} = getWellSol(W, xtp, fluid);
    subplot(2,2,1), cla,
       plotCellData(G, xtp.pressure/barsa, pargs{:});
       caxis([100 200]); title(sprintf('TPFA: %.0f days', t/day));
    subplot(2,2,3), cla,
       % plotGrid(G,'FaceColor','none');
       plotCellData(G, xtp.s(:,1),xtp.s(:,1)>.01,pargs{:});
       axis([0 400 0 100]); caxis([0 1]);

    xmi  = incompMimetic(xmi, G, IP, fluid, 'wells', W);
    xmi  = implicitTransport(xmi, G, dt(n), rock, fluid, 'wells', W);
    wellSols{n,2} = getWellSol(W, xmi, fluid);
    subplot(2,2,2), cla,
       plotCellData(G, xmi.pressure/barsa, pargs{:});
       caxis([100 200]); title(sprintf('Mimetic: %.0f days', t/day));
       colorbar('peer', gca, 'Position', [0.94 0.58 0.0175 0.345]);
    subplot(2,2,4), cla,
       % plotGrid(G,'FaceColor','none');
       plotCellData(G, xmi.s(:,1),xmi.s(:,1)>.01, pargs{:});
       axis([0 400 0 100]); caxis([0 1]);
       colorbar('peer', gca, 'Position', [0.94 0.105 0.0175 0.345]);

    for i=1:4,
        subplot(2,2,i), hold on
        plot(G.cells.centroids(wcells,1),G.cells.centroids(wcells,2),...
            'ok','MarkerSize',6,'MarkerFaceColor',[.6 .6 .6]);
        hold off
    end
    drawnow
end

plotWellSols({wellSols(:,1), wellSols(:,2)}, ...
    cumsum(dt), 'datasetnames', {'TPFA','MFD'});
_images/skewGridErrors_01.png
_images/skewGridErrors_02.png

Heterogeneous quarter five-spot: temporal splitting errors

Generated from splittingErrorQ5het.m

In this example, we use a heterogeneous quarter five-spot with petrophysical data sampled from the topmost layer of the SPE 10 data set as an example to illustrate splitting errors that may arise when using a sequential solution procedure. We consider three different mobility ratios: an unfavorable, equal mobilities, and favorable.

mrstModule add incomp spe10 ad-core

Set up model and parameters

dims      = [60 120 1];
domain    = dims.*[20 10 2]*ft;
G         = computeGeometry(cartGrid(dims,domain));
rock      = getSPE10rock((1:dims(1)),(1:dims(2))+60,1);
rock.poro = max(rock.poro,.0005);
pv        = poreVolume(G,rock);
gravity reset off

rate   = sum(poreVolume(G,rock));
W = addWell([],G, rock, 1, 'Type', 'rate', ...
    'Val', rate, 'name', 'I', 'radius', .1, 'Comp_i', [1 0]);
W = addWell(W,G, rock, G.cells.num, 'Type', 'rate', ...
    'Val', -rate, 'name', 'P', 'radius', .1, 'Comp_i', [0 1]);
hT = computeTrans(G, rock);
x0 = initState(G,W,100*barsa, [0 1]);

Self convergence with respect to time step

For the first test, we illustrate difference in self convergence as a function of mobility ratio M = muw/muo for M=0.1, 1, and 10. These cases have a leading displacement profile that moves faster than the Darcy velocity by a factor rvel=1/(2M(sqrt((M+1)/M)-1)). For each mobility ratio, we set up a simulation that would take 2^m time splitting steps to reach 0.5 PVI for m=1:6. However, to get saturation profiles that cover a more similar portion of the reservoir, we scale the end time for M=0.1 and M=10 by rvel(1)/rvel(M). The self convergence is measured relative to a solution that would have used 256 steps to get to 0.5 PVI.

mu     = [1 10; 1 1 ; 10 1];
rvel   = @(M) 1./(2.*M.*(sqrt((M+1)./M)-1));
tscale = rvel(mu(:,1)./mu(:,2));
tscale = tscale(2)./tscale;
T      = 0.5;

% Prepare plotting
set(gcf,'Position',[200 550 1240 480]);
cval = linspace(0,1,11);
cval = .5*cval(1:end-1)+.5*cval(2:end);
plotData = @(x) ...
    contourf(reshape(G.cells.centroids(:,1), G.cartDims),...
    reshape(G.cells.centroids(:,2), G.cartDims), ...
    reshape(x.s(:,1),G.cartDims), [cval 1]);
colormap(flipud(.5*jet(10)+.5*ones(10,3)));

% Loop over all three viscosity ratios
err = zeros(3,6);
for n=1:3
    fluid = initSimpleFluid('mu', mu(n,:).* centi*poise, ...
        'rho', [1000,1000].*kilogram/meter^3, 'n',  [2 2]);
    for m=[8 1:6]

        % Time loop
        [x,dt,t,tend]  = deal(x0, T*2^(-m), 0, tscale(n)*T);
        while t<tend
            x = incompTPFA(x, G, hT, fluid, 'wells', W);
            [tl,dtl,tle] = deal(0,T/2^8, min(tend-t,dt));
            while tl < tle
                x = implicitTransport(x, G, dtl, rock, fluid, 'wells', W);
                tl = tl+dtl;
            end
            t = t+dt;
        end

        % Compute discrepancy from reference solution
        if m==8
            sref = x.s(:,1); snorm = sum(abs(sref).*pv);
        else
            err(n,m) = sum(abs(x.s(:,1)-sref).*pv)/snorm;
        end

        % Plot contour plot of solution
        subplot(3,7,sub2ind([7,3],min(m,7),n))
        plotData(x); axis equal; axis([0 domain(1) 0 domain(2)]);
        if n==1
            title(sprintf('%d steps',2^m));
        else
            pos=get(gca,'position');
            set(gca,'position',pos+(n-1).*[0 .05 0 0]);
        end
        set(gca,'XTick',[],'YTick',[]);
        drawnow;
    end
end

% Plot convergence
figure
semilogy(err','o-','MarkerSize',7,'MarkerFaceColor',[.8 .8 .8],'LineWidth',1);
set(gca,'XTick',1:6,'XTickLabel',{2.^(1:6)}); legend('1:10','1:1','10:1');
_images/splittingErrorQ5het_01.png
_images/splittingErrorQ5het_02.png

Well curves convergence

We repeate the same simulation as above, except that we continue until 1.5 PVI for all three mobility ratios. We run simulations with constant time-step length (3*4^[0:4] steps) and with a rampup.

Tn    = 1.5;

% Constant time step
nstep = [1 4 16 64 256];
dt    = T/nstep(end);
wellSols = cell(nstep(end),numel(nstep),3);

% Rampup
dT = repmat(T/32, Tn/T*32,1);
dT = [dT(1)*sort(repmat((2.^-[1:4 4])',2,1)); dT(3:end)];
wSol     = cell(numel(dT),3);
for n=1:3
    fprintf('Fluid %d: ', n);
    fluid = initSimpleFluid('mu', mu(n,:).* centi*poise, ...
        'rho', [1000,1000].*kilogram/meter^3, 'n',  [2 2]);
    for k=1:numel(nstep)
        x = x0;
        ws = 1;
        fprintf('%d,', nstep(k));
        for i=1:nstep(k)*Tn/T
            x = incompTPFA(x, G, hT, fluid, 'wells', W);
            for m=1:nstep(end)/nstep(k)
                x = implicitTransport(x, G, dt, rock, fluid, 'wells', W);
                wellSols{ws,k,n} = getWellSol(W, x, fluid); ws = ws+1;
            end
        end
    end
    fprintf('rampup\n');
    x = x0;
    for i=1:numel(dT)
        x = incompTPFA(x, G, hT, fluid, 'wells', W);
        x = implicitTransport(x, G, dT(i), rock, fluid, 'wells', W);
        wSol{i,n} = getWellSol(W, x, fluid);
    end
end
Fluid 1: 1,4,16,64,256,rampup
Fluid 2: 1,4,16,64,256,rampup
Fluid 3: 1,4,16,64,256,rampup
n = 2;
[tws,dsn,t] = deal(cell(1,numel(nstep)+1));
for i=1:numel(nstep)
    tws(i) = {vertcat(wellSols(:,i,n))};
    t(i)   = {cumsum(dt*ones(Tn/T*nstep(end),1))};
    dsn(i) = {num2str(nstep(i))};
end
tws(end) = {vertcat(wSol(:,n))};
t(end)   = {cumsum(dT)};
dsn(end) = {'rampup'};

plotWellSols(tws,t, 'datasetnames', dsn);
_images/splittingErrorQ5het_03.png

Quarter five-spot: Illustration of temporal splitting errors

Generated from splittingErrorQ5hom.m

We use the standard quarter five-spot test case to illustrate splitting errors that may arise when using a sequential solution procedure to simulate multiphase flow

mrstModule add book incomp diagnostics

Set up model

T = 1;
cartDim = [128 128 1];
gravity reset off
fluid = initSimpleFluid('mu' , [   1,    1] .* centi*poise     , ...
                        'rho', [1000,  850] .* kilogram/meter^3, ...
                        'n'  , [   2,    2]);
domain = [250 250 20];
G      = computeGeometry(cartGrid(cartDim,domain));
rock   = makeRock(G, 450*milli*darcy, 0.2);
rate   = .6*sum(poreVolume(G,rock))/T;
W = addWell([],G, rock, 1, 'Type', 'rate', ...
    'Val', rate, 'name', 'I', 'radius', .1, 'Comp_i', [1 0]);
W = addWell(W,G, rock, G.cells.num, 'Type', 'rate', ...
    'Val', -rate, 'name', 'P', 'radius', .1, 'Comp_i', [0 1]);
hT = computeTrans(G, rock);

Compute reference solution

xr = initState(G,W,100*barsa, [0 1]);
for n=1:128
    xr = incompTPFA(xr, G, hT, fluid, 'wells', W);
    xr = explicitTransport(xr, G, T/128, rock, fluid, 'wells', W);
end
pv = poreVolume(G,rock);
l1s = norm(xr.s(:,1).*pv,1);
l2p = norm(xr.pressure.^2.*pv, 1);

Contrast multiphase displacement with single-phase time lines

For Corey exponent 2 and equal viscosities, the displacement front will propagate at a speed of a=1/(2(sqrt(2)-1)) relative to the total velocity. Compute the countour showing the position of the displacement front at time 0.6 PVI.

a  = 1/(2*(sqrt(2)-1));
x  = initState(G,W,100*barsa, [0 1]);
x  = incompTPFA(x, G, hT, fluid, 'wells', W);

tau = computeTimeOfFlight(x, G, rock,  'wells', W);

figure('Position',[300 550 1100 300]);
C = contour(reshape(G.cells.centroids(:,1), G.cartDims),...
        reshape(G.cells.centroids(:,2), G.cartDims), ...
        reshape(tau/T,G.cartDims), a, '-k','LineWidth',1); clf
_images/splittingErrorQ5hom_01.png

Run simulation with different time steps

Finally, we compute the multiphase flow solution at time 0.6 PVI using a sequence of increasing splitting time steps. As the number of splitting steps increases, the approximate solution gradually approaches the correct solution computable on this grid

nval = 10;
cval = linspace(0,1,nval+1); cval=.5*cval(1:end-1)+.5*cval(2:end);
colormap(flipud(.5*jet(nval)+.5*ones(nval,3)));
err = nan(7,2);
for n=0:6
    nstep = 2^n;
    x  = initState(G,W,100*barsa, [0 1]);
    for i=1:nstep
        x  = incompTPFA(x, G, hT, fluid, 'wells', W);
        x  = explicitTransport(x, G, T/nstep, rock, fluid, 'wells', W);
    end

    err(n+1,1) = norm((xr.s(:,1) - x.s(:,1)).*pv, 1)/l1s;
    err(n+1,2) = norm((xr.pressure - x.pressure).^2.*pv, 1)/l2p;

    if rem(n,2), continue, end

    subplot(1,4,n/2+1);
    contourf(reshape(G.cells.centroids(:,1), G.cartDims),...
        reshape(G.cells.centroids(:,2), G.cartDims), ...
        reshape(x.s(:,1),G.cartDims), [0 cval 1], 'EdgeColor','none');
    hold on, plot(C(1,2:end),C(2,2:end),'k-','LineWidth',1); hold off
    axis equal; axis([0 domain(1) 0 domain(2)]); caxis([0 1]);
    title([num2str(nstep) ' steps'])
    set(gca,'XTick',[],'YTick',[]);
    drawnow;
end
_images/splittingErrorQ5hom_02.png

Show convergence table

convrate = log(bsxfun(@rdivide,err(1:end-1,:),err(2:end,:)))./log(2);
convrate = [nan nan; convrate];
clc
fprintf('\n\t  L1(S)     rate\t  L2(p)     rate\n');
fprintf('\t-----------------------------------------\n');
fprintf('\t%.3e   %.2f \t%.3e   %.2f\n', ...
    [err(:,1),convrate(:,1),err(:,2),convrate(:,2)]');
fprintf('\t-----------------------------------------\n');
L1(S)     rate          L2(p)     rate
      -----------------------------------------
      5.570e-02   NaN         4.946e-03   NaN
      4.364e-02   0.35        5.052e-04   3.29
      2.770e-02   0.66        1.497e-04   1.76
      1.434e-02   0.95        4.187e-05   1.84
      6.255e-03   1.20        1.046e-05   2.00
...

Heterogeneous quarter five-spot

Generated from viscousFingeringQ5.m

In this example, we will study a heterogeneous quarter five-spot with petrophysical data sampled from the SPE 10 benchmark. We compare fingering effects in a quarter five-spot problem as function of viscosity ratio. We show that an unfavorable mobility ratio (viscosity of injected water is less than the viscosity of the resident oil) leads to pronounced fingers, whereas a piston-like displacement with minimal fingering effects is observed for a favorable mobility ratio.

mrstModule add incomp ad-core spe10

Set up model and parameters

T         = 20*year;
dims      = [60 120 1];
domain    = dims.*[20 10 2]*ft;
G         = computeGeometry(cartGrid(dims,domain));
rock      = getSPE10rock((1:dims(1)),(1:dims(2))+60,1);
rock.poro = max(rock.poro,.0005);
gravity reset off

rate   = sum(poreVolume(G,rock))/T;
W = addWell([],G, rock, 1, 'Type', 'rate', ...
    'Val', rate, 'name', 'I', 'radius', .1, 'Comp_i', [1 0]);
W = addWell(W,G, rock, G.cells.num, 'Type', 'rate', ...
    'Val', -rate, 'name', 'P', 'radius', .1, 'Comp_i', [0 1]);
hT = computeTrans(G, rock);
x0 = initState(G,W,100*barsa, [0 1]);

cval = linspace(0,1,11); cval=.5*cval(1:end-1)+.5*cval(2:end);

Compare fingering effects

We compute solutions for three different viscosity ratios, mu_w : mu_o = N, for N=10, 1, and 1/10.

set(gcf,'Position',[250 150 1100 660]);
plotData = @(x) ...
    contourf(reshape(G.cells.centroids(:,1), G.cartDims),...
    reshape(G.cells.centroids(:,2), G.cartDims), ...
    reshape(x.s(:,1),G.cartDims), [cval 1]);
colormap(flipud(.5*jet(10)+.5*ones(10,3)));

[N,M]   = deal(10,200);
mu      = [1 N; 1 1 ; N 1];
[dt,dT] = deal(zeros(M,1), T/M);
is      = 1;
wellSol = cell(M,3);
oip     = zeros(M,3);
for n=1:3
    fluid = initSimpleFluid('mu', mu(n,:).* centi*poise, ...
        'rho', [1000,1000].*kilogram/meter^3, 'n',  [2 2]);
    [x,ip] = deal(x0,1);
    for i=1:M
        x  = incompTPFA(x, G, hT, fluid, 'wells', W);
        x  = implicitTransport(x, G, dT, rock, fluid, 'wells', W);

        dt(i) = dT;
        oip(i,n) = sum(x.s(:,2).*poreVolume(G,rock));
        wellSol{i,n} = getWellSol(W,x, fluid);

        if ~mod(i,40) && ip<=5
            subplot(3,5,is); ip=ip+1; is=is+1;
            plotData(x); axis equal; axis([0 domain(1) 0 domain(2)]);
            set(gca,'XTick',[],'YTick',[]);
            if n==1
                title(sprintf('%.0f years', sum(dt)/year));
            else
                set(gca,'position',get(gca,'position')+(n-1)*[0 .05 0 0]);
            end
            drawnow;
        end
    end
end
_images/viscousFingeringQ5_01.png

Plot well responses

This plotting only works for predefined quantities like surface rates, bottom-hole pressure, etc. If the well structure contains other quantities, these will appear in the list of available fields but cannot be plotted.

plotWellSols({wellSol(:,1),wellSol(:,2),wellSol(:,3)},...
    cumsum(dt),'datasetnames',{'Ratio 1:10','Ratio 1:1','Ratio 10:1'});
_images/viscousFingeringQ5_02.png
n = 5000000;
pt = randn(n,3);
tic
I = sum(bsxfun(@times, pt>0, [1 2 4]),2)+1;
num = accumarray(I,1);
toc
Elapsed time is 0.149304 seconds.
tic
avg = zeros(8,3);
for j=1:size(pt,1)
    quad = sum((pt(j,:)>0).*[1 2 4])+1;
    avg(quad,:) = avg(quad,:)+pt(j,:);
end
avg = bsxfun(@rdivide, avg, num);
toc
Elapsed time is 9.847593 seconds.
tic
avg = bsxfun(@rdivide, sparse(I,1:n,1)*pt, accumarray(I,1));
toc
Elapsed time is 0.249902 seconds.
tic
avg = sparse(I,1:n,1)*[pt, ones(n,1)];
avg = bsxfun(@rdivide, avg(:,1:end-1), avg(:,end));
toc
Elapsed time is 0.297510 seconds.

Simple formula using two scalars

Generated from introToAD.m

Compute z = 3 exp(-xy) and its partial derivatives wrt x and y. First, we compute it manually and then using automatic differentiation (AD). To understand what is going on behind the curtains when using AD, you can set a breakpoint at line 13 and use the ‘Step into’ function to step through the different functions that are invoked when evaluating z

[x,y]=deal(1,2);
disp('Manually computed:')
disp([1 -y -x]*3*exp(-x*y))

disp('Automatic differentiation:')
[x,y] = initVariablesADI(1,2);
z = 3*exp(-x*y)
Manually computed:
    0.4060   -0.8120   -0.4060

Automatic differentiation:

z =

  ADI with properties:
...

Demonstrate the importance of vectorization

m = 15;
[n,t1,t2,t3,t4] = deal(zeros(m,1));
for i = 1:m
   n(i) = 2^(i-1);
   xv = rand(n(i),1); yv=rand(n(i),1);
   [x,y] = initVariablesADI(xv,yv);
   tic, z = xv.*yv; zx=yv; zy = xv; t1(i)=toc;
   tic, z = x.*y;                   t2(i)=toc;
   if i<17
      tic, for k =1:n(i), z(k)=x(k)*y(k); end;  t3(i)=toc;
      tic, for k =1:n(i), z(k)=x(k).*y(k); end; t4(i)=toc;
   end
   fprintf('%7d: %6.5f sec, %6.5f sec, %6.5f sec, %6.5f sec\n', ...
      n(i), t1(i), t2(i), t3(i), t4(i))
end
1: 0.00018 sec, 0.00024 sec, 0.00118 sec, 0.00054 sec
      2: 0.00052 sec, 0.00018 sec, 0.00150 sec, 0.00100 sec
      4: 0.00011 sec, 0.00017 sec, 0.00217 sec, 0.00202 sec
      8: 0.00006 sec, 0.00017 sec, 0.00213 sec, 0.00206 sec
     16: 0.00060 sec, 0.00055 sec, 0.00460 sec, 0.00456 sec
     32: 0.00001 sec, 0.00007 sec, 0.00834 sec, 0.00822 sec
     64: 0.00001 sec, 0.00007 sec, 0.01669 sec, 0.01636 sec
    128: 0.00001 sec, 0.00010 sec, 0.03507 sec, 0.03493 sec
...
clf, set(gca,'FontSize',14);
loglog(n,t1,'-*',n,t2,'-+',n,t3,'-o',n,t4,'-s','LineWidth',2);
legend('exact formulat','vectorized ADI','for loop and mtimes','for loop and times','Location','NorthWest');
_images/introToAD_01.png

Use automatic differentation to assmble a linear system

First we just evaluate the residual equation as a matrix vector product

x = initVariablesADI(zeros(3,1));
A = [3,  2, -4; 1, -4,  2; -2,- 2.  4];
eq = A*x + [5; 1; -6];
u  = -eq.jac{1}\eq.val
u =

    1.0000
    2.0000
    3.0000

Secondly, we can evaluate the equations one by one and then use ADI to assemble the resulting matrix

x = initVariablesADI(zeros(3,1));
eq1 = [ 3,  2, -4]*x + 5;
eq2 = [ 1, -4,  2]*x + 1;
eq3 = [-2,- 2.  4]*x - 6;
eq = cat(eq1,eq2,eq3);
u  = -eq.jac{1}\eq.val
u =

    1.0000
    2.0000
    3.0000

Use AD to solve the nonlinear Rosenbrock problem

Problem: minimize f(x,y) = (a-x)^2 + b(y-x^2)^2 = 0 The solution is (a,a^2). A necessary condition for minimum is that grad(f(x,y))=0, which gives two residual equations 2(a-x) - 4bx(y-x^2) = 0 2b(y-x^2) = 0 Here, we set a=1 and b=100

[a,b,tol] = deal(1,100,1e-6);
[x0,incr] = deal([-.5;4]);
while norm(incr)>tol
    x = initVariablesADI(x0);
    eq = cat( 2*(a-x(1)) - 4*b.*x(1).*(x(2)-x(1).^2), ...
        2*b.*(x(2)-x(1).^2));
    incr = - eq.jac{1}\eq.val;
    x0 = x0 + incr;
end

Slightly more elaborate version with plotting

a,b] = deal(1,100);
fx = @(x) 2*(a-x(1)) - 4*b.*x(1).*(x(2)-x(1).^2);
fy = @(x) 2*b.*(x(2)-x(1).^2);
[tol,res,nit] = deal(1e-6, inf,0);
x = [-.5 4];
while res>tol && nit<100
    nit = nit+1;
    xd = initVariablesADI(x(end,:)');
    eq = cat(fx(xd),fy(xd));
    incr = - eq.jac{1}\eq.val;
    res = norm(incr);
    x = [x; x(end,:)+incr']; %#ok<AGROW>
end

% Plot solution: use modified colormap to clearly distinguish the region
% with lowest colors. Likewise, show the value of f per iteration on a
% nonlinear scale, i.e., f(x,y)^0.1
subplot(4,1,1:3);
f = @(x,y) (a-x).^2 + b.*(y-x.^2).^2;
[xg, yg] = meshgrid(-2:0.2:2, -1.5:0.2:4);
surf(xg,yg,reshape(f(xg(:), yg(:)),size(xg))); shading interp
hold on;
plot3(x(:,1),x(:,2),f(x(:,1),x(:,2)),'-or', 'LineWidth', 2, ...
    'MarkerSize',8,'MarkerEdgeColor','k','MarkerFaceColor','r');
hold off
view(-130,45); axis tight; colormap([.3 .3 1; jet(99)]);
subplot(4,1,4)
bar(f(x(:,1),x(:,2)).^.1,'b'); axis tight; set(gca,'YTick',[]);
_images/introToAD_02.png

Bed Models 1

Generated from showBedModels.m

pth = getDatasetPath('BedModels1');
fn = dir(fullfile(pth,'*.grdecl'));
for i = 1:numel(fn)
    grdecl = readGRDECL(fullfile(pth, fn(i).name));
    G = processGRDECL(grdecl);
    rock = grdecl2Rock(grdecl,G.cells.indexMap);
    clf
    if isfield(grdecl,'SATNUM')
        q = grdecl.SATNUM(G.cells.indexMap);
    else
        q = rock.poro;
    end
    plotCellData(G, q, 'EdgeAlpha',.1, 'EdgeColor','k');
    view(3); axis tight off, drawnow
    print('-dpng',sprintf('bedmodels1-%d.png', i));
end
_images/showBedModels_01.png

Bed Model 2

grdecl = readGRDECL(fullfile(getDatasetPath('BedModel2'),'BedModel2.grdecl'));
G      = processGRDECL(grdecl);
rock   = grdecl2Rock(grdecl,G.cells.indexMap);
clf,
plotCellData(G,grdecl.SATNUM(G.cells.indexMap),'EdgeColor','none');
view(3); axis tight off
_images/showBedModels_02.png

Show fluid models from SPE 1, SPE 3 and SPE 9

Generated from showSPEfluids.m

The purpose of this example is to demonstrate how to read an Eclipse input file, create a fluid model, and inspect it. To this end, we will consider the SPE1, SPE 3, and SPE9 data sets. The example assumes that you have downloaded them already. If not, you should uncomment and run the following line, to bring up the dataset manager that enables you to download the data sets

% mrstDatasetGUI

Read data

If you have used the mrstDatasetGUI functionality to download the SPE 1 and SPE 9 data set, it should be possible to find them on a standard path. We read the data and convert to SI units.

mrstModule add deckformat ad-props ad-core

nr    = [1 3 9];
fo    = cell(3,1);
col   = lines(3);
name  = cell(3,1);
decks = cell(3,1);
for i=1:3
    name{i}  = sprintf('spe%d',nr(i));
    pth      = getDatasetPath(name{i});
    fn       = fullfile(pth, sprintf('BENCH_SPE%d.DATA',nr(i)));
    decks{i} = readEclipseDeck(fn);
    decks{i} = convertDeckUnits(decks{i});
    fo{i}    = initDeckADIFluid(decks{i});
    if ~isfield(fo{i},'sWcon')
        fo{i}.sWcon = 0.0;
    end
    fo{i}    = assignRelPerm(fo{i});
end

Plot two-phase relative permeability curves

For a three-phase model we have four relative permeability curves. One for both gas and water and two curves for the oil phase. The oil relative permeability is tabulated for both water-oil and oil-gas systems.

s = linspace(0,1,51)';
for i=1:numel(name), name{i} = upper(name{i}); end

figure; hold all, set(gca,'FontSize',14)
for i=1:3
    plot(s, fo{i}.krW(s), 'linewidth', 2, 'Color',col(i,:))
end
grid on
legend(name{:},'Location','NorthWest');
xlabel('Water saturation');
title('Water relative permeability curve')

figure; hold all, set(gca,'FontSize',14)
for i=1:3
    krOW = fo{i}.krOW(s); krOW(s>1-fo{i}.sWcon)=nan;
    krOG = fo{i}.krOG(s); krOG(s>1-fo{i}.sWcon)=nan;
    plot(s, krOW, '-','Color',col(i,:), 'linewidth',2);
    plot(s, krOG, '--', 'linewidth', 4, 'Color',col(i,:))
end
grid on
h = findobj(gca,'Type','line');
legend(h([6 5 6 4 2]),'oil/water system', 'oil/gas system', name{:}, ...
       'Location', 'NorthWest');
xlabel('Oil saturation');
title('Oil relative permeability curves')

figure; hold all, set(gca,'FontSize',14)
for i=1:3
    krG = fo{i}.krG(s); krG(s>1-fo{i}.sWcon)=nan;
    plot(s, krG, 'linewidth', 2,'Color',col(i,:))
end
grid on
legend(name{:},'Location','SouthEast');
xlabel('Gas saturation');
title('Gas relative permeability curve')
_images/showSPEfluids_01.png
_images/showSPEfluids_02.png
_images/showSPEfluids_03.png

Plot three-phase relative permeability for oil

When all three phases are present simultaneously in a single cell, we need to use some functional relationship to combine the two-phase curves in a reasonable manner, resulting in a two-dimensional relative permeability model. Herein, we use a simple linear interpolation, which is also the default in Eclipse

[sw, sg] = meshgrid(linspace(0,1,201));
so=1-sw-sg;
for i=1:3
    [~, krO] = fo{i}.relPerm(sw(:),sg(:));
    krO = reshape(krO,size(sw)); krO(sw+sg>1)=nan;

    figure; set(gca,'FontSize',14);
    [mapx,mapy] = ternaryAxis('names',{'S_w';'S_g';'S_o'},'FontSize',14);
    contourf(mapx(sw,sg,so), mapy(sw,sg,so), krO, 20)

    set(gca,'YLim',[0 sqrt(3)/2]);
    text(mapx(.45,.45,.1),mapy(.45,.45,.1),'Oil relative permeability', ...
        'BackgroundColor','w','HorizontalAlignment','center','FontSize',14);
    caxis([0 1]);
end

% =========================================================================
_images/showSPEfluids_04.png
_images/showSPEfluids_05.png
_images/showSPEfluids_06.png

Phase behavior of live oil for the SPE 1 or SPE 9 model

In the following, we will briefly show the phase behavior for gas and oil in the model. That is, we will show both the input data points and the interpolated data obtained from the functions in the fluid object

% Set input deck and fluid object
[deck, f] = deal(decks{1}, fo{1});

% Extract data from the input deck
pvto = deck.PROPS.PVTO{1};
rsd  = pvto.key([1:end end]);
pbp  = pvto.data([pvto.pos(1:end-1)' end],1);
Bod  = pvto.data([pvto.pos(1:end-1)' end],2);
muOd = pvto.data([pvto.pos(1:end-1)' end],3);

Plot Rs

figure, set(gca,'FontSize',14)
pargs = {'LineWidth', 2, 'MarkerSize',7,'MarkerFaceColor',[.5 .5 .5]};
plot(pbp/barsa,rsd,'-o',pargs{:}); xlabel('Pressure [bar]');
_images/showSPEfluids_07.png

Plot oil formation-volume factor and viscosity for oil

For Bo, we plot both the tabulated data as well as interpolated data at various combinations of dissolved gas and reservoir pressures. For each data point, we must first determine whether the state is saturated or not by comparing the actual amount of dissolved gas against the maximum possible amount of solved gas for this pressure.

[M, N]    = deal(11,51);
[RsMax,pMax] = deal(max(rsd), max(pbp));
[rs,p]    = meshgrid(linspace(10,RsMax-10,M), linspace(0,pMax,N));
Rv        = reshape(f.rsSat(p(:)), N, M);
isSat     = rs >= Rv;
rs(isSat) = Rv(isSat);
Bo        = 1./reshape(f.bO(p(:), rs(:), isSat(:)),N,M);
muO       = reshape(f.muO(p(:), rs(:), isSat(:)),N,M);

% Formation-volume factor
figure, hold on, set(gca,'FontSize',14)
for j=1:M
    i = isSat(:,j);
    plot(p(i,j)/barsa, Bo(i,j),'b-',p(~i,j)/barsa,Bo(~i,j),'-r');
end
plot(pbp/barsa,Bod,'-bo',pargs{:});
hold off, axis tight, xlabel('Pressure [bar]');
title('Oil formation-volume factor [-]');

% Viscosity
figure, hold on, set(gca,'FontSize',14)
for j=1:M
    i = isSat(:,j);
    plot(p(i,j)/barsa,  convertTo(muO(i,j),  centi*poise),'b-',...
         p(~i,j)/barsa, convertTo(muO(~i,j), centi*poise),'-r');
end
plot(pbp/barsa, convertTo(muOd, centi*poise),'-bo',pargs{:});
hold off, axis tight, xlabel('Pressure [bar]');
title('Oil viscosity [cP]');
_images/showSPEfluids_08.png
_images/showSPEfluids_09.png

Phase behavior for the dry gas (SPE 1 or SPE9)

pvdg = deck.PROPS.PVDG{1};

figure, set(gca,'FontSize',14);
plot(pvdg(:,1)/barsa,pvdg(:,2),'-o',pargs{:})
set(gca,'YScale','log')
xlabel('Pressure [bar]');
title('Gas formation-volume factor [-]');

figure, set(gca,'FontSize',14);
plot(pvdg(:,1)/barsa,convertTo(pvdg(:,3), centi*poise), '-o',pargs{:})
xlabel('Pressure [bar]');
title('Gas viscosity [cP]');
_images/showSPEfluids_10.png
_images/showSPEfluids_11.png

Show inconsistencies for large pressures (only SPE1)

For sufficintly high pressure/gas-oil ratios, the interpolated shrinkage factors will cross zero, which causes blowup and negative Bo values. In turn, this leads to negative partial volumes.

[M, N]    = deal(101,201);
[rs,p]    = meshgrid(linspace(f.rsSat(100*barsa),f.rsSat(1300*barsa),M), ...
              linspace(100,1300,N)*barsa);
Rv        = reshape(f.rsSat(p(:)), N, M);
isSat     = rs >= Rv;
rs(isSat) = Rv(isSat);
bo        = reshape(f.bO (p(:), rs(:), isSat(:)),N,M);
Bo        = 1./bo;
muO       = reshape(f.muO(p(:), rs(:), isSat(:)),N,M);
bg        = reshape(f.bG (p(:)), N, M);
Bg        = 1./bg;

figure
contourf(p/barsa,rs,bo,[-inf linspace(-.2,.9,12) inf]); axis tight
set(gca,'FontSize',20); xlabel('Pressure [bar]'); ylabel('Gas-oil ratio');
cb = colorbar; set(cb,'FontSize',20);
colormap(repmat([linspace(.2,.3,4) linspace(.6,1,20)]',1,3));

figure
contourf(p/barsa,rs,1./bo - rs./bg,[-inf linspace(-10,10,41) inf]); axis tight
set(gca,'FontSize',20); xlabel('Pressure [bar]');ylabel('Gas-oil ratio');
caxis([-10,10]); cb = colorbar; set(cb,'FontSize',20);
colormap(repmat([linspace(.25,.5,20) linspace(.7,.95,20)]',1,3));

% =========================================================================
_images/showSPEfluids_12.png
_images/showSPEfluids_13.png

Dead oil and gas with vaporized oil (SPE3)

The SPE 3 benchmark was designed to study gas cycling in a rich�?gas reservoir with retrograde condensation. The original setup was for compositional simulation. The current input file describes a black-oil translation of the compositional model. into a

[deck, f] = deal(decks{2}, fo{2});
pvdo = deck.PROPS.PVDO{1};

figure, set(gca,'FontSize',14);
pargs = {'LineWidth', 2, 'MarkerSize',7,'MarkerFaceColor',[.5 .5 .5]};
plot(pvdo(:,1)/barsa, pvdo(:,2),'-o',pargs{:})
xlabel('Pressure [bar]');
title('Oil formation-volume factor [-]');

figure, set(gca,'FontSize',14);
plot(pvdo(:,1)/barsa, convertTo(pvdo(:,3), centi*poise), '-o',pargs{:})
xlabel('Pressure [bar]');
title('Oil viscosity [cP]');
_images/showSPEfluids_14.png
_images/showSPEfluids_15.png

Rich gas with retrograde condensation

Extract data from the input deck

pvtg = deck.PROPS.PVTG{1};
pbp  = convertTo(pvtg.key,barsa);
rvd  = pvtg.data([1 pvtg.pos(2:end-1)'],1);
Bgd  = pvtg.data(:,2);
muGd = convertTo(pvtg.data(:,3), centi*poise);
p    = pvtg.key(rldecode((1:numel(pvtg.key))',diff(pvtg.pos),1))/barsa;

Plot Rv

figure, set(gca,'FontSize',14)
hold on
for i=1:numel(pbp)
    ind = pvtg.pos(i):pvtg.pos(i+1)-1;
    plot(p(ind),pvtg.data(ind,1),'-ok',...
        'MarkerSize',6,'MarkerFaceColor',[.5 .5 .5]);
end
plot(pbp,rvd,'-o',pargs{:}); xlabel('Pressure [bar]');
title('Maximum vaporized oil-gas ratio [-]');
hold off
_images/showSPEfluids_16.png

Plot Bg

figure, set(gca,'FontSize',14)
plot(pbp,Bgd([pvtg.pos(2:end-1)'-1 end]),'-r', ...
    pbp, Bgd(pvtg.pos(1:end-1)), '-bo', pargs{:});
xlabel('Pressure [bar]');
title('Gas formation-volume factor [-]');

% Make inset
axes('position',[.48 .45 .4 .4]);
hold on
for i=1:numel(pbp)
    ind = pvtg.pos(i):pvtg.pos(i+1)-1;
    plot(p(ind),pvtg.data(ind,2),'-ok',...
        'MarkerSize',6,'MarkerFaceColor',[.5 .5 .5]);
end
plot(pbp,Bgd([pvtg.pos(2:end-1)'-1 end]),'-r', ...
    pbp, Bgd(pvtg.pos(1:end-1)), '-bo', pargs{:});
hold off
axis([205 240 4.2e-3 4.8e-3]);
_images/showSPEfluids_17.png

Plot gas density

bG = f.bG(p*barsa, pvtg.data(:,1), false(size(p)));
rho = bG*f.rhoGS + bG.*pvtg.data(:,1)*f.rhoOS;
figure, hold on, set(gca,'FontSize', 14)
for i=1:numel(pbp),
    ind = pvtg.pos(i):pvtg.pos(i+1)-1;
    plot(p(ind),rho(ind),'-ok',...
        'MarkerSize',6,'MarkerFaceColor',[.5 .5 .5]);
end
plot(pbp,rho([pvtg.pos(2:end-1)'-1 end]),'-or', ...
    pbp, rho(pvtg.pos(1:end-1)), '-bo', pargs{:});
hold off
xlabel('Pressure [bar]'); title('Density of gaseous phase [kg/m3]');
_images/showSPEfluids_18.png

Plot muG

figure, hold on, set(gca,'FontSize',14)
for i=1:numel(pbp)
    ind = pvtg.pos(i):pvtg.pos(i+1)-1;
    plot(p(ind), muGd(ind), '-ok',...
        'MarkerSize',6,'MarkerFaceColor',[.5 .5 .5] );
end
plot(pbp,muGd([pvtg.pos(2:end-1)'-1 end]),'-or', ...
    pbp, muGd(pvtg.pos(1:end-1)), '-ob',pargs{:});
hold off
xlabel('Pressure [bar]');
title('Gas viscosity [cP]');
_images/showSPEfluids_19.png

Interpolation/extrapolation of viscosity

For sufficintly high pressure/gas-oil ratios, the interpolated viscosity values exhibit unphysical behavior

[M, N]    = deal(51,51);
[rs,pp]    = meshgrid(linspace(0,f.rvSat(240*barsa),M), ...
              linspace(200,240,N)*barsa);
Rv        = reshape(f.rvSat(pp(:)), N, M);
isSat     = rs >= Rv;
rs(isSat) = Rv(isSat);
muG       = reshape(f.muG(pp(:), rs(:), isSat(:)),N,M);

figure;
contourf(pp/barsa,rs,convertTo(muG,centi*poise),21);
hold on
p    = pvtg.key(rldecode((1:numel(pvtg.key))',diff(pvtg.pos),1))/barsa;
for i=1:numel(pbp)
    ind = pvtg.pos(i):pvtg.pos(i+1)-1;
    plot(p(ind),pvtg.data(ind,1),'-ok',...
        'MarkerSize',6,'MarkerFaceColor',[.5 .5 .5]);
end
plot(pbp,rvd,'-o',pargs{:});
hold off
cb = colorbar; set([gca;cb],'FontSize',20); xlabel('Pressure [bar]');
_images/showSPEfluids_20.png

Plot viscosity as function of pressure only for a larger pressure interval

[M, N]    = deal(11,21);
[rs,pp]    = meshgrid(linspace(0,f.rvSat(280*barsa),M), ...
              linspace(160,280,N)*barsa);
Rv        = reshape(f.rvSat(pp(:)), N, M);
isSat     = rs >= Rv;
rs(isSat) = Rv(isSat);
muG       = convertTo(reshape(f.muG(pp(:), rs(:), isSat(:)),N,M),centi*poise);

figure
y = muG; y(isSat)=NaN;
z = muG; z(~isSat) = NaN;
plot(pp/barsa, y,'k--');
hold on
plot(pp/barsa, z, '--k','LineWidth',3);
hold on, set(gca,'FontSize',20)
for i=1:numel(pbp)
    ind = pvtg.pos(i):pvtg.pos(i+1)-1;
    plot(p(ind), muGd(ind), '-ok',...
        'MarkerSize',6,'MarkerFaceColor',[.5 .5 .5] );
end
plot(pbp,muGd([pvtg.pos(2:end-1)'-1 end]),'-ok', ...
    pbp, muGd(pvtg.pos(1:end-1)), '-ok',pargs{:});
hold off
xlabel('Pressure [bar]');
axis([160 280 0.015 0.08]);
_images/showSPEfluids_21.png

Conceptual three-phase compressible model

Generated from threePhaseAD.m

The purpose of the example is to show how one can implement compressible three-phase flow models using automatic differentiation and the abstract differentiation operators in MRST. To this end, we set up a relatively simple model in which the upper 1/3 is initially filled with gas, the middle 1/3 is filled with oil, and the bottom 1/3 is filled with water. Water is injected in the lower southwest corner and fluids are produced from the northeast corner of the middle layer. To mimic the effect of injector and producer wells, we manipulate the flow equations to ensure reasonable inflow/outflow for the perforated cells. The code can be run with or without water injection. Relative permeabilities are sampled from the SPE3 benchmark. Physical properties are in generally chosen quite haphazardly for illustration purposes. The last plots use ‘hold all’ so that if you run the function twice with or without water injection, the plots of pressures and oil and gas production will be added to the same plots.

mrstModule add ad-core ad-props deckformat

if exist('pvi','var')~=1
    pvi = .5;
end
useInj = (pvi>0);

Set up model geometry

[nx,ny,nz] = deal(  11,  11,  3);
[Dx,Dy,Dz] = deal(200, 200, 50);
G = cartGrid([nx, ny, nz], [Dx, Dy, Dz]);
G = computeGeometry(G);

Define rock model

rock = makeRock(G, 30*milli*darcy, 0.2);
cr   = 1e-6/psia;
pR   = 200*barsa;
pvR  = poreVolume(G, rock);
pv   = @(p) pvR .* exp(cr * (p - pR) );

Fluid model

Water is slightly compressible with quadratic relative permeability

muW    = 1*centi*poise;
cw     = 2e-6/psia;
rhoWR  = 1014*kilogram/meter^3;
rhoWS  = 1000*kilogram/meter^3;
rhoW   = @(p) rhoWR .* exp( cw * (p - pR) );

% Oil phase is lighter and has a cubic relative permeability
muO    = 0.5*centi*poise;
co     = 1e-5/psia;
rhoOR  = 850*kilogram/meter^3;
rhoOS  = 750*kilogram/meter^3;
rhoO   = @(p) rhoOR .* exp( co * (p - pR) );

% Gas
muG    = 0.015*centi*poise;
cg     = 1e-3/psia;
rhoGR  = 1.2;
rhoGS  = 1;
rhoG   = @(p) rhoGR .* exp( cg * (p - pR) );

% Get relative permeabilities from the SPE3 data set
pth  = getDatasetPath('spe3');
fn   = fullfile(pth, 'BENCH_SPE3.DATA');
deck = readEclipseDeck(fn);
deck = convertDeckUnits(deck);
f    = initDeckADIFluid(deck);
f    = assignRelPerm(f);

Extract grid information

nf = G.faces.num;                                 % Number of faces
nc = G.cells.num;                                 % Number of cells
N  = double(G.faces.neighbors);                   % Map: face -> cell
F  = G.cells.faces(:,1);                          % Map: cell -> face
intInx = all(N ~= 0, 2);                          % Interior faces
N  = N(intInx, :);                                % Interior neighbors

Compute transmissibilities

hT = computeTrans(G, rock);                       % Half-transmissibilities
T  = 1 ./ accumarray(F, 1 ./ hT, [nf, 1]);        % Harmonic average
T  = T(intInx);                                   % Restricted to interior
nf = size(N,1);

Define discrete operators

D     = sparse([(1:nf)'; (1:nf)'], N, ones(nf,1)*[-1 1], nf, nc);
grad  = @(x)  D*x;
div   = @(x)-D'*x;
avg   = @(x) 0.5 * (x(N(:,1)) + x(N(:,2)));
upw   = @(flag, x) flag.*x(N(:, 1)) + ~flag.*x(N(:, 2));
gradz = grad(G.cells.centroids(:,3));

Impose vertical equilibrium

gravity reset on,
g     = norm(gravity);
equil = ode23(@(z,p) g.* rhoO(p), [0, max(G.cells.centroids(:,3))], pR);
p0    = reshape(deval(equil, G.cells.centroids(:,3)), [], 1);  clear equil
sW0   = f.sWcon*ones(G.cells.num, 1);
sW0   = reshape(sW0,G.cartDims); sW0(:,:,nz)=1; sW0 = sW0(:);
sG0   = zeros(G.cells.num, 1);
sG0   = reshape(sG0,G.cartDims); sG0(:,:, 1)=1-f.sWcon; sG0 = sG0(:);

ioip  = sum(pv(p0).*(1-sW0 - sG0).*rhoO(p0));
igip  = sum(pv(p0).*sG0.*rhoG(p0));

Schedule and injection/production

nstep = 108;
Tf = 1080*day;
dt = Tf/nstep*ones(1,nstep);
dt = [dt(1).*sort(repmat(2.^-[1:5 5],1,1)) dt(2:end)];
nstep = numel(dt);

[inRate,  inIx ] = deal(pvi*sum(pv(p0))/Tf, nx*ny*nz-nx*ny+1);
[outPres, outIx] = deal(100*barsa, 2*nx*ny);

Initialize for solution loop

[p, sW, sG]     = initVariablesADI(p0, sW0, sG0);
[pIx, wIx, gIx] = deal(1:nc, (nc+1):(2*nc), (2*nc+1):(3*nc));
[tol, maxits]   = deal(1e-5,15);
pargs = {};% {'EdgeColor','k','EdgeAlpha',.1};

sol = repmat(struct('time',[],'pressure',[], 'sW', [], 'sG', []),[nstep+1,1]);
sol(1) = struct('time', 0, 'pressure', value(p),'sW', value(sW), 'sG', value(sG));

Main loop

t = 0;
hwb = waitbar(t,'Simulation ..');
nits = nan(nstep,1);
i = 1;
for n=1:nstep

    t = t + dt(n);
    fprintf('\nTime step %d: Time %.2f -> %.2f days\n', ...
        n, convertTo(t - dt(n), day), convertTo(t, day));

    % Newton loop
    resNorm   = 1e99;
    [p0, sW0, sG0] = deal(value(p), value(sW),value(sG));
    nit = 0;
    while (resNorm > tol) && (nit <= maxits)

        % Densities and pore volumes
        [rW,rW0,rO,rO0,rG,rG0] = ....
            deal(rhoW(p), rhoW(p0), rhoO(p), rhoO(p0), rhoG(p), rhoG(p0));
        [vol, vol0]     = deal(pv(p), pv(p0));

        % Mobility: Relative permeability over constant viscosity
        [krW, krO, krG] = f.relPerm(sW,sG);
        mobW = krW./muW;
        mobO = krO./muO;
        mobG = krG./muG;

        % Pressure differences across each interface
        dp  = grad(p);
        dpW = dp-g*avg(rW).*gradz;
        dpO = dp-g*avg(rO).*gradz;
        dpG = dp-g*avg(rG).*gradz;

        % Phase fluxes:
        % Density and mobility is taken upwinded (value on interface is
        % defined as taken from the cell from which the phase flow is
        % currently coming from). This gives more physical solutions than
        % averaging or downwinding.
        vW = -upw(value(dpW) <= 0, rW.*mobW).*T.*dpW;
        vO = -upw(value(dpO) <= 0, rO.*mobO).*T.*dpO;
        vG = -upw(value(dpG) <= 0, rG.*mobG).*T.*dpG;

        % Conservation of water and oil
        water = (1/dt(n)).*(vol.*rW.*sW - vol0.*rW0.*sW0) + div(vW);
        oil   = (1/dt(n)).*(vol.*rO.*(1-sW-sG) - vol0.*rO0.*(1-sW0-sG0)) + div(vO);
        gas   = (1/dt(n)).*(vol.*rG.*sG - vol0.*rG0.*sG0) + div(vG);

        % Injector: volumetric source term multiplied by surface density
        if useInj
            water(inIx) = water(inIx) - inRate.*rhoWS;
        end

        % Producer: replace equations by new ones specifying fixed pressure
        % and zero water saturation
        oil(outIx) = p(outIx) - outPres;
        water(outIx) = sW(outIx);
        gas(outIx)   = sG(outIx);

        % Collect and concatenate all equations (i.e., assemble and
        % linearize system)
        eqs = {oil, water, gas};
        eq  = cat(eqs{:});

        % Compute Newton update and update variable
        res = eq.val;
        upd = -(eq.jac{1} \ res);
        p.val  = p.val   + upd(pIx);
        sW.val = sW.val + upd(wIx);
        %sW.val = max( min(sW.val, 1), 0);
        sG.val = sG.val + upd(gIx);
        %sG.val = max( min(sG.val, 1), 0);

        resNorm = norm(res);
        nit     = nit + 1;
        fprintf('  Iteration %3d:  Res = %.4e\n', nit, resNorm);
    end
    if nit > maxits
        error('Newton solves did not converge')
    else % plot
        nits(n) = nit;

        figure(1); clf
        subplot(2,1,1)
        plotCellData(G, value(p)/barsa, pargs{:});
        title('Pressure'), view(30, 40);

        subplot(2,1,2)
        sg = value(sG); sw = value(sW);
        plotCellData(G,[sg, 1-sg-sw, sw]/1.001+1e-4,pargs{:});             % Avoid color artifacts
        caxis([0, 1]), view(30, 40); title('Saturation')
        drawnow

        sol(n+1) = struct('time', t, ...
                          'pressure', value(p), ...
                          'sW', value(sW), ...
                          'sG', value(sG));
        waitbar(t/Tf,hwb);
        pause(.1);
    end
end
close(hwb);
Time step 1: Time 0.00 -> 0.31 days
  Iteration   1:  Res = 1.0208e+07
  Iteration   2:  Res = 4.1526e+00
  Iteration   3:  Res = 9.3067e-01
  Iteration   4:  Res = 6.8833e-02
  Iteration   5:  Res = 2.6936e-02
  Iteration   6:  Res = 6.3870e-02
...
_images/threePhaseAD_01.png
figure(2),
subplot(2,1,2-value(useInj));
bar(nits,1,'EdgeColor','r','FaceColor',[.6 .6 1]);
axis tight, set(gca,'YLim',[0 10]);
_images/threePhaseAD_02.png
figure(3); hold all
ipr = arrayfun(@(x) x.pressure(inIx), sol)/barsa;
t   = arrayfun(@(x) x.time, sol)/day;
fpr = arrayfun(@(x) sum(x.pressure)/G.cells.num, sol)/barsa;
plot(t,ipr,'-', t,fpr, '--');
_images/threePhaseAD_03.png
igure(4);
oip = arrayfun(@(x) sum(pv(x.pressure).*(1-x.sW-x.sG).*rhoO(x.pressure)), sol);
gip = arrayfun(@(x) sum(pv(x.pressure).*x.sG.*rhoG(x.pressure)), sol);
subplot(1,2,1), hold all
plot(t,(ioip-oip)./rhoOS)
subplot(1,2,2), hold all
plot(t,(igip-gip)./rhoGS)
_images/threePhaseAD_04.png

Conceptual two-phase compressible model

Generated from twoPhaseAD.m

The purpose of the example is to show how one can implement compressible multiphase flow models using automatic differentiation and the abstract differentiation operators in MRST. To this end, we set up a relatively simple model in which the upper 2/3 is initially filled with oil and the bottom 1/3 is filled with water. Water is injected in the lower southwest corner and fluids are produced from the upper northeast corner. To mimic the effect of injector and producer wells, we manipulate the flow equations to ensure reasonable inflow/outflow for the perforated cells. The code can be run with compressible or incompressible fluids and rock. (The compressibility of oil is somewhat exaggerated for illustration purposes.) The last plots use ‘hold all’ so that if you run the function twice with compressible/incompressible model, the plots of pressures, condition numbers, and oil production will be added to the same plots.

if exist('isCompr','var')~=1
    isCompr = false;
end

Set up model geometry

[nx,ny,nz] = deal(  11,  11,  3);
[Dx,Dy,Dz] = deal(200, 200, 50);
G = cartGrid([nx, ny, nz], [Dx, Dy, Dz]);
G = computeGeometry(G);

figure(1); clf
plotGrid(G); view(3); axis tight
_images/twoPhaseAD_01.png

Define rock model

rock = makeRock(G, 30*milli*darcy, 0.2);
cr   = 1e-6/psia*double(isCompr);
pR   = 200*barsa;
pvR  = poreVolume(G, rock);
pv   = @(p) pvR .* exp(cr * (p - pR) );

p = linspace(100*barsa,250*barsa,30)';
s = linspace(0,1,50)';
figure(1), clf
plot(p/barsa, pvR(1).*exp(cr*(p-pR)),'LineWidth',2);
_images/twoPhaseAD_02.png

Fluid model

Water is slightly compressible with quadratic relative permeability

muW    = 1*centi*poise;
cw     = 2e-6/psia*double(isCompr);
rhoWR  = 1014*kilogram/meter^3;
rhoWS  = 1000*kilogram/meter^3;
rhoW   = @(p) rhoWR .* exp( cw * (p - pR) );
krW    = @(S) S.^2;

% Oil phase is lighter and has a cubic relative permeability
muO    = 5*centi*poise;
co     = 1e-5/psia*double(isCompr);
rhoOR  = 850*kilogram/meter^3;
rhoOS  = 750*kilogram/meter^3;
rhoO   = @(p) rhoOR .* exp( co * (p - pR) );
krO    = @(S) S.^3;

% Plot compressibility factors
figure(1), subplot(1,2,1)
plot(p/barsa, [rhoW(p), rhoO(p)],'LineWidth',2);
legend('Water density', 'Oil density','Location','best')

% Plot relative permeability
subplot(1,2,2);
plot(s, [krW(s), krO(s)],'LineWidth',2);
legend('krW', 'krO','Location','NorthWest');
_images/twoPhaseAD_03.png

Extract grid information

nf = G.faces.num;                                 % Number of faces
nc = G.cells.num;                                 % Number of cells
N  = double(G.faces.neighbors);                   % Map: face -> cell
F  = G.cells.faces(:,1);                          % Map: cell -> face
intInx = all(N ~= 0, 2);                          % Interior faces
N  = N(intInx, :);                                % Interior neighbors

Compute transmissibilities

hT = computeTrans(G, rock);                       % Half-transmissibilities
T  = 1 ./ accumarray(F, 1 ./ hT, [nf, 1]);        % Harmonic average
T  = T(intInx);                                   % Restricted to interior
nf = size(N,1);

Define discrete operators

D     = sparse([(1:nf)'; (1:nf)'], N, ones(nf,1)*[-1 1], nf, nc);
grad  = @(x)  D*x;
div   = @(x)-D'*x;
avg   = @(x) 0.5 * (x(N(:,1)) + x(N(:,2)));
upw   = @(flag, x) flag.*x(N(:, 1)) + ~flag.*x(N(:, 2));
gradz = grad(G.cells.centroids(:,3));
spy(D)
_images/twoPhaseAD_04.png

Impose vertical equilibrium

gravity reset on,
g     = norm(gravity);
equil = ode23(@(z,p) g.* rhoO(p), [0, max(G.cells.centroids(:,3))], pR);
p0    = reshape(deval(equil, G.cells.centroids(:,3)), [], 1);  clear equil
sW0   = zeros(G.cells.num, 1);
sW0   = reshape(sW0,G.cartDims); sW0(:,:,nz)=1; sW0 = sW0(:);
ioip  = sum(pv(p0).*(1-sW0).*rhoO(p0));

Schedule and injection/production

nstep = 36;
Tf = 360*day;
dt = Tf/nstep*ones(1,nstep);
dt = [dt(1).*sort(repmat(2.^-[1:5 5],1,1)) dt(2:end)];
nstep = numel(dt);

[inRate,  inIx ] = deal(.1*sum(pv(p0))/Tf, nx*ny*nz-nx*ny+1);
[outPres, outIx] = deal(100*barsa, nx*ny);

Initialize for solution loop

[p, sW]    = initVariablesADI(p0, sW0);
[pIx, sIx] = deal(1:nc, (nc+1):(2*nc));
[tol, maxits] = deal(1e-5,15);
pargs = {};% {'EdgeColor','k','EdgeAlpha',.1};

sol = repmat(struct('time',[],'pressure',[], 's', []),[nstep+1,1]);
sol(1) = struct('time', 0, 'pressure', value(p),'s', value(sW));

Main loop

t = 0;
hwb = waitbar(t,'Simulation ..');
nits = nan(nstep,1);
i = 1; clear cnd;
for n=1:nstep

    t = t + dt(n);
    fprintf('\nTime step %d: Time %.2f -> %.2f days\n', ...
        n, convertTo(t - dt(n), day), convertTo(t, day));

    % Newton loop
    resNorm   = 1e99;
    [p0, sW0] = deal(value(p), value(sW));
    nit = 0;
    while (resNorm > tol) && (nit <= maxits)

        % Densities and pore volumes
        [rW,rW0,rO,rO0] = deal(rhoW(p), rhoW(p0), rhoO(p), rhoO(p0));
        [vol, vol0]     = deal(pv(p), pv(p0));

        % Mobility: Relative permeability over constant viscosity
        mobW = krW(sW)./muW;
        mobO = krO(1-sW)./muO;

        % Pressure differences across each interface
        dp  = grad(p);
        dpW = dp-g*avg(rW).*gradz;
        dpO = dp-g*avg(rO).*gradz;

        % Phase fluxes:
        % Density and mobility is taken upwinded (value on interface is
        % defined as taken from the cell from which the phase flow is
        % currently coming from). This gives more physical solutions than
        % averaging or downwinding.
        vW  = -upw(value(dpW) <= 0, rW.*mobW).*T.*dpW;
        vO  = -upw(value(dpO) <= 0, rO.*mobO).*T.*dpO;

        % Conservation of water and oil
        water = (1/dt(n)).*(vol.*rW.*sW - vol0.*rW0.*sW0) + div(vW);
        oil   = (1/dt(n)).*(vol.*rO.*(1-sW) - vol0.*rO0.*(1-sW0)) + div(vO);

        % Injector: volumetric source term multiplied by surface density
        water(inIx) = water(inIx) - inRate.*rhoWS;

        % Producer: replace equations by new ones specifying fixed pressure
        % and zero water saturation
        oil(outIx) = p(outIx) - outPres;
        water(outIx)   = sW(outIx);

        % Collect and concatenate all equations (i.e., assemble and
        % linearize system)
        eqs = {oil, water};
        eq  = combineEquations(eqs{:});

        % Measure condition number
        cnd(i) = condest(eq.jac{1}); i = i+1;                              %#ok<SAGROW>

        % Compute Newton update and update variable
        res = eq.val;
        upd = -(eq.jac{1} \ res);
        p.val  = p.val   + upd(pIx);
        sW.val = sW.val + upd(sIx);
        sW.val = max( min(sW.val, 1), 0);

        resNorm = norm(res);
        nit     = nit + 1;
        fprintf('  Iteration %3d:  Res = %.4e\n', nit, resNorm);
    end
    % Add line with NaN in cnd variable to signify end of time step
    cnd(i) = NaN; i=i+1;

    if nit > maxits
        error('Newton solves did not converge')
    else % plot
        nits(n) = nit;

        figure(1); clf
        subplot(2,1,1)
        plotCellData(G, value(p)/barsa, pargs{:});
        title('Pressure'), view(30, 40); caxis([100 250]);

        subplot(2,1,2)
        plotCellData(G, 1-value(sW), pargs{:});
        caxis([0, 1]), view(30, 40); title('Saturation')
        drawnow

        sol(n+1) = struct('time', t, ...
                          'pressure', value(p), ...
                          's', value(sW));
        waitbar(t/Tf,hwb);
        pause(.1);
    end
end
close(hwb);
Time step 1: Time 0.00 -> 0.31 days
Warning: Matrix is close to singular or badly scaled. Results may be inaccurate.
RCOND =  4.099831e-23.
  Iteration   1:  Res = 1.0069e+07
Warning: Matrix is close to singular or badly scaled. Results may be inaccurate.
RCOND =  5.906300e-22.
  Iteration   2:  Res = 1.1772e+15
...
_images/twoPhaseAD_05.png
figure(2),
subplot(2,1,2-double(isCompr));
bar(nits,1,'EdgeColor','r','FaceColor',[.6 .6 1]);
axis tight, set(gca,'YLim',[0 10]);
hold on,
plotyy(nan,nan,1:numel(dt),dt/day,@plot, ...
    @(x,y) plot(x,y,'-o','MarkerFaceColor',[.6 .6 .6]));
_images/twoPhaseAD_06.png
figure(3); hold all
ipr = arrayfun(@(x) x.pressure(inIx), sol)/barsa;
t   = arrayfun(@(x) x.time, sol)/day;
fpr = arrayfun(@(x) sum(x.pressure)/G.cells.num, sol)/barsa;
plot(t,ipr,'-', t,fpr, '--');
% chl = get(gca,'Children');
% col=flipud(lines(2));
% for i=1:numel(chl), set(chl(i),'Color',col(ceil(i/2),:),'LineWidth',2); end
_images/twoPhaseAD_07.png
figure(4); hold all
oip = arrayfun(@(x) sum(pv(x.pressure).*(1-x.s).*rhoO(x.pressure)), sol);
plot(t,(ioip - oip)./rhoOS,'-o','MarkerFaceColor',[.6 .6 .6])
_images/twoPhaseAD_08.png
figure(5); hold all
plot(cumsum(isnan(cnd))+1, 1./cnd,'-o');
_images/twoPhaseAD_09.png
{
g = cartGrid([1 1 1],[Dx Dy Dz]);
figure; colormap(winter);
for i=1:nstep+1
    s = sol(i).s;
    clf
    plotCellData(G,1-s,s>1e-3,'EdgeColor','k','EdgeAlpha',.1);
    plotGrid(g,'EdgeColor','k','FaceColor','none');
    view(3); plotGrid(G,[inIx; outIx],'FaceColor',[.6 .6 .6]);
    view(30,40); caxis([0 1]); axis tight off
    axis([-.5 Dx+.5 -.5 Dy+.5 -.5 Dz+.5]);
    h=colorbar('horiz');
    pos = get(gca,'Position');
    set(h,'Position',[.13 .15 .775 .03]); set(gca,'Position',pos);
    title(['Time: ' num2str(t(i)) ' days']);
    drawnow, pause(.5)
    print('-dpng','-r0',sprintf('twoPhaseAD-%02d.png',i-1));
end
function u = outflow(u,~)
end

Advection: classic schemes

Generated from runCases.m

Initial data: set up the initial data and remember to add one ghost cell at each end of the interval. These cells will be used to impose boundary conditions

FontSize = 12; pargs = {'MarkerSize',8};
dx = 1/100;
x  = -.5*dx:dx:1+.5*dx;
u0 = sin((x-.1)*pi/.3).^2.*double(x>=.1 & x<=.4);
u0((x<.9) & (x>.6)) = 1;

% Flux function
f  = @(u) u;
df = @(u) 0*u + 1;

% Run simulation
uu = upw(u0, .5, dx, 1, f, df, @periodic);
uf = lxf(u0, .5, dx, 1, f, df, @periodic);
uw = lxw(u0, .5, dx, 1, f, df, @periodic);

% Plot results
figure('Position',[293 539 1000 300]);
i = (x>=.1 & x<=.4);
subplot(1,4,1,'FontSize',FontSize)
plot([0 x(i) .6 .6 .9 .9 1], [0 u0(i) 0 1 1 0 0], '-k',...
    x(2:end-1), uu(2:end-1), '.', pargs{:});
axis([0 1 -.2 1.2]); title('Upwind');

subplot(1,4,2,'FontSize',FontSize)
plot([0 x(i) .6 .6 .9 .9 1], [0 u0(i) 0 1 1 0 0], '-k',...
    x(2:end-1), uf(2:end-1), '.', pargs{:});
axis([0 1 -.2 1.2]); title('Lax-Friedrichs');

subplot(1,4,3,'FontSize',FontSize)
plot([0 x(i) .6 .6 .9 .9 1], [0 u0(i) 0 1 1 0 0], '-k',...
    x(2:end-1), uw(2:end-1), '.', pargs{:});
axis([0 1 -.2 1.2]); title('Lax-Wendroff');
_images/runCases_01.png

Advection: high-resolution schemes

lim = @(r) max(0,min(2*r,min(.5+.5*r,2)));
xh  = -1.5*dx:dx:1+1.5*dx;
u0 = sin((xh-.1)*pi/.3).^2.*double(xh>=.1 & xh<=.4);
u0((xh<.9) & (xh>.6)) = 1;

un = nt (u0, .25, dx, 1, f, df, @periodic, lim);
%uc = cuw(u0, .25, dx, 1, f, df, @periodic, lim);

subplot(1,4,4,'FontSize',FontSize)
i = (xh>=.1 & xh<=.4);
plot([0 x(i) .6 .6 .9 .9 1], [0 u0(i) 0 1 1 0 0], '-k', ...
    xh(3:end-2), un(3:end-2), '.', pargs{:});
title('Nessyahu-Tadmor');
axis([0 1 -.2 1.2]);
_images/runCases_02.png

Burgers’ equation

f = @(u) .5*u.^2;
df = @(u) u;

% Run simulations
dx = 1/20;
x  = -1-0.5*dx:dx:1+0.5*dx;
uf = sin(2*pi*x).*double(x>=-.5 & x<=.5);
uw = lxw (uf, .995, dx, .6, f, df, @outflow);
uf = lxf (uf, .995, dx, .6, f, df, @outflow);

xh = -1-1.5*dx:dx:1+1.5*dx;
uh = sin(2*pi*xh).*double(xh>=-.5 & xh<=.5);
uh = nt(uh, .495, dx, .6, f, df, @outflow, lim);

dx = 1/1000;
xr = -1-1.5*dx:dx:1+1.5*dx;
ur = sin(2*pi*xr).*double(xr>=-.5 & xr<=.5);
ur = cuw(ur, .495, dx, .6, f, df, @outflow);

% Plot results
figure('Position',[293 539 1000 300]);
subplot(1,3,1)
plot(xr(3:end-2),ur(3:end-2), '-', x(2:end-1), uf(2:end-1), 'o');
axis([-1 1 -1.1 1.1]); title('Lax--Friedrichs');
subplot(1,3,2)
plot(xr(3:end-2),ur(3:end-2), '-', x(2:end-1), uw(2:end-1), 'o');
axis([-1 1 -1.1 1.1]); title('Lax-Wendroff');
subplot(1,3,3)
plot(xr(3:end-2),ur(3:end-2), '-', xh(3:end-2), uh(3:end-2), 'o');
axis([-1 1 -1.1 1.1]); title('Nessyahu-Tadmor');
_images/runCases_03.png

Buckley-Leverett problem: classical schemes vs high-resolution

Flux function

f = @(u) u.^2./(u.^2 + (1-u).^2);
s = linspace(0,1,501);
dfv = max(diff(f(s))./diff(s));
df = @(u) 0*u + dfv;

% reference solution
T = 0.65;
dx = 1/1000;
xr = -.5*dx:dx:1+.5*dx;
u0 = 0*xr; u0(xr<.1)=1.0;
ur = upw(u0, .995, dx, T, f, df, @outflow);

% Solutions on coarser grids
N  = 50;
dx = 1/N;
x  = -.5*dx:dx:1+.5*dx;
u0 = 0*x; u0(x<.1)=1.0;

figure('Position',[293 539 1000 300]);
subplot(1,4,1,'FontSize',FontSize)
uu = upw(u0, .995, dx, T, f, df, @outflow);
plot(xr(2:end-1),ur(2:end-1), '-k', x(2:end-1), uu(2:end-1), '.', pargs{:});
axis([0 1 -.05 1.05]); title('Upwind');

subplot(1,4,2,'FontSize',FontSize)
uf = lxf(u0, .995, dx, T, f, df, @outflow);
plot(xr(2:end-1),ur(2:end-1), '-k', x(2:end-1), uf(2:end-1), '.', pargs{:});
axis([0 1 -.05 1.05]); title('Lax-Friedrichs');

subplot(1,4,3,'FontSize',FontSize)
uw = lxw(u0, .995, dx, T, f, df, @outflow);
plot(xr(2:end-1),ur(2:end-1), '-k', x(2:end-1), uw(2:end-1), '.', pargs{:});
axis([0 1 -.05 1.05]); title('Lax-Wendroff');

% High-resolution schemes
xh  = -1.5*dx:dx:1+1.5*dx;
u0 = 0*xh; u0(xh<.1)=1.0;
un = nt(u0, .495, dx, T, f, df, @inflow);
uc = cuw(u0, .495, dx, T, f, df, @inflow);
subplot(1,4,4,'FontSize',FontSize)
plot(xh(3:end-2), un(3:end-2), '.', xh(3:end-2), uc(3:end-2), '.r', ...
    xr(2:end-1),ur(2:end-1), 'k-', pargs{:});
axis([0 1 -.05 1.05]); title('High-resolution');
_images/runCases_04.png

Buckley-Leverett problem: using ‘incomp’ module

mrstModule add incomp
G = computeGeometry(cartGrid([100,1],[1 1]));
rock = makeRock(G, ones(G.cells.num,1), ones(G.cells.num,1));

fluid = initSimpleFluid('mu' , [   1,    1] .* centi*poise     , ...
                        'rho', [1000, 1000] .* kilogram/meter^3, ...
                        'n'  , [   2,    2]);
bc = fluxside([], G, 'Left',   1, 'sat', [1 0]);
bc = fluxside(bc, G, 'Right', -1, 'sat', [0 1]);

hT = computeTrans(G, rock);
rSol = initState(G, [], 0, [0 1]);

rSol = incompTPFA(rSol, G, hT, fluid, 'bc', bc);
rSole = explicitTransport(rSol, G, .65, rock, fluid, 'bc', bc);
rSoli = rSol; n = 13;
for i=1:n
    rSoli = implicitTransport(rSoli, G, .65/n, rock, fluid, 'bc', bc);
end
rSolt = rSol; n = 130;
for i=1:n
    rSolt = implicitTransport(rSolt, G, .65/n, rock, fluid, 'bc', bc);
end

dx = 1/1000;
xr = -.5*dx:dx:1+.5*dx;
u0 = 0*xr; u0(1)=1.0;
ur = upw(u0, .995, dx, .65, f, df, @outflow);

figure;
plot(G.cells.centroids(:,1), rSole.s(:,1),'o', ...
    G.cells.centroids(:,1), rSolt.s(:,1), 's', ...
    G.cells.centroids(:,1), rSoli.s(:,1), '*', ...
    xr(2:end-1),ur(2:end-1),'-k');
legend('Explicit','Implicit, CFL=1','Implicit, CFL=10','Location','SouthWest');
_images/runCases_05.png
function u=upw(u0,cfl,dx,T,flux,df,boundary)
u = u0; t = 0.0;
dt = cfl*dx/max(abs(df(u0)));
i = 2:numel(u0)-1;
while (t<T)
  if (t+dt > T)
     dt = (T-t);
  end
  t=t+dt;
  u = boundary(u);
  f = flux(u);
  u(i) = u(i) - dt/dx*(f(i)-f(i-1));
  dt = cfl*dx/max(abs(df(u)));
end

Visualizing the Johansen Data Set

Generated from showJohansenNPD.m

The Johansen formation is a candidate site for large-scale CO2 storage offshore the south-west coast of Norway. The MatMoRA project has developed a set of geological models based on available seismic and well data. Herein, we will inspect one instance of the model in more detail.

Faults and active/inactive cells

We start by reading the model from a file in the Eclipse format (GRDECL), picking the sector model with five vertical layers in the Johansen formation and with five shale layers above and one below.

sector = fullfile(getDatasetPath('johansen'), 'NPD5');
filename = [sector, '.grdecl'];
G = processGRDECL(readGRDECL(filename));

Porosity

The porosity data are given with one value for each cell in the model. We read all values and then pick only the values corresponding to active cells in the model.

args = {'EdgeAlpha'; 0.1; 'EdgeColor'; 'k'};
p    = load([sector, '_Porosity.txt'])'; p = p(G.cells.indexMap);
h    = plotCellData(G, p, args{:}); view(-45,15),
axis tight off, zoom(1.15), caxis([0.1 0.3]), colorbar;
set(gca,'Clipping','off')
_images/showJohansenNPD_01.png

From the plot, it seems like the formation has been pinched out and only contains the shale layers in the front part of the model. We verify this by plotting a filtered porosity field in which all values smaller than or equal 0.1 have been taken out.

delete(h), view(-15,40)
plotGrid(G,'FaceColor','none', args{:});
plotCellData(G, p, find(p>0.1), args{:})
_images/showJohansenNPD_02.png

Permeability

The permeability is given as a scalar field (Kx) similarly as the porosity. The tensor is given as K = diag(Kx, Kx, 0.1Kx) and we therefore only plot the x-component, Kx, using a logarithmic color scale.

clf
K = load([sector '_Permeability.txt'])'; K=K(G.cells.indexMap);
plotCellData(G, log10(K), args{:});
view(-45,15), axis tight off, zoom(1.15)
set(gca,'Clipping','off')

[hc,hh] = colorbarHist(K, [0.001 1000], 'East', 100, true);
p = get(hh,'Position'); set(hh,'Position',p.*[1 1 1.5 1]);
set(get(hh,'Children'),'FaceColor',[.6 .6 .6]);
set(hc,'Fontsize',16,'FontWeight','bold', 'XTick', 0.5, 'XTickLabel','mD', ...
    'YTickLabel', {10.^(-3:3)});
_images/showJohansenNPD_03.png

To show more of the permeability structure, we strip away the shale layers, starting with the layers with lowest permeability on top.

clf,
plotGrid(G,'FaceColor','none',args{:});
plotCellData(G, log10(K), find(K>0.01), args{:});
view(-60,40), axis tight off, zoom(1.15)
set(gca,'Clipping','off')

[hc,hh] = colorbarHist(K(K>0.01), [0.01 1000], 'East', 100, true);
set(get(hh,'Children'),'FaceColor',[.6 .6 .6]);
set(hc,'Fontsize',16,'FontWeight','bold', 'XTick', 0.5, 'XTickLabel','mD', ...
    'YTickLabel', {10.^(-2:3)});
_images/showJohansenNPD_04.png

Then we also take away the lower shale layer and plot the permeability using a linear color scale.

lf; plotGrid(G,'FaceColor','none',args{:});
h = plotCellData(G, K, find(K>0.1), args{:});
view(-60,40), axis tight off, zoom(1.15)
set(gca,'Clipping','off')

[hc,hh] = colorbarHist(K(K>0.1), [0 900], 'East', 100, false);
set(get(hh,'Children'),'FaceColor',[.6 .6 .6]);
set(hc,'Fontsize',16,'FontWeight','bold', 'XTick', 0.5, 'XTickLabel','mD');
_images/showJohansenNPD_05.png

Visualizing a realization of the SAIGUP project

Generated from showRockSAIGUP.m

The <http://www.fault-analysis-group.ucd.ie/Projects/SAIGUP.html>SAIGUP project is a systematic assessment of uncertainty in reserves and production estimates within an objectively defined geological parameterisation encompassing the majority of European clastic oil reservoirs. A broad suite of shallow marine sedimentological reservoir types are indexed to continuously varying 3D anisotropy and heterogeneity levels. Structural complexity ranges from unfaulted to compartmentalised, and fault types from transmissive to sealing. Several geostatistical realisations each for the geologically diverse reservoir types covering the pre-defined parameter-space are up-scaled, faulted and simulated with an appropriate production strategy for an approximately 20 year period. Herein, we will inspect in detail one instance of the model, which can be downloaded from the <http://www.sintef.no/Projectweb/MRST>MRST webpage

Show the structural model

We start by reading the model from a file in the Eclipse format (GRDECL), The file contains both active and inactive cells

grdecl = fullfile(getDatasetPath('SAIGUP'), 'SAIGUP.GRDECL');
grdecl = readGRDECL(grdecl);

MRST uses the strict SI conventions in all of its internal caluclations. The SAIGUP model, however, is provided using the ECLIPSE ‘METRIC’ conventions (permeabilities in mD and so on). We use the functions getUnitSystem and convertInputUnits to assist in converting the input data to MRST’s internal unit conventions.

usys   = getUnitSystem('METRIC');
grdecl = convertInputUnits(grdecl, usys);

Define geometry and rock properties

We generate a space-filling geometry using the processGRDECL function and then compute a few geometric primitives (cell volumes, centroids, etc.) by means of the computeGeometry function.

G = processGRDECL(grdecl);
G = computeGeometry(G);

figure
args = {'EdgeAlpha'; 0.1; 'EdgeColor'; 'k'};
plotGrid(G,'FaceColor','none', args{:});
plotFaces(G,find(G.faces.tag>0),'FaceColor','r', args{:});
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,60); zoom(1.1); camdolly(0,-0.2,0)
set(gca,'Clipping','off')

% The media (rock) properties can be extracted by means of the
% <matlab:doc('grdecl2Rock') grdecl2Rock> function.
rock = grdecl2Rock(grdecl, G.cells.indexMap);
_images/showRockSAIGUP_01.png

Porosity

The porosity data are given with one value for each cell in the model. First, we show the porosities as they are generated on a Cartesian grid that corresponds to geological ‘time zero’, i.e., an imaginary time at which all the depositional layers were stacked as horizontally on a Cartesian grid.

p = reshape(grdecl.PORO, G.cartDims);
clf
slice(p, 1, 1, 1); view(-135,30), shading flat,
axis equal off, set(gca,'ydir','reverse','zdir','reverse')
colorbar('horiz'); caxis([0.01 0.3]);
_images/showRockSAIGUP_02.png

Likewise, we show a histogram of the porosity

hist(p(:),100);
h=get(gca,'Children');
set(h(1),'EdgeColor',[0 0 0.4],'FaceColor','none')
h = legend('Porosity'); set(h,'FontSize',16);
_images/showRockSAIGUP_03.png

Then we show the porosities mapped onto the structural grid

clf
plotCellData(G,rock.poro, args{:});
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,55); zoom(1.4); camdolly(0,-0.2,0)
set(gca,'Clipping','off')
colorbar('horiz'); caxis([0.1 0.3])
_images/showRockSAIGUP_04.png

show also the net-to-gross

clf
plotCellData(G,rock.ntg, args{:});
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,60); zoom(1.4); camdolly(0,-0.2,0)
set(gca,'Clipping','off')
colorbar('horiz'); %caxis([0.1 0.3])
_images/showRockSAIGUP_05.png

and the satnum

clf
SN = grdecl.SATNUM(G.cells.indexMap); j = jet(60);
plotCellData(G,SN, args{:});
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,60); zoom(1.4); camdolly(0,-0.2,0)
set(gca,'Clipping','off')
colorbar('horiz'); caxis([0.5 6.5]), colormap(j(1:10:end,:))
_images/showRockSAIGUP_06.png

and a plot where we split them up

clf
plotGrid(G,'FaceColor','none', args{:});
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,60); zoom(1.4); camdolly(0,-0.2,0)
set(gca,'Clipping','off')
caxis([0.5 6.5]), colormap(j(1:10:end,:))

h1 = plotCellData(G,SN, find(SN==1), args{:});
h2 = plotCellData(G,SN, find(SN==5), args{:});
_images/showRockSAIGUP_07.png
delete([h1,h2])
h1 = plotCellData(G,SN, find(SN==2), args{:});
h2 = plotCellData(G,SN, find(SN==4), args{:});
_images/showRockSAIGUP_08.png
delete([h1,h2])
h1 = plotCellData(G,SN, find(SN==3), args{:});
h2 = plotCellData(G,SN, find(SN==6), args{:});
_images/showRockSAIGUP_09.png

Permeability

The permeability is given as a tridiagonal tensor K = diag(Kx, Ky, Kz) and we therefore plot each of the horizontal and vertical components using a logarithmic color scale.

figure;
h = plotCellData(G,log10(rock.perm(:,1)), args{:});
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,55); zoom(1.4); camdolly(0,-0.2,0)
set(gca,'Clipping','off')

% Manipulate the colorbar to get the ticks we want
hc = colorbar('horiz');
cs = [0.001 0.01 0.1 1 10 100 1000 10000];
caxis(log10([min(cs) max(cs)]*milli*darcy));
set(hc, 'YTick', 0.5, 'YTickLabel','mD', ...
   'XTick', log10(cs*milli*darcy), 'XTickLabel', num2str(cs'));
_images/showRockSAIGUP_10.png

Vertical permeability

delete(h)
plotCellData(G,log10(rock.perm(:,3)), args{:});
_images/showRockSAIGUP_11.png

Likewise, we show a histogram of the permeability

clf
Kx = rock.perm(:,1)./(milli*darcy);
Kz = rock.perm(:,3)./(milli*darcy);
hist(log10(Kx),100);
hold on
hist(log10(Kz(Kz>0)),100);
hold off
h=get(gca,'Children');
set(h(1),'EdgeColor',[0 0 0.4],'FaceColor','none')
set(h(2),'EdgeColor',[0.6 0 0],'FaceColor','none')
h = legend('Horizontal','Vertical'); set(h,'FontSize',16);
_images/showRockSAIGUP_12.png

and one histogram per rock type

figure('Position',[0 60 900 350]);
col = jet(60); col=col(1:10:end,:);
for i=1:6
   subplot(2,3,i);
   hist(log10(Kx(SN==i)), 100);
   h = findobj(gca,'Type','patch');
   set(h,'FaceColor',col(i,:),'EdgeColor',col(i,:))
   set(gca,'XLim',[-3 4],'XTick',-2:2:4,'XTickLabel',num2str(10.^(-2:2:4)'));
end
_images/showRockSAIGUP_13.png

Finally, show the multiplicator values

igure
Mx = grdecl.MULTX(G.cells.indexMap);
My = grdecl.MULTY(G.cells.indexMap);
Mz = grdecl.MULTZ(G.cells.indexMap);
plotGrid(G,'FaceColor','none','EdgeAlpha',0.1);
plotCellData(G,Mz,find(Mz<1),args{:});
axis tight off; set(gca,'DataAspect',[1 1 0.1]);
view(-65,55); zoom(1.4); camdolly(0,-0.2,0)
set(gca,'Clipping','off')
colorbar('horiz');
_images/showRockSAIGUP_14.png

Modle 2 of the 10th SPE Comparative Solution Project

Generated from showSPE10.m

The data set was originally posed as a benchmark for upscaling methods. The 3-D geological model consists of 60x220x85 grid cells, each of size 20ftx10ftx2ft. The model is a geostatistical realization from the Jurassic Upper Brent formations, in which one can find the giant North Sea fields of Statfjord, Gullfaks, Oseberg, and Snorre. In this specific model, the top 70 ft (35 layers) represent the shallow-marine Tarbert formation and the lower 100 ft (50 layers) the fluvial Ness formation. The data can be obtained from the SPE website: http://www.spe.org/web/csp/ The data set is used extensively in the literature, and for this reason, MRST has a special module that will download and provide easy access to the data set. In this example, we will inspect the SPE10 model in more detail.

mrstModule add spe10
doprint = false;

Load the model

The first time you access the model, it will be downloaded from the official websiet of the comparative solution project. Depending upon your internet connection, this may take quite a while, so please be patient.

rock = getSPE10rock();
p = rock.poro;
K = convertTo(rock.perm,milli*darcy);

show p

slice( reshape(p,60,220,85), [1 220], 60, [1 85]);
shading flat, axis equal off, set(gca,'zdir','reverse'), box on;
colorbar('horiz');
set(gcf,'renderer','painters');
if doprint
   print -depsc2 rock-spe10-poro.eps;                           %#ok<UNRCH>
   print -dpng rock-spe10-poro.png;
end
_images/showSPE10_01.png

show Kx

slice( reshape(log10(K(:,1)),60,220,85), [1 220], 1, [1 85]);
shading flat, axis equal off, set(gca,'zdir','reverse'), box on;
h=colorbar('horiz');
set(h,'XTickLabel',10.^get(h,'XTick'));
set(h,'YTick',mean(get(h,'YLim')),'YTickLabel','mD');
set(gcf,'renderer','painters');
if doprint
   print -depsc2 rock-spe10-Kx.eps;                             %#ok<UNRCH>
   print -dpng rock-spe10-Kx.png;
end
_images/showSPE10_02.png

show Ky

slice( reshape(log10(K(:,2)),60,220,85), [1 220], 60, [1 85]);
shading flat, axis equal off, set(gca,'zdir','reverse'), box on;
h=colorbar('horiz');
set(h,'XTickLabel',10.^get(h,'XTick'));
set(h,'YTick',mean(get(h,'YLim')),'YTickLabel','mD');
set(gcf,'renderer','painters');
if doprint
   print -depsc2 rock-spe10-Ky.eps;                             %#ok<UNRCH>
   print -dpng rock-spe10-Ky.png;
end
_images/showSPE10_03.png

show Kz

slice( reshape(log10(K(:,3)),60,220,85), [1 220], 60, [1 85]);
shading flat, axis equal off, set(gca,'zdir','reverse'), box on;
h=colorbar('horiz');
set(h,'XTickLabel',10.^get(h,'XTick'));
set(h,'YTick',mean(get(h,'YLim')),'YTickLabel','mD');
set(gcf,'renderer','painters');
if doprint
   print -depsc2 rock-spe10-Kz.eps;                             %#ok<UNRCH>
   print -dpng rock-spe10-Kz.png;
end
_images/showSPE10_04.png

show horizontal permeability distributions

hist(log10(K(220*60*35+1:end,1)),101);
hold on
hist(log10(K(1:220*60*35,1)),101)
hold off
h=get(gca,'Children');
set(h(1),'EdgeColor',[0 0 0.4],'FaceColor','none')
set(h(2),'EdgeColor',[0.7 0 0],'FaceColor','none')
h=legend('Ness','Tarbert'); set(h,'FontSize',16);
if doprint
   print -depsc2 rock-spe10-Kx-hist.eps;                        %#ok<UNRCH>
end
_images/showSPE10_05.png

show vertical permeability distributions

clf;
hist(log10(K(1:220*60*35,3)),101)
hold on
hist(log10(K(220*60*35+1:end,3)),101);
hold off
h=get(gca,'Children');
set(h(2),'EdgeColor',[0 0 0.4],'FaceColor','none')
set(h(1),'EdgeColor',[0.7 0 0],'FaceColor','none')
h=legend('Tarbert','Ness');set(h,'FontSize',16);
if doprint
   print -depsc2 rock-spe10-Kz-hist.eps;                        %#ok<UNRCH>
end
_images/showSPE10_06.png

show porosity distributions

T = p(1:220*60*35); pN = p(220*60*35+1:end);
pb = linspace(0,0.5,101);
N=histc(pN,pb); bar(pb,N,'histc');
hold on
N=histc(pT,pb); bar(pb,N,'histc');
hold off;
h=get(gca,'Children');
set(h(1),'EdgeColor',[0 0 0.4],'FaceColor','none')
set(h(2),'EdgeColor',[0.7 0 0],'FaceColor','none')
h=legend('Ness','Tarbert');set(h,'FontSize',16);
set(gca,'XLim',[0 0.5]);
if doprint
   print -depsc2 rock-spe10-poro-hist.eps;                      %#ok<UNRCH>
end
_images/showSPE10_07.png

Simple examples

Generated from simpleRockExamples.m

In this example we will go through a set of elementary examples that demonstrate how to generate petrophysical properties in MRST

Homogeneous, isotropic properties

G = cartGrid([1000 100]);
rock = makeRock(G, 200*milli*darcy,.2);
plotCellData(G, rock.poro,'EdgeColor','w');
_images/simpleRockExamples_01.png

Homogeneous, anisotropic properties

rock.perm = repmat( [100 100 10].*milli*darcy, [G.cells.num,1]);
clf
plotCellData(G, rock.perm./max(rock.perm(:)), 'edgeColor', 'w');
_images/simpleRockExamples_02.png

Carman-Kozeny

First in 2D

G = cartGrid([50 20]);
p = gaussianField(G.cartDims, [0.2 0.4], [11 3], 2.5);
K = p.^3.*(1e-5)^2./(0.81*72*(1-p).^2);
rock = makeRock(G, K(:), p(:));

clf
plotCellData(G,rock.poro);
colorbar('horiz'); axis equal tight;
_images/simpleRockExamples_03.png

Then in 3D

G = cartGrid([50 20 10]);
p = gaussianField(G.cartDims, [0.2 0.4], [5 3 1], 2.5);
K = p.^3.*(1e-5)^2./(0.81*72*(1-p).^2);
rock = makeRock(G, K(:), p(:));

clf
plotCellData(G,convertTo(rock.perm,milli*darcy));
colorbar('horiz'); axis equal tight; view(3);
_images/simpleRockExamples_04.png

Lognormal, layered in 3D

= processGRDECL(simpleGrdecl([50 30 10], 0.12));
K = logNormLayers(G.cartDims, [100 400 50 350], ...
   'indices', [1 2 5 7 11]);

clf
plotCellData(G,log10(K),'EdgeColor','k'); view(45,30);
axis tight off, set(gca,'DataAspect',[0.5 1 1])
h=colorbar('horiz'); ticks=25*2.^(0:5);
set(h,'XTick',log10(ticks),'XTickLabel',ticks);
_images/simpleRockExamples_05.png

Averaging of SPE10 layer

Generated from averagingExample1.m

To illustrate the implementation of various types of averaging techniques, we consider a single layer of the SPE10 model and compare the effective permeabilities obtained by arithmetic, harmonic, and harmonic-arithmetic averaging.

mrstModule add spe10 coarsegrid;

Setup model

fine = [40 60];
G    = cartGrid(fine, fine);
G    = computeGeometry(G);
rock = getSPE10rock(1:fine(1),1:fine(2),46);
rock.perm = rock.perm(:,1:2);

% The coarse grid
coarse = [10 15];
q = partitionUI(G,coarse);
cG = cartGrid(coarse,fine);

Compute the averages

The arithmetic and harmonic averages are straightforward. To compute the harmonic-arithmetic average, we temporary partition that coincides with the coarse grid in along the given axial direction and with the original fine grid in the other directions. Then we map the averaged values back onto the fine grid and perform a standard arithmetic averaging.

clear crock*
vol = G.cells.volumes;
for i=1:size(rock.perm,2)
   crock1.perm(:,i) = accumarray(q,vol.*rock.perm(:,i))./accumarray(q,vol);

   crock2.perm(:,i) = accumarray(q,vol)./accumarray(q,vol./rock.perm(:,i));

   dims = G.cartDims; dims(i)=coarse(i);
   qq = partitionUI(G, dims);
   K = accumarray(qq,vol)./accumarray(qq,vol./rock.perm(:,i));
   crock3.perm(:,i) = accumarray(q,K(qq).*vol)./accumarray(q,vol);
end

Plot the resulting effective permeabilities

To improve the visualization we enhance the colorbar by adding a small histogram of permeability values

px = log10([min(rock.perm(:,1)) max(rock.perm(:,1))]);
clf, set(gcf,'Position',[500 400 880 410]);
subplot(1,4,1);
plotCellData(G,log10(rock.perm(:,2)),'EdgeColor','none');
plotGrid(cG,'FaceColor','none','EdgeColor','k'); caxis(px); axis tight off
title('Fine scale');

subplot(1,4,2);
plotCellData(cG,log10(crock1.perm(:,2)),'EdgeColor','k');
caxis(px); axis tight off
title('Arithmetic');

subplot(1,4,3);
plotCellData(cG,log10(crock2.perm(:,2)),'EdgeColor','k');
caxis(px); axis tight off
title('Harmonic');

subplot(1,4,4);
plotCellData(cG,log10(crock3.perm(:,2)),'EdgeColor','k');
caxis(px); axis tight off
title('Harmonic-arithmetic');

perm{1} = log10(rock.perm(:,2));
perm{2} = log10(crock1.perm(:,2));
perm{3} = log10(crock2.perm(:,2));
perm{4} = log10(crock3.perm(:,2));

for i=1:4
   subplot(1,4,i);
   [h,ax] = colorbarHist(perm{i},[-18 -10],'South');
   set(h,'XTick',-16:2:-12,'XTickLabel',{'.1', '10', '1000'});
end
_images/averagingExample1_01.png

Assessment of upscaling accuracy on 8x8x8 grid

Generated from averagingExample2.m

We upscale a small 8x8x8 cube with three different permeability realizations and measure the ratio between the outflows computed on the upscaled and the original fine-scale model.

mrstModule add coarsegrid incomp spe10 upscaling

Setup case

G = computeGeometry(cartGrid([8 8 8]));
coarse = [1 1 1];
cG = computeGeometry(cartGrid(coarse,G.cartDims));
fluid     = initSingleFluid('mu' ,    1*centi*poise     , ...
                            'rho', 1014*kilogram/meter^3);
fl = {'East', 'North', 'Top'};
fr = {'West', 'South', 'Bottom'};

clc, clf, set(gcf,'Position',[500 400 800 300]);
disp('                      Arithmetic        Harmonic      Harm-arith');
for n=1:3
disp('-----------------------------------------------------------------');
   switch n
      case 1
         K = repmat([50 150 400 300 10 250 100 350], ...
            prod(G.cartDims(1:2)),1);
         rock.perm = K(:)*[1 1 1]*milli*darcy;
         disp('Layered:');
      case 2
         disp('Tarbert');
         rock = getSPE10rock(9:16,9:16,3:10);
      case 3
         disp('Upper Ness');
         rock = getSPE10rock(9:16,9:16,43:50);
   end

   subplot(1,3,n);
   plotCellData(G,log10(rock.perm(:,1)/(milli*darcy))); view(3); axis off; zoom(1.3);
   set(gca,'Clipping','off')
-----------------------------------------------------------------
Layered:
_images/averagingExample2_01.png
-----------------------------------------------------------------
Tarbert
_images/averagingExample2_02.png
-----------------------------------------------------------------
Upper Ness
_images/averagingExample2_03.png

Upscale values

q   = ones(G.cells.num,1);
   vol = G.cells.volumes;
   crock = cell(3,1);
   for i=1:size(rock.perm,2)
      % Arithmetic
      crock{1}.perm(:,i) = accumarray(q,vol.*rock.perm(:,i)) ./ ...
         accumarray(q,vol);

      % Harmonic
      crock{2}.perm(:,i) = accumarray(q,vol) ./ ...
         accumarray(q,vol./rock.perm(:,i));

      % Harmonic-arithmetic
      dims = G.cartDims; dims(i)=coarse(i);
      qq = partitionUI(G, dims);
      K = accumarray(qq,vol)./accumarray(qq,vol./rock.perm(:,i));
      crock{3}.perm(:,i) = accumarray(q,K(qq).*vol)./accumarray(q,vol);
   end

Compare upscaling

for j=1:numel(fl)
      % Fine-scale problem
      bc    = pside([], G, fl{j}, barsa);
      faces = bc.face;
      bc    = pside(bc, G, fr{j}, 0);
      hT    = computeTrans(G, rock);
      xr    = incompTPFA(initResSol(G,0), G, hT, fluid, 'bc', bc);
      flux  = sum(xr.flux(faces));

      fprintf('  %5s -> %6s: ', fl{j}, fr{j});
      for i=1:3
         cbc    = pside([], cG, fl{j}, barsa);
         cfaces = cbc.face;
         cbc    = pside(cbc, cG, fr{j}, 0);
         chT    = computeTrans(cG, crock{i});
         x      = incompTPFA(initResSol(cG,0), cG, chT, fluid, 'bc', cbc);
         fprintf('\t%f', x.flux(cfaces) / flux);
      end
      fprintf('\n');
   end
East ->   West:       1.000000        0.266151        1.000000
  North ->  South:    1.000000        0.266151        1.000000
    Top -> Bottom:    3.757266        1.000000        1.000000
East ->   West:       1.659138        0.024628        0.852037
  North ->  South:    1.633738        0.024250        0.852491
    Top -> Bottom:    47428.068405    0.323872        0.858847
East ->   West:       3.406023        0.000850        0.830259
  North ->  South:    1.953673        0.000487        0.712832
    Top -> Bottom:    6776.849373     0.001990        0.339976
end
disp('-----------------------------------------------------------------');
cax = caxis;
for i=1:3, subplot(1,3,i), caxis(cax); end
h = colorbar;
set(h,'Position',[0.94 0.1100 0.02 0.8150], ...
   'XTick', .5, 'XTickLabel','[mD]', ...
   'YTickLabel',num2str(10.^(-2:3)'));
Arithmetic        Harmonic      Harm-arith
-----------------------------------------------------------------

Figures for conceptual illustration of upscaling

Generated from illustrateUpscaling.m

We generate a grid that consists of two facies and then perform an upscaling so that 5x5x5 fine cells are replaced by a coarse block. We show the following four plots: fine grid with one coarse cell, porosity inside this coarse cell, the porosity inside the coarse block, and the coarse model

mrstModule add coarsegrid upscaling

Fine model

G  = computeGeometry(cartGrid([40 20 15]));
K1 = gaussianField(G.cartDims, [300 2000]);
p1 = K1(:)*1e-4 + .2;
K2 = gaussianField(G.cartDims, [10 900]);
p2 = K2(:)*1e-4 + .2;

rad1 = G.cells.centroids(:,1).^2 + .5*G.cells.centroids(:,2).^2 ...
   + (G.cells.centroids(:,3)-2).^2;
rad2 = .5*(G.cells.centroids(:,1)-40).^2 + 2*G.cells.centroids(:,2).^2 ...
   + 2*(G.cells.centroids(:,3)-2).^2;

ind = ((rad1>600) & (rad1<1500)) | ((rad2>700) & (rad2<1400));
rock.poro = p2(:);
rock.poro(ind) = p1(ind);

% Plot fine model
figure('Position', [680 280 560 540]);
plotCellData(G,rock.poro,'EdgeColor','k','edgealpha',.1);
view(3); axis tight off
[hc,hh]=colorbarHist(rock.poro,[.2 .4],'South', 80);
pos=get(hc,'Position'); set(hc,'Position',pos - [.02 0 .2 -.01],'FontSize',12);
pos=get(hh,'Position'); set(hh,'Position',pos - [.02 0.02 .2 -.01]);
set(gca,'Position',[.13 .2 .775 .775])

% Generate and plot coarse grid block
p = partitionCartGrid(G.cartDims, G.cartDims/5);
CG = generateCoarseGrid(G, p);
crock.poro = accumarray(p, rock.poro)./accumarray(p,1);
plotGrid(CG,1,'FaceColor','none','EdgeColor','k','LineWidth',1.5);
set(gcf,'PaperPositionMode','auto');
% print -dpng illUpscaling-fine.png;
_images/illustrateUpscaling_01.png

Plot cells inside a single block

figure;
plotCellData(G,rock.poro, p==1, 'EdgeColor','k');
view(3); axis equal off; caxis([0.2 0.4]);
% print -dpng illUpscaling-cells.png;
_images/illustrateUpscaling_02.png

Plot the corresponding coarse block

figure;
cG = computeGeometry(cartGrid(G.cartDims/5,G.cartDims));
plotCellData(cG,crock.poro,1,'EdgeColor','k','LineWidth',1.5);
view(3); axis equal off; caxis([0.2 0.4]);
% print -dpng illUpscaling-block.png;
_images/illustrateUpscaling_03.png

Coarse model

figure('Position', [680 280 560 540]);
plotCellData(cG,crock.poro,'EdgeColor','k','edgealpha',.1);
view(3); axis tight off

% Generate and plot coarse grid block
plotGrid(CG,1,'FaceColor','none','EdgeColor','k','LineWidth',1.5);

[hc,hh]=colorbarHist(crock.poro,[.2 .4],'South', 40);
pos=get(hc,'Position'); set(hc,'Position',pos + [.2    0 -.2 .01],'FontSize',12);
pos=get(hh,'Position'); set(hh,'Position',pos + [.2 -.025 -.2 .01]);
set(gca,'Position',[.13 .2 .775 .775])
set(gcf,'PaperPositionMode','auto');
% print -dpng illUpscaling-coarse.png;
_images/illustrateUpscaling_04.png

Upscale a problem with a diagonal trend

Generated from permeabilityExample1.m

In this example we will upscale a problem with diagonal trend and compare the results obtained with a flow-based method with pressure-drop and periodic boundary conditions

mrstModule add incomp upscaling;

Set up model

[Lx,Ly] = deal(10);
[nx,ny] = deal(20);
G = computeGeometry(cartGrid([nx ny], [Lx Ly]));
permX = 30*milli*darcy;
[i, j] = gridLogicalIndices(G);
poro = 0.3;
rock.perm = repmat(30*milli*darcy, [G.cells.num, 1]);
rock.poro = repmat(.3, [G.cells.num, 1]);
rock.perm( sin(2*sum(G.cells.centroids(:,:),2)*pi/Lx)>0 ) = 10*milli*darcy;
hT    = computeTrans(G, rock);
fluid = initSingleFluid('mu' ,1, 'rho', 1);

clf, plotCellData(G,rock.perm/(milli*darcy),'EdgeColor','none'); colorbar
_images/permeabilityExample1_01.png

Structures with boundary conditions

d = G.griddim;
[bcl,bcr, Dp]=deal(cell(d,1));
bcsides = {'XMin', 'XMax'; 'YMin', 'YMax'; 'ZMin', 'ZMax'};
for j = 1:d;
   bcl{j} = pside([], G, bcsides{j, 1}, 0);
   bcr{j} = pside([], G, bcsides{j, 2}, 0);
   Dp{j}  = 0;
end
Dp{1} = 4*barsa;
L  = max(G.faces.centroids)-min(G.faces.centroids);

Pressure-drop boundary conditions

[v,dp] = deal(zeros(d, 1));
for i=1:d
   bc = addBC([], bcl{i}.face, 'pressure', Dp{1});
   bc = addBC(bc, bcr{i}.face, 'pressure', Dp{2});

   xr = initResSol(G, 100*barsa, 1);
   xr = incompTPFA(xr, G, hT, fluid, 'bc', bc);

   v(i)  = sum(xr.flux(bcr{i}.face)) / ...
           sum(G.faces.areas(bcr{i}.face));
   dp(i) = Dp{1}/L(i);
end
K = convertTo(v./dp, milli*darcy)                                          %#ok<NASGU,NOPTS>
K =

   17.2295
   17.2295

Periodic boundary conditions

[Gp, bcp] = makePeriodicGridMulti3d(G, bcl, bcr, Dp);
ofaces = cell(d,1);
for j=1:d, ofaces{j} = bcp.face(bcp.tags==j); end
v  = nan(d);
dp = Dp{1}*eye(d);
nbcp = bcp;
for i=1:d
   for j=1:d, nbcp.value(bcp.tags==j) = dp(j,i); end

   xr = initResSol(Gp, 100*barsa, 1);
   xr = incompTPFA(xr, Gp, hT, fluid, 'bcp', nbcp);

   for j=1:d
      v(j,i) = sum(xr.flux(ofaces{j})) / ...
*bcp.sign(bcp.tags==j)) / ...
               sum(Gp.faces.areas(ofaces{j}));
   end
end
dp = bsxfun(@rdivide, dp, L);
K = convertTo(v/dp, milli*darcy)                                          %#ok<NOPTS>
[V,D] = eig(K)                                                             %#ok<NOPTS>
Warning: Face pressures are not well defined for periodic boundary faces
Warning: Face pressures are not well defined for periodic boundary faces

K =

   17.2500   -2.2500
   -2.2500   17.2500

...

Assessment of upscaling accuracy on 8x8x8 grid

Generated from permeabilityExample2.m

We upscale a small 8x8x8 cube with three different permeability realizations and measure the ratio between the outflows computed on the upscaled and the original fine-scale model.

mrstModule add coarsegrid incomp spe10 upscaling

Set up grid

warning('off','mrst:periodic_bc');
G = computeGeometry(cartGrid([8 8 8]));
coarse = [1 1 1];
fluid = initSingleFluid('mu', 1*centi*poise, 'rho', 1014*kilogram/meter^3);
fl = {'East', 'North', 'Top'};
fr = {'West', 'South', 'Bottom'};

Structures with boundary conditions

d = G.griddim;
[bcl,bcr,Dp]=deal(cell(d,1));
bcsides = {'XMin', 'XMax'; 'YMin', 'YMax'; 'ZMin', 'ZMax'};
for j = 1:d
   bcl{j} = pside([], G, bcsides{j, 1}, 0);
   bcr{j} = pside([], G, bcsides{j, 2}, 0);
   Dp{j}  = 0;
end
Dp{1}     = 4*barsa;
L         = max(G.faces.centroids)-min(G.faces.centroids);
[Gp, bcp] = makePeriodicGridMulti3d(G, bcl, bcr, Dp);
ofaces    = cell(d,1);
for j=1:d, ofaces{j} = bcp.face(bcp.tags==j); end

clc;
figure('Position',[440 460 800 300]);
disp('                      Harm/arith      Axial drop        Periodic');
for n=1:3
disp('--------------------------------------------------------------------');
   switch n
      case 1
         rock.perm = repmat(300*milli*darcy, [G.cells.num, 1]);
         rock.perm( sin(4*sum(G.cells.centroids(:,[1 3]),2)*pi/norm(L([1 3])))>0 ) = 50*milli*darcy;
         disp('Layered:');
      case 2
         disp('Tarbert');
         rock = getSPE10rock(49:56,9:16,1:8);
      case 3
         disp('Upper Ness');
         rock = getSPE10rock(9:16,9:16,43:50);
   end
   hT = computeTrans(G, rock);

   subplot(1,3,n);
   plotCellData(G,log10(rock.perm(:,1)/(milli*darcy))); view(3); axis off; zoom(1.3);
   set(gca,'Clipping','off')
--------------------------------------------------------------------
Layered:
_images/permeabilityExample2_01.png
--------------------------------------------------------------------
Tarbert
_images/permeabilityExample2_02.png
--------------------------------------------------------------------
Upper Ness
_images/permeabilityExample2_03.png

Upscale values

crock = cell(3,1);

   % Harmonic-arithmetic
   q   = ones(G.cells.num,1);
   vol = G.cells.volumes;
   for i=1:size(rock.perm,2)
      dims = G.cartDims; dims(i)=coarse(i);
      qq = partitionUI(G, dims);
      K = accumarray(qq,vol)./accumarray(qq,vol./rock.perm(:,i));
      crock{1}.perm(:,i) = accumarray(q,K(qq).*vol)./accumarray(q,vol);
   end

   % Pressure drop
   [v,dp] = deal(zeros(d, 1));
   for i=1:d
      bc = addBC([], bcl{i}.face, 'pressure', Dp{1});
      bc = addBC(bc, bcr{i}.face, 'pressure', Dp{2});

      xr = initResSol(G, 100*barsa, 1);
      xr = incompTPFA(xr, G, hT, fluid, 'bc', bc);

      v(i)  = sum(xr.flux(bcr{i}.face)) / ...
          sum(G.faces.areas(bcr{i}.face));
       dp(i) = Dp{1}/L(i);
   end
   crock{2}.perm = fluid.properties()*(v./dp)';

   % Periodic
   v  = nan(d);
   dp = Dp{1}*eye(d);
   nbcp = bcp;
   for i=1:d
      for j=1:d, nbcp.value(bcp.tags==j) = dp(j,i); end
      xr = initResSol(Gp, 100*barsa, 1);
      xr = incompTPFA(xr, Gp, hT, fluid, 'bcp', nbcp);
      for j=1:d
         v(j,i) = sum(xr.flux(ofaces{j})) / ...
            sum(Gp.faces.areas(ofaces{j}));
      end
   end
   dp = bsxfun(@rdivide, dp, L);
   K = fluid.properties()*v/dp;
   crock{3}.perm = abs(K([1 2 3 5 6 9]));

Compare upscaling

for j=1:numel(fl)
      % Fine-scale problem
      bc    = pside([], G, fl{j}, barsa);
      faces = bc.face;
      if j<3
         bc    = pside(bc, G, fr{j}, 0);
      else
         cf = gridCellFaces(G,1);
         cf = cf(any(G.faces.neighbors(cf,:)==0,2));
         bc = addBC([], cf, 'pressure', barsa);

         cf = gridCellFaces(G,G.cells.num);
         cf = cf(any(G.faces.neighbors(cf,:)==0,2));
         bc = addBC(bc, cf, 'pressure', 0);
      end
      x0    = initResSol(G,0);
      xr    = incompTPFA(x0, G, hT, fluid, 'bc', bc);
      flux  = sum(xr.flux(faces));

      fprintf('  %5s -> %6s: ', fl{j}, fr{j});
      for i=1:3
         nrock.perm = crock{i}.perm(ones(G.cells.num,1),:);
         chT    = computeTrans(G, nrock);
         x      = incompTPFA(x0, G, chT, fluid, 'bc', bc);
         fprintf('\t%f', sum(x.flux(faces)) / flux);
      end
      fprintf('\n');
   end
East ->   West:       0.784068        1.000000        1.025937
  North ->  South:    0.499741        1.000000        1.000000
    Top -> Bottom:    1.042730        1.305649        1.342283
East ->   West:       0.867555        1.000000        0.561113
  North ->  South:    0.892983        1.000000        0.538797
    Top -> Bottom:    0.000034        0.000270        39.119231
East ->   West:       0.830259        1.000000        0.401965
  North ->  South:    0.712832        1.000000        0.280813
    Top -> Bottom:    0.095459        0.623773        2183.266271
end
disp('--------------------------------------------------------------------');
cax = caxis;
for i=1:3, subplot(1,3,i), caxis(cax); end
h = colorbar;
set(h,'Position',[0.94 0.1100 0.02 0.8150], ...
   'XTick', .5, 'XTickLabel','[mD]', ...
   'YTickLabel',num2str(10.^(-2:3)'));
Harm/arith      Axial drop        Periodic
--------------------------------------------------------------------

Upscale a problem with a diagonal trend

Generated from transmissibilityExample1.m

In this example we will upscale a problem with diagonal trend and compute the transmissibility associated with the coarse interface between two blocks in the x-direction

mrstModule add coarsegrid incomp;

Set up model

[Lx,Ly] = deal(200,100);
[nx,ny] = deal(30,15);
Dp      = [1 0]*barsa;
G = computeGeometry(cartGrid([nx ny], [Lx Ly]));
rock = makeRock(G, 30*milli*darcy, 0.3);
rock.perm( sin(4*sum(G.cells.centroids(:,:),2)*pi/Lx)>0 ) = 10*milli*darcy;
rock.perm = rock.perm .* (1+.2*rand(size(rock.perm)));
hT    = computeTrans(G, rock);
fluid = initSingleFluid('mu' ,1, 'rho', 1);

clf, plotCellData(G,rock.perm/(milli*darcy),'EdgeColor','none'); colorbar
_images/transmissibilityExample1_01.png

Solve fine-scale problem

bc   = pside([], G, 'XMin', Dp(1));
bc   = pside(bc, G, 'XMax', Dp(2));
xr   = initResSol(G, 100*barsa, 1);
xr   = incompTPFA(xr, G, hT, fluid, 'bc', bc);

Loop over various partititions

pow=4:-1:-2;
x = G.cells.centroids(:,1);
y = G.cells.centroids(:,2);
sp = [1 2 3 4 5 6 11];
clf, set(gcf,'Position',[360 500 930 300]);
for avg=[false, true]
   T = zeros(size(pow));
   for n=1:numel(pow);

      % Find faces on coarse interface
      q =  (y> 50 + 2^pow(n)*(100-x))+1; q = q(:);
      CG = generateCoarseGrid(G, q);
      CG = coarsenGeometry(CG);
      i = find(~any(CG.faces.neighbors==0,2));
      faces = CG.faces.fconn(CG.faces.connPos(i):CG.faces.connPos(i+1)-1);
      sgn   = 2*(CG.faces.neighbors(i, 1) == 1) - 1;

      % Upscale transmissibility
      flux = sgn*sum(xr.flux(faces));
      mu   = fluid.properties();
      if avg
         P = accumarray(q,xr.pressure)./accumarray(q,1);
      else
         cells = findEnclosingCell(G,CG.cells.centroids);
         P = xr.pressure(cells);
      end
      T(n) = mu*flux/(P(1) - P(2));

      if avg, continue; end

      % Plot grid
      subplot(3,5,sp(n));
      plotCellData(G,rock.perm(:,1),'EdgeColor','none');
      plotGrid(CG,'FaceColor','none','LineWidth',1);
      plotGrid(G, cells, 'FaceColor','k', 'EdgeColor','none');
      axis equal off; axis([-1 201 -1 101]); title(num2str(n));
   end
   flag = true(15,1); flag(sp) = false;
   subplot(3,5,find(flag)); hold on
   if avg
      plot(1:numel(pow),T,'--sb','LineWidth',1.5, 'MarkerSize',8, ...
         'MarkerEdgeColor','k','MarkerFaceColor','r');
   else
      plot(1:numel(pow),T,'--or','LineWidth',1.5, 'MarkerSize',8, ...
         'MarkerEdgeColor','k','MarkerFaceColor','b');
   end
   hold off
end
%axis([.5 7.5 [1.6 2.8]*1e-14]); legend('Centroid','Average',2);
colormap(.5*parula+.5*ones(size(parula)));
_images/transmissibilityExample1_02.png

Illustrate use of flow diagnostics to verify upscaling

Generated from upscalingExample1.m

In this example we explain how to use match in cumulative well-allocation factors to verify the quality of an upscaling. In each well completion, the well-allocation factor is the percentage of the flux in/out of the completion that can be attributed to a pair of injection and production wells. The script can run two types of upscaling, if use_trans=false, we use a simple flow-based method to upscale permeability, while if use_trans=true, we use a more comprehensive method to upscale the transmissibilities and the well indices.

mrstModule add agglom upscaling coarsegrid diagnostics incomp;

use_trans = false;

Make model

We make a small model that consists of two different facies with contrasting petrophysical properties. An injector and a producer are placed diagonally opposite of each other and completed mainly in the high-permeable part of the model.

G  = computeGeometry(cartGrid([40 20 15]));
K1 = gaussianField(G.cartDims, [200 2000]);
p1 = K1(:)*1e-4 + .2;
K1 = K1(:)*milli*darcy;
K2 = gaussianField(G.cartDims, [10 500]);
p2 = K2(:)*1e-4 + .2;
K2 = K2(:)*milli*darcy;

rad1 = G.cells.centroids(:,1).^2 + .5*G.cells.centroids(:,2).^2 ...
   + (G.cells.centroids(:,3)-2).^2;
rad2 = .5*(G.cells.centroids(:,1)-40).^2 + 2*G.cells.centroids(:,2).^2 ...
   + 2*(G.cells.centroids(:,3)-2).^2;

ind = ((rad1>600) & (rad1<1500)) | ((rad2>700) & (rad2<1400));
rock.perm = K2(:);
rock.perm(ind) = K1(ind);
rock.perm = bsxfun(@times, rock.perm, [1 1 1]);
rock.poro = p2(:);
rock.poro(ind) = p1(ind);
pv = poreVolume(G, rock);

W = verticalWell([], G, rock, 4, 17, 4:15, 'Comp_i', [1 0], ...
   'Type', 'rate', 'Val', 0.2*sum(pv)/year, 'Name', 'I');
W = verticalWell(W,  G, rock, 35, 3, 1:10, 'Comp_i', [0 1], ...
   'Type', 'rate', 'Val', -0.2*sum(pv)/year, 'Name', 'P');

figure(1); clf,
set(gcf,'Position', [860 450 840 400],'PaperPositionMode','auto');
subplot(1,2,1);
plotCellData(G,rock.poro,'EdgeAlpha',.5); view(3);
plotWell(G,W,'Color','k'); axis off tight
pax = [min(rock.poro) max(rock.poro)];
[hc,hh] = colorbarHist(rock.poro, pax,'West');
pos = get(hh,'Position'); set(hh,'Position', pos - [.01 0 0 0]);
pos = get(hc,'Position'); set(hc,'Position', pos - [.03 0 0 0]);
set(gca,'Position',[.12 .075 .315 .9])
_images/upscalingExample1_01.png

Compute flow on the fine-scale model

Transmissibility

hT = computeTrans(G, rock);
trans = 1 ./ accumarray(G.cells.faces(:,1), 1 ./ hT, [G.faces.num, 1]);

% Fluid model
dfluid = initSimpleFluid('mu' , [   1,  10]*centi*poise     , ...
                         'rho', [1014, 859]*kilogram/meter^3, ...
                         'n', [2 2]);
gravity reset off;

% Pressure solution
xd  = initState(G, W, 100*barsa);
nw = numel(W);
xd  = incompTPFA(xd, G, trans, dfluid, 'wells', W, 'use_trans', true);

Flow diagnostics on fine-scale model

To better reveal the communication in the reservoir, we subdivide each well into two segments, an upper and a lower segments, so that we altogether will have four well-pairs whose well-allocation factors can be used to verify the quality of the upscaling

nsegment=2;
[xd,Wdf] = expandWellCompletions(xd, W,[(1:nw)' repmat(nsegment,nw,1)]);
Df  = computeTOFandTracer(xd, G, rock, 'wells', Wdf);
WPf = computeWellPairs(xd, G, rock, Wdf, Df);

Coarse-scale solution

We make a 5x5x15 coarse grid, in which we have chosen to keep the layering of the fine-scale model to simplify the comparison of well-allocation factors. Transmissibilities and well indices are upscaled using two slightly different methods: for the transmissibilities we use global generic boundary conditions and on each coarse face use the solution that has the largest flux orthogonal to the face to compute the upscaled transmissibility. For the wells, we use specific well conditions and use least squares for the flux.

p  = partitionCartGrid(G.cartDims, [5 5 15]);
CG = coarsenGeometry(generateCoarseGrid(G, p));
crock = convertRock2Coarse(G, CG, rock);
if use_trans  %#ok<*UNRCH>
   [~,CTrans] = upscaleTrans(CG, hT, 'match_method', 'max_flux', ...
                          'bc_method', 'bc_simple');
   [~,~,WC]   = upscaleTrans(CG, hT, 'match_method', 'lsq_flux', ...
                          'bc_method', 'wells_simple', 'wells', W);
else
   crock.perm = upscalePerm(G, CG, rock);
end
figure(1); subplot(1,2,2); cla
plotCellData(CG,crock.poro,'EdgeColor','none');
plotFaces(CG, boundaryFaces(CG),...
   'EdgeColor', [0.4 0.4 0.4],'EdgeAlpha',.5, 'FaceColor', 'none'); view(3);
plotWell(G,W,'Color','k'); axis off tight
[hc,hh]=colorbarHist(crock.poro, pax,'East');
pos = get(hh,'Position'); set(hh,'Position', pos + [.01 0 0 0]);
pos = get(hc,'Position'); set(hc,'Position', pos + [.02 0 0 0]);
set(gca,'Position',[.56 .075 .315 .9])
_images/upscalingExample1_02.png

If permeability upscaling: to assess the effect of permeability upscaling only, we project the upscaled permeability back to the fine grid

if ~use_trans
   crock.poro = crock.poro(p);
   crock.perm = crock.perm(p,:);
   CG = G;
   WC = W;
   CTrans = computeTrans(CG, crock);
   CTrans = 1 ./ accumarray(CG.cells.faces(:,1), 1 ./ CTrans, [CG.faces.num, 1]);
   p = (1:G.cells.num)';
end
xd    = initState(CG, WC, accumarray(p,100*barsa)./accumarray(p,1));
xd    = incompTPFA(xd, CG, CTrans, dfluid, 'wells', WC, 'use_trans', true);

Flow diagnostics on the upscaled model

Having obtained fluxes on the coarse model, we can expand the wells into two segments that exactly match the subdivision of in the fine-scale model and compute flow diagnostics.

nw    = numel(WC);
[xdc,Wdc] = expandCoarseWellCompletions(xd, WC, Wdf, p);
Dc    = computeTOFandTracer(xdc, CG, crock, 'wells', Wdc);
WPc   = computeWellPairs(xdc, CG, crock, Wdc, Dc);
nit   = numel(Df.inj);
npt   = numel(Df.prod);
nseg  = nit + npt;

Display bar charts of flow-allocation factors

For each well segment, we plot bar chart of the cumulative flux in/out of the completions that make up the segment, from bottom to top. In the plots, each well segment is assigned a unique color and each bar is subdivided into the fraction of the total in/outflux that blongs to the different well-pairs the segment is part of. The plots show the allocation factors for the upper half of the injector (I:1, upper-left plot), the lower part of the injector (I:2, upper-right plot), the upper part of the producer (P:1, lower-left plot), and the lower part of the producer (P:2, lower-right plot). Looking at the bar chart for I:1, we see that the majority of the flux from this injector is colored yellow and hence goes to P:1, which corresponds to the upper part of the producer.

figure(2); set(gcf,'Position', [860 450 840 400]); clf
plotWellAllocationComparison(Dc, WPc, Df, WPf);
dy = [-.05 -.05 -.025 -.025];
dx = [-.5 0 -.5 0 0];
for i=1:4
   subplot(2,2,i);
   pos = get(gca,'Position');
   pos = pos + [-.05 dy(i) .075 .075];
   set(gca,'Position',pos,'XTick',[],'YTick',[]);
end
cmap = jet(nseg); cmap = 0.6*cmap + .4*ones(size(cmap)); colormap(cmap);
_images/upscalingExample1_03.png

Display the partition

Last we show the cells that belong to the four different well segments and show the corresponding injector/producer partitions. We can now compare the partitions with the well-allocation factors in Figure 2. Let us take the allocation for I:2 as an example. In the upper-right plot of Figure 2 we see that almost all the flux from this injector goes to P:2. Looking at the cyan region in the left plot of Figure 1 confirms this: this region is hardly in contact with the yellow segment of the producer. The blue region, on the other hand, contains both the upper segment of the producer (P:1, yellow color) and parts of the lower segment (P:2, red color). Likewise, the red volume in the right-hand plot of Figure 3 covers both the lower segment of the injector (I:2, cyan) and parts of the upper segment (I:1, blue). In the bars of P:2 to the lower-right in Figure 2, we therefore see that relatively large portion of the bars are in blue color.

oG  = computeGeometry(cartGrid([1 1 1],[40 20 15]));
figure(3); clf
set(gcf,'Position', [860 450 840 310],'PaperPositionMode','auto');
subplot(1,2,1);
for i=1:nit
   plotCellData(G,Df.ipart,Df.ipart==i,'FaceAlpha',.3,'EdgeAlpha',.2);
end
plotGrid(oG,'FaceColor','none');
for i=1:nseg,
   plotGrid(G,Wdf(i).cells,'FaceColor',cmap(i,:));
end
plotWell(G,W,'Color','k');
view(-40,10); axis tight off; caxis([1 nit+npt]);

subplot(1,2,2);
for i=1:npt
   plotCellData(G,Df.ppart+nit,Df.ppart==i,'FaceAlpha',.4,'EdgeAlpha',.2);
end
plotGrid(oG,'FaceColor','none');
for i=1:nseg,
   plotGrid(G,Wdf(i).cells,'FaceColor',cmap(i,:));
end
plotWell(G,W,'Color','k');
view(-40,10); axis tight off; caxis([1 nit+npt]);
colormap(cmap);
_images/upscalingExample1_04.png

Upscaling workflow

Generated from upscalingExample3.m

This example aims to show the complete workflow for creating, running and analyzing a simulation model. Unlike the other examples, we will create all features of the model manually to get a self-contained script without any input files required. The model we setup is a slightly compressible two-phase oil/water model with multiple wells. The reservoir has a layered strategraphy consisting of three layered sections, four layers with different permeability, and three major faults. Note that this example features a simple conceptual model designed to show the workflow rather than a problem representing a realistic scenario in terms of well locations and fluid physics.

mrstModule add ad-core ad-blackoil ad-props diagnostics mrst-gui
close all;

Horizons for internal and external geology

We begin by building the horizons that define the top and bottom structure of the sector, as well as two internal erosions.

% Define areal mesh
[xmax,ymax, n]  = deal(1000*meter, 1000*meter, 30);
[x, y] = meshgrid(linspace(0,xmax,n+1), linspace(0,ymax,n+1));
[x, y] = deal(x',y');

% Basic dome structure
dome = 1-exp(sqrt((x - xmax/2).^2 + (y - ymax/2).^2)*1e-3);

% Periodic perturbation
[xn,yn] = deal(pi*x/xmax,pi*y/ymax);
perturb = sin(5*xn) + .5*sin(4*xn+6*yn) + cos(.25*xn*yn./pi^2) + cos(3*yn);
perturb = perturb/3.5;

% Random small-scale perturbation
rng(0);
[h, hr] = deal(8,1);
zt = 50 + h*perturb + rand(size(x))*hr - 20*dome;
zb = zt + 30;
zmb = min(zb + 4 + 0.01*x - 0.020*y + hr*rand(size(x)), zb);
zmt = max(zb -15 + 0.01*x - 0.025*y + hr*rand(size(x)), zt);
horizons = {struct('x', x, 'y', y, 'z', zt), ...
    struct('x', x, 'y', y, 'z', zmt), ...
    struct('x', x, 'y', y, 'z', zmb), ...
    struct('x', x, 'y', y, 'z', zb)};

surf(x,y,zt-.2, 'EdgeC','r','FaceC',[.8 .8 .8]),  hold on
mesh(x,y,zmt-.2,'EdgeC','g','FaceC',[.7 .7 .7]),
mesh(x,y,zmb-.2,'EdgeC','g','FaceC',[.6 .6 .6]),
mesh(x,y,zb-.2, 'EdgeC','b','FaceC',[.5 .5 .5]); hold off
set(gca,'ZDir','reverse')
view(-50,10); axis off
_images/upscalingExample3_01.png

Interpolate to build unfaulted corner-point grid

dims = [40, 40]; layers = [3 6 3];
grdecl = convertHorizonsToGrid(horizons, 'dims', dims, 'layers', layers);
G = processGRDECL(grdecl);
[~,~,k] = gridLogicalIndices(G);

figure, plotCellData(G,k,'EdgeAlpha',.2); view(3);
colormap(.5*jet + .5*ones(size(jet)));
view(-50,30); axis off
_images/upscalingExample3_02.png

Insert faults

[X,Y,Z]  = buildCornerPtNodes(grdecl);

i=47:80; Z(i,:,:) = Z(i,:,:) + .022*min(0,Y(i,:,:)-550);
j= 1:30; Z(:,j,:) = Z(:,j,:) + .021*min(0,X(:,j,:)-400);
j=57:80; Z(:,j,:) = Z(:,j,:) + .023*min(0,X(:,j,:)-750);
grdecl.ZCORN = Z(:);

G = processGRDECL(grdecl);
G = computeGeometry(G);
[~,~,k] = gridLogicalIndices(G);

figure, plotCellData(G,k,'EdgeAlpha',.2); view(3);
plotFaces(G,find(G.faces.tag>0),'EdgeColor','r','FaceColor',[.8 .8 .8]);
colormap(.5*jet + .5*ones(size(jet)));
view(-50,30); axis tight off
_images/upscalingExample3_03.png

Petrophysics

Set up permeability based on K-indices and introduce anisotropy by setting K_z = .1*K_x

rng(357371);
[K,L] = logNormLayers(G.cartDims, [100 400 10 50]*milli*darcy);
K = K(G.cells.indexMap);
perm = [K, K, 0.1*K];
rock = makeRock(G, perm, 0.3);

% Plot horizontal permeability
figure
K = convertTo(K,milli*darcy);
plotCellData(G, log10(K),'EdgeAlpha',.1)
colorbarHist(K,[1 1500],'South',100,true);
view(-50, 50), axis tight off
_images/upscalingExample3_04.png

Define wells

Hydrocarbon is recovered from producers, operating at fixed bottom-hole pressure and perforated throughout all layers of the model. The producers are supported by a single water injector set to inject one pore volume over 10 years (the total simulation length).

% Producers
simTime = 10*year;
pv      = poreVolume(G, rock);
injRate = 1*sum(pv)/simTime;
offset  = 10;
W = verticalWell([], G, rock, offset, offset, [],...
                'Name', 'P1', 'comp_i', [1 0], ...
                'Val', 250*barsa, 'Type', 'bhp', 'refDepth', 50);
W = verticalWell(W, G, rock,  offset, floor(G.cartDims(1)/2)+3, [],...
                'Name', 'P2', 'comp_i', [1 0], ...
                'Val', 250*barsa, 'Type', 'bhp', 'refDepth', 50);
W = verticalWell(W, G, rock, offset, G.cartDims(2) - offset/2, [], ...
                'Name', 'P3', 'comp_i', [1 0], ...
                'Val', 250*barsa, 'Type', 'bhp', 'refDepth', 50);

% Injectors
W = verticalWell(W, G, rock, G.cartDims(1)-5, offset, [],...
                'Name', 'I1', 'comp_i', [1 0], ...
                'Val', injRate, 'Type', 'rate', 'refDepth', 50);

% Plot the wells
plotWell(G, W,'color','k')
axis tight
_images/upscalingExample3_05.png

Define fluid behavior and instantiate model

We set up a two-phase oil-water simulation model based on automatic differentiation. The resulting object is a special case of a general three-phase model and to instantiate it, we start by constructing a three-phase fluid structure with properties given for oil, water, and gas. (The gas properties will be neglected once we construct the two-phase object). Water is assumed to be incompressible, whereas oil has constant compressibility, giving an expansion factor of the form,

. To define this relation, we set the ‘bo’ field of the fluid structure to be an anonymous function that calls the builtin ‘exp’ function with appropriate arguments. Since the fluid model is a struct containing function handles, it is simple to modify the fluid to use alternate functions. We then pass the fundamental structures (grid, rock and fluid) onto the two-phase oil/water model constructor.
% Three-phase template model
fluid = initSimpleADIFluid('mu',    [1, 5, 0]*centi*poise, ...
                           'rho',   [1000, 700, 0]*kilogram/meter^3, ...
                           'n',     [2, 2, 0]);

% Constant oil compressibility
c        = 0.001/barsa;
p_ref    = 300*barsa;
fluid.bO = @(p, varargin) exp((p - p_ref)*c);

% Construct reservoir model
gravity reset on
model = TwoPhaseOilWaterModel(G, rock, fluid);

Define initial state

Once we have a model, we need to set up an initial state. We set up a very simple initial state; we let the bottom part of the reservoir be completely water filled, and the top completely oil filled. MRST uses water, oil, gas ordering internally, so in this case we have water in the first column and oil in the second for the saturations.

depthOW = 85*meter;
depthD  = 10*meter;
region = getInitializationRegionsBlackOil(model, depthOW, ...
            'datum_depth', depthD, 'datum_pressure', p_ref);
state0 = initStateBlackOilAD(model, region);

figure
plotCellData(G, state0.s(:,1),'EdgeAlpha',.1), colormap(flipud(winter));
plotWell(G,W,'color','k')
patch([-50 1050 1050 -50],[-50 -50 1050 1050],depthOW*ones(1,4), ...
    ones(1,4), 'FaceColor',[.6 .6 1],'EdgeColor','r','LineWidth',1);
view(-50, 50), axis tight off
_images/upscalingExample3_06.png

Define simulation schedule and set solver parameters

We define a relatively simple schedule consisting of five small control steps initially, followed by 25 larger steps. We keep the well controls fixed throughout the simulation. To accelerate the simulation, we set somewhat stricter tolerances and use a CPR preconditioner with an algebraic multigrid solver (agmg) for the elliptic pressure system

% Compute the timestep
nstep   = 25;
refine  = 5;
startSteps = repmat((simTime/(nstep + 1))/refine, refine, 1);
restSteps=  repmat(simTime/(nstep + 1), nstep, 1);
timesteps = [startSteps; restSteps];

% Set up the schedule containing both the wells and the timestep
schedule = simpleSchedule(timesteps, 'W', W);

% Tighten tolerences
model.drsMaxRel = inf;
model.dpMaxRel  = .1;
model.dsMaxAbs  = .1;

% Set up CPR preconditioner
try
    mrstModule add agmg
    pressureSolver = AGMGSolverAD('tolerance', 1e-4);
catch
    pressureSolver = BackslashSolverAD();
end
linsolve = CPRSolverAD('ellipticSolver', pressureSolver, 'relativeTolerance', 1e-3);
No module mapping found for
  * agmg

Simulate base case

We now have an intial state, the schedule defines dynamic controls and time steps, and the model gives the mathematical description of how to advance the solution one time step. We then have all we need to simulate the problem. Because the simulation will consume some time, we launch a progress report and a plotting tool for the well solutions (well rates and bottom-hole pressures)

fn = getPlotAfterStep(state0, model, schedule,'view',[50 50], ...
                     'field','s:1','wells',W);
[wellSols, states, report] = ...
   simulateScheduleAD(state0, model, schedule, ...
                       'LinearSolver', linsolve, 'afterStepFn',fn);
Solving timestep 01/30:          -> 28 Days, 1 Hour, 3046.15 Seconds
Warning: Matrix is close to singular or badly scaled. Results may be inaccurate.
RCOND =  1.216893e-16.
Warning: Matrix is close to singular or badly scaled. Results may be inaccurate.
RCOND =  1.216893e-16.
Warning: Matrix is close to singular or badly scaled. Results may be inaccurate.
RCOND =  1.216893e-16.
Warning: Matrix is close to singular or badly scaled. Results may be inaccurate.
...
_images/upscalingExample3_07.png
_images/upscalingExample3_08.png
_images/upscalingExample3_09.png

Plot reservoir states

We launch a plotting tool for the reservoir quantities (pressures and saturations, located in states).

figure, plotToolbar(G, states)
view(50, 50);
plotWell(G,W);

figure
mrstModule add coarsegrid
CG = generateCoarseGrid(G,ones(G.cells.num,1));
plotWell(G, W,'color','k')
plotFaces(CG,1:CG.faces.num,'EdgeColor','k','FaceColor','none','LineWidth',2);
s=states{6}.s(:,1); plotCellData(G,s,s>1e-2); caxis([0 1]);
colormap(flipud(winter)), view(-53,46); axis tight off
_images/upscalingExample3_10.png
_images/upscalingExample3_11.png

Launch flow diagnostics

interactiveDiagnostics(G, rock, W, 'state', states{end}, 'computeFlux', false);
set(gcf,'position',[440 317 866 480]); axis normal
view(-85,73)
New state encountered, computing diagnostics...
_images/upscalingExample3_12.png
_images/upscalingExample3_13.png

Create an upscaled, coarser model

The fine scale model has 16 350 cells. If we want a smaller model we can easily define an upscaled model. We start by setting up a straightforward uniform partition of the IJK-indices. This gives 1388 coarse blocks (after a postprocessing to split disconnected blocks), which corresponds to more than 20 times reduction in the number of unknowns.

mrstModule add coarsegrid
cdims = [10, 10, 12];
p0 = partitionUI(G, cdims);
% p0 = partitionLayers(G, cdims(1:2), L);

figure;
CG = generateCoarseGrid(G,p0);
plotGrid(G,'FaceColor',[1 .8 .9],'EdgeAlpha',.1);
plotFaces(CG,1:CG.faces.num,'EdgeColor','k','FaceColor','none','LineWidth',1.5);
axis tight off, view(125, 55)
title('Straightforward index partition');
_images/upscalingExample3_14.png

Split blocks over the fault lines

The partition above may give blocks that have cells on opposite sides of a fault. To ensure that as many as possible of the coarse blocks are hexahedral (except for those that are partially eroded), we split blocks that have cells on both sides of a fault. To do this, we introduce a temporary grid in which the faults act as barriers, and then perform a simple postprocessing to split any coarse blocks intersected by one of the faults. Afterwards, we show the new partition and highlight blocks created due to the modification of the fault.

Gf = makeInternalBoundary(G, find(G.faces.tag > 0));
p = processPartition(Gf, p0);
plotGrid(G,p>prod(max(p0)),'FaceColor','g','EdgeColor','none');
title('Splitting over fault lines');
_images/upscalingExample3_15.png

Upscale the model and simulate the coarser problem

We can now directly upscale the model, schedule, and initial state. By default, the upscaling routine uses the simplest possible options, i.e., harmonic averaging of permeabilities. It is possible to use more advanced options, but for the purpose of this example we will use the defaults.

modelC1    = upscaleModelTPFA(model, p);
scheduleC1 = upscaleSchedule(modelC1, schedule);
state0C1   = upscaleState(modelC1, model, state0);

% Plot the initial state
figure,
GC    = modelC1.G;
rockC = modelC1.rock;
plotCellData(GC,state0C1.s(:,1),'EdgeColor','none'), colormap(flipud(winter));
plotWell(G,W,'color','k')
plotFaces(GC,1:GC.faces.num,'FaceColor','none');
patch([-50 1050 1050 -50],[-50 -50 1050 1050],85*ones(1,4), ...
    ones(1,4), 'FaceColor',[.6 .6 1],'EdgeColor','r','LineWidth',1);
view(-50, 50), axis tight off

% Once we have an upscaled model, we can again simulate the new schedule
% and observe that the time taken is greatly reduced, even without the use
% of a CPR preconditioner.
[wellSolsC1, statesC1] = simulateScheduleAD(state0C1, modelC1, scheduleC1);
Solving timestep 01/30:          -> 28 Days, 1 Hour, 3046.15 Seconds
Solving timestep 02/30: 28 Days, 1 Hour, 3046.15 Seconds -> 56 Days, 3 Hours, 2492.31 Seconds
Solving timestep 03/30: 56 Days, 3 Hours, 2492.31 Seconds -> 84 Days, 5 Hours, 1938.46 Seconds
Solving timestep 04/30: 84 Days, 5 Hours, 1938.46 Seconds -> 112 Days, 7 Hours, 1384.62 Seconds
Solving timestep 05/30: 112 Days, 7 Hours, 1384.62 Seconds -> 140 Days, 9 Hours, 830.77 Seconds
Solving timestep 06/30: 140 Days, 9 Hours, 830.77 Seconds -> 280 Days, 18 Hours, 1661.54 Seconds
Solving timestep 07/30: 280 Days, 18 Hours, 1661.54 Seconds -> 1 Year, 56 Days, 3.69 Hours
Solving timestep 08/30: 1 Year, 56 Days, 3.69 Hours -> 1 Year, 196 Days, 12.92 Hours
...
_images/upscalingExample3_16.png

Create a second upscaled model with flow-based upscaling

We use the upscaling module to create a tailored upscaled model. This upscaling routine uses an incompressible flow field with the wells of the problem to perform a global upscaling.

mrstModule add incomp agglom upscaling

[~, TC, WC] = upscaleTrans(GC, model.operators.T_all, ...
    'Wells', W, 'bc_method', 'wells', 'fix_trans', true);

modelC2 = upscaleModelTPFA(model, p, 'transCoarse', TC);
scheduleC2 = schedule;
for i = 1:numel(scheduleC2.control)
    scheduleC2.control(i).W = WC;
end
[wellSolsC2, statesC2] = simulateScheduleAD(state0C1, modelC2, scheduleC2);
Upscaled transmissibility does not reproduce upscaling scenario. Results may be inaccurate.
Pressure solve 1/1 with upscaled transmissibility does not give comparable results
Warning: Set boundary face trans to zero
Solving timestep 01/30:          -> 28 Days, 1 Hour, 3046.15 Seconds
Solving timestep 02/30: 28 Days, 1 Hour, 3046.15 Seconds -> 56 Days, 3 Hours, 2492.31 Seconds
Solving timestep 03/30: 56 Days, 3 Hours, 2492.31 Seconds -> 84 Days, 5 Hours, 1938.46 Seconds
Solving timestep 04/30: 84 Days, 5 Hours, 1938.46 Seconds -> 112 Days, 7 Hours, 1384.62 Seconds
Solving timestep 05/30: 112 Days, 7 Hours, 1384.62 Seconds -> 140 Days, 9 Hours, 830.77 Seconds
...

Compare the well solutions

plotWellSols({wellSols, wellSolsC1, wellSolsC2}, cumsum(schedule.step.val), ...
   'DatasetNames', {'Fine scale', 'Harmonic', 'Flow-based'}, 'Field', 'qOs');
_images/upscalingExample3_17.png

Compute flow diagnostics

As an alternative to looking at well curves, we can also look at the flow diagnostics of the models. Flow diagnostics are simple routines based on time-of-flight and tracer equations, which aim to give a qualitative understanding of reservoir dynamics. Here, we take the state after a single time step as a snapshot for both the fine and coarse model and compute time-of-flight and well tracers.

mrstModule add diagnostics
D    = computeTOFandTracer(states{2},   G,  rock,  'Wells', schedule.control.W);
DC1  = computeTOFandTracer(statesC1{2}, GC, rockC, 'Wells', scheduleC1.control.W);
DC2  = computeTOFandTracer(statesC2{2}, GC, rockC, 'Wells', scheduleC1.control.W);
WP   = computeWellPairs(states{1},   G,  rock,  schedule.control.W,   D);
WPC1 = computeWellPairs(statesC1{1}, GC, rockC, scheduleC1.control.W, DC1);
WPC2 = computeWellPairs(statesC2{1}, GC, rockC, scheduleC1.control.W, DC2);

figure, plotWellAllocationComparison(DC1, WPC1, D, WP);%, 'plotrow', true);
figure, plotWellAllocationComparison(DC2, WPC2, D, WP);%, 'plotrow', true);
_images/upscalingExample3_18.png
_images/upscalingExample3_19.png