Orion Events: Add restart schedule cutoff

This commit is contained in:
Kristian Bendiksen committed 2026-08-20 11:02:55 +02:00
1 parent 1278ee2404
commit 27b45353ea
3 files changed
+145 -4

No files matched your search

@@ -80,11 +80,26 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
[&exportName]( const RimWellPath* a, const RimWellPath* b )
{ return QString::compare( exportName( a ), exportName( b ) ) < 0; } );
// Generate section for each date. The first (earliest) date may be emitted as a comment
// instead of a DATES keyword when firstDateAsComment is set.
std::optional<QDateTime> restartDate;
for ( const auto* event : timeline.events() )
{
if ( event->eventType() != RimWellEvent::EventType::SCHEDULE_KEYWORD ) continue;
const auto* keywordEvent = dynamic_cast<const RimKeywordEvent*>( event );
if ( keywordEvent && keywordEvent->keywordName().compare( "RESTART", Qt::CaseInsensitive ) == 0 )
{
restartDate = event->eventDate();
break;
}
}
// Generate section for each date. The first retained date may be emitted as a comment instead
// of a DATES keyword when firstDateAsComment is set. A RESTART marker truncates earlier dates,
// including user-specified additional report dates.
bool isFirstDate = true;
for ( const auto& date : dates )
{
if ( restartDate.has_value() && date < *restartDate ) continue;
QString dateSection =
generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment, alignColumns );
if ( !dateSection.isEmpty() )
@@ -229,7 +244,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
{
if ( event->eventType() != RimWellEvent::EventType::SCHEDULE_KEYWORD ) continue;
auto* keywordEvent = dynamic_cast<RimKeywordEvent*>( event );
if ( !keywordEvent ) continue;
if ( !keywordEvent || keywordEvent->keywordName().compare( "RESTART", Qt::CaseInsensitive ) == 0 ) continue;
if ( auto kw = keywordEvent->generateDeckKeyword( "" ) )
{
mergeKeyword( keywordBlocks, keywordEvent->keywordName().toUpper(), std::move( *kw ) );
+45 -1
View File
@@ -80,7 +80,9 @@ Notes on the grammar:
A bare ``SCHEDULE`` line opens a block of schedule-level keyword events not
tied to any well (RPTRST, GRUPTREE, TUNING, ...). Empty blocks are legal.
``MEMBER MEMBERS="A,B"`` inside a GROUP block is shorthand for one GRUPTREE
record per unique member, with the enclosing group as parent.
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>`` (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
@@ -414,6 +416,7 @@ def parse_orion_events(text: str) -> OrionDocument:
if version is None:
raise OrionParseError("Empty file: missing 'ORIONEVENTS' header")
errors.extend(_restart_validation_issues(wells, groups, schedule_events))
if errors:
raise OrionParseError(errors=errors)
@@ -429,6 +432,39 @@ def parse_orion_events(text: str) -> OrionDocument:
)
def _restart_validation_issues(
wells: List[WellBlock],
groups: List[GroupBlock],
schedule_events: List[OrionEvent],
) -> List[ParseIssue]:
"""Validate placement, cardinality and shape of RESTART events."""
issues: List[ParseIssue] = []
for well_block in wells:
for event in well_block.events:
if event.event_type.upper() == "RESTART":
issues.append(
ParseIssue("RESTART is only valid in a SCHEDULE block", event.loc)
)
for group_block in groups:
for event in group_block.events:
if event.event_type.upper() == "RESTART":
issues.append(
ParseIssue("RESTART is only valid in a SCHEDULE block", event.loc)
)
restart_events = [
event for event in schedule_events if event.event_type.upper() == "RESTART"
]
for event in restart_events:
if event.attributes:
issues.append(ParseIssue("RESTART takes no attributes", event.loc))
for event in restart_events[1:]:
issues.append(
ParseIssue("Only one RESTART event is allowed per schedule", event.loc)
)
return issues
def _check_version(version: str, loc: SourceLoc) -> None:
major = version.split(".")[0]
if major == "2":
@@ -1293,6 +1329,14 @@ def _apply_schedule_event(
) -> None:
"""Apply one GROUP- or SCHEDULE-block event as an Eclipse keyword."""
event_type = event.event_type.upper()
if event_type == "RESTART":
timeline.add_keyword_event(
event_date=_iso_event_date(event.event_date),
keyword_name="RESTART",
keyword_data={},
)
report.events_applied += 1
return
if event_type == "MEMBER":
_apply_member_event(event, timeline, report, group_name)
return
@@ -465,6 +465,41 @@ class TestParsing:
assert attributes["LABEL"].value == "True"
assert isinstance(attributes["LABEL"].value, str)
def test_single_restart_event_parses(self):
document = parse_orion_events(
"ORIONEVENTS 2.0\nSCHEDULE\n @2024-02-01 RESTART\n"
)
restart = document.schedule_events[0]
assert restart.event_type == "RESTART"
assert restart.attributes == {}
@pytest.mark.parametrize(
"text,expected_error",
[
(
'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',
"only valid in a SCHEDULE block",
),
(
"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",
"Only one RESTART event",
),
],
)
def test_invalid_restart_event_rejected(self, text, expected_error):
with pytest.raises(OrionParseError, match=expected_error):
parse_orion_events(text)
def test_schedule_line_with_arguments_rejected(self):
with pytest.raises(OrionParseError, match="SCHEDULE takes no arguments"):
parse_orion_events("ORIONEVENTS 2.0\nSCHEDULE NOW\n")
@@ -1059,6 +1094,20 @@ class TestApplying:
assert rptrst["keyword_data"] == {"BASIC": 2, "FREQ": 1}
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"
timeline, report = self._apply(text)
assert report.events_applied == 1
assert report.errors == []
assert timeline.schedule_keyword_calls == [
{
"event_date": "2024-02-01",
"keyword_name": "RESTART",
"keyword_data": {},
}
]
def test_group_events_inject_group_name(self):
text = (
'ORIONEVENTS 2.0\nGROUP "OP"\n'
@@ -1433,6 +1482,39 @@ class TestOrionEventsIntegration:
assert "'WELL_A' 'PRODUCERS'" in normalized_block
assert "'WELL_B' 'PRODUCERS'" in normalized_block
def test_restart_truncates_generated_schedule(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'
" @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"
)
report = apply_orion_document(document, timeline, project)
assert report.errors == []
schedule = timeline.generate_schedule_text(
eclipse_case=case,
first_date_as_comment=False,
additional_dates=report.report_dates,
)
assert "1 'JAN' 2024" not in schedule
assert "15 'JAN' 2024" not in schedule
assert "1 'FEB' 2024" in schedule
assert "1 'MAR' 2024" in schedule
assert "1 'APR' 2024" in schedule
assert " 100" not in schedule
assert " 200" in schedule
assert " 300" in schedule
assert "RESTART\n" not 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