"""Object to represent GasWater, implemented as a Container
object for one WaterOil and one GasOil object"""
from typing import Optional
import pandas as pd
from pyscal import getLogger_pyscal
from pyscal.utils.relperm import crosspoint
from .gasoil import GasOil
from .wateroil import WaterOil
logger = getLogger_pyscal(__name__)
[docs]
def is_documented_by(original):
"""Decorator to avoid duplicating function docstrings"""
def wrapper(target):
target.__doc__ = original.__doc__
return target
return wrapper
[docs]
class GasWater:
"""A representation of two-phase properties for gas-water
Internally, this class handles gas-water by using one WaterOil
object and one GasOil object, with dummy parameters for oil.
Args:
swirr: Irreducible water saturation for capillary pressure
swl: First water saturation point in outputted tables.
sgl: Minimum gas saturation in a gas paleozone.
swcr: Critical water saturation, water is immobile below this
sgrw: Residual gas saturation after water flooding.
sgcr: Critical gas saturation, gas is immobile below this
h: Saturation intervals in generated tables.
tag: Optional text that will be included as comments.
fast: Set to True if you prefer speed over robustness. Not recommended,
pyscal will not guarantee valid output in this mode.
"""
def __init__(
self,
swirr: float = 0.0,
swl: float = 0.0,
sgl: float = 0.0,
swcr: float = 0.0,
sgrw: float = 0.0,
sgcr: float = 0.0,
h: Optional[float] = None,
tag: str = "",
fast: bool = False,
) -> None:
"""Sets up the saturation range for a GasWater object,
by initializing one WaterOil and one GasOil object, with
endpoints set to fit with the GasWater proxy object."""
self.fast: bool = fast
if h is None:
h = 0.01
self.wateroil: WaterOil = WaterOil(
swirr=swirr,
swl=swl,
swcr=swcr,
sorw=sgrw,
h=h,
tag=tag,
fast=fast,
_sgcr=sgcr,
_sgl=sgl,
)
self.sgcr: float = sgcr
self.sgrw: float = sgrw
self.sgl: float = sgl
# (remaining parameters are implemented as property functions)
self.gasoil: GasOil = GasOil(
swirr=swirr, # Reserved for use in capillary pressure
sgcr=sgcr,
sorg=0, # Irrelevant for GasWater
swl=swl,
h=h,
tag=tag,
fast=fast,
_sgl=sgl,
)
# Dummy oil curves, just to avoid objects being invalid.
self.wateroil.add_corey_oil()
self.gasoil.add_corey_oil()
[docs]
def selfcheck(self) -> bool:
"""Run selfcheck on the data.
Performs tests if necessary data is ready in the object for
printing Eclipse include files, and checks some numerical
properties (direction and monotonicity)
"""
if self.wateroil is not None and self.gasoil is not None:
return self.wateroil.selfcheck() and self.gasoil.selfcheck()
logger.error("None objects in GasWater (bug)")
return False
[docs]
def add_corey_water(
self, nw: float = 2.0, krwend: float = 1.0, krwmax: Optional[float] = None
) -> None:
"""Add krw data through the Corey parametrization
A column named 'krw' will be added. If it exists, it will
be replaced.
The Corey model applies for sw < 1 - sgrw. For higher
water saturations, krw is linear between krwend and krwmax.
krwmax will be ignored if sgrw is close to zero
Args:
nw: Corey parameter for water.
krwend: value of krw at 1 - sgcr.
krwmax: maximal value at Sw=1. Default 1
"""
self.wateroil.add_corey_water(nw, krwend, krwmax)
[docs]
def add_corey_gas(self, ng: float = 2.0, krgend: float = 1.0) -> None:
"""Add krg data through the Corey parametrization
A column named 'krg' will be added. If it exists, it will
be replaced.
Args:
ng: Corey parameter for gas
krgend: value of krg at swl.
"""
self.gasoil.add_corey_gas(ng, krgend, krgmax=None)
[docs]
def add_LET_water(
self,
l: float = 2.0,
e: float = 2.0,
t: float = 2.0,
krwend: float = 1.0,
krwmax: Optional[float] = None,
) -> None:
"""Add krw data through LET parametrization
The LET model applies for sw < 1 - sgrw. For higher
water saturations, krw is linear between krwend and krwmax.
krwmax will be ignored if sorw is close to zero.
Args:
l: LET parameter
e: LET parameter
t: LET parameter
krwend: value of krw at 1 - sorw
krwmax: maximal value at Sw=1. Default 1
"""
self.wateroil.add_LET_water(l, e, t, krwend, krwmax)
[docs]
def add_LET_gas(
self, l: float = 2.0, e: float = 2.0, t: float = 2.0, krgend: float = 1.0
) -> None:
"""Add krg data through the LET parametrization
A column named 'krg' will be added. If it exists, it will
be replaced.
Args:
l: LET parameter
e: LET parameter
t: LET parameter
krgend: value of krg at swl
"""
self.gasoil.add_LET_gas(l, e, t, krgend, krgmax=None)
[docs]
@is_documented_by(WaterOil.add_simple_J)
def add_simple_J(
self,
a: float = 5.0,
b: float = -1.5,
poro_ref: float = 0.25,
perm_ref: float = 100.0,
drho: float = 300.0,
g: float = 9.81,
) -> None:
"""Add a simple J-function, handed over to the WaterOil object"""
self.wateroil.add_simple_J(a, b, poro_ref, perm_ref, drho, g)
[docs]
@is_documented_by(WaterOil.add_simple_J_petro)
def add_simple_J_petro(
self,
a: float,
b: float,
poro_ref: float = 0.25,
perm_ref: float = 100.0,
drho: float = 300.0,
g: float = 9.81,
) -> None:
"""Add a simple petrophysical variant of the J-function, handed
over to the WaterOil object"""
self.wateroil.add_simple_J_petro(a, b, poro_ref, perm_ref, drho, g)
[docs]
def SWFN(self, header: bool = True, dataincommentrow: bool = True):
"""Produce SWFN input to Eclipse
The columns sw, krw and pc are outputted and formatted
accordingly.
Meta-information for the tabulated data are printed
as Eclipse comments.
Args:
header: boolean for whether the SWFN string should be emitted.
If you have multiple satnums, you should have True only
for the first (or False for all, and emit the SWFN yourself).
Defaults to True.
dataincommentrow: boolean for wheter metadata should be printed,
defaults to True.
"""
if self.fast:
crosspointcomment = ""
else:
crosspoint_value = self.crosspoint()
if crosspoint_value is not None:
crosspointcomment = f"-- krw = krg @ sw={crosspoint_value:1.5f}\n"
else:
crosspointcomment = ""
return self.wateroil.SWFN(
header,
dataincommentrow,
swcomment=self.swcomment,
crosspointcomment=crosspointcomment,
)
[docs]
def SGFN(self, header: bool = True, dataincommentrow: bool = True):
"""Produce SGFN input for Eclipse reservoir simulator.
The columns sg and krg are outputted and formatted accordingly.
Meta-information for the tabulated data are printed as Eclipse comments.
Args:
header: boolean for whether the SGFN string should be emitted.
If you have multiple satnums, you should have True only
for the first (or False for all, and emit the SGFN yourself).
Defaults to True.
dataincommentrow: boolean for wheter metadata should be printed,
defaults to True.
"""
if self.fast:
crosspointcomment = ""
else:
crosspoint_value = self.crosspoint()
if crosspoint_value is not None:
crosspointcomment = f"-- krw = krg @ sw={crosspoint_value:1.5f}\n"
else:
crosspointcomment = ""
return self.gasoil.SGFN(
header,
dataincommentrow,
sgcomment=self.sgcomment,
crosspointcomment=crosspointcomment,
)
[docs]
def crosspoint(self) -> Optional[float]:
"""Calculate the sw value where krg == krw.
Accuracy of this crosspoint depends on the resolution chosen
when initializing the saturation range (it uses linear
interpolation to solve for the zero)
Returns:
The gas saturation where krw == krg, for relperm linearly
interpolated in water saturation.
"""
if not {"SW", "KRW"}.issubset(self.wateroil.table.columns):
logger.warning("Can't compute crosspoint when KRW is not present")
return None
if not {"SL", "KRG"}.issubset(self.gasoil.table.columns):
logger.warning("Can't compute crosspoint when KRG is not present")
return None
dframe = pd.concat(
[self.wateroil.table[["SW", "KRW"]], self.gasoil.table[["SL", "KRG"]]],
sort=False,
)
# The "SL" column in the GasOil object corresponds exactly to "SW" in WaterOil
# but since they are floating point, we do not want to "merge" dataframes on it,
# rather concatenate and let linear interpolation fill in values.
dframe["SW"] = dframe["SW"].fillna(value=0)
dframe["SL"] = dframe["SL"].fillna(value=0)
dframe["sat"] = dframe["SL"] + dframe["SW"]
dframe = (
dframe.set_index("sat")
.sort_index()
.interpolate(method="slinear")
.dropna()
.reset_index()
)
return crosspoint(dframe, "sat", "KRW", "KRG")
[docs]
def plotkrwkrg(
self,
mpl_ax=None,
color: str = "blue",
alpha: float = 1,
linewidth: int = 1,
linestyle: str = "-",
marker: Optional[str] = None,
label: str = "",
logyscale: bool = False,
):
"""Plot krw and krg
If the argument 'mpl_ax' is not supplied, a new plot
window will be made. If supplied, it will draw on
the specified axis."""
# Lazy import of matplotlib for speed reasons.
import matplotlib
import matplotlib.pyplot as plt
if mpl_ax is None:
matplotlib.style.use("ggplot")
_, useax = plt.subplots()
else:
useax = mpl_ax
if logyscale:
useax.set_yscale("log")
useax.set_ylim([1e-8, 1])
self.wateroil.table.plot(
ax=useax,
x="SW",
y="KRW",
c=color,
alpha=alpha,
legend=None,
label=label,
linewidth=linewidth,
linestyle=linestyle,
marker=marker,
)
self.gasoil.table.plot(
ax=useax,
x="SL",
y="KRG",
c=color,
alpha=alpha,
label=None,
legend=None,
linewidth=linewidth,
linestyle=linestyle,
marker=marker,
)
plt.xlabel("SW")
if mpl_ax is None:
plt.show()
@property
def swirr(self) -> float:
"""Get the swirr, irreducible water saturation used for pc-init"""
return self.wateroil.swirr
@property
def swl(self) -> float:
"""Get the swl"""
return self.wateroil.swl
@property
def swcr(self) -> float:
"""Get the swcr"""
return self.wateroil.swcr
@property
def swcomment(self) -> str:
"""Get a string representation of the endpoints used for water"""
return self.wateroil.swcomment.replace("sorw", "sgrw")
@property
def sgcomment(self) -> str:
"""Get a string representation of the endpoints used for gas"""
return (
self.gasoil.sgcomment.replace("sorg", "sgrw")
.replace(", krgendanchor=sgrw", "")
.replace(", krgendanchor=", "")
)
@property
def krwcomment(self) -> str:
"""Get a string representation describing krw"""
return self.wateroil.krwcomment
@property
def krgcomment(self) -> str:
"""Get a string representation describing krg"""
return self.gasoil.krgcomment
@property
def tag(self) -> str:
"""Get the user configured tag"""
if self.wateroil.tag == self.gasoil.tag:
return self.wateroil.tag
raise ValueError("Internal tag-inconsistency in GasWater")