minires.fluids
The two-phase fluid, held at minires.ResSim.fluid.
Fluid holds the phase viscosities and the parameters of the Corey relative
permeability curves, and computes what the model needs of them: the mobilities
(RelPerm, Listing 6 of the reference paper) and the fractional flow
(fractional_flow), each with its derivative (dRelPerm, dfractional_flow).
Curves of another shape (e.g. tabulated) are a subclass overriding RelPerm and
dRelPerm.
Theory
Fossil fuel hydrocarbons is sedimented, pressurized, organic material
(mostly plants?) that used to live on the sub-sea continental shelves.
On-land organic material turns into coal.
⇒ Saudi-Arabia used to be sub-sea?
The energy in oil & gas comes from the sun (photosynthesis),
not the compression.
The lightest hydrocarbons (methane, ethane, etc.) usually escapes quickly,
while oils moves slowly towards the surface.
Sometimes the geology is bends to form caps of non-permeable rock
(ref minires.ResSim.K), so that the migrating hydrocarbons are trapped.
In the North Sea, these reservoirs lie 1000-3000 meters below the sea bed.
Norway is also surrounded by the Norwegian sea,
and the Barents sea, towards Murmansk.
Reservoir simulators implement porous media flow on upscaled geophysical parameters typically with grid blocks between 1 - 100 m. They usually parameterize multiphase flow. If only the two phases of oil and water are used it is called black-oil. A common assumption is that the flow is immiscible: not mixing (oil and water). But this does not mean that gas cannot be dissolved in oil.
The phases (water, oil, gas), whose saturations sum to $1$,
contains components (e.g. methane, ethane, propane),
usually grouped as pseudo-components.
Each phase's mass fraction component, $c_{phase,i}$, sums to $1$.
Each phase has density, $ρ$ and viscosity, $μ$ (Fluid.vw, Fluid.vo),
generally functions of the phase pressure,
but usually neglected except for gas.
The differences in pressure are named capillary pressure
because they arise due to interfacial tensions; this model has none.
A phase's compressibility is defined similar as for the rock's
(ref minires.ResSim.ct, which lumps them all into one).
Confusingly, it is also denoted with $c$, but using only a single subscript.
Phases do not really mix. But in macro-scale modelling all phases
may be present at the same location. Therefore a phase's permeability
should depend on the saturations, to which end we introduce relative permeability,
$k_{r,i} = k_{r,i}(s_g, s_o), i = g, o, w$
a nonlinear function, yielding an (effective) permeability
$\mathbf{K_i} = \mathbf{K} k_{r,i}$
Relative permeability curves do not extend all over the interval $[0, 1]$.
The smallest saturation where a phase is mobile is called the
residual saturation (Fluid.swc, Fluid.sor).
This adsorption effects may vary, and this may have important effects,
particularly for simulation of polymer injection.
The uncertainty regarding relative permeability is modest compared to
the enormous uncertainty of the rock permeability (minires.ResSim.K).
Everything depends on thermodynamics, but this is often complex and neglected, except perhaps for the bubble/boiling point pressures, which govern how much of the gas dissolves in oil.
Since compressibility relates volumes to pressure, a volume must be qualified by where it is measured. The formation volume factor, $B$, is the ratio of the volume at reservoir conditions to that of the same mass at the surface ("stock tank"), and is how field rates (measured at the surface) are converted to the reservoir rates that a simulator works in. This model has $B = 1$. Related PVT (pressure-volume-temperature) vocabulary: the bubble point is the pressure below which gas comes out of solution; an oil above it is undersaturated, and the amount of gas it holds is the solution gas-oil ratio, $R_s$.
1"""The two-phase fluid, held at `minires.ResSim.fluid`. 2 3`Fluid` holds the phase viscosities and the parameters of the Corey relative 4permeability curves, and computes what the model needs of them: the mobilities 5(`RelPerm`, Listing 6 of the reference paper) and the fractional flow 6(`fractional_flow`), each with its derivative (`dRelPerm`, `dfractional_flow`). 7Curves of another shape (e.g. tabulated) are a subclass overriding `RelPerm` and 8`dRelPerm`. 9 10## Theory 11 12Fossil fuel hydrocarbons is sedimented, pressurized, organic material 13(mostly plants?) that used to live on the **sub-sea** continental shelves. 14**On-land** organic material turns into coal. 15⇒ Saudi-Arabia used to be sub-sea? 16The *energy* in oil & gas comes from the sun (photosynthesis), 17not the compression. 18The lightest *hydrocarbons* (methane, ethane, etc.) usually escapes quickly, 19while oils moves slowly towards the surface. 20Sometimes the geology is bends to form caps of non-permeable rock 21(ref `minires.ResSim.K`), so that the migrating hydrocarbons are trapped. 22In the *North Sea*, these reservoirs lie 1000-3000 meters below the sea bed. 23Norway is also surrounded by the *Norwegian sea*, 24and the *Barents sea*, towards Murmansk. 25 26Reservoir simulators implement porous media flow 27on upscaled geophysical parameters typically with grid blocks between 1 - 100 m. 28They usually parameterize multiphase flow. 29If only the two phases of oil and water are used it is called **black-oil**. 30A common assumption is that the flow is **immiscible**: not mixing (oil and water). 31But this does not mean that gas cannot be *dissolved* in oil. 32 33The **phases** (water, oil, gas), whose saturations sum to $1$, 34contains *components* (e.g. methane, ethane, propane), 35usually grouped as pseudo-components. 36Each phase's *mass fraction* component, $c_{phase,i}$, sums to $1$. 37Each phase has **density**, $ρ$ and **viscosity**, $μ$ (`Fluid.vw`, `Fluid.vo`), 38generally functions of the phase **pressure**, 39but usually neglected except for gas. 40The differences in pressure are named **capillary pressure** 41because they arise due to **interfacial tensions**; this model has none. 42A phase's **compressibility** is defined similar as for the rock's 43(ref `minires.ResSim.ct`, which lumps them all into one). 44Confusingly, it is also denoted with $c$, but using only a single subscript. 45 46Phases do not really mix. But in macro-scale modelling all phases 47may be present at the same location. Therefore a phase's permeability 48should depend on the saturations, to which end we introduce *relative permeability*, 49$k_{r,i} = k_{r,i}(s_g, s_o), i = g, o, w$ 50a nonlinear function, yielding an (effective) permeability 51$\\mathbf{K_i} = \\mathbf{K} k_{r,i}$ 52Relative permeability curves do not extend all over the interval $[0, 1]$. 53The smallest saturation where a phase is mobile is called the 54**residual saturation** (`Fluid.swc`, `Fluid.sor`). 55This *adsorption* effects may vary, and this may have important effects, 56particularly for simulation of *polymer injection*. 57The uncertainty regarding relative permeability is modest compared to 58the enormous uncertainty of the rock permeability (`minires.ResSim.K`). 59 60Everything depends on *thermodynamics*, but this is often complex and neglected, 61except perhaps for the bubble/boiling point pressures, 62which govern how much of the gas dissolves in oil. 63 64Since *compressibility* relates volumes to pressure, 65a volume must be qualified by where it is measured. 66The **formation volume factor**, $B$, is the ratio of the volume at reservoir 67conditions to that of the same mass at the surface ("stock tank"), 68and is how field rates (measured at the surface) are converted 69to the reservoir rates that a simulator works in. This model has $B = 1$. 70Related **PVT** (pressure-volume-temperature) vocabulary: 71the **bubble point** is the pressure below which gas comes out of solution; 72an oil above it is **undersaturated**, and the amount of gas it holds is the 73*solution gas-oil ratio*, $R_s$. 74""" 75 76from dataclasses import dataclass 77 78import numpy as np 79from struct_tools import AlignedRepr 80 81 82@dataclass(repr=False) 83class Fluid(AlignedRepr): 84 """A two-phase (water/oil) fluid: viscosities and Corey relative permeabilities. 85 86 The relative permeabilities are Corey (power-law) curves of the normalized 87 saturation $ S $ (`rescale_sat`), 88 89 $$ k_{rw} = k_{rw}^0 \\, S^{n_w} \\,, \\qquad k_{ro} = k_{ro}^0 \\, (1 - S)^{n_o} \\,, 90 \\qquad S = \\frac{s - S_{wc}}{1 - S_{wc} - S_{or}} \\,, $$ 91 92 with $ S $ clipped to $[0, 1]$, so that a phase below its residual saturation 93 is immobile (rather than mobile with the wrong sign, as an odd power would 94 make it). The defaults give the quadratic curves of the reference paper 95 (Listing 6), $ S^2 $ and $ (1 - S)^2 $, and unit viscosities. 96 97 >>> fluid = Fluid(vo=2, swc=.2, sor=.2) 98 >>> Mw, Mo = fluid.RelPerm(np.array([.1, .2, .5, .8, .9])) 99 >>> Mw.round(4), Mo.round(4) 100 (array([0. , 0. , 0.25, 1. , 1. ]), array([0.5 , 0.5 , 0.125, 0. , 0. ])) 101 """ 102 103 vw: float = 1.0 104 """Viscosity for water.""" 105 vo: float = 1.0 106 """Viscosity for oil.""" 107 swc: float = 0.0 108 """Irreducible saturation, water: $ k_{rw} = 0 $ below it.""" 109 sor: float = 0.0 110 """Irreducible saturation, oil: $ k_{ro} = 0 $ above $ s = 1 - S_{or} $.""" 111 nw: float = 2.0 112 """Corey exponent of the water relative permeability.""" 113 no: float = 2.0 114 """Corey exponent of the oil relative permeability.""" 115 krw0: float = 1.0 116 """End-point (maximal) water relative permeability, at $ s = 1 - S_{or} $.""" 117 kro0: float = 1.0 118 """End-point (maximal) oil relative permeability, at $ s = S_{wc} $.""" 119 120 def rescale_sat(self, s: np.ndarray) -> np.ndarray: 121 """The normalized saturation $ S $ (unclipped). Ref paper, p. 32.""" 122 return (s - self.swc) / (1 - self.swc - self.sor) 123 124 # RelPerm() -- listing 6 125 def RelPerm(self, s: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 126 """Rel. permeabilities of water and oil, as mobilities (perm/viscosity).""" 127 S = np.clip(self.rescale_sat(s), 0, 1) 128 Mw = self.krw0 * S**self.nw / self.vw 129 Mo = self.kro0 * (1 - S) ** self.no / self.vo 130 return Mw, Mo 131 132 def dRelPerm(self, s: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 133 """Derivatives of `RelPerm` wrt `s` (zero where the curves are clipped).""" 134 S = self.rescale_sat(s) 135 inside = (0 <= S) & (S <= 1) # one-sided at the ends, as the reference code 136 S = np.clip(S, 0, 1) 137 w = 1 - self.swc - self.sor 138 dMw = np.where(inside, self.krw0 * self.nw * S ** (self.nw - 1) / w, 0) / self.vw 139 dMo = -np.where(inside, self.kro0 * self.no * (1 - S) ** (self.no - 1) / w, 0) / self.vo 140 return dMw, dMo 141 142 def fractional_flow(self, s: np.ndarray) -> np.ndarray: 143 """The water fractional flow, $ f_w = λ_w / λ_t $.""" 144 Mw, Mo = self.RelPerm(s) 145 return Mw / (Mw + Mo) 146 147 def dfractional_flow(self, s: np.ndarray) -> np.ndarray: 148 """Derivative of `fractional_flow` wrt `s`. 149 150 Separate from `fractional_flow` (rather than a second output of it) because 151 the explicit transport scheme wants $ f_w $ alone, in its innermost loop, 152 where the derivative would cost as much again as everything else. 153 """ 154 Mw, Mo = self.RelPerm(s) 155 dMw, dMo = self.dRelPerm(s) 156 return (dMw * Mo - Mw * dMo) / (Mw + Mo) ** 2
83@dataclass(repr=False) 84class Fluid(AlignedRepr): 85 """A two-phase (water/oil) fluid: viscosities and Corey relative permeabilities. 86 87 The relative permeabilities are Corey (power-law) curves of the normalized 88 saturation $ S $ (`rescale_sat`), 89 90 $$ k_{rw} = k_{rw}^0 \\, S^{n_w} \\,, \\qquad k_{ro} = k_{ro}^0 \\, (1 - S)^{n_o} \\,, 91 \\qquad S = \\frac{s - S_{wc}}{1 - S_{wc} - S_{or}} \\,, $$ 92 93 with $ S $ clipped to $[0, 1]$, so that a phase below its residual saturation 94 is immobile (rather than mobile with the wrong sign, as an odd power would 95 make it). The defaults give the quadratic curves of the reference paper 96 (Listing 6), $ S^2 $ and $ (1 - S)^2 $, and unit viscosities. 97 98 >>> fluid = Fluid(vo=2, swc=.2, sor=.2) 99 >>> Mw, Mo = fluid.RelPerm(np.array([.1, .2, .5, .8, .9])) 100 >>> Mw.round(4), Mo.round(4) 101 (array([0. , 0. , 0.25, 1. , 1. ]), array([0.5 , 0.5 , 0.125, 0. , 0. ])) 102 """ 103 104 vw: float = 1.0 105 """Viscosity for water.""" 106 vo: float = 1.0 107 """Viscosity for oil.""" 108 swc: float = 0.0 109 """Irreducible saturation, water: $ k_{rw} = 0 $ below it.""" 110 sor: float = 0.0 111 """Irreducible saturation, oil: $ k_{ro} = 0 $ above $ s = 1 - S_{or} $.""" 112 nw: float = 2.0 113 """Corey exponent of the water relative permeability.""" 114 no: float = 2.0 115 """Corey exponent of the oil relative permeability.""" 116 krw0: float = 1.0 117 """End-point (maximal) water relative permeability, at $ s = 1 - S_{or} $.""" 118 kro0: float = 1.0 119 """End-point (maximal) oil relative permeability, at $ s = S_{wc} $.""" 120 121 def rescale_sat(self, s: np.ndarray) -> np.ndarray: 122 """The normalized saturation $ S $ (unclipped). Ref paper, p. 32.""" 123 return (s - self.swc) / (1 - self.swc - self.sor) 124 125 # RelPerm() -- listing 6 126 def RelPerm(self, s: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 127 """Rel. permeabilities of water and oil, as mobilities (perm/viscosity).""" 128 S = np.clip(self.rescale_sat(s), 0, 1) 129 Mw = self.krw0 * S**self.nw / self.vw 130 Mo = self.kro0 * (1 - S) ** self.no / self.vo 131 return Mw, Mo 132 133 def dRelPerm(self, s: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 134 """Derivatives of `RelPerm` wrt `s` (zero where the curves are clipped).""" 135 S = self.rescale_sat(s) 136 inside = (0 <= S) & (S <= 1) # one-sided at the ends, as the reference code 137 S = np.clip(S, 0, 1) 138 w = 1 - self.swc - self.sor 139 dMw = np.where(inside, self.krw0 * self.nw * S ** (self.nw - 1) / w, 0) / self.vw 140 dMo = -np.where(inside, self.kro0 * self.no * (1 - S) ** (self.no - 1) / w, 0) / self.vo 141 return dMw, dMo 142 143 def fractional_flow(self, s: np.ndarray) -> np.ndarray: 144 """The water fractional flow, $ f_w = λ_w / λ_t $.""" 145 Mw, Mo = self.RelPerm(s) 146 return Mw / (Mw + Mo) 147 148 def dfractional_flow(self, s: np.ndarray) -> np.ndarray: 149 """Derivative of `fractional_flow` wrt `s`. 150 151 Separate from `fractional_flow` (rather than a second output of it) because 152 the explicit transport scheme wants $ f_w $ alone, in its innermost loop, 153 where the derivative would cost as much again as everything else. 154 """ 155 Mw, Mo = self.RelPerm(s) 156 dMw, dMo = self.dRelPerm(s) 157 return (dMw * Mo - Mw * dMo) / (Mw + Mo) ** 2
A two-phase (water/oil) fluid: viscosities and Corey relative permeabilities.
The relative permeabilities are Corey (power-law) curves of the normalized
saturation $ S $ (rescale_sat),
$$ k_{rw} = k_{rw}^0 \, S^{n_w} \,, \qquad k_{ro} = k_{ro}^0 \, (1 - S)^{n_o} \,, \qquad S = \frac{s - S_{wc}}{1 - S_{wc} - S_{or}} \,, $$
with $ S $ clipped to $[0, 1]$, so that a phase below its residual saturation is immobile (rather than mobile with the wrong sign, as an odd power would make it). The defaults give the quadratic curves of the reference paper (Listing 6), $ S^2 $ and $ (1 - S)^2 $, and unit viscosities.
>>> fluid = Fluid(vo=2, swc=.2, sor=.2)
>>> Mw, Mo = fluid.RelPerm(np.array([.1, .2, .5, .8, .9]))
>>> Mw.round(4), Mo.round(4)
(array([0. , 0. , 0.25, 1. , 1. ]), array([0.5 , 0.5 , 0.125, 0. , 0. ]))
121 def rescale_sat(self, s: np.ndarray) -> np.ndarray: 122 """The normalized saturation $ S $ (unclipped). Ref paper, p. 32.""" 123 return (s - self.swc) / (1 - self.swc - self.sor)
The normalized saturation $ S $ (unclipped). Ref paper, p. 32.
126 def RelPerm(self, s: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 127 """Rel. permeabilities of water and oil, as mobilities (perm/viscosity).""" 128 S = np.clip(self.rescale_sat(s), 0, 1) 129 Mw = self.krw0 * S**self.nw / self.vw 130 Mo = self.kro0 * (1 - S) ** self.no / self.vo 131 return Mw, Mo
Rel. permeabilities of water and oil, as mobilities (perm/viscosity).
133 def dRelPerm(self, s: np.ndarray) -> tuple[np.ndarray, np.ndarray]: 134 """Derivatives of `RelPerm` wrt `s` (zero where the curves are clipped).""" 135 S = self.rescale_sat(s) 136 inside = (0 <= S) & (S <= 1) # one-sided at the ends, as the reference code 137 S = np.clip(S, 0, 1) 138 w = 1 - self.swc - self.sor 139 dMw = np.where(inside, self.krw0 * self.nw * S ** (self.nw - 1) / w, 0) / self.vw 140 dMo = -np.where(inside, self.kro0 * self.no * (1 - S) ** (self.no - 1) / w, 0) / self.vo 141 return dMw, dMo
Derivatives of RelPerm wrt s (zero where the curves are clipped).
143 def fractional_flow(self, s: np.ndarray) -> np.ndarray: 144 """The water fractional flow, $ f_w = λ_w / λ_t $.""" 145 Mw, Mo = self.RelPerm(s) 146 return Mw / (Mw + Mo)
The water fractional flow, $ f_w = λ_w / λ_t $.
148 def dfractional_flow(self, s: np.ndarray) -> np.ndarray: 149 """Derivative of `fractional_flow` wrt `s`. 150 151 Separate from `fractional_flow` (rather than a second output of it) because 152 the explicit transport scheme wants $ f_w $ alone, in its innermost loop, 153 where the derivative would cost as much again as everything else. 154 """ 155 Mw, Mo = self.RelPerm(s) 156 dMw, dMo = self.dRelPerm(s) 157 return (dMw * Mo - Mw * dMo) / (Mw + Mo) ** 2
Derivative of fractional_flow wrt s.
Separate from fractional_flow (rather than a second output of it) because
the explicit transport scheme wants $ f_w $ alone, in its innermost loop,
where the derivative would cost as much again as everything else.