From 64317dfad786893f5f1828eb3e440b0263559a14 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Fri, 21 Aug 2026 13:07:35 +0200 Subject: [PATCH] #14575 Orion Events: Rename WELLSPEC event to WELSPECS --- .../experimental/well_event_schedule_orion.py | 12 ++++---- GrpcInterface/Python/rips/orion_events.py | 28 ++++++++--------- .../Python/rips/tests/test_orion_events.py | 30 ++++++++++++------- docs/orionEvents.md | 4 +-- 4 files changed, 41 insertions(+), 33 deletions(-) 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 e3ab03913f..3b40d06f8d 100644 --- a/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py +++ b/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py @@ -11,7 +11,7 @@ applied in one go with rips.orion_events.apply_orion_document(). It demonstrates the full event coverage of the format: 1. SEGMENT, PERFORATION (incl. a time-of-day date), VALVE and STATE completion events on a well -2. Partial WELLSPEC updates that cumulatively change completion export settings +2. Partial WELSPECS updates that cumulatively change completion export settings and generate dated WELSPECS records 3. A FILTER declaration (qualified result name) referenced by a perforation, materialized as a case-level combined data filter @@ -59,10 +59,10 @@ DURATION RAMP = 31 DAYS WELL W1 = "{well_name}" WELL W1 - # WELLSPEC updates completion export settings and emits WELSPECS. Attributes + # WELSPECS updates completion export settings and emits WELSPECS. Attributes # are optional: the second event inherits GROUP from the first event. - @2024-01-05 WELLSPEC GROUP="ORION_GROUP" CROSSFLOW=True REFDEPTH=1002 PHASE=WATER - @2024-04-15 WELLSPEC CROSSFLOW=False REFDEPTH=1000 PHASE=OIL + @2024-01-05 WELSPECS GROUP="ORION_GROUP" CROSSFLOW=True REFDEPTH=1002 PHASE=WATER + @2024-04-15 WELSPECS CROSSFLOW=False REFDEPTH=1000 PHASE=OIL # COMMENT is stored on the event and safely emitted as a schedule comment. @STARTUP SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 ROUGHNESS=1.0e-5 PRESSURE_COMPONENTS=HFA COMMENT="Install production segment" @@ -155,9 +155,9 @@ def main(): # Apply events up to a date to materialize completions timeline.set_timestamp(timestamp="2024-12-24") - print("\n4. Verifying created completions and WELLSPEC settings...") + print("\n4. Verifying created completions and WELSPECS settings...") completion_settings = well_path.completion_settings() - print(" Completion export settings after the latest WELLSPEC:") + print(" Completion export settings after the latest WELSPECS:") print(f" Group: {completion_settings.group_name_for_export}") print(f" Cross-flow: {completion_settings.allow_well_cross_flow}") print(f" Ref. depth: {completion_settings.reference_depth_for_export}") diff --git a/GrpcInterface/Python/rips/orion_events.py b/GrpcInterface/Python/rips/orion_events.py index cfbf855896..227b150f61 100644 --- a/GrpcInterface/Python/rips/orion_events.py +++ b/GrpcInterface/Python/rips/orion_events.py @@ -109,14 +109,14 @@ Notes on the grammar: attribute values, e.g. ``FILTER="SOIL > 0.8 AND PERMX > 200"``. * Every attribute is ``KEY=VALUE``; bare positional tokens are rejected. * Event types inside a WELL block are either the built-in completion events - ``PERFORATION``, ``SEGMENT``, ``VALVE``, ``STATE`` and ``WELLSPEC``, or any + ``PERFORATION``, ``SEGMENT``, ``VALVE``, ``STATE`` and ``WELSPECS``, or any Eclipse well keyword (``WCONHIST``, ``WELTARG``, ``WRFTPLT``, ``WCONPROD``, ...), which is passed through generically with the well name injected as WELL. Event - ``WELLSPEC`` accepts partial updates to ``GROUP``, ``CROSSFLOW``, ``REFDEPTH`` + ``WELSPECS`` accepts partial updates to ``GROUP``, ``CROSSFLOW``, ``REFDEPTH`` and ``PHASE`` (OIL/GAS/WATER/LIQUID). Omitted values inherit the previous - WELLSPEC state, initially using the well's completion export settings. There - may be multiple WELLSPEC events for a well, but not at the same timestamp. + WELSPECS state, initially using the well's completion export settings. There + may be multiple WELSPECS events for a well, but not at the same timestamp. Each emits a WELSPECS record with the cumulative state. Event values are materialized back onto completion settings by ``timeline.set_timestamp()``. Event types inside a GROUP block are Eclipse group keywords with the group @@ -273,7 +273,7 @@ class AttrValue: @dataclass class WellSpecState: - """Fully resolved cumulative state for one WELLSPEC event.""" + """Fully resolved cumulative state for one WELSPECS event.""" group: str crossflow: bool @@ -592,19 +592,19 @@ def _restart_validation_issues( def _wellspec_validation_issues(wells: List[WellBlock]) -> List[ParseIssue]: - """Reject multiple WELLSPEC events for one well at the same timestamp.""" + """Reject multiple WELSPECS events for one well at the same timestamp.""" seen: Dict[Tuple[str, Union[datetime.date, datetime.datetime]], OrionEvent] = {} issues: List[ParseIssue] = [] for well in wells: for event in well.events: - if event.event_type.upper() != "WELLSPEC": + if event.event_type.upper() != "WELSPECS": continue key = (well.well_name, event.event_date) previous = seen.get(key) if previous is not None: issues.append( ParseIssue( - f"{_event_context(event)}: WELLSPEC already defined " + f"{_event_context(event)}: WELSPECS already defined " f"(first definition on line {previous.loc.line})", event.loc, ) @@ -1315,7 +1315,7 @@ def _enum_text(value: Any) -> str: def _prepare_wellspec_events( events: List[OrionEvent], completion_settings: Any, report: ApplyReport ) -> None: - """Validate WELLSPEC attributes and resolve partial updates chronologically.""" + """Validate WELSPECS attributes and resolve partial updates chronologically.""" state = WellSpecState( group=str(completion_settings.group_name_for_export), crossflow=bool(completion_settings.allow_well_cross_flow), @@ -1324,7 +1324,7 @@ def _prepare_wellspec_events( ) wellspecs = sorted( - (event for event in events if event.event_type.upper() == "WELLSPEC"), + (event for event in events if event.event_type.upper() == "WELSPECS"), key=lambda event: event.event_date, ) for event in wellspecs: @@ -1334,14 +1334,14 @@ def _prepare_wellspec_events( report.errors.append( _event_message( event, - f"unknown WELLSPEC attribute(s): {', '.join(sorted(unknown))}", + f"unknown WELSPECS attribute(s): {', '.join(sorted(unknown))}", ) ) report.events_skipped += 1 continue if not (set(attrs) - {"COMMENT"}): report.errors.append( - _event_message(event, "WELLSPEC needs at least one setting attribute") + _event_message(event, "WELSPECS needs at least one setting attribute") ) report.events_skipped += 1 continue @@ -2037,7 +2037,7 @@ _EVENT_DISPATCH: Dict[str, _EventDispatch] = { "SEGMENT": _apply_segment, "VALVE": _apply_valve, "STATE": _apply_state, - "WELLSPEC": _apply_wellspec, + "WELSPECS": _apply_wellspec, "WCONHIST": _apply_wconhist, "WELTARG": _apply_weltarg, } @@ -2049,7 +2049,7 @@ _COMPLETION_EVENT_TYPES = ( "SEGMENT", "VALVE", "STATE", - "WELLSPEC", + "WELSPECS", ) diff --git a/GrpcInterface/Python/rips/tests/test_orion_events.py b/GrpcInterface/Python/rips/tests/test_orion_events.py index 2bce7228f2..f5471ad1f8 100644 --- a/GrpcInterface/Python/rips/tests/test_orion_events.py +++ b/GrpcInterface/Python/rips/tests/test_orion_events.py @@ -494,19 +494,19 @@ class TestParsing: def test_duplicate_wellspec_for_well_and_date_is_rejected(self): text = ( 'ORIONEVENTS 2.0\nWELL "W"\n' - " @2024-01-01 WELLSPEC GROUP=A\n" + " @2024-01-01 WELSPECS GROUP=A\n" 'WELL "W"\n' - " @2024-01-01 WELLSPEC PHASE=GAS\n" + " @2024-01-01 WELSPECS PHASE=GAS\n" ) - with pytest.raises(OrionParseError, match="WELLSPEC already defined.*line 3"): + with pytest.raises(OrionParseError, match="WELSPECS already defined.*line 3"): parse_orion_events(text) def test_wellspec_same_date_for_different_wells_is_allowed(self): document = parse_orion_events( 'ORIONEVENTS 2.0\nWELL "A"\n' - " @2024-01-01 WELLSPEC GROUP=GA\n" + " @2024-01-01 WELSPECS GROUP=GA\n" 'WELL "B"\n' - " @2024-01-01 WELLSPEC GROUP=GB\n" + " @2024-01-01 WELSPECS GROUP=GB\n" ) assert [len(well.events) for well in document.wells] == [1, 1] @@ -1144,8 +1144,8 @@ class TestApplying: def test_wellspec_partial_updates_are_cumulative_by_date(self): text = ( 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n' - " @2019-01-01 WELLSPEC CROSSFLOW=False PHASE=gas\n" - " @2018-01-01 WELLSPEC GROUP=my_group REFDEPTH=1002 PHASE=water\n" + " @2019-01-01 WELSPECS CROSSFLOW=False PHASE=gas\n" + " @2018-01-01 WELSPECS GROUP=my_group REFDEPTH=1002 PHASE=water\n" ) timeline, report = self._apply(text) @@ -1171,13 +1171,13 @@ class TestApplying: ("REFDEPTH=deep", "REFDEPTH must be numeric"), ("PHASE=steam", "PHASE must be OIL, GAS, WATER, or LIQUID"), ("GROUP=1", "GROUP must be a non-empty string"), - ("UNKNOWN=1", "unknown WELLSPEC attribute"), + ("UNKNOWN=1", "unknown WELSPECS attribute"), ("COMMENT=empty", "needs at least one setting attribute"), ], ) def test_invalid_wellspec_is_reported_and_skipped(self, attributes, expected_error): text = ( - f'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2018-01-01 WELLSPEC {attributes}\n' + f'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2018-01-01 WELSPECS {attributes}\n' ) timeline, report = self._apply(text) @@ -1231,6 +1231,14 @@ class TestApplying: assert timeline.keyword_calls == [] assert any("did you mean 'SEGMENT'" in warning for warning in report.warnings) + def test_wellspec_is_reported_as_renamed_event(self): + text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2024-01-01 WELLSPEC GROUP=FIELD\n' + timeline, report = self._apply(text) + + assert report.events_skipped == 1 + assert timeline.wellspec_calls == [] + assert any("did you mean 'WELSPECS'" in warning for warning in report.warnings) + def test_valve_mapping(self): text = ( 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n' @@ -1806,8 +1814,8 @@ class TestOrionEventsIntegration: document = parse_orion_events( "ORIONEVENTS 2.0\n" f'WELL "{well.name}"\n' - " @2018-01-01 WELLSPEC GROUP=my_group REFDEPTH=1002 PHASE=water\n" - " @2019-01-01 WELLSPEC CROSSFLOW=False REFDEPTH=1000 PHASE=oil\n" + " @2018-01-01 WELSPECS GROUP=my_group REFDEPTH=1002 PHASE=water\n" + " @2019-01-01 WELSPECS CROSSFLOW=False REFDEPTH=1000 PHASE=oil\n" ) report = apply_orion_document(document, timeline, project) diff --git a/docs/orionEvents.md b/docs/orionEvents.md index 6c42c0d5e5..a9387af9aa 100644 --- a/docs/orionEvents.md +++ b/docs/orionEvents.md @@ -77,7 +77,7 @@ group_block_open = "GROUP" , quoted_string ; (* group keyword events *) schedule_block_open = "SCHEDULE" ; (* well-less keyword events *) event_line = "@" , date_expr , event_type , { attribute } ; event_type = completion_event_type | eclipse_keyword ; -completion_event_type = "PERFORATION" | "SEGMENT" | "VALVE" | "STATE" | "WELLSPEC" ; +completion_event_type = "PERFORATION" | "SEGMENT" | "VALVE" | "STATE" | "WELSPECS" ; segment_attribute = "MDSTART" | "MDEND" | "INNER_DIAMETER" | "ROUGHNESS" | "PRESSURE_COMPONENTS" | "COMMENT" ; (* accepted attribute names when event_type is "SEGMENT" *) @@ -221,7 +221,7 @@ SCHEDULE @STARTUP TUNING TSINIT=1 TSMAXZ=30 NEWTMX=12 ``` -Completion event types (`PERFORATION`, `SEGMENT`, `VALVE`, `STATE`, `WELLSPEC`) are rejected in a SCHEDULE block. A later `WELL` line switches back to well events; blocks can be interleaved freely. +Completion event types (`PERFORATION`, `SEGMENT`, `VALVE`, `STATE`, `WELSPECS`) are rejected in a SCHEDULE block. A later `WELL` line switches back to well events; blocks can be interleaved freely. ## Attributes and quoting