#14637 Orion Events: Rename report date directive

This commit is contained in:
Kristian Bendiksen
2026-09-01 11:56:59 +02:00
committed by Magne Sjaastad
parent 753cd65672
commit d226e10e89
4 changed files with 92 additions and 64 deletions
@@ -25,7 +25,7 @@ It demonstrates the full event coverage of the format:
7. A GROUP-level MEMBER event expanded to one GRUPTREE record per member
8. SCHEDULE-level keyword events not tied to a well: RPTRST, GRUPTREE, TUNING
9. Multiline RAW_TEXT inserted at a chosen position without parsing its contents
10. Recurring REPORT dates with explicit and implicit end dates, passed to
10. Recurring INSERT_DATE directives with explicit and implicit end dates, passed to
generate_schedule_text(additional_dates=...) as summary-report triggers
11. Schedule metadata, COMPORD generation and aligned-column output
@@ -114,10 +114,10 @@ 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.
REPORT STARTUP EVERY MONTH
REPORT 2024-07-01 EVERY 3 MONTHS UNTIL STARTUP + 365
# Recurring inserted dates become bare DATES keywords. The first series ends at
# the last event; the second uses an explicit inclusive end date.
INSERT_DATE STARTUP EVERY MONTH
INSERT_DATE 2024-07-01 EVERY 3 MONTHS UNTIL STARTUP + 365
"""
@@ -177,7 +177,7 @@ def main():
)
print(f" Events applied: {report.events_applied}")
print(f" Events skipped: {report.events_skipped}")
print(f" Report dates: {report.report_dates}")
print(f" Inserted dates: {report.report_dates}")
for warning in report.warnings:
print(f" WARNING: {warning}")
for error in report.errors:
@@ -210,7 +210,7 @@ def main():
if case is None:
print(" No Eclipse case loaded - skipping schedule generation.")
return
# REPORT dates become bare DATES keywords via additional_dates. Aligned output
# INSERT_DATE values become bare DATES keywords via additional_dates. Aligned output
# adds column-title comments; the schedule header identifies its timestamp and
# user, and each generated WELSPECS record has a matching COMPORD INPUT record.
schedule_text = timeline.generate_schedule_text(
@@ -19,10 +19,11 @@ 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.
REPORT A1_STARTUP EVERY MONTH
REPORT A2_STARTUP EVERY 30 DAYS UNTIL A2_STARTUP + 90
# Inserted dates become bare DATES keywords in the generated schedule. Recurring
# dates may use an explicit inclusive end date or default to the last event.
SCHEDULE
INSERT_DATE A1_STARTUP EVERY MONTH
INSERT_DATE A2_STARTUP EVERY 30 DAYS UNTIL A2_STARTUP + 90
# Raw schedule text is preserved verbatim. This block is inserted immediately
# after the date declaration; lower priorities are emitted first when multiple
+29 -18
View File
@@ -22,11 +22,11 @@ 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
statement = unit_directive | declaration | insert_date_line | well_block_open
| group_block_open | schedule_block_open | event_line
| raw_text_event ;
unit_directive = "UNIT" , ( "METRIC" | "FIELD" | "LAB" ) ;
report_line = "REPORT" , date_expr , [ recurrence ] ;
insert_date_line = "INSERT_DATE" , date_expr , [ recurrence ] ;
recurrence = "EVERY" , [ positive_integer ] , period ,
[ "UNTIL" , date_expr ] ;
period = "DAY" | "DAYS" | "MONTH" | "MONTHS" | "YEAR" | "YEARS" ;
@@ -68,7 +68,7 @@ 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 an event date. Anything else is an
``GROUP``, ``SCHEDULE``, ``INSERT_DATE`` 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.
@@ -94,8 +94,8 @@ Notes on the grammar:
record per unique member, with the enclosing group as parent. A schedule may
contain one attribute-free ``RESTART`` event; it truncates generated schedule
output before its timestamp and is not itself emitted as a keyword.
* ``REPORT <date_expr>`` (anywhere after the header) names a date that should
appear as a bare ``DATES`` keyword in the generated schedule even when no
* ``INSERT_DATE <date_expr>`` inside a ``SCHEDULE`` block 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 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
@@ -105,7 +105,7 @@ Notes on the grammar:
surfaced by the applier as sorted ISO strings on
:attr:`ApplyReport.report_dates`, ready to pass to
``WellEventTimeline.generate_schedule_text(additional_dates=...)``. A
``REPORT`` line is not tied to any well and does not close an open block.
``INSERT_DATE`` line is not tied to any well.
* ``RAW_TEXT`` is valid only inside a ``SCHEDULE`` block. Its body is copied
without parsing or formatting through the mandatory standalone
``END_RAW_TEXT`` line. ``PLACEMENT`` is ``AFTER_DATE``, ``BEFORE_KEYWORD``,
@@ -328,7 +328,7 @@ class GroupBlock:
@dataclass(frozen=True)
class _ReportSpec:
"""One REPORT declaration, expanded after all event dates are known."""
"""One INSERT_DATE declaration, expanded after all event dates are known."""
start: Union[datetime.date, datetime.datetime]
interval: Optional[int]
@@ -407,7 +407,7 @@ _KEYWORDS = (
"FILTER",
"GROUP",
"SCHEDULE",
"REPORT",
"INSERT_DATE",
)
_IDENT = r"[A-Za-z_]\w*"
@@ -423,8 +423,8 @@ _DURATION_DECL_RE = re.compile(
r"(?:\s+(?:DAYS|days))?$"
)
_WELL_DECL_RE = re.compile(rf'^WELL\s+(?P<name>{_IDENT})\s*=\s*"(?P<well>[^"]*)"$')
_REPORT_RE = re.compile(
rf"^REPORT\s+{_DATE_BASE}{_TERMS}"
_INSERT_DATE_RE = re.compile(
rf"^INSERT_DATE\s+{_DATE_BASE}{_TERMS}"
rf"(?:\s+EVERY\s+(?:(?P<count>\d+)\s+)?"
rf"(?P<period>DAY|DAYS|MONTH|MONTHS|YEAR|YEARS)"
rf"(?:\s+UNTIL\s+(?P<end_base>{_ISO_DATE}|{_IDENT})"
@@ -624,7 +624,7 @@ def _expand_report_specs(
if spec.interval is None or spec.interval <= 0:
issues.append(
ParseIssue("REPORT interval must be greater than zero", spec.loc)
ParseIssue("INSERT_DATE interval must be greater than zero", spec.loc)
)
continue
@@ -632,14 +632,16 @@ def _expand_report_specs(
if end is None:
issues.append(
ParseIssue(
"Recurring REPORT without UNTIL requires at least one event",
"Recurring INSERT_DATE without UNTIL requires at least one event",
spec.loc,
)
)
continue
if _as_datetime(end) < _as_datetime(spec.start):
issues.append(
ParseIssue("REPORT end date must not precede its start date", spec.loc)
ParseIssue(
"INSERT_DATE end date must not precede its start date", spec.loc
)
)
continue
@@ -790,12 +792,14 @@ def _parse_line(
)
return schedule_events
if first == "REPORT":
match = _REPORT_RE.match(line)
if first == "INSERT_DATE":
if current_events is not schedule_events:
raise OrionParseError("INSERT_DATE is only valid in a SCHEDULE block", loc)
match = _INSERT_DATE_RE.match(line)
if match is None:
raise OrionParseError(
f"Malformed REPORT line: {line!r} "
"(expected REPORT <date-expr> [EVERY [count] "
f"Malformed INSERT_DATE line: {line!r} "
"(expected INSERT_DATE <date-expr> [EVERY [count] "
"DAYS|MONTHS|YEARS [UNTIL <date-expr>]])",
loc,
)
@@ -905,6 +909,13 @@ def _parse_line(
wells.append(new_well)
return new_well.events
if first == "REPORT":
raise OrionParseError(
"REPORT has been renamed to INSERT_DATE and is only valid in a "
"SCHEDULE block",
loc,
)
if current_events is not None and _EVENT_RE.match(line):
current_events.append(_parse_event_line(line, variables, loc))
return current_events
@@ -1601,7 +1612,7 @@ def apply_orion_document(
event types are passed through as generic Eclipse keywords.
Returns:
ApplyReport: counts plus collected warnings/errors. ``REPORT`` dates
ApplyReport: counts plus collected warnings/errors. ``INSERT_DATE`` dates
from the document are returned as sorted, deduplicated ISO strings
on ``report_dates`` — they do not create timeline events; pass them
to ``timeline.generate_schedule_text(additional_dates=...)`` to
@@ -627,8 +627,9 @@ class TestParsing:
text = (
"ORIONEVENTS 2.0\n"
"DATE START = 2024-01-01\n"
"REPORT 2024-06-01\n"
"REPORT START + 31\n"
"SCHEDULE\n"
"INSERT_DATE 2024-06-01\n"
"INSERT_DATE START + 31\n"
)
doc = parse_orion_events(text)
assert doc.report_dates == [
@@ -637,31 +638,31 @@ class TestParsing:
]
def test_report_keeps_duplicates_and_file_order(self):
text = "ORIONEVENTS 2.0\nREPORT 2024-06-01\nREPORT 2024-06-01\n"
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n"
"INSERT_DATE 2024-06-01\nINSERT_DATE 2024-06-01\n"
)
doc = parse_orion_events(text)
assert doc.report_dates == [datetime.date(2024, 6, 1)] * 2
def test_report_inside_block_does_not_close_it(self):
text = (
'ORIONEVENTS 2.0\nWELL "W"\n'
" 2024-01-01 WCONHIST STATUS=OPEN\n"
"REPORT 2024-06-01\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]
assert doc.report_dates == [datetime.date(2024, 6, 1)]
def test_insert_date_is_only_valid_in_schedule_block(self):
with pytest.raises(OrionParseError, match="only valid in a SCHEDULE"):
parse_orion_events("ORIONEVENTS 2.0\nINSERT_DATE 2024-06-01\n")
def test_legacy_report_keyword_is_rejected(self):
with pytest.raises(OrionParseError, match="renamed to INSERT_DATE"):
parse_orion_events("ORIONEVENTS 2.0\nSCHEDULE\nREPORT 2024-06-01\n")
def test_report_with_undeclared_variable_raises(self):
with pytest.raises(OrionParseError, match="NOPE"):
parse_orion_events("ORIONEVENTS 2.0\nREPORT NOPE + 1\n")
parse_orion_events("ORIONEVENTS 2.0\nSCHEDULE\nINSERT_DATE NOPE + 1\n")
def test_malformed_report_line_raises(self):
with pytest.raises(OrionParseError, match="Malformed REPORT line"):
parse_orion_events("ORIONEVENTS 2.0\nREPORT\n")
with pytest.raises(OrionParseError, match="Malformed INSERT_DATE line"):
parse_orion_events("ORIONEVENTS 2.0\nSCHEDULE\nINSERT_DATE\n")
def test_report_with_datetime_literal(self):
text = "ORIONEVENTS 2.0\nREPORT 2024-06-01T14:45:30.500\n"
text = "ORIONEVENTS 2.0\nSCHEDULE\nINSERT_DATE 2024-06-01T14:45:30.500\n"
doc = parse_orion_events(text)
assert doc.report_dates == [datetime.datetime(2024, 6, 1, 14, 45, 30, 500000)]
@@ -670,7 +671,8 @@ class TestParsing:
"ORIONEVENTS 2.0\n"
"DATE START = 2024-01-01\n"
"DATE END = 2024-01-05\n"
"REPORT START + 1 EVERY 2 DAYS UNTIL END\n"
"SCHEDULE\n"
"INSERT_DATE START + 1 EVERY 2 DAYS UNTIL END\n"
)
doc = parse_orion_events(text)
assert doc.report_dates == [
@@ -679,7 +681,10 @@ class TestParsing:
]
def test_monthly_report_recurrence_is_anchored_to_initial_day(self):
text = "ORIONEVENTS 2.0\nREPORT 2024-01-31 EVERY MONTH UNTIL 2024-04-30\n"
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n"
"INSERT_DATE 2024-01-31 EVERY MONTH UNTIL 2024-04-30\n"
)
doc = parse_orion_events(text)
assert doc.report_dates == [
datetime.date(2024, 1, 31),
@@ -689,7 +694,10 @@ class TestParsing:
]
def test_yearly_report_recurrence_clamps_leap_day(self):
text = "ORIONEVENTS 2.0\nREPORT 2024-02-29 EVERY YEAR UNTIL 2028-02-29\n"
text = (
"ORIONEVENTS 2.0\nSCHEDULE\n"
"INSERT_DATE 2024-02-29 EVERY YEAR UNTIL 2028-02-29\n"
)
doc = parse_orion_events(text)
assert doc.report_dates == [
datetime.date(2024, 2, 29),
@@ -702,7 +710,8 @@ class TestParsing:
def test_report_recurrence_without_until_uses_last_event(self):
text = (
"ORIONEVENTS 2.0\n"
"REPORT 2024-01-01 EVERY MONTH\n"
"SCHEDULE\n"
"INSERT_DATE 2024-01-01 EVERY MONTH\n"
'WELL "W"\n'
" 2024-03-15 WCONHIST STATUS=OPEN\n"
)
@@ -716,7 +725,8 @@ class TestParsing:
def test_recurring_report_preserves_datetime(self):
text = (
"ORIONEVENTS 2.0\n"
"REPORT 2024-06-01T14:45:30.500 EVERY DAY "
"SCHEDULE\n"
"INSERT_DATE 2024-06-01T14:45:30.500 EVERY DAY "
"UNTIL 2024-06-02T14:45:30.500\n"
)
doc = parse_orion_events(text)
@@ -728,14 +738,17 @@ class TestParsing:
@pytest.mark.parametrize(
("report_line", "message"),
[
("REPORT 2024-01-01 EVERY 0 DAYS UNTIL 2024-01-02", "greater than zero"),
("REPORT 2024-01-02 EVERY DAY UNTIL 2024-01-01", "must not precede"),
("REPORT 2024-01-01 EVERY DAY", "requires at least one event"),
(
"INSERT_DATE 2024-01-01 EVERY 0 DAYS UNTIL 2024-01-02",
"greater than zero",
),
("INSERT_DATE 2024-01-02 EVERY DAY UNTIL 2024-01-01", "must not precede"),
("INSERT_DATE 2024-01-01 EVERY DAY", "requires at least one event"),
],
)
def test_invalid_report_recurrence_rejected(self, report_line, message):
with pytest.raises(OrionParseError, match=message):
parse_orion_events(f"ORIONEVENTS 2.0\n{report_line}\n")
parse_orion_events(f"ORIONEVENTS 2.0\nSCHEDULE\n{report_line}\n")
def test_event_dates_parse_without_prefix(self):
text = (
@@ -1325,9 +1338,10 @@ class TestApplying:
text = (
'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n'
" 2018-01-01 WCONHIST STATUS=OPEN\n"
"REPORT 2018-07-01\n"
"REPORT 2018-03-01\n"
"REPORT 2018-07-01\n"
"SCHEDULE\n"
"INSERT_DATE 2018-07-01\n"
"INSERT_DATE 2018-03-01\n"
"INSERT_DATE 2018-07-01\n"
)
timeline, report = self._apply(text)
# Sorted, deduplicated ISO strings ready for
@@ -2112,9 +2126,9 @@ class TestOrionEventsIntegration:
" 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"
"INSERT_DATE 2024-01-15\n"
"INSERT_DATE 2024-04-01\n"
" 2024-02-01 RESTART\n"
)
@@ -2139,7 +2153,8 @@ class TestOrionEventsIntegration:
def test_report_only_document_generates_schedule(self, project_with_case_and_wells):
project, case, timeline = project_with_case_and_wells
document = parse_orion_events(
"ORIONEVENTS 2.0\nREPORT 2024-02-01\nREPORT 2024-06-01\n"
"ORIONEVENTS 2.0\nSCHEDULE\n"
"INSERT_DATE 2024-02-01\nINSERT_DATE 2024-06-01\n"
)
report = apply_orion_document(document, timeline, project)
@@ -2169,7 +2184,8 @@ class TestOrionEventsIntegration:
" 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"
"SCHEDULE\n"
"INSERT_DATE 2024-07-01\n"
)
document = parse_orion_events(text)
report = apply_orion_document(document, timeline, project)
@@ -2187,7 +2203,7 @@ class TestOrionEventsIntegration:
assert abs(perf.end_measured_depth - 2200.0) < 1.0
# The generated schedule should carry the mapped keywords, and the
# REPORT date should appear as a bare DATES entry (issue #14514).
# INSERT_DATE date should appear as a bare DATES entry (issue #14514).
schedule = timeline.generate_schedule_text(
eclipse_case=case,
export_msw_for_wells=[],
@@ -2197,7 +2213,7 @@ class TestOrionEventsIntegration:
assert "WCONHIST" in schedule
assert "WELTARG" in schedule
assert "1 'JUL' 2024" in schedule, (
"REPORT date should be emitted as a DATES entry"
"INSERT_DATE date should be emitted as a DATES entry"
)
def test_apply_full_event_coverage_and_schedule(self, project_with_case_and_wells):