#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:
Kristian Bendiksen committed 2026-09-25 14:35:45 +02:00
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();
+26
View File
@@ -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.