diff --git a/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py b/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py index b70c831c46..9b78944c2c 100644 --- a/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py +++ b/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py @@ -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 diff --git a/GrpcInterface/Python/rips/orion_events.py b/GrpcInterface/Python/rips/orion_events.py index 2aac893f74..e95786fe61 100644 --- a/GrpcInterface/Python/rips/orion_events.py +++ b/GrpcInterface/Python/rips/orion_events.py @@ -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 diff --git a/GrpcInterface/Python/rips/tests/test_orion_events.py b/GrpcInterface/Python/rips/tests/test_orion_events.py index e65123d7d6..2cd010f69d 100644 --- a/GrpcInterface/Python/rips/tests/test_orion_events.py +++ b/GrpcInterface/Python/rips/tests/test_orion_events.py @@ -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"