mirror of
https://github.com/OPM/ResInsight.git
synced 2026-08-26 21:27:15 -05:00
#14554 Orion Events: Add WELLSPEC event
This commit is contained in:
@@ -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<const RimKeywordEvent*>( event );
|
||||
@@ -353,7 +358,7 @@ std::optional<Opm::DeckKeyword> 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<Opm::DeckKeyword> 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<RimWellEventWellSpec*>( 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<RimWellSpecData> 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;
|
||||
}
|
||||
|
||||
@@ -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<double> depth )
|
||||
{
|
||||
m_referenceDepth = depth;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellPathCompletionSettings::setWellType( WellType wellType )
|
||||
{
|
||||
m_preferredFluidPhase = wellType;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellPathCompletionSettings::setAllowWellCrossFlow( bool allowCrossFlow )
|
||||
{
|
||||
m_allowWellCrossFlow = allowCrossFlow;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -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<double> depth );
|
||||
void setWellType( WellType wellType );
|
||||
void setAllowWellCrossFlow( bool allowCrossFlow );
|
||||
|
||||
void setWellNameForExport( const QString& name );
|
||||
void updateWellPathNameHasChanged( const QString& newWellPathName, const QString& previousWellPathName );
|
||||
|
||||
@@ -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<Opm::DeckItem> 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<RimWellPathCompletionSettings::WellType>( 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() ) );
|
||||
|
||||
@@ -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<RigCompletionData>& compdata, const std::string wellName );
|
||||
Opm::DeckKeyword wpimultKeyword( const std::vector<RigCompletionData>& compdata, const std::string wellName );
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,7 @@ void AppEnum<RimWellEvent::EventType>::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" );
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
TUBING,
|
||||
WSTATE,
|
||||
WTYPE,
|
||||
WELLSPEC,
|
||||
WCONTROL,
|
||||
KEYWORD,
|
||||
SCHEDULE_KEYWORD
|
||||
|
||||
@@ -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<RimWellEventWellSpec*>( 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;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -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<RimWellPath*> 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<RimWellEvent*> filteredAndSortedEventsForUi() const;
|
||||
void updateEditorsAfterEventChange();
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// 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::WellType>( 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::WellType>( 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();
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWellEvent.h"
|
||||
#include "RimWellPathCompletionSettings.h"
|
||||
|
||||
#include "cafPdmField.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
struct RimWellSpecData
|
||||
{
|
||||
QString groupName;
|
||||
bool allowCrossFlow = true;
|
||||
std::optional<double> 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<QString> m_groupName;
|
||||
caf::PdmField<bool> m_allowCrossFlow;
|
||||
caf::PdmField<std::optional<double>> m_referenceDepth;
|
||||
caf::PdmField<caf::AppEnum<RimWellPathCompletionSettings::WellType>> m_wellType;
|
||||
caf::PdmField<QString> m_baselineGroupName;
|
||||
caf::PdmField<bool> m_baselineAllowCrossFlow;
|
||||
caf::PdmField<std::optional<double>> m_baselineReferenceDepth;
|
||||
caf::PdmField<caf::AppEnum<RimWellPathCompletionSettings::WellType>> m_baselineWellType;
|
||||
};
|
||||
@@ -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::WellType>( RimWellPathCompletionSettings::OIL ),
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Preferred Fluid Phase" );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_addWellSpecEvent::execute()
|
||||
{
|
||||
auto timeline = self<RimWellEventTimeline>();
|
||||
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" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -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<double> m_roughness;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
class RimcWellEventTimeline_addWellSpecEvent : public caf::PdmObjectCreationMethod
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RimcWellEventTimeline_addWellSpecEvent( caf::PdmObjectHandle* self );
|
||||
|
||||
std::expected<caf::PdmObjectHandle*, QString> execute() override;
|
||||
QString classKeywordReturnedType() const override;
|
||||
|
||||
private:
|
||||
caf::PdmField<QString> m_eventDate;
|
||||
caf::PdmPtrField<RimWellPath*> m_wellPath;
|
||||
caf::PdmField<QString> m_groupName;
|
||||
caf::PdmField<bool> m_allowCrossFlow;
|
||||
caf::PdmField<std::optional<double>> m_referenceDepth;
|
||||
caf::PdmField<caf::AppEnum<RimWellPathCompletionSettings::WellType>> m_wellType;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user