Skip to content

Commit 271d111

Browse files
committed
Update rips module and proto files
1 parent 8126e6e commit 271d111

3 files changed

Lines changed: 245 additions & 15 deletions

File tree

docs/rips/generated/generated_classes.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3931,16 +3931,18 @@ def events(self) -> List[WellEvent]:
39313931
return self.children("Events", WellEvent)
39323932

39333933

3934-
def generate_schedule(self, eclipse_case_id: int=-1) -> DataContainerString:
3934+
def generate_schedule(self, eclipse_case: Optional[Reservoir]=None, include_welsegs: bool=True, include_compsegs: bool=True) -> DataContainerString:
39353935
"""
39363936
Generate Eclipse schedule text for all wells in the collection
39373937
39383938
Arguments:
3939-
eclipse_case_id (int): Eclipse Case ID
3939+
eclipse_case (Optional[Reservoir]): Eclipse Case
3940+
include_welsegs (bool): Include WELSEGS keyword in the exported schedule
3941+
include_compsegs (bool): Include COMPSEGS keyword in the exported schedule
39403942
Returns:
39413943
DataContainerString
39423944
"""
3943-
return self._call_pdm_method_return_value("GenerateSchedule", DataContainerString, eclipse_case_id=eclipse_case_id)
3945+
return self._call_pdm_method_return_value("GenerateSchedule", DataContainerString, eclipse_case=eclipse_case, include_welsegs=include_welsegs, include_compsegs=include_compsegs)
39443946

39453947

39463948
def set_timestamp(self, timestamp: str="2024-01-01") -> None:

docs/rips/tests/test_well_events.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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"\nSchedule 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"\nSchedule 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

12351331
class 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"\nSchedule 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"\nRPTRST 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

docs/rips/well_events.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from datetime import date, datetime
1111

1212
from .pdmobject import add_method
13-
from .resinsight_classes import Case
13+
from .resinsight_classes import EclipseCase
1414
from .generated.generated_classes import (
1515
KeywordEvent,
1616
WellEventKeyword,
@@ -121,15 +121,24 @@ def add_well_keyword_event(
121121
item_values = []
122122

123123
for name, value in keyword_data.items():
124+
# Handle bool before int (bool is subclass of int in Python).
125+
# bool semantics: True -> flag (emitted as bare KEY in mnemonic-list keywords,
126+
# ignored elsewhere); False -> the entry is dropped entirely.
127+
if isinstance(value, bool):
128+
if not value:
129+
continue
130+
item_names.append(name)
131+
item_types.append("FLAG")
132+
# The value is ignored for FLAG items but must be a non-empty string;
133+
# empty strings get dropped by the GRPC vector<string> serialization.
134+
item_values.append("1")
135+
continue
136+
124137
item_names.append(name)
125138

126139
if isinstance(value, str):
127140
item_types.append("STRING")
128141
item_values.append(value)
129-
elif isinstance(value, bool):
130-
# Handle bool before int (bool is subclass of int in Python)
131-
item_types.append("INT")
132-
item_values.append("1" if value else "0")
133142
elif isinstance(value, int):
134143
item_types.append("INT")
135144
item_values.append(str(value))
@@ -228,15 +237,24 @@ def add_keyword_event(
228237
item_values = []
229238

230239
for name, value in keyword_data.items():
240+
# Handle bool before int (bool is subclass of int in Python).
241+
# bool semantics: True -> flag (emitted as bare KEY in mnemonic-list keywords,
242+
# ignored elsewhere); False -> the entry is dropped entirely.
243+
if isinstance(value, bool):
244+
if not value:
245+
continue
246+
item_names.append(name)
247+
item_types.append("FLAG")
248+
# The value is ignored for FLAG items but must be a non-empty string;
249+
# empty strings get dropped by the GRPC vector<string> serialization.
250+
item_values.append("1")
251+
continue
252+
231253
item_names.append(name)
232254

233255
if isinstance(value, str):
234256
item_types.append("STRING")
235257
item_values.append(value)
236-
elif isinstance(value, bool):
237-
# Handle bool before int (bool is subclass of int in Python)
238-
item_types.append("INT")
239-
item_values.append("1" if value else "0")
240258
elif isinstance(value, int):
241259
item_types.append("INT")
242260
item_values.append(str(value))
@@ -263,7 +281,12 @@ def add_keyword_event(
263281

264282

265283
@add_method(WellEventTimeline)
266-
def generate_schedule_text(self: WellEventTimeline, eclipse_case: Case) -> str:
284+
def generate_schedule_text(
285+
self: WellEventTimeline,
286+
eclipse_case: EclipseCase,
287+
include_welsegs: bool = True,
288+
include_compsegs: bool = True,
289+
) -> str:
267290
"""Generate Eclipse schedule text for all wells in the collection.
268291
269292
The timeline is shared across all wells in the well path collection.
@@ -274,7 +297,13 @@ def generate_schedule_text(self: WellEventTimeline, eclipse_case: Case) -> str:
274297
text directly instead of a DataContainerString.
275298
276299
Arguments:
277-
eclipse_case (Case): Eclipse case to use for schedule generation.
300+
eclipse_case (EclipseCase): Eclipse case to use for schedule generation.
301+
include_welsegs (bool): When False, omit the WELSEGS keyword from the
302+
output. Other multi-segment-well keywords (COMPSEGS, WSEGVALV,
303+
WSEGAICD) are not affected. Defaults to True.
304+
include_compsegs (bool): When False, omit the COMPSEGS keyword from
305+
the output. WELSEGS, WSEGVALV, WSEGAICD are not affected.
306+
Defaults to True.
278307
279308
Returns:
280309
str: Eclipse schedule text containing DATES, COMPDAT, WELSEGS, WCONPROD, etc.
@@ -312,7 +341,11 @@ def generate_schedule_text(self: WellEventTimeline, eclipse_case: Case) -> str:
312341
print(schedule_text)
313342
```
314343
"""
315-
container = self.generate_schedule(eclipse_case_id=eclipse_case.id)
344+
container = self.generate_schedule(
345+
eclipse_case=eclipse_case,
346+
include_welsegs=include_welsegs,
347+
include_compsegs=include_compsegs,
348+
)
316349
if container and container.values:
317350
return "".join(container.values)
318351
return ""

0 commit comments

Comments
 (0)