diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b1d9ecc..bba51bd60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased ### Added +- [#417] (https://github.com/equinor/flownet/pull/417) Added functionality to history match dissolved salts (TDS) in produced water. - [#404](https://github.com/equinor/flownet/pull/404) Added possibility for regional multipliers for permeability, porosity and bulkvolume multiplier. Current implementation allows for defining either one global multiplier, or a regional multipliers based on a region parameter extracted from an existing simulation model (typically FIPNUM, EQLNUM, SATNUM etc). The regional multiplier will be in addition to the per tube multipliers. New keys in config yaml are: porosity_regional_scheme (global, individual or regions_from_sim), porosity_regional (define prior same way as for other model parameters) and porosity_parameter_from_sim_model (name of region parameter in simulation model). The same three keys exists for permeability and bulkvolume_mult. - [#383](https://github.com/equinor/flownet/pull/383) Added option to either define a prior distribution for KRWMAX directly by using krwmax in the config yaml, or to let KRWMAX be calculated as KRWEND + delta. To do the latter, set krwmax_add_to_krwend to true, and then the prior distribution definition in the config yaml for krwmax will be interpreted as a prior distribution for the delta value to be added to KRWEND to get the KRWMAX. - [#386](https://github.com/equinor/flownet/pull/386) Expose FlowNet timeout to user. diff --git a/src/flownet/config_parser/_config_parser.py b/src/flownet/config_parser/_config_parser.py index 891625db9..d7c53443a 100644 --- a/src/flownet/config_parser/_config_parser.py +++ b/src/flownet/config_parser/_config_parser.py @@ -241,6 +241,58 @@ def _to_abs_path(path: Optional[str]) -> str: }, }, }, + "WSPR": { + MK.Type: types.NamedDict, + MK.Content: { + "rel_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + "min_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + }, + }, + "WSPT": { + MK.Type: types.NamedDict, + MK.Content: { + "rel_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + "min_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + }, + }, + "WSIR": { + MK.Type: types.NamedDict, + MK.Content: { + "rel_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + "min_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + }, + }, + "WSIT": { + MK.Type: types.NamedDict, + MK.Content: { + "rel_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + "min_error": { + MK.Type: types.Number, + MK.AllowNone: True, + }, + }, + }, }, }, "layers": { diff --git a/src/flownet/data/from_flow.py b/src/flownet/data/from_flow.py index f110be293..5983ed338 100644 --- a/src/flownet/data/from_flow.py +++ b/src/flownet/data/from_flow.py @@ -163,12 +163,16 @@ def _production_data(self) -> pd.DataFrame: - WGPR Well Gas Production Rate - WWPR Well Water Production Rate - WOPT Well Cumulative Oil Production - - WGPT Well Cumulative Gas Production Rate - - WWPT Well Cumulative Water Production Rate + - WGPT Well Cumulative Gas Production + - WWPT Well Cumulative Water Production - WBHP Well Bottom Hole Pressure - WTHP Well Tubing Head Pressure - WGIR Well Gas Injection Rate - WWIR Well Water Injection Rate + - WSPR Well Salt Production Rate + - WSIR Well Salt Injection Rate + - WSPT Well Cumulative Salt Production + - WSIT Well Cumulative Salt Injection - WSTAT Well status (OPEN, SHUT, STOP) - TYPE Well Type: "OP", "GP", "WI", "GI" - PHASE Main producing/injecting phase fluid: "OIL", "GAS", "WATER" @@ -191,6 +195,10 @@ def _production_data(self) -> pd.DataFrame: "WWIR", "WGIT", "WWIT", + "WSPR", + "WSIR", + "WSPT", + "WSIT", "WSTAT", ] diff --git a/src/flownet/realization/_schedule.py b/src/flownet/realization/_schedule.py index 7d324b6be..fa5ad852f 100644 --- a/src/flownet/realization/_schedule.py +++ b/src/flownet/realization/_schedule.py @@ -8,7 +8,7 @@ import pandas as pd from configsuite import ConfigSuite -from ._simulation_keywords import Keyword, COMPDAT, WCONHIST, WCONINJH, WELSPECS +from ._simulation_keywords import Keyword, COMPDAT, WCONHIST, WCONINJH, WELSPECS, WSALT from ..network_model import NetworkModel @@ -61,8 +61,27 @@ def _create_schedule(self): self._calculate_welspecs() self._calculate_wconhist() self._calculate_wconinjh() + self._calculate_wsalt() print("done.", flush=True) + def _calculate_wsalt(self): + """ + Helper Function that generates the WSALT keywords based on salt measurements. + + Returns: + Nothing + + """ + for _, value in self._df_production_data.iterrows(): + if value["WWIR"] > 0 and value["WSIR"] > 0: + self.append( + WSALT( + date=value["date"], + well_name=value["WELL_NAME"], + salt_concentration=value["WSIR"] / value["WWIR"], + ) + ) + def _calculate_compdat(self): """ Helper Function that generates the COMPDAT keywords based on geometrical information from the NetworkModel. @@ -214,6 +233,8 @@ def _calculate_wconhist(self): oil_total=value["WOPT"], water_total=value["WWPT"], gas_total=value["WGPT"], + salt_rate=value["WSPR"], + salt_total=value["WSPT"], bhp=value["WBHP"], thp=value["WTHP"], ) @@ -533,6 +554,23 @@ def get_vfp(self) -> Dict: return vfp_tables + def has_brine(self) -> bool: + """Helper function to determine whether the schedule has brine data. + + Returns: + True if non zero brine data found, otherwise False + """ + return ( + sum( + [ + kw.salt_concentration + for kw in self._schedule_items + if kw.name == "WSALT" + ] + ) + > 0 + ) + def get_nr_observations(self, training_set_fraction: float) -> int: """ Helper function to retrieve the number of unique observations in the training process. diff --git a/src/flownet/realization/_simulation_keywords.py b/src/flownet/realization/_simulation_keywords.py index 0e3276c2f..72ee75b1a 100644 --- a/src/flownet/realization/_simulation_keywords.py +++ b/src/flownet/realization/_simulation_keywords.py @@ -107,6 +107,8 @@ def __init__( oil_rate: float = np.nan, water_rate: float = np.nan, gas_rate: float = np.nan, + salt_rate: float = np.nan, + salt_total: float = np.nan, oil_total: float = np.nan, water_total: float = np.nan, gas_total: float = np.nan, @@ -123,6 +125,8 @@ def __init__( self.oil_rate: float = oil_rate self.water_rate: float = water_rate self.gas_rate: float = gas_rate + self.salt_rate: float = salt_rate + self.salt_total: float = salt_total self.oil_total: float = oil_total self.water_total: float = water_total self.gas_total: float = gas_total @@ -215,3 +219,25 @@ def __init__( self.pvt_table: str = pvt_table self.density_calc: str = density_calc self.fip: str = fip + + +class WSALT(Keyword): + """ + The WSALT keyword defines the salt concentration of the injected water. + + See the OPM Flow manual for further details. + + """ + + # pylint: disable=too-many-instance-attributes,too-many-arguments + + def __init__( + self, + date: datetime.date, + well_name: str, + salt_concentration: float = np.nan, + ): + super().__init__(date) + self.name = "WSALT" + self.well_name: str = well_name + self.salt_concentration: float = salt_concentration diff --git a/src/flownet/static/SUMMARY.inc b/src/flownet/static/SUMMARY.inc index 891e40a39..cc738fbde 100644 --- a/src/flownet/static/SUMMARY.inc +++ b/src/flownet/static/SUMMARY.inc @@ -151,3 +151,15 @@ WBHPH / WTHPH / + +-- Salt rates +WSPR +/ +WSIR +/ + +-- Salt cumulatives +WSPT +/ +WSIT +/ \ No newline at end of file diff --git a/src/flownet/templates/HISTORY_SCHEDULE.inc.jinja2 b/src/flownet/templates/HISTORY_SCHEDULE.inc.jinja2 index d11a7a7e1..9458481c9 100644 --- a/src/flownet/templates/HISTORY_SCHEDULE.inc.jinja2 +++ b/src/flownet/templates/HISTORY_SCHEDULE.inc.jinja2 @@ -40,6 +40,16 @@ WCONINJH / {%- endif %} +{%- if schedule.get_keywords(dates=date, kw_class="WSALT"): %} +WSALT +-- WELL SALT +-- NAME SALTCON +{%- for kw in schedule.get_keywords(dates=date, kw_class="WSALT"): %} + '{{ kw.well_name }}' {{ kw.salt_concentration }} / +{%- endfor %} +/ +{%- endif %} + {%- if date > startdate: %} DATES {{ date.strftime('%d %b %Y').upper() }} / diff --git a/src/flownet/templates/TEMPLATE_MODEL.DATA.jinja2 b/src/flownet/templates/TEMPLATE_MODEL.DATA.jinja2 index 0864f79aa..ff11a3c72 100644 --- a/src/flownet/templates/TEMPLATE_MODEL.DATA.jinja2 +++ b/src/flownet/templates/TEMPLATE_MODEL.DATA.jinja2 @@ -40,6 +40,10 @@ INCLUDE INCLUDE './include/RUNSPEC.inc' / +{% if schedule.has_brine() -%} +BRINE +{%- endif %} + ---- GRID ---- diff --git a/src/flownet/templates/observations.ertobs.jinja2 b/src/flownet/templates/observations.ertobs.jinja2 index 57fc400d3..6ad63ea78 100644 --- a/src/flownet/templates/observations.ertobs.jinja2 +++ b/src/flownet/templates/observations.ertobs.jinja2 @@ -24,6 +24,12 @@ SUMMARY_OBSERVATION WGPR_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.gas {%- if not isnan(kw.water_rate) and error_config.WWPR.rel_error is not none and error_config.WWPR.min_error is not none: %} SUMMARY_OBSERVATION WWPR_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.water_rate }}; ERROR = {{ [error_config.WWPR.rel_error * kw.water_rate, error_config.WWPR.min_error] | max }}; DATE = {{ date_formatted }}; KEY = WWPR:{{ kw.well_name }}; }; {%- endif %} +{%- if not isnan(kw.salt_total) and error_config.WSPT.rel_error is not none and error_config.WSPT.min_error is not none: %} +SUMMARY_OBSERVATION WSPT_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.salt_total }}; ERROR = {{ [error_config.WSPT.rel_error * kw.salt_total, error_config.WSPR.min_error] | max }}; DATE = {{ date_formatted }}; KEY = WSPR:{{ kw.well_name }}; }; +{%- endif %} +{%- if not isnan(kw.salt_rate) and error_config.WSPR.rel_error is not none and error_config.WSPR.min_error is not none: %} +SUMMARY_OBSERVATION WSPR_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.salt_rate }}; ERROR = {{ [error_config.WSPR.rel_error * kw.salt_rate, error_config.WSPR.min_error] | max }}; DATE = {{ date_formatted }}; KEY = WSPR:{{ kw.well_name }}; }; +{%- endif %} {%- if not isnan(kw.bhp) and error_config.WBHP.rel_error is not none and error_config.WBHP.min_error is not none : %} SUMMARY_OBSERVATION WBHP_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.bhp }}; ERROR = {{ [error_config.WBHP.rel_error * kw.bhp, error_config.WBHP.min_error] | max }}; DATE = {{ date_formatted }}; KEY = WBHP:{{ kw.well_name }}; }; {%- endif %} @@ -58,6 +64,12 @@ SUMMARY_OBSERVATION WGIT_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.tot {%- if not isnan(kw.total) and kw.inj_type == "WATER" and error_config.WWIT.rel_error is not none and error_config.WWIT.min_error is not none: %} SUMMARY_OBSERVATION WWIT_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.total }}; ERROR = {{ [error_config.WWIT.rel_error * kw.total, error_config.WWIT.min_error] | max }}; DATE = {{ date_formatted }}; KEY = WWIT:{{ kw.well_name }}; }; {%- endif %} +{%- if not isnan(kw.total) and kw.inj_type == "WATER" and error_config.WSIR.rel_error is not none and error_config.WSIR.min_error is not none: %} +SUMMARY_OBSERVATION WSIR_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.rate }}; ERROR = {{ [error_config.WSIR.rel_error * kw.rate, error_config.WSIR.min_error] | max }}; DATE = {{ date_formatted }}; KEY = WSIR:{{ kw.well_name }}; }; +{%- endif %} +{%- if not isnan(kw.total) and kw.inj_type == "WATER" and error_config.WSIT.rel_error is not none and error_config.WSIT.min_error is not none: %} +SUMMARY_OBSERVATION WSIT_{{ kw.well_name }}_{{ index_name }} { VALUE = {{ kw.total }}; ERROR = {{ [error_config.WSIT.rel_error * kw.total, error_config.WSIT.min_error] | max }}; DATE = {{ date_formatted }}; KEY = WSIT:{{ kw.well_name }}; }; +{%- endif %} {% endfor %} {%- endif %} diff --git a/src/flownet/templates/observations.yamlobs.jinja2 b/src/flownet/templates/observations.yamlobs.jinja2 index 35569132c..4bb2030b7 100644 --- a/src/flownet/templates/observations.yamlobs.jinja2 +++ b/src/flownet/templates/observations.yamlobs.jinja2 @@ -51,6 +51,23 @@ smry: {%- endif -%} {%- endfor %} {%- endif -%} + {%- if (error_config.WSPR.rel_error is not none) and (error_config.WSPR.min_error is not none) -%} + {%- for kw in schedule.get_keywords(kw_class="WCONHIST", well_name=well_name, ignore_nan="salt_rate", dates=dates[num_beginning_date:num_end_date]) -%} + {% if loop.first and not loop.last: %} + - key: WSPR:{{ well_name }} + observations: + {%- endif %} + - date: {{ kw.date.strftime('%Y-%m-%d') }} + value: {{ kw.salt_rate }} + error: {{ [error_config.WSPR.rel_error * kw.salt_rate, error_config.WSPR.min_error] | max }} + comment: + {%- if (kw.date > last_training_date) -%} + {{ " Test" }} + {%- else -%} + {{ " Training" }} + {%- endif -%} + {%- endfor %} + {%- endif -%} {%- if (error_config.WOPT.rel_error is not none) and (error_config.WOPT.min_error is not none) -%} {%- for kw in schedule.get_keywords(kw_class="WCONHIST", well_name=well_name, ignore_nan="oil_total", dates=dates[num_beginning_date:num_end_date]) -%} {% if loop.first and not loop.last: %} @@ -102,6 +119,23 @@ smry: {%- endif -%} {%- endfor %} {%- endif -%} + {%- if (error_config.WSPT.rel_error is not none) and (error_config.WSPT.min_error is not none) -%} + {%- for kw in schedule.get_keywords(kw_class="WCONHIST", well_name=well_name, ignore_nan="salt_total", dates=dates[num_beginning_date:num_end_date]) -%} + {% if loop.first and not loop.last: %} + - key: WSPT:{{ well_name }} + observations: + {%- endif %} + - date: {{ kw.date.strftime('%Y-%m-%d') }} + value: {{ kw.salt_total }} + error: {{ [error_config.WSPT.rel_error * kw.salt_total, error_config.WSPT.min_error] | max }} + comment: + {%- if (kw.date > last_training_date) -%} + {{ " Test" }} + {%- else -%} + {{ " Training" }} + {%- endif -%} + {%- endfor %} + {%- endif -%} {%- if (error_config.WBHP.rel_error is not none) and (error_config.WBHP.min_error is not none) -%} {%- for kw in schedule.get_keywords(kw_class="WCONHIST", well_name=well_name, ignore_nan="bhp", dates=dates[num_beginning_date:num_end_date]) -%} {% if loop.first and not loop.last: %} @@ -241,4 +275,38 @@ smry: {%- endif -%} {%- endfor %} {%- endif -%} + {%- if (error_config.WSIR.rel_error is not none) and (error_config.WSIR.min_error is not none) -%} + {%- for kw in schedule.get_keywords(kw_class="WCONINJH", well_name=well_name, ignore_nan="rate", dates=dates[num_beginning_date:num_end_date]) if kw.inj_type == "WATER" -%} + {% if loop.first and not loop.last: %} + - key: WSIR:{{ well_name }} + observations: + {%- endif %} + - date: {{ kw.date.strftime('%Y-%m-%d') }} + value: {{ kw.rate }} + error: {{ [error_config.WSIR.rel_error * kw.rate, error_config.WSIR.min_error] | max }} + comment: + {%- if (kw.date > last_training_date) -%} + {{ " Test" }} + {%- else -%} + {{ " Training" }} + {%- endif -%} + {%- endfor %} + {%- endif -%} + {%- if (error_config.WSIT.rel_error is not none) and (error_config.WSIT.min_error is not none) -%} + {%- for kw in schedule.get_keywords(kw_class="WCONHIST", well_name=well_name, ignore_nan="salt_total", dates=dates[num_beginning_date:num_end_date]) -%} + {% if loop.first and not loop.last: %} + - key: WSIT:{{ well_name }} + observations: + {%- endif %} + - date: {{ kw.date.strftime('%Y-%m-%d') }} + value: {{ kw.salt_total }} + error: {{ [error_config.WSIT.rel_error * kw.salt_total, error_config.WSIT.min_error] | max }} + comment: + {%- if (kw.date > last_training_date) -%} + {{ " Test" }} + {%- else -%} + {{ " Training" }} + {%- endif -%} + {%- endfor %} + {%- endif -%} {%- endfor -%} diff --git a/tests/test_check_obsfiles_ert_yaml.py b/tests/test_check_obsfiles_ert_yaml.py index 9f2e9befc..726c753b0 100644 --- a/tests/test_check_obsfiles_ert_yaml.py +++ b/tests/test_check_obsfiles_ert_yaml.py @@ -268,6 +268,30 @@ def test_check_obsfiles_ert_yaml() -> None: config.flownet.data_source.simulation.vectors.WWIT.min_error = _MIN_ERROR config.flownet.data_source.simulation.vectors.WWIT.rel_error = _REL_ERROR + config.flownet.data_source.simulation.vectors.WSPR = collections.namedtuple( + "WSPR", "min_error" + ) + config.flownet.data_source.simulation.vectors.WSPR.min_error = _MIN_ERROR + config.flownet.data_source.simulation.vectors.WSPR.rel_error = _REL_ERROR + + config.flownet.data_source.simulation.vectors.WSPT = collections.namedtuple( + "WSPT", "min_error" + ) + config.flownet.data_source.simulation.vectors.WSPT.min_error = _MIN_ERROR + config.flownet.data_source.simulation.vectors.WSPT.rel_error = _REL_ERROR + + config.flownet.data_source.simulation.vectors.WSIR = collections.namedtuple( + "WSIR", "min_error" + ) + config.flownet.data_source.simulation.vectors.WSIR.min_error = _MIN_ERROR + config.flownet.data_source.simulation.vectors.WSIR.rel_error = _REL_ERROR + + config.flownet.data_source.simulation.vectors.WSIT = collections.namedtuple( + "WSIT", "min_error" + ) + config.flownet.data_source.simulation.vectors.WSIT.min_error = _MIN_ERROR + config.flownet.data_source.simulation.vectors.WSIT.rel_error = _REL_ERROR + config.flownet.data_source.resampling = _RESAMPLING # Load production