Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,13 @@ RimcWellEventTimeline_generateSchedule::RimcWellEventTimeline_generateSchedule(
"",
"",
"Emit a column-header comment and right-aligned, fixed-width columns instead of the compact form" );
CAF_PDM_InitScriptableFieldNoDefault( &m_additionalDates,
"AdditionalDates",
"",
"",
"",
"Additional dates (YYYY-MM-DD or full ISO timestamp) emitted as DATES keywords, e.g. to "
"force summary reports at those dates" );
}

//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -641,6 +648,24 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_generateSche
return std::unexpected( QString( "No well paths with events found" ) );
}

// Merge in user-specified additional dates: each becomes a DATES keyword even when no events
// fall on it (e.g. to force a summary report). They are deliberately not filtered by the last
// applied timestamp.
if ( !m_additionalDates().empty() )
{
std::set<QDateTime> mergedDates( dates.begin(), dates.end() );
for ( const QString& dateString : m_additionalDates() )
{
QDateTime additionalDate = QDateTime::fromString( dateString, Qt::ISODate );
if ( !additionalDate.isValid() )
{
return std::unexpected( QString( "Invalid date format: %1. Expected YYYY-MM-DD" ).arg( dateString ) );
}
mergedDates.insert( additionalDate );
Comment thread
kriben marked this conversation as resolved.
}
dates.assign( mergedDates.begin(), mergedDates.end() );
Comment thread
kriben marked this conversation as resolved.
}

std::vector<RimWellPath*> mswWellPaths = m_exportMswForWells.ptrReferencedObjectsByType();
std::set<const RimWellPath*> mswWells( mswWellPaths.begin(), mswWellPaths.end() );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,5 @@ class RimcWellEventTimeline_generateSchedule : public caf::PdmObjectMethod
caf::PdmPtrArrayField<RimWellPath*> m_exportMswForWells;
caf::PdmField<bool> m_firstDateAsComment;
caf::PdmField<bool> m_alignColumns;
caf::PdmField<std::vector<QString>> m_additionalDates;
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
3. Well keyword events: WCONHIST and WELTARG (with attribute translation) and
WRFTPLT (generic Eclipse well keyword pass-through)
4. SCHEDULE-level keyword events not tied to a well: RPTRST, GRUPTREE, TUNING
5. Generating Eclipse schedule text from the resulting timeline
5. REPORT dates, passed to generate_schedule_text(additional_dates=...) so
they appear as bare DATES keywords (summary-report triggers)
6. Generating Eclipse schedule text from the resulting timeline

The ORIONEVENTS text is built inline with the name of the first well path in
the project (like well_event_schedule.py, which uses wells[0]), so the example
Expand Down Expand Up @@ -74,6 +76,11 @@ def build_orion_text(well_name, with_filter):
@STARTUP RPTRST BASIC=2 FREQ=1
@STARTUP GRUPTREE CHILD=OP PARENT=FIELD
@STARTUP TUNING TSINIT=1 TSMAXZ=30 TMAXWC=1 NEWTMX=12 NEWTMN=1 LITMAX=50 LITMIN=1 MXWSIT=50 MXWPIT=50

# Report dates: emitted as bare DATES keywords so Eclipse/Flow writes a
# summary report at these dates even though no events fall on them.
REPORT 2024-07-01
REPORT STARTUP + 365
"""


Expand Down Expand Up @@ -114,6 +121,7 @@ def main():
)
print(f" Events applied: {report.events_applied}")
print(f" Events skipped: {report.events_skipped}")
print(f" Report dates: {report.report_dates}")
for warning in report.warnings:
print(f" WARNING: {warning}")
for error in report.errors:
Expand All @@ -139,8 +147,12 @@ def main():
if case is None:
print(" No Eclipse case loaded - skipping schedule generation.")
return
# REPORT dates from the ORIONEVENTS text become bare DATES keywords
# (summary-report triggers) via additional_dates.
schedule_text = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=[well_path]
eclipse_case=case,
export_msw_for_wells=[well_path],
additional_dates=report.report_dates,
)
if schedule_text:
print(f" Generated schedule text ({len(schedule_text)} characters)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,12 @@ def main():

# Generate schedule text. Pass the wells that should get multi-segment-well
# keywords (WELSEGS, COMPSEGS, WSEGVALV, WSEGAICD); an empty list omits them.
# additional_dates are emitted as bare DATES keywords even when no events
# fall on them - in Eclipse/Flow a DATES entry ensures a summary report.
schedule_text = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=[well_path]
eclipse_case=case,
export_msw_for_wells=[well_path],
additional_dates=["2024-07-01"],
)

# Generate the same schedule with align_columns=True, which adds a "--"-prefixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ FILTER POROPERM = "PORO > 0.1 AND PERMX > 100.0"

WELL A1 = "55_33-A-1"

# Report dates: each becomes a bare DATES keyword in the generated schedule
# (in Eclipse/Flow a DATES entry ensures a summary report at that date).
REPORT 2018-07-01
REPORT A2_STARTUP + 90

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
Expand Down
68 changes: 57 additions & 11 deletions GrpcInterface/Python/rips/orion_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@

document = header , { statement } ;
header = "ORIONEVENTS" , "2.0" ; (* first meaningful line *)
statement = unit_directive | declaration | well_block_open
statement = unit_directive | declaration | report_line | well_block_open
| schedule_block_open | event_line ;
unit_directive = "UNIT" , ( "METRIC" | "FIELD" | "LAB" ) ;
report_line = "REPORT" , date_expr ; (* REPORT 2024-06-01 *)

declaration = date_decl | duration_decl | well_decl | filter_decl ;
date_decl = "DATE" , ident , "=" , date_expr ; (* DATE X = 2018-03-01 + 9 *)
Expand Down Expand Up @@ -75,6 +76,14 @@
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.
* ``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
ensures a summary report at that date. The dates are collected on
:attr:`OrionDocument.report_dates` and 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.
* Double quotes are used everywhere: well names, filter expressions and
attribute values, e.g. ``FILTER="SOIL > 0.8 AND PERMX > 200"``.
* Every attribute is ``KEY=VALUE``; bare positional tokens are rejected.
Expand All @@ -94,8 +103,8 @@
searched in STATIC_NATIVE, then DYNAMIC_NATIVE, then GENERATED results; a
``TYPE.`` qualifier (``STATIC``/``DYNAMIC``/``GENERATED`` or the full
``*_NATIVE`` form, case-insensitive) restricts the search to that type.
* Any other attribute key parses; keys the applier does not support yet
(``PERFID``, ``DSHIFT``) are ignored with a warning when applied.
* Any other attribute key parses; ``FILTER`` on events other than
PERFORATION is ignored with a warning when applied.
* The parser recovers per line and reports **all** errors in one pass: the
raised :class:`OrionParseError` carries one :class:`ParseIssue` per problem.
Unknown names come with "did you mean" suggestions where possible.
Expand Down Expand Up @@ -260,14 +269,26 @@ class OrionDocument:
variables: Dict[str, OrionValue] = field(default_factory=dict)
wells: List[WellBlock] = field(default_factory=list)
schedule_events: List[OrionEvent] = field(default_factory=list)
report_dates: List[Union[datetime.date, datetime.datetime]] = field(
default_factory=list
)
warnings: List[ParseWarning] = field(default_factory=list)


# ---------------------------------------------------------------------------
# Layer A: pure parser
# ---------------------------------------------------------------------------

_KEYWORDS = ("ORIONEVENTS", "UNIT", "DATE", "DURATION", "WELL", "FILTER", "SCHEDULE")
_KEYWORDS = (
"ORIONEVENTS",
"UNIT",
"DATE",
"DURATION",
"WELL",
"FILTER",
"SCHEDULE",
"REPORT",
)

_IDENT = r"[A-Za-z_]\w*"
_ISO_DATE = r"\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?)?"
Expand All @@ -282,6 +303,7 @@ class OrionDocument:
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}$")
_FILTER_DECL_RE = re.compile(rf'^FILTER\s+(?P<name>{_IDENT})\s*=\s*"(?P<expr>[^"]*)"$')
_FILTER_SPLIT_RE = re.compile(r"\s+(AND|OR)\s+")
_NUMBER = r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?"
Expand Down Expand Up @@ -321,6 +343,7 @@ def parse_orion_events(text: str) -> OrionDocument:
variables: Dict[str, OrionValue] = {}
wells: List[WellBlock] = []
schedule_events: List[OrionEvent] = []
report_dates: List[Union[datetime.date, datetime.datetime]] = []
warnings: List[ParseWarning] = []
errors: List[ParseIssue] = []
# Event lines append to the current sink: a WellBlock's event list or the
Expand Down Expand Up @@ -352,6 +375,7 @@ def parse_orion_events(text: str) -> OrionDocument:
variables,
wells,
schedule_events,
report_dates,
warnings,
current_events,
unit_holder,
Expand All @@ -377,6 +401,7 @@ def parse_orion_events(text: str) -> OrionDocument:
variables=variables,
wells=wells,
schedule_events=schedule_events,
report_dates=report_dates,
warnings=warnings,
)

Expand Down Expand Up @@ -404,6 +429,7 @@ def _parse_line(
variables: Dict[str, OrionValue],
wells: List[WellBlock],
schedule_events: List[OrionEvent],
report_dates: List[Union[datetime.date, datetime.datetime]],
warnings: List[ParseWarning],
current_events: Optional[List[OrionEvent]],
unit_holder: List[str],
Expand Down Expand Up @@ -435,6 +461,19 @@ def _parse_line(
)
return schedule_events

if first == "REPORT":
match = _REPORT_RE.match(line)
if match is None:
raise OrionParseError(
f"Malformed REPORT line: {line!r} "
"(expected REPORT <iso-date|DATE-var> [+|- <days|DURATION-var> ...])",
loc,
)
report_dates.append(
_eval_date_expr(match.group("base"), match.group("terms"), variables, loc)
)
return current_events

if first == "DATE":
match = _DATE_DECL_RE.match(line)
if match is None:
Expand Down Expand Up @@ -805,6 +844,7 @@ class ApplyReport:

events_applied: int = 0
events_skipped: int = 0
report_dates: List[str] = field(default_factory=list)
warnings: List[str] = field(default_factory=list)
errors: List[str] = field(default_factory=list)

Expand All @@ -813,11 +853,11 @@ class ApplyReport:
_POLICIES = ("warn", "error", "skip")

# Attributes accepted on a keyword event but intentionally not emitted.
_IGNORED_KEYWORD_ATTRS = {"DSHIFT", "FILTER", "PERFID"}
_IGNORED_KEYWORD_ATTRS = {"FILTER"}

# Completion event attribute handling: (required, known-optional) per type.
# FILTER is applied on PERFORATION events; FILTER/PERFID are accepted on the
# other completion events but ignored with a warning.
# FILTER is applied on PERFORATION events; it is accepted on the other
# completion events but ignored with a warning.
_PERF_REQUIRED = ("MDSTART", "MDEND")
_PERF_KNOWN = {"MDSTART", "MDEND", "RADIUS", "SKIN", "COMPLETION_NUMBER", "FILTER"}
_TUBING_REQUIRED = ("MDSTART", "MDEND")
Expand All @@ -832,7 +872,7 @@ class ApplyReport:
}
_STATE_REQUIRED = ("STATE",)
_STATE_KNOWN = {"STATE"}
_COMPLETION_IGNORED = {"FILTER", "PERFID"}
_COMPLETION_IGNORED = {"FILTER"}
_PERF_IGNORED = _COMPLETION_IGNORED # backwards-compatible alias

# ORIONEVENTS -> Eclipse item-name translations per keyword.
Expand Down Expand Up @@ -890,11 +930,16 @@ def apply_orion_document(
event types are passed through as generic Eclipse keywords.

Returns:
ApplyReport: counts plus collected warnings/errors.
ApplyReport: counts plus collected warnings/errors. ``REPORT`` 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
emit them as DATES keywords.
"""
_validate_policy(on_unknown_well, "on_unknown_well")
_validate_policy(on_unknown_event, "on_unknown_event")
report = ApplyReport()
report.report_dates = sorted({d.isoformat() for d in document.report_dates})

ctx = _prepare_filter_context(document, project, case)

Expand Down Expand Up @@ -1144,7 +1189,7 @@ def _apply_perforation(
ctx: Optional[_FilterContext] = None,
) -> None:
if not _check_completion_attrs(
event, "PERFORATION", _PERF_KNOWN, _PERF_REQUIRED, report, ignored={"PERFID"}
event, "PERFORATION", _PERF_KNOWN, _PERF_REQUIRED, report, ignored=set()
):
return

Expand Down Expand Up @@ -1398,7 +1443,8 @@ 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.schedule_events)} schedule event(s)"
f"{len(document.schedule_events)} schedule event(s), "
f"{len(document.report_dates)} report date(s)"
)
for warning in document.warnings:
print(f" Warning line {warning.loc.line}: {warning.message}")
Expand Down
Loading