#14513 Orion Events: Add raw schedule text events

This commit is contained in:
Kristian Bendiksen
2026-08-24 13:26:19 +02:00
parent 1803c636a5
commit d6878ea562
14 changed files with 810 additions and 78 deletions
@@ -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;
};
//==================================================================================================
///
//==================================================================================================
+139 -3
View File
@@ -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
):
+26
View File
@@ -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,