mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-03 12:16:45 -05:00
#14640 Simulator Events: Add menu to export schedule based on events
The schedule generated from the well event timeline can now be written to file from the GUI, without scripting. - RicScheduleDataGenerator::generateScheduleForTimeline() collects the dates and wells from the timeline (honoring the last applied timestamp, never filtering insert-date events). The Python generate_schedule() method now calls it, so GUI and Python export the same schedule. - New RicExportScheduleFeature "Export Schedule from Events" asks only for the file name and uses fixed defaults: the Eclipse case of the active view (else the first case of the project), MSW keywords for every well with events, the first date as a comment and aligned columns. - Available from the context menu of the well event timeline and the well path collection, and from File -> Export, gated by the new experimental feature "simulator-events-export".
This commit is contained in:
1 parent
8324af966c
commit
9dda0faa3c
13 files changed
+508
-45
No files matched your search
@@ -34,6 +34,9 @@ const std::vector<RiaExperimentalFeatures::Feature>& RiaExperimentalFeatures::av
|
||||
{ "undo-redo-view", "Undo/Redo View", "Show the command undo/redo history view." },
|
||||
{ "oil-volume-result", "Oil Volume Result", "Compute the derived oil volume cell result." },
|
||||
{ "simulator-events-import", "Simulator Events Import", "Enable import of simulator events from file." },
|
||||
{ "simulator-events-export",
|
||||
"Simulator Events Schedule Export",
|
||||
"Enable export of an Eclipse schedule generated from the well event timeline." },
|
||||
{ "remember-dialog-size",
|
||||
"Remember Dialog Size",
|
||||
"Remember the size of property and preferences dialogs between sessions and restore it the next time they "
|
||||
|
||||
@@ -17,6 +17,7 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/MswExport/RicMswBranchBuilder.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicMswTableDataTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicExportScheduleFeature.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicTransmissibilityCalculator.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicPerforationCellFilterEvaluator.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 "RicExportScheduleFeature.h"
|
||||
|
||||
#include "RicScheduleDataGenerator.h"
|
||||
|
||||
#include "RiaApplication.h"
|
||||
#include "RiaLogging.h"
|
||||
#include "RiaQStringFormatter.h"
|
||||
|
||||
#include "RimEclipseCase.h"
|
||||
#include "RimEclipseView.h"
|
||||
#include "RimOilField.h"
|
||||
#include "RimProject.h"
|
||||
#include "RimWellEventTimeline.h"
|
||||
#include "RimWellPath.h"
|
||||
#include "RimWellPathCollection.h"
|
||||
|
||||
#include "Riu3DMainWindowTools.h"
|
||||
#include "RiuFileDialogTools.h"
|
||||
#include "RiuMessageDialog.h"
|
||||
|
||||
#include "cafSelectionManager.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <format>
|
||||
|
||||
CAF_CMD_SOURCE_INIT( RicExportScheduleFeature, "RicExportScheduleFeature" );
|
||||
|
||||
namespace
|
||||
{
|
||||
const QString exportDialogName = "SCHEDULE_EXPORT";
|
||||
const QString featureTitle = "Export Schedule";
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<QString, QString>
|
||||
RicExportScheduleFeature::exportScheduleToFile( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const QString& fileName )
|
||||
{
|
||||
if ( fileName.isEmpty() ) return std::unexpected( QString( "No file name specified" ) );
|
||||
|
||||
// Fixed defaults: multi-segment-well keywords for every well with events, the first
|
||||
// (simulation start) date as a comment, and aligned columns for readability.
|
||||
auto wellPaths = timeline.getWellPathsWithEvents();
|
||||
std::set<const RimWellPath*> mswWells( wellPaths.begin(), wellPaths.end() );
|
||||
|
||||
auto scheduleText = RicScheduleDataGenerator::generateScheduleForTimeline( timeline, eclipseCase, mswWells, true, true );
|
||||
if ( !scheduleText ) return std::unexpected( scheduleText.error() );
|
||||
|
||||
QFile file( fileName );
|
||||
if ( !file.open( QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate ) )
|
||||
{
|
||||
return std::unexpected( QString( "Could not open file for writing: %1" ).arg( fileName ) );
|
||||
}
|
||||
|
||||
QTextStream stream( &file );
|
||||
stream << *scheduleText;
|
||||
stream.flush();
|
||||
|
||||
if ( stream.status() != QTextStream::Ok || file.error() != QFileDevice::NoError )
|
||||
{
|
||||
return std::unexpected( QString( "Could not write schedule to file: %1 (%2)" ).arg( fileName, file.errorString() ) );
|
||||
}
|
||||
|
||||
file.close();
|
||||
return fileName;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWellEventTimeline* RicExportScheduleFeature::timelineToExport()
|
||||
{
|
||||
RimWellEventTimeline* timeline = caf::SelectionManager::instance()->selectedItemOfType<RimWellEventTimeline>();
|
||||
if ( !timeline )
|
||||
{
|
||||
RimWellPathCollection* wellPathCollection = caf::SelectionManager::instance()->selectedItemOfType<RimWellPathCollection>();
|
||||
if ( !wellPathCollection )
|
||||
{
|
||||
// Invoked without a related selection, e.g. from the File -> Export menu.
|
||||
if ( auto* project = RimProject::current(); project && project->activeOilField() )
|
||||
{
|
||||
wellPathCollection = project->activeOilField()->wellPathCollection();
|
||||
}
|
||||
}
|
||||
|
||||
if ( wellPathCollection ) timeline = wellPathCollection->eventTimeline();
|
||||
}
|
||||
|
||||
if ( timeline && timeline->eventCount() > 0 ) return timeline;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimEclipseCase* RicExportScheduleFeature::defaultEclipseCase()
|
||||
{
|
||||
if ( auto* eclipseView = dynamic_cast<RimEclipseView*>( RiaApplication::instance()->activeReservoirView() ) )
|
||||
{
|
||||
if ( eclipseView->eclipseCase() ) return eclipseView->eclipseCase();
|
||||
}
|
||||
|
||||
if ( auto* project = RimProject::current() )
|
||||
{
|
||||
auto eclipseCases = project->eclipseCases();
|
||||
if ( !eclipseCases.empty() ) return eclipseCases.front();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RicExportScheduleFeature::isCommandEnabled() const
|
||||
{
|
||||
return timelineToExport() != nullptr && defaultEclipseCase() != nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicExportScheduleFeature::onActionTriggered( bool isChecked )
|
||||
{
|
||||
QWidget* parentWidget = Riu3DMainWindowTools::mainWindowWidget();
|
||||
|
||||
RimWellEventTimeline* timeline = timelineToExport();
|
||||
if ( !timeline )
|
||||
{
|
||||
RiuMessageDialog::showError( parentWidget, featureTitle, "The well event timeline has no events to export." );
|
||||
return;
|
||||
}
|
||||
|
||||
RimEclipseCase* eclipseCase = defaultEclipseCase();
|
||||
if ( !eclipseCase )
|
||||
{
|
||||
RiuMessageDialog::showError( parentWidget, featureTitle, "Schedule export requires a loaded Eclipse case." );
|
||||
return;
|
||||
}
|
||||
|
||||
RiaApplication* app = RiaApplication::instance();
|
||||
QString defaultDir = app->lastUsedDialogDirectoryWithFallbackToProjectFolder( exportDialogName );
|
||||
QString defaultFile = defaultDir + "/schedule.SCH";
|
||||
|
||||
QString fileName = RiuFileDialogTools::getSaveFileName( parentWidget,
|
||||
featureTitle,
|
||||
defaultFile,
|
||||
"Eclipse Schedule Files (*.SCH);;Include Files (*.inc);;All Files (*.*)" );
|
||||
if ( fileName.isEmpty() ) return;
|
||||
|
||||
app->setLastUsedDialogDirectory( exportDialogName, QFileInfo( fileName ).absolutePath() );
|
||||
|
||||
auto exportedFile = exportScheduleToFile( *timeline, *eclipseCase, fileName );
|
||||
if ( !exportedFile )
|
||||
{
|
||||
RiaLogging::error( std::format( "Schedule export failed: {}", exportedFile.error() ) );
|
||||
RiuMessageDialog::showError( parentWidget, featureTitle, exportedFile.error() );
|
||||
return;
|
||||
}
|
||||
|
||||
RiaLogging::info( std::format( "Exported schedule based on {} well event(s) to {}", timeline->eventCount(), *exportedFile ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RicExportScheduleFeature::setupActionLook( QAction* actionToSetup )
|
||||
{
|
||||
actionToSetup->setText( "Export Schedule from Events" );
|
||||
actionToSetup->setIcon( QIcon( ":/Save.svg" ) );
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 "cafCmdFeature.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <expected>
|
||||
|
||||
class RimEclipseCase;
|
||||
class RimWellEventTimeline;
|
||||
|
||||
//==================================================================================================
|
||||
/// Export an Eclipse schedule generated from the well event timeline.
|
||||
///
|
||||
/// The file name is the only thing asked for; everything else uses fixed defaults (see
|
||||
/// exportScheduleToFile).
|
||||
//==================================================================================================
|
||||
class RicExportScheduleFeature : public caf::CmdFeature
|
||||
{
|
||||
CAF_CMD_HEADER_INIT;
|
||||
|
||||
public:
|
||||
// Generate the schedule for the timeline and write it to fileName. MSW keywords are exported
|
||||
// for every well with events, the first (simulation start) date is written as a comment
|
||||
// instead of a DATES keyword, and keywords are emitted with aligned columns.
|
||||
// Returns the written file name, or an error message.
|
||||
static std::expected<QString, QString>
|
||||
exportScheduleToFile( const RimWellEventTimeline& timeline, RimEclipseCase& eclipseCase, const QString& fileName );
|
||||
|
||||
protected:
|
||||
bool isCommandEnabled() const override;
|
||||
void onActionTriggered( bool isChecked ) override;
|
||||
void setupActionLook( QAction* actionToSetup ) override;
|
||||
|
||||
private:
|
||||
// The timeline to export: the selected timeline, the one of a selected well path collection,
|
||||
// or - when the selection has neither, e.g. when the feature is invoked from the File menu -
|
||||
// the timeline of the project's well path collection. Returns nullptr when no timeline has
|
||||
// events.
|
||||
static RimWellEventTimeline* timelineToExport();
|
||||
|
||||
// The Eclipse case of the active view, or the first Eclipse case of the project.
|
||||
static RimEclipseCase* defaultEclipseCase();
|
||||
};
|
||||
@@ -148,6 +148,53 @@ std::vector<QDateTime> RicScheduleDataGenerator::collectAllDates( const RimWellE
|
||||
return std::vector<QDateTime>( uniqueDates.begin(), uniqueDates.end() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<QString, QString> RicScheduleDataGenerator::generateScheduleForTimeline( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool firstDateAsComment,
|
||||
bool alignColumns )
|
||||
{
|
||||
// The last applied timestamp (from RimWellEventTimeline::applyEventsUpToDate) limits the
|
||||
// schedule to the events that have been materialized.
|
||||
const QDateTime lastTimestamp = timeline.lastAppliedTimestamp();
|
||||
|
||||
std::vector<QDateTime> dates;
|
||||
std::vector<RimWellPath*> wellPathsWithEvents;
|
||||
|
||||
if ( lastTimestamp.isValid() )
|
||||
{
|
||||
dates = timeline.getAllEventDates();
|
||||
std::erase_if( dates, [&lastTimestamp]( const QDateTime& date ) { return date > lastTimestamp; } );
|
||||
|
||||
wellPathsWithEvents = timeline.getWellPathsWithEventsUpToDate( lastTimestamp );
|
||||
|
||||
// Insert-date events only force a DATES keyword (e.g. to trigger a summary report), so
|
||||
// they are deliberately not filtered by the last applied timestamp.
|
||||
std::set<QDateTime> mergedDates( dates.begin(), dates.end() );
|
||||
for ( const auto* event : timeline.getEventsByType( RimWellEvent::EventType::INSERT_DATE ) )
|
||||
{
|
||||
if ( event->eventDate().isValid() ) mergedDates.insert( event->eventDate() );
|
||||
}
|
||||
dates.assign( mergedDates.begin(), mergedDates.end() );
|
||||
}
|
||||
else
|
||||
{
|
||||
// No timestamp set - include all events and wells
|
||||
dates = timeline.getAllEventDates();
|
||||
wellPathsWithEvents = timeline.getWellPathsWithEvents();
|
||||
}
|
||||
|
||||
if ( dates.empty() )
|
||||
{
|
||||
return std::unexpected( QString( "No events found in timeline" ) );
|
||||
}
|
||||
|
||||
return generateSchedule( timeline, eclipseCase, wellPathsWithEvents, dates, mswWells, firstDateAsComment, alignColumns );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Append the records of `kw` into the entry for `name`, creating that entry from a copy of `kw`
|
||||
/// on first encounter so the accumulated DeckKeyword is bound to the right ParserKeyword.
|
||||
|
||||
@@ -65,6 +65,17 @@ public:
|
||||
// Collect all unique dates from all wells' timelines
|
||||
static std::vector<QDateTime> collectAllDates( const RimWellEventTimeline& timeline, const std::vector<RimWellPath*>& wellPaths );
|
||||
|
||||
// Generate schedule for every event in the timeline. The dates and well paths are collected
|
||||
// from the timeline: when events have been applied up to a timestamp (see
|
||||
// RimWellEventTimeline::applyEventsUpToDate), later dates and wells are left out. Insert-date
|
||||
// events only force a DATES keyword and are therefore never filtered by that timestamp.
|
||||
// The remaining arguments are passed on to generateSchedule().
|
||||
static std::expected<QString, QString> generateScheduleForTimeline( const RimWellEventTimeline& timeline,
|
||||
RimEclipseCase& eclipseCase,
|
||||
const std::set<const RimWellPath*>& mswWells,
|
||||
bool firstDateAsComment = true,
|
||||
bool alignColumns = false );
|
||||
|
||||
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.
|
||||
|
||||
@@ -41,6 +41,7 @@ inline const std::map<std::string, std::string>& featureSweepDenylist()
|
||||
{ "RicSaveProjectFeature", "may open a save QFileDialog and writes to disk" },
|
||||
{ "RicSaveProjectAsFeature", "opens a save QFileDialog" },
|
||||
{ "RicSaveProjectNoGlobalPathsFeature", "opens a save QFileDialog" },
|
||||
{ "RicExportScheduleFeature", "opens a save QFileDialog and writes to disk" },
|
||||
|
||||
// External processes
|
||||
{ "RicExecuteScriptFeature", "launches an external Octave/Python process" },
|
||||
|
||||
@@ -12,6 +12,7 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewWellLogPlotFeature-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicCreatePolygonFeature-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicNewCellIndexFilterFeature-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RicExportScheduleFeature-Test.cpp
|
||||
)
|
||||
|
||||
list(APPEND SOURCE_FEATURETEST_FILES ${SOURCE_GROUP_SOURCE_FILES})
|
||||
@@ -0,0 +1,142 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 "gtest/gtest.h"
|
||||
|
||||
#include "RiaFeatureTestModelBuilder.h"
|
||||
|
||||
#include "CompletionExportCommands/RicExportScheduleFeature.h"
|
||||
|
||||
#include "RimKeywordEvent.h"
|
||||
#include "RimOilField.h"
|
||||
#include "RimProject.h"
|
||||
#include "RimWellEventInsertDate.h"
|
||||
#include "RimWellEventTimeline.h"
|
||||
#include "RimWellPathCollection.h"
|
||||
|
||||
#include "cafCmdFeature.h"
|
||||
#include "cafCmdFeatureManager.h"
|
||||
#include "cafSelectionManager.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTextStream>
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Curated test for RicExportScheduleFeature, which writes the schedule generated from the well
|
||||
/// event timeline to file. The interactive part (the save file dialog) is skipped by calling
|
||||
/// exportScheduleToFile() directly; the feature is only queried for whether it accepts the
|
||||
/// selection.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
class RicExportScheduleFeatureTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void TearDown() override
|
||||
{
|
||||
caf::SelectionManager::instance()->clearAll();
|
||||
RiaFeatureTestModelBuilder::closeProject();
|
||||
}
|
||||
|
||||
static RimWellEventTimeline* timelineWithEvents()
|
||||
{
|
||||
RimOilField* oilField = RimProject::current()->activeOilField();
|
||||
if ( !oilField || !oilField->wellPathCollection() ) return nullptr;
|
||||
|
||||
RimWellEventTimeline* timeline = oilField->wellPathCollection()->eventTimeline();
|
||||
if ( !timeline ) return nullptr;
|
||||
|
||||
const QDateTime firstDate( QDate( 2024, 1, 1 ), QTime( 0, 0, 0 ) );
|
||||
auto* keywordEvent = timeline->addKeywordEvent( firstDate, "RPTRST" );
|
||||
keywordEvent->addIntItem( "BASIC", 2 );
|
||||
|
||||
auto* insertDate = timeline->addInsertDateEvent( QDateTime( QDate( 2024, 7, 1 ), QTime( 0, 0, 0 ) ) );
|
||||
insertDate->setComment( "Mid-year summary report" );
|
||||
|
||||
return timeline;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F( RicExportScheduleFeatureTest, ExportWritesScheduleForTimelineEvents )
|
||||
{
|
||||
FeatureTestModel model = RiaFeatureTestModelBuilder::combinedModel();
|
||||
ASSERT_TRUE( model.eclipseCase != nullptr );
|
||||
|
||||
RimWellEventTimeline* timeline = timelineWithEvents();
|
||||
ASSERT_TRUE( timeline != nullptr );
|
||||
|
||||
caf::SelectionManager::instance()->setSelectedItem( timeline );
|
||||
|
||||
caf::CmdFeature* feature = caf::CmdFeatureManager::instance()->getCommandFeature( "RicExportScheduleFeature" );
|
||||
ASSERT_TRUE( feature != nullptr );
|
||||
EXPECT_TRUE( feature->canFeatureBeExecuted() );
|
||||
|
||||
// Without a related selection (e.g. from the File -> Export menu) the timeline of the
|
||||
// project's well path collection is used.
|
||||
caf::SelectionManager::instance()->clearAll();
|
||||
EXPECT_TRUE( feature->canFeatureBeExecuted() );
|
||||
|
||||
QTemporaryDir temporaryDir;
|
||||
ASSERT_TRUE( temporaryDir.isValid() );
|
||||
const QString fileName = temporaryDir.filePath( "schedule.SCH" );
|
||||
|
||||
auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, *model.eclipseCase, fileName );
|
||||
ASSERT_TRUE( exportedFile.has_value() ) << exportedFile.error().toStdString();
|
||||
EXPECT_EQ( fileName.toStdString(), exportedFile->toStdString() );
|
||||
|
||||
QFile file( fileName );
|
||||
ASSERT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) );
|
||||
const QString content = QTextStream( &file ).readAll();
|
||||
|
||||
// The first (earliest) date is written as a comment, the inserted date as a DATES keyword
|
||||
// followed by the comment of the insert-date event. Columns are aligned, so the records are
|
||||
// padded: " 1 'JUL' 2024 /".
|
||||
EXPECT_TRUE( content.contains( "-- Date: 1 JAN 2024" ) );
|
||||
EXPECT_TRUE( content.contains( "RPTRST" ) );
|
||||
EXPECT_TRUE( content.contains( "BASIC=2" ) );
|
||||
EXPECT_TRUE( content.contains( "DATES" ) );
|
||||
EXPECT_TRUE( content.contains( "'JUL'" ) );
|
||||
EXPECT_TRUE( content.contains( "-- Mid-year summary report" ) );
|
||||
// Aligned output adds a column-header comment per keyword.
|
||||
EXPECT_TRUE( content.contains( "--DAY" ) );
|
||||
}
|
||||
|
||||
TEST_F( RicExportScheduleFeatureTest, ExportOfEmptyTimelineFails )
|
||||
{
|
||||
FeatureTestModel model = RiaFeatureTestModelBuilder::combinedModel();
|
||||
ASSERT_TRUE( model.eclipseCase != nullptr );
|
||||
|
||||
RimOilField* oilField = RimProject::current()->activeOilField();
|
||||
ASSERT_TRUE( oilField != nullptr && oilField->wellPathCollection() != nullptr );
|
||||
RimWellEventTimeline* timeline = oilField->wellPathCollection()->eventTimeline();
|
||||
ASSERT_TRUE( timeline != nullptr );
|
||||
|
||||
// No events: the feature is not offered, and a direct export reports why.
|
||||
caf::SelectionManager::instance()->setSelectedItem( timeline );
|
||||
caf::CmdFeature* feature = caf::CmdFeatureManager::instance()->getCommandFeature( "RicExportScheduleFeature" );
|
||||
ASSERT_TRUE( feature != nullptr );
|
||||
EXPECT_FALSE( feature->canFeatureBeExecuted() );
|
||||
|
||||
QTemporaryDir temporaryDir;
|
||||
ASSERT_TRUE( temporaryDir.isValid() );
|
||||
|
||||
auto exportedFile = RicExportScheduleFeature::exportScheduleToFile( *timeline, *model.eclipseCase, temporaryDir.filePath( "empty.SCH" ) );
|
||||
ASSERT_FALSE( exportedFile.has_value() );
|
||||
EXPECT_EQ( std::string( "No events found in timeline" ), exportedFile.error().toStdString() );
|
||||
}
|
||||
@@ -136,6 +136,7 @@
|
||||
#include "RimViewLinkerCollection.h"
|
||||
#include "RimVirtualPerforationResults.h"
|
||||
#include "RimWellAllocationPlot.h"
|
||||
#include "RimWellEventTimeline.h"
|
||||
#include "RimWellIASettings.h"
|
||||
#include "RimWellLogChannel.h"
|
||||
#include "RimWellLogCurve.h"
|
||||
@@ -361,8 +362,19 @@ caf::CmdFeatureMenuBuilder RimContextCommandBuilder::commandsFromSelection()
|
||||
menuBuilder << "RicExportSelectedWellPathsFeature";
|
||||
menuBuilder << "RicExportVisibleWellPathsFeature";
|
||||
menuBuilder.subMenuEnd();
|
||||
if ( RiaPreferencesSystem::current()->isFeatureEnabled( "simulator-events-export" ) )
|
||||
{
|
||||
menuBuilder << "RicExportScheduleFeature";
|
||||
}
|
||||
appendExportCompletions( menuBuilder );
|
||||
}
|
||||
else if ( dynamic_cast<RimWellEventTimeline*>( firstUiItem ) )
|
||||
{
|
||||
if ( RiaPreferencesSystem::current()->isFeatureEnabled( "simulator-events-export" ) )
|
||||
{
|
||||
menuBuilder << "RicExportScheduleFeature";
|
||||
}
|
||||
}
|
||||
else if ( dynamic_cast<RimWellMeasurementFilePath*>( firstUiItem ) )
|
||||
{
|
||||
menuBuilder << "RicReloadWellMeasurementsFeature";
|
||||
|
||||
@@ -832,54 +832,12 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_generateSche
|
||||
return std::unexpected( QString( "Eclipse case is required" ) );
|
||||
}
|
||||
|
||||
// Get the last applied timestamp (from set_timestamp call)
|
||||
QDateTime lastTimestamp = timeline->lastAppliedTimestamp();
|
||||
|
||||
// If no timestamp was set, use all events
|
||||
std::vector<QDateTime> dates;
|
||||
std::vector<RimWellPath*> wellPathsWithEvents;
|
||||
|
||||
if ( lastTimestamp.isValid() )
|
||||
{
|
||||
// Filter to only include dates up to the last applied timestamp
|
||||
dates = timeline->getAllEventDates();
|
||||
dates.erase( std::remove_if( dates.begin(), dates.end(), [&lastTimestamp]( const QDateTime& date ) { return date > lastTimestamp; } ),
|
||||
dates.end() );
|
||||
|
||||
// Get only well paths that have events up to the last applied timestamp
|
||||
wellPathsWithEvents = timeline->getWellPathsWithEventsUpToDate( lastTimestamp );
|
||||
|
||||
// Insert-date events only force a DATES keyword (e.g. to trigger a summary report), so
|
||||
// they are deliberately not filtered by the last applied timestamp.
|
||||
std::set<QDateTime> mergedDates( dates.begin(), dates.end() );
|
||||
for ( const auto* event : timeline->getEventsByType( RimWellEvent::EventType::INSERT_DATE ) )
|
||||
{
|
||||
if ( event->eventDate().isValid() ) mergedDates.insert( event->eventDate() );
|
||||
}
|
||||
dates.assign( mergedDates.begin(), mergedDates.end() );
|
||||
}
|
||||
else
|
||||
{
|
||||
// No timestamp set - include all events and wells
|
||||
dates = timeline->getAllEventDates();
|
||||
wellPathsWithEvents = timeline->getWellPathsWithEvents();
|
||||
}
|
||||
|
||||
if ( dates.empty() )
|
||||
{
|
||||
return std::unexpected( QString( "No events found in timeline" ) );
|
||||
}
|
||||
|
||||
// The dates and wells are collected from the timeline, honoring any set_timestamp() call.
|
||||
std::vector<RimWellPath*> mswWellPaths = m_exportMswForWells.ptrReferencedObjectsByType();
|
||||
std::set<const RimWellPath*> mswWells( mswWellPaths.begin(), mswWellPaths.end() );
|
||||
|
||||
auto scheduleText = RicScheduleDataGenerator::generateSchedule( *timeline,
|
||||
*eclipseCase,
|
||||
wellPathsWithEvents,
|
||||
dates,
|
||||
mswWells,
|
||||
m_firstDateAsComment(),
|
||||
m_alignColumns() );
|
||||
auto scheduleText =
|
||||
RicScheduleDataGenerator::generateScheduleForTimeline( *timeline, *eclipseCase, mswWells, m_firstDateAsComment(), m_alignColumns() );
|
||||
if ( !scheduleText ) return std::unexpected( scheduleText.error() );
|
||||
|
||||
// Return the schedule text in a data container
|
||||
|
||||
@@ -511,6 +511,10 @@ void RiuMainWindow::createMenus()
|
||||
exportMenu->addAction( cmdFeatureMgr->action( "RicSaveEclipseInputActiveVisibleCellsFeature" ) );
|
||||
exportMenu->addAction( cmdFeatureMgr->action( "RicExportCompletionsForVisibleWellPathsFeature" ) );
|
||||
exportMenu->addAction( cmdFeatureMgr->action( "RicExportVisibleWellPathsFeature" ) );
|
||||
if ( RiaPreferencesSystem::current()->isFeatureEnabled( "simulator-events-export" ) )
|
||||
{
|
||||
exportMenu->addAction( cmdFeatureMgr->action( "RicExportScheduleFeature" ) );
|
||||
}
|
||||
|
||||
// Save menu actions
|
||||
fileMenu->addSeparator();
|
||||
|
||||
@@ -340,6 +340,32 @@ They also accept two policies, each `"warn"` (default), `"error"` or `"skip"`:
|
||||
|
||||
The returned `ApplyReport` carries `events_applied`, `events_skipped`, `warnings` and `errors`.
|
||||
|
||||
## Exporting a schedule from the GUI
|
||||
|
||||
The schedule generated from the well event timeline can be written to file without scripting.
|
||||
Enable *Preferences -> System -> Experimental Features -> Simulator Events Schedule Export*, then
|
||||
use **Export Schedule from Events** from either:
|
||||
|
||||
- the context menu of the *Well Event Timeline* item (shown under *Wells* once the timeline has
|
||||
events),
|
||||
- the context menu of the *Wells* (well path collection) item, or
|
||||
- *File -> Export*.
|
||||
|
||||
The only thing asked for is the file name (default `schedule.SCH` in the last used export folder).
|
||||
Everything else uses fixed defaults:
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Eclipse case | the case of the active 3D view, else the first Eclipse case of the project |
|
||||
| Multi-segment-well keywords | exported for every well with events |
|
||||
| First date | written as a `-- Date: ...` comment instead of a `DATES` keyword |
|
||||
| Formatting | aligned columns with a `--`-prefixed column header per keyword |
|
||||
|
||||
As in `generate_schedule_text()`, a `set_timestamp()`/*Apply events up to date* timestamp limits the
|
||||
export to the events up to that date, while inserted dates (`INSERT_DATE`) are always emitted.
|
||||
The written file name and the event count are logged to the message panel; an export without events
|
||||
or without a loaded Eclipse case reports the reason in a dialog.
|
||||
|
||||
## Version history
|
||||
|
||||
- **1.1** — current: all dates are datetimes with second resolution (`T` separator required for a time-of-day; fractional seconds rounded); `DURATION` values and offset terms are Go-style literals (`5d`, `12h30m`, `1.5h`, `3mon`, `-3d`) with mandatory units, strict descending unit order, and calendar-aware months. Unitless numbers and the `DAYS` suffix are rejected; `1.0` files are rejected with a migration hint.
|
||||
|
||||
Reference in new issue
Block a user