mirror of
https://github.com/OPM/ResInsight.git
synced 2026-08-26 21:27:15 -05:00
#14513 Orion Events: Add raw schedule text events
This commit is contained in:
+110
-47
@@ -28,6 +28,7 @@
|
||||
#include "ProjectDataModel/Jobs/RimKeywordFactory.h"
|
||||
#include "RimEclipseCase.h"
|
||||
#include "RimKeywordEvent.h"
|
||||
#include "RimWellEventRawText.h"
|
||||
#include "RimWellEventTimeline.h"
|
||||
#include "RimWellEventWellSpec.h"
|
||||
#include "RimWellPath.h"
|
||||
@@ -47,13 +48,13 @@
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const std::vector<QDateTime>& dates,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool firstDateAsComment,
|
||||
bool alignColumns )
|
||||
std::expected<QString, QString> RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const std::vector<QDateTime>& dates,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool firstDateAsComment,
|
||||
bool alignColumns )
|
||||
{
|
||||
QString result;
|
||||
|
||||
@@ -101,12 +102,10 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
|
||||
{
|
||||
if ( restartDate.has_value() && date < *restartDate ) continue;
|
||||
|
||||
QString dateSection =
|
||||
auto dateSection =
|
||||
generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells, isFirstDate && firstDateAsComment, alignColumns );
|
||||
if ( !dateSection.isEmpty() )
|
||||
{
|
||||
result += dateSection;
|
||||
}
|
||||
if ( !dateSection ) return std::unexpected( dateSection.error() );
|
||||
result += *dateSection;
|
||||
isFirstDate = false;
|
||||
}
|
||||
|
||||
@@ -159,13 +158,13 @@ void RicScheduleDataGenerator::mergeKeyword( std::map<QString, Opm::DeckKeyword>
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const QDateTime& date,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool dateAsComment,
|
||||
bool alignColumns )
|
||||
std::expected<QString, QString> RicScheduleDataGenerator::generateDateSection( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const QDateTime& date,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool dateAsComment,
|
||||
bool alignColumns )
|
||||
{
|
||||
// Keyword priority order for output
|
||||
static const std::vector<QString> keywordOrder = { "WELSPECS",
|
||||
@@ -188,6 +187,33 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
|
||||
|
||||
QString result;
|
||||
|
||||
auto events = timeline.getEventsAtDate( date );
|
||||
|
||||
std::vector<const RimWellEventRawText*> rawTextEvents;
|
||||
for ( const auto* event : events )
|
||||
{
|
||||
if ( event->eventType() != RimWellEvent::EventType::RAW_TEXT ) continue;
|
||||
if ( const auto* rawTextEvent = dynamic_cast<const RimWellEventRawText*>( event ) )
|
||||
{
|
||||
rawTextEvents.push_back( rawTextEvent );
|
||||
}
|
||||
}
|
||||
std::stable_sort( rawTextEvents.begin(),
|
||||
rawTextEvents.end(),
|
||||
[]( const RimWellEventRawText* lhs, const RimWellEventRawText* rhs ) { return lhs->priority() < rhs->priority(); } );
|
||||
|
||||
auto appendRawText = [&]( RimWellEventRawText::Placement placement, const QString& anchorKeyword = QString() )
|
||||
{
|
||||
for ( const auto* rawTextEvent : rawTextEvents )
|
||||
{
|
||||
if ( rawTextEvent->placement() != placement ) continue;
|
||||
if ( !anchorKeyword.isEmpty() && rawTextEvent->anchorKeyword().compare( anchorKeyword, Qt::CaseInsensitive ) != 0 ) continue;
|
||||
|
||||
result += rawTextEvent->text();
|
||||
if ( !result.endsWith( '\n' ) ) result += '\n';
|
||||
}
|
||||
};
|
||||
|
||||
auto serializeKeyword = [&]( const Opm::DeckKeyword& kw )
|
||||
{ return alignColumns ? RimKeywordFactory::deckKeywordToAlignedString( kw ) : RimKeywordFactory::deckKeywordToString( kw ); };
|
||||
|
||||
@@ -212,6 +238,7 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
|
||||
{
|
||||
result += serializeKeyword( RimKeywordFactory::datesKeyword( date ) ) + "\n";
|
||||
}
|
||||
appendRawText( RimWellEventRawText::Placement::AFTER_DATE );
|
||||
|
||||
// Records for each keyword name are accumulated across wells, then serialised once below.
|
||||
std::map<QString, Opm::DeckKeyword> keywordBlocks;
|
||||
@@ -240,7 +267,6 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
|
||||
}
|
||||
|
||||
// Schedule-level keyword events (not tied to a specific well)
|
||||
auto events = timeline.getEventsAtDate( date );
|
||||
for ( auto* event : events )
|
||||
{
|
||||
if ( event->eventType() != RimWellEvent::EventType::SCHEDULE_KEYWORD ) continue;
|
||||
@@ -280,6 +306,10 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
|
||||
const auto* keywordEvent = dynamic_cast<const RimKeywordEvent*>( event );
|
||||
isRelevant = keywordEvent && keywordEvent->keywordName().compare( keywordName, Qt::CaseInsensitive ) == 0;
|
||||
}
|
||||
else if ( event->eventType() == RimWellEvent::EventType::RAW_TEXT )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto eventKeyword = RifEventKeywordFormatter::buildWellEvent( event, event->wellName() );
|
||||
@@ -304,43 +334,76 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin
|
||||
result += "\n";
|
||||
};
|
||||
|
||||
auto appendUnmergedBlocks = [&]( const QString& name )
|
||||
std::set<QString> availableKeywords;
|
||||
for ( const auto& [name, keyword] : keywordBlocks )
|
||||
{
|
||||
auto it = unmergedBlocks.find( name );
|
||||
if ( it == unmergedBlocks.end() ) return;
|
||||
for ( const auto& kw : it->second )
|
||||
{
|
||||
appendKeywordText( kw );
|
||||
}
|
||||
};
|
||||
availableKeywords.insert( name );
|
||||
}
|
||||
for ( const auto& [name, blocks] : unmergedBlocks )
|
||||
{
|
||||
if ( !blocks.empty() ) availableKeywords.insert( name );
|
||||
}
|
||||
|
||||
// Output keywords in priority order
|
||||
std::set<QString> emitted;
|
||||
for ( const auto& kw : keywordOrder )
|
||||
for ( const auto* rawTextEvent : rawTextEvents )
|
||||
{
|
||||
if ( auto it = keywordBlocks.find( kw ); it != keywordBlocks.end() )
|
||||
if ( rawTextEvent->text().isEmpty() )
|
||||
{
|
||||
return std::unexpected( QString( "Raw text event at %1 has no text" ).arg( date.toString( Qt::ISODate ) ) );
|
||||
}
|
||||
|
||||
const bool anchored = rawTextEvent->placement() == RimWellEventRawText::Placement::BEFORE_KEYWORD ||
|
||||
rawTextEvent->placement() == RimWellEventRawText::Placement::AFTER_KEYWORD;
|
||||
if ( anchored && rawTextEvent->anchorKeyword().isEmpty() )
|
||||
{
|
||||
return std::unexpected( QString( "Raw text event at %1 requires an anchor keyword" ).arg( date.toString( Qt::ISODate ) ) );
|
||||
}
|
||||
if ( !anchored && !rawTextEvent->anchorKeyword().isEmpty() )
|
||||
{
|
||||
return std::unexpected(
|
||||
QString( "Raw text event at %1 has an anchor keyword for a placement that does not use one" ).arg( date.toString( Qt::ISODate ) ) );
|
||||
}
|
||||
if ( anchored && !availableKeywords.contains( rawTextEvent->anchorKeyword() ) )
|
||||
{
|
||||
return std::unexpected( QString( "Raw text event at %1 references keyword '%2', which is not emitted on that date" )
|
||||
.arg( date.toString( Qt::ISODate ), rawTextEvent->anchorKeyword() ) );
|
||||
}
|
||||
}
|
||||
|
||||
auto appendKeywordBlocks = [&]( const QString& name )
|
||||
{
|
||||
if ( !availableKeywords.contains( name ) ) return;
|
||||
|
||||
appendRawText( RimWellEventRawText::Placement::BEFORE_KEYWORD, name );
|
||||
if ( auto it = keywordBlocks.find( name ); it != keywordBlocks.end() )
|
||||
{
|
||||
appendKeywordText( it->second );
|
||||
}
|
||||
appendUnmergedBlocks( kw );
|
||||
emitted.insert( kw );
|
||||
}
|
||||
|
||||
// Output remaining keywords not in the priority list
|
||||
for ( const auto& [kw, deckKw] : keywordBlocks )
|
||||
{
|
||||
if ( emitted.contains( kw ) ) continue;
|
||||
appendKeywordText( deckKw );
|
||||
}
|
||||
for ( const auto& [kw, blocks] : unmergedBlocks )
|
||||
{
|
||||
if ( emitted.contains( kw ) ) continue;
|
||||
for ( const auto& deckKw : blocks )
|
||||
if ( auto it = unmergedBlocks.find( name ); it != unmergedBlocks.end() )
|
||||
{
|
||||
appendKeywordText( deckKw );
|
||||
for ( const auto& keyword : it->second )
|
||||
{
|
||||
appendKeywordText( keyword );
|
||||
}
|
||||
}
|
||||
appendRawText( RimWellEventRawText::Placement::AFTER_KEYWORD, name );
|
||||
};
|
||||
|
||||
// Output keywords in priority order.
|
||||
std::set<QString> emitted;
|
||||
for ( const auto& name : keywordOrder )
|
||||
{
|
||||
appendKeywordBlocks( name );
|
||||
emitted.insert( name );
|
||||
}
|
||||
|
||||
// Output remaining keywords alphabetically, as before.
|
||||
for ( const auto& name : availableKeywords )
|
||||
{
|
||||
if ( emitted.contains( name ) ) continue;
|
||||
appendKeywordBlocks( name );
|
||||
}
|
||||
|
||||
appendRawText( RimWellEventRawText::Placement::END_OF_DATE );
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
|
||||
#include <expected>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
@@ -53,13 +54,13 @@ public:
|
||||
// equals the simulation start date. Later dates are always emitted as DATES keywords.
|
||||
// When alignColumns is true, keywords are serialised with a "--"-prefixed column-header comment
|
||||
// and right-aligned, fixed-width columns instead of the compact default form.
|
||||
static QString generateSchedule( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const std::vector<QDateTime>& dates,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool firstDateAsComment = true,
|
||||
bool alignColumns = false );
|
||||
static std::expected<QString, QString> generateSchedule( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const std::vector<QDateTime>& dates,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool firstDateAsComment = true,
|
||||
bool alignColumns = false );
|
||||
|
||||
// Collect all unique dates from all wells' timelines
|
||||
static std::vector<QDateTime> collectAllDates( const RimWellEventTimeline& timeline, const std::vector<RimWellPath*>& wellPaths );
|
||||
@@ -67,13 +68,13 @@ public:
|
||||
private:
|
||||
// Generate schedule section for a single date. When dateAsComment is true, the date is
|
||||
// written as a comment line instead of a DATES keyword.
|
||||
static QString generateDateSection( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const QDateTime& date,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool dateAsComment = false,
|
||||
bool alignColumns = false );
|
||||
static std::expected<QString, QString> generateDateSection( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::vector<RimWellPath*>& wellPaths,
|
||||
const QDateTime& date,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool dateAsComment = false,
|
||||
bool alignColumns = false );
|
||||
|
||||
static std::optional<Opm::DeckKeyword>
|
||||
generateWelspecsForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date );
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellEvent.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellEventPerf.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellEventRawText.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellEventValve.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellEventTubing.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWellEventState.cpp
|
||||
|
||||
@@ -39,6 +39,7 @@ void AppEnum<RimWellEvent::EventType>::setUp()
|
||||
addItem( RimWellEvent::EventType::WCONTROL, "WCONTROL", "Well Control" );
|
||||
addItem( RimWellEvent::EventType::KEYWORD, "KEYWORD", "Well Keyword" );
|
||||
addItem( RimWellEvent::EventType::SCHEDULE_KEYWORD, "SCHEDULE_KEYWORD", "Schedule Keyword" );
|
||||
addItem( RimWellEvent::EventType::RAW_TEXT, "RAW_TEXT", "Raw Text" );
|
||||
setDefault( RimWellEvent::EventType::PERF );
|
||||
}
|
||||
} // namespace caf
|
||||
|
||||
@@ -48,7 +48,8 @@ public:
|
||||
WELLSPEC,
|
||||
WCONTROL,
|
||||
KEYWORD,
|
||||
SCHEDULE_KEYWORD
|
||||
SCHEDULE_KEYWORD,
|
||||
RAW_TEXT
|
||||
};
|
||||
|
||||
RimWellEvent();
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 "RimWellEventRawText.h"
|
||||
|
||||
#include "cafPdmFieldScriptingCapability.h"
|
||||
#include "cafPdmObjectScriptingCapability.h"
|
||||
#include "cafPdmUiOrdering.h"
|
||||
#include "cafPdmUiTextEditor.h"
|
||||
#include "cafPdmUiTreeOrdering.h"
|
||||
|
||||
namespace caf
|
||||
{
|
||||
template <>
|
||||
void AppEnum<RimWellEventRawText::Placement>::setUp()
|
||||
{
|
||||
addItem( RimWellEventRawText::Placement::AFTER_DATE, "AFTER_DATE", "After Date" );
|
||||
addItem( RimWellEventRawText::Placement::BEFORE_KEYWORD, "BEFORE_KEYWORD", "Before Keyword" );
|
||||
addItem( RimWellEventRawText::Placement::AFTER_KEYWORD, "AFTER_KEYWORD", "After Keyword" );
|
||||
addItem( RimWellEventRawText::Placement::END_OF_DATE, "END_OF_DATE", "End of Date" );
|
||||
setDefault( RimWellEventRawText::Placement::AFTER_DATE );
|
||||
}
|
||||
} // namespace caf
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWellEventRawText, "RawTextEvent" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellEventRawText::RimWellEventRawText()
|
||||
{
|
||||
CAF_PDM_InitScriptableObject( "Raw Text Event", "", "", "RawTextEvent" );
|
||||
|
||||
CAF_PDM_InitScriptableField( &m_text, "Text", QString(), "Text" );
|
||||
m_text.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() );
|
||||
CAF_PDM_InitScriptableField( &m_placement, "Placement", Placement::AFTER_DATE, "Placement" );
|
||||
CAF_PDM_InitScriptableField( &m_anchorKeyword, "AnchorKeyword", QString(), "Anchor Keyword" );
|
||||
CAF_PDM_InitScriptableField( &m_priority, "Priority", 0, "Priority" );
|
||||
|
||||
setDeletable( true );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellEventRawText::~RimWellEventRawText()
|
||||
{
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWellEventRawText::text() const
|
||||
{
|
||||
return m_text();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventRawText::setText( const QString& text )
|
||||
{
|
||||
m_text = text;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellEventRawText::Placement RimWellEventRawText::placement() const
|
||||
{
|
||||
return m_placement();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventRawText::setPlacement( Placement placement )
|
||||
{
|
||||
m_placement = placement;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWellEventRawText::anchorKeyword() const
|
||||
{
|
||||
return m_anchorKeyword();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventRawText::setAnchorKeyword( const QString& keyword )
|
||||
{
|
||||
m_anchorKeyword = keyword.toUpper();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
int RimWellEventRawText::priority() const
|
||||
{
|
||||
return m_priority();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventRawText::setPriority( int priority )
|
||||
{
|
||||
m_priority = priority;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellEvent::EventType RimWellEventRawText::eventType() const
|
||||
{
|
||||
return EventType::RAW_TEXT;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWellEventRawText::generateScheduleKeyword( const QString& wellName ) const
|
||||
{
|
||||
return m_text();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventRawText::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
|
||||
{
|
||||
uiOrdering.add( &m_eventDate );
|
||||
uiOrdering.add( &m_placement );
|
||||
uiOrdering.add( &m_anchorKeyword );
|
||||
uiOrdering.add( &m_priority );
|
||||
uiOrdering.add( &m_text );
|
||||
uiOrdering.skipRemainingFields();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventRawText::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName )
|
||||
{
|
||||
setUiName( QString( "Raw Text %1" ).arg( m_eventDate().toString( "yyyy-MM-dd" ) ) );
|
||||
uiTreeOrdering.skipRemainingChildren( true );
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 "cafAppEnum.h"
|
||||
#include "cafPdmField.h"
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
/// Raw schedule text inserted at a specific position in a dated schedule section.
|
||||
///
|
||||
//==================================================================================================
|
||||
class RimWellEventRawText : public RimWellEvent
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
enum class Placement
|
||||
{
|
||||
AFTER_DATE,
|
||||
BEFORE_KEYWORD,
|
||||
AFTER_KEYWORD,
|
||||
END_OF_DATE
|
||||
};
|
||||
|
||||
RimWellEventRawText();
|
||||
~RimWellEventRawText() override;
|
||||
|
||||
QString text() const;
|
||||
void setText( const QString& text );
|
||||
Placement placement() const;
|
||||
void setPlacement( Placement placement );
|
||||
QString anchorKeyword() const;
|
||||
void setAnchorKeyword( const QString& keyword );
|
||||
int priority() const;
|
||||
void setPriority( int priority );
|
||||
|
||||
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_text;
|
||||
caf::PdmField<caf::AppEnum<Placement>> m_placement;
|
||||
caf::PdmField<QString> m_anchorKeyword;
|
||||
caf::PdmField<int> m_priority;
|
||||
};
|
||||
|
||||
namespace caf
|
||||
{
|
||||
template <>
|
||||
void caf::AppEnum<RimWellEventRawText::Placement>::setUp();
|
||||
} // namespace caf
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "RimWellEventControl.h"
|
||||
#include "RimWellEventKeyword.h"
|
||||
#include "RimWellEventPerf.h"
|
||||
#include "RimWellEventRawText.h"
|
||||
#include "RimWellEventState.h"
|
||||
#include "RimWellEventTubing.h"
|
||||
#include "RimWellEventType.h"
|
||||
@@ -343,6 +344,18 @@ RimKeywordEvent* RimWellEventTimeline::addKeywordEvent( const QDateTime& date, c
|
||||
return event;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellEventRawText* RimWellEventTimeline::addRawTextEvent( const QDateTime& date )
|
||||
{
|
||||
auto* event = new RimWellEventRawText();
|
||||
event->setEventDate( date );
|
||||
m_events.push_back( event );
|
||||
updateEditorsAfterEventChange();
|
||||
return event;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -37,6 +37,7 @@ class RimWellEventWellSpec;
|
||||
class RimWellEventControl;
|
||||
class RimWellEventKeyword;
|
||||
class RimKeywordEvent;
|
||||
class RimWellEventRawText;
|
||||
class RimWellPath;
|
||||
class RimWellPathCollection;
|
||||
|
||||
@@ -81,6 +82,7 @@ public:
|
||||
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 );
|
||||
RimWellEventRawText* addRawTextEvent( const QDateTime& date );
|
||||
|
||||
// Generic add event method
|
||||
void addEvent( RimWellEvent* event );
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "RimWellEventControl.h"
|
||||
#include "RimWellEventKeyword.h"
|
||||
#include "RimWellEventPerf.h"
|
||||
#include "RimWellEventRawText.h"
|
||||
#include "RimWellEventState.h"
|
||||
#include "RimWellEventTimeline.h"
|
||||
#include "RimWellEventTubing.h"
|
||||
@@ -634,6 +635,73 @@ QString RimcWellEventTimeline_addKeywordEvent::classKeywordReturnedType() const
|
||||
return RimKeywordEvent::classKeywordStatic();
|
||||
}
|
||||
|
||||
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellEventTimeline, RimcWellEventTimeline_addRawTextEvent, "AddRawTextEventInternal" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimcWellEventTimeline_addRawTextEvent::RimcWellEventTimeline_addRawTextEvent( caf::PdmObjectHandle* self )
|
||||
: caf::PdmObjectCreationMethod( self )
|
||||
{
|
||||
CAF_PDM_InitObject( "Add Raw Text Event", "", "", "Add raw text at a specific position in a dated schedule section" );
|
||||
|
||||
CAF_PDM_InitScriptableField( &m_eventDate, "EventDate", QString( "2024-01-01" ), "", "", "", "Event Date (YYYY-MM-DD)" );
|
||||
CAF_PDM_InitScriptableField( &m_text, "Text", QString(), "", "", "", "Raw schedule text" );
|
||||
CAF_PDM_InitScriptableField( &m_placement,
|
||||
"Placement",
|
||||
RimWellEventRawText::Placement::AFTER_DATE,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Position in the dated schedule section" );
|
||||
CAF_PDM_InitScriptableField( &m_anchorKeyword, "AnchorKeyword", QString(), "", "", "", "Keyword used for before/after placement" );
|
||||
CAF_PDM_InitScriptableField( &m_priority, "Priority", 0, "", "", "", "Ascending order among raw text events at the same position" );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_addRawTextEvent::execute()
|
||||
{
|
||||
auto timeline = self<RimWellEventTimeline>();
|
||||
|
||||
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() ) );
|
||||
}
|
||||
if ( m_text().isEmpty() )
|
||||
{
|
||||
return std::unexpected( QString( "Raw text is required" ) );
|
||||
}
|
||||
|
||||
const bool anchored = m_placement() == RimWellEventRawText::Placement::BEFORE_KEYWORD ||
|
||||
m_placement() == RimWellEventRawText::Placement::AFTER_KEYWORD;
|
||||
if ( anchored && m_anchorKeyword().trimmed().isEmpty() )
|
||||
{
|
||||
return std::unexpected( QString( "Anchor keyword is required for BEFORE_KEYWORD and AFTER_KEYWORD placement" ) );
|
||||
}
|
||||
if ( !anchored && !m_anchorKeyword().trimmed().isEmpty() )
|
||||
{
|
||||
return std::unexpected( QString( "Anchor keyword is only valid for BEFORE_KEYWORD and AFTER_KEYWORD placement" ) );
|
||||
}
|
||||
|
||||
auto* event = timeline->addRawTextEvent( date );
|
||||
event->setText( m_text() );
|
||||
event->setPlacement( m_placement() );
|
||||
event->setAnchorKeyword( m_anchorKeyword().trimmed() );
|
||||
event->setPriority( m_priority() );
|
||||
return event;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimcWellEventTimeline_addRawTextEvent::classKeywordReturnedType() const
|
||||
{
|
||||
return RimWellEventRawText::classKeywordStatic();
|
||||
}
|
||||
|
||||
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellEventTimeline, RimcWellEventTimeline_setTimestamp, "SetTimestamp" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -753,11 +821,6 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_generateSche
|
||||
return std::unexpected( QString( "No events found in timeline" ) );
|
||||
}
|
||||
|
||||
if ( wellPathsWithEvents.empty() )
|
||||
{
|
||||
return std::unexpected( QString( "No well paths with events found" ) );
|
||||
}
|
||||
|
||||
// Merge in user-specified additional dates: each becomes a DATES keyword even when no events
|
||||
// fall on it (e.g. to force a summary report). They are deliberately not filtered by the last
|
||||
// applied timestamp.
|
||||
@@ -779,17 +842,18 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_generateSche
|
||||
std::vector<RimWellPath*> mswWellPaths = m_exportMswForWells.ptrReferencedObjectsByType();
|
||||
std::set<const RimWellPath*> mswWells( mswWellPaths.begin(), mswWellPaths.end() );
|
||||
|
||||
QString scheduleText = RicScheduleDataGenerator::generateSchedule( *timeline,
|
||||
*eclipseCase,
|
||||
wellPathsWithEvents,
|
||||
dates,
|
||||
mswWells,
|
||||
m_firstDateAsComment(),
|
||||
m_alignColumns() );
|
||||
auto scheduleText = RicScheduleDataGenerator::generateSchedule( *timeline,
|
||||
*eclipseCase,
|
||||
wellPathsWithEvents,
|
||||
dates,
|
||||
mswWells,
|
||||
m_firstDateAsComment(),
|
||||
m_alignColumns() );
|
||||
if ( !scheduleText ) return std::unexpected( scheduleText.error() );
|
||||
|
||||
// Return the schedule text in a data container
|
||||
auto* dataObject = new RimcDataContainerString();
|
||||
dataObject->m_stringValues = { scheduleText };
|
||||
dataObject->m_stringValues = { *scheduleText };
|
||||
|
||||
return dataObject;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "RimKeywordEvent.h"
|
||||
#include "RimWellEventControl.h"
|
||||
#include "RimWellEventPerf.h"
|
||||
#include "RimWellEventRawText.h"
|
||||
#include "RimWellEventState.h"
|
||||
#include "RimWellEventTimeline.h"
|
||||
#include "RimWellEventValve.h"
|
||||
@@ -220,6 +221,27 @@ private:
|
||||
caf::PdmField<std::vector<QString>> m_itemValues;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
class RimcWellEventTimeline_addRawTextEvent : public caf::PdmObjectCreationMethod
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RimcWellEventTimeline_addRawTextEvent( caf::PdmObjectHandle* self );
|
||||
|
||||
std::expected<caf::PdmObjectHandle*, QString> execute() override;
|
||||
QString classKeywordReturnedType() const override;
|
||||
|
||||
private:
|
||||
caf::PdmField<QString> m_eventDate;
|
||||
caf::PdmField<QString> m_text;
|
||||
caf::PdmField<caf::AppEnum<RimWellEventRawText::Placement>> m_placement;
|
||||
caf::PdmField<QString> m_anchorKeyword;
|
||||
caf::PdmField<int> m_priority;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
|
||||
@@ -23,7 +23,8 @@ File format grammar, version 2.0 (EBNF-ish)::
|
||||
document = header , { statement } ;
|
||||
header = "ORIONEVENTS" , "2.0" ; (* first meaningful line *)
|
||||
statement = unit_directive | declaration | report_line | well_block_open
|
||||
| group_block_open | schedule_block_open | event_line ;
|
||||
| group_block_open | schedule_block_open | event_line
|
||||
| raw_text_event ;
|
||||
unit_directive = "UNIT" , ( "METRIC" | "FIELD" | "LAB" ) ;
|
||||
report_line = "REPORT" , date_expr ; (* REPORT 2024-06-01 *)
|
||||
|
||||
@@ -42,6 +43,12 @@ File format grammar, version 2.0 (EBNF-ish)::
|
||||
group_block_open = "GROUP" , quoted_string ; (* group keyword events *)
|
||||
schedule_block_open = "SCHEDULE" ; (* well-less keyword events *)
|
||||
event_line = "@" , date_expr , event_type , { attribute } ;
|
||||
raw_text_event = "@" , date_expr , "RAW_TEXT" , raw_text_attributes , newline,
|
||||
{ raw_line , newline } , "END_RAW_TEXT" ;
|
||||
raw_text_attributes = "PLACEMENT=" ,
|
||||
( "AFTER_DATE" | "BEFORE_KEYWORD" |
|
||||
"AFTER_KEYWORD" | "END_OF_DATE" ) ,
|
||||
[ "ANCHOR=" , ident ] , [ "PRIORITY=" , integer ] ;
|
||||
|
||||
date_expr = ( iso_date | iso_datetime | date_ident ) , { sign , term } ;
|
||||
duration_expr = ( integer | dur_ident ) , { sign , term } , [ "DAYS" | "days" ] ;
|
||||
@@ -91,6 +98,13 @@ Notes on the grammar:
|
||||
strings on :attr:`ApplyReport.report_dates`, ready to pass to
|
||||
``WellEventTimeline.generate_schedule_text(additional_dates=...)``. A
|
||||
``REPORT`` line is not tied to any well and does not close an open block.
|
||||
* ``RAW_TEXT`` is valid only inside a ``SCHEDULE`` block. Its body is copied
|
||||
without parsing or formatting through the mandatory standalone
|
||||
``END_RAW_TEXT`` line. ``PLACEMENT`` is ``AFTER_DATE``, ``BEFORE_KEYWORD``,
|
||||
``AFTER_KEYWORD`` or ``END_OF_DATE``. Before/after-keyword placement requires
|
||||
``ANCHOR=<Eclipse keyword>``; the other placements forbid it. ``PRIORITY`` is
|
||||
an optional integer (default 0); lower values are emitted first and source
|
||||
order breaks ties.
|
||||
* Double quotes are used everywhere: well names, filter expressions and
|
||||
attribute values, e.g. ``FILTER="SOIL > 0.8 AND PERMX > 200"``.
|
||||
* Every attribute is ``KEY=VALUE``; bare positional tokens are rejected.
|
||||
@@ -277,6 +291,10 @@ class OrionEvent:
|
||||
loc: SourceLoc
|
||||
filter: Optional[EventFilter] = None
|
||||
well_spec: Optional[WellSpecState] = None
|
||||
raw_text: Optional[str] = None
|
||||
raw_placement: Optional[str] = None
|
||||
raw_anchor: Optional[str] = None
|
||||
raw_priority: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -391,9 +409,13 @@ def parse_orion_events(text: str) -> OrionDocument:
|
||||
# document-level schedule_events list.
|
||||
current_events: Optional[List[OrionEvent]] = None
|
||||
|
||||
for lineno, raw_line in enumerate(text.splitlines(), start=1):
|
||||
loc = SourceLoc(line=lineno, text=raw_line)
|
||||
source_lines = text.splitlines()
|
||||
line_index = 0
|
||||
while line_index < len(source_lines):
|
||||
raw_line = source_lines[line_index]
|
||||
loc = SourceLoc(line=line_index + 1, text=raw_line)
|
||||
line = _strip_comment(raw_line).strip()
|
||||
line_index += 1
|
||||
if not line:
|
||||
continue
|
||||
|
||||
@@ -409,6 +431,31 @@ def parse_orion_events(text: str) -> OrionDocument:
|
||||
_check_version(version, loc)
|
||||
continue
|
||||
|
||||
if _is_raw_text_header(line):
|
||||
end_index = line_index
|
||||
while (
|
||||
end_index < len(source_lines)
|
||||
and source_lines[end_index].strip() != "END_RAW_TEXT"
|
||||
):
|
||||
end_index += 1
|
||||
if end_index == len(source_lines):
|
||||
errors.append(ParseIssue("Unterminated RAW_TEXT block", loc))
|
||||
break
|
||||
|
||||
body_lines = source_lines[line_index:end_index]
|
||||
line_index = end_index + 1
|
||||
try:
|
||||
if current_events is not schedule_events:
|
||||
raise OrionParseError(
|
||||
"RAW_TEXT is only valid in a SCHEDULE block", loc
|
||||
)
|
||||
current_events.append(
|
||||
_parse_raw_text_event(line, body_lines, variables, loc)
|
||||
)
|
||||
except OrionParseError as exc:
|
||||
errors.extend(exc.errors)
|
||||
continue
|
||||
|
||||
try:
|
||||
current_events = _parse_line(
|
||||
line,
|
||||
@@ -864,6 +911,81 @@ def _strip_comment(line: str) -> str:
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def _is_raw_text_header(line: str) -> bool:
|
||||
"""Return whether an event line starts a multiline RAW_TEXT block."""
|
||||
match = _EVENT_RE.match(line)
|
||||
if match is None:
|
||||
return False
|
||||
return match.group("rest").split(None, 1)[0].upper() == "RAW_TEXT"
|
||||
|
||||
|
||||
def _parse_raw_text_event(
|
||||
line: str,
|
||||
body_lines: List[str],
|
||||
variables: Dict[str, OrionValue],
|
||||
loc: SourceLoc,
|
||||
) -> OrionEvent:
|
||||
"""Parse and validate a RAW_TEXT header and attach its unparsed body."""
|
||||
event = _parse_event_line(line, variables, loc)
|
||||
allowed = {"PLACEMENT", "ANCHOR", "PRIORITY"}
|
||||
unknown = set(event.attributes) - allowed
|
||||
if unknown:
|
||||
raise OrionParseError(
|
||||
f"Unknown RAW_TEXT attribute(s): {', '.join(sorted(unknown))}", loc
|
||||
)
|
||||
if "PLACEMENT" not in event.attributes:
|
||||
raise OrionParseError("RAW_TEXT requires PLACEMENT", loc)
|
||||
if not body_lines:
|
||||
raise OrionParseError("RAW_TEXT body must not be empty", loc)
|
||||
|
||||
placement_value = event.attributes["PLACEMENT"].value
|
||||
placement = placement_value.upper() if isinstance(placement_value, str) else ""
|
||||
valid_placements = {
|
||||
"AFTER_DATE",
|
||||
"BEFORE_KEYWORD",
|
||||
"AFTER_KEYWORD",
|
||||
"END_OF_DATE",
|
||||
}
|
||||
if placement not in valid_placements:
|
||||
raise OrionParseError(
|
||||
"RAW_TEXT PLACEMENT must be AFTER_DATE, BEFORE_KEYWORD, "
|
||||
"AFTER_KEYWORD, or END_OF_DATE",
|
||||
loc,
|
||||
)
|
||||
|
||||
anchor: Optional[str] = None
|
||||
if "ANCHOR" in event.attributes:
|
||||
anchor_value = event.attributes["ANCHOR"].value
|
||||
if not isinstance(anchor_value, str) or not anchor_value.strip():
|
||||
raise OrionParseError("RAW_TEXT ANCHOR must be a keyword name", loc)
|
||||
anchor = anchor_value.strip().upper()
|
||||
|
||||
anchored = placement in {"BEFORE_KEYWORD", "AFTER_KEYWORD"}
|
||||
if anchored and anchor is None:
|
||||
raise OrionParseError(
|
||||
"RAW_TEXT ANCHOR is required for BEFORE_KEYWORD and AFTER_KEYWORD",
|
||||
loc,
|
||||
)
|
||||
if not anchored and anchor is not None:
|
||||
raise OrionParseError(
|
||||
"RAW_TEXT ANCHOR is only valid for BEFORE_KEYWORD and AFTER_KEYWORD",
|
||||
loc,
|
||||
)
|
||||
|
||||
priority = 0
|
||||
if "PRIORITY" in event.attributes:
|
||||
priority_value = event.attributes["PRIORITY"].value
|
||||
if isinstance(priority_value, bool) or not isinstance(priority_value, int):
|
||||
raise OrionParseError("RAW_TEXT PRIORITY must be an integer", loc)
|
||||
priority = priority_value
|
||||
|
||||
event.raw_text = "\n".join(body_lines) + "\n"
|
||||
event.raw_placement = placement
|
||||
event.raw_anchor = anchor
|
||||
event.raw_priority = priority
|
||||
return event
|
||||
|
||||
|
||||
def _parse_event_line(
|
||||
line: str, variables: Dict[str, OrionValue], loc: SourceLoc
|
||||
) -> OrionEvent:
|
||||
@@ -1064,6 +1186,10 @@ def coalesce_orion_document(document: OrionDocument) -> OrionDocument:
|
||||
Tuple[Union[datetime.date, datetime.datetime], str], OrionEvent
|
||||
] = {}
|
||||
for event in events:
|
||||
if event.event_type.upper() == "RAW_TEXT":
|
||||
merged.append(event)
|
||||
continue
|
||||
|
||||
key = (event.event_date, event.event_type.upper())
|
||||
existing = by_key.get(key)
|
||||
if existing is None:
|
||||
@@ -1459,6 +1585,16 @@ def _apply_schedule_event(
|
||||
) -> None:
|
||||
"""Apply one GROUP- or SCHEDULE-block event as an Eclipse keyword."""
|
||||
event_type = event.event_type.upper()
|
||||
if event_type == "RAW_TEXT":
|
||||
timeline.add_raw_text_event(
|
||||
event_date=_iso_event_date(event.event_date),
|
||||
text=event.raw_text,
|
||||
placement=event.raw_placement,
|
||||
anchor_keyword=event.raw_anchor or "",
|
||||
priority=event.raw_priority,
|
||||
)
|
||||
report.events_applied += 1
|
||||
return
|
||||
if event_type == "RESTART":
|
||||
timeline.add_keyword_event(
|
||||
event_date=_iso_event_date(event.event_date),
|
||||
|
||||
@@ -188,6 +188,7 @@ class FakeTimeline:
|
||||
self.state_calls = []
|
||||
self.wellspec_calls = []
|
||||
self.schedule_keyword_calls = []
|
||||
self.raw_text_calls = []
|
||||
self.created_events = []
|
||||
|
||||
def add_perf_event(self, **kwargs):
|
||||
@@ -225,6 +226,10 @@ class FakeTimeline:
|
||||
self.schedule_keyword_calls.append(kwargs)
|
||||
return self._new_event()
|
||||
|
||||
def add_raw_text_event(self, **kwargs):
|
||||
self.raw_text_calls.append(kwargs)
|
||||
return self._new_event()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Layer A: parsing
|
||||
@@ -557,6 +562,69 @@ class TestParsing:
|
||||
with pytest.raises(OrionParseError, match="SCHEDULE takes no arguments"):
|
||||
parse_orion_events("ORIONEVENTS 2.0\nSCHEDULE NOW\n")
|
||||
|
||||
def test_raw_text_block_preserves_body_and_attributes(self):
|
||||
text = (
|
||||
"ORIONEVENTS 2.0\nSCHEDULE\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD "
|
||||
"ANCHOR=COMPDAT PRIORITY=-2\n"
|
||||
"# not an ORION comment\n"
|
||||
" @this is raw too\n"
|
||||
"END\n"
|
||||
"END_RAW_TEXT\n"
|
||||
)
|
||||
event = parse_orion_events(text).schedule_events[0]
|
||||
|
||||
assert event.event_type == "RAW_TEXT"
|
||||
assert event.raw_text == "# not an ORION comment\n @this is raw too\nEND\n"
|
||||
assert event.raw_placement == "BEFORE_KEYWORD"
|
||||
assert event.raw_anchor == "COMPDAT"
|
||||
assert event.raw_priority == -2
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"header,error",
|
||||
[
|
||||
("RAW_TEXT", "requires PLACEMENT"),
|
||||
("RAW_TEXT PLACEMENT=NOPE", "PLACEMENT must be"),
|
||||
(
|
||||
"RAW_TEXT PLACEMENT=BEFORE_KEYWORD",
|
||||
"ANCHOR is required",
|
||||
),
|
||||
(
|
||||
"RAW_TEXT PLACEMENT=AFTER_DATE ANCHOR=COMPDAT",
|
||||
"ANCHOR is only valid",
|
||||
),
|
||||
(
|
||||
'RAW_TEXT PLACEMENT=END_OF_DATE PRIORITY="1"',
|
||||
"PRIORITY must be an integer",
|
||||
),
|
||||
(
|
||||
"RAW_TEXT PLACEMENT=END_OF_DATE EXTRA=1",
|
||||
"Unknown RAW_TEXT attribute",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_invalid_raw_text_header_rejected(self, header, error):
|
||||
text = f"ORIONEVENTS 2.0\nSCHEDULE\n @2024-01-01 {header}\nx\nEND_RAW_TEXT\n"
|
||||
with pytest.raises(OrionParseError, match=error):
|
||||
parse_orion_events(text)
|
||||
|
||||
def test_raw_text_outside_schedule_rejected(self):
|
||||
text = (
|
||||
'ORIONEVENTS 2.0\nWELL "W"\n'
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\n"
|
||||
"x\nEND_RAW_TEXT\n"
|
||||
)
|
||||
with pytest.raises(OrionParseError, match="only valid in a SCHEDULE"):
|
||||
parse_orion_events(text)
|
||||
|
||||
def test_unterminated_raw_text_rejected(self):
|
||||
text = (
|
||||
"ORIONEVENTS 2.0\nSCHEDULE\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\ntext\n"
|
||||
)
|
||||
with pytest.raises(OrionParseError, match="Unterminated RAW_TEXT"):
|
||||
parse_orion_events(text)
|
||||
|
||||
def test_report_lines_parse(self):
|
||||
text = (
|
||||
"ORIONEVENTS 2.0\n"
|
||||
@@ -1238,6 +1306,29 @@ class TestApplying:
|
||||
}
|
||||
]
|
||||
|
||||
def test_raw_text_event_is_applied_without_coalescing(self):
|
||||
text = (
|
||||
"ORIONEVENTS 2.0\nSCHEDULE\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=2\n"
|
||||
"first\nEND_RAW_TEXT\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=1\n"
|
||||
"second\nEND_RAW_TEXT\n"
|
||||
)
|
||||
timeline, report = self._apply(text)
|
||||
|
||||
assert report.events_applied == 2
|
||||
assert [call["text"] for call in timeline.raw_text_calls] == [
|
||||
"first\n",
|
||||
"second\n",
|
||||
]
|
||||
assert timeline.raw_text_calls[0] == {
|
||||
"event_date": "2024-01-01",
|
||||
"text": "first\n",
|
||||
"placement": "AFTER_DATE",
|
||||
"anchor_keyword": "",
|
||||
"priority": 2,
|
||||
}
|
||||
|
||||
def test_group_events_inject_group_name(self):
|
||||
text = (
|
||||
'ORIONEVENTS 2.0\nGROUP "OP"\n'
|
||||
@@ -1544,6 +1635,78 @@ class TestOrionEventsIntegration:
|
||||
assert f"{flag}=True" not in rptrst_block
|
||||
assert "NORST" not in tokens
|
||||
|
||||
def test_raw_text_placement_and_priority(self, project_with_case_and_wells):
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
well = project.well_paths()[0]
|
||||
document = parse_orion_events(
|
||||
"ORIONEVENTS 2.0\n"
|
||||
f'WELL "{well.name}"\n'
|
||||
" @2024-01-01 WCONHIST STATUS=OPEN\n"
|
||||
"SCHEDULE\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=5\n"
|
||||
"-- after-date-late\nEND_RAW_TEXT\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE PRIORITY=-1\n"
|
||||
"-- after-date-early\nEND_RAW_TEXT\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD ANCHOR=WCONHIST\n"
|
||||
"-- before-wconhist\nEND_RAW_TEXT\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_KEYWORD ANCHOR=WCONHIST\n"
|
||||
"-- after-wconhist\nEND_RAW_TEXT\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=END_OF_DATE\n"
|
||||
"-- end-of-date\nEND_RAW_TEXT\n"
|
||||
)
|
||||
|
||||
report = apply_orion_document(document, timeline, project)
|
||||
assert report.errors == []
|
||||
schedule = timeline.generate_schedule_text(
|
||||
eclipse_case=case, first_date_as_comment=False
|
||||
)
|
||||
|
||||
positions = [
|
||||
schedule.index(marker)
|
||||
for marker in (
|
||||
"DATES",
|
||||
"-- after-date-early",
|
||||
"-- after-date-late",
|
||||
"-- before-wconhist",
|
||||
"WCONHIST",
|
||||
"-- after-wconhist",
|
||||
"-- end-of-date",
|
||||
)
|
||||
]
|
||||
assert positions == sorted(positions)
|
||||
|
||||
def test_raw_text_only_schedule_is_generated(self, project_with_case_and_wells):
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
document = parse_orion_events(
|
||||
"ORIONEVENTS 2.0\nSCHEDULE\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=AFTER_DATE\n"
|
||||
"-- raw-only\nEND_RAW_TEXT\n"
|
||||
)
|
||||
|
||||
apply_orion_document(document, timeline, project)
|
||||
schedule = timeline.generate_schedule_text(
|
||||
eclipse_case=case, first_date_as_comment=False
|
||||
)
|
||||
assert "DATES" in schedule
|
||||
assert "-- raw-only\n" in schedule
|
||||
|
||||
schedule_with_date_comment = timeline.generate_schedule_text(eclipse_case=case)
|
||||
assert "-- Date: 1 JAN 2024\n-- raw-only\n" in schedule_with_date_comment
|
||||
|
||||
def test_raw_text_missing_anchor_fails_generation(
|
||||
self, project_with_case_and_wells
|
||||
):
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
document = parse_orion_events(
|
||||
"ORIONEVENTS 2.0\nSCHEDULE\n"
|
||||
" @2024-01-01 RAW_TEXT PLACEMENT=BEFORE_KEYWORD ANCHOR=COMPDAT\n"
|
||||
"text\nEND_RAW_TEXT\n"
|
||||
)
|
||||
|
||||
apply_orion_document(document, timeline, project)
|
||||
with pytest.raises(rips.RipsError, match="COMPDAT.*not emitted"):
|
||||
timeline.generate_schedule_text(eclipse_case=case)
|
||||
|
||||
def test_event_comment_precedes_generated_keyword(
|
||||
self, project_with_case_and_wells
|
||||
):
|
||||
|
||||
@@ -13,6 +13,7 @@ from .pdmobject import add_method
|
||||
from .resinsight_classes import EclipseCase
|
||||
from .generated.generated_classes import (
|
||||
KeywordEvent,
|
||||
Placement,
|
||||
WellEventKeyword,
|
||||
WellEventTimeline,
|
||||
WellPath,
|
||||
@@ -283,6 +284,31 @@ def add_keyword_event(
|
||||
)
|
||||
|
||||
|
||||
@add_method(WellEventTimeline)
|
||||
def add_raw_text_event(
|
||||
self: WellEventTimeline,
|
||||
event_date: str | date | datetime,
|
||||
text: str,
|
||||
placement: str = "AFTER_DATE",
|
||||
anchor_keyword: str = "",
|
||||
priority: int = 0,
|
||||
) -> Any:
|
||||
"""Add raw text at a specific position in a dated schedule section.
|
||||
|
||||
``placement`` is one of ``AFTER_DATE``, ``BEFORE_KEYWORD``,
|
||||
``AFTER_KEYWORD``, or ``END_OF_DATE``. ``anchor_keyword`` is required for
|
||||
before/after-keyword placement and must be empty for the other placements.
|
||||
Lower priority values are emitted first; source order breaks ties.
|
||||
"""
|
||||
return self.add_raw_text_event_internal(
|
||||
event_date=_format_date(event_date),
|
||||
text=text,
|
||||
placement=Placement(placement),
|
||||
anchor_keyword=anchor_keyword,
|
||||
priority=priority,
|
||||
)
|
||||
|
||||
|
||||
@add_method(WellEventTimeline)
|
||||
def generate_schedule_text(
|
||||
self: WellEventTimeline,
|
||||
|
||||
Reference in New Issue
Block a user