Source code for pyscal.wateroilgas

"""Container object for one WaterOil and one GasOil object"""

from typing import Optional

import numpy as np
import pandas as pd

import pyscal
from pyscal.constants import SWINTEGERS
from pyscal.utils.string import comment_formatter, df2str

from .gasoil import GasOil
from .wateroil import WaterOil

logger = pyscal.getLogger_pyscal(__name__)


[docs]class WaterOilGas(object): """A representation of three-phase properties for oil-water-gas Use one object for each satnum. One WaterOil and one GasOil object will be created, with compatible saturation ranges. Access the class members 'wateroil' and 'gasoil' directly to add curves. All arguments to the initializer can be defaulted, and all are zero except h. Either the gasoil or the wateroil member is allowed to be None in case of only two phases present. It is allowed for functions to set these members to None after initialization, as done in SCALrecommendation.interpolate() Args: swirr: Irreducible water saturation for capillary pressure swl: First water saturation point in outputted tables. swcr: Critical water saturation, water is immobile below this sorw: Residual oil saturation 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, swcr: float = 0.0, sorw: float = 0.0, sorg: float = 0.0, sgcr: float = 0.0, h: Optional[float] = None, tag: str = "", fast: bool = False, ) -> None: """Sets up the saturation range for three phases""" self.fast: bool = fast self.wateroil: Optional[WaterOil] = WaterOil( swirr=swirr, swl=swl, swcr=swcr, sorw=sorw, h=h, tag=tag, fast=fast ) self.gasoil: Optional[GasOil] = GasOil( swirr=swirr, sgcr=sgcr, sorg=sorg, swl=swl, h=h, tag=tag, fast=fast )
[docs] def selfcheck(self) -> bool: """Run selfcheck on both wateroil and gasoil. Returns true only if both passes if both are present. If only wateroil or gasoil is present (the other being None, only the relevant is checked. """ if self.wateroil is not None and self.gasoil is not None: return self.wateroil.selfcheck() and self.gasoil.selfcheck() if self.wateroil is not None: return self.wateroil.selfcheck() if self.gasoil is not None: return self.gasoil.selfcheck() logger.error("Both wateroil and gasoil are None in WaterOilGas") return False
[docs] def SWOF(self, header: bool = True, dataincommentrow: bool = True) -> str: """Return a SWOF string. Delegated to the wateroil object""" if self.wateroil is None: logger.error("No WaterOil object in this WaterOilGas object") return "" if "KRW" not in self.wateroil.table or "KROW" not in self.wateroil.table: logger.error("Missing KRW/KROW curves in WaterOilGas object") return "" return self.wateroil.SWOF(header, dataincommentrow)
[docs] def SGOF(self, header: bool = True, dataincommentrow: bool = True) -> str: """Return a SGOF string. Delegated to the gasoil object.""" if self.gasoil is None: logger.error("No GasOil object in this WaterOilGas object") return "" if "KRG" not in self.gasoil.table or "KROG" not in self.gasoil.table: logger.error("Missing KRG/KROG curves in WaterOilGas object") return "" return self.gasoil.SGOF(header, dataincommentrow)
[docs] def SLGOF(self, header: bool = True, dataincommentrow: bool = True) -> str: """Return a SLGOF string. Delegated to the gasoil object.""" if self.gasoil is None: logger.error("No GasOil object in this WaterOilGas object") return "" if "KRG" not in self.gasoil.table or "KROG" not in self.gasoil.table: logger.error("Missing KRG/KROG in WaterOilGas object") return "" return self.gasoil.SLGOF(header, dataincommentrow)
[docs] def SGFN(self, header: bool = True, dataincommentrow: bool = True) -> str: """Return a SGFN string. Delegated to the gasoil object.""" if self.gasoil is None: logger.error("No GasOil object in this WaterOilGas object") return "" if "KRG" not in self.gasoil.table: logger.error("Missing KRG in WaterOilGas object") return "" return self.gasoil.SGFN(header, dataincommentrow)
[docs] def SWFN(self, header: bool = True, dataincommentrow: bool = True) -> str: """Return a SWFN string. Delegated to the wateroil object.""" if self.wateroil is None: logger.error("No WaterOil object in this WaterOilGas object") return "" if "KRW" not in self.wateroil.table: logger.error("Missing KRW in WaterOilGas object") return "" return self.wateroil.SWFN(header, dataincommentrow)
[docs] def SOF3(self, header: bool = True, dataincommentrow: bool = True) -> str: """Return a SOF3 string, combining data from the wateroil and gasoil objects. So - the oil saturation ranges from 0 to 1-swl. The saturation points from the WaterOil object is used to generate these """ if (self.wateroil is None or self.gasoil is None) or ( "KROW" not in self.wateroil.table or "KROG" not in self.gasoil.table ): logger.error("Both WaterOil and GasOil krow/krog is needed for SOF3") return "" self.threephaseconsistency() # Copy of the wateroil data: table = pd.DataFrame(self.wateroil.table[["SW", "KROW"]]) table["SO"] = 1 - table["SW"] # Copy of the gasoil data: gastable = pd.DataFrame(self.gasoil.table[["SG", "KROG"]]) gastable["SO"] = 1 - gastable["SG"] - self.wateroil.swl # Merge WaterOil and GasOil on oil saturation, interpolate for # missing data (potentially different sg- and sw-grids) sof3table = ( pd.concat([table, gastable], sort=True) .set_index("SO") .sort_index() .interpolate(method="slinear") .fillna(method="ffill") .fillna(method="bfill") .reset_index() ) sof3table["soint"] = list(map(round, sof3table["SO"] * SWINTEGERS)) sof3table.drop_duplicates("soint", inplace=True) # The 'so' column has been calculated from floating point numbers # and the zero value easily becomes a negative zero, circumvent this: zerorow = np.isclose(sof3table["SO"], 0.0) sof3table.loc[zerorow, "SO"] = abs(sof3table.loc[zerorow, "SO"]) string = "" if header: string += "SOF3\n" wo_tag = comment_formatter(self.wateroil.tag) go_tag = comment_formatter(self.gasoil.tag) if wo_tag != go_tag: string += wo_tag string += go_tag else: # Only print once if they are equal string += wo_tag string += "-- pyscal: " + str(pyscal.__version__) + "\n" if dataincommentrow: string += self.wateroil.swcomment string += self.gasoil.sgcomment string += self.wateroil.krowcomment string += self.gasoil.krogcomment width = 10 string += ( "-- " + "SO".ljust(width - 3) + "KROW".ljust(width) + "KROG".ljust(width) + "\n" ) string += df2str(sof3table[["SO", "KROW", "KROG"]]) string += "/\n" return string
@property def swirr(self) -> float: """Get the swirr used for the WaterOil object""" if self.wateroil is None: raise ValueError("wateroil is None in WaterOilGas object") return self.wateroil.swirr @property def swl(self) -> float: """Get the swl used for the WaterOil object""" if self.wateroil is None: raise ValueError("wateroil is None in WaterOilGas object") return self.wateroil.swl @property def sorg(self) -> float: """Get the sorg used in the GasOil object""" if self.gasoil is None: raise ValueError("gasoil is None in WaterOilGas object") return self.gasoil.sorg @property def sorw(self) -> float: """Get the sorw used for the WaterOil object""" if self.wateroil is None: raise ValueError("wateroil is None in WaterOilGas object") return self.wateroil.sorw @property def tag(self) -> str: """Get the tag, a combination of the tags in WaterOil and GasOil only if they are different""" if self.wateroil is not None and self.gasoil is not None: if self.wateroil.tag == self.gasoil.tag: return self.wateroil.tag return self.wateroil.tag + " " + self.gasoil.tag if self.wateroil is not None: return self.wateroil.tag if self.gasoil is not None: return self.gasoil.tag return ""
[docs] def threephaseconsistency(self) -> bool: """Perform consistency checks on produced curves, similar to what Eclipse does at startup. If any (likely) errors are found, these are printed as warnings and this function returns False. "Errors" from this function should be treated as warnings, as false positives from this function should not have breaking consequences. """ # Eclipse errors: # 1: Error in saturation table number 1 at the maximum oil # saturation (0.9) krow and krog should both be equal the oil # relative permeability for a system with oil and connate # water only - but in this case they are different (krow=1.0 # and krog=0.93) if self.wateroil is None or self.gasoil is None: # Nothing here to check if we only have two phases return True wog_is_ok = True if not np.isclose( self.wateroil.table["KROW"].max(), self.gasoil.table["KROG"].max() ): logger.warning( "Eclipse will fail, max(KROW)=%g is not equal to max(KROG)=%g", self.wateroil.table["KROW"].max(), self.gasoil.table["KROG"].max(), ) wog_is_ok = False # 2: Inconsistent end points in saturation table 1 the maximum # gas saturation (0.91) plus the connate water saturation # (0.10) must not exceed 1.0 if self.gasoil.table["SG"].max() + self.wateroil.table["SW"].min() > 1.0: logger.warning("Eclipse will fail, max(SG) + swl > 1.0") wog_is_ok = False # 3: Warning: Consistency problem for gas phase endpoint (krgr > krg) # in grid cell (i, j, k) for saturation end-points krgr=1.0 # krg = 0.49. # 4: Warning: Consistency problem for oil phase endpoint (sgu > 1-swl) # in grid cell (i, j, k) for saturation endpoints sgu=0.78, # swl=0.45, (1-swl) = 0.55 return wog_is_ok