mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 07:00:17 -05:00
Orion Events: Add restart schedule cutoff
This commit is contained in:
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 ) );
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user