mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
#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:
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:
|
||||
|
||||
+1
-1
@@ -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")
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user