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
@dataclass(repr=False)
class Fluid(struct_tools.AlignedRepr):
 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.   ]))
Fluid( vw: float = 1.0, vo: float = 1.0, swc: float = 0.0, sor: float = 0.0, nw: float = 2.0, no: float = 2.0, krw0: float = 1.0, kro0: float = 1.0)
vw: float = 1.0

Viscosity for water.

vo: float = 1.0

Viscosity for oil.

swc: float = 0.0

Irreducible saturation, water: $ k_{rw} = 0 $ below it.

sor: float = 0.0

Irreducible saturation, oil: $ k_{ro} = 0 $ above $ s = 1 - S_{or} $.

nw: float = 2.0

Corey exponent of the water relative permeability.

no: float = 2.0

Corey exponent of the oil relative permeability.

krw0: float = 1.0

End-point (maximal) water relative permeability, at $ s = 1 - S_{or} $.

kro0: float = 1.0

End-point (maximal) oil relative permeability, at $ s = S_{wc} $.

def rescale_sat(self, s: numpy.ndarray) -> numpy.ndarray:
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.

def RelPerm(self, s: numpy.ndarray) -> tuple[numpy.ndarray, numpy.ndarray]:
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).

def dRelPerm(self, s: numpy.ndarray) -> tuple[numpy.ndarray, numpy.ndarray]:
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).

def fractional_flow(self, s: numpy.ndarray) -> numpy.ndarray:
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 $.

def dfractional_flow(self, s: numpy.ndarray) -> numpy.ndarray:
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.