#14065 Well Event: Emit keyword items in canonical Eclipse order

`add_well_keyword_event` previously emitted items in `keyword_data` dict
insertion order. Eclipse keywords such as WCONHIST are positional, so a
non-canonical order silently corrupted the simulator input.

`RifEventKeywordFormatter::formatKeyword` now walks the OPM
`ParserRecord` schema and emits items in canonical order, inserting
`1*` defaults for intermediate gaps and dropping trailing unspecified
items. Items that are not part of the keyword schema (e.g. mnemonic-list
keywords like RPTRST/RPTSCHED) are appended after the canonical block in
caller-supplied order to preserve existing behaviour.

Fixes #14065.
This commit is contained in:
Kristian Bendiksen committed 2026-05-28 08:21:17 +02:00
1 parent 8dad4bd410
commit 9ada4eed09
2 files changed
+109 -5

No files matched your search

@@ -1428,6 +1428,53 @@ class TestKeywordEvents:
assert "WRFTPLT" in schedule_text, "Schedule should contain WRFTPLT keyword"
assert "DATES" in schedule_text, "Schedule should contain DATES keyword"
def test_wconhist_item_order_canonical(self, project_with_case_and_well):
"""Regression for #14065: WCONHIST items must appear in the Eclipse-defined
canonical order regardless of how keyword_data was constructed in Python.
Canonical WCONHIST order: WELL, STATUS, CMODE, ORAT, WRAT, GRAT, VFP_TABLE,
ALQ, THP, BHP, WGASRAT_HIS, NGLRAT_HIS.
"""
project, case, timeline = project_with_case_and_well
well_path = project.well_paths()[0]
# Intentionally non-canonical insertion order: BHP placed before ORAT/WRAT/GRAT.
timeline.add_well_keyword_event(
event_date="2024-01-15",
well_path=well_path,
keyword_name="WCONHIST",
keyword_data={
"WELL": well_path.name,
"STATUS": "OPEN",
"CMODE": "RESV",
"BHP": 250.0,
"ORAT": 3999.99,
"WRAT": 0.01,
"GRAT": 550678.44,
"VFP_TABLE": 1,
},
)
schedule_text = timeline.generate_schedule_text(eclipse_case=case)
print(f"\nSchedule text for canonical-order check:\n{schedule_text}")
assert "WCONHIST" in schedule_text
# Extract the WCONHIST record body (between the keyword and its terminating '/').
wconhist_block = schedule_text.split("WCONHIST", 1)[1].split("/", 1)[0]
orat_pos = wconhist_block.find("3999.99")
grat_pos = wconhist_block.find("550678")
bhp_pos = wconhist_block.find("250")
assert orat_pos >= 0, "ORAT value missing from WCONHIST output"
assert grat_pos >= 0, "GRAT value missing from WCONHIST output"
assert bhp_pos >= 0, "BHP value missing from WCONHIST output"
assert orat_pos < bhp_pos, (
f"ORAT must precede BHP in canonical WCONHIST order; got block: {wconhist_block!r}"
)
assert grat_pos < bhp_pos, (
f"GRAT must precede BHP in canonical WCONHIST order; got block: {wconhist_block!r}"
)
def test_invalid_keyword_data_unsupported_type(self, project_with_case_and_well):
"""Test error handling for unsupported data types in keyword events."""
project, case, timeline = project_with_case_and_well