diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index c76f48cf25..78738795f6 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -29,6 +29,7 @@ #include "RimEclipseCase.h" #include "RimKeywordEvent.h" #include "RimWellEventTimeline.h" +#include "RimWellEventWellSpec.h" #include "RimWellPath.h" #include "RimWellPathCompletionSettings.h" @@ -270,6 +271,10 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin { isRelevant = keywordName == "WSEGVALV" || keywordName == "WSEGAICD"; } + else if ( event->eventType() == RimWellEvent::EventType::WELLSPEC ) + { + isRelevant = keywordName == "WELSPECS"; + } else if ( event->eventType() == RimWellEvent::EventType::SCHEDULE_KEYWORD ) { const auto* keywordEvent = dynamic_cast( event ); @@ -353,7 +358,7 @@ std::optional RicScheduleDataGenerator::generateWelspecsForWel for ( auto* event : events ) { if ( ( event->eventType() == RimWellEvent::EventType::PERF || event->eventType() == RimWellEvent::EventType::VALVE || - event->eventType() == RimWellEvent::EventType::TUBING ) && + event->eventType() == RimWellEvent::EventType::TUBING || event->eventType() == RimWellEvent::EventType::WELLSPEC ) && event->wellName() == well.name() ) { hasEvents = true; @@ -363,8 +368,33 @@ std::optional RicScheduleDataGenerator::generateWelspecsForWel if ( !hasEvents ) return std::nullopt; + const RimWellEventWellSpec* firstWellSpec = nullptr; + const RimWellEventWellSpec* latestWellSpec = nullptr; + for ( auto* event : timeline.events() ) + { + if ( event->eventType() != RimWellEvent::EventType::WELLSPEC || event->wellName() != well.name() ) continue; + + auto* wellSpec = dynamic_cast( event ); + if ( !wellSpec ) continue; + if ( !firstWellSpec || wellSpec->eventDate() < firstWellSpec->eventDate() ) firstWellSpec = wellSpec; + if ( wellSpec->eventDate() <= date && ( !latestWellSpec || wellSpec->eventDate() > latestWellSpec->eventDate() ) ) + { + latestWellSpec = wellSpec; + } + } + + std::optional wellSpecData; + if ( latestWellSpec ) + { + wellSpecData = latestWellSpec->wellSpecData(); + } + else if ( firstWellSpec ) + { + wellSpecData = firstWellSpec->baselineData(); + } + std::string wellGroupName = well.completionSettings()->groupNameForExport().toStdString(); - auto welspecsKw = RimKeywordFactory::welspecsKeyword( wellGroupName, &eclipseCase, &well ); + auto welspecsKw = RimKeywordFactory::welspecsKeyword( wellGroupName, &eclipseCase, &well, wellSpecData ? &wellSpecData.value() : nullptr ); if ( welspecsKw.name().empty() ) return std::nullopt; return welspecsKw; } diff --git a/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.cpp b/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.cpp index 555a9a7cc0..6c6203a0fb 100644 --- a/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.cpp +++ b/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.cpp @@ -266,6 +266,22 @@ QString RimWellPathCompletionSettings::groupName() const return m_groupName(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellPathCompletionSettings::WellType RimWellPathCompletionSettings::wellType() const +{ + return m_preferredFluidPhase(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimWellPathCompletionSettings::allowWellCrossFlow() const +{ + return m_allowWellCrossFlow(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -274,6 +290,30 @@ void RimWellPathCompletionSettings::setGroupName( const QString& name ) m_groupName = name; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellPathCompletionSettings::setReferenceDepth( std::optional depth ) +{ + m_referenceDepth = depth; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellPathCompletionSettings::setWellType( WellType wellType ) +{ + m_preferredFluidPhase = wellType; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellPathCompletionSettings::setAllowWellCrossFlow( bool allowCrossFlow ) +{ + m_allowWellCrossFlow = allowCrossFlow; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.h b/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.h index d0830b0f2d..9603691e93 100644 --- a/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.h +++ b/ApplicationLibCode/ProjectDataModel/Completions/RimWellPathCompletionSettings.h @@ -71,9 +71,15 @@ public: RimWellPathCompletionSettings( const RimWellPathCompletionSettings& rhs ); RimWellPathCompletionSettings& operator=( const RimWellPathCompletionSettings& rhs ); - QString groupName() const; - QString wellName() const; - void setGroupName( const QString& name ); + QString groupName() const; + QString wellName() const; + WellType wellType() const; + bool allowWellCrossFlow() const; + + void setGroupName( const QString& name ); + void setReferenceDepth( std::optional depth ); + void setWellType( WellType wellType ); + void setAllowWellCrossFlow( bool allowCrossFlow ); void setWellNameForExport( const QString& name ); void updateWellPathNameHasChanged( const QString& newWellPathName, const QString& previousWellPathName ); diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.cpp index f1ba0f7bca..68afc70a42 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.cpp +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.cpp @@ -32,6 +32,7 @@ #include "RigSimulationInputTool.h" #include "RimEclipseCase.h" +#include "RimWellEventWellSpec.h" #include "RimWellPath.h" #include "RimWellPathCompletionSettings.h" @@ -68,7 +69,8 @@ namespace RimKeywordFactory //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -Opm::DeckKeyword welspecsKeyword( const std::string wellGrpName, RimEclipseCase* eCase, RimWellPath* wellPath ) +Opm::DeckKeyword + welspecsKeyword( const std::string wellGrpName, RimEclipseCase* eCase, RimWellPath* wellPath, const RimWellSpecData* wellSpecData ) { if ( eCase == nullptr || wellPath == nullptr || wellPath->completionSettings() == nullptr || eCase->eclipseCaseData() == nullptr ) { @@ -85,15 +87,24 @@ Opm::DeckKeyword welspecsKeyword( const std::string wellGrpName, RimEclipseCase* std::vector items; items.push_back( RifOpmDeckTools::item( W::WELL::itemName, wellName ) ); - items.push_back( RifOpmDeckTools::item( W::GROUP::itemName, wellGrpName ) ); + const std::string groupName = wellSpecData ? ( wellSpecData->groupName.isEmpty() ? "1*" : wellSpecData->groupName.toStdString() ) + : wellGrpName; + const auto referenceDepth = wellSpecData ? wellSpecData->referenceDepth : compSettings->referenceDepth(); + const std::string phase = wellSpecData + ? caf::AppEnum( wellSpecData->wellType ).text().toStdString() + : compSettings->wellTypeNameForExport().toStdString(); + + items.push_back( RifOpmDeckTools::item( W::GROUP::itemName, groupName ) ); items.push_back( RifOpmDeckTools::item( W::HEAD_I::itemName, ijPos.second.x() + 1 ) ); items.push_back( RifOpmDeckTools::item( W::HEAD_J::itemName, ijPos.second.y() + 1 ) ); - items.push_back( RifOpmDeckTools::optionalItem( W::REF_DEPTH::itemName, compSettings->referenceDepth() ) ); - items.push_back( RifOpmDeckTools::item( W::PHASE::itemName, compSettings->wellTypeNameForExport().toStdString() ) ); + items.push_back( RifOpmDeckTools::optionalItem( W::REF_DEPTH::itemName, referenceDepth ) ); + items.push_back( RifOpmDeckTools::item( W::PHASE::itemName, phase ) ); items.push_back( RifOpmDeckTools::optionalItem( W::D_RADIUS::itemName, compSettings->drainageRadius() ) ); items.push_back( RifOpmDeckTools::item( W::INFLOW_EQ::itemName, compSettings->gasInflowEquationForExport().toStdString() ) ); items.push_back( RifOpmDeckTools::item( W::AUTO_SHUTIN::itemName, compSettings->automaticWellShutInForExport().toStdString() ) ); - items.push_back( RifOpmDeckTools::item( W::CROSSFLOW::itemName, compSettings->allowWellCrossFlowForExport().toStdString() ) ); + const std::string crossFlow = wellSpecData ? ( wellSpecData->allowCrossFlow ? "YES" : "NO" ) + : compSettings->allowWellCrossFlowForExport().toStdString(); + items.push_back( RifOpmDeckTools::item( W::CROSSFLOW::itemName, crossFlow ) ); items.push_back( RifOpmDeckTools::item( W::P_TABLE::itemName, compSettings->wellBoreFluidPVT() ) ); items.push_back( RifOpmDeckTools::item( W::DENSITY_CALC::itemName, compSettings->hydrostaticDensityForExport().toStdString() ) ); items.push_back( RifOpmDeckTools::item( W::FIP_REGION::itemName, compSettings->fluidInPlaceRegion() ) ); diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.h index 554f99470f..250afb9ae2 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.h +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimKeywordFactory.h @@ -36,6 +36,7 @@ class RigMainGrid; class RigMswTableData; class RimEclipseCase; class RimWellPath; +struct RimWellSpecData; namespace Opm { @@ -55,7 +56,8 @@ struct BorderCellFace; namespace RimKeywordFactory { -Opm::DeckKeyword welspecsKeyword( const std::string wellGrpName, RimEclipseCase* eCase, RimWellPath* wellPath ); +Opm::DeckKeyword + welspecsKeyword( const std::string wellGrpName, RimEclipseCase* eCase, RimWellPath* wellPath, const RimWellSpecData* wellSpecData = nullptr ); Opm::DeckKeyword compordKeyword( const std::string& wellName ); Opm::DeckKeyword compdatKeyword( const std::vector& compdata, const std::string wellName ); Opm::DeckKeyword wpimultKeyword( const std::vector& compdata, const std::string wellName ); diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake index e79fa3762e..a925956187 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake @@ -5,6 +5,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimWellEventTubing.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellEventState.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellEventType.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimWellEventWellSpec.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellEventControl.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellEventKeyword.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellEventKeywordItem.cpp diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp index f14d1c0a4e..83d3c029f6 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp @@ -35,6 +35,7 @@ void AppEnum::setUp() addItem( RimWellEvent::EventType::TUBING, "TUBING", "Tubing" ); addItem( RimWellEvent::EventType::WSTATE, "WSTATE", "Well State" ); addItem( RimWellEvent::EventType::WTYPE, "WTYPE", "Well Type" ); + addItem( RimWellEvent::EventType::WELLSPEC, "WELLSPEC", "Well Specification" ); addItem( RimWellEvent::EventType::WCONTROL, "WCONTROL", "Well Control" ); addItem( RimWellEvent::EventType::KEYWORD, "KEYWORD", "Well Keyword" ); addItem( RimWellEvent::EventType::SCHEDULE_KEYWORD, "SCHEDULE_KEYWORD", "Schedule Keyword" ); diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h index ed01031504..d3a6f47898 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h @@ -45,6 +45,7 @@ public: TUBING, WSTATE, WTYPE, + WELLSPEC, WCONTROL, KEYWORD, SCHEDULE_KEYWORD diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp index 8b11326ed5..35fa0a2bf6 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp @@ -27,6 +27,7 @@ #include "RimWellEventTubing.h" #include "RimWellEventType.h" #include "RimWellEventValve.h" +#include "RimWellEventWellSpec.h" #include "RimDiameterRoughnessInterval.h" #include "RimDiameterRoughnessIntervalCollection.h" @@ -281,6 +282,26 @@ RimWellEventType* RimWellEventTimeline::addTypeEvent( RimWellPath* wellPath, con return event; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellEventWellSpec* RimWellEventTimeline::addWellSpecEvent( RimWellPath* wellPath, const QDateTime& date ) +{ + auto* event = new RimWellEventWellSpec(); + event->setWellPath( wellPath ); + event->setEventDate( date ); + + if ( wellPath && wellPath->completionSettings() ) + { + auto* settings = wellPath->completionSettings(); + event->setBaselineData( { settings->groupName(), settings->allowWellCrossFlow(), settings->referenceDepth(), settings->wellType() } ); + } + + m_events.push_back( event ); + updateEditorsAfterEventChange(); + return event; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -453,6 +474,14 @@ bool RimWellEventTimeline::applyEvent( RimWellPathCollection* wellPathCollection } break; } + case RimWellEvent::EventType::WELLSPEC: + { + if ( auto* wellSpecEvent = dynamic_cast( event ) ) + { + return applyWellSpecEvent( *wellSpecEvent, *wellPath ); + } + break; + } case RimWellEvent::EventType::WSTATE: case RimWellEvent::EventType::WCONTROL: case RimWellEvent::EventType::WTYPE: @@ -464,6 +493,23 @@ bool RimWellEventTimeline::applyEvent( RimWellPathCollection* wellPathCollection return false; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimWellEventTimeline::applyWellSpecEvent( const RimWellEventWellSpec& event, RimWellPath& wellPath ) +{ + auto* settings = wellPath.completionSettings(); + if ( !settings ) return false; + + const auto data = event.wellSpecData(); + settings->setGroupName( data.groupName ); + settings->setAllowWellCrossFlow( data.allowCrossFlow ); + settings->setReferenceDepth( data.referenceDepth ); + settings->setWellType( data.wellType ); + settings->updateConnectedEditors(); + return true; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h index 1969825f20..dabbb94c85 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h @@ -33,6 +33,7 @@ class RimWellEventValve; class RimWellEventTubing; class RimWellEventState; class RimWellEventType; +class RimWellEventWellSpec; class RimWellEventControl; class RimWellEventKeyword; class RimKeywordEvent; @@ -71,14 +72,15 @@ public: std::vector getWellPathsWithEventsUpToDate( const QDateTime& date ) 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 ); - RimWellEventTubing* addTubingEvent( RimWellPath* wellPath, const QDateTime& date ); - RimWellEventState* addStateEvent( RimWellPath* wellPath, const QDateTime& date ); - RimWellEventType* addTypeEvent( RimWellPath* wellPath, const QDateTime& date ); - RimWellEventControl* addControlEvent( RimWellPath* wellPath, const QDateTime& date ); - RimWellEventKeyword* addWellKeywordEvent( RimWellPath* wellPath, const QDateTime& date, const QString& keywordName ); - RimKeywordEvent* addKeywordEvent( const QDateTime& date, const QString& keywordName ); + RimWellEventPerf* addPerforationEvent( RimWellPath* wellPath, const QDateTime& date ); + RimWellEventValve* addValveEvent( RimWellPath* wellPath, const QDateTime& date ); + RimWellEventTubing* addTubingEvent( RimWellPath* wellPath, const QDateTime& date ); + RimWellEventState* addStateEvent( RimWellPath* wellPath, const QDateTime& date ); + RimWellEventType* addTypeEvent( RimWellPath* wellPath, const QDateTime& date ); + RimWellEventWellSpec* addWellSpecEvent( RimWellPath* wellPath, const QDateTime& date ); + RimWellEventControl* addControlEvent( RimWellPath* wellPath, const QDateTime& date ); + RimWellEventKeyword* addWellKeywordEvent( RimWellPath* wellPath, const QDateTime& date, const QString& keywordName ); + RimKeywordEvent* addKeywordEvent( const QDateTime& date, const QString& keywordName ); // Generic add event method void addEvent( RimWellEvent* event ); @@ -110,6 +112,7 @@ private: bool applyTubingEvent( const RimWellEventTubing& event, RimWellPath& wellPath ); bool applyPerfEvent( const RimWellEventPerf& event, RimWellPath& wellPath ); bool applyValveEvent( RimWellEventValve& event, RimWellPath& wellPath ); + bool applyWellSpecEvent( const RimWellEventWellSpec& event, RimWellPath& wellPath ); std::vector filteredAndSortedEventsForUi() const; void updateEditorsAfterEventChange(); diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventWellSpec.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventWellSpec.cpp new file mode 100644 index 0000000000..d8c2ed1f9a --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventWellSpec.cpp @@ -0,0 +1,134 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimWellEventWellSpec.h" + +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" +#include "cafPdmUiTreeOrdering.h" + +CAF_PDM_SOURCE_INIT( RimWellEventWellSpec, "WellEventWellSpec" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellEventWellSpec::RimWellEventWellSpec() +{ + CAF_PDM_InitScriptableObject( "Well Specification Event", "", "", "WellEventWellSpec" ); + + CAF_PDM_InitScriptableField( &m_groupName, "GroupName", QString(), "Group Name" ); + CAF_PDM_InitScriptableField( &m_allowCrossFlow, "AllowCrossFlow", true, "Allow Cross-Flow" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_referenceDepth, "ReferenceDepth", "Reference Depth" ); + CAF_PDM_InitScriptableField( &m_wellType, + "WellType", + caf::AppEnum( RimWellPathCompletionSettings::OIL ), + "Preferred Fluid Phase" ); + + CAF_PDM_InitField( &m_baselineGroupName, "BaselineGroupName", QString(), "Baseline Group Name" ); + CAF_PDM_InitField( &m_baselineAllowCrossFlow, "BaselineAllowCrossFlow", true, "Baseline Allow Cross-Flow" ); + CAF_PDM_InitFieldNoDefault( &m_baselineReferenceDepth, "BaselineReferenceDepth", "Baseline Reference Depth" ); + CAF_PDM_InitField( &m_baselineWellType, + "BaselineWellType", + caf::AppEnum( RimWellPathCompletionSettings::OIL ), + "Baseline Preferred Fluid Phase" ); + + setDeletable( true ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellEventWellSpec::~RimWellEventWellSpec() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellSpecData RimWellEventWellSpec::wellSpecData() const +{ + return { m_groupName(), m_allowCrossFlow(), m_referenceDepth(), m_wellType() }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellSpecData RimWellEventWellSpec::baselineData() const +{ + return { m_baselineGroupName(), m_baselineAllowCrossFlow(), m_baselineReferenceDepth(), m_baselineWellType() }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventWellSpec::setWellSpecData( const RimWellSpecData& data ) +{ + m_groupName = data.groupName; + m_allowCrossFlow = data.allowCrossFlow; + m_referenceDepth = data.referenceDepth; + m_wellType = data.wellType; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventWellSpec::setBaselineData( const RimWellSpecData& data ) +{ + m_baselineGroupName = data.groupName; + m_baselineAllowCrossFlow = data.allowCrossFlow; + m_baselineReferenceDepth = data.referenceDepth; + m_baselineWellType = data.wellType; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellEvent::EventType RimWellEventWellSpec::eventType() const +{ + return EventType::WELLSPEC; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimWellEventWellSpec::generateScheduleKeyword( const QString& wellName ) const +{ + return QString( "-- %1 WELLSPEC\n" ).arg( wellName ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventWellSpec::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + RimWellEvent::defineUiOrdering( uiConfigName, uiOrdering ); + uiOrdering.add( &m_groupName ); + uiOrdering.add( &m_allowCrossFlow ); + uiOrdering.add( &m_referenceDepth ); + uiOrdering.add( &m_wellType ); + uiOrdering.skipRemainingFields(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventWellSpec::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName ) +{ + setUiName( QString( "WELLSPEC %1" ).arg( m_eventDate().toString( "yyyy-MM-dd" ) ) ); + uiTreeOrdering.skipRemainingChildren(); +} diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventWellSpec.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventWellSpec.h new file mode 100644 index 0000000000..f84e99a10b --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventWellSpec.h @@ -0,0 +1,71 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "RimWellEvent.h" +#include "RimWellPathCompletionSettings.h" + +#include "cafPdmField.h" + +#include + +struct RimWellSpecData +{ + QString groupName; + bool allowCrossFlow = true; + std::optional referenceDepth; + RimWellPathCompletionSettings::WellType wellType = RimWellPathCompletionSettings::OIL; +}; + +//================================================================================================== +/// +/// Dated snapshot of the WELSPECS-related completion export settings for a well. +/// +//================================================================================================== +class RimWellEventWellSpec : public RimWellEvent +{ + CAF_PDM_HEADER_INIT; + +public: + RimWellEventWellSpec(); + ~RimWellEventWellSpec() override; + + RimWellSpecData wellSpecData() const; + RimWellSpecData baselineData() const; + + void setWellSpecData( const RimWellSpecData& data ); + void setBaselineData( const RimWellSpecData& data ); + + EventType eventType() const override; + QString generateScheduleKeyword( const QString& wellName ) const override; + +protected: + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName = "" ) override; + +private: + caf::PdmField m_groupName; + caf::PdmField m_allowCrossFlow; + caf::PdmField> m_referenceDepth; + caf::PdmField> m_wellType; + caf::PdmField m_baselineGroupName; + caf::PdmField m_baselineAllowCrossFlow; + caf::PdmField> m_baselineReferenceDepth; + caf::PdmField> m_baselineWellType; +}; diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index c18217c0cf..7e7dd0a359 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -29,7 +29,9 @@ #include "RimWellEventTimeline.h" #include "RimWellEventTubing.h" #include "RimWellEventValve.h" +#include "RimWellEventWellSpec.h" #include "RimWellPath.h" +#include "RimWellPathCompletionSettings.h" #include "RimcDataContainerString.h" #include "cafAppEnum.h" @@ -362,6 +364,66 @@ QString RimcWellEventTimeline_addTubingEvent::classKeywordReturnedType() const return RimWellEventTubing::classKeywordStatic(); } +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellEventTimeline, RimcWellEventTimeline_addWellSpecEvent, "AddWellspecEvent" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcWellEventTimeline_addWellSpecEvent::RimcWellEventTimeline_addWellSpecEvent( caf::PdmObjectHandle* self ) + : caf::PdmObjectCreationMethod( self ) +{ + CAF_PDM_InitObject( "Add Well Specification Event", "", "", "Add a WELLSPEC event to the timeline" ); + + CAF_PDM_InitScriptableField( &m_eventDate, "EventDate", QString( "2024-01-01" ), "", "", "", "Event Date (YYYY-MM-DD)" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_wellPath, "WellPath", "", "", "", "Well Path" ); + CAF_PDM_InitScriptableField( &m_groupName, "GroupName", QString(), "", "", "", "Group Name" ); + CAF_PDM_InitScriptableField( &m_allowCrossFlow, "AllowCrossFlow", true, "", "", "", "Allow Well Cross-Flow" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_referenceDepth, "ReferenceDepth", "", "", "", "Reference Depth" ); + CAF_PDM_InitScriptableField( &m_wellType, + "WellType", + caf::AppEnum( RimWellPathCompletionSettings::OIL ), + "", + "", + "", + "Preferred Fluid Phase" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcWellEventTimeline_addWellSpecEvent::execute() +{ + auto timeline = self(); + if ( !m_wellPath() ) return std::unexpected( QString( "Well path is required" ) ); + + QDateTime date = QDateTime::fromString( m_eventDate(), Qt::ISODate ); + if ( !date.isValid() ) + { + return std::unexpected( QString( "Invalid date format: %1. Expected YYYY-MM-DD" ).arg( m_eventDate() ) ); + } + + for ( const auto* existing : timeline->events() ) + { + if ( existing->eventType() == RimWellEvent::EventType::WELLSPEC && existing->wellPath() == m_wellPath() && existing->eventDate() == date ) + { + return std::unexpected( + QString( "A WELLSPEC event already exists for well '%1' at %2" ).arg( m_wellPath()->name(), m_eventDate() ) ); + } + } + + auto* event = timeline->addWellSpecEvent( m_wellPath(), date ); + event->setWellSpecData( { m_groupName(), m_allowCrossFlow(), m_referenceDepth(), m_wellType() } ); + return event; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimcWellEventTimeline_addWellSpecEvent::classKeywordReturnedType() const +{ + return RimWellEventWellSpec::classKeywordStatic(); +} + CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellEventTimeline, RimcWellEventTimeline_addWellKeywordEvent, "AddWellKeywordEventInternal" ); //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h index fbeb3e4b49..eed8a6fb46 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h @@ -24,6 +24,7 @@ #include "RimWellEventState.h" #include "RimWellEventTimeline.h" #include "RimWellEventValve.h" +#include "RimWellEventWellSpec.h" #include "cafAppEnum.h" #include "cafPdmField.h" @@ -154,6 +155,28 @@ private: caf::PdmField m_roughness; }; +//================================================================================================== +/// +//================================================================================================== +class RimcWellEventTimeline_addWellSpecEvent : public caf::PdmObjectCreationMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcWellEventTimeline_addWellSpecEvent( caf::PdmObjectHandle* self ); + + std::expected execute() override; + QString classKeywordReturnedType() const override; + +private: + caf::PdmField m_eventDate; + caf::PdmPtrField m_wellPath; + caf::PdmField m_groupName; + caf::PdmField m_allowCrossFlow; + caf::PdmField> m_referenceDepth; + caf::PdmField> m_wellType; +}; + //================================================================================================== /// //================================================================================================== diff --git a/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py b/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py index 55b9b2c240..f904ad241f 100644 --- a/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py +++ b/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_orion.py @@ -11,19 +11,21 @@ applied in one go with rips.orion_events.apply_orion_document(). It demonstrates the full event coverage of the format: 1. TUBING, PERFORATION (incl. a time-of-day date), VALVE and STATE completion events on a well -2. A FILTER declaration (qualified result name) referenced by a perforation, +2. Partial WELLSPEC updates that cumulatively change completion export settings + and generate dated WELSPECS records +3. A FILTER declaration (qualified result name) referenced by a perforation, materialized as a case-level combined data filter -3. COMMENT attributes preserved on timeline events and emitted before their +4. COMMENT attributes preserved on timeline events and emitted before their generated schedule keywords -4. Same-owner/type/date WCONHIST lines merged into one event, with later +5. Same-owner/type/date WCONHIST lines merged into one event, with later attributes extending or overriding earlier attributes -5. Well keyword events: WCONHIST and WELTARG (with attribute translation) and +6. Well keyword events: WCONHIST and WELTARG (with attribute translation) and WRFTPLT (generic Eclipse well keyword pass-through) -6. A GROUP-level MEMBER event expanded to one GRUPTREE record per member -7. SCHEDULE-level keyword events not tied to a well: RPTRST, GRUPTREE, TUNING -8. REPORT dates, passed to generate_schedule_text(additional_dates=...) so +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. REPORT dates, passed to generate_schedule_text(additional_dates=...) so they appear as bare DATES keywords (summary-report triggers) -9. Schedule metadata, COMPORD generation and aligned-column output +10. Schedule metadata, COMPORD generation and aligned-column output The ORIONEVENTS text is built inline with the name of the first well path in the project (like well_event_schedule.py, which uses wells[0]), so the example @@ -57,6 +59,11 @@ DURATION RAMP = 31 DAYS WELL W1 = "{well_name}" WELL W1 + # WELLSPEC updates completion export settings and emits WELSPECS. Attributes + # are optional: the second event inherits GROUP from the first event. + @2024-01-05 WELLSPEC GROUP="ORION_GROUP" CROSSFLOW=True REFDEPTH=1002 PHASE=WATER + @2024-04-15 WELLSPEC CROSSFLOW=False REFDEPTH=1000 PHASE=OIL + # COMMENT is stored on the event and safely emitted as a schedule comment. @STARTUP TUBING MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 ROUGHNESS=1.0e-5 COMMENT="Install production tubing" @@ -148,7 +155,14 @@ def main(): # Apply events up to a date to materialize completions timeline.set_timestamp(timestamp="2024-12-24") - print("\n4. Verifying created completions...") + print("\n4. Verifying created completions and WELLSPEC settings...") + completion_settings = well_path.completion_settings() + print(" Completion export settings after the latest WELLSPEC:") + print(f" Group: {completion_settings.group_name_for_export}") + print(f" Cross-flow: {completion_settings.allow_well_cross_flow}") + print(f" Ref. depth: {completion_settings.reference_depth_for_export}") + print(f" Phase: {completion_settings.well_type_for_export}") + perforations = well_path.completions().perforations().perforations() print(f" Perforations created: {len(perforations)}") for perf in perforations: diff --git a/GrpcInterface/Python/rips/orion_events.py b/GrpcInterface/Python/rips/orion_events.py index bd83e85a10..3641083c1e 100644 --- a/GrpcInterface/Python/rips/orion_events.py +++ b/GrpcInterface/Python/rips/orion_events.py @@ -95,11 +95,18 @@ Notes on the grammar: attribute values, e.g. ``FILTER="SOIL > 0.8 AND PERMX > 200"``. * Every attribute is ``KEY=VALUE``; bare positional tokens are rejected. * Event types inside a WELL block are either the built-in completion events - ``PERFORATION``, ``TUBING``, ``VALVE`` and ``STATE``, or any Eclipse well - keyword (``WCONHIST``, ``WELTARG``, ``WRFTPLT``, ``WCONPROD``, ...), which + ``PERFORATION``, ``TUBING``, ``VALVE``, ``STATE`` and ``WELLSPEC``, or any + Eclipse well keyword (``WCONHIST``, ``WELTARG``, ``WRFTPLT``, ``WCONPROD``, + ...), which is passed through generically with the well name injected as WELL. Event - types inside a GROUP block are Eclipse group keywords with the group name - injected as GROUP. Event types inside a SCHEDULE block are Eclipse schedule + ``WELLSPEC`` accepts partial updates to ``GROUP``, ``CROSSFLOW``, ``REFDEPTH`` + and ``PHASE`` (OIL/GAS/WATER/LIQUID). Omitted values inherit the previous + WELLSPEC state, initially using the well's completion export settings. There + may be multiple WELLSPEC events for a well, but not at the same timestamp. + Each emits a WELSPECS record with the cumulative state. Event values are + materialized back onto completion settings by ``timeline.set_timestamp()``. + Event types inside a GROUP block are Eclipse group keywords with the group + name injected as GROUP. Event types inside a SCHEDULE block are Eclipse schedule keywords passed through as-is. An event type that closely resembles a built-in is treated as a typo per the ``on_unknown_event`` policy instead of being passed through. @@ -250,6 +257,16 @@ class AttrValue: quoted: bool +@dataclass +class WellSpecState: + """Fully resolved cumulative state for one WELLSPEC event.""" + + group: str + crossflow: bool + refdepth: Optional[float] + phase: str + + @dataclass class OrionEvent: """One event line in an enclosing WELL, GROUP or SCHEDULE block.""" @@ -259,6 +276,7 @@ class OrionEvent: attributes: Dict[str, AttrValue] loc: SourceLoc filter: Optional[EventFilter] = None + well_spec: Optional[WellSpecState] = None @dataclass @@ -417,6 +435,7 @@ def parse_orion_events(text: str) -> OrionDocument: raise OrionParseError("Empty file: missing 'ORIONEVENTS' header") errors.extend(_restart_validation_issues(wells, groups, schedule_events)) + errors.extend(_wellspec_validation_issues(wells)) if errors: raise OrionParseError(errors=errors) @@ -465,6 +484,29 @@ def _restart_validation_issues( return issues +def _wellspec_validation_issues(wells: List[WellBlock]) -> List[ParseIssue]: + """Reject multiple WELLSPEC events for one well at the same timestamp.""" + seen: Dict[Tuple[str, Union[datetime.date, datetime.datetime]], OrionEvent] = {} + issues: List[ParseIssue] = [] + for well in wells: + for event in well.events: + if event.event_type.upper() != "WELLSPEC": + continue + key = (well.well_name, event.event_date) + previous = seen.get(key) + if previous is not None: + issues.append( + ParseIssue( + f"WELLSPEC already defined for well '{well.well_name}' " + f"at this date (line {previous.loc.line})", + event.loc, + ) + ) + else: + seen[key] = event + return issues + + def _check_version(version: str, loc: SourceLoc) -> None: major = version.split(".")[0] if major == "2": @@ -957,6 +999,8 @@ _VALVE_KNOWN = {"MD", "TYPE", "STATE", "CV", "AREA", "COMMENT"} | { } _STATE_REQUIRED = ("STATE",) _STATE_KNOWN = {"STATE", "COMMENT"} +_WELLSPEC_KNOWN = {"GROUP", "CROSSFLOW", "REFDEPTH", "PHASE", "COMMENT"} +_WELLSPEC_PHASES = {"OIL", "GAS", "WATER", "LIQUID"} _COMPLETION_IGNORED = {"FILTER"} _PERF_IGNORED = _COMPLETION_IGNORED # backwards-compatible alias @@ -1061,6 +1105,80 @@ def coalesce_orion_document(document: OrionDocument) -> OrionDocument: return result +def _enum_text(value: Any) -> str: + """Return the serialized text of a generated enum or plain string.""" + return str(getattr(value, "value", value)).upper() + + +def _prepare_wellspec_events( + events: List[OrionEvent], completion_settings: Any, report: ApplyReport +) -> None: + """Validate WELLSPEC attributes and resolve partial updates chronologically.""" + state = WellSpecState( + group=str(completion_settings.group_name_for_export), + crossflow=bool(completion_settings.allow_well_cross_flow), + refdepth=completion_settings.reference_depth_for_export, + phase=_enum_text(completion_settings.well_type_for_export), + ) + + wellspecs = sorted( + (event for event in events if event.event_type.upper() == "WELLSPEC"), + key=lambda event: event.event_date, + ) + for event in wellspecs: + attrs = event.attributes + unknown = set(attrs) - _WELLSPEC_KNOWN + if unknown: + report.errors.append( + f"Line {event.loc.line}: unknown WELLSPEC attribute(s): " + f"{', '.join(sorted(unknown))}" + ) + report.events_skipped += 1 + continue + if not (set(attrs) - {"COMMENT"}): + report.errors.append( + f"Line {event.loc.line}: WELLSPEC needs at least one setting attribute" + ) + report.events_skipped += 1 + continue + + next_state = copy.copy(state) + errors: List[str] = [] + if "GROUP" in attrs: + value = attrs["GROUP"].value + if not isinstance(value, str) or not value: + errors.append("GROUP must be a non-empty string") + else: + next_state.group = value + if "CROSSFLOW" in attrs: + value = attrs["CROSSFLOW"].value + if not isinstance(value, bool): + errors.append("CROSSFLOW must be True or False") + else: + next_state.crossflow = value + if "REFDEPTH" in attrs: + value = attrs["REFDEPTH"].value + if isinstance(value, bool) or not isinstance(value, (int, float)): + errors.append("REFDEPTH must be numeric") + else: + next_state.refdepth = float(value) + if "PHASE" in attrs: + value = attrs["PHASE"].value + phase = value.upper() if isinstance(value, str) else "" + if phase not in _WELLSPEC_PHASES: + errors.append("PHASE must be OIL, GAS, WATER, or LIQUID") + else: + next_state.phase = phase + + if errors: + report.errors.extend(f"Line {event.loc.line}: {error}" for error in errors) + report.events_skipped += 1 + continue + + state = next_state + event.well_spec = copy.copy(state) + + def apply_orion_document( document: OrionDocument, timeline: Any, @@ -1114,6 +1232,8 @@ def apply_orion_document( report.events_skipped += len(well.events) continue + _prepare_wellspec_events(well.events, well_path.completion_settings(), report) + for event in well.events: event_type = event.event_type.upper() dispatch = _EVENT_DISPATCH.get(event_type) @@ -1557,6 +1677,29 @@ def _apply_state( report.events_applied += 1 +def _apply_wellspec( + event: OrionEvent, + well_path: Any, + timeline: Any, + report: ApplyReport, + ctx: Optional[_FilterContext] = None, +) -> None: + if event.well_spec is None: + return + + state = event.well_spec + timeline_event = timeline.add_wellspec_event( + event_date=_iso_event_date(event.event_date), + well_path=well_path, + group_name=state.group, + allow_cross_flow=state.crossflow, + reference_depth=state.refdepth, + well_type=state.phase, + ) + _apply_event_comment(event, timeline_event) + report.events_applied += 1 + + def _apply_keyword( event: OrionEvent, well_path: Any, @@ -1635,13 +1778,20 @@ _EVENT_DISPATCH: Dict[str, _EventDispatch] = { "TUBING": _apply_tubing, "VALVE": _apply_valve, "STATE": _apply_state, + "WELLSPEC": _apply_wellspec, "WCONHIST": _apply_wconhist, "WELTARG": _apply_weltarg, } # Completion event types that require a well and cannot appear in a SCHEDULE # block or be emitted as Eclipse keywords. -_COMPLETION_EVENT_TYPES = ("PERFORATION", "TUBING", "VALVE", "STATE") +_COMPLETION_EVENT_TYPES = ( + "PERFORATION", + "TUBING", + "VALVE", + "STATE", + "WELLSPEC", +) # --------------------------------------------------------------------------- diff --git a/GrpcInterface/Python/rips/tests/test_orion_events.py b/GrpcInterface/Python/rips/tests/test_orion_events.py index 85970ce5fa..63d0cc41b0 100644 --- a/GrpcInterface/Python/rips/tests/test_orion_events.py +++ b/GrpcInterface/Python/rips/tests/test_orion_events.py @@ -55,9 +55,21 @@ WELL "55_33-A-2" # --------------------------------------------------------------------------- +class FakeCompletionSettings: + def __init__(self): + self.group_name_for_export = "FIELD" + self.allow_well_cross_flow = True + self.reference_depth_for_export = None + self.well_type_for_export = "OIL" + + class FakeWellPath: def __init__(self, name): self.name = name + self._completion_settings = FakeCompletionSettings() + + def completion_settings(self): + return self._completion_settings class FakeProject: @@ -157,6 +169,7 @@ class FakeTimeline: self.tubing_calls = [] self.valve_calls = [] self.state_calls = [] + self.wellspec_calls = [] self.schedule_keyword_calls = [] self.created_events = [] @@ -187,6 +200,10 @@ class FakeTimeline: self.state_calls.append(kwargs) return self._new_event() + def add_wellspec_event(self, **kwargs): + self.wellspec_calls.append(kwargs) + return self._new_event() + def add_keyword_event(self, **kwargs): self.schedule_keyword_calls.append(kwargs) return self._new_event() @@ -452,6 +469,25 @@ class TestParsing: with pytest.raises(OrionParseError, match="Malformed GROUP line"): parse_orion_events("ORIONEVENTS 2.0\nGROUP OP\n") + def test_duplicate_wellspec_for_well_and_date_is_rejected(self): + text = ( + 'ORIONEVENTS 2.0\nWELL "W"\n' + " @2024-01-01 WELLSPEC GROUP=A\n" + 'WELL "W"\n' + " @2024-01-01 WELLSPEC PHASE=GAS\n" + ) + with pytest.raises(OrionParseError, match="WELLSPEC already defined.*line 3"): + parse_orion_events(text) + + def test_wellspec_same_date_for_different_wells_is_allowed(self): + document = parse_orion_events( + 'ORIONEVENTS 2.0\nWELL "A"\n' + " @2024-01-01 WELLSPEC GROUP=GA\n" + 'WELL "B"\n' + " @2024-01-01 WELLSPEC GROUP=GB\n" + ) + assert [len(well.events) for well in document.wells] == [1, 1] + def test_boolean_attributes_are_typed_unless_quoted(self): text = ( "ORIONEVENTS 2.0\n" @@ -1019,6 +1055,51 @@ class TestApplying: assert report.events_skipped == 1 assert any("ZZZ" in e for e in report.errors) + def test_wellspec_partial_updates_are_cumulative_by_date(self): + text = ( + 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n' + " @2019-01-01 WELLSPEC CROSSFLOW=False PHASE=gas\n" + " @2018-01-01 WELLSPEC GROUP=my_group REFDEPTH=1002 PHASE=water\n" + ) + timeline, report = self._apply(text) + + assert report.errors == [] + assert report.events_applied == 2 + # Calls retain source order, but snapshots are resolved chronologically. + assert timeline.wellspec_calls[0] == { + "event_date": "2019-01-01", + "well_path": timeline.wellspec_calls[0]["well_path"], + "group_name": "my_group", + "allow_cross_flow": False, + "reference_depth": 1002.0, + "well_type": "GAS", + } + assert timeline.wellspec_calls[1]["group_name"] == "my_group" + assert timeline.wellspec_calls[1]["allow_cross_flow"] is True + assert timeline.wellspec_calls[1]["well_type"] == "WATER" + + @pytest.mark.parametrize( + "attributes,expected_error", + [ + ("CROSSFLOW=YES", "CROSSFLOW must be True or False"), + ("REFDEPTH=deep", "REFDEPTH must be numeric"), + ("PHASE=steam", "PHASE must be OIL, GAS, WATER, or LIQUID"), + ("GROUP=1", "GROUP must be a non-empty string"), + ("UNKNOWN=1", "unknown WELLSPEC attribute"), + ("COMMENT=empty", "needs at least one setting attribute"), + ], + ) + def test_invalid_wellspec_is_reported_and_skipped(self, attributes, expected_error): + text = ( + f'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n @2018-01-01 WELLSPEC {attributes}\n' + ) + timeline, report = self._apply(text) + + assert report.events_applied == 0 + assert report.events_skipped == 1 + assert expected_error in report.errors[0] + assert timeline.wellspec_calls == [] + def test_tubing_mapping(self): text = ( 'ORIONEVENTS 2.0\nWELL "55_33-A-1"\n' @@ -1482,6 +1563,55 @@ class TestOrionEventsIntegration: assert "'WELL_A' 'PRODUCERS'" in normalized_block assert "'WELL_B' 'PRODUCERS'" in normalized_block + def test_wellspec_updates_settings_and_generates_cumulative_welspecs( + self, project_with_case_and_wells + ): + project, case, timeline = project_with_case_and_wells + well = next(wp for wp in project.well_paths() if "A" in wp.name) + document = parse_orion_events( + "ORIONEVENTS 2.0\n" + f'WELL "{well.name}"\n' + " @2018-01-01 WELLSPEC GROUP=my_group REFDEPTH=1002 PHASE=water\n" + " @2019-01-01 WELLSPEC CROSSFLOW=False REFDEPTH=1000 PHASE=oil\n" + ) + + report = apply_orion_document(document, timeline, project) + assert report.errors == [] + assert report.events_applied == 2 + + schedule = timeline.generate_schedule_text( + eclipse_case=case, first_date_as_comment=False, align_columns=True + ) + assert schedule.count("WELSPECS\n") == 2 + blocks = schedule.split("WELSPECS\n")[1:] + first_record = " ".join(blocks[0].split("\n/\n", 1)[0].split()) + second_record = " ".join(blocks[1].split("\n/\n", 1)[0].split()) + + assert "'my_group'" in first_record + assert "1002" in first_record + assert "'WATER'" in first_record + assert "'YES'" in first_record + assert "1*" not in first_record.split("'my_group'", 1)[1].split("1002", 1)[0] + + assert "'my_group'" in second_record + assert "1000" in second_record + assert "'OIL'" in second_record + assert "'NO'" in second_record + + timeline.set_timestamp(timestamp="2018-06-01") + settings = well.completion_settings() + assert settings.group_name_for_export == "my_group" + assert settings.allow_well_cross_flow is True + assert settings.reference_depth_for_export == 1002 + assert settings.well_type_for_export == "WATER" + + timeline.set_timestamp(timestamp="2019-06-01") + settings = well.completion_settings() + assert settings.group_name_for_export == "my_group" + assert settings.allow_well_cross_flow is False + assert settings.reference_depth_for_export == 1000 + assert settings.well_type_for_export == "OIL" + def test_restart_truncates_generated_schedule(self, project_with_case_and_wells): project, case, timeline = project_with_case_and_wells well = project.well_paths()[0]