@@ -1231,6 +1231,102 @@ def test_schedule_multiple_dates_in_order(self, project_with_case_and_well):
12311231 assert jan_pos < feb_pos , "January should come before February"
12321232 assert feb_pos < jun_pos , "February should come before June"
12331233
1234+ def test_welsegs_compsegs_optional (self , project_with_case_and_well ):
1235+ """Regression for #14064: callers can suppress WELSEGS and/or COMPSEGS
1236+ without affecting other keywords (WSEGVALV/WSEGAICD/COMPDAT/WELSPECS).
1237+ """
1238+ project , case , timeline = project_with_case_and_well
1239+ well_paths = project .well_paths ()
1240+ assert len (well_paths ) >= 1
1241+
1242+ wp = well_paths [0 ]
1243+ timeline .add_tubing_event (
1244+ event_date = "2024-01-01" ,
1245+ well_path = wp ,
1246+ start_md = 0.0 ,
1247+ end_md = 2500.0 ,
1248+ inner_diameter = 0.15 ,
1249+ roughness = 1.0e-5 ,
1250+ )
1251+ timeline .add_perf_event (
1252+ event_date = "2024-01-01" ,
1253+ well_path = wp ,
1254+ start_md = 2000.0 ,
1255+ end_md = 2200.0 ,
1256+ diameter = 0.1 ,
1257+ state = "OPEN" ,
1258+ )
1259+ timeline .set_timestamp (timestamp = "2024-12-31" )
1260+
1261+ # Baseline (defaults) must still emit WELSEGS and COMPSEGS.
1262+ baseline = timeline .generate_schedule_text (eclipse_case = case )
1263+ assert "WELSEGS" in baseline
1264+ assert "COMPSEGS" in baseline
1265+
1266+ # Suppress both.
1267+ suppressed = timeline .generate_schedule_text (
1268+ eclipse_case = case , include_welsegs = False , include_compsegs = False
1269+ )
1270+ print (f"\n Schedule with WELSEGS/COMPSEGS suppressed:\n { suppressed } " )
1271+ assert "WELSEGS" not in suppressed , (
1272+ f"WELSEGS should be suppressed:\n { suppressed } "
1273+ )
1274+ assert "COMPSEGS" not in suppressed , (
1275+ f"COMPSEGS should be suppressed:\n { suppressed } "
1276+ )
1277+ # COMPDAT (perforation completions) is unrelated to the MSW flags.
1278+ assert "COMPDAT" in suppressed
1279+
1280+ # Independent toggling.
1281+ only_compsegs_off = timeline .generate_schedule_text (
1282+ eclipse_case = case , include_compsegs = False
1283+ )
1284+ assert "WELSEGS" in only_compsegs_off
1285+ assert "COMPSEGS" not in only_compsegs_off
1286+
1287+ only_welsegs_off = timeline .generate_schedule_text (
1288+ eclipse_case = case , include_welsegs = False
1289+ )
1290+ assert "WELSEGS" not in only_welsegs_off
1291+ assert "COMPSEGS" in only_welsegs_off
1292+
1293+ def test_keywords_grouped_across_wells (self , project_with_case_and_well ):
1294+ """Regression for #14063: WELSPECS / COMPDAT records for multiple wells on
1295+ the same date must appear under a single keyword header (not one block per well).
1296+ """
1297+ project , case , timeline = project_with_case_and_well
1298+ well_paths = project .well_paths ()
1299+ assert len (well_paths ) >= 2 , (
1300+ "Test requires at least two well paths in the fixture"
1301+ )
1302+
1303+ for wp in well_paths [:2 ]:
1304+ timeline .add_perf_event (
1305+ event_date = "2024-01-01" ,
1306+ well_path = wp ,
1307+ start_md = 2000.0 ,
1308+ end_md = 2200.0 ,
1309+ diameter = 0.1 ,
1310+ state = "OPEN" ,
1311+ )
1312+
1313+ timeline .set_timestamp (timestamp = "2024-12-31" )
1314+ schedule_text = timeline .generate_schedule_text (eclipse_case = case )
1315+ print (f"\n Schedule text for multi-well grouping:\n { schedule_text } " )
1316+
1317+ # Exactly one "WELSPECS\n" header for all wells.
1318+ welspecs_count = schedule_text .count ("WELSPECS\n " )
1319+ assert welspecs_count == 1 , (
1320+ f"WELSPECS should appear once for grouped output; got { welspecs_count } :\n { schedule_text } "
1321+ )
1322+
1323+ # Both well names must appear inside the WELSPECS block (between header and trailing '/' line).
1324+ welspecs_block = schedule_text .split ("WELSPECS\n " , 1 )[1 ].split ("\n /\n " , 1 )[0 ]
1325+ for wp in well_paths [:2 ]:
1326+ assert wp .name .replace (" " , "" ) in welspecs_block .replace (" " , "" ), (
1327+ f"Well { wp .name !r} missing from grouped WELSPECS block: { welspecs_block !r} "
1328+ )
1329+
12341330
12351331class TestKeywordEvents :
12361332 """Tests for well keyword event functionality."""
@@ -1428,6 +1524,53 @@ def test_keyword_event_schedule_output_multiple_keywords(
14281524 assert "WRFTPLT" in schedule_text , "Schedule should contain WRFTPLT keyword"
14291525 assert "DATES" in schedule_text , "Schedule should contain DATES keyword"
14301526
1527+ def test_wconhist_item_order_canonical (self , project_with_case_and_well ):
1528+ """Regression for #14065: WCONHIST items must appear in the Eclipse-defined
1529+ canonical order regardless of how keyword_data was constructed in Python.
1530+
1531+ Canonical WCONHIST order: WELL, STATUS, CMODE, ORAT, WRAT, GRAT, VFP_TABLE,
1532+ ALQ, THP, BHP, WGASRAT_HIS, NGLRAT_HIS.
1533+ """
1534+ project , case , timeline = project_with_case_and_well
1535+ well_path = project .well_paths ()[0 ]
1536+
1537+ # Intentionally non-canonical insertion order: BHP placed before ORAT/WRAT/GRAT.
1538+ timeline .add_well_keyword_event (
1539+ event_date = "2024-01-15" ,
1540+ well_path = well_path ,
1541+ keyword_name = "WCONHIST" ,
1542+ keyword_data = {
1543+ "WELL" : well_path .name ,
1544+ "STATUS" : "OPEN" ,
1545+ "CMODE" : "RESV" ,
1546+ "BHP" : 250.0 ,
1547+ "ORAT" : 3999.99 ,
1548+ "WRAT" : 0.01 ,
1549+ "GRAT" : 550678.44 ,
1550+ "VFP_TABLE" : 1 ,
1551+ },
1552+ )
1553+
1554+ schedule_text = timeline .generate_schedule_text (eclipse_case = case )
1555+ print (f"\n Schedule text for canonical-order check:\n { schedule_text } " )
1556+
1557+ assert "WCONHIST" in schedule_text
1558+ # Extract the WCONHIST record body (between the keyword and its terminating '/').
1559+ wconhist_block = schedule_text .split ("WCONHIST" , 1 )[1 ].split ("/" , 1 )[0 ]
1560+
1561+ orat_pos = wconhist_block .find ("3999.99" )
1562+ grat_pos = wconhist_block .find ("550678" )
1563+ bhp_pos = wconhist_block .find ("250" )
1564+ assert orat_pos >= 0 , "ORAT value missing from WCONHIST output"
1565+ assert grat_pos >= 0 , "GRAT value missing from WCONHIST output"
1566+ assert bhp_pos >= 0 , "BHP value missing from WCONHIST output"
1567+ assert orat_pos < bhp_pos , (
1568+ f"ORAT must precede BHP in canonical WCONHIST order; got block: { wconhist_block !r} "
1569+ )
1570+ assert grat_pos < bhp_pos , (
1571+ f"GRAT must precede BHP in canonical WCONHIST order; got block: { wconhist_block !r} "
1572+ )
1573+
14311574 def test_invalid_keyword_data_unsupported_type (self , project_with_case_and_well ):
14321575 """Test error handling for unsupported data types in keyword events."""
14331576 project , case , timeline = project_with_case_and_well
@@ -1848,3 +1991,55 @@ def test_keyword_event_type_inference(self, project_with_case_and_well):
18481991 )
18491992
18501993 assert event is not None , "Event with mixed types should be created"
1994+
1995+ def test_rptrst_mnemonic_output (self , project_with_case_and_well ):
1996+ """RPTRST/RPTSCHED are mnemonic-list keywords. bool True must emit a
1997+ bare KEY, int/float/str must emit KEY=VALUE, bool False must be omitted.
1998+ """
1999+ project , case , timeline = project_with_case_and_well
2000+ well_path = project .well_paths ()[0 ]
2001+
2002+ # Schedule generation requires at least one well event so a well path is
2003+ # selected for output; the assertions below target only the RPTRST block.
2004+ timeline .add_control_event (
2005+ event_date = "2024-01-01" ,
2006+ well_path = well_path ,
2007+ control_mode = "ORAT" ,
2008+ control_value = 1000.0 ,
2009+ oil_rate = 1000.0 ,
2010+ is_producer = True ,
2011+ )
2012+
2013+ timeline .add_keyword_event (
2014+ event_date = "2024-01-01" ,
2015+ keyword_name = "RPTRST" ,
2016+ keyword_data = {
2017+ "BASIC" : 2 ,
2018+ "DEN" : True ,
2019+ "ROCKC" : True ,
2020+ "RPORV" : True ,
2021+ "RFIP" : True ,
2022+ "FLOWS" : True ,
2023+ "NORST" : 1 ,
2024+ "FLORES" : True ,
2025+ "OBSOLETE" : False ,
2026+ },
2027+ )
2028+
2029+ schedule_text = timeline .generate_schedule_text (eclipse_case = case )
2030+ print (f"\n RPTRST mnemonic output:\n { schedule_text } " )
2031+
2032+ assert "RPTRST" in schedule_text
2033+ rptrst_block = schedule_text .split ("RPTRST" , 1 )[1 ].split ("/" , 1 )[0 ]
2034+
2035+ # Keyed mnemonics rendered as KEY=VALUE.
2036+ assert "BASIC=2" in rptrst_block
2037+ assert "NORST=1" in rptrst_block
2038+ # Flag mnemonics rendered as bare tokens. Whitespace-bounded so we don't accept
2039+ # accidental substring matches like 'DEN' inside another token.
2040+ for flag in ("DEN" , "ROCKC" , "RPORV" , "RFIP" , "FLOWS" , "FLORES" ):
2041+ assert f" { flag } " in rptrst_block or rptrst_block .rstrip ().endswith (
2042+ f" { flag } "
2043+ ), f"flag { flag !r} missing from RPTRST output: { rptrst_block !r} "
2044+ # False-valued flag must be omitted entirely.
2045+ assert "OBSOLETE" not in rptrst_block
0 commit comments