mirror of
https://github.com/OPM/ResInsight.git
synced 2026-08-26 21:27:15 -05:00
#14533 Orion events: Add group sections
This commit is contained in:
@@ -23,7 +23,7 @@ File format grammar, version 2.0 (EBNF-ish)::
|
||||
document = header , { statement } ;
|
||||
header = "ORIONEVENTS" , "2.0" ; (* first meaningful line *)
|
||||
statement = unit_directive | declaration | report_line | well_block_open
|
||||
| schedule_block_open | event_line ;
|
||||
| group_block_open | schedule_block_open | event_line ;
|
||||
unit_directive = "UNIT" , ( "METRIC" | "FIELD" | "LAB" ) ;
|
||||
report_line = "REPORT" , date_expr ; (* REPORT 2024-06-01 *)
|
||||
|
||||
@@ -39,6 +39,7 @@ File format grammar, version 2.0 (EBNF-ish)::
|
||||
comp_op = ">" | ">=" | "<" | "<=" ;
|
||||
|
||||
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 } ;
|
||||
|
||||
@@ -57,8 +58,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``,
|
||||
``SCHEDULE`` or ``@``. Anything else is an error. Keywords are uppercase and
|
||||
case-sensitive (the ``DAYS`` suffix is also accepted as ``days``).
|
||||
``GROUP``, ``SCHEDULE`` or ``@``. 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``
|
||||
(well-name alias) and ``FILTER`` (cell filter expression) declarations share
|
||||
@@ -74,8 +75,10 @@ Notes on the grammar:
|
||||
* ``WELL <ident>`` opens an event block for a declared ``WELL`` alias;
|
||||
``WELL "<name>"`` opens a block for the literal well name and never consults
|
||||
variables. A ``WELL`` line containing ``=`` is always a declaration. A bare
|
||||
``SCHEDULE`` line opens a block of schedule-level keyword events not tied to
|
||||
any well (RPTRST, GRUPTREE, TUNING, ...). Empty blocks are legal.
|
||||
``GROUP "<name>"`` opens a block of group-level Eclipse keyword events; the
|
||||
group name is injected as the ``GROUP`` item when each event is applied.
|
||||
A bare ``SCHEDULE`` line opens a block of schedule-level keyword events not
|
||||
tied to any well (RPTRST, GRUPTREE, TUNING, ...). Empty blocks are legal.
|
||||
* ``REPORT <date_expr>`` (one date per line, anywhere after the header) names
|
||||
a date that should appear as a bare ``DATES`` keyword in the generated
|
||||
schedule even when no events fall on it — in Eclipse/Flow a ``DATES`` entry
|
||||
@@ -91,9 +94,11 @@ Notes on the grammar:
|
||||
``PERFORATION``, ``TUBING``, ``VALVE`` and ``STATE``, or any Eclipse well
|
||||
keyword (``WCONHIST``, ``WELTARG``, ``WRFTPLT``, ``WCONPROD``, ...), which
|
||||
is passed through generically with the well name injected as WELL. Event
|
||||
types inside a SCHEDULE block are Eclipse schedule keywords passed through
|
||||
as-is. An event type that closely resembles a built-in is treated as a typo
|
||||
per the ``on_unknown_event`` policy instead of being passed through.
|
||||
types inside a GROUP block are Eclipse group keywords with the group name
|
||||
injected as GROUP. Event types inside a SCHEDULE block are Eclipse schedule
|
||||
keywords passed through as-is. An event type that closely resembles a built-in
|
||||
is treated as a typo per the ``on_unknown_event`` policy instead of being
|
||||
passed through.
|
||||
* On a PERFORATION event, ``FILTER=<name>`` references a declared ``FILTER``
|
||||
variable and ``FILTER="<expr>"`` is an inline anonymous filter expression.
|
||||
The applier materializes each used filter as a case-level combined data
|
||||
@@ -242,7 +247,7 @@ class AttrValue:
|
||||
|
||||
@dataclass
|
||||
class OrionEvent:
|
||||
"""One event line: a dated action on the enclosing WELL or SCHEDULE block."""
|
||||
"""One event line in an enclosing WELL, GROUP or SCHEDULE block."""
|
||||
|
||||
event_type: str
|
||||
event_date: Union[datetime.date, datetime.datetime]
|
||||
@@ -260,6 +265,15 @@ class WellBlock:
|
||||
loc: SourceLoc = SourceLoc(0, "")
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupBlock:
|
||||
"""A ``GROUP`` block header followed by its keyword events."""
|
||||
|
||||
group_name: str
|
||||
events: List[OrionEvent] = field(default_factory=list)
|
||||
loc: SourceLoc = SourceLoc(0, "")
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrionDocument:
|
||||
"""Parsed, lossless representation of an ORIONEVENTS file."""
|
||||
@@ -268,6 +282,7 @@ class OrionDocument:
|
||||
unit_system: str = "METRIC"
|
||||
variables: Dict[str, OrionValue] = field(default_factory=dict)
|
||||
wells: List[WellBlock] = field(default_factory=list)
|
||||
groups: List[GroupBlock] = field(default_factory=list)
|
||||
schedule_events: List[OrionEvent] = field(default_factory=list)
|
||||
report_dates: List[Union[datetime.date, datetime.datetime]] = field(
|
||||
default_factory=list
|
||||
@@ -286,6 +301,7 @@ _KEYWORDS = (
|
||||
"DURATION",
|
||||
"WELL",
|
||||
"FILTER",
|
||||
"GROUP",
|
||||
"SCHEDULE",
|
||||
"REPORT",
|
||||
)
|
||||
@@ -320,6 +336,7 @@ _RESULT_TYPE_ALIASES = {
|
||||
"GENERATED": "GENERATED",
|
||||
}
|
||||
_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>.+)$")
|
||||
_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+))')
|
||||
@@ -342,6 +359,7 @@ def parse_orion_events(text: str) -> OrionDocument:
|
||||
unit_holder = ["METRIC"] # mutable so _parse_line can update it
|
||||
variables: Dict[str, OrionValue] = {}
|
||||
wells: List[WellBlock] = []
|
||||
groups: List[GroupBlock] = []
|
||||
schedule_events: List[OrionEvent] = []
|
||||
report_dates: List[Union[datetime.date, datetime.datetime]] = []
|
||||
warnings: List[ParseWarning] = []
|
||||
@@ -374,6 +392,7 @@ def parse_orion_events(text: str) -> OrionDocument:
|
||||
loc,
|
||||
variables,
|
||||
wells,
|
||||
groups,
|
||||
schedule_events,
|
||||
report_dates,
|
||||
warnings,
|
||||
@@ -382,7 +401,7 @@ def parse_orion_events(text: str) -> OrionDocument:
|
||||
)
|
||||
except OrionParseError as exc:
|
||||
errors.extend(exc.errors)
|
||||
if line.split(None, 1)[0] in ("WELL", "SCHEDULE") or (
|
||||
if line.split(None, 1)[0] in ("WELL", "GROUP", "SCHEDULE") or (
|
||||
line.startswith("@") and current_events is None
|
||||
):
|
||||
# Suppress cascading errors from lines belonging to a broken
|
||||
@@ -400,6 +419,7 @@ def parse_orion_events(text: str) -> OrionDocument:
|
||||
unit_system=unit_holder[0],
|
||||
variables=variables,
|
||||
wells=wells,
|
||||
groups=groups,
|
||||
schedule_events=schedule_events,
|
||||
report_dates=report_dates,
|
||||
warnings=warnings,
|
||||
@@ -428,6 +448,7 @@ def _parse_line(
|
||||
loc: SourceLoc,
|
||||
variables: Dict[str, OrionValue],
|
||||
wells: List[WellBlock],
|
||||
groups: List[GroupBlock],
|
||||
schedule_events: List[OrionEvent],
|
||||
report_dates: List[Union[datetime.date, datetime.datetime]],
|
||||
warnings: List[ParseWarning],
|
||||
@@ -454,6 +475,17 @@ def _parse_line(
|
||||
unit_holder[0] = match.group("unit")
|
||||
return current_events
|
||||
|
||||
if first == "GROUP":
|
||||
match = _GROUP_BLOCK_RE.match(line)
|
||||
if match is None:
|
||||
raise OrionParseError(
|
||||
f'Malformed GROUP line: {line!r} (expected GROUP "<group-name>")',
|
||||
loc,
|
||||
)
|
||||
new_group = GroupBlock(group_name=match.group("name"), loc=loc)
|
||||
groups.append(new_group)
|
||||
return new_group.events
|
||||
|
||||
if first == "SCHEDULE":
|
||||
if line != "SCHEDULE":
|
||||
raise OrionParseError(
|
||||
@@ -979,6 +1011,10 @@ def apply_orion_document(
|
||||
dispatch = _apply_generic_well_keyword
|
||||
dispatch(event, well_path, timeline, report, ctx)
|
||||
|
||||
for group in document.groups:
|
||||
for event in group.events:
|
||||
_apply_schedule_event(event, timeline, report, group.group_name)
|
||||
|
||||
for event in document.schedule_events:
|
||||
_apply_schedule_event(event, timeline, report)
|
||||
|
||||
@@ -1116,14 +1152,17 @@ def _suspected_typo(event_type: str) -> Optional[str]:
|
||||
|
||||
|
||||
def _apply_schedule_event(
|
||||
event: OrionEvent, timeline: Any, report: ApplyReport
|
||||
event: OrionEvent,
|
||||
timeline: Any,
|
||||
report: ApplyReport,
|
||||
group_name: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Apply one SCHEDULE-block event as a schedule-level Eclipse keyword."""
|
||||
"""Apply one GROUP- or SCHEDULE-block event as an Eclipse keyword."""
|
||||
event_type = event.event_type.upper()
|
||||
if event_type in _COMPLETION_EVENT_TYPES:
|
||||
report.errors.append(
|
||||
f"Line {event.loc.line}: {event_type} is a completion event and "
|
||||
"needs a WELL block, not SCHEDULE"
|
||||
"needs a WELL block, not GROUP or SCHEDULE"
|
||||
)
|
||||
report.events_skipped += 1
|
||||
return
|
||||
@@ -1137,6 +1176,8 @@ def _apply_schedule_event(
|
||||
)
|
||||
continue
|
||||
keyword_data[key] = attr.value
|
||||
if group_name is not None:
|
||||
keyword_data["GROUP"] = group_name
|
||||
|
||||
timeline.add_keyword_event(
|
||||
event_date=_iso_event_date(event.event_date),
|
||||
@@ -1442,6 +1483,7 @@ def _cli(argv: Optional[List[str]] = None) -> int:
|
||||
return 1
|
||||
|
||||
event_count = sum(len(well.events) for well in document.wells)
|
||||
group_event_count = sum(len(group.events) for group in document.groups)
|
||||
print(
|
||||
f"{args.file}: OK (ORIONEVENTS {document.version}, "
|
||||
f"units {document.unit_system})"
|
||||
@@ -1449,6 +1491,7 @@ def _cli(argv: Optional[List[str]] = None) -> int:
|
||||
print(
|
||||
f" {len(document.variables)} variable(s), {len(document.wells)} "
|
||||
f"well block(s), {event_count} well event(s), "
|
||||
f"{len(document.groups)} group block(s), {group_event_count} group event(s), "
|
||||
f"{len(document.schedule_events)} schedule event(s), "
|
||||
f"{len(document.report_dates)} report date(s)"
|
||||
)
|
||||
|
||||
@@ -393,6 +393,34 @@ class TestParsing:
|
||||
# WELL after SCHEDULE switches the sink back to the well block.
|
||||
assert [len(w.events) for w in doc.wells] == [1, 1]
|
||||
|
||||
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"
|
||||
'GROUP "WI"\n'
|
||||
" @2020-07-01 GCONINJE TYPE=WATER CMODE=RATE RATE=16000\n"
|
||||
"SCHEDULE\n"
|
||||
" @2020-07-01 RPTRST BASIC=2\n"
|
||||
)
|
||||
doc = parse_orion_events(text)
|
||||
assert [group.group_name for group in doc.groups] == ["OP", "WI"]
|
||||
assert [event.event_type for event in doc.groups[0].events] == [
|
||||
"GEFAC",
|
||||
"GCONPROD",
|
||||
]
|
||||
assert [event.event_type for event in doc.groups[1].events] == ["GCONINJE"]
|
||||
assert [event.event_type for event in doc.schedule_events] == ["RPTRST"]
|
||||
|
||||
def test_empty_group_block_ok(self):
|
||||
doc = parse_orion_events('ORIONEVENTS 2.0\nGROUP "OP"\n')
|
||||
assert doc.groups[0].group_name == "OP"
|
||||
assert doc.groups[0].events == []
|
||||
|
||||
def test_malformed_group_line_rejected(self):
|
||||
with pytest.raises(OrionParseError, match="Malformed GROUP line"):
|
||||
parse_orion_events("ORIONEVENTS 2.0\nGROUP OP\n")
|
||||
|
||||
def test_boolean_attributes_are_typed_unless_quoted(self):
|
||||
text = (
|
||||
"ORIONEVENTS 2.0\n"
|
||||
@@ -919,6 +947,29 @@ class TestApplying:
|
||||
assert rptrst["keyword_data"] == {"BASIC": 2, "FREQ": 1}
|
||||
assert "WELL" not in rptrst["keyword_data"]
|
||||
|
||||
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"
|
||||
'GROUP "WI"\n'
|
||||
" @2020-07-01 GCONINJE PHASE=WATER CONTROL_MODE=RATE SURFACE_TARGET=16000\n"
|
||||
)
|
||||
timeline, report = self._apply(text)
|
||||
assert report.events_applied == 3
|
||||
assert [call["keyword_name"] for call in timeline.schedule_keyword_calls] == [
|
||||
"GEFAC",
|
||||
"GCONPROD",
|
||||
"GCONINJE",
|
||||
]
|
||||
assert timeline.schedule_keyword_calls[0]["keyword_data"] == {
|
||||
"GROUP": "OP",
|
||||
"EFFICIENCY_FACTOR": 1.0,
|
||||
"USE_GEFAC_IN_NETWORK": "YES",
|
||||
}
|
||||
assert timeline.schedule_keyword_calls[1]["keyword_data"]["GROUP"] == "OP"
|
||||
assert timeline.schedule_keyword_calls[2]["keyword_data"]["GROUP"] == "WI"
|
||||
|
||||
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"
|
||||
@@ -1158,6 +1209,35 @@ class TestOrionEventsIntegration:
|
||||
assert f"{flag}=True" not in rptrst_block
|
||||
assert "NORST" not in tokens
|
||||
|
||||
def test_group_sections_generate_group_keywords(self, project_with_case_and_wells):
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
well = project.well_paths()[0]
|
||||
document = parse_orion_events(
|
||||
"ORIONEVENTS 2.0\n"
|
||||
f'WELL "{well.name}"\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 "
|
||||
"WATER_TARGET=20000 OIL_TARGET=20000\n"
|
||||
'GROUP "WI"\n'
|
||||
" @2020-07-01 GCONINJE PHASE=WATER CONTROL_MODE=RATE "
|
||||
"SURFACE_TARGET=16000\n"
|
||||
)
|
||||
|
||||
report = apply_orion_document(document, timeline, project)
|
||||
assert report.errors == []
|
||||
assert report.events_applied == 4
|
||||
|
||||
schedule = timeline.generate_schedule_text(
|
||||
eclipse_case=case, export_msw_for_wells=[]
|
||||
)
|
||||
assert "GEFAC" in schedule
|
||||
assert "GCONPROD" in schedule
|
||||
assert "GCONINJE" in schedule
|
||||
assert "'OP'" in schedule
|
||||
assert "'WI'" in schedule
|
||||
|
||||
def test_apply_creates_perforations_and_schedule(self, project_with_case_and_wells):
|
||||
"""End-to-end: parse -> apply -> set_timestamp -> generate schedule."""
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
|
||||
Reference in New Issue
Block a user