#14637 Orion Events: Remove date prefix

This commit is contained in:
Kristian Bendiksen
2026-09-01 11:56:59 +02:00
committed by Magne Sjaastad
parent 73783a3e0a
commit 753cd65672
4 changed files with 217 additions and 206 deletions
@@ -63,51 +63,51 @@ WELL W1 = "{well_name}"
WELL W1
# WELSPECS updates completion export settings and emits WELSPECS. Attributes
# are optional: the second event inherits GROUP from the first event.
@2024-01-05 WELSPECS GROUP="ORION_GROUP" CROSSFLOW=True REFDEPTH=1002 PHASE=WATER
@2024-04-15 WELSPECS 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"
STARTUP SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 ROUGHNESS=1.0e-5 PRESSURE_COMPONENTS=HFA COMMENT="Install production segment"
# Perforations; COMPLETION_NUMBER groups connections for COMPLUMP. Same-date
# perforations are kept as separate events during normalization.{filter_comment}
@STARTUP + RAMP PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 SKIN=0.5 COMPLETION_NUMBER=1{filter_ref} COMMENT="Open high-priority interval"
@STARTUP + RAMP PERFORATION MDSTART=2400 MDEND=2600 RADIUS=0.05 SKIN=0.3 COMPLETION_NUMBER=2
STARTUP + RAMP PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 SKIN=0.5 COMPLETION_NUMBER=1{filter_ref} COMMENT="Open high-priority interval"
STARTUP + RAMP PERFORATION MDSTART=2400 MDEND=2600 RADIUS=0.05 SKIN=0.3 COMPLETION_NUMBER=2
# Time-of-day is preserved and emitted as the TIME field of DATES
@2024-05-15T14:45:30.500 PERFORATION MDSTART=2300 MDEND=2350 RADIUS=0.05 SKIN=0.4 COMPLETION_NUMBER=3
2024-05-15T14:45:30.500 PERFORATION MDSTART=2300 MDEND=2350 RADIUS=0.05 SKIN=0.4 COMPLETION_NUMBER=3
# Valve in the first perforation; state event for documentation
@2024-03-01 VALVE MD=2100 TYPE=ICV STATE=OPEN CV=0.7 AREA=0.0001
@2024-02-15 STATE STATE=OPEN
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.
# 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
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
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
2024-05-01 WELTARG CMODE=ORAT VALUE=5000.0
2024-06-01 WRFTPLT OUTPUT_RFT=YES OUTPUT_PLT=NO OUTPUT_SEGMENT=NO
# MEMBER expands into one GRUPTREE record per unique comma-delimited member.
GROUP "OP"
@STARTUP MEMBER MEMBERS="{well_name},OBSERVER" COMMENT="Define operating group members"
STARTUP MEMBER MEMBERS="{well_name},OBSERVER" COMMENT="Define operating group members"
# Schedule-level keywords (not tied to a well)
SCHEDULE
@STARTUP RPTRST BASIC=2 FREQ=1
@STARTUP GRUPTREE CHILD_GROUP=OP PARENT_GROUP=FIELD
@STARTUP TUNING TSINIT=1 TSMAXZ=30 TMAXWC=1 NEWTMX=12 NEWTMN=1 LITMAX=50 LITMIN=1 MXWSIT=50 MXWPIT=50
STARTUP RPTRST BASIC=2 FREQ=1
STARTUP GRUPTREE CHILD_GROUP=OP PARENT_GROUP=FIELD
STARTUP TUNING TSINIT=1 TSMAXZ=30 TMAXWC=1 NEWTMX=12 NEWTMN=1 LITMAX=50 LITMIN=1 MXWSIT=50 MXWPIT=50
# RAW_TEXT preserves its body verbatim. This block is emitted after RPTRST;
# PRIORITY orders multiple raw blocks sharing the same placement and anchor.
@STARTUP RAW_TEXT PLACEMENT=AFTER_KEYWORD ANCHOR=RPTRST PRIORITY=10
STARTUP RAW_TEXT PLACEMENT=AFTER_KEYWORD ANCHOR=RPTRST PRIORITY=10
-- Custom schedule text not modeled by the timeline API
WTRACER
'{well_name}' 'ORION_TRACER' 1.0 /
@@ -115,7 +115,7 @@ WTRACER
END_RAW_TEXT
# Recurring report dates become bare DATES keywords. The first series ends at
# the last @ event; the second uses an explicit inclusive end date.
# the last event; the second uses an explicit inclusive end date.
REPORT STARTUP EVERY MONTH
REPORT 2024-07-01 EVERY 3 MONTHS UNTIL STARTUP + 365
"""
@@ -20,7 +20,7 @@ FILTER POROPERM = "PORO > 0.1 AND PERMX > 100.0"
WELL A1 = "55_33-A-1"
# Report dates become bare DATES keywords in the generated schedule. Recurring
# dates may use an explicit inclusive end date or default to the last @ event.
# dates may use an explicit inclusive end date or default to the last event.
REPORT A1_STARTUP EVERY MONTH
REPORT A2_STARTUP EVERY 30 DAYS UNTIL A2_STARTUP + 90
@@ -28,7 +28,7 @@ REPORT A2_STARTUP EVERY 30 DAYS UNTIL A2_STARTUP + 90
# after the date declaration; lower priorities are emitted first when multiple
# blocks use the same placement.
SCHEDULE
@A1_STARTUP + RAMP RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=10
A1_STARTUP + RAMP RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=10
-- Custom schedule text not modeled by the timeline API
WTRACER
'55_33-A-1' 'ORION_TRACER' 1.0 /
@@ -36,14 +36,14 @@ WTRACER
END_RAW_TEXT
WELL A1
@A1_STARTUP PERFORATION MDSTART=1644.49 MDEND=1664.28 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1 FILTER=POROPERM
@A1_STARTUP PERFORATION MDSTART=1664.28 MDEND=1674.18 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=2 FILTER=POROPERM
@A1_STARTUP PERFORATION MDSTART=1674.18 MDEND=1689.82 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=3
@A1_STARTUP + RAMP WCONHIST STATUS=OPEN CMODE=ORAT VFP=1
@A1_STARTUP + RAMP WELTARG CMODE=BHP VALUE=50
A1_STARTUP PERFORATION MDSTART=1644.49 MDEND=1664.28 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1 FILTER=POROPERM
A1_STARTUP PERFORATION MDSTART=1664.28 MDEND=1674.18 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=2 FILTER=POROPERM
A1_STARTUP PERFORATION MDSTART=1674.18 MDEND=1689.82 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=3
A1_STARTUP + RAMP WCONHIST STATUS=OPEN CMODE=ORAT VFP=1
A1_STARTUP + RAMP WELTARG CMODE=BHP VALUE=50
WELL "55_33-A-2"
# An inline quoted expression declares an anonymous filter for one event.
@A2_STARTUP - 1 PERFORATION MDSTART=1692.79 MDEND=1706 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1 FILTER="PORO > 0.2"
@A2_STARTUP WCONHIST STATUS=OPEN CMODE=ORAT VFP=2
@A2_STARTUP WELTARG CMODE=BHP VALUE=50
A2_STARTUP - 1 PERFORATION MDSTART=1692.79 MDEND=1706 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1 FILTER="PORO > 0.2"
A2_STARTUP WCONHIST STATUS=OPEN CMODE=ORAT VFP=2
A2_STARTUP WELTARG CMODE=BHP VALUE=50
+15 -16
View File
@@ -45,8 +45,8 @@ File format grammar, version 2.0 (EBNF-ish)::
well_block_open = "WELL" , ( quoted_string | ident ) ; (* no "=" present *)
group_block_open = "GROUP" , quoted_string ; (* group keyword events *)
schedule_block_open = "SCHEDULE" ; (* well-less keyword events *)
event_line = "@" , date_expr , event_type , { attribute } ;
raw_text_event = "@" , date_expr , "RAW_TEXT" , raw_text_attributes , newline,
event_line = date_expr , event_type , { attribute } ;
raw_text_event = date_expr , "RAW_TEXT" , raw_text_attributes , newline,
{ raw_line , newline } , "END_RAW_TEXT" ;
raw_text_attributes = "PLACEMENT=" ,
( "AFTER_DATE" | "BEFORE_KEYWORD" |
@@ -68,7 +68,8 @@ Notes on the grammar:
* The format is line-oriented; every non-blank line is dispatched on its first
token: ``ORIONEVENTS`` (once), ``UNIT``, ``DATE``, ``DURATION``, ``WELL``,
``GROUP``, ``SCHEDULE``, ``REPORT`` or ``@``. Anything else is an error. Keywords are
``GROUP``, ``SCHEDULE``, ``REPORT`` or an event date. Anything else is an
error. Keywords are
uppercase and case-sensitive (the ``DAYS`` suffix is also accepted as ``days``).
* Comments start with ``#`` (outside of double quotes) and run to end of line.
* Variables are **typed**: ``DATE``, ``DURATION`` (whole days), ``WELL``
@@ -78,9 +79,9 @@ Notes on the grammar:
the declaration site. Redeclaring a name with the same type warns and the
last value wins; redeclaring with a different type is an error.
* Date arithmetic is a chain of signed whole-day terms, each an integer or a
``DURATION`` variable: ``@START + RAMP - 2``. Whitespace around ``+``/``-``
``DURATION`` variable: ``START + RAMP - 2``. Whitespace around ``+``/``-``
is optional but conventional. An event date may carry a time-of-day
(``@2024-05-15T14:45:30.500``), which the schedule generator preserves as
(``2024-05-15T14:45:30.500``), which the schedule generator preserves as
the optional TIME field of the DATES keyword.
* ``WELL <ident>`` opens an event block for a declared ``WELL`` alias;
``WELL "<name>"`` opens a block for the literal well name and never consults
@@ -98,7 +99,7 @@ Notes on the grammar:
events fall on it — in Eclipse/Flow a ``DATES`` entry ensures a summary
report at that date. ``EVERY [n] DAYS|MONTHS|YEARS`` makes it recurring and
an inclusive ``UNTIL <date_expr>`` sets the end date. When ``UNTIL`` is
omitted, the latest ``@`` event date is used. Monthly and yearly recurrences
omitted, the latest event date is used. Monthly and yearly recurrences
stay anchored to the initial calendar day, clamping to the end of shorter
months. The dates are collected on :attr:`OrionDocument.report_dates` and
surfaced by the applier as sorted ISO strings on
@@ -447,7 +448,7 @@ _RESULT_TYPE_ALIASES = {
}
_WELL_BLOCK_RE = re.compile(rf'^WELL\s+(?:"(?P<qname>[^"]*)"|(?P<ref>{_IDENT}))$')
_GROUP_BLOCK_RE = re.compile(r'^GROUP\s+"(?P<name>[^"]*)"$')
_EVENT_RE = re.compile(rf"^@\s*{_DATE_BASE}{_TERMS}\s+(?P<rest>.+)$")
_EVENT_RE = re.compile(rf"^{_DATE_BASE}{_TERMS}\s+(?P<rest>.+)$")
_TERM_RE = re.compile(rf"([-+])\s*(\d+|{_IDENT})")
_ATTR_RE = re.compile(r'(?P<key>[A-Za-z_]\w*)\s*=\s*(?:"(?P<qval>[^"]*)"|(?P<val>\S+))')
@@ -541,7 +542,7 @@ def parse_orion_events(text: str) -> OrionDocument:
except OrionParseError as exc:
errors.extend(exc.errors)
if line.split(None, 1)[0] in ("WELL", "GROUP", "SCHEDULE") or (
line.startswith("@") and current_events is None
_EVENT_RE.match(line) is not None and current_events is None
):
# Suppress cascading errors from lines belonging to a broken
# (or missing) block: swallow them into a discarded list.
@@ -760,14 +761,6 @@ def _parse_line(
unit_holder: List[str],
) -> Optional[List[OrionEvent]]:
"""Dispatch one non-header line; returns the current event sink."""
if line.startswith("@"):
if current_events is None:
raise OrionParseError(
"Event line found before any WELL or SCHEDULE block", loc
)
current_events.append(_parse_event_line(line, variables, loc))
return current_events
first = line.split(None, 1)[0]
if first == "UNIT":
@@ -912,6 +905,12 @@ def _parse_line(
wells.append(new_well)
return new_well.events
if current_events is not None and _EVENT_RE.match(line):
current_events.append(_parse_event_line(line, variables, loc))
return current_events
if first[0].isdigit() and _EVENT_RE.match(line):
raise OrionParseError("Event line found before any WELL or SCHEDULE block", loc)
raise OrionParseError(_unrecognized_line_message(line, first), loc)
@@ -41,12 +41,12 @@ DURATION RAMP = 5 DAYS
WELL A1 = "55_33-A-1"
WELL A1
@A1_STARTUP PERFORATION MDSTART=1644.49 MDEND=1664.28 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1
@A1_STARTUP + RAMP WCONHIST STATUS=OPEN CMODE=ORAT VFP=1
@A1_STARTUP + RAMP WELTARG CMODE=BHP VALUE=50
A1_STARTUP PERFORATION MDSTART=1644.49 MDEND=1664.28 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1
A1_STARTUP + RAMP WCONHIST STATUS=OPEN CMODE=ORAT VFP=1
A1_STARTUP + RAMP WELTARG CMODE=BHP VALUE=50
WELL "55_33-A-2"
@A2_STARTUP PERFORATION MDSTART=1692.79 MDEND=1706 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1
A2_STARTUP PERFORATION MDSTART=1692.79 MDEND=1706 RADIUS=0.12065 SKIN=5 COMPLETION_NUMBER=1
"""
@@ -265,7 +265,7 @@ class TestParsing:
"WCONHIST",
"WELTARG",
]
# @A1_STARTUP + RAMP -> 2018-01-06
# A1_STARTUP + RAMP -> 2018-01-06
assert well1.events[1].event_date == datetime.date(2018, 1, 6)
def test_value_type_inference(self):
@@ -280,7 +280,7 @@ class TestParsing:
def test_quoted_filter_value_is_single_attribute(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
' @2018-01-01 PERFORATION MDSTART=1 MDEND=2 FILTER="SOIL > 0.8 AND PERMX > 200"\n'
' 2018-01-01 PERFORATION MDSTART=1 MDEND=2 FILTER="SOIL > 0.8 AND PERMX > 200"\n'
)
doc = parse_orion_events(text)
filter_attr = doc.wells[0].events[0].attributes["FILTER"]
@@ -291,7 +291,7 @@ class TestParsing:
def test_comment_attribute_is_preserved(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
' @2018-01-01 WCONHIST STATUS=OPEN COMMENT="Startup target"\n'
' 2018-01-01 WCONHIST STATUS=OPEN COMMENT="Startup target"\n'
)
event = parse_orion_events(text).wells[0].events[0]
assert event.attributes["COMMENT"].value == "Startup target"
@@ -300,7 +300,7 @@ class TestParsing:
def test_trailing_comment_ignored_but_not_inside_quotes(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
' @2018-01-01 WCONHIST NOTE="a # b" CMODE=ORAT # trailing comment\n'
' 2018-01-01 WCONHIST NOTE="a # b" CMODE=ORAT # trailing comment\n'
)
doc = parse_orion_events(text)
attrs = doc.wells[0].events[0].attributes
@@ -308,29 +308,27 @@ class TestParsing:
assert "CMODE" in attrs
def test_iso_date_literal_event(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n @2020-12-31 PERFORATION MDSTART=1 MDEND=2\n'
)
text = 'ORIONEVENTS 2.0\nWELL "W"\n 2020-12-31 PERFORATION MDSTART=1 MDEND=2\n'
doc = parse_orion_events(text)
assert doc.wells[0].events[0].event_date == datetime.date(2020, 12, 31)
def test_negative_offset(self):
text = (
"ORIONEVENTS 2.0\nDATE START = 2018-01-10\n"
'WELL "W"\n @START - 5 PERFORATION MDSTART=1 MDEND=2\n'
'WELL "W"\n START - 5 PERFORATION MDSTART=1 MDEND=2\n'
)
doc = parse_orion_events(text)
assert doc.wells[0].events[0].event_date == datetime.date(2018, 1, 5)
def test_minus_after_iso_date(self):
text = 'ORIONEVENTS 2.0\nWELL "W"\n @2018-01-01 - 5 PERFORATION MDSTART=1 MDEND=2\n'
text = 'ORIONEVENTS 2.0\nWELL "W"\n 2018-01-01 - 5 PERFORATION MDSTART=1 MDEND=2\n'
doc = parse_orion_events(text)
assert doc.wells[0].events[0].event_date == datetime.date(2017, 12, 27)
def test_offset_chain_with_duration_variable(self):
text = (
"ORIONEVENTS 2.0\nDATE START = 2018-01-01\nDURATION RAMP = 5\n"
'WELL "W"\n @START + RAMP - 2 PERFORATION MDSTART=1 MDEND=2\n'
'WELL "W"\n START + RAMP - 2 PERFORATION MDSTART=1 MDEND=2\n'
)
doc = parse_orion_events(text)
assert doc.wells[0].events[0].event_date == datetime.date(2018, 1, 4)
@@ -379,19 +377,19 @@ class TestParsing:
def test_event_before_well_block_raises(self):
with pytest.raises(OrionParseError, match="before any WELL or SCHEDULE block"):
parse_orion_events(
"ORIONEVENTS 2.0\n @2018-01-01 PERFORATION MDSTART=1 MDEND=2\n"
"ORIONEVENTS 2.0\n 2018-01-01 PERFORATION MDSTART=1 MDEND=2\n"
)
def test_unknown_variable_raises(self):
with pytest.raises(OrionParseError, match="Unknown variable 'NOPE'"):
parse_orion_events(
'ORIONEVENTS 2.0\nWELL "W"\n @NOPE PERFORATION MDSTART=1 MDEND=2\n'
'ORIONEVENTS 2.0\nWELL "W"\n NOPE PERFORATION MDSTART=1 MDEND=2\n'
)
def test_duration_where_date_expected_raises(self):
text = (
"ORIONEVENTS 2.0\nDURATION RAMP = 5\n"
'WELL "W"\n @RAMP WCONHIST STATUS=OPEN\n'
'WELL "W"\n RAMP WCONHIST STATUS=OPEN\n'
)
with pytest.raises(
OrionParseError, match=r"is a DURATION \(declared line 2\) but a DATE"
@@ -401,13 +399,13 @@ class TestParsing:
def test_date_where_duration_expected_raises(self):
text = (
"ORIONEVENTS 2.0\nDATE START = 2018-01-01\nDATE OTHER = 2018-02-01\n"
'WELL "W"\n @START + OTHER WCONHIST STATUS=OPEN\n'
'WELL "W"\n START + OTHER WCONHIST STATUS=OPEN\n'
)
with pytest.raises(OrionParseError, match="is a DATE .* but a DURATION"):
parse_orion_events(text)
def test_well_variable_in_date_context_raises(self):
text = 'ORIONEVENTS 2.0\nWELL A1 = "X"\nWELL A1\n @A1 WCONHIST STATUS=OPEN\n'
text = 'ORIONEVENTS 2.0\nWELL A1 = "X"\nWELL A1\n A1 WCONHIST STATUS=OPEN\n'
with pytest.raises(OrionParseError, match="is a WELL .* but a DATE"):
parse_orion_events(text)
@@ -420,7 +418,7 @@ class TestParsing:
def test_forward_reference_raises(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n @START PERFORATION MDSTART=1 MDEND=2\n'
'ORIONEVENTS 2.0\nWELL "W"\n START PERFORATION MDSTART=1 MDEND=2\n'
"DATE START = 2018-01-01\n"
)
with pytest.raises(OrionParseError, match="Unknown variable 'START'"):
@@ -437,7 +435,7 @@ class TestParsing:
parse_orion_events(text)
def test_malformed_attribute_raises(self):
text = 'ORIONEVENTS 2.0\nWELL "W"\n @2018-01-01 PERFORATION MDSTART=1 bogus MDEND=2\n'
text = 'ORIONEVENTS 2.0\nWELL "W"\n 2018-01-01 PERFORATION MDSTART=1 bogus MDEND=2\n'
with pytest.raises(OrionParseError, match="Malformed attribute"):
parse_orion_events(text)
@@ -450,12 +448,12 @@ class TestParsing:
def test_schedule_block_parses(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
" @2024-01-01 WCONHIST STATUS=OPEN\n"
" 2024-01-01 WCONHIST STATUS=OPEN\n"
"SCHEDULE\n"
" @2024-01-01 RPTRST BASIC=2 FREQ=1\n"
" @2024-01-01 GRUPTREE CHILD_GROUP=OP PARENT_GROUP=FIELD\n"
" 2024-01-01 RPTRST BASIC=2 FREQ=1\n"
" 2024-01-01 GRUPTREE CHILD_GROUP=OP PARENT_GROUP=FIELD\n"
'WELL "W"\n'
" @2024-02-01 WELTARG CMODE=ORAT VALUE=5000\n"
" 2024-02-01 WELTARG CMODE=ORAT VALUE=5000\n"
)
doc = parse_orion_events(text)
assert [e.event_type for e in doc.schedule_events] == ["RPTRST", "GRUPTREE"]
@@ -466,12 +464,12 @@ class TestParsing:
def test_group_blocks_parse_and_switch_event_sink(self):
text = (
'ORIONEVENTS 2.0\nGROUP "OP"\n'
" @2020-07-01 GEFAC FACTOR=1.0 TRANSFER=YES\n"
" @2020-07-01 GCONPROD CMODE=LRAT LRAT=20000\n"
" 2020-07-01 GEFAC FACTOR=1.0 TRANSFER=YES\n"
" 2020-07-01 GCONPROD CMODE=LRAT LRAT=20000\n"
'GROUP "WI"\n'
" @2020-07-01 GCONINJE TYPE=WATER CMODE=RATE RATE=16000\n"
" 2020-07-01 GCONINJE TYPE=WATER CMODE=RATE RATE=16000\n"
"SCHEDULE\n"
" @2020-07-01 RPTRST BASIC=2\n"
" 2020-07-01 RPTRST BASIC=2\n"
)
doc = parse_orion_events(text)
assert [group.group_name for group in doc.groups] == ["OP", "WI"]
@@ -494,9 +492,9 @@ class TestParsing:
def test_duplicate_wellspec_for_well_and_date_is_rejected(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
" @2024-01-01 WELSPECS GROUP=A\n"
" 2024-01-01 WELSPECS GROUP=A\n"
'WELL "W"\n'
" @2024-01-01 WELSPECS PHASE=GAS\n"
" 2024-01-01 WELSPECS PHASE=GAS\n"
)
with pytest.raises(OrionParseError, match="WELSPECS already defined.*line 3"):
parse_orion_events(text)
@@ -504,9 +502,9 @@ class TestParsing:
def test_wellspec_same_date_for_different_wells_is_allowed(self):
document = parse_orion_events(
'ORIONEVENTS 2.0\nWELL "A"\n'
" @2024-01-01 WELSPECS GROUP=GA\n"
" 2024-01-01 WELSPECS GROUP=GA\n"
'WELL "B"\n'
" @2024-01-01 WELSPECS GROUP=GB\n"
" 2024-01-01 WELSPECS GROUP=GB\n"
)
assert [len(well.events) for well in document.wells] == [1, 1]
@@ -514,7 +512,7 @@ class TestParsing:
text = (
"ORIONEVENTS 2.0\n"
"SCHEDULE\n"
' @2024-01-01 RPTRST DEN=True ROCKC=FALSE LABEL="True"\n'
' 2024-01-01 RPTRST DEN=True ROCKC=FALSE LABEL="True"\n'
)
attributes = parse_orion_events(text).schedule_events[0].attributes
@@ -525,7 +523,7 @@ class TestParsing:
def test_single_restart_event_parses(self):
document = parse_orion_events(
"ORIONEVENTS 2.0\nSCHEDULE\n @2024-02-01 RESTART\n"
"ORIONEVENTS 2.0\nSCHEDULE\n 2024-02-01 RESTART\n"
)
restart = document.schedule_events[0]
assert restart.event_type == "RESTART"
@@ -535,21 +533,21 @@ class TestParsing:
"text,expected_error",
[
(
'ORIONEVENTS 2.0\nWELL "W"\n @2024-01-01 RESTART\n',
'ORIONEVENTS 2.0\nWELL "W"\n 2024-01-01 RESTART\n',
"only valid in a SCHEDULE block",
),
(
'ORIONEVENTS 2.0\nGROUP "G"\n @2024-01-01 RESTART\n',
'ORIONEVENTS 2.0\nGROUP "G"\n 2024-01-01 RESTART\n',
"only valid in a SCHEDULE block",
),
(
"ORIONEVENTS 2.0\nSCHEDULE\n @2024-01-01 RESTART VALUE=1\n",
"ORIONEVENTS 2.0\nSCHEDULE\n 2024-01-01 RESTART VALUE=1\n",
"takes no attributes",
),
(
"ORIONEVENTS 2.0\nSCHEDULE\n"
" @2024-01-01 RESTART\n"
" @2024-02-01 RESTART\n",
" 2024-01-01 RESTART\n"
" 2024-02-01 RESTART\n",
"Only one RESTART event",
),
],
@@ -565,17 +563,17 @@ class TestParsing:
def test_raw_text_block_preserves_body_and_attributes(self):
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n"
" @2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD "
" 2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD "
"ANCHOR=COMPDAT PRIORITY=-2\n"
"# not an ORION comment\n"
" @this is raw too\n"
" this is raw too\n"
"END\n"
"END_RAW_TEXT\n"
)
event = parse_orion_events(text).schedule_events[0]
assert event.event_type == "RAW_TEXT"
assert event.raw_text == "# not an ORION comment\n @this is raw too\nEND\n"
assert event.raw_text == "# not an ORION comment\n this is raw too\nEND\n"
assert event.raw_placement == "BEFORE_KEYWORD"
assert event.raw_anchor == "COMPDAT"
assert event.raw_priority == -2
@@ -604,14 +602,14 @@ class TestParsing:
],
)
def test_invalid_raw_text_header_rejected(self, header, error):
text = f"ORIONEVENTS 2.0\nSCHEDULE\n @2024-01-01 {header}\nx\nEND_RAW_TEXT\n"
text = f"ORIONEVENTS 2.0\nSCHEDULE\n 2024-01-01 {header}\nx\nEND_RAW_TEXT\n"
with pytest.raises(OrionParseError, match=error):
parse_orion_events(text)
def test_raw_text_outside_schedule_rejected(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\n"
"x\nEND_RAW_TEXT\n"
)
with pytest.raises(OrionParseError, match="only valid in a SCHEDULE"):
@@ -620,7 +618,7 @@ class TestParsing:
def test_unterminated_raw_text_rejected(self):
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n"
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\ntext\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\ntext\n"
)
with pytest.raises(OrionParseError, match="Unterminated RAW_TEXT"):
parse_orion_events(text)
@@ -646,9 +644,9 @@ class TestParsing:
def test_report_inside_block_does_not_close_it(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
" @2024-01-01 WCONHIST STATUS=OPEN\n"
" 2024-01-01 WCONHIST STATUS=OPEN\n"
"REPORT 2024-06-01\n"
" @2024-02-01 WELTARG CMODE=ORAT VALUE=5000\n"
" 2024-02-01 WELTARG CMODE=ORAT VALUE=5000\n"
)
doc = parse_orion_events(text)
assert [len(w.events) for w in doc.wells] == [2]
@@ -706,7 +704,7 @@ class TestParsing:
"ORIONEVENTS 2.0\n"
"REPORT 2024-01-01 EVERY MONTH\n"
'WELL "W"\n'
" @2024-03-15 WCONHIST STATUS=OPEN\n"
" 2024-03-15 WCONHIST STATUS=OPEN\n"
)
doc = parse_orion_events(text)
assert doc.report_dates == [
@@ -739,10 +737,28 @@ class TestParsing:
with pytest.raises(OrionParseError, match=message):
parse_orion_events(f"ORIONEVENTS 2.0\n{report_line}\n")
def test_event_dates_parse_without_prefix(self):
text = (
"ORIONEVENTS 2.0\nDATE START = 2024-05-15\n"
'WELL "W"\n'
" START + 1 PERFORATION MDSTART=1 MDEND=2\n"
" 2024-05-17 WCONHIST STATUS=OPEN\n"
)
doc = parse_orion_events(text)
assert [event.event_date for event in doc.wells[0].events] == [
datetime.date(2024, 5, 16),
datetime.date(2024, 5, 17),
]
def test_at_prefix_is_rejected(self):
text = 'ORIONEVENTS 2.0\nWELL "W"\n @2024-05-17 WCONHIST STATUS=OPEN\n'
with pytest.raises(OrionParseError, match="Unrecognized line"):
parse_orion_events(text)
def test_datetime_literal_event(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
" @2024-05-15T14:45:30.500 PERFORATION MDSTART=1 MDEND=2\n"
" 2024-05-15T14:45:30.500 PERFORATION MDSTART=1 MDEND=2\n"
)
doc = parse_orion_events(text)
assert doc.wells[0].events[0].event_date == datetime.datetime(
@@ -752,7 +768,7 @@ class TestParsing:
def test_datetime_with_day_offset(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
" @2024-05-15T14:45:30 + 2 PERFORATION MDSTART=1 MDEND=2\n"
" 2024-05-15T14:45:30 + 2 PERFORATION MDSTART=1 MDEND=2\n"
)
doc = parse_orion_events(text)
assert doc.wells[0].events[0].event_date == datetime.datetime(
@@ -762,7 +778,7 @@ class TestParsing:
def test_date_variable_with_time_of_day(self):
text = (
"ORIONEVENTS 2.0\nDATE T0 = 2024-05-15T14:45:30.500\n"
'WELL "W"\n @T0 + 1 PERFORATION MDSTART=1 MDEND=2\n'
'WELL "W"\n T0 + 1 PERFORATION MDSTART=1 MDEND=2\n'
)
doc = parse_orion_events(text)
assert doc.wells[0].events[0].event_date == datetime.datetime(
@@ -776,7 +792,7 @@ class TestFilterParsing:
"ORIONEVENTS 2.0\n"
+ decls
+ 'WELL "W"\n'
+ f" @2018-01-01 PERFORATION MDSTART=1 MDEND=2 FILTER={filter_value}\n"
+ f" 2018-01-01 PERFORATION MDSTART=1 MDEND=2 FILTER={filter_value}\n"
)
return parse_orion_events(text)
@@ -863,7 +879,7 @@ class TestFilterParsing:
def test_filter_on_non_perforation_event_is_plain_attribute(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
' @2018-01-01 WCONHIST STATUS=OPEN FILTER="PERMX > 200"\n'
' 2018-01-01 WCONHIST STATUS=OPEN FILTER="PERMX > 200"\n'
)
doc = parse_orion_events(text)
event = doc.wells[0].events[0]
@@ -917,7 +933,7 @@ class TestFilterParsing:
def test_filter_variable_in_date_context_raises(self):
text = (
'ORIONEVENTS 2.0\nFILTER F = "poro>1"\nWELL "W"\n'
" @F PERFORATION MDSTART=1 MDEND=2\n"
" F PERFORATION MDSTART=1 MDEND=2\n"
)
with pytest.raises(OrionParseError, match="is a FILTER .* but a DATE"):
parse_orion_events(text)
@@ -940,8 +956,8 @@ class TestDiagnostics:
"DATE A1_STARTUP = 2018-01-01\n"
"SET X = 2018-01-01\n" # line 3
'WELL "W"\n'
" @A1_STRTUP PERFORATION MDSTART=1 MDEND=2\n" # line 5
" @A1_STARTUP + NOPE WCONHIST STATUS=OPEN\n" # line 6
" A1_STRTUP PERFORATION MDSTART=1 MDEND=2\n" # line 5
" A1_STARTUP + NOPE WCONHIST STATUS=OPEN\n" # line 6
)
with pytest.raises(OrionParseError) as excinfo:
parse_orion_events(text)
@@ -951,8 +967,8 @@ class TestDiagnostics:
text = (
"ORIONEVENTS 2.0\n"
"WELL 55_33-A-2\n" # malformed: unquoted special characters
" @2018-01-01 PERFORATION MDSTART=1 MDEND=2\n"
" @2018-01-01 WCONHIST STATUS=OPEN\n"
" 2018-01-01 PERFORATION MDSTART=1 MDEND=2\n"
" 2018-01-01 WCONHIST STATUS=OPEN\n"
)
with pytest.raises(OrionParseError) as excinfo:
parse_orion_events(text)
@@ -962,7 +978,7 @@ class TestDiagnostics:
def test_unknown_variable_hint(self):
text = (
"ORIONEVENTS 2.0\nDATE A1_STARTUP = 2018-01-01\n"
'WELL "W"\n @A1_STRTUP PERFORATION MDSTART=1 MDEND=2\n'
'WELL "W"\n A1_STRTUP PERFORATION MDSTART=1 MDEND=2\n'
)
with pytest.raises(OrionParseError, match="did you mean 'A1_STARTUP'"):
parse_orion_events(text)
@@ -988,7 +1004,7 @@ class TestValidatorCli:
def test_invalid_file_exits_nonzero_with_errors(self, tmp_path, capsys):
path = self._write(
tmp_path,
'ORIONEVENTS 2.0\nSET X = 2018-01-01\nWELL "W"\n @NOPE WCONHIST A=1\n',
'ORIONEVENTS 2.0\nSET X = 2018-01-01\nWELL "W"\n NOPE WCONHIST A=1\n',
)
assert _cli([path]) == 1
out = capsys.readouterr().out
@@ -1097,12 +1113,12 @@ class TestApplying:
text = (
"ORIONEVENTS 2.0\n"
'WELL "55_33-A-1"\n'
" @2018-01-01 WCONHIST STATUS=OPEN ORAT=100\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"
" 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"
" 2018-01-01 WCONHIST STATUS=OPEN\n"
)
document = parse_orion_events(text)
merged = coalesce_orion_document(document)
@@ -1129,12 +1145,12 @@ class TestApplying:
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"
" 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"
" 2024-01-20 WCONHIST WRAT=0.04\n"
)
document = parse_orion_events(text)
merged = coalesce_orion_document(document)
@@ -1192,8 +1208,8 @@ class TestApplying:
text = (
"ORIONEVENTS 2.0\n"
'WELL "55_33-A-1"\n'
" @2018-01-01 PERFORATION MDSTART=1000 MDEND=1100 COMPLETION_NUMBER=1\n"
" @2018-01-01 PERFORATION MDSTART=1200 MDEND=1300 COMPLETION_NUMBER=2\n"
" 2018-01-01 PERFORATION MDSTART=1000 MDEND=1100 COMPLETION_NUMBER=1\n"
" 2018-01-01 PERFORATION MDSTART=1200 MDEND=1300 COMPLETION_NUMBER=2\n"
)
document = parse_orion_events(text)
@@ -1214,15 +1230,15 @@ class TestApplying:
text = (
"ORIONEVENTS 2.0\n"
'GROUP "OP"\n'
" @2018-01-01 GCONPROD CONTROL_MODE=ORAT\n"
" 2018-01-01 GCONPROD CONTROL_MODE=ORAT\n"
'GROUP "OP"\n'
" @2018-01-01 GCONPROD OIL_TARGET=100\n"
" 2018-01-01 GCONPROD OIL_TARGET=100\n"
'GROUP "OTHER"\n'
" @2018-01-01 GCONPROD OIL_TARGET=200\n"
" 2018-01-01 GCONPROD OIL_TARGET=200\n"
"SCHEDULE\n"
" @2018-01-01 RPTRST BASIC=1\n"
" 2018-01-01 RPTRST BASIC=1\n"
"SCHEDULE\n"
" @2018-01-01 RPTRST FREQ=2\n"
" 2018-01-01 RPTRST FREQ=2\n"
)
merged = coalesce_orion_document(parse_orion_events(text))
@@ -1263,7 +1279,7 @@ class TestApplying:
def test_comment_is_applied_to_timeline_event_not_keyword_data(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
' @2018-01-01 WCONHIST STATUS=OPEN COMMENT="Startup target"\n'
' 2018-01-01 WCONHIST STATUS=OPEN COMMENT="Startup target"\n'
)
timeline, report = self._apply(text)
@@ -1275,7 +1291,7 @@ class TestApplying:
def test_perforation_comment_is_applied(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2018-01-01 PERFORATION MDSTART=1 MDEND=2 COMMENT=Interval\n"
" 2018-01-01 PERFORATION MDSTART=1 MDEND=2 COMMENT=Interval\n"
)
timeline, report = self._apply(text)
@@ -1297,7 +1313,7 @@ class TestApplying:
# other attribute instead of being stripped.
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2018-01-01 WCONHIST STATUS=OPEN CMODE=ORAT DSHIFT=10\n"
" 2018-01-01 WCONHIST STATUS=OPEN CMODE=ORAT DSHIFT=10\n"
)
timeline, report = self._apply(text)
data = timeline.keyword_calls[0]["keyword_data"]
@@ -1308,7 +1324,7 @@ class TestApplying:
def test_report_dates_on_apply_report(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2018-01-01 WCONHIST STATUS=OPEN\n"
" 2018-01-01 WCONHIST STATUS=OPEN\n"
"REPORT 2018-07-01\n"
"REPORT 2018-03-01\n"
"REPORT 2018-07-01\n"
@@ -1324,7 +1340,7 @@ class TestApplying:
# unknown completion attribute.
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2018-01-01 PERFORATION MDSTART=1 MDEND=2 PERFID=Valysar\n"
" 2018-01-01 PERFORATION MDSTART=1 MDEND=2 PERFID=Valysar\n"
)
timeline, report = self._apply(text)
assert report.events_applied == 0
@@ -1335,7 +1351,7 @@ class TestApplying:
def test_filter_on_keyword_event_warns_and_applies(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
' @2018-01-01 WCONHIST STATUS=OPEN FILTER="PERMX > 200"\n'
' 2018-01-01 WCONHIST STATUS=OPEN FILTER="PERMX > 200"\n'
)
timeline, report = self._apply(text)
assert report.events_applied == 1
@@ -1355,7 +1371,7 @@ class TestApplying:
self._apply(SAMPLE, names=("55_33-A-1",), on_unknown_well="error")
def test_unknown_event_type_warns_with_hint(self):
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2018-01-01 WCONHST STATUS=OPEN\n'
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n 2018-01-01 WCONHST STATUS=OPEN\n'
_, report = self._apply(text)
assert report.events_skipped == 1
assert any(
@@ -1363,16 +1379,14 @@ class TestApplying:
)
def test_perforation_missing_required_attr_is_error(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2018-01-01 PERFORATION MDSTART=1\n'
)
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n 2018-01-01 PERFORATION MDSTART=1\n'
_, report = self._apply(text)
assert report.events_skipped == 1
assert any("MDEND" in e for e in report.errors)
assert 'Line 3 [WELL "55_33-A-1", date 2018-01-01]' in report.errors[0]
def test_perforation_unknown_attr_is_error(self):
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2018-01-01 PERFORATION MDSTART=1 MDEND=2 ZZZ=3\n'
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n 2018-01-01 PERFORATION MDSTART=1 MDEND=2 ZZZ=3\n'
_, report = self._apply(text)
assert report.events_skipped == 1
assert any("ZZZ" in e for e in report.errors)
@@ -1380,8 +1394,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 WELSPECS CROSSFLOW=False PHASE=gas\n"
" @2018-01-01 WELSPECS 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)
@@ -1413,7 +1427,7 @@ class TestApplying:
)
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 WELSPECS {attributes}\n'
f'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n 2018-01-01 WELSPECS {attributes}\n'
)
timeline, report = self._apply(text)
@@ -1425,7 +1439,7 @@ class TestApplying:
def test_segment_mapping_creates_custom_interval_and_sets_pressure_drop(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2024-01-01 SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 "
" 2024-01-01 SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 "
"ROUGHNESS=1.0e-5 PRESSURE_COMPONENTS=HFA\n"
)
timeline, report = self._apply(text)
@@ -1446,7 +1460,7 @@ class TestApplying:
def test_segment_rejects_invalid_pressure_components(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2024-01-01 SEGMENT MDSTART=0 MDEND=2500 "
" 2024-01-01 SEGMENT MDSTART=0 MDEND=2500 "
"PRESSURE_COMPONENTS=INVALID\n"
)
timeline, report = self._apply(text)
@@ -1459,7 +1473,7 @@ class TestApplying:
def test_tubing_is_reported_as_renamed_event(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2024-01-01 TUBING MDSTART=0 MDEND=2500\n"
" 2024-01-01 TUBING MDSTART=0 MDEND=2500\n"
)
timeline, report = self._apply(text)
@@ -1468,7 +1482,7 @@ class TestApplying:
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'
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
@@ -1478,7 +1492,7 @@ class TestApplying:
def test_valve_mapping(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2024-03-01 VALVE MD=2100 TYPE=ICV STATE=OPEN CV=0.7 AREA=0.0001\n"
" 2024-03-01 VALVE MD=2100 TYPE=ICV STATE=OPEN CV=0.7 AREA=0.0001\n"
)
timeline, report = self._apply(text)
assert report.events_applied == 1
@@ -1490,13 +1504,13 @@ class TestApplying:
assert call["area"] == pytest.approx(0.0001)
def test_valve_missing_type_is_error(self):
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2024-03-01 VALVE MD=2100\n'
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n 2024-03-01 VALVE MD=2100\n'
_, report = self._apply(text)
assert report.events_skipped == 1
assert any("TYPE" in e for e in report.errors)
def test_state_mapping(self):
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2024-02-15 STATE STATE=SHUT\n'
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n 2024-02-15 STATE STATE=SHUT\n'
timeline, report = self._apply(text)
assert report.events_applied == 1
assert timeline.state_calls[0]["well_state"] == "SHUT"
@@ -1504,7 +1518,7 @@ class TestApplying:
def test_generic_well_keyword_pass_through(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2024-06-01 WRFTPLT OUTPUT_RFT=YES OUTPUT_PLT=NO OUTPUT_SEGMENT=NO\n"
" 2024-06-01 WRFTPLT OUTPUT_RFT=YES OUTPUT_PLT=NO OUTPUT_SEGMENT=NO\n"
)
timeline, report = self._apply(text)
assert report.events_applied == 1
@@ -1517,7 +1531,7 @@ class TestApplying:
def test_typo_of_builtin_is_not_passed_through(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2024-01-01 PERFORATIN MDSTART=1 MDEND=2\n"
" 2024-01-01 PERFORATIN MDSTART=1 MDEND=2\n"
)
timeline, report = self._apply(text)
assert report.events_skipped == 1
@@ -1527,8 +1541,8 @@ class TestApplying:
def test_schedule_events_applied_without_well(self):
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n"
" @2024-01-01 RPTRST BASIC=2 FREQ=1\n"
" @2024-01-01 TUNING TSINIT=1 TSMAXZ=30\n"
" 2024-01-01 RPTRST BASIC=2 FREQ=1\n"
" 2024-01-01 TUNING TSINIT=1 TSMAXZ=30\n"
)
timeline, report = self._apply(text)
assert report.events_applied == 2
@@ -1538,7 +1552,7 @@ class TestApplying:
assert "WELL" not in rptrst["keyword_data"]
def test_restart_event_creates_non_emitting_timeline_marker(self):
text = "ORIONEVENTS 2.0\nSCHEDULE\n @2024-02-01 RESTART\n"
text = "ORIONEVENTS 2.0\nSCHEDULE\n 2024-02-01 RESTART\n"
timeline, report = self._apply(text)
assert report.events_applied == 1
@@ -1554,9 +1568,9 @@ class TestApplying:
def test_raw_text_event_is_applied_without_coalescing(self):
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n"
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=2\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=2\n"
"first\nEND_RAW_TEXT\n"
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=1\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=1\n"
"second\nEND_RAW_TEXT\n"
)
timeline, report = self._apply(text)
@@ -1577,10 +1591,10 @@ class TestApplying:
def test_group_events_inject_group_name(self):
text = (
'ORIONEVENTS 2.0\nGROUP "OP"\n'
" @2020-07-01 GEFAC EFFICIENCY_FACTOR=1.0 USE_GEFAC_IN_NETWORK=YES\n"
" @2020-07-01 GCONPROD CONTROL_MODE=LRAT LIQUID_TARGET=20000 WATER_TARGET=20000 OIL_TARGET=20000\n"
" 2020-07-01 GEFAC EFFICIENCY_FACTOR=1.0 USE_GEFAC_IN_NETWORK=YES\n"
" 2020-07-01 GCONPROD CONTROL_MODE=LRAT LIQUID_TARGET=20000 WATER_TARGET=20000 OIL_TARGET=20000\n"
'GROUP "WI"\n'
" @2020-07-01 GCONINJE PHASE=WATER CONTROL_MODE=RATE SURFACE_TARGET=16000\n"
" 2020-07-01 GCONINJE PHASE=WATER CONTROL_MODE=RATE SURFACE_TARGET=16000\n"
)
timeline, report = self._apply(text)
assert report.events_applied == 3
@@ -1600,7 +1614,7 @@ class TestApplying:
def test_member_event_expands_to_unique_grouptree_events(self):
text = (
'ORIONEVENTS 2.0\nGROUP "PRODUCERS"\n'
' @2024-01-01 MEMBER MEMBERS="WELL_A, WELL_B,WELL_A" '
' 2024-01-01 MEMBER MEMBERS="WELL_A, WELL_B,WELL_A" '
'COMMENT="Group membership"\n'
)
timeline, report = self._apply(text)
@@ -1633,7 +1647,7 @@ class TestApplying:
],
)
def test_invalid_member_event_is_skipped(self, block, event, expected_error):
text = f"ORIONEVENTS 2.0\n{block}\n @2024-01-01 {event}\n"
text = f"ORIONEVENTS 2.0\n{block}\n 2024-01-01 {event}\n"
timeline, report = self._apply(text)
assert report.events_applied == 0
@@ -1644,9 +1658,7 @@ class TestApplying:
assert f"[{expected_scope}, date 2024-01-01]" in report.errors[0]
def test_completion_event_in_schedule_block_is_error(self):
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n @2024-01-01 PERFORATION MDSTART=1 MDEND=2\n"
)
text = "ORIONEVENTS 2.0\nSCHEDULE\n 2024-01-01 PERFORATION MDSTART=1 MDEND=2\n"
timeline, report = self._apply(text)
assert report.events_skipped == 1
assert timeline.schedule_keyword_calls == []
@@ -1655,7 +1667,7 @@ class TestApplying:
def test_datetime_event_date_keeps_milliseconds(self):
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" @2024-05-15T14:45:30.500 PERFORATION MDSTART=1 MDEND=2\n"
" 2024-05-15T14:45:30.500 PERFORATION MDSTART=1 MDEND=2\n"
)
timeline, _ = self._apply(text)
assert timeline.perf_calls[0]["event_date"] == "2024-05-15T14:45:30.500"
@@ -1685,7 +1697,7 @@ class TestFilterApplying:
def _perf_text(self, decls, *filter_values):
events = "".join(
f" @2018-01-{index:02d} PERFORATION MDSTART=1 MDEND=2 FILTER={value}\n"
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
@@ -1772,7 +1784,7 @@ class TestFilterApplying:
self._apply(text, cases=[])
def test_no_filter_without_case_is_fine(self):
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2018-01-01 PERFORATION MDSTART=1 MDEND=2\n'
text = 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n 2018-01-01 PERFORATION MDSTART=1 MDEND=2\n'
_, _, report = self._apply(text, cases=[])
assert report.events_applied == 1
@@ -1800,7 +1812,7 @@ class TestFilterApplying:
def test_declared_but_unused_filter_creates_nothing(self):
text = (
'ORIONEVENTS 2.0\nFILTER unused = "poro>0.4"\nWELL "55_33-A-1"\n'
" @2018-01-01 PERFORATION MDSTART=1 MDEND=2\n"
" 2018-01-01 PERFORATION MDSTART=1 MDEND=2\n"
)
_, project, report = self._apply(text)
assert report.events_applied == 1
@@ -1837,7 +1849,7 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2024-01-01 WCONHIST STATUS=OPEN INVALID_FIELD=1.0\n"
" 2024-01-01 WCONHIST STATUS=OPEN INVALID_FIELD=1.0\n"
)
report = apply_orion_document(document, timeline, project)
@@ -1860,8 +1872,8 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2024-01-01 NOT_A_KEYWORD VALUE=1\n"
" @2024-01-02 WCONHIST STATUS=OPEN\n"
" 2024-01-01 NOT_A_KEYWORD VALUE=1\n"
" 2024-01-02 WCONHIST STATUS=OPEN\n"
)
report = apply_orion_document(document, timeline, project)
@@ -1881,9 +1893,9 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2024-01-01 WCONHIST STATUS=OPEN\n"
" 2024-01-01 WCONHIST STATUS=OPEN\n"
"SCHEDULE\n"
" @2024-01-01 RPTRST BASIC=2 DEN=True ROCKC=True RPORV=True "
" 2024-01-01 RPTRST BASIC=2 DEN=True ROCKC=True RPORV=True "
"RFIP=True FLOWS=True FLORES=True NORST=False\n"
)
@@ -1908,17 +1920,17 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2024-01-01 WCONHIST STATUS=OPEN\n"
" 2024-01-01 WCONHIST STATUS=OPEN\n"
"SCHEDULE\n"
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=5\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=5\n"
"-- after-date-late\nEND_RAW_TEXT\n"
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=-1\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=-1\n"
"-- after-date-early\nEND_RAW_TEXT\n"
" @2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD ANCHOR=WCONHIST\n"
" 2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD ANCHOR=WCONHIST\n"
"-- before-wconhist\nEND_RAW_TEXT\n"
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_KEYWORD ANCHOR=WCONHIST\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_KEYWORD ANCHOR=WCONHIST\n"
"-- after-wconhist\nEND_RAW_TEXT\n"
" @2024-01-01 RAW_TEXT PLACEMENT=END_OF_DATE\n"
" 2024-01-01 RAW_TEXT PLACEMENT=END_OF_DATE\n"
"-- end-of-date\nEND_RAW_TEXT\n"
)
@@ -1946,7 +1958,7 @@ class TestOrionEventsIntegration:
project, case, timeline = project_with_case_and_wells
document = parse_orion_events(
"ORIONEVENTS 2.0\nSCHEDULE\n"
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\n"
" 2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\n"
"-- raw-only\nEND_RAW_TEXT\n"
)
@@ -1966,7 +1978,7 @@ class TestOrionEventsIntegration:
project, case, timeline = project_with_case_and_wells
document = parse_orion_events(
"ORIONEVENTS 2.0\nSCHEDULE\n"
" @2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD ANCHOR=COMPDAT\n"
" 2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD ANCHOR=COMPDAT\n"
"text\nEND_RAW_TEXT\n"
)
@@ -1982,7 +1994,7 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
' @2024-01-01 WCONHIST STATUS=OPEN COMMENT="Startup target"\n'
' 2024-01-01 WCONHIST STATUS=OPEN COMMENT="Startup target"\n'
)
report = apply_orion_document(document, timeline, project)
@@ -1998,13 +2010,13 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2020-07-01 WCONHIST STATUS=OPEN CMODE=ORAT\n"
" 2020-07-01 WCONHIST STATUS=OPEN CMODE=ORAT\n"
'GROUP "OP"\n'
" @2020-07-01 GEFAC EFFICIENCY_FACTOR=1.0 USE_GEFAC_IN_NETWORK=YES\n"
" @2020-07-01 GCONPROD CONTROL_MODE=LRAT LIQUID_TARGET=20000 "
" 2020-07-01 GEFAC EFFICIENCY_FACTOR=1.0 USE_GEFAC_IN_NETWORK=YES\n"
" 2020-07-01 GCONPROD CONTROL_MODE=LRAT LIQUID_TARGET=20000 "
"WATER_TARGET=20000 OIL_TARGET=20000\n"
'GROUP "WI"\n'
" @2020-07-01 GCONINJE PHASE=WATER CONTROL_MODE=RATE "
" 2020-07-01 GCONINJE PHASE=WATER CONTROL_MODE=RATE "
"SURFACE_TARGET=16000\n"
)
@@ -2027,9 +2039,9 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2024-01-01 WCONHIST STATUS=OPEN\n"
" 2024-01-01 WCONHIST STATUS=OPEN\n"
'GROUP "PRODUCERS"\n'
' @2024-01-01 MEMBER MEMBERS="WELL_A,WELL_B"\n'
' 2024-01-01 MEMBER MEMBERS="WELL_A,WELL_B"\n'
)
report = apply_orion_document(document, timeline, project)
@@ -2050,8 +2062,8 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2018-01-01 WELSPECS GROUP=my_group REFDEPTH=1002 PHASE=water\n"
" @2019-01-01 WELSPECS 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)
@@ -2097,13 +2109,13 @@ class TestOrionEventsIntegration:
document = parse_orion_events(
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
" @2024-01-01 WCONHIST STATUS=OPEN CMODE=ORAT ORAT=100\n"
" @2024-02-01 WCONHIST STATUS=OPEN CMODE=ORAT ORAT=200\n"
" @2024-03-01 WCONHIST STATUS=OPEN CMODE=ORAT ORAT=300\n"
" 2024-01-01 WCONHIST STATUS=OPEN CMODE=ORAT ORAT=100\n"
" 2024-02-01 WCONHIST STATUS=OPEN CMODE=ORAT ORAT=200\n"
" 2024-03-01 WCONHIST STATUS=OPEN CMODE=ORAT ORAT=300\n"
"REPORT 2024-01-15\n"
"REPORT 2024-04-01\n"
"SCHEDULE\n"
" @2024-02-01 RESTART\n"
" 2024-02-01 RESTART\n"
)
report = apply_orion_document(document, timeline, project)
@@ -2154,9 +2166,9 @@ class TestOrionEventsIntegration:
"DATE START = 2024-01-01\n"
"DURATION RAMP = 5 DAYS\n"
f'WELL "{well.name}"\n'
" @START PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 SKIN=0.5 COMPLETION_NUMBER=1\n"
" @START + RAMP WCONHIST STATUS=OPEN CMODE=ORAT VFP=1\n"
" @START + RAMP WELTARG CMODE=BHP VALUE=50\n"
" START PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 SKIN=0.5 COMPLETION_NUMBER=1\n"
" START + RAMP WCONHIST STATUS=OPEN CMODE=ORAT VFP=1\n"
" START + RAMP WELTARG CMODE=BHP VALUE=50\n"
"REPORT 2024-07-01\n"
)
document = parse_orion_events(text)
@@ -2199,18 +2211,18 @@ class TestOrionEventsIntegration:
"DATE STARTUP = 2024-01-01\n"
"DURATION RAMP = 31 DAYS\n"
f'WELL "{well.name}"\n'
" @STARTUP SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 ROUGHNESS=1.0e-5 PRESSURE_COMPONENTS=HFA\n"
" @STARTUP + RAMP PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 SKIN=0.5 COMPLETION_NUMBER=1\n"
" @2024-05-15T14:45:30.500 PERFORATION MDSTART=2300 MDEND=2350 RADIUS=0.05 SKIN=0.4 COMPLETION_NUMBER=2\n"
" @2024-03-01 VALVE MD=2100 TYPE=ICV STATE=OPEN CV=0.7 AREA=0.0001\n"
" @2024-02-15 STATE STATE=OPEN\n"
" @2024-01-15 WCONHIST STATUS=OPEN CMODE=RESV ORAT=3999.99 VFP=1\n"
" @2024-05-01 WELTARG CMODE=ORAT VALUE=5000.0\n"
" @2024-06-01 WRFTPLT OUTPUT_RFT=YES OUTPUT_PLT=NO OUTPUT_SEGMENT=NO\n"
" STARTUP SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 ROUGHNESS=1.0e-5 PRESSURE_COMPONENTS=HFA\n"
" STARTUP + RAMP PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 SKIN=0.5 COMPLETION_NUMBER=1\n"
" 2024-05-15T14:45:30.500 PERFORATION MDSTART=2300 MDEND=2350 RADIUS=0.05 SKIN=0.4 COMPLETION_NUMBER=2\n"
" 2024-03-01 VALVE MD=2100 TYPE=ICV STATE=OPEN CV=0.7 AREA=0.0001\n"
" 2024-02-15 STATE STATE=OPEN\n"
" 2024-01-15 WCONHIST STATUS=OPEN CMODE=RESV ORAT=3999.99 VFP=1\n"
" 2024-05-01 WELTARG CMODE=ORAT VALUE=5000.0\n"
" 2024-06-01 WRFTPLT OUTPUT_RFT=YES OUTPUT_PLT=NO OUTPUT_SEGMENT=NO\n"
"SCHEDULE\n"
" @STARTUP RPTRST BASIC=2 FREQ=1\n"
" @STARTUP GRUPTREE CHILD_GROUP=OP PARENT_GROUP=FIELD\n"
" @STARTUP TUNING TSINIT=1 TSMAXZ=30 NEWTMX=12\n"
" STARTUP RPTRST BASIC=2 FREQ=1\n"
" STARTUP GRUPTREE CHILD_GROUP=OP PARENT_GROUP=FIELD\n"
" STARTUP TUNING TSINIT=1 TSMAXZ=30 NEWTMX=12\n"
)
document = parse_orion_events(text)
report = apply_orion_document(document, timeline, project)
@@ -2249,7 +2261,7 @@ class TestOrionEventsIntegration:
project, _case, timeline = project_with_case_and_wells
text = (
'ORIONEVENTS 2.0\nWELL "NO_SUCH_WELL"\n'
" @2024-01-01 PERFORATION MDSTART=1 MDEND=2\n"
" 2024-01-01 PERFORATION MDSTART=1 MDEND=2\n"
)
document = parse_orion_events(text)
report = apply_orion_document(document, timeline, project)
@@ -2267,7 +2279,7 @@ class TestOrionEventsIntegration:
"ORIONEVENTS 2.0\n"
'FILTER hiperm = "PERMX > 50.0"\n'
f'WELL "{well.name}"\n'
" @2024-01-01 PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 FILTER=hiperm\n"
" 2024-01-01 PERFORATION MDSTART=2000 MDEND=2200 RADIUS=0.05 FILTER=hiperm\n"
)
document = parse_orion_events(text)
report = apply_orion_document(document, timeline, project, case=case)
@@ -2303,7 +2315,7 @@ class TestOrionEventsIntegration:
text = (
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
' @2024-01-01 PERFORATION MDSTART=2000 MDEND=2200 FILTER="static.PERMX >= 50"\n'
' 2024-01-01 PERFORATION MDSTART=2000 MDEND=2200 FILTER="static.PERMX >= 50"\n'
)
document = parse_orion_events(text)
report = apply_orion_document(document, timeline, project, case=case)
@@ -2326,7 +2338,7 @@ class TestOrionEventsIntegration:
text = (
"ORIONEVENTS 2.0\n"
f'WELL "{well.name}"\n'
' @2024-01-01 PERFORATION MDSTART=2000 MDEND=2200 FILTER="NO_SUCH_RESULT > 1"\n'
' 2024-01-01 PERFORATION MDSTART=2000 MDEND=2200 FILTER="NO_SUCH_RESULT > 1"\n'
)
document = parse_orion_events(text)
with pytest.raises(RipsError, match="NO_SUCH_RESULT"):