mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 07:00:17 -05:00
#14666 Simulator Events: Expand INSERT_DATE into insert-date events
INSERT_DATE statements are now expanded into one timeline event per occurrence instead of a list of bare dates, so each generated date keeps the COMMENT of its statement. - New RimWellEventInsertDate (EventType::INSERT_DATE): a date-only event that generates no keyword, added with WellEventTimeline.add_insert_date_event(event_date, comment). - The schedule generator writes the comment of an insert-date event as "--" lines directly below its DATES keyword. - generate_schedule(_text) no longer takes additional_dates; insert-date events are part of the timeline, but keep the previous behavior of not being filtered by set_timestamp(). - SimulatorEventsDocument.report_dates and ApplyReport.report_dates are replaced by SimulatorEventsDocument.insert_date_events/insert_dates; merging files drops insert dates repeating both date and comment.
This commit is contained in:
1 parent
70e10cb386
commit
8324af966c
18 files changed
+453
-177
No files matched your search
@@ -102,7 +102,7 @@ def main() -> None:
|
||||
)
|
||||
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: {len(document.insert_date_events)}")
|
||||
for warning in report.warnings:
|
||||
print(f" WARNING: {warning}")
|
||||
for error in report.errors:
|
||||
@@ -122,13 +122,12 @@ def main() -> None:
|
||||
|
||||
# Generate Eclipse schedule text from the timeline.
|
||||
print("\n4. Generating Eclipse schedule text...")
|
||||
if report.events_applied == 0 and not report.report_dates:
|
||||
print(" No events or report dates found - skipping schedule generation.")
|
||||
if report.events_applied == 0:
|
||||
print(" No events found - skipping schedule generation.")
|
||||
return
|
||||
schedule_text = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=project.well_paths(),
|
||||
additional_dates=report.report_dates,
|
||||
align_columns=True,
|
||||
)
|
||||
if schedule_text:
|
||||
|
||||
+8
-8
@@ -25,8 +25,8 @@ 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 INSERT_DATE events with explicit and implicit end dates, passed to
|
||||
generate_schedule_text(additional_dates=...) as summary-report triggers
|
||||
10. Recurring INSERT_DATE events with explicit and implicit end dates, expanded
|
||||
into commented insert-date events acting as summary-report triggers
|
||||
11. Schedule metadata, COMPORD generation and aligned-column output
|
||||
|
||||
The SIMEVENTS text is built inline with the name of the first well path in
|
||||
@@ -114,9 +114,10 @@ WTRACER
|
||||
/
|
||||
END_RAW_TEXT
|
||||
|
||||
# Recurring inserted dates become bare DATES keywords. The first series ends at
|
||||
# the last event; the second uses an explicit inclusive end date.
|
||||
STARTUP INSERT_DATE EVERY=1mon
|
||||
# Recurring inserted dates become bare DATES keywords, each with the COMMENT of
|
||||
# its statement. The first series ends at the last event; the second uses an
|
||||
# explicit inclusive end date.
|
||||
STARTUP INSERT_DATE EVERY=1mon COMMENT="Monthly report"
|
||||
2024-07-01 INSERT_DATE EVERY=3mon UNTIL="STARTUP + 365d"
|
||||
"""
|
||||
|
||||
@@ -179,7 +180,7 @@ def main() -> None:
|
||||
)
|
||||
print(f" Events applied: {report.events_applied}")
|
||||
print(f" Events skipped: {report.events_skipped}")
|
||||
print(f" Inserted dates: {report.report_dates}")
|
||||
print(f" Inserted dates: {len(document.insert_date_events)}")
|
||||
for warning in report.warnings:
|
||||
print(f" WARNING: {warning}")
|
||||
for error in report.errors:
|
||||
@@ -212,13 +213,12 @@ def main() -> None:
|
||||
if case is None:
|
||||
print(" No Eclipse case loaded - skipping schedule generation.")
|
||||
return
|
||||
# INSERT_DATE values become bare DATES keywords via additional_dates. Aligned output
|
||||
# INSERT_DATE events become bare DATES keywords with their comment. 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(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=[well_path],
|
||||
additional_dates=report.report_dates,
|
||||
align_columns=True,
|
||||
)
|
||||
if schedule_text:
|
||||
|
||||
@@ -251,12 +251,14 @@ 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
|
||||
# Insert-date events are emitted as bare DATES keywords even when no other events
|
||||
# fall on them - in Eclipse/Flow a DATES entry ensures a summary report.
|
||||
timeline.add_insert_date_event(
|
||||
event_date="2024-07-01", comment="Summary report only"
|
||||
)
|
||||
schedule_text = timeline.generate_schedule_text(
|
||||
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
|
||||
|
||||
@@ -20,9 +20,10 @@ FILTER POROPERM = "PORO > 0.1 AND PERMX > 100.0"
|
||||
WELL A1 = "55_33-A-1"
|
||||
|
||||
# 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.
|
||||
# dates may use an explicit inclusive end date or default to the last event, and
|
||||
# a COMMENT is repeated below every generated date.
|
||||
SCHEDULE
|
||||
A1_STARTUP INSERT_DATE EVERY=1mon
|
||||
A1_STARTUP INSERT_DATE EVERY=1mon COMMENT="Monthly report"
|
||||
A2_STARTUP INSERT_DATE EVERY=30d UNTIL="A2_STARTUP + 90d"
|
||||
|
||||
# Raw schedule text is preserved verbatim. This block is inserted immediately
|
||||
|
||||
@@ -128,13 +128,14 @@ Notes on the grammar:
|
||||
is used. Occurrence ``n`` is the start date plus ``n`` times the month part,
|
||||
then plus ``n`` times the fixed part, so monthly and yearly recurrences stay
|
||||
anchored to the initial calendar day, clamping to the end of shorter
|
||||
months. ``EVERY`` must be positive. ``COMMENT`` is accepted and ignored.
|
||||
The dates are collected on :attr:`SimulatorEventsDocument.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=...)``. An
|
||||
``INSERT_DATE`` event is not tied to any well and does not create a
|
||||
timeline event.
|
||||
months. ``EVERY`` must be positive. ``COMMENT`` is written as a ``--`` comment
|
||||
directly below each generated date.
|
||||
A statement is expanded into one insert-date event per occurrence, each
|
||||
keeping the ``COMMENT`` of the statement; they are collected on
|
||||
:attr:`SimulatorEventsDocument.insert_date_events`. The applier turns each
|
||||
into a timeline event with ``WellEventTimeline.add_insert_date_event()``, so
|
||||
``generate_schedule_text()`` emits the dates without further arguments. An
|
||||
``INSERT_DATE`` event is not tied to any well and generates no keyword.
|
||||
* ``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``,
|
||||
@@ -193,7 +194,7 @@ import datetime
|
||||
import difflib
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass, field, replace
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from .exception import RipsError
|
||||
@@ -465,12 +466,19 @@ class GroupBlock:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ReportSpec:
|
||||
"""One INSERT_DATE declaration, expanded after all event dates are known."""
|
||||
"""One INSERT_DATE event, expanded after all event dates are known."""
|
||||
|
||||
start: datetime.datetime
|
||||
event: SimulatorEvent
|
||||
every: Optional[Duration]
|
||||
end: Optional[datetime.datetime]
|
||||
loc: SourceLoc
|
||||
|
||||
@property
|
||||
def start(self) -> datetime.datetime:
|
||||
return self.event.event_date
|
||||
|
||||
@property
|
||||
def loc(self) -> SourceLoc:
|
||||
return self.event.loc
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -483,10 +491,15 @@ class SimulatorEventsDocument:
|
||||
wells: List[WellBlock] = field(default_factory=list)
|
||||
groups: List[GroupBlock] = field(default_factory=list)
|
||||
schedule_events: List[SimulatorEvent] = field(default_factory=list)
|
||||
report_dates: List[datetime.datetime] = field(default_factory=list)
|
||||
insert_date_events: List[SimulatorEvent] = field(default_factory=list)
|
||||
warnings: List[ParseWarning] = field(default_factory=list)
|
||||
source: Optional[str] = None
|
||||
|
||||
@property
|
||||
def insert_dates(self) -> List[datetime.datetime]:
|
||||
"""The dates of the expanded ``INSERT_DATE`` events, in document order."""
|
||||
return [event.event_date for event in self.insert_date_events]
|
||||
|
||||
|
||||
def _iso_event_date(event_date: datetime.datetime) -> str:
|
||||
"""Format an event date for the timeline API.
|
||||
@@ -702,7 +715,6 @@ def merge_simulator_events_documents(
|
||||
version=max((document.version for document in sources), key=_version_key),
|
||||
unit_system=first.unit_system,
|
||||
)
|
||||
report_dates: Set[datetime.datetime] = set()
|
||||
for document in sources:
|
||||
result.warnings.extend(document.warnings)
|
||||
for name, value in document.variables.items():
|
||||
@@ -724,8 +736,7 @@ def merge_simulator_events_documents(
|
||||
result.wells.extend(document.wells)
|
||||
result.groups.extend(document.groups)
|
||||
result.schedule_events.extend(document.schedule_events)
|
||||
report_dates.update(document.report_dates)
|
||||
result.report_dates = sorted(report_dates)
|
||||
result.insert_date_events.extend(document.insert_date_events)
|
||||
|
||||
errors = _restart_validation_issues(
|
||||
result.wells, result.groups, result.schedule_events
|
||||
@@ -836,7 +847,7 @@ def parse_simulator_events(
|
||||
_set_event_scopes(wells, groups, schedule_events)
|
||||
errors.extend(_restart_validation_issues(wells, groups, schedule_events))
|
||||
errors.extend(_wellspec_validation_issues(wells))
|
||||
report_dates, report_errors = _expand_report_specs(
|
||||
insert_date_events, report_errors = _expand_report_specs(
|
||||
report_specs, wells, groups, schedule_events
|
||||
)
|
||||
errors.extend(report_errors)
|
||||
@@ -850,7 +861,7 @@ def parse_simulator_events(
|
||||
wells=wells,
|
||||
groups=groups,
|
||||
schedule_events=schedule_events,
|
||||
report_dates=report_dates,
|
||||
insert_date_events=insert_date_events,
|
||||
warnings=warnings,
|
||||
source=source,
|
||||
)
|
||||
@@ -872,17 +883,32 @@ def _expand_report_specs(
|
||||
wells: List[WellBlock],
|
||||
groups: List[GroupBlock],
|
||||
schedule_events: List[SimulatorEvent],
|
||||
) -> Tuple[List[datetime.datetime], List[ParseIssue]]:
|
||||
) -> Tuple[List[SimulatorEvent], List[ParseIssue]]:
|
||||
"""Expand every ``INSERT_DATE`` spec into one event per occurrence.
|
||||
|
||||
Each occurrence is a copy of the source event, so an attached ``COMMENT``
|
||||
is kept for every date of a recurring series.
|
||||
"""
|
||||
event_dates = [event.event_date for well in wells for event in well.events]
|
||||
event_dates.extend(event.event_date for group in groups for event in group.events)
|
||||
event_dates.extend(event.event_date for event in schedule_events)
|
||||
last_event_date = max(event_dates) if event_dates else None
|
||||
|
||||
dates: List[datetime.datetime] = []
|
||||
def occurrence_event(
|
||||
spec: _ReportSpec, event_date: datetime.datetime
|
||||
) -> SimulatorEvent:
|
||||
attributes = {
|
||||
name: value
|
||||
for name, value in spec.event.attributes.items()
|
||||
if name == "COMMENT"
|
||||
}
|
||||
return replace(spec.event, event_date=event_date, attributes=attributes)
|
||||
|
||||
events: List[SimulatorEvent] = []
|
||||
issues: List[ParseIssue] = []
|
||||
for spec in report_specs:
|
||||
if spec.every is None:
|
||||
dates.append(spec.start)
|
||||
events.append(occurrence_event(spec, spec.start))
|
||||
continue
|
||||
|
||||
end = spec.end if spec.end is not None else last_event_date
|
||||
@@ -910,10 +936,10 @@ def _expand_report_specs(
|
||||
break
|
||||
if value > end:
|
||||
break
|
||||
dates.append(value)
|
||||
events.append(occurrence_event(spec, value))
|
||||
occurrence += 1
|
||||
|
||||
return dates, issues
|
||||
return events, issues
|
||||
|
||||
|
||||
def _restart_validation_issues(
|
||||
@@ -1249,7 +1275,7 @@ def _insert_date_spec(
|
||||
)
|
||||
end = _eval_date_expr(match.group("base"), match.group("terms"), variables, loc)
|
||||
|
||||
return _ReportSpec(start=event.event_date, every=every, end=end, loc=loc)
|
||||
return _ReportSpec(event=event, every=every, end=end)
|
||||
|
||||
|
||||
def _unrecognized_line_message(line: str, first: str) -> str:
|
||||
@@ -1714,7 +1740,6 @@ 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)
|
||||
|
||||
@@ -1851,7 +1876,8 @@ def coalesce_simulator_events_document(
|
||||
applied in source order. Well keyword attributes are then carried forward
|
||||
chronologically to later events of the same type. Events that create or
|
||||
expand domain objects are kept separate so, for example, same-date
|
||||
perforation intervals are not lost.
|
||||
perforation intervals are not lost. Inserted dates repeating both date and
|
||||
comment are reduced to one event.
|
||||
"""
|
||||
result = copy.deepcopy(document)
|
||||
|
||||
@@ -1947,9 +1973,23 @@ def coalesce_simulator_events_document(
|
||||
block.events = merge_events(block.events)
|
||||
return merged_blocks
|
||||
|
||||
def merge_insert_date_events(events: List[SimulatorEvent]) -> List[SimulatorEvent]:
|
||||
"""Drop repeated occurrences of the same date and comment."""
|
||||
merged: List[SimulatorEvent] = []
|
||||
seen: Set[Tuple[datetime.datetime, str]] = set()
|
||||
for event in events:
|
||||
comment = event.attributes.get("COMMENT")
|
||||
key = (event.event_date, "" if comment is None else str(comment.value))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
merged.append(event)
|
||||
return merged
|
||||
|
||||
result.wells = merge_well_blocks(result.wells)
|
||||
result.groups = merge_group_blocks(result.groups)
|
||||
result.schedule_events = merge_events(result.schedule_events)
|
||||
result.insert_date_events = merge_insert_date_events(result.insert_date_events)
|
||||
return result
|
||||
|
||||
|
||||
@@ -2057,17 +2097,15 @@ def apply_simulator_events_document(
|
||||
event types are passed through as generic Eclipse keywords.
|
||||
|
||||
Returns:
|
||||
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
|
||||
emit them as DATES keywords.
|
||||
ApplyReport: counts plus collected warnings/errors. Each ``INSERT_DATE``
|
||||
occurrence becomes an insert-date event on the timeline, so its date
|
||||
is emitted as a DATES keyword (with its comment, if any) by
|
||||
``timeline.generate_schedule_text()``.
|
||||
"""
|
||||
_validate_policy(on_unknown_well, "on_unknown_well")
|
||||
_validate_policy(on_unknown_event, "on_unknown_event")
|
||||
document = coalesce_simulator_events_document(document)
|
||||
report = ApplyReport()
|
||||
report.report_dates = sorted({_iso_event_date(d) for d in document.report_dates})
|
||||
report.warnings.extend(
|
||||
f"{_loc_label(warning.loc)}: {warning.message}" for warning in document.warnings
|
||||
)
|
||||
@@ -2114,6 +2152,9 @@ def apply_simulator_events_document(
|
||||
for event in document.schedule_events:
|
||||
_apply_schedule_event(event, timeline, report)
|
||||
|
||||
for event in document.insert_date_events:
|
||||
_apply_insert_date_event(event, timeline, report)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
@@ -2375,6 +2416,18 @@ def _apply_schedule_event(
|
||||
report.events_applied += 1
|
||||
|
||||
|
||||
def _apply_insert_date_event(
|
||||
event: SimulatorEvent, timeline: Any, report: ApplyReport
|
||||
) -> None:
|
||||
"""Apply one expanded ``INSERT_DATE`` occurrence, comment included."""
|
||||
comment = event.attributes.get("COMMENT")
|
||||
timeline.add_insert_date_event(
|
||||
event_date=_iso_event_date(event.event_date),
|
||||
comment="" if comment is None else str(comment.value),
|
||||
)
|
||||
report.events_applied += 1
|
||||
|
||||
|
||||
def _validate_policy(value: str, name: str) -> None:
|
||||
if value not in _POLICIES:
|
||||
raise ValueError(f"{name} must be one of {_POLICIES}, got {value!r}")
|
||||
@@ -2748,8 +2801,6 @@ def _apply_to_running_instance(document: SimulatorEventsDocument) -> int:
|
||||
|
||||
print(f" Events applied: {report.events_applied}")
|
||||
print(f" Events skipped: {report.events_skipped}")
|
||||
if report.report_dates:
|
||||
print(f" Report dates: {', '.join(report.report_dates)}")
|
||||
for warning in report.warnings:
|
||||
print(f" Warning: {warning}")
|
||||
for error in report.errors:
|
||||
@@ -2798,13 +2849,15 @@ def _cli(argv: Optional[List[str]] = None) -> int:
|
||||
|
||||
event_count = sum(len(well.events) for well in document.wells)
|
||||
group_event_count = sum(len(group.events) for group in document.groups)
|
||||
insert_date_count = len(document.insert_date_events)
|
||||
schedule_event_count = len(document.schedule_events)
|
||||
print(f"{label}: OK (SIMEVENTS {document.version}, units {document.unit_system})")
|
||||
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)"
|
||||
f"{schedule_event_count} schedule event(s), "
|
||||
f"{insert_date_count} inserted date(s)"
|
||||
)
|
||||
normalized = coalesce_simulator_events_document(document)
|
||||
for warning in normalized.warnings:
|
||||
|
||||
@@ -195,6 +195,7 @@ class FakeTimeline:
|
||||
self.wellspec_calls = []
|
||||
self.schedule_keyword_calls = []
|
||||
self.raw_text_calls = []
|
||||
self.insert_date_calls = []
|
||||
self.created_events = []
|
||||
|
||||
def add_perf_event(self, **kwargs):
|
||||
@@ -236,6 +237,10 @@ class FakeTimeline:
|
||||
self.raw_text_calls.append(kwargs)
|
||||
return self._new_event()
|
||||
|
||||
def add_insert_date_event(self, **kwargs):
|
||||
self.insert_date_calls.append(kwargs)
|
||||
return self._new_event()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Layer A: parsing
|
||||
@@ -261,7 +266,7 @@ class TestParsing:
|
||||
text = example["build_simulator_events_text"]("W", with_filter)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.wells[0].well_name == "W"
|
||||
assert doc.report_dates[-1] == datetime.datetime(2024, 10, 1)
|
||||
assert doc.insert_dates[-1] == datetime.datetime(2024, 10, 1)
|
||||
|
||||
def test_date_declarations_with_day_offset(self):
|
||||
doc = parse_simulator_events(SAMPLE)
|
||||
@@ -381,7 +386,7 @@ class TestParsing:
|
||||
'2024-01-01 + -1d INSERT_DATE EVERY=1d UNTIL="2024-01-05 + -2d"\n'
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2023, 12, 31),
|
||||
datetime.datetime(2024, 1, 1),
|
||||
datetime.datetime(2024, 1, 2),
|
||||
@@ -719,7 +724,7 @@ class TestParsing:
|
||||
"START + 31d INSERT_DATE\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 6, 1),
|
||||
datetime.datetime(2024, 2, 1),
|
||||
]
|
||||
@@ -729,7 +734,7 @@ class TestParsing:
|
||||
"SIMEVENTS 1.2\nSCHEDULE\n2024-06-01 INSERT_DATE\n2024-06-01 INSERT_DATE\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [datetime.datetime(2024, 6, 1)] * 2
|
||||
assert doc.insert_dates == [datetime.datetime(2024, 6, 1)] * 2
|
||||
|
||||
def test_insert_date_is_only_valid_in_schedule_block(self):
|
||||
with pytest.raises(SimulatorEventsParseError, match="only valid in a SCHEDULE"):
|
||||
@@ -755,7 +760,7 @@ class TestParsing:
|
||||
def test_report_with_datetime_literal(self):
|
||||
text = "SIMEVENTS 1.2\nSCHEDULE\n2024-06-01T14:45:30.700 INSERT_DATE\n"
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [datetime.datetime(2024, 6, 1, 14, 45, 31)]
|
||||
assert doc.insert_dates == [datetime.datetime(2024, 6, 1, 14, 45, 31)]
|
||||
|
||||
def test_daily_report_recurrence_with_inclusive_until(self):
|
||||
text = (
|
||||
@@ -766,7 +771,7 @@ class TestParsing:
|
||||
"START + 1d INSERT_DATE EVERY=2d UNTIL=END\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 1, 2),
|
||||
datetime.datetime(2024, 1, 4),
|
||||
]
|
||||
@@ -777,7 +782,7 @@ class TestParsing:
|
||||
"2024-01-31 INSERT_DATE EVERY=1mon UNTIL=2024-04-30\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 1, 31),
|
||||
datetime.datetime(2024, 2, 29),
|
||||
datetime.datetime(2024, 3, 31),
|
||||
@@ -790,7 +795,7 @@ class TestParsing:
|
||||
"2024-02-29 INSERT_DATE EVERY=12mon UNTIL=2028-02-29\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 2, 29),
|
||||
datetime.datetime(2025, 2, 28),
|
||||
datetime.datetime(2026, 2, 28),
|
||||
@@ -807,7 +812,7 @@ class TestParsing:
|
||||
" 2024-03-15 WCONHIST STATUS=OPEN\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 1, 1),
|
||||
datetime.datetime(2024, 2, 1),
|
||||
datetime.datetime(2024, 3, 1),
|
||||
@@ -821,7 +826,7 @@ class TestParsing:
|
||||
"UNTIL=2024-06-02T14:45:30.700\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 6, 1, 14, 45, 31),
|
||||
datetime.datetime(2024, 6, 2, 14, 45, 31),
|
||||
]
|
||||
@@ -848,7 +853,7 @@ class TestParsing:
|
||||
+ "SCHEDULE\n"
|
||||
+ "".join(f" {line}\n" for line in lines)
|
||||
)
|
||||
return parse_simulator_events(text).report_dates
|
||||
return parse_simulator_events(text).insert_dates
|
||||
|
||||
def test_insert_date_every_accepts_duration_variable(self):
|
||||
dates = self._insert_dates(
|
||||
@@ -914,30 +919,37 @@ class TestParsing:
|
||||
dates = self._insert_dates("2024-01-01 insert_date every=1d until=2024-01-02")
|
||||
assert dates == [datetime.datetime(2024, 1, 1), datetime.datetime(2024, 1, 2)]
|
||||
|
||||
def test_insert_date_comment_is_accepted_and_ignored(self):
|
||||
def test_insert_date_comment_is_kept_for_every_occurrence(self):
|
||||
text = (
|
||||
"SIMEVENTS 1.2\nSCHEDULE\n"
|
||||
' 2024-01-01 INSERT_DATE EVERY=1d UNTIL=2024-01-02 COMMENT="Daily"\n'
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 1, 1),
|
||||
datetime.datetime(2024, 1, 2),
|
||||
]
|
||||
# Each occurrence keeps the comment, and nothing else, of the statement.
|
||||
for event in doc.insert_date_events:
|
||||
assert set(event.attributes) == {"COMMENT"}
|
||||
assert event.attributes["COMMENT"].value == "Daily"
|
||||
assert doc.schedule_events == []
|
||||
assert doc.warnings == []
|
||||
|
||||
def test_insert_date_does_not_create_schedule_event(self):
|
||||
def test_insert_date_creates_insert_date_events_only(self):
|
||||
text = (
|
||||
"SIMEVENTS 1.2\nSCHEDULE\n"
|
||||
" 2024-01-01 RPTRST BASIC=1\n"
|
||||
" 2024-02-01 INSERT_DATE\n"
|
||||
' 2024-02-01 INSERT_DATE COMMENT="Report"\n'
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert [event.event_type for event in doc.schedule_events] == ["RPTRST"]
|
||||
report = apply_simulator_events_document(doc, FakeTimeline(), FakeProject(()))
|
||||
assert report.events_applied == 1
|
||||
assert report.report_dates == ["2024-02-01"]
|
||||
timeline = FakeTimeline()
|
||||
report = apply_simulator_events_document(doc, timeline, FakeProject(()))
|
||||
assert report.events_applied == 2
|
||||
assert timeline.insert_date_calls == [
|
||||
{"event_date": "2024-02-01", "comment": "Report"}
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attributes", "message"),
|
||||
@@ -1215,7 +1227,7 @@ class TestDurations:
|
||||
"2024-01-31 + STEP INSERT_DATE EVERY=1d UNTIL=2024-01-31+STEP+1d\n"
|
||||
)
|
||||
doc = parse_simulator_events(text)
|
||||
assert doc.report_dates == [
|
||||
assert doc.insert_dates == [
|
||||
datetime.datetime(2024, 2, 29),
|
||||
datetime.datetime(2024, 3, 1),
|
||||
]
|
||||
@@ -1767,20 +1779,21 @@ class TestApplying:
|
||||
assert timeline.keyword_calls[0]["event_date"] == "2018-01-01"
|
||||
assert not report.warnings
|
||||
|
||||
def test_report_dates_on_apply_report(self):
|
||||
def test_insert_dates_become_timeline_events(self):
|
||||
text = (
|
||||
'SIMEVENTS 1.2\nWELL "55_33-A-1"\n'
|
||||
" 2018-01-01 WCONHIST STATUS=OPEN\n"
|
||||
"SCHEDULE\n"
|
||||
"2018-07-01 INSERT_DATE\n"
|
||||
"2018-03-01 INSERT_DATE\n"
|
||||
"2018-07-01 INSERT_DATE\n"
|
||||
'2018-03-01 INSERT_DATE COMMENT="Mid-year"\n'
|
||||
)
|
||||
timeline, report = self._apply(text)
|
||||
# Sorted, deduplicated ISO strings ready for
|
||||
# generate_schedule_text(additional_dates=...). No timeline events.
|
||||
assert report.report_dates == ["2018-03-01", "2018-07-01"]
|
||||
assert report.events_applied == 1
|
||||
# One timeline event per inserted date, in document order.
|
||||
assert timeline.insert_date_calls == [
|
||||
{"event_date": "2018-07-01", "comment": ""},
|
||||
{"event_date": "2018-03-01", "comment": "Mid-year"},
|
||||
]
|
||||
assert report.events_applied == 3
|
||||
|
||||
def test_radius_on_perforation_is_unknown_attribute_error(self):
|
||||
text = (
|
||||
@@ -2346,7 +2359,8 @@ class TestMultipleFiles:
|
||||
assert len(merged.schedule_events) == 1
|
||||
assert set(merged.schedule_events[0].attributes) == {"BASIC", "FREQ"}
|
||||
|
||||
assert merged.report_dates == [
|
||||
# The repeated 2018-02-01 insert date is reduced to one event.
|
||||
assert merged.insert_dates == [
|
||||
datetime.datetime(2018, 2, 1),
|
||||
datetime.datetime(2018, 3, 1),
|
||||
]
|
||||
@@ -2491,8 +2505,9 @@ class TestMultipleFiles:
|
||||
out = capsys.readouterr().out
|
||||
assert f"{files[0]}, {files[1]}: OK" in out
|
||||
assert "2 well block(s), 4 well event(s)" in out
|
||||
# One merged WCONHIST, two perforations, one GCONPROD and one RPTRST.
|
||||
assert "Events applied: 5" in out
|
||||
# One merged WCONHIST, two perforations, one GCONPROD, one RPTRST and
|
||||
# two inserted dates (the repeated 2018-02-01 is applied once).
|
||||
assert "Events applied: 7" in out
|
||||
assert len(timeline.keyword_calls) == 1
|
||||
assert len(timeline.perf_calls) == 2
|
||||
|
||||
@@ -2881,7 +2896,6 @@ class TestSimulatorEventsIntegration:
|
||||
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
|
||||
@@ -2893,23 +2907,30 @@ class TestSimulatorEventsIntegration:
|
||||
assert " 300" in schedule
|
||||
assert "RESTART\n" not in schedule
|
||||
|
||||
def test_report_only_document_generates_schedule(self, project_with_case_and_wells):
|
||||
def test_insert_date_only_document_generates_schedule(
|
||||
self, project_with_case_and_wells
|
||||
):
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
document = parse_simulator_events(
|
||||
"SIMEVENTS 1.2\nSCHEDULE\n2024-02-01 INSERT_DATE\n2024-06-01 INSERT_DATE\n"
|
||||
"SIMEVENTS 1.2\nSCHEDULE\n"
|
||||
'2024-02-01 INSERT_DATE EVERY=1mon UNTIL=2024-04-01 COMMENT="Quarterly"\n'
|
||||
"2024-06-01 INSERT_DATE\n"
|
||||
)
|
||||
|
||||
report = apply_simulator_events_document(document, timeline, project)
|
||||
schedule = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
first_date_as_comment=False,
|
||||
additional_dates=report.report_dates,
|
||||
)
|
||||
|
||||
assert report.events_applied == 0
|
||||
assert report.report_dates == ["2024-02-01", "2024-06-01"]
|
||||
# Three occurrences of the recurring statement plus the single date.
|
||||
assert report.events_applied == 4
|
||||
assert "1 'FEB' 2024" in schedule
|
||||
assert "1 'MAR' 2024" in schedule
|
||||
assert "1 'APR' 2024" in schedule
|
||||
assert "1 'JUN' 2024" in schedule
|
||||
# The comment of the statement follows every generated date.
|
||||
assert schedule.count("-- Quarterly") == 3
|
||||
|
||||
def test_end_is_first_keyword_after_date(self, project_with_case_and_wells):
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
@@ -2952,8 +2973,7 @@ class TestSimulatorEventsIntegration:
|
||||
document = parse_simulator_events(text)
|
||||
report = apply_simulator_events_document(document, timeline, project)
|
||||
assert report.errors == []
|
||||
assert report.events_applied == 3
|
||||
assert report.report_dates == ["2024-07-01"]
|
||||
assert report.events_applied == 4
|
||||
|
||||
# Materialize completions from the perforation event.
|
||||
timeline.set_timestamp(timestamp="2024-01-15")
|
||||
@@ -2970,7 +2990,6 @@ class TestSimulatorEventsIntegration:
|
||||
schedule = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=[],
|
||||
additional_dates=report.report_dates,
|
||||
)
|
||||
assert "COMPDAT" in schedule
|
||||
assert "WCONHIST" in schedule
|
||||
|
||||
@@ -444,8 +444,10 @@ class TestScheduleGeneration:
|
||||
assert "DATES" in schedule_text, "Schedule should contain DATES keyword"
|
||||
assert "2024" in schedule_text, "Schedule should contain event dates"
|
||||
|
||||
def test_generate_schedule_with_additional_dates(self, project_with_case_and_well):
|
||||
"""Additional dates become bare DATES keywords, merged chronologically (issue #14514)."""
|
||||
def test_generate_schedule_with_insert_date_events(
|
||||
self, project_with_case_and_well
|
||||
):
|
||||
"""Insert-date events become bare DATES keywords, merged chronologically (issue #14514)."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
|
||||
well_paths = project.well_paths()
|
||||
@@ -467,20 +469,26 @@ class TestScheduleGeneration:
|
||||
diameter=0.1,
|
||||
state="OPEN",
|
||||
)
|
||||
timeline.add_insert_date_event(event_date="2024-02-01")
|
||||
timeline.add_insert_date_event(
|
||||
event_date="2024-06-01", comment="Mid-year summary report"
|
||||
)
|
||||
timeline.set_timestamp(timestamp="2024-12-31")
|
||||
|
||||
schedule_text = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=project.well_paths(),
|
||||
first_date_as_comment=False,
|
||||
additional_dates=["2024-02-01", "2024-06-01"],
|
||||
)
|
||||
|
||||
assert "1 'FEB' 2024" in schedule_text, (
|
||||
"Additional date 2024-02-01 should be emitted as a DATES entry"
|
||||
"Inserted date 2024-02-01 should be emitted as a DATES entry"
|
||||
)
|
||||
assert "1 'JUN' 2024" in schedule_text, (
|
||||
"Additional date 2024-06-01 should be emitted as a DATES entry"
|
||||
"Inserted date 2024-06-01 should be emitted as a DATES entry"
|
||||
)
|
||||
assert "-- Mid-year summary report" in schedule_text, (
|
||||
"The comment of an insert-date event should be emitted below its date"
|
||||
)
|
||||
# Dates must appear in chronological order: JAN (event), FEB, MAR (event), JUN
|
||||
positions = [
|
||||
@@ -496,8 +504,8 @@ class TestScheduleGeneration:
|
||||
f"Dates should appear chronologically, got positions {positions}"
|
||||
)
|
||||
|
||||
def test_additional_dates_deduplicated_and_merged(self, project_with_case_and_well):
|
||||
"""An additional date equal to an event date must not produce a duplicate DATES entry."""
|
||||
def test_insert_dates_deduplicated_and_merged(self, project_with_case_and_well):
|
||||
"""An inserted date equal to an event date must not produce a duplicate DATES entry."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
|
||||
well_paths = project.well_paths()
|
||||
@@ -511,46 +519,30 @@ class TestScheduleGeneration:
|
||||
diameter=0.1,
|
||||
state="OPEN",
|
||||
)
|
||||
timeline.add_insert_date_event(event_date="2024-01-01")
|
||||
timeline.add_insert_date_event(event_date="2024-01-01")
|
||||
timeline.set_timestamp(timestamp="2024-12-31")
|
||||
|
||||
schedule_text = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=project.well_paths(),
|
||||
first_date_as_comment=False,
|
||||
additional_dates=["2024-01-01", "2024-01-01"],
|
||||
)
|
||||
|
||||
assert schedule_text.count("1 'JAN' 2024") == 1, (
|
||||
"Duplicate additional dates should be merged with the event date"
|
||||
"Duplicate inserted dates should be merged with the event date"
|
||||
)
|
||||
|
||||
def test_additional_dates_invalid_format(self, project_with_case_and_well):
|
||||
"""An unparsable additional date must raise an error mentioning the format."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
|
||||
well_paths = project.well_paths()
|
||||
well_path_a = [wp for wp in well_paths if "A" in wp.name][0]
|
||||
|
||||
timeline.add_perf_event(
|
||||
event_date="2024-01-01",
|
||||
well_path=well_path_a,
|
||||
start_md=1800.0,
|
||||
end_md=2000.0,
|
||||
diameter=0.1,
|
||||
state="OPEN",
|
||||
)
|
||||
timeline.set_timestamp(timestamp="2024-12-31")
|
||||
def test_insert_date_invalid_format(self, project_with_case_and_well):
|
||||
"""An unparsable inserted date must raise an error mentioning the format."""
|
||||
_, _, timeline = project_with_case_and_well
|
||||
|
||||
with pytest.raises(rips.RipsError) as exc_info:
|
||||
timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=project.well_paths(),
|
||||
additional_dates=["not-a-date"],
|
||||
)
|
||||
timeline.add_insert_date_event(event_date="not-a-date")
|
||||
assert "Invalid date format" in str(exc_info.value)
|
||||
|
||||
def test_additional_date_before_first_event(self, project_with_case_and_well):
|
||||
"""An additional date earlier than all events becomes the first date of the schedule."""
|
||||
def test_insert_date_before_first_event(self, project_with_case_and_well):
|
||||
"""An inserted date earlier than all events becomes the first date of the schedule."""
|
||||
project, case, timeline = project_with_case_and_well
|
||||
|
||||
well_paths = project.well_paths()
|
||||
@@ -564,17 +556,17 @@ class TestScheduleGeneration:
|
||||
diameter=0.1,
|
||||
state="OPEN",
|
||||
)
|
||||
timeline.add_insert_date_event(event_date="2023-06-01")
|
||||
timeline.set_timestamp(timestamp="2024-12-31")
|
||||
|
||||
# Default first_date_as_comment=True: the earliest date (the additional
|
||||
# Default first_date_as_comment=True: the earliest date (the inserted
|
||||
# one) becomes the comment; the event date is a real DATES entry.
|
||||
schedule_text = timeline.generate_schedule_text(
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=project.well_paths(),
|
||||
additional_dates=["2023-06-01"],
|
||||
)
|
||||
assert "-- Date: 1 JUN 2023" in schedule_text, (
|
||||
"Earliest (additional) date should be emitted as a comment by default"
|
||||
"Earliest (inserted) date should be emitted as a comment by default"
|
||||
)
|
||||
assert "1 'JAN' 2024" in schedule_text, (
|
||||
"Event date should be a DATES entry when it is no longer first"
|
||||
@@ -585,10 +577,9 @@ class TestScheduleGeneration:
|
||||
eclipse_case=case,
|
||||
export_msw_for_wells=project.well_paths(),
|
||||
first_date_as_comment=False,
|
||||
additional_dates=["2023-06-01"],
|
||||
)
|
||||
assert "1 'JUN' 2023" in schedule_text, (
|
||||
"Additional date should be a DATES entry with first_date_as_comment=False"
|
||||
"Inserted date should be a DATES entry with first_date_as_comment=False"
|
||||
)
|
||||
|
||||
def test_first_date_as_comment(self, project_with_case_and_well):
|
||||
|
||||
@@ -309,6 +309,24 @@ def add_raw_text_event(
|
||||
)
|
||||
|
||||
|
||||
@add_method(WellEventTimeline)
|
||||
def add_insert_date_event(
|
||||
self: WellEventTimeline,
|
||||
event_date: str | date | datetime,
|
||||
comment: str = "",
|
||||
) -> Any:
|
||||
"""Add a date that is emitted as a DATES keyword even without other events.
|
||||
|
||||
In Eclipse/Flow a DATES entry ensures a summary report at that date. The
|
||||
event creates no keyword of its own; ``comment`` (if any) is written as
|
||||
"--"-prefixed lines directly below the date.
|
||||
"""
|
||||
return self.add_insert_date_event_internal(
|
||||
event_date=_format_date(event_date),
|
||||
comment=comment,
|
||||
)
|
||||
|
||||
|
||||
@add_method(WellEventTimeline)
|
||||
def generate_schedule_text(
|
||||
self: WellEventTimeline,
|
||||
@@ -316,7 +334,6 @@ def generate_schedule_text(
|
||||
export_msw_for_wells: List[WellPath] = [],
|
||||
first_date_as_comment: bool = True,
|
||||
align_columns: bool = False,
|
||||
additional_dates: List[str] = [],
|
||||
) -> str:
|
||||
"""Generate Eclipse schedule text for all wells in the collection.
|
||||
|
||||
@@ -341,15 +358,12 @@ def generate_schedule_text(
|
||||
align_columns (bool): When True, emit each keyword with a "--"-prefixed
|
||||
column-header comment and right-aligned, fixed-width columns instead
|
||||
of the compact default form. Defaults to False.
|
||||
additional_dates (List[str]): Additional dates ("YYYY-MM-DD" or a full
|
||||
ISO timestamp such as "2024-05-15T14:45:30") emitted as DATES
|
||||
keywords even when no events fall on them. In Eclipse/Flow a DATES
|
||||
entry ensures a summary report at that date. The dates are merged,
|
||||
deduplicated and sorted together with the event dates, and are not
|
||||
filtered by set_timestamp(). If an additional date precedes all
|
||||
event dates it becomes the earliest date and is therefore emitted
|
||||
as a comment when first_date_as_comment is True; pass
|
||||
first_date_as_comment=False to emit every date as a DATES keyword.
|
||||
|
||||
Dates added with add_insert_date_event() are ordinary timeline events, but
|
||||
since they only force a DATES keyword (e.g. to trigger a summary report)
|
||||
they are not filtered by set_timestamp(). If such a date precedes all other
|
||||
event dates it becomes the earliest date and is therefore emitted as a
|
||||
comment when first_date_as_comment is True.
|
||||
|
||||
Returns:
|
||||
str: Eclipse schedule text containing DATES, COMPDAT, WELSEGS, WCONPROD, etc.
|
||||
@@ -394,7 +408,6 @@ def generate_schedule_text(
|
||||
export_msw_for_wells=export_msw_for_wells,
|
||||
first_date_as_comment=first_date_as_comment,
|
||||
align_columns=align_columns,
|
||||
additional_dates=additional_dates,
|
||||
)
|
||||
if container and container.values:
|
||||
return "".join(container.values)
|
||||
|
||||
Reference in new issue
Block a user