#14688 Sim Events: Add API to query event dates and wells

Add event date and well queries with date, owner and event type/keyword
filters to SimulatorEventsDocument and WellEventTimeline, including
latest_event_date() and wells_with_msw_events() for building the
export_msw_for_wells list.
This commit is contained in:
Kristian Bendiksen committed 2026-10-19 12:00:28 +02:00
1 parent 360c3c69db
commit 8dc1d56bd3
11 files changed
+903 -3

No files matched your search

@@ -219,6 +219,50 @@ QDateTime RimWellEventTimeline::lastAppliedTimestamp() const
return m_lastAppliedTimestamp;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RimWellEvent*> RimWellEventTimeline::queryEvents( const EventQuery& query ) const
{
auto keywordName = []( const RimWellEvent* event ) -> QString
{
if ( auto keywordEvent = dynamic_cast<const RimWellEventKeyword*>( event ) ) return keywordEvent->keywordName().toUpper();
if ( auto scheduleKeywordEvent = dynamic_cast<const RimKeywordEvent*>( event ) )
return scheduleKeywordEvent->keywordName().toUpper();
return {};
};
auto matchesType = [&]( const RimWellEvent* event )
{
if ( query.eventTypes.empty() && query.keywords.empty() ) return true;
if ( query.eventTypes.contains( event->eventType() ) ) return true;
if ( query.keywords.empty() ) return false;
const QString keyword = keywordName( event );
return !keyword.isEmpty() && query.keywords.contains( keyword );
};
std::vector<RimWellEvent*> result;
for ( auto& event : m_events )
{
if ( !event || !event->eventDate().isValid() ) continue;
const QDateTime date = event->eventDate();
if ( query.startDate.isValid() && date < query.startDate ) continue;
if ( query.endDate.isValid() && date > query.endDate ) continue;
if ( !query.wellPaths.empty() && !query.wellPaths.contains( event->wellPath() ) ) continue;
if ( !matchesType( event ) ) continue;
result.push_back( event );
}
std::stable_sort( result.begin(),
result.end(),
[]( const RimWellEvent* a, const RimWellEvent* b ) { return a->eventDate() < b->eventDate(); } );
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -26,6 +26,9 @@
#include "cafPdmPtrArrayField.h"
#include <QDateTime>
#include <QString>
#include <set>
#include <vector>
class RimWellEventPerf;
@@ -59,6 +62,16 @@ public:
TYPE
};
// Filter for queryEvents(). Invalid dates leave the range open, empty sets disable the filter.
struct EventQuery
{
QDateTime startDate;
QDateTime endDate;
std::set<const RimWellPath*> wellPaths;
std::set<RimWellEvent::EventType> eventTypes;
std::set<QString> keywords; // Upper case keyword names
};
RimWellEventTimeline();
~RimWellEventTimeline() override;
@@ -73,6 +86,9 @@ public:
std::vector<RimWellPath*> getWellPathsWithEvents() const;
std::vector<RimWellPath*> getWellPathsWithEventsUpToDate( const QDateTime& date ) const;
// Events matching all filters in the query, sorted by date. Ignores the applied timestamp.
std::vector<RimWellEvent*> queryEvents( const EventQuery& query ) const;
// Add event methods (return the created event for further configuration)
RimWellEventPerf* addPerforationEvent( RimWellPath* wellPath, const QDateTime& date );
RimWellEventValve* addValveEvent( RimWellPath* wellPath, const QDateTime& date );
@@ -48,6 +48,52 @@
namespace
{
//--------------------------------------------------------------------------------------------------
/// Builds a timeline query from the scripting arguments. Empty dates leave the range open. Each event
/// type is either an event type name (SEGMENT, PERF, ...) or a keyword name (WCONHIST, ...).
//--------------------------------------------------------------------------------------------------
std::expected<RimWellEventTimeline::EventQuery, QString> createEventQuery( const QString& startDate,
const QString& endDate,
const std::vector<RimWellPath*>& wellPaths,
const std::vector<QString>& eventTypes )
{
RimWellEventTimeline::EventQuery query;
auto parseDate = []( const QString& text, QDateTime& date ) -> std::expected<void, QString>
{
if ( text.trimmed().isEmpty() ) return {};
date = QDateTime::fromString( text.trimmed(), Qt::ISODate );
if ( !date.isValid() ) return std::unexpected( QString( "Invalid date format: %1. Expected YYYY-MM-DD" ).arg( text ) );
return {};
};
if ( auto result = parseDate( startDate, query.startDate ); !result ) return std::unexpected( result.error() );
if ( auto result = parseDate( endDate, query.endDate ); !result ) return std::unexpected( result.error() );
for ( auto wellPath : wellPaths )
{
if ( wellPath ) query.wellPaths.insert( wellPath );
}
for ( const auto& eventType : eventTypes )
{
const QString name = eventType.trimmed().toUpper();
if ( name.isEmpty() ) continue;
if ( caf::AppEnum<RimWellEvent::EventType>::isValid( name ) )
{
query.eventTypes.insert( caf::AppEnum<RimWellEvent::EventType>::fromText( name ) );
}
else
{
query.keywords.insert( name );
}
}
return query;
}
std::expected<void, QString> validateKeywordItems( const QString& keywordName, const std::vector<QString>& itemNames )
{
auto keywordInfo = RiaOpmKeywordTools::keywordInfo( keywordName );
@@ -898,3 +944,134 @@ QString RimcWellEventTimeline_generateSchedule::classKeywordReturnedType() const
{
return RimcDataContainerString::classKeywordStatic();
}
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellEventTimeline, RimcWellEventTimeline_eventDates, "QueryEventDates" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimcWellEventTimeline_eventDates::RimcWellEventTimeline_eventDates( caf::PdmObjectHandle* self )
: caf::PdmObjectMethod( self, PdmObjectMethod::NullPointerType::NULL_IS_INVALID, PdmObjectMethod::ResultType::PERSISTENT_FALSE )
{
CAF_PDM_InitObject( "Event Dates", "", "", "Sorted, unique dates of the events matching the filters, as ISO date-time strings" );
CAF_PDM_InitScriptableField( &m_startDate, "StartDate", QString(), "", "", "", "Earliest date to include (inclusive). Empty: no limit" );
CAF_PDM_InitScriptableField( &m_endDate,
"EndDate",
QString(),
"",
"",
"",
"Latest date to include (inclusive). A date-only value means midnight, excluding events later that day. "
"Empty: no limit" );
CAF_PDM_InitScriptableFieldNoDefault( &m_wellPaths,
"WellPaths",
"",
"",
"",
"Only include events for these wells. Empty: events for all wells and schedule events" );
CAF_PDM_InitScriptableFieldNoDefault( &m_eventTypes,
"EventTypes",
"",
"",
"",
"Event types (SEGMENT, PERF, VALVE, ...) or keyword names (WCONHIST, ...). Empty: all events" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_eventDates::execute()
{
auto timeline = self<RimWellEventTimeline>();
auto query = createEventQuery( m_startDate(), m_endDate(), m_wellPaths.ptrReferencedObjectsByType(), m_eventTypes() );
if ( !query ) return std::unexpected( query.error() );
std::set<QDateTime> dates;
for ( auto event : timeline->queryEvents( *query ) )
{
dates.insert( event->eventDate() );
}
// Explicit format without a time zone suffix, as the event dates are stored in local time
std::vector<QString> dateTexts;
for ( const auto& date : dates )
{
dateTexts.push_back( date.toString( "yyyy-MM-ddTHH:mm:ss" ) );
}
auto* dataObject = new RimcDataContainerString();
dataObject->m_stringValues = dateTexts;
return dataObject;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimcWellEventTimeline_eventDates::classKeywordReturnedType() const
{
return RimcDataContainerString::classKeywordStatic();
}
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellEventTimeline, RimcWellEventTimeline_wellsWithEvents, "QueryWellsWithEvents" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimcWellEventTimeline_wellsWithEvents::RimcWellEventTimeline_wellsWithEvents( caf::PdmObjectHandle* self )
: caf::PdmObjectMethod( self, PdmObjectMethod::NullPointerType::NULL_IS_INVALID, PdmObjectMethod::ResultType::PERSISTENT_FALSE )
{
CAF_PDM_InitObject( "Wells With Events",
"",
"",
"Sorted names of the well paths with events matching the filters. "
"Well path names must be unique within the well path collection" );
CAF_PDM_InitScriptableField( &m_startDate, "StartDate", QString(), "", "", "", "Earliest date to include (inclusive). Empty: no limit" );
CAF_PDM_InitScriptableField( &m_endDate,
"EndDate",
QString(),
"",
"",
"",
"Latest date to include (inclusive). A date-only value means midnight, excluding events later that day. "
"Empty: no limit" );
CAF_PDM_InitScriptableFieldNoDefault( &m_eventTypes,
"EventTypes",
"",
"",
"",
"Event types (SEGMENT, PERF, VALVE, ...) or keyword names (WCONHIST, ...). Empty: all events" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_wellsWithEvents::execute()
{
auto timeline = self<RimWellEventTimeline>();
auto query = createEventQuery( m_startDate(), m_endDate(), {}, m_eventTypes() );
if ( !query ) return std::unexpected( query.error() );
std::set<QString> wellNames;
for ( auto event : timeline->queryEvents( *query ) )
{
if ( event->wellPath() ) wellNames.insert( event->wellPath()->name() );
}
auto* dataObject = new RimcDataContainerString();
dataObject->m_stringValues = std::vector<QString>( wellNames.begin(), wellNames.end() );
return dataObject;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimcWellEventTimeline_wellsWithEvents::classKeywordReturnedType() const
{
return RimcDataContainerString::classKeywordStatic();
}
@@ -300,3 +300,42 @@ private:
caf::PdmField<bool> m_firstDateAsComment;
caf::PdmField<bool> m_alignColumns;
};
//==================================================================================================
///
//==================================================================================================
class RimcWellEventTimeline_eventDates : public caf::PdmObjectMethod
{
CAF_PDM_HEADER_INIT;
public:
RimcWellEventTimeline_eventDates( caf::PdmObjectHandle* self );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
QString classKeywordReturnedType() const override;
private:
caf::PdmField<QString> m_startDate;
caf::PdmField<QString> m_endDate;
caf::PdmPtrArrayField<RimWellPath*> m_wellPaths;
caf::PdmField<std::vector<QString>> m_eventTypes;
};
//==================================================================================================
///
//==================================================================================================
class RimcWellEventTimeline_wellsWithEvents : public caf::PdmObjectMethod
{
CAF_PDM_HEADER_INIT;
public:
RimcWellEventTimeline_wellsWithEvents( caf::PdmObjectHandle* self );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
QString classKeywordReturnedType() const override;
private:
caf::PdmField<QString> m_startDate;
caf::PdmField<QString> m_endDate;
caf::PdmField<std::vector<QString>> m_eventTypes;
};
@@ -125,9 +125,15 @@ def main() -> None:
if report.events_applied == 0:
print(" No events found - skipping schedule generation.")
return
# Export multi-segment-well keywords only for the wells with SEGMENT events.
msw_wells = timeline.wells_with_msw_events()
print(
f" Event dates: {timeline.earliest_event_date()} to {timeline.latest_event_date()}"
)
print(f" MSW wells: {[well.name for well in msw_wells]}")
schedule_text = timeline.generate_schedule_text(
eclipse_case=case,
export_msw_for_wells=project.well_paths(),
export_msw_for_wells=msw_wells,
align_columns=True,
)
if schedule_text:
@@ -237,7 +237,7 @@ def main() -> None:
# 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],
export_msw_for_wells=timeline.wells_with_msw_events(),
align_columns=True,
)
if schedule_text:
@@ -502,6 +502,138 @@ class SimulatorEventsDocument:
"""The dates of the expanded ``INSERT_DATE`` events, in document order."""
return [event.event_date for event in self.insert_date_events]
def events(
self,
start_date: Optional[datetime.datetime] = None,
end_date: Optional[datetime.datetime] = None,
wells: Optional[List[str]] = None,
groups: Optional[List[str]] = None,
event_types: Optional[List[str]] = None,
) -> List[SimulatorEvent]:
"""Return events for the selected owners, filtered by date and type.
Events are sorted by date. Equal dates keep their document order (WELL
blocks, GROUP blocks, SCHEDULE events, then INSERT_DATE events).
When both ``wells`` and ``groups`` are given, matching WELL and GROUP
events are combined (union). Date and type filters apply to that union.
Arguments:
start_date: Earliest event date to include (inclusive). ``None``
leaves the range open.
end_date: Latest event date to include (inclusive). ``None``
leaves the range open.
wells: Well names. Only events in matching WELL blocks are
included.
groups: Group names. Only events in matching GROUP blocks are
included. When ``wells`` and/or ``groups`` is given, SCHEDULE
and INSERT_DATE events are excluded.
event_types: Event types (``SEGMENT``, ``PERFORATION``,
``INSERT_DATE``, ...) or keyword names (``WCONHIST``,
``RPTRST``, ...), matched case-insensitively.
An empty list or ``None`` disables the corresponding filter.
"""
well_names = set(wells or [])
group_names = set(groups or [])
type_names = {name.upper() for name in event_types or []}
candidates: List[SimulatorEvent] = []
if not group_names or well_names:
for well in self.wells:
if not well_names or well.well_name in well_names:
candidates.extend(well.events)
if not well_names or group_names:
for group in self.groups:
if not group_names or group.group_name in group_names:
candidates.extend(group.events)
if not well_names and not group_names:
candidates.extend(self.schedule_events)
candidates.extend(self.insert_date_events)
selected = [
event
for event in candidates
if (start_date is None or event.event_date >= start_date)
and (end_date is None or event.event_date <= end_date)
and (not type_names or event.event_type.upper() in type_names)
]
return sorted(selected, key=lambda event: event.event_date)
def event_dates(self, **filters: Any) -> List[datetime.datetime]:
"""Return the sorted, unique dates of the events matching ``filters``.
Accepts the same keyword arguments as :meth:`events`.
"""
return sorted({event.event_date for event in self.events(**filters)})
def earliest_event_date(self, **filters: Any) -> Optional[datetime.datetime]:
"""Return the earliest date of the events matching ``filters``, or
``None`` when no event matches. Accepts the arguments of :meth:`events`.
"""
dates = self.event_dates(**filters)
return dates[0] if dates else None
def latest_event_date(self, **filters: Any) -> Optional[datetime.datetime]:
"""Return the latest date of the events matching ``filters``, or
``None`` when no event matches. Accepts the arguments of :meth:`events`.
"""
dates = self.event_dates(**filters)
return dates[-1] if dates else None
def wells_with_events(
self,
start_date: Optional[datetime.datetime] = None,
end_date: Optional[datetime.datetime] = None,
event_types: Optional[List[str]] = None,
) -> List[str]:
"""Return the sorted names of the wells with events matching the filters.
See :meth:`events` for the filter semantics.
"""
return sorted(
{
well.well_name
for well in self.wells
if self.events(
start_date=start_date,
end_date=end_date,
wells=[well.well_name],
event_types=event_types,
)
}
)
def wells_with_msw_events(self) -> List[str]:
"""Return the sorted names of wells with ``SEGMENT`` or ``VALVE`` events.
Use these names to build ``export_msw_for_wells``. Wells with only
``PERFORATION`` events are not included, even if segments were configured
directly on their well paths; add such MSW wells explicitly.
"""
return self.wells_with_events(event_types=["SEGMENT", "VALVE"])
def groups_with_events(
self,
start_date: Optional[datetime.datetime] = None,
end_date: Optional[datetime.datetime] = None,
event_types: Optional[List[str]] = None,
) -> List[str]:
"""Return the sorted names of the groups with events matching the
filters. See :meth:`events` for the filter semantics.
"""
return sorted(
{
group.group_name
for group in self.groups
if self.events(
start_date=start_date,
end_date=end_date,
groups=[group.group_name],
event_types=event_types,
)
}
)
def _iso_event_date(event_date: datetime.datetime) -> str:
"""Format an event date for the timeline API.
@@ -1107,6 +1107,90 @@ class TestParsing:
assert value == datetime.datetime(2018, 1, 1, 0, 0, 0)
class TestDocumentQueries:
"""Query event dates, wells and groups from a parsed document (#14688)."""
TEXT = (
"SIMEVENTS 1.2\n"
'WELL "OP_1"\n'
" 2024-01-01 SEGMENT MDSTART=0 MDEND=2500\n"
" 2024-01-01 PERFORATION MDSTART=2000 MDEND=2200\n"
" 2024-06-01 WCONHIST STATUS=OPEN\n"
'WELL "OP_2"\n'
" 2024-03-01 PERFORATION MDSTART=2000 MDEND=2200\n"
'GROUP "FIELD"\n'
" 2024-02-01 GCONPROD CMODE=LRAT LRAT=1000\n"
"SCHEDULE\n"
" 2025-01-01 RPTRST BASIC=2\n"
" 2030-01-01 INSERT_DATE\n"
)
@pytest.fixture
def doc(self):
return parse_simulator_events(self.TEXT)
def test_event_dates_cover_all_blocks(self, doc):
D = datetime.datetime
assert doc.event_dates() == [
D(2024, 1, 1),
D(2024, 2, 1),
D(2024, 3, 1),
D(2024, 6, 1),
D(2025, 1, 1),
D(2030, 1, 1),
]
assert doc.latest_event_date() == D(2030, 1, 1)
assert doc.earliest_event_date() == D(2024, 1, 1)
assert doc.latest_event_date(event_types=["INSERT_DATE"]) == D(2030, 1, 1)
assert doc.latest_event_date(event_types=["NOSUCH"]) is None
def test_date_bounds_are_inclusive(self, doc):
D = datetime.datetime
assert doc.event_dates(start_date=D(2024, 2, 1), end_date=D(2024, 6, 1)) == [
D(2024, 2, 1),
D(2024, 3, 1),
D(2024, 6, 1),
]
def test_owner_and_type_filters(self, doc):
D = datetime.datetime
assert [e.event_type for e in doc.events(wells=["OP_1"])] == [
"SEGMENT",
"PERFORATION",
"WCONHIST",
]
assert [e.event_type for e in doc.events(groups=["FIELD"])] == ["GCONPROD"]
# Owner filters exclude SCHEDULE and INSERT_DATE events
assert doc.event_dates(wells=["OP_2"], groups=["FIELD"]) == [
D(2024, 2, 1),
D(2024, 3, 1),
]
assert doc.event_dates(event_types=["perforation", "rptrst"]) == [
D(2024, 1, 1),
D(2024, 3, 1),
D(2025, 1, 1),
]
def test_wells_and_groups_with_events(self, doc):
assert doc.wells_with_events() == ["OP_1", "OP_2"]
assert doc.wells_with_events(start_date=datetime.datetime(2024, 2, 1)) == [
"OP_1",
"OP_2",
]
assert doc.wells_with_events(event_types=["WCONHIST"]) == ["OP_1"]
assert doc.wells_with_msw_events() == ["OP_1"]
assert doc.groups_with_events() == ["FIELD"]
assert doc.groups_with_events(event_types=["GCONINJE"]) == []
def test_msw_wells_include_valves_without_segment_events(self):
doc = parse_simulator_events(
self.TEXT
+ 'WELL "VALVE_ONLY"\n'
+ " 2024-03-01 VALVE MD=2100 TYPE=ICV STATE=OPEN CV=0.7 AREA=0.0001\n"
)
assert doc.wells_with_msw_events() == ["OP_1", "VALVE_ONLY"]
class TestDurations:
"""Go-style duration literals (issue #14651)."""
@@ -3178,3 +3178,210 @@ class TestScheduleGenerationWithoutCase:
assert any("COMPDAT" in w and export_name in w for w in warnings)
assert any("WELSPECS" in w for w in warnings)
assert any("WELSEGS" in w for w in warnings)
class TestTimelineQueries:
"""#14688: query event dates and wells from the timeline."""
@pytest.fixture
def populated_timeline(self, rips_instance, initialize_test):
"""Timeline with a SEGMENT well (A), a perforation-only well (B), a
schedule keyword and an insert date."""
case_root = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
project = rips_instance.project
case = project.load_case(path=case_root + "/TEST10K_FLT_LGR_NNC.EGRID")
project.import_well_paths(
well_path_files=[
case_root + "/wellpath_a.dev",
case_root + "/wellpath_b.dev",
]
)
timeline = project.descendants(rips.WellPathCollection)[0].event_timeline()
well_paths = project.well_paths()
well_a = [wp for wp in well_paths if "A" in wp.name][0]
well_b = [wp for wp in well_paths if "B" in wp.name][0]
timeline.add_segment_event(
event_date="2024-01-01",
well_path=well_a,
start_md=0.0,
end_md=2500.0,
inner_diameter=0.15,
roughness=1.0e-5,
)
timeline.add_perf_event(
event_date="2024-01-01",
well_path=well_a,
start_md=2000.0,
end_md=2200.0,
diameter=0.1,
state="OPEN",
)
timeline.add_perf_event(
event_date="2024-03-01",
well_path=well_b,
start_md=2000.0,
end_md=2200.0,
diameter=0.1,
state="OPEN",
)
timeline.add_well_keyword_event(
event_date="2024-06-01",
well_path=well_b,
keyword_name="WCONHIST",
keyword_data={"WELL": well_b.name, "STATUS": "OPEN"},
)
timeline.add_keyword_event(
event_date="2025-01-01",
keyword_name="RPTRST",
keyword_data={"BASIC": 2},
)
timeline.add_insert_date_event(event_date="2030-01-01")
return case, timeline, well_a, well_b
def test_event_dates_and_bounds(self, populated_timeline):
from datetime import datetime
_, timeline, _, _ = populated_timeline
assert timeline.event_dates() == [
datetime(2024, 1, 1),
datetime(2024, 3, 1),
datetime(2024, 6, 1),
datetime(2025, 1, 1),
datetime(2030, 1, 1),
]
assert timeline.latest_event_date() == datetime(2030, 1, 1)
assert timeline.earliest_event_date() == datetime(2024, 1, 1)
assert timeline.latest_event_date(event_types=["INSERT_DATE"]) == datetime(
2030, 1, 1
)
# Inclusive bounds
assert timeline.event_dates(start_date="2024-03-01", end_date="2024-06-01") == [
datetime(2024, 3, 1),
datetime(2024, 6, 1),
]
assert timeline.latest_event_date(end_date=datetime(2024, 12, 31)) == datetime(
2024, 6, 1
)
assert timeline.latest_event_date(start_date="2031-01-01") is None
def test_event_dates_filters(self, populated_timeline):
from datetime import datetime
_, timeline, well_a, well_b = populated_timeline
assert timeline.event_dates(wells=[well_b]) == [
datetime(2024, 3, 1),
datetime(2024, 6, 1),
]
assert timeline.event_dates(event_types=["segment"]) == [datetime(2024, 1, 1)]
# Keyword names match well and schedule keyword events
assert timeline.event_dates(event_types=["WCONHIST", "rptrst"]) == [
datetime(2024, 6, 1),
datetime(2025, 1, 1),
]
assert timeline.event_dates(wells=[well_a], event_types=["WCONHIST"]) == []
def test_set_timestamp_does_not_limit_queries(self, populated_timeline):
from datetime import datetime
_, timeline, _, _ = populated_timeline
timeline.set_timestamp(timestamp="2024-02-01")
assert timeline.latest_event_date() == datetime(2030, 1, 1)
def test_wells_with_events(self, populated_timeline):
_, timeline, well_a, well_b = populated_timeline
assert [wp.name for wp in timeline.wells_with_events()] == sorted(
[well_a.name, well_b.name]
)
assert [
wp.name for wp in timeline.wells_with_events(start_date="2024-02-01")
] == [well_b.name]
assert [wp.name for wp in timeline.wells_with_msw_events()] == [well_a.name]
def test_msw_wells_drive_schedule_export(self, populated_timeline):
case, timeline, well_a, well_b = populated_timeline
timeline.set_timestamp(timestamp="2030-01-01")
schedule = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=timeline.wells_with_msw_events()
)
lines = schedule.splitlines()
welsegs_wells = [
lines[i + 1].split()[0].strip("'")
for i, line in enumerate(lines)
if line == "WELSEGS"
]
assert welsegs_wells == [well_a.completion_settings().well_name_for_export]
assert well_b.completion_settings().well_name_for_export in schedule
@pytest.mark.parametrize(
"valve_type, keyword", [("ICD", "WSEGVALV"), ("AICD", "WSEGAICD")]
)
def test_msw_wells_include_valves_without_segment_events(
self, populated_timeline, valve_type, keyword
):
case, timeline, well_a, well_b = populated_timeline
well_b.segment_collection().add_segment_interval(
start_md=0.0, end_md=2500.0, diameter=0.15, roughness_factor=1.0e-5
)
timeline.add_valve_event(
event_date="2024-03-01",
well_path=well_b,
measured_depth=2100.0,
valve_type=valve_type,
state="OPEN",
)
assert timeline.event_dates(wells=[well_b], event_types=["SEGMENT"]) == []
msw_wells = timeline.wells_with_msw_events()
assert [wp.name for wp in msw_wells] == sorted([well_a.name, well_b.name])
timeline.set_timestamp(timestamp="2030-01-01")
schedule = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=msw_wells
)
assert f"\n{keyword}\n" in schedule
@pytest.mark.parametrize("event_types", [None, ["SEGMENT"]])
def test_wells_with_events_rejects_ambiguous_names(
self, populated_timeline, event_types
):
_, timeline, well_a, well_b = populated_timeline
well_b.name = well_a.name
well_b.update()
assert well_a.address() != well_b.address()
with pytest.raises(ValueError, match="Ambiguous well path name"):
timeline.wells_with_events(event_types=event_types)
def test_date_only_end_excludes_later_events(self, populated_timeline):
from datetime import datetime
_, timeline, _, well_b = populated_timeline
timeline.add_valve_event(
event_date="2024-03-01T12:30:00", well_path=well_b, measured_depth=2100.0
)
assert timeline.event_dates(wells=[well_b], end_date="2024-03-01") == [
datetime(2024, 3, 1)
]
assert timeline.event_dates(wells=[well_b], end_date="2024-03-01T12:30:00") == [
datetime(2024, 3, 1),
datetime(2024, 3, 1, 12, 30),
]
assert (
timeline.wells_with_events(end_date="2024-03-01", event_types=["VALVE"])
== []
)
assert [
wp.name
for wp in timeline.wells_with_events(
end_date="2024-03-01T12:30:00", event_types=["VALVE"]
)
] == [well_b.name]
def test_invalid_date_raises(self, populated_timeline):
_, timeline, _, _ = populated_timeline
with pytest.raises(rips.RipsError):
timeline.event_dates(start_date="not-a-date")
+145
View File
@@ -17,6 +17,7 @@ from .generated.generated_classes import (
WellEventKeyword,
WellEventTimeline,
WellPath,
WellPathCollection,
)
@@ -419,3 +420,147 @@ def generate_schedule_text(
if container and container.values:
return "".join(container.values)
return ""
def _format_query_date(query_date: Optional[str | date | datetime]) -> str:
"""Convert an optional query bound to an ISO string. None gives an empty
string (no limit). A datetime keeps its time of day."""
if query_date is None:
return ""
if isinstance(query_date, datetime):
return query_date.isoformat(timespec="seconds")
return _format_date(query_date)
@add_method(WellEventTimeline)
def event_dates(
self: WellEventTimeline,
start_date: Optional[str | date | datetime] = None,
end_date: Optional[str | date | datetime] = None,
wells: Optional[List[WellPath]] = None,
event_types: Optional[List[str]] = None,
) -> List[datetime]:
"""Return the sorted, unique dates of the timeline events matching all filters.
The full timeline is queried; set_timestamp() does not limit the result.
Arguments:
start_date: Earliest event date to include (inclusive). None: no limit.
end_date: Latest event date to include (inclusive). None: no limit. A
date without time of day means midnight, so events later that day
are excluded.
wells (Optional[List[WellPath]]): Only include events for these wells.
When given, schedule keyword, raw text and insert-date events are
excluded. None or empty: no well filter.
event_types (Optional[List[str]]): Timeline event types (SEGMENT, PERF,
VALVE, WSTATE, WTYPE, WELLSPEC, WCONTROL, KEYWORD,
SCHEDULE_KEYWORD, RAW_TEXT, INSERT_DATE) or keyword names
(WCONHIST, RPTRST, ...), case-insensitive. None or empty: all
events.
Returns:
List[datetime]: The event dates.
Example:
.. code-block:: python
last_date = timeline.latest_event_date()
timeline.set_timestamp(timestamp=last_date.isoformat())
"""
container = self.query_event_dates(
start_date=_format_query_date(start_date),
end_date=_format_query_date(end_date),
well_paths=wells or [],
event_types=event_types or [],
)
values = container.values if container and container.values else []
return [datetime.fromisoformat(value) for value in values]
@add_method(WellEventTimeline)
def earliest_event_date(
self: WellEventTimeline,
start_date: Optional[str | date | datetime] = None,
end_date: Optional[str | date | datetime] = None,
wells: Optional[List[WellPath]] = None,
event_types: Optional[List[str]] = None,
) -> Optional[datetime]:
"""Return the earliest date of the timeline events matching all filters,
or None when no event matches. See event_dates() for the arguments."""
dates = event_dates(self, start_date, end_date, wells, event_types)
return dates[0] if dates else None
@add_method(WellEventTimeline)
def latest_event_date(
self: WellEventTimeline,
start_date: Optional[str | date | datetime] = None,
end_date: Optional[str | date | datetime] = None,
wells: Optional[List[WellPath]] = None,
event_types: Optional[List[str]] = None,
) -> Optional[datetime]:
"""Return the latest date of the timeline events matching all filters, or
None when no event matches. See event_dates() for the arguments."""
dates = event_dates(self, start_date, end_date, wells, event_types)
return dates[-1] if dates else None
@add_method(WellEventTimeline)
def wells_with_events(
self: WellEventTimeline,
start_date: Optional[str | date | datetime] = None,
end_date: Optional[str | date | datetime] = None,
event_types: Optional[List[str]] = None,
) -> List[WellPath]:
"""Return the well paths with timeline events matching all filters, sorted
by name. See event_dates() for the arguments.
Matching well names must be unique within the well path collection.
Raises ValueError if a name is ambiguous or cannot be resolved.
Returns:
List[WellPath]: The well paths.
"""
container = self.query_wells_with_events(
start_date=_format_query_date(start_date),
end_date=_format_query_date(end_date),
event_types=event_types or [],
)
names = container.values if container and container.values else []
if not names:
return []
collection = self.ancestor(WellPathCollection)
if collection is None:
raise ValueError("Cannot resolve event wells without a well path collection")
matching_names = set(names)
well_paths_by_name: Dict[str, WellPath] = {}
for well_path in collection.descendants(WellPath):
if well_path.name not in matching_names:
continue
if well_path.name in well_paths_by_name:
raise ValueError(f"Ambiguous well path name: {well_path.name!r}")
well_paths_by_name[well_path.name] = well_path
missing_names = matching_names - well_paths_by_name.keys()
if missing_names:
raise ValueError(f"Cannot resolve event well names: {sorted(missing_names)!r}")
return [well_paths_by_name[name] for name in names]
@add_method(WellEventTimeline)
def wells_with_msw_events(self: WellEventTimeline) -> List[WellPath]:
"""Return the well paths with SEGMENT or VALVE events, sorted by name.
Wells with only PERF events are not included, even if segments were
configured directly on their well paths; add such MSW wells explicitly
to export_msw_for_wells.
Example:
.. code-block:: python
schedule_text = timeline.generate_schedule_text(
eclipse_case=case,
export_msw_for_wells=timeline.wells_with_msw_events(),
)
"""
return wells_with_events(self, event_types=["SEGMENT", "VALVE"])
+51 -1
View File
@@ -348,7 +348,7 @@ print(report.events_applied, report.events_skipped, report.warnings, report.erro
timeline.set_timestamp(timestamp="2024-12-24") # materialize completions up to a date
schedule_text = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=[well_path]
eclipse_case=case, export_msw_for_wells=timeline.wells_with_msw_events()
)
```
@@ -363,6 +363,56 @@ They also accept two policies, each `"warn"` (default), `"error"` or `"skip"`:
The returned `ApplyReport` carries `events_applied`, `events_skipped`, `warnings` and `errors`.
### Querying events
Both the parsed document and the timeline can answer common questions directly, so clients do not
need to walk wells, groups and schedule events themselves. The filters are the same in both layers:
| Filter | Meaning |
|---|---|
| `start_date`, `end_date` | Inclusive date range. `None` leaves the bound open. A date without time of day is midnight. |
| `wells` | Only events for these wells. When given, schedule-level and `INSERT_DATE` events are excluded. |
| `event_types` | Event types or keyword names (`WCONHIST`, `RPTRST`, ...), case-insensitive. |
An empty list or `None` disables a filter. The filters are combined with AND.
On a `SimulatorEventsDocument` (pure Python, no ResInsight needed), wells and groups are given by name and
event types use the SIMEVENTS names (`SEGMENT`, `PERFORATION`, `INSERT_DATE`, ...). `groups` is an additional
owner filter. When both `wells` and `groups` are supplied, matching WELL and GROUP events are combined
(union), then filtered by date and event type:
```python
document = rips.simulator_events.parse_simulator_events_file("simulator_events.events")
document.events(wells=["OP_1"], event_types=["WCONHIST"]) # List[SimulatorEvent], sorted by date
document.event_dates(start_date=datetime.datetime(2024, 1, 1))
document.latest_event_date() # or earliest_event_date(); None when nothing matches
document.wells_with_events(event_types=["PERFORATION"]) # well names
document.wells_with_msw_events() # wells with SEGMENT or VALVE events
document.groups_with_events()
```
On a `WellEventTimeline`, wells are `WellPath` objects. Event types use the timeline names `SEGMENT`, `PERF`,
`VALVE`, `WSTATE`, `WTYPE`, `WELLSPEC`, `WCONTROL`, `KEYWORD`, `SCHEDULE_KEYWORD`, `RAW_TEXT` and `INSERT_DATE`.
Any other name is matched against keyword names. The queries always use the full timeline. `set_timestamp()` does
not limit them:
```python
timeline.event_dates(wells=[well_path]) # List[datetime]
timeline.set_timestamp(timestamp=timeline.latest_event_date().isoformat())
timeline.wells_with_events(start_date="2025-01-01") # List[WellPath]
schedule_text = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=timeline.wells_with_msw_events()
)
```
`wells_with_msw_events()` includes wells with `SEGMENT` or `VALVE` events. It does not inspect
segment configuration on well paths: add MSW wells with only `PERFORATION` (`PERF` on the timeline)
events explicitly to `export_msw_for_wells`.
Timeline well queries resolve names within the well path collection. Matching names must be unique;
`wells_with_events()` and `wells_with_msw_events()` raise `ValueError` rather than choose between
well paths sharing a name.
### Generating a schedule without a grid
`eclipse_case` is optional in `generate_schedule_text()`. It accepts any Eclipse-type case, including Roff and