diff --git a/.gitignore b/.gitignore index e46e4d29..b4e58150 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/pyscal/ docs/modules.rst docs/pyscal.rst diff --git a/docs/images/gasoil-condensate-cid2.png b/docs/images/gasoil-condensate-cid2.png new file mode 100644 index 00000000..115eade7 Binary files /dev/null and b/docs/images/gasoil-condensate-cid2.png differ diff --git a/docs/images/gaswater-condensate-icd2.png b/docs/images/gaswater-condensate-icd2.png new file mode 100644 index 00000000..e0c3dcc3 Binary files /dev/null and b/docs/images/gaswater-condensate-icd2.png differ diff --git a/docs/images/wateroil-condensate-idc2.png b/docs/images/wateroil-condensate-idc2.png new file mode 100644 index 00000000..f4a944a9 Binary files /dev/null and b/docs/images/wateroil-condensate-idc2.png differ diff --git a/docs/images/wet-condensate_field_illustration.png b/docs/images/wet-condensate_field_illustration.png new file mode 100755 index 00000000..db81f34b Binary files /dev/null and b/docs/images/wet-condensate_field_illustration.png differ diff --git a/docs/make_plots.py b/docs/make_plots.py index 2dda79e2..ea40ad0a 100644 --- a/docs/make_plots.py +++ b/docs/make_plots.py @@ -5,6 +5,7 @@ import matplotlib.pyplot as plt from pyscal import GasOil, GasWater, WaterOil +from pyscal.factory import create_water_oil_gas IMG_DIR = Path(__file__).absolute().parent / "images" @@ -24,6 +25,9 @@ def main() -> None: "gaswater-icd2", "gasoil-cid2", "gaswater-dci1", + "gasoil-condensate-cid2", + "wateroil-condensate-idc2", + "gaswater-condensate-icd2", "gaswater-co2-icd2", "gaswater-paleogas-dci3", "wateroil-paleooil-idc1", @@ -437,6 +441,317 @@ def make_gasoil_cdi2(show: bool = True) -> None: plt.show() +def make_gasoil_condensate_cid2(show: bool = True) -> None: + plt.xkcd() + _, axes = plt.subplots() + swl = 0.1 + sgcr = 0.15 + sgro = 0.15 + sorg = 0.15 + krgend = 1 + krgmax = 1 + krowend = 0.8 + krogend = 0.8 + kromax = 1 + krgendanchor = "" + nw = 2 + now = 3 + ng = 3 + nog = 3 + wog = create_water_oil_gas( + { + "nw": nw, + "now": now, + "ng": ng, + "nog": nog, + "krowend": krowend, + "krogend": krogend, + "kromax": kromax, + "krgend": krgend, + "krgmax": krgmax, + "krgendanchor": krgendanchor, + "swl": swl, + "sgcr": sgcr, + "sgro": sgro, + "sorg": sorg, + } + ) + wog.gasoil.table.plot( + ax=axes, x="SG", y="KRG", c="red", alpha=1, label="KRG", linewidth=2 + ) + wog.gasoil.table.plot( + ax=axes, x="SG", y="KROG", c="green", alpha=1, label="KROG", linewidth=2 + ) + plt.ylim([-0.02, 1]) + plt.xlim([-0.01, 1]) + plt.xticks([0, 1]) + plt.yticks([0, 1]) + axes.annotate( + "KROGEND", + xy=(sgro, krogend), + arrowprops={"arrowstyle": "->"}, + xytext=(sgro + 0.1, krogend - 0.05), + ) + axes.annotate( + "KROMAX", + xy=(0, kromax), + arrowprops={"arrowstyle": "->"}, + xytext=(0.1, kromax - 0.1), + ) + axes.annotate( + "KRGEND", + xy=(1 - swl, krgend - 0.01), + arrowprops={"arrowstyle": "->"}, + xytext=(1 - 0.35, krgend - 0.05), + ) + axes.text(0.03, 0.04, "SGRO") + axes.annotate( + "", xy=(0, 0.02), xytext=(sgro, 0.02), arrowprops={"arrowstyle": "<->"} + ) + axes.text(0.03, 0.14, "SGCR") + axes.annotate( + "", xy=(0, 0.12), xytext=(sgcr, 0.12), arrowprops={"arrowstyle": "<->"} + ) + plt.xlabel("SG", labelpad=-10) + axes.annotate( + "", + xy=(1 - sorg - swl, 0.08), + xytext=(1 - swl, 0.08), + arrowprops={"arrowstyle": "<->"}, + ) + axes.text(1 - sorg - swl + 0.02, 0.1, "SORG") + axes.annotate( + "", xy=(1 - swl, 0.02), xytext=(1, 0.02), arrowprops={"arrowstyle": "<->"} + ) + axes.text(1 - swl + 0.02, 0.04, "SWL") + axes.legend(loc="upper center") + + # Krog endpoint and critical saturations: + plt.vlines(sgro, ymin=0, ymax=krogend, colors="black", linestyles="dashed") + + # Initial state and saturation direction: + plt.vlines(1 - swl, ymin=0, ymax=1, colors="darkorange", linestyles="dashed") + axes.arrow( + 1 - swl - 0.03, + krgend - 0.03, + -0.08, + -0.2, + head_width=0.025, + fill=True, + facecolor="darkorange", + ) + axes.arrow( + 1 - swl - 0.03, + 0.03, + -0.13, + 0.001, + head_width=0.025, + fill=True, + facecolor="darkorange", + ) + if show: + plt.show() + + +def make_wateroil_condensate_idc2(show: bool = True) -> None: + plt.xkcd() + _, axes = plt.subplots() + swl = 0.1 + swcr = 0.1 + sorw = 0.2 + krwend = 0.8 + krwmax = 1 + krowend = 0.85 + kromax = 1 + nw = 4 + now = 1.8 + ng = 3 + nog = 3 + wog = create_water_oil_gas( + { + "nw": nw, + "now": now, + "ng": ng, + "nog": nog, + "krowend": krowend, + "kromax": kromax, + "krwend": krwend, + "swl": swl, + "sorw": sorw, + "swcr": swcr, + } + ) + wog.wateroil.table.plot( + ax=axes, x="SW", y="KRW", c="blue", alpha=1, label="KRW", linewidth=2 + ) + wog.wateroil.table.plot( + ax=axes, x="SW", y="KROW", c="green", alpha=1, label="KROW", linewidth=2 + ) + plt.ylim([-0.02, 1.02]) + plt.xlim([-0.02, 1.02]) + plt.xticks([0, 1]) + plt.yticks([0, 1]) + axes.annotate( + "KROWEND", + xy=(swl, krowend), + arrowprops={"arrowstyle": "->"}, + xytext=(swl, krowend + 0.1), + ) + axes.annotate( + "KRWEND", + xy=(1 - sorw, krwend), + arrowprops={"arrowstyle": "->"}, + xytext=(1 - 0.15, krwend - 0.02), + ) + axes.annotate( + "KRWMAX", + xy=(1, krwmax), + arrowprops={"arrowstyle": "->"}, + xytext=(1 - 0.3, krwmax - 0.04), + ) + axes.annotate( + "SWL≈SWCR", + xy=(swl, 0), + arrowprops={"arrowstyle": "->"}, + xytext=(swl - 0.055, 0 + 0.14), + ) + plt.xlabel("SW", labelpad=-10) + axes.legend(loc="upper center") + axes.annotate( + "", xy=(1 - sorw, 0.02), xytext=(1, 0.02), arrowprops={"arrowstyle": "<->"} + ) + axes.text(1 - sorw + 0.04, 0.04, "SORW") + + # Krw endpoint and critical saturations: + plt.vlines(1 - sorw, ymin=0, ymax=krwend, colors="black", linestyles="dashed") + + # Initial state and saturation direction: + plt.vlines(swl, ymin=0, ymax=1, colors="darkorange", linestyles="dashed") + axes.arrow( + swl + 0.03, + krowend - 0.01, + 0.05, + -0.09, + head_width=0.025, + fill=True, + facecolor="darkorange", + ) + axes.arrow( + swl + 0.03, + 0.03, + 0.05, + 0.01, + head_width=0.025, + fill=True, + facecolor="darkorange", + ) + if show: + plt.show() + + +def make_gaswater_condensate_icd2(show: bool = True) -> None: + plt.xkcd() + _, axes = plt.subplots() + swl = 0.1 + swcr = 0.1 + sgcr = 0.15 + sgrw = 0.15 + krgend = 1 + krgmax = 1 + krwend = 0.8 + krwmax = 1 + nw = 4 + now = 3 + ng = 3 + nog = 3 + wog = create_water_oil_gas( + { + "nw": nw, + "now": now, + "ng": ng, + "nog": nog, + "krwend": krwend, + "krwmax": krwmax, + "krgend": krgend, + "krgmax": krgmax, + "swl": swl, + "swcr": swcr, + "sgcr": sgcr, + "sgrw": sgrw, + } + ) + wog.wateroil.table.plot( + ax=axes, x="SW", y="KRW", c="blue", alpha=1, label="KRW", linewidth=2 + ) + wog.gasoil.table.plot( + ax=axes, x="SL", y="KRG", c="red", alpha=1, label="KRG", linewidth=2 + ) + plt.xlim([-0.02, 1.01]) + plt.ylim([-0.02, 1]) + plt.xticks([0, 1]) + plt.yticks([0, 1]) + axes.annotate( + "KRGEND", + xy=(swl, krgend), + arrowprops={"arrowstyle": "->"}, + xytext=(swl + 0.1, krgend - 0.1), + ) + axes.annotate( + "KRWEND", + xy=(1 - sgrw, krwend), + arrowprops={"arrowstyle": "->"}, + xytext=(1 - 0.13, krwend - 0.15), + ) + axes.annotate( + "KRWMAX", + xy=(1 - 0.01, krwmax), + arrowprops={"arrowstyle": "->"}, + xytext=(1 - 0.23, krwmax - 0.05), + ) + axes.annotate( + "SWL≈SWCR", + xy=(swl, 0), + arrowprops={"arrowstyle": "->"}, + xytext=(swl - 0.055, 0 + 0.14), + ) + plt.xlabel("SW", labelpad=-10) + axes.legend(loc="upper center") + axes.text(1 - sgrw + 0.03, 0.04, "SGRW") + axes.annotate( + "", xy=(1 - sgrw, 0.02), xytext=(1, 0.02), arrowprops={"arrowstyle": "<->"} + ) + axes.text(1 - sgcr + 0.03, 0.14, "SGCR") + axes.annotate( + "", xy=(1 - sgcr, 0.12), xytext=(1, 0.12), arrowprops={"arrowstyle": "<->"} + ) + + # Krw endpoint and critical saturations: + plt.vlines(1 - sgcr, ymin=0, ymax=krwend, colors="black", linestyles="dashed") + + # Initial state and saturation direction: + plt.vlines(swl, ymin=0, ymax=1, colors="darkorange", linestyles="dashed") + axes.arrow( + swl + 0.03, + krgend - 0.03, + 0.08, + -0.2, + head_width=0.025, + fill=True, + facecolor="darkorange", + ) + axes.arrow( + swl + 0.03, + 0.03, + 0.1, + 0.005, + head_width=0.025, + fill=True, + facecolor="darkorange", + ) + if show: + plt.show() + + def make_gasoil_cid2(show: bool = True) -> None: plt.xkcd() _, axes = plt.subplots() diff --git a/docs/modelling.rst b/docs/modelling.rst index 1dac7d7c..f64bcfa6 100644 --- a/docs/modelling.rst +++ b/docs/modelling.rst @@ -133,10 +133,84 @@ dropout can occur it should be modelled as a three-phase simulation. - KRW - -Wet gas/condensate field, oil displacing gas (CID2) ---------------------------------------------------- +Wet gas/condensate field (ICD2, CID2, IDC2) +------------------------------------------- -.. image:: images/gasoil-cid2.png +This field scenario is for a three-phase reservoir simulation allowing for condensate +dropout (oil displacing gas) in addition to water displacing both gas and oil. +The figure below illustrates different regions and processes in a wet gas/condensate field. + +.. image:: images/wet-condensate_field_illustration.png + :width: 600 + +The following processes are associated with the regions depicted in the figure above: + +.. list-table:: + :widths: 25 25 + :header-rows: 1 + + * - Region + - Process + * - 1 + - ICD2 + * - 2 + - ICD2 & CID2 + * - 3 + - CID2 & IDC2 + +Three-phase consistency requirements: + +- **SWL**: The same input must be used for all three flow scenarios. +- The following relationships must hold: **SGRW** = **SGCR** = **SGRO** = **SORW** + + +**The processes are described in the following figures:** + +Water displacing gas (ICD2) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is the main process for a gas/condensate field. + +.. image:: images/gaswater-condensate-icd2.png + :width: 600 + +.. list-table:: + :widths: 25 25 50 + :header-rows: 1 + + * - Pyscal + - Eclipse + - Comment + * - SWL + - SWL + - Initial state + * - SWCR + - SWCR + - Critical water, when water becomes mobile. Usually set epsilon + higher than SWL. + * - SGRW + - SGCR + - Trapped gas/residual gas. SGCR must be set equal to SGRW + * - SGL + - SGL + - Set to zero + * - KRGEND + - KRG + - Usually 1 + * - + - KRGR + - Not to be used. + * - KRWEND + - KRWR + - + * - KRWMAX + - KRW + - Usually 1 + +Oil displacing gas (CID2) +~~~~~~~~~~~~~~~~~~~~~~~~~ +This is the process for condensate dropout. + +.. image:: images/gasoil-condensate-cid2.png :width: 600 .. list-table:: @@ -161,12 +235,49 @@ Wet gas/condensate field, oil displacing gas (CID2) * - KRGMAX - - Not used. Leave defaulted at 1 - * - KROEND + * - KROGEND - KROR - - Kro at trapped gas + - Kro at trapped gas in a three-phase system * - KROMAX - KRO + - Usually set to 1 + + +Water displacing oil (IDC2) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: images/wateroil-condensate-idc2.png + :width: 600 + +.. list-table:: + :widths: 25 25 50 + :header-rows: 1 + + * - Pyscal + - Eclipse + - Comment + * - SWL + - SWL - + * - SWCR + - SWCR + - Critical water. Equal to SWL or SWL + 0.01 if this improves simulation performance + * - SORW + - SOWCR + - Residual oil + * - KROWEND + - KRO + - kro @ SWL in in a three-phase system + * - + - KRORW + - Not used in Pyscal (kro @ SWCR) + * - KRWEND + - KRWR + - krw @ SORW + * - KRWMAX + - KRW + - Usually set to 1 + .. |CO2| replace:: CO\ :sub:`2`\ @@ -236,7 +347,7 @@ Process is analoguous to "Dry gas field". - KRWR - Usually set to 1 -Oil paleo zone - IDC1 +Oil paleo zone (IDC1) --------------------- In an oil paleo zone, the saturation starts at `sorw`. `socr` (critical oil @@ -272,7 +383,7 @@ interpreted as residual water. - KRW - Usually set to 1 -Gas paleo zone - DCI3 +Gas paleo zone (DCI3) --------------------- .. image:: images/gaswater-paleogas-dci3.png diff --git a/src/pyscal/factory.py b/src/pyscal/factory.py index 6ab57919..0657611b 100644 --- a/src/pyscal/factory.py +++ b/src/pyscal/factory.py @@ -433,31 +433,34 @@ def create_water_oil_gas( if not isinstance(params, dict): raise TypeError("Parameter to create_water_oil_gas must be a dictionary") - check_deprecated(params) - # For case insensitiveness, all keys are converted to lower case: params = {key.lower(): value for (key, value) in params.items()} + # create separate params for wo and go systems, + # and represent krowend and krogend as kroend + params_wo = kro_endpoint_wo(params) + params_go = kro_endpoint_go(params) + wateroil: WaterOil | None - if sufficient_water_oil_params(params, failhard=False): - wateroil = create_water_oil(params, fast=fast) + if sufficient_water_oil_params(params_wo, failhard=False): + wateroil = create_water_oil(params_wo, fast=fast) else: logger.info("No wateroil parameters. Assuming only gas-oil in wateroilgas") wateroil = None # If the swl in WaterOil was initialized with swlheight, # ensure that result is passed on to the GasOil object: - if "swl" not in params and "swlheight" in params and wateroil is not None: - params["swl"] = wateroil.swl + if "swl" not in params_go and "swlheight" in params_go and wateroil is not None: + params_go["swl"] = wateroil.swl gasoil: GasOil | None - if sufficient_gas_oil_params(params, failhard=False): - gasoil = create_gas_oil(params, fast=fast) + if sufficient_gas_oil_params(params_go, failhard=False): + gasoil = create_gas_oil(params_go, fast=fast) else: logger.info("No gasoil parameters, assuming two-phase oilwatergas") gasoil = None - wog_init_params = slicedict(params, WOG_INIT) + wog_init_params = slicedict(params_wo, WOG_INIT) wateroilgas = WaterOilGas(**wog_init_params, fast=fast) # The wateroilgas __init__ has already created WaterOil and GasOil objects # but we overwrite the references with newly created ones, this factory function @@ -1407,6 +1410,78 @@ def check_deprecated(params: dict[str, Any]) -> None: # Block long deprecated parameters with an exception. # Remove this block in pyscal 1.x if "krowend" in params and "kroend" not in params: - raise ValueError("krowend is not supported by pyscal. Use kroend") + raise ValueError( + "krowend is not supported by pyscal for WaterOil and GasOil objects. " + "Use kroend" + ) if "krogend" in params and "kroend" not in params: - raise ValueError("krogend is not supported by pyscal. Use kroend") + raise ValueError( + "krogend is not supported by pyscal for WaterOil and GasOil objects. " + "Use kroend" + ) + + +def kro_endpoint_wo(params: dict[str, Any]) -> dict[str, Any]: + """ + Normalize parameters to be used for creating a WaterOil object: + - If 'krowend' is present, rename it to 'kroend'. + - If both 'krowend' and 'kroend' are provided, use the 'krowend' value + and log a warning. + - Always remove 'krogend' if present (not relevant for WaterOil). + + Args: + params: Dictionary of parameters. + + Returns: + params: Dictionary of parameters. + """ + params_copy = params.copy() + + if "krowend" in params_copy: + if "kroend" in params_copy: + logger.warning( + "Both 'krowend'=%r and 'kroend'=%r were provided; using the " + "'krowend' value for WaterOil construction.", + params_copy["krowend"], + params_copy["kroend"], + ) + # Overwrite any existing 'kroend' with the 'krowend' value, then drop 'krowend' + params_copy["kroend"] = params_copy.pop("krowend") + + # Remove GasOil key if it sneaks in + params_copy.pop("krogend", None) + + return params_copy + + +def kro_endpoint_go(params: dict[str, Any]) -> dict[str, Any]: + """ + Normalize parameters to be used for creating a GasOil object: + - If 'krogend' is present, rename it to 'kroend'. + - If both 'krogend' and 'kroend' are provided, use the 'krogend' value + and log a warning. + - Always remove 'krowend' if present (not relevant for GasOil). + + Args: + params: Dictionary of parameters. + + Returns: + params: Dictionary of parameters. + """ + params_copy = params.copy() + + if "krogend" in params_copy: + if "kroend" in params_copy: + logger.warning( + "Both 'krogend'=%r and 'kroend'=%r were provided; using the " + "'krogend' value for GasOil construction.", + params_copy["krogend"], + params_copy["kroend"], + ) + # Overwrite any existing 'kroend' with the 'krowend' value, then drop 'krowend' + params_copy["kroend"] = params_copy.pop("krogend") + + # Remove WaterOil key if it sneaks in + params_copy.pop("krowend", None) + + return params_copy diff --git a/tests/test_factory.py b/tests/test_factory.py index 5bec8962..d1389f24 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,5 +1,6 @@ """Test the PyscalFactory module""" +import logging import os from pathlib import Path @@ -658,12 +659,71 @@ def test_factory_wateroilgas(): assert wateroil.gasoil is None -def test_factory_wateroilgas_deprecated_krowgend(): - """Using long-time deprecated krowend and krogend will fail""" - with pytest.raises(ValueError): +def test_factory_wateroilgas_krowgend(): + """ + Test normalize krowend and krogend to kroend and put into + create_water_oil and create_oil_gas, respectively + Normalize WaterOil parameters to be used for creating a + WaterOil(look into swof) object: + - If 'krowend' is present, rename it to 'kroend'. + - If both 'krowend' and 'kroend' are provided, use the 'krowend' value + and log a warning. + - Always remove 'krogend' if present (not relevant for WaterOil). + """ + + wog = create_water_oil_gas( + { + "nw": 2, + "now": 3, + "ng": 1, + "nog": 2.5, + "krowend": 0.6, + "krogend": 0.7, + "kroend": 0.5, + } + ) + swof = wog.SWOF() + sgof = wog.SGOF() + sat_table_str_ok(swof) + sat_table_str_ok(sgof) + assert "Corey krg" in sgof + assert "Corey krog" in sgof + assert "kroend=0.7" in sgof + assert "Corey krw" in swof + assert "Corey krow" in swof + assert "kroend=0.6" in swof + assert "krogend" not in swof + assert "krowend" not in sgof + + check_table(wog.gasoil.table) + check_table(wog.wateroil.table) + + +def test_factory_wateroilgas_warning(caplog): + """ + Test for expected WARNINGS in logs + """ + with caplog.at_level(logging.WARNING): create_water_oil_gas( - {"nw": 2, "now": 3, "ng": 1, "nog": 2.5, "krowend": 0.6, "krogend": 0.7} + { + "nw": 2, + "now": 3, + "ng": 1, + "nog": 2.5, + "krowend": 0.6, + "krogend": 0.7, + "kroend": 0.5, + } ) + assert ( + "Both 'krowend'=0.6 and 'kroend'=0.5 were provided; " + "using the 'krowend' value for WaterOil construction." + ) in caplog.text + + assert ( + "Both 'krogend'=0.7 and 'kroend'=0.5 were provided; " + "using the 'krogend' value for GasOil construction." + ) in caplog.text def test_factory_wateroilgas_wo(): @@ -1068,7 +1128,10 @@ def test_check_deprecated_krowgend(): """Up until pyscal 0.5.x, krogend and krowend were parameters to the oil curve parametrization for WaterOil and GasOil. From pyscal 0.6.0, krogend and krowend are merged to kroend. - After pyscal 0.8 presence of krogend and krowend is a ValueError + After pyscal 0.8 presence of krogend and krowend is a ValueError. + After pyscal 0.16 presence krogend and krowend is accepted as + parameters for create_wateroilgas, but is still ValueError for + create_water_oil and create_gas_oil. """ with pytest.raises(ValueError): create_water_oil({"swl": 0.1, "nw": 2, "now": 2, "krowend": 0.4})