|
1 | | -# Petroleum reservoirs 🚧 |
| 1 | +# Petroleum reservoirs |
2 | 2 |
|
3 | | -## Coming soon... |
| 3 | +```{julia} |
| 4 | +#| echo: false |
| 5 | +#| output: false |
| 6 | +import Pkg |
| 7 | +Pkg.activate(".") |
| 8 | +``` |
| 9 | + |
| 10 | +Petroleum reservoirs present various modeling challenges related to |
| 11 | +their complex geometry and distribution of rock and fluid properties. |
| 12 | +Some of these challenges are still open in industry given the lack of |
| 13 | +software for advanced geospatial data science with unstructured meshes. |
| 14 | +In this chapter, we will illustrate how a very important "oil-in-place" |
| 15 | +calculation in reservoir management can be automated with the framework. |
| 16 | + |
| 17 | +**TOOLS COVERED:** `@groupby`, `@transform`, `@combine`, `Unitify`, `Unit`, |
| 18 | +`GHC`, `volume`, `viewer` |
| 19 | + |
| 20 | +**MODULES:** |
| 21 | + |
| 22 | +```{julia} |
| 23 | +# framework |
| 24 | +using GeoStats |
| 25 | +
|
| 26 | +# IO modules |
| 27 | +using GeoIO |
| 28 | +
|
| 29 | +# viz modules |
| 30 | +import CairoMakie as Mke |
| 31 | +``` |
| 32 | + |
| 33 | +```{julia} |
| 34 | +#| echo: false |
| 35 | +#| output: false |
| 36 | +Mke.activate!(type = "png") |
| 37 | +``` |
| 38 | + |
| 39 | +::: {.callout-note} |
| 40 | + |
| 41 | +Although we use CairoMakie.jl in this book, many of the 3D visualizations |
| 42 | +in this chapter demand a more performant Makie.jl backend. Consider using |
| 43 | +GLMakie.jl if you plan to reproduce the code locally. |
| 44 | + |
| 45 | +::: |
| 46 | + |
| 47 | +## Data |
| 48 | + |
| 49 | +We will use reservoir simulation results of the Norne benchmark case, a real |
| 50 | +oil field from the Norwegian Sea. For more information, please check the |
| 51 | +[OPM project](https://opm-project.org). These results were simulated with |
| 52 | +the [JutulDarcy.jl](https://github.com/sintefmath/JutulDarcy.jl) reservoir |
| 53 | +simulator by @Moyner2025. |
| 54 | + |
| 55 | +In particular, we will consider only two time steps of the simulation, named |
| 56 | +`norne1.vtu` and `norne2.vtu`. The data are stored in the open VTK format with |
| 57 | +`.vtu` extension, indicating that it is georeferenced over an unstructured mesh: |
| 58 | + |
| 59 | +```{julia} |
| 60 | +norne₁ = GeoIO.load("data/norne1.vtu") |
| 61 | +norne₂ = GeoIO.load("data/norne2.vtu") |
| 62 | +
|
| 63 | +norne₁ |> viewer |
| 64 | +``` |
| 65 | + |
| 66 | +::: {.callout-note} |
| 67 | + |
| 68 | +The [vtk.org](https://vtk.org) website provides official documentation for |
| 69 | +the various VTK file formats, including formats for image data (`.vti`), |
| 70 | +rectilinear grids (`.vtr`), structured grids (`.vts`), unstructured meshes |
| 71 | +(`.vtu`), etc. |
| 72 | + |
| 73 | +::: |
| 74 | + |
| 75 | +## Objectives |
| 76 | + |
| 77 | +The Volume of Oil In Place ($VOIP$) is a global estimate of the volume of oil |
| 78 | +trapped in the subsurface. It is defined as an integral over the volume $V$ |
| 79 | +of the reservoir: |
| 80 | + |
| 81 | +$$ |
| 82 | +VOIP = \int_V S_o \phi\ dV |
| 83 | +$$ |
| 84 | + |
| 85 | +where $\phi$ is the rock porosity and $S_o$ is the oil saturation. The integrand |
| 86 | +can be converted into Mass of Oil in Place ($MOIP$) using the oil density $\rho_o$: |
| 87 | + |
| 88 | +$$ |
| 89 | +MOIP = \int_V \rho_o S_o \phi\ dV |
| 90 | +$$ |
| 91 | + |
| 92 | +Likewise, the Mass of Water In Place ($MWIP$) and Mass of Gas in Place ($MGIP$) |
| 93 | +are defined using the respective fluid saturations and densities. |
| 94 | + |
| 95 | +Our main objective is to estimate these masses of fluids in place over a reservoir model |
| 96 | +with non-trivial geometry, for different time steps within a physical reservoir simulation. |
| 97 | +This can be useful to understand rates of depletion and guide reservoir management. |
| 98 | + |
| 99 | +Secondary objectives include the localization (through 3D visualization) of zones with high |
| 100 | +mass of hydrocarbons (oil + gas), and the calculation of zonal depletion, i.e., the change |
| 101 | +of hydrocarbon mass per zone, from a time step $t_1$ to a time step $t_2$: |
| 102 | + |
| 103 | +$$ |
| 104 | +Depletion = \left\{MOIP + MGIP\right\}_{t_1} - \left\{MOIP + MGIP\right\}_{t_2} |
| 105 | +$$ |
| 106 | + |
| 107 | +## Methodology |
| 108 | + |
| 109 | +In order to identify zones of the reservoir with high mass of hydrocarbons, we need to |
| 110 | +compute the fluids in place for each element of the mesh, and group the elements based |
| 111 | +on their calculated masses. Given the zones, we can compute the zonal depletion. |
| 112 | + |
| 113 | +The proposed methodology has the following steps: |
| 114 | + |
| 115 | +1. Analysis of oil, gas and water in place |
| 116 | +2. Localization of hydrocarbon zones |
| 117 | +3. Calculation of zonal depletion |
| 118 | + |
| 119 | +### Fluid analysis |
| 120 | + |
| 121 | +Before we start our calculations, we need to rename the variables in the dataset to |
| 122 | +match our concise notation. We also need to correct the units of the variables to |
| 123 | +make sure that our final report has values that are easy to read. |
| 124 | + |
| 125 | +The following pipeline performs the desired cleaning steps by exploiting bracket |
| 126 | +notation (e.g., `[kg/m^3]`) for units. The `Unitify` transform takes a geotable |
| 127 | +with bracket notation as input and converts the values of columns to unitful |
| 128 | +values: |
| 129 | + |
| 130 | +```{julia} |
| 131 | +clean = Select( |
| 132 | + "porosity" => "ϕ", |
| 133 | + "saturation_oil" => "So", |
| 134 | + "saturation_gas" => "Sg", |
| 135 | + "saturation_water" => "Sw", |
| 136 | + "density_oil" => "ρo [kg/m^3]", |
| 137 | + "density_gas" => "ρg [kg/m^3]", |
| 138 | + "density_water" => "ρw [kg/m^3]" |
| 139 | +) → Unitify() |
| 140 | +``` |
| 141 | + |
| 142 | +The resulting geotable has variables with concise names and correct units: |
| 143 | + |
| 144 | +```{julia} |
| 145 | +reservoir₁ = clean(norne₁) |
| 146 | +reservoir₂ = clean(norne₂) |
| 147 | +``` |
| 148 | + |
| 149 | +We `@transform` the reservoir and compute masses of fluids for each |
| 150 | +element of the mesh using the formulae in the beginning of the chapter: |
| 151 | + |
| 152 | +```{julia} |
| 153 | +mass(reservoir) = @transform(reservoir, |
| 154 | + :MOIP = :ρo * :So * :ϕ * volume(:geometry), |
| 155 | + :MGIP = :ρg * :Sg * :ϕ * volume(:geometry), |
| 156 | + :MWIP = :ρw * :Sw * :ϕ * volume(:geometry) |
| 157 | +) |
| 158 | +
|
| 159 | +mass₁ = mass(reservoir₁) |
| 160 | +mass₂ = mass(reservoir₂) |
| 161 | +
|
| 162 | +mass₁ |> Select("MWIP") |> viewer |
| 163 | +``` |
| 164 | + |
| 165 | +### Hydrocarbon zones |
| 166 | + |
| 167 | +We compute the mass of hydrocarbon in place $MHIP$ as the sum of oil and gas in |
| 168 | +the first time step, and cluster it with geostatistical hierarchical clustering |
| 169 | +(`GHC`) [@Fouedjio2016]. The method requires an approximate number of clusters |
| 170 | +that we set to $k=3$ (low, medium and high values) and a maximum range of |
| 171 | +geospatial association that we set to $\lambda = 500m$. |
| 172 | + |
| 173 | +Additionally, we set an upper bound `nmax=1000` on the number of elements used |
| 174 | +in the dissimilarity matrix computation and the option `as="zone"` to name the |
| 175 | +column with clustering results. |
| 176 | + |
| 177 | + |
| 178 | +```{julia} |
| 179 | +zones = @transform(mass₁, :MHIP = :MOIP + :MGIP) |> |
| 180 | + Select("MHIP") |> GHC(3, 500u"m", nmax=1000, as="zone") |
| 181 | +
|
| 182 | +zones |> viewer |
| 183 | +``` |
| 184 | + |
| 185 | +### Zonal depletion |
| 186 | + |
| 187 | +We concatenate all variables of interest in a single geotable to be able to use |
| 188 | +the geospatial [split-apply-combine](08-splitcombine.qmd) pattern, and compute |
| 189 | +the final summary table with statistics per zone: |
| 190 | + |
| 191 | +```{julia} |
| 192 | +carbon₁ = mass₁ |> Select("MOIP" => "MOIP₁", "MGIP" => "MGIP₁") |
| 193 | +carbon₂ = mass₂ |> Select("MOIP" => "MOIP₂", "MGIP" => "MGIP₂") |
| 194 | +
|
| 195 | +data = [carbon₁ carbon₂ zones] |
| 196 | +``` |
| 197 | + |
| 198 | +The depletion per zone can be computed with |
| 199 | + |
| 200 | +```{julia} |
| 201 | +summary = @chain data begin |
| 202 | + @groupby(:zone) |
| 203 | + @transform(:delta = :MOIP₁ + :MGIP₁ - :MOIP₂ - :MGIP₂) |
| 204 | + @combine(:depletion = sum(:delta)) |
| 205 | +end |
| 206 | +``` |
| 207 | + |
| 208 | +or in $Mg$ (ton) after a change of `Unit`: |
| 209 | + |
| 210 | +```{julia} |
| 211 | +summary |> Unit("depletion" => u"Mg") |
| 212 | +``` |
| 213 | + |
| 214 | +## Summary |
| 215 | + |
| 216 | +In this chapter, we illustrated the application of the framework in the petroleum |
| 217 | +industry. Among other things, we learned how to |
| 218 | + |
| 219 | +- Perform simple calculations involving fluids in place and unstructured meshes. |
| 220 | +- Identify zones of a petroleum reservoir using clustering methods and visualizations. |
| 221 | + |
| 222 | +This open source technology can be used to create advanced dashboards for reservoir |
| 223 | +management without advanced programming skills. It addresses real issues raised by |
| 224 | +geospatial data scientists in industry who feel unproductive using rigid geomodeling |
| 225 | +software. |
0 commit comments