From 479b68a6cdc0bc1c3968dc56d8f6190d0f51dfed Mon Sep 17 00:00:00 2001 From: "Wouter J. de Bruin" <9119793+wouterjdb@users.noreply.github.com> Date: Tue, 20 Oct 2020 15:52:41 +0200 Subject: [PATCH] Add WELOPEN support in ecl2df COMPDAT parsing (#186) * Add WELOPEN support in ecl2df COMPDAT parsing * Closes #185 * Drop f-string for py2 compatibility * Change into a function * Add test for applywelopen * Improved readability * Fix nan to None * Add test with defined IJK WELOPEN * Type welLopen * Warning if lumped connections * Add test for invalid WELOPEN well * Move test, remove unneeded columns * Removed varitation in indentation test sim decks * Add comment for column name translation * Updated docstring for applywelopen * Add WELOPEN to bash script --- ecl2df/common.py | 1 + ecl2df/compdat.py | 115 +++++++++++++- ecl2df/opmkeywords/WELOPEN | 37 +++++ ecl2df/opmkeywords/runmetoupdate.sh | 1 + tests/test_compdat.py | 52 +++++++ tests/test_welopen.py | 228 ++++++++++++++++++++++++++++ 6 files changed, 430 insertions(+), 4 deletions(-) create mode 100644 ecl2df/opmkeywords/WELOPEN create mode 100644 tests/test_welopen.py diff --git a/ecl2df/common.py b/ecl2df/common.py index 08da970b1..90fc73ccb 100644 --- a/ecl2df/common.py +++ b/ecl2df/common.py @@ -54,6 +54,7 @@ "WCONINJE", "WCONINJH", "WCONPROD", + "WELOPEN", "WELSEGS", "WELSPECS", ]: diff --git a/ecl2df/compdat.py b/ecl2df/compdat.py index fb53ea1da..07c04791e 100644 --- a/ecl2df/compdat.py +++ b/ecl2df/compdat.py @@ -67,6 +67,7 @@ def deck2dfs(deck, start_date=None, unroll=True): """ compdatrecords = [] # List of dicts of every line in input file compsegsrecords = [] + welopenrecords = [] welsegsrecords = [] date = start_date # DATE column will always be there, but can contain NaN/None for kword in deck: @@ -105,6 +106,18 @@ def deck2dfs(deck, start_date=None, unroll=True): rec_data["WELL"] = wellname rec_data["DATE"] = date compsegsrecords.append(rec_data) + elif kword.name == "WELOPEN": + for rec in kword: + rec_data = parse_opmio_deckrecord(rec, "WELOPEN") + rec_data["DATE"] = date + if rec_data["STATUS"] not in ["OPEN", "SHUT", "STOP", "AUTO"]: + rec_data["STATUS"] = "SHUT" + logger.warning( + "WELOPEN status %s is not a valid " + "COMPDAT state. Using 'SHUT' instead." % rec_data["STATUS"] + ) + welopenrecords.append(rec_data) + elif kword.name == "WELSEGS": # First record contains meta-information for well # (opm deck returns default values for unspecified items.) @@ -126,18 +139,19 @@ def deck2dfs(deck, start_date=None, unroll=True): if "INFO_TYPE" in rec_data and rec_data["INFO_TYPE"] == "ABS": rec_data["SEGMENT_MD"] = rec_data["SEGMENT_LENGTH"] welsegsrecords.append(rec_data) - elif kword.name == "TSTEP": - logger.warning("Possible premature stop at first TSTEP") - break compdat_df = pd.DataFrame(compdatrecords) + welopen_df = pd.DataFrame(welopenrecords) if unroll and not compdat_df.empty: compdat_df = unrolldf(compdat_df, "K1", "K2") - compsegs_df = pd.DataFrame(compsegsrecords) + if not welopen_df.empty: + compdat_df = applywelopen(compdat_df, welopen_df) + compsegs_df = pd.DataFrame(compsegsrecords) welsegs_df = pd.DataFrame(welsegsrecords) + if unroll and not welsegs_df.empty: welsegs_df = unrolldf(welsegs_df, "SEGMENT1", "SEGMENT2") @@ -241,6 +255,99 @@ def unrolldf(dframe, start_column="K1", end_column="K2"): return unrolled +def applywelopen(compdat_df, welopen_df): + """Apply WELOPEN actions to the COMPDAT dataframe. + + Each record in the WELOPEN keyword acts as an operator on existing connections + in existing wells. + + Example: COMPDAT and WELOPEN keyword:: + + COMPDAT + 'OP1' 33 44 10 11 'OPEN' / + 'OP2' 66 44 10 11 'OPEN' / + / + WELOPEN + 'OP1' SHUT / + 'OP2' SHUT 66 44 10 / + / + + This deck would define two wells where OP1 and OP2 have two connected grid cells + each. Although the COMPDAT defines all connections to be open, WELOPEN overwrites + this: all connections in OP1 will be SHUT and in OP2 the upper connection will + be SHUT. + + WELOPEN can also be used at different dates and changes therefore the state of + connections without explicit use of the COMPDAT keyword. This function translates + WELOPEN actions into explicit additional COMPDAT definitions in the exported df. + + Args: + compdat_df (pd.DataFrame): Dataframe with unrolled COMPDAT data + welopen_df (pd.DataFrame): Dataframe with WELOPEN actions + + Returns: + pd.Dataframe, compdat_df now including WELOPEN actions + + """ + welopen_df = welopen_df.astype(object).where(pd.notnull(welopen_df), None) + for _, row in welopen_df.iterrows(): + if row["I"] and row["J"] and row["K"]: + previous_state = compdat_df[ + (compdat_df["WELL"] == row["WELL"]) + & (compdat_df["DATE"] <= row["DATE"]) + & (compdat_df["I"] == row["I"]) + & (compdat_df["J"] == row["J"]) + & (compdat_df["K1"] == row["K"]) + & (compdat_df["K2"] == row["K"]) + ].drop_duplicates(subset=["I", "J", "K1", "K2"], keep="last") + elif row["C1"] or row["C2"]: + logger.warning( + "Lumped connections are not supported in a WELOPEN keyword. " + "Skipping WELOPEN actions for lumped connections '%s' and/or '%s'" + % (row["C1"], row["C2"]) + ) + continue + elif not (row["I"] and row["J"] and row["K"]): + previous_state = compdat_df[ + (compdat_df["WELL"] == row["WELL"]) + & (compdat_df["DATE"] <= row["DATE"]) + ].drop_duplicates(subset=["I", "J", "K1", "K2"], keep="last") + else: + raise ValueError( + "A WELOPEN keyword contains data that could not be parsed. " + "(I=%s,J=%s,K=%s)" % (row["I"], row["J"], row["K"]) + ) + + if previous_state.empty: + raise ValueError( + "A WELOPEN keyword is not acting on any existing connection. " + "(I=%s,J=%s,K=%s)" % (row["I"], row["J"], row["K"]) + ) + + new_state = previous_state + + # The COMPDAT DataFrame uses COMPDAT_RENAMER and therefore uses "OP/SH" as a + # column name for the state of a well. WELOPEN uses "STATUS" for the state + # column name and therefore a translation step needs to be done. The + # underlying problem is that the opm-common definitions for the state of a + # well in COMPDAT and WELOPEN are not identical. These translation steps can + # be dropped when unity in the opm-common keyword definitions is reached. + new_state["OP/SH"] = row["STATUS"] + + new_state["DATE"] = row["DATE"] + + compdat_df = compdat_df.append(new_state) + + if not compdat_df.empty: + compdat_df = ( + compdat_df.sort_values(by=["DATE", "WELL"]) + .drop_duplicates(subset=["I", "J", "K1", "K2", "DATE"], keep="last") + .reset_index(drop=True) + ) + + return compdat_df + + def fill_parser(parser): """Set up sys.argv parsers. diff --git a/ecl2df/opmkeywords/WELOPEN b/ecl2df/opmkeywords/WELOPEN new file mode 100644 index 000000000..72c473800 --- /dev/null +++ b/ecl2df/opmkeywords/WELOPEN @@ -0,0 +1,37 @@ +{ + "name": "WELOPEN", + "sections": [ + "SCHEDULE" + ], + "items": [ + { + "name": "WELL", + "value_type": "STRING" + }, + { + "name": "STATUS", + "value_type": "STRING", + "default": "OPEN" + }, + { + "name": "I", + "value_type": "INT" + }, + { + "name": "J", + "value_type": "INT" + }, + { + "name": "K", + "value_type": "INT" + }, + { + "name": "C1", + "value_type": "INT" + }, + { + "name": "C2", + "value_type": "INT" + } + ] +} diff --git a/ecl2df/opmkeywords/runmetoupdate.sh b/ecl2df/opmkeywords/runmetoupdate.sh index 6911ba125..2cf4918d6 100644 --- a/ecl2df/opmkeywords/runmetoupdate.sh +++ b/ecl2df/opmkeywords/runmetoupdate.sh @@ -35,6 +35,7 @@ WCONHIST WCONINJE WCONINJH WCONPROD +WELOPEN WELSEGS WELSPECS " diff --git a/tests/test_compdat.py b/tests/test_compdat.py index dfa8bbcd7..2537628fd 100644 --- a/tests/test_compdat.py +++ b/tests/test_compdat.py @@ -181,6 +181,58 @@ def test_tstep(): assert "2001-05-07" in dates +def test_applywelopen(): + schstr = """ +DATES + 1 MAY 2001 / +/ + +COMPDAT + 'OP1' 33 110 31 31 'OPEN' / +/ +WELOPEN + 'OP1' 'SHUT' / +/ + +TSTEP + 1 / + +COMPDAT + 'OP2' 66 110 31 31 'OPEN' / +/ + +WELOPEN + 'OP1' 'OPEN' / +/ + +TSTEP + 2 3 / + +WELOPEN + 'OP1' 'POPN' / + 'OP2' 'SHUT' / +/ +""" + df = compdat.deck2dfs(EclFiles.str2deck(schstr))["COMPDAT"] + assert df.shape[0] == 5 + assert df["OP/SH"].nunique() == 2 + assert df["DATE"].nunique() == 3 + + schstr = """ +DATES + 1 MAY 2001 / +/ + +COMPDAT + 'OP1' 33 110 31 31 'OPEN' / +/ +WELOPEN + 'OP2' 'SHUT' / +/""" + with pytest.raises(ValueError): + compdat.deck2dfs(EclFiles.str2deck(schstr))["COMPDAT"] + + def test_unrollcompdatk1k2(): """Test unrolling of k1-k2 ranges in COMPDAT""" schstr = """ diff --git a/tests/test_welopen.py b/tests/test_welopen.py new file mode 100644 index 000000000..4e7f3b2ca --- /dev/null +++ b/tests/test_welopen.py @@ -0,0 +1,228 @@ +import datetime + +import pandas as pd + +import pytest + +from ecl2df import compdat +from ecl2df import EclFiles + +WELOPEN_CASES = [ + ( + """ + DATES + 1 MAY 2001 / + / + + COMPDAT + 'OP1' 33 110 31 31 'OPEN' / + / + + WELOPEN + 'OP1' 'SHUT' / + / + + TSTEP + 1 / + + COMPDAT + 'OP1' 34 111 32 32 'OPEN' / + / + + TSTEP + 2 3 / + + COMPDAT + 'OP1' 35 111 33 33 'SHUT' / + / + """, + pd.DataFrame( + { + "WELL": {0: "OP1", 1: "OP1", 2: "OP1"}, + "I": {0: 33, 1: 34, 2: 35}, + "J": {0: 110, 1: 111, 2: 111}, + "K1": {0: 31, 1: 32, 2: 33}, + "K2": {0: 31, 1: 32, 2: 33}, + "OP/SH": {0: "SHUT", 1: "OPEN", 2: "SHUT"}, + "DATE": { + 0: datetime.date(2001, 5, 1), + 1: datetime.date(2001, 5, 2), + 2: datetime.date(2001, 5, 7), + }, + } + ), + ), + ( + """ + DATES + 1 MAY 2001 / + / + + COMPDAT + 'OP1' 33 110 31 31 'OPEN' / + / + + WELOPEN + 'OP1' 'OPEN' / + / + + TSTEP + 1 / + + COMPDAT + 'OP1' 34 111 32 32 'OPEN' / + / + + TSTEP + 2 3 / + + COMPDAT + 'OP1' 35 111 33 33 'SHUT' / + / + """, + pd.DataFrame( + { + "WELL": {0: "OP1", 1: "OP1", 2: "OP1"}, + "I": {0: 33, 1: 34, 2: 35}, + "J": {0: 110, 1: 111, 2: 111}, + "K1": {0: 31, 1: 32, 2: 33}, + "K2": {0: 31, 1: 32, 2: 33}, + "OP/SH": {0: "OPEN", 1: "OPEN", 2: "SHUT"}, + "DATE": { + 0: datetime.date(2001, 5, 1), + 1: datetime.date(2001, 5, 2), + 2: datetime.date(2001, 5, 7), + }, + } + ), + ), + ( + """ + DATES + 1 MAY 2001 / + / + + COMPDAT + 'OP1' 33 110 31 31 'OPEN' / + 'OP2' 66 110 31 31 'OPEN' / + / + + WELOPEN + 'OP2' 'OPEN' / + / + + DATES + 2 MAY 2001 / + / + + COMPDAT + 'OP1' 34 111 32 32 'OPEN' / + / + WELOPEN + 'OP1' 'SHUT' / + / + + DATES + 3 MAY 2001 / + / + + WELOPEN + 'OP1' 'OPEN' / + 'OP2' 'SHUT' / + / + """, + pd.DataFrame( + { + "WELL": { + 0: "OP1", + 1: "OP2", + 2: "OP1", + 3: "OP1", + 4: "OP1", + 5: "OP1", + 6: "OP2", + }, + "I": {0: 33, 1: 66, 2: 33, 3: 34, 4: 33, 5: 34, 6: 66}, + "J": {0: 110, 1: 110, 2: 110, 3: 111, 4: 110, 5: 111, 6: 110}, + "K1": {0: 31, 1: 31, 2: 31, 3: 32, 4: 31, 5: 32, 6: 31}, + "K2": {0: 31, 1: 31, 2: 31, 3: 32, 4: 31, 5: 32, 6: 31}, + "OP/SH": { + 0: "OPEN", + 1: "OPEN", + 2: "SHUT", + 3: "SHUT", + 4: "OPEN", + 5: "OPEN", + 6: "SHUT", + }, + "DATE": { + 0: datetime.date(2001, 5, 1), + 1: datetime.date(2001, 5, 1), + 2: datetime.date(2001, 5, 2), + 3: datetime.date(2001, 5, 2), + 4: datetime.date(2001, 5, 3), + 5: datetime.date(2001, 5, 3), + 6: datetime.date(2001, 5, 3), + }, + } + ), + ), + ( + """ + DATES + 1 MAY 2001 / + / + + COMPDAT + 'OP1' 33 110 1 2 'OPEN' / + / + + WELOPEN + 'OP1' 'SHUT' 33 110 1 / + / + + DATES + 2 MAY 2001 / + / + + WELOPEN + 'OP1' 'SHUT' 33 110 2 / + / + + DATES + 3 MAY 2001 / + / + + WELOPEN + 'OP1' 'OPEN' / + / + """, + pd.DataFrame( + { + "WELL": {0: "OP1", 1: "OP1", 2: "OP1", 3: "OP1", 4: "OP1"}, + "I": {0: 33, 1: 33, 2: 33, 3: 33, 4: 33}, + "J": {0: 110, 1: 110, 2: 110, 3: 110, 4: 110}, + "K1": {0: 2, 1: 1, 2: 2, 3: 1, 4: 2}, + "K2": {0: 2, 1: 1, 2: 2, 3: 1, 4: 2}, + "OP/SH": {0: "OPEN", 1: "SHUT", 2: "SHUT", 3: "OPEN", 4: "OPEN"}, + "DATE": { + 0: datetime.date(2001, 5, 1), + 1: datetime.date(2001, 5, 1), + 2: datetime.date(2001, 5, 2), + 3: datetime.date(2001, 5, 3), + 4: datetime.date(2001, 5, 3), + }, + } + ), + ), +] + + +@pytest.mark.parametrize("test_input,expected", WELOPEN_CASES) +def test_welopen(test_input, expected): + """Test with WELOPEN present""" + deck = EclFiles.str2deck(test_input) + compdf = compdat.deck2dfs(deck)["COMPDAT"] + + columns_to_check = ["WELL", "I", "J", "K1", "K2", "OP/SH", "DATE"] + assert all(compdf[columns_to_check] == expected[columns_to_check])