#14554 Orion Events: Add WELLSPEC event

This commit is contained in:
Kristian Bendiksen
2026-08-20 11:02:55 +02:00
parent 3947e5ee4e
commit db1c1f1f13
17 changed files with 758 additions and 33 deletions
@@ -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:
+155 -5
View File
@@ -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]