#14575 Orion Events: Rename WELLSPEC event to WELSPECS

This commit is contained in:
Kristian Bendiksen committed 2026-08-24 13:26:19 +02:00
1 parent da6eb2062c
commit 64317dfad7
4 files changed
+41 -33

No files matched your search

@@ -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}")
+14 -14
View File
@@ -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",
)
@@ -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)
+2 -2
View File
@@ -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