#14615 Orion Events: Preserve historical keyword values

This commit is contained in:
Kristian Bendiksen
2026-08-27 10:14:27 +02:00
parent 6c63c755e7
commit 0cbb5f5935
3 changed files with 128 additions and 10 deletions
@@ -17,8 +17,9 @@ It demonstrates the full event coverage of the format:
materialized as a case-level combined data filter
4. COMMENT attributes preserved on timeline events and emitted before their
generated schedule keywords
5. Same-owner/type/date WCONHIST lines merged into one event, while same-date
perforations remain separate
5. Same-owner/type/date WCONHIST lines merged with conflict diagnostics, and
historical keyword values carried forward to later partial events, while
same-date perforations remain separate
6. Well keyword events: WCONHIST and WELTARG (with attribute translation) and
WRFTPLT (generic Eclipse well keyword pass-through)
7. A GROUP-level MEMBER event expanded to one GRUPTREE record per member
@@ -80,11 +81,16 @@ WELL W1
@2024-03-01 VALVE MD=2100 TYPE=ICV STATE=OPEN CV=0.7 AREA=0.0001
@2024-02-15 STATE STATE=OPEN
# Matching owner/type/date lines merge. The second line extends the first;
# repeated attributes would use the value from the later line.
@2024-01-15 WCONHIST STATUS=OPEN CMODE=RESV COMMENT="Start production history controls"
# Matching owner/type/date lines merge. The second line extends the first.
# Conflicting GRAT values produce a warning, and the later value wins.
@2024-01-15 WCONHIST STATUS=OPEN CMODE=RESV GRAT=4756545.5 COMMENT="Start production history controls"
@2024-01-15 WCONHIST ORAT=3999.99 WRAT=0.01 GRAT=550678.44 VFP=1
# Later partial keyword events inherit historical values for the same well
# and keyword. This event overrides WRAT and inherits STATUS, CMODE, ORAT,
# GRAT and VFP from January 15; COMMENT is event-local and is not inherited.
@2024-01-20 WCONHIST WRAT=0.03
# WRFTPLT is passed through as a generic Eclipse keyword.
@2024-05-01 WELTARG CMODE=ORAT VALUE=5000.0
@2024-06-01 WRFTPLT OUTPUT_RFT=YES OUTPUT_PLT=NO OUTPUT_SEGMENT=NO
+54 -5
View File
@@ -1381,6 +1381,7 @@ _NON_COALESCING_EVENT_TYPES = {
"VALVE",
"WELSPECS",
}
_NON_HISTORICAL_KEYWORD_ATTRS = {"COMMENT", "FILTER"}
def coalesce_orion_document(document: OrionDocument) -> OrionDocument:
@@ -1388,16 +1389,23 @@ def coalesce_orion_document(document: OrionDocument) -> OrionDocument:
Keyword events with the same owner, type and timestamp are merged. The first
event retains its position, and attributes from later matching events are
applied in source order. Events that create or expand domain objects are
kept separate so, for example, same-date perforation intervals are not lost.
applied in source order. Well keyword attributes are then carried forward
chronologically to later events of the same type. Events that create or
expand domain objects are kept separate so, for example, same-date
perforation intervals are not lost.
"""
result = copy.deepcopy(document)
def merge_events(events: List[OrionEvent]) -> List[OrionEvent]:
def merge_events(
events: List[OrionEvent], *, inherit_history: bool = False
) -> List[OrionEvent]:
merged: List[OrionEvent] = []
by_key: Dict[
Tuple[Union[datetime.date, datetime.datetime], str], OrionEvent
] = {}
attribute_locs: Dict[
Tuple[Union[datetime.date, datetime.datetime], str], Dict[str, SourceLoc]
] = {}
for event in events:
event_type = event.event_type.upper()
if event_type in _NON_COALESCING_EVENT_TYPES:
@@ -1408,13 +1416,50 @@ def coalesce_orion_document(document: OrionDocument) -> OrionDocument:
existing = by_key.get(key)
if existing is None:
by_key[key] = event
attribute_locs[key] = {name: event.loc for name in event.attributes}
merged.append(event)
continue
repeated = (
set(existing.attributes)
& set(event.attributes) - _NON_HISTORICAL_KEYWORD_ATTRS
)
for name in sorted(repeated):
previous = existing.attributes[name]
replacement = event.attributes[name]
if previous.value != replacement.value:
result.warnings.append(
ParseWarning(
f"{_event_context(event)}: conflicting {event_type} "
f"attribute '{name}' (previous value on line "
f"{attribute_locs[key][name].line}); using "
f"{replacement.raw!r}",
event.loc,
)
)
existing.attributes.update(event.attributes)
attribute_locs[key].update({name: event.loc for name in event.attributes})
if "FILTER" in event.attributes:
existing.filter = event.filter
existing.loc = event.loc
if inherit_history:
history: Dict[str, Dict[str, AttrValue]] = {}
for event in sorted(merged, key=lambda item: _as_datetime(item.event_date)):
event_type = event.event_type.upper()
if event_type in _NON_COALESCING_EVENT_TYPES:
continue
attributes = history.get(event_type, {}).copy()
attributes.update(event.attributes)
event.attributes = attributes
history[event_type] = {
name: value
for name, value in attributes.items()
if name not in _NON_HISTORICAL_KEYWORD_ATTRS
}
return merged
def merge_well_blocks(blocks: List[WellBlock]) -> List[WellBlock]:
@@ -1429,7 +1474,7 @@ def coalesce_orion_document(document: OrionDocument) -> OrionDocument:
merged_blocks.append(block)
by_name[block.well_name].events.extend(block_events)
for block in merged_blocks:
block.events = merge_events(block.events)
block.events = merge_events(block.events, inherit_history=True)
return merged_blocks
def merge_group_blocks(blocks: List[GroupBlock]) -> List[GroupBlock]:
@@ -1568,6 +1613,9 @@ def apply_orion_document(
document = coalesce_orion_document(document)
report = ApplyReport()
report.report_dates = sorted({d.isoformat() for d in document.report_dates})
report.warnings.extend(
f"Line {warning.loc.line}: {warning.message}" for warning in document.warnings
)
ctx = _prepare_filter_context(document, project, case)
@@ -2241,7 +2289,8 @@ def _cli(argv: Optional[List[str]] = None) -> int:
f"{len(document.schedule_events)} schedule event(s), "
f"{len(document.report_dates)} report date(s)"
)
for warning in document.warnings:
normalized = coalesce_orion_document(document)
for warning in normalized.warnings:
print(f" Warning line {warning.loc.line}: {warning.message}")
return 0
@@ -1046,6 +1046,69 @@ class TestApplying:
assert timeline.keyword_calls[0]["keyword_data"]["ORAT"] == 100
assert timeline.keyword_calls[0]["keyword_data"]["CMODE"] == "ORAT"
def test_well_keyword_history_is_inherited_chronologically(self):
text = (
"ORIONEVENTS 2.0\n"
'WELL "55_33-A-1"\n'
" @2024-01-20 WCONHIST WRAT=0.03\n"
' @2024-01-15 WCONHIST STATUS=OPEN CMODE=RESV GRAT=4756545.5 COMMENT="Startup"\n'
" @2024-01-15 WCONHIST ORAT=3999.99 WRAT=0.01 GRAT=550678.44 VFP=1\n"
" @2024-01-15 WELTARG CMODE=ORAT VALUE=5000\n"
'WELL "55_33-A-2"\n'
" @2024-01-20 WCONHIST WRAT=0.04\n"
)
document = parse_orion_events(text)
merged = coalesce_orion_document(document)
first_well_events = merged.wells[0].events
january_15 = next(
event
for event in first_well_events
if event.event_type.upper() == "WCONHIST"
and event.event_date.isoformat() == "2024-01-15"
)
january_20 = next(
event
for event in first_well_events
if event.event_type.upper() == "WCONHIST"
and event.event_date.isoformat() == "2024-01-20"
)
assert january_15.attributes["GRAT"].value == 550678.44
assert january_15.attributes["COMMENT"].value == "Startup"
assert january_20.attributes["STATUS"].value == "OPEN"
assert january_20.attributes["CMODE"].value == "RESV"
assert january_20.attributes["ORAT"].value == 3999.99
assert january_20.attributes["WRAT"].value == 0.03
assert january_20.attributes["GRAT"].value == 550678.44
assert january_20.attributes["VFP"].value == 1
assert "COMMENT" not in january_20.attributes
# Historical state is isolated by owner and keyword type.
assert set(merged.wells[1].events[0].attributes) == {"WRAT"}
weltarg = next(
event
for event in first_well_events
if event.event_type.upper() == "WELTARG"
)
assert set(weltarg.attributes) == {"CMODE", "VALUE"}
assert len(merged.warnings) == 1
warning = merged.warnings[0]
assert warning.loc.line == 5
assert "conflicting WCONHIST attribute 'GRAT'" in warning.message
assert "previous value on line 4" in warning.message
assert "using '550678.44'" in warning.message
timeline, report = self._apply(text)
assert report.events_applied == 4
assert len(report.warnings) == 1
assert "conflicting WCONHIST attribute 'GRAT'" in report.warnings[0]
january_20_call = timeline.keyword_calls[0]
assert january_20_call["event_date"] == "2024-01-20"
assert january_20_call["keyword_data"]["STATUS"] == "OPEN"
assert january_20_call["keyword_data"]["WRAT"] == 0.03
def test_same_date_perforations_are_not_merged(self):
text = (
"ORIONEVENTS 2.0\n"