Reservoir

Subsurface reservoirs are the main bodies for the modelling in DARTS. Reservoir is usually represented by its geometry covered by some computaional grid, properties like porosity, permeability or stiffness defined in the grid, boundary conditions that define fluxes over the reservoir boundary and some other things. Along with the model, reservoir defines the parameters required for any kind of modelling in DARTS.

The kind of computational grid spanning reservoir produces two types of reservoirs: structured and unstructured. The treatment of structured reservoir can be generalized that was done by StructReservoir class provided in DARTS. Many models use it directly without overloading and extension built-in methods. The models working with unstructured reservoir have to provide their own implementation that is usually represented by UnstructReservoir class defined in reservoir.py script.

Unstructured reservoir

Let us describe the basic parts which unstructured reservoir must and may include.

Constructor

In many (simple) models the constructor may get some model parameters like porosity and permeability, the type of physics and boundary conditions. In general case it becomes difficult to define reservoir parameters in model and send them to reservoir constructor. In this case all reservoir properties and associated parameters may be defined in reservoir.py.

Below we will discuss only the basic pieces of code, excluding many other lines of code.

Usually constructor starts includes the creating of conn_mesh class defined in C++ back-end:

self.mesh = conn_mesh()

followed by the calling the constructor of unstructured discretizer, e.g.

self.unstr_discr = UnstructDiscretizer(permx=permx, permy=permy, permz=permz, frac_aper=frac_aper, mesh_file=mesh_file)

mesh loading

self.unstr_discr.load_mesh()

and some processing

self.unstr_discr.calc_cell_information()

This is basic scenario for this part of constructor, which can be limited for many models. Some reservoirs require different implementation of last two functions (unstructured, fluidflower, mpfa, mpsa) including some pre- and post-processing of reservoir parameters required for construction of unstructured discretizer, some reservoirs use different discretizer at all (mpfa-mpsa). However, almost all reservoir parameters are defined (or have to be defined) in this block of code in order to initialize and run discretizer. Therefore, in the models working with multi-point approximations this block of code was placed into separate method of unstructured reservoir to be called here in the constructor(mpfa, mpsa mpfa-mpsa).

Once the necessary properties prescribed and discretizer is initialized, the discretization can be run as follows

cell_m, cell_p, tran, tran_thermal = self.unstr_discr.calc_connections_all_cells(cache=False)

in the case of two-point flux approximation,

self.cell_m, self.cell_p, self.stencil, self.offset, self.trans = self.unstr_discr.calc_mpfa_connections_all_cells(True)

in the case of multi-point flux approximation,

self.pm.reconstruct_gradients_per_cell(dt)
self.pm.calc_all_fluxes_once(dt)

in the case of poromechanics discretizer pm.

The output obtained from discretizer then can be written in files and have to be provided to self.mesh in an initialization call

self.mesh.init(...)

where the call and number of arguments varies with different types of physics and discretization.

Next step is to expose all the specified arrays to c++ backend. Pybind11 allows to make a numpy wrap around c++ vectors in order to copy the data.

In many models the boundary conditions are specified in the end of constructor because they do not affect the results of discretization. Generally, it is not the case and we need to specify them before running discretization (mpfa, mpsa, mpfa-mpsa). Also in some models the well locations are found in the constructor. Although it is not needed for discretization, these data is used afterwards in the post-proccessing of discretization results.

The constructor represents the main functionality of UnstructReservoir class: defining the input data for discretizer, runnning discretization and passing data and results to c++ backend. Some models may introduce extra functions that called in the constructor in order to assign input for discretization, for pre- or post-processing or other purposes (fluidflower, mpfa, mpsa, mpfa-mpsa).

Well functions

The most of implementations of UnstructReservoir class contain a few functions that help to initialize wells. The calls

def add_well(self, name, depth)
def add_perforation(self, well, res_block, well_index)
def init_wells(self)

may have different implementations, but they are quite general for most of the models.

Writing output

One of the calls

def write_to_vtk(self, ...)
def export_vtk(self, ...)

is usually used in order to write output in VTK format. They can have different number of arguments and different implementations specific for a particular model or reservoir. However, it can be generalized.