From c94d00a02ade5563aff35932d0eec6d3f7e42c80 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 19 Aug 2026 11:43:56 +0200 Subject: [PATCH] Orion Events: Merge matching dated events --- GrpcInterface/Python/rips/orion_events.py | 67 +++++++++++++++++++ .../Python/rips/tests/test_orion_events.py | 63 ++++++++++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/GrpcInterface/Python/rips/orion_events.py b/GrpcInterface/Python/rips/orion_events.py index d8647e3f6a..3e1e052b0c 100644 --- a/GrpcInterface/Python/rips/orion_events.py +++ b/GrpcInterface/Python/rips/orion_events.py @@ -119,6 +119,7 @@ Notes on the grammar: from __future__ import annotations +import copy import datetime import difflib import os @@ -957,6 +958,71 @@ def apply_orion_events_file( return apply_orion_document(document, timeline, project, case=case, **options) +def coalesce_orion_document(document: OrionDocument) -> OrionDocument: + """Return a copy with same-owner/type/timestamp events merged. + + The first event retains its position. Attributes from later matching events + are applied in source order, adding missing values and overriding repeated + values. Owners are matched by well name, group name, or the global SCHEDULE + scope. + """ + result = copy.deepcopy(document) + + def merge_events(events: List[OrionEvent]) -> List[OrionEvent]: + merged: List[OrionEvent] = [] + by_key: Dict[ + Tuple[Union[datetime.date, datetime.datetime], str], OrionEvent + ] = {} + for event in events: + key = (event.event_date, event.event_type.upper()) + existing = by_key.get(key) + if existing is None: + by_key[key] = event + merged.append(event) + continue + + existing.attributes.update(event.attributes) + if "FILTER" in event.attributes: + existing.filter = event.filter + existing.loc = event.loc + return merged + + def merge_well_blocks(blocks: List[WellBlock]) -> List[WellBlock]: + merged_blocks: List[WellBlock] = [] + by_name: Dict[str, WellBlock] = {} + for block in blocks: + block_events = block.events + existing = by_name.get(block.well_name) + if existing is None: + block.events = [] + by_name[block.well_name] = block + merged_blocks.append(block) + by_name[block.well_name].events.extend(block_events) + for block in merged_blocks: + block.events = merge_events(block.events) + return merged_blocks + + def merge_group_blocks(blocks: List[GroupBlock]) -> List[GroupBlock]: + merged_blocks: List[GroupBlock] = [] + by_name: Dict[str, GroupBlock] = {} + for block in blocks: + block_events = block.events + existing = by_name.get(block.group_name) + if existing is None: + block.events = [] + by_name[block.group_name] = block + merged_blocks.append(block) + by_name[block.group_name].events.extend(block_events) + for block in merged_blocks: + block.events = merge_events(block.events) + return merged_blocks + + result.wells = merge_well_blocks(result.wells) + result.groups = merge_group_blocks(result.groups) + result.schedule_events = merge_events(result.schedule_events) + return result + + def apply_orion_document( document: OrionDocument, timeline: Any, @@ -993,6 +1059,7 @@ def apply_orion_document( """ _validate_policy(on_unknown_well, "on_unknown_well") _validate_policy(on_unknown_event, "on_unknown_event") + document = coalesce_orion_document(document) report = ApplyReport() report.report_dates = sorted({d.isoformat() for d in document.report_dates}) diff --git a/GrpcInterface/Python/rips/tests/test_orion_events.py b/GrpcInterface/Python/rips/tests/test_orion_events.py index 7be857e8ce..388347ceee 100644 --- a/GrpcInterface/Python/rips/tests/test_orion_events.py +++ b/GrpcInterface/Python/rips/tests/test_orion_events.py @@ -25,6 +25,7 @@ from rips.orion_events import ( # noqa: E402 OrionParseError, _cli, apply_orion_document, + coalesce_orion_document, parse_orion_events, ) @@ -785,6 +786,64 @@ class TestApplying: report = apply_orion_document(doc, timeline, project, **opts) return timeline, report + def test_same_owner_type_and_date_events_are_merged(self): + text = ( + "ORIONEVENTS 2.0\n" + 'WELL "55_33-A-1"\n' + " @2018-01-01 WCONHIST STATUS=OPEN ORAT=100\n" + 'WELL "55_33-A-1"\n' + " @2018-01-01 wconhist STATUS=SHUT CMODE=ORAT\n" + " @2018-01-02 WCONHIST STATUS=OPEN\n" + 'WELL "55_33-A-2"\n' + " @2018-01-01 WCONHIST STATUS=OPEN\n" + ) + document = parse_orion_events(text) + merged = coalesce_orion_document(document) + + # Normalization does not mutate the parsed source representation. + assert len(document.wells) == 3 + assert len(merged.wells) == 2 + assert len(merged.wells[0].events) == 2 + + merged_event = merged.wells[0].events[0] + assert merged_event.event_type == "WCONHIST" + assert merged_event.attributes["STATUS"].value == "SHUT" + assert merged_event.attributes["ORAT"].value == 100 + assert merged_event.attributes["CMODE"].value == "ORAT" + + timeline, report = self._apply(text) + assert report.events_applied == 3 + assert len(timeline.keyword_calls) == 3 + assert timeline.keyword_calls[0]["keyword_data"]["STATUS"] == "SHUT" + assert timeline.keyword_calls[0]["keyword_data"]["ORAT"] == 100 + assert timeline.keyword_calls[0]["keyword_data"]["CMODE"] == "ORAT" + + def test_group_and_schedule_events_merge_only_within_owner(self): + text = ( + "ORIONEVENTS 2.0\n" + 'GROUP "OP"\n' + " @2018-01-01 GCONPROD CONTROL_MODE=ORAT\n" + 'GROUP "OP"\n' + " @2018-01-01 GCONPROD OIL_TARGET=100\n" + 'GROUP "OTHER"\n' + " @2018-01-01 GCONPROD OIL_TARGET=200\n" + "SCHEDULE\n" + " @2018-01-01 RPTRST BASIC=1\n" + "SCHEDULE\n" + " @2018-01-01 RPTRST FREQ=2\n" + ) + merged = coalesce_orion_document(parse_orion_events(text)) + + assert len(merged.groups) == 2 + assert len(merged.groups[0].events) == 1 + assert set(merged.groups[0].events[0].attributes) == { + "CONTROL_MODE", + "OIL_TARGET", + } + assert len(merged.groups[1].events) == 1 + assert len(merged.schedule_events) == 1 + assert set(merged.schedule_events[0].attributes) == {"BASIC", "FREQ"} + def test_perforation_mapping_radius_to_diameter(self): timeline, report = self._apply(SAMPLE) assert report.events_applied == 4 # 2 perfs + WCONHIST + WELTARG @@ -1065,8 +1124,8 @@ class TestFilterApplying: def _perf_text(self, decls, *filter_values): events = "".join( - f" @2018-01-01 PERFORATION MDSTART=1 MDEND=2 FILTER={value}\n" - for value in filter_values + f" @2018-01-{index:02d} PERFORATION MDSTART=1 MDEND=2 FILTER={value}\n" + for index, value in enumerate(filter_values, start=1) ) return "ORIONEVENTS 2.0\n" + decls + 'WELL "55_33-A-1"\n' + events