From d6878ea562337cdb31efef359e9529d96bcf07cc Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Fri, 21 Aug 2026 12:00:37 +0200 Subject: [PATCH] #14513 Orion Events: Add raw schedule text events --- .../RicScheduleDataGenerator.cpp | 157 ++++++++++++----- .../RicScheduleDataGenerator.h | 29 +-- .../WellEvents/CMakeLists_files.cmake | 1 + .../WellEvents/RimWellEvent.cpp | 1 + .../WellEvents/RimWellEvent.h | 3 +- .../WellEvents/RimWellEventRawText.cpp | 165 ++++++++++++++++++ .../WellEvents/RimWellEventRawText.h | 74 ++++++++ .../WellEvents/RimWellEventTimeline.cpp | 13 ++ .../WellEvents/RimWellEventTimeline.h | 2 + .../RimcWellEventTimeline.cpp | 90 ++++++++-- .../RimcWellEventTimeline.h | 22 +++ GrpcInterface/Python/rips/orion_events.py | 142 ++++++++++++++- .../Python/rips/tests/test_orion_events.py | 163 +++++++++++++++++ GrpcInterface/Python/rips/well_events.py | 26 +++ 14 files changed, 810 insertions(+), 78 deletions(-) create mode 100644 ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.h diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp index 78738795f6..8381d6f61d 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp @@ -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& wellPaths, - const std::vector& dates, - const std::set& mswWells, - bool firstDateAsComment, - bool alignColumns ) +std::expected RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + const std::vector& wellPaths, + const std::vector& dates, + const std::set& 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 RicScheduleDataGenerator::generateDateSection( const RimWellEventTimeline& timeline, - RimEclipseCase& eclipseCase, - const std::vector& wellPaths, - const QDateTime& date, - const std::set& mswWells, - bool dateAsComment, - bool alignColumns ) +std::expected RicScheduleDataGenerator::generateDateSection( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + const std::vector& wellPaths, + const QDateTime& date, + const std::set& mswWells, + bool dateAsComment, + bool alignColumns ) { // Keyword priority order for output static const std::vector keywordOrder = { "WELSPECS", @@ -188,6 +187,33 @@ QString RicScheduleDataGenerator::generateDateSection( const RimWellEventTimelin QString result; + auto events = timeline.getEventsAtDate( date ); + + std::vector rawTextEvents; + for ( const auto* event : events ) + { + if ( event->eventType() != RimWellEvent::EventType::RAW_TEXT ) continue; + if ( const auto* rawTextEvent = dynamic_cast( 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 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( 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 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 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 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; } diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h index 71b2f719c1..b1a1b5df9b 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.h @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -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& wellPaths, - const std::vector& dates, - const std::set& mswWells, - bool firstDateAsComment = true, - bool alignColumns = false ); + static std::expected generateSchedule( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + const std::vector& wellPaths, + const std::vector& dates, + const std::set& mswWells, + bool firstDateAsComment = true, + bool alignColumns = false ); // Collect all unique dates from all wells' timelines static std::vector collectAllDates( const RimWellEventTimeline& timeline, const std::vector& 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& wellPaths, - const QDateTime& date, - const std::set& mswWells, - bool dateAsComment = false, - bool alignColumns = false ); + static std::expected generateDateSection( const RimWellEventTimeline& timeline, + RimEclipseCase& eclipseCase, + const std::vector& wellPaths, + const QDateTime& date, + const std::set& mswWells, + bool dateAsComment = false, + bool alignColumns = false ); static std::optional generateWelspecsForWell( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, RimWellPath& well, const QDateTime& date ); diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake index a925956187..200b85d44d 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/CMakeLists_files.cmake @@ -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 diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp index 83d3c029f6..6009aa6b18 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.cpp @@ -39,6 +39,7 @@ void AppEnum::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 diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h index d3a6f47898..2fb767bed1 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEvent.h @@ -48,7 +48,8 @@ public: WELLSPEC, WCONTROL, KEYWORD, - SCHEDULE_KEYWORD + SCHEDULE_KEYWORD, + RAW_TEXT }; RimWellEvent(); diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.cpp new file mode 100644 index 0000000000..36b5332abc --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.cpp @@ -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 +// 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::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 ); +} diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.h new file mode 100644 index 0000000000..659e98c651 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventRawText.h @@ -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 +// 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 m_text; + caf::PdmField> m_placement; + caf::PdmField m_anchorKeyword; + caf::PdmField m_priority; +}; + +namespace caf +{ +template <> +void caf::AppEnum::setUp(); +} // namespace caf diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp index 35fa0a2bf6..d9617cf9e7 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp @@ -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; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h index dabbb94c85..f4b5e27964 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.h @@ -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 ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index 7e7dd0a359..e5762a3b13 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -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 RimcWellEventTimeline_addRawTextEvent::execute() +{ + auto timeline = self(); + + 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 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 RimcWellEventTimeline_generateSche std::vector mswWellPaths = m_exportMswForWells.ptrReferencedObjectsByType(); std::set 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; } diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h index eed8a6fb46..2e41a74b4b 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h @@ -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> m_itemValues; }; +//================================================================================================== +/// +//================================================================================================== +class RimcWellEventTimeline_addRawTextEvent : public caf::PdmObjectCreationMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcWellEventTimeline_addRawTextEvent( caf::PdmObjectHandle* self ); + + std::expected execute() override; + QString classKeywordReturnedType() const override; + +private: + caf::PdmField m_eventDate; + caf::PdmField m_text; + caf::PdmField> m_placement; + caf::PdmField m_anchorKeyword; + caf::PdmField m_priority; +}; + //================================================================================================== /// //================================================================================================== diff --git a/GrpcInterface/Python/rips/orion_events.py b/GrpcInterface/Python/rips/orion_events.py index 7244509470..b76ce2bb9a 100644 --- a/GrpcInterface/Python/rips/orion_events.py +++ b/GrpcInterface/Python/rips/orion_events.py @@ -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=``; 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), diff --git a/GrpcInterface/Python/rips/tests/test_orion_events.py b/GrpcInterface/Python/rips/tests/test_orion_events.py index 371f671356..16ad7a51e6 100644 --- a/GrpcInterface/Python/rips/tests/test_orion_events.py +++ b/GrpcInterface/Python/rips/tests/test_orion_events.py @@ -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 ): diff --git a/GrpcInterface/Python/rips/well_events.py b/GrpcInterface/Python/rips/well_events.py index 43f725e648..1c100d73f8 100644 --- a/GrpcInterface/Python/rips/well_events.py +++ b/GrpcInterface/Python/rips/well_events.py @@ -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,