mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 14:40:26 -05:00
Orion Events: Add group member events
This commit is contained in:
1 parent
c94d00a02a
commit
1278ee2404
2 files changed
+118
No files matched your search
@@ -79,6 +79,8 @@ Notes on the grammar:
|
||||
group name is injected as the ``GROUP`` item when each event is applied.
|
||||
A bare ``SCHEDULE`` line opens a block of schedule-level keyword events not
|
||||
tied to any well (RPTRST, GRUPTREE, TUNING, ...). Empty blocks are legal.
|
||||
``MEMBER MEMBERS="A,B"`` inside a GROUP block is shorthand for one GRUPTREE
|
||||
record per unique member, with the enclosing group as parent.
|
||||
* ``REPORT <date_expr>`` (one date per line, anywhere after the header) names
|
||||
a date that should appear as a bare ``DATES`` keyword in the generated
|
||||
schedule even when no events fall on it — in Eclipse/Flow a ``DATES`` entry
|
||||
@@ -1235,6 +1237,54 @@ def _suspected_typo(event_type: str) -> Optional[str]:
|
||||
return close[0] if close else None
|
||||
|
||||
|
||||
def _apply_member_event(
|
||||
event: OrionEvent,
|
||||
timeline: Any,
|
||||
report: ApplyReport,
|
||||
group_name: Optional[str],
|
||||
) -> None:
|
||||
"""Expand a GROUP MEMBER event into one GRUPTREE event per member."""
|
||||
if group_name is None:
|
||||
report.errors.append(f"Line {event.loc.line}: MEMBER needs a GROUP block")
|
||||
report.events_skipped += 1
|
||||
return
|
||||
|
||||
unknown = set(event.attributes) - {"MEMBERS", "COMMENT"}
|
||||
if unknown:
|
||||
report.errors.append(
|
||||
f"Line {event.loc.line}: unknown MEMBER attribute(s): "
|
||||
f"{', '.join(sorted(unknown))}"
|
||||
)
|
||||
report.events_skipped += 1
|
||||
return
|
||||
if "MEMBERS" not in event.attributes:
|
||||
report.errors.append(
|
||||
f"Line {event.loc.line}: MEMBER missing required attribute: MEMBERS"
|
||||
)
|
||||
report.events_skipped += 1
|
||||
return
|
||||
|
||||
raw_members = str(event.attributes["MEMBERS"].value)
|
||||
members = [member.strip() for member in raw_members.split(",")]
|
||||
if not members or any(not member for member in members):
|
||||
report.errors.append(
|
||||
f"Line {event.loc.line}: MEMBERS must be a comma-delimited list of "
|
||||
"non-empty names"
|
||||
)
|
||||
report.events_skipped += 1
|
||||
return
|
||||
|
||||
unique_members = list(dict.fromkeys(members))
|
||||
for member in unique_members:
|
||||
timeline_event = timeline.add_keyword_event(
|
||||
event_date=_iso_event_date(event.event_date),
|
||||
keyword_name="GRUPTREE",
|
||||
keyword_data={"CHILD_GROUP": member, "PARENT_GROUP": group_name},
|
||||
)
|
||||
_apply_event_comment(event, timeline_event)
|
||||
report.events_applied += 1
|
||||
|
||||
|
||||
def _apply_schedule_event(
|
||||
event: OrionEvent,
|
||||
timeline: Any,
|
||||
@@ -1243,6 +1293,9 @@ def _apply_schedule_event(
|
||||
) -> None:
|
||||
"""Apply one GROUP- or SCHEDULE-block event as an Eclipse keyword."""
|
||||
event_type = event.event_type.upper()
|
||||
if event_type == "MEMBER":
|
||||
_apply_member_event(event, timeline, report, group_name)
|
||||
return
|
||||
if event_type in _COMPLETION_EVENT_TYPES:
|
||||
report.errors.append(
|
||||
f"Line {event.loc.line}: {event_type} is a completion event and "
|
||||
|
||||
@@ -1082,6 +1082,50 @@ class TestApplying:
|
||||
assert timeline.schedule_keyword_calls[1]["keyword_data"]["GROUP"] == "OP"
|
||||
assert timeline.schedule_keyword_calls[2]["keyword_data"]["GROUP"] == "WI"
|
||||
|
||||
def test_member_event_expands_to_unique_grouptree_events(self):
|
||||
text = (
|
||||
'ORIONEVENTS 2.0\nGROUP "PRODUCERS"\n'
|
||||
' @2024-01-01 MEMBER MEMBERS="WELL_A, WELL_B,WELL_A" '
|
||||
'COMMENT="Group membership"\n'
|
||||
)
|
||||
timeline, report = self._apply(text)
|
||||
|
||||
assert report.events_applied == 2
|
||||
assert report.errors == []
|
||||
assert [call["keyword_data"] for call in timeline.schedule_keyword_calls] == [
|
||||
{"CHILD_GROUP": "WELL_A", "PARENT_GROUP": "PRODUCERS"},
|
||||
{"CHILD_GROUP": "WELL_B", "PARENT_GROUP": "PRODUCERS"},
|
||||
]
|
||||
assert all(
|
||||
event.comment == "Group membership" for event in timeline.created_events
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"block,event,expected_error",
|
||||
[
|
||||
("SCHEDULE", 'MEMBER MEMBERS="A"', "needs a GROUP block"),
|
||||
('GROUP "G"', "MEMBER", "missing required attribute"),
|
||||
(
|
||||
'GROUP "G"',
|
||||
'MEMBER MEMBERS="A,,B"',
|
||||
"non-empty names",
|
||||
),
|
||||
(
|
||||
'GROUP "G"',
|
||||
'MEMBER MEMBERS="A" EXTRA=1',
|
||||
"unknown MEMBER attribute",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_invalid_member_event_is_skipped(self, block, event, expected_error):
|
||||
text = f"ORIONEVENTS 2.0\n{block}\n @2024-01-01 {event}\n"
|
||||
timeline, report = self._apply(text)
|
||||
|
||||
assert report.events_applied == 0
|
||||
assert report.events_skipped == 1
|
||||
assert timeline.schedule_keyword_calls == []
|
||||
assert expected_error in report.errors[0]
|
||||
|
||||
def test_completion_event_in_schedule_block_is_error(self):
|
||||
text = (
|
||||
"ORIONEVENTS 2.0\nSCHEDULE\n @2024-01-01 PERFORATION MDSTART=1 MDEND=2\n"
|
||||
@@ -1368,6 +1412,27 @@ class TestOrionEventsIntegration:
|
||||
assert "'OP'" in schedule
|
||||
assert "'WI'" in schedule
|
||||
|
||||
def test_member_event_generates_grouptree(self, project_with_case_and_wells):
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
well = project.well_paths()[0]
|
||||
document = parse_orion_events(
|
||||
"ORIONEVENTS 2.0\n"
|
||||
f'WELL "{well.name}"\n'
|
||||
" @2024-01-01 WCONHIST STATUS=OPEN\n"
|
||||
'GROUP "PRODUCERS"\n'
|
||||
' @2024-01-01 MEMBER MEMBERS="WELL_A,WELL_B"\n'
|
||||
)
|
||||
|
||||
report = apply_orion_document(document, timeline, project)
|
||||
assert report.errors == []
|
||||
assert report.events_applied == 3
|
||||
|
||||
schedule = timeline.generate_schedule_text(eclipse_case=case)
|
||||
grouptree_block = schedule.split("GRUPTREE", 1)[1].split("\n/\n", 1)[0]
|
||||
normalized_block = " ".join(grouptree_block.split())
|
||||
assert "'WELL_A' 'PRODUCERS'" in normalized_block
|
||||
assert "'WELL_B' 'PRODUCERS'" in normalized_block
|
||||
|
||||
def test_apply_creates_perforations_and_schedule(self, project_with_case_and_wells):
|
||||
"""End-to-end: parse -> apply -> set_timestamp -> generate schedule."""
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
|
||||
Reference in new issue
Block a user