diff --git a/ApplicationLibCode/Application/Tools/RiaFilePathTools.cpp b/ApplicationLibCode/Application/Tools/RiaFilePathTools.cpp index e3d5c3c87a..e0c1bef185 100644 --- a/ApplicationLibCode/Application/Tools/RiaFilePathTools.cpp +++ b/ApplicationLibCode/Application/Tools/RiaFilePathTools.cpp @@ -418,4 +418,39 @@ std::string makeSuitableAsFileName( const std::string candidateName ) return tmp.toStdString(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::string replaceSubFolderInPath( const std::string& path, const std::string& subFolderToReplace, const std::string& newSubFolder ) +{ + auto normPath = std::filesystem::path( normalizePath( path ) ); + + std::filesystem::path newPath; + + for ( const auto& part : normPath ) + { + if ( part == subFolderToReplace ) + newPath /= newSubFolder; + else + newPath /= part; + } + + return newPath.generic_string(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::string replaceFileExtension( const std::string& filePath, const std::string& newExtension ) +{ + if ( filePath.empty() ) + { + return filePath; + } + + std::filesystem::path path( filePath ); + path.replace_extension( newExtension ); + return path.string(); +} + } // namespace RiaFilePathTools diff --git a/ApplicationLibCode/Application/Tools/RiaFilePathTools.h b/ApplicationLibCode/Application/Tools/RiaFilePathTools.h index 916d00df60..9a567ba3f6 100644 --- a/ApplicationLibCode/Application/Tools/RiaFilePathTools.h +++ b/ApplicationLibCode/Application/Tools/RiaFilePathTools.h @@ -42,10 +42,12 @@ QString removeDuplicatePathSeparators( const QString& path ) QString rootSearchPathFromSearchFilter( const QString& searchFilter ); QString removeFileExtension( const QString& filePath ); QString commonRootOfFileNames( const QStringList& filePaths ); -std::string makeSuitableAsFileName( const std::string candidateName ); -std::string normalizePath( std::string path ); +QStringList splitPathIntoComponents( const QString& path, bool splitExtensionIntoSeparateEntry = false ); -QStringList splitPathIntoComponents( const QString& path, bool splitExtensionIntoSeparateEntry = false ); +std::string makeSuitableAsFileName( const std::string candidateName ); +std::string normalizePath( std::string path ); +std::string replaceSubFolderInPath( const std::string& path, const std::string& subFolderToReplace, const std::string& newSubFolder ); +std::string replaceFileExtension( const std::string& filePath, const std::string& newExtension ); std::map keyPathComponentsForEachFilePath( const QStringList& filePaths ); diff --git a/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake index 30dc16314a..9a1d561af0 100644 --- a/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/JobCommands/CMakeLists_files.cmake @@ -5,6 +5,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RicViewJobLogFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicStopJobFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicStopAllJobsFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicNewEnsembleJobFeature.cpp ) list(APPEND COMMAND_CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES}) diff --git a/ApplicationLibCode/Commands/JobCommands/RicDuplicateJobFeature.cpp b/ApplicationLibCode/Commands/JobCommands/RicDuplicateJobFeature.cpp index 9821c5884e..51f7f21567 100644 --- a/ApplicationLibCode/Commands/JobCommands/RicDuplicateJobFeature.cpp +++ b/ApplicationLibCode/Commands/JobCommands/RicDuplicateJobFeature.cpp @@ -20,6 +20,7 @@ #include "RicNewOpmFlowJobFeature.h" +#include "Jobs/RimEnsembleJob.h" #include "Jobs/RimJobCollection.h" #include "Jobs/RimOpmFlowJob.h" #include "RimTools.h" @@ -52,12 +53,27 @@ void RicDuplicateJobFeature::onActionTriggered( bool isChecked ) { copiedJob->setWorkingDirectory( workDir ); copiedJob->setName( job->name() + " (copy)" ); - copiedJob->initAfterCopy(); auto jobColl = RimTools::jobCollection(); jobColl->addNewJob( copiedJob ); copiedJob->resolveReferencesRecursively(); + copiedJob->initAfterCopy(); + + Riu3DMainWindowTools::selectAsCurrentItem( copiedJob ); + } + } + else if ( auto job = dynamic_cast( caf::SelectionManager::instance()->selectedItem() ) ) + { + if ( auto copiedJob = job->copyObject() ) + { + copiedJob->setName( job->name() + " (copy)" ); + + auto jobColl = RimTools::jobCollection(); + jobColl->addNewJob( copiedJob ); + + copiedJob->resolveReferencesRecursively(); + copiedJob->initAfterCopy(); Riu3DMainWindowTools::selectAsCurrentItem( copiedJob ); } diff --git a/ApplicationLibCode/Commands/JobCommands/RicNewEnsembleJobFeature.cpp b/ApplicationLibCode/Commands/JobCommands/RicNewEnsembleJobFeature.cpp new file mode 100644 index 0000000000..c29e50c09e --- /dev/null +++ b/ApplicationLibCode/Commands/JobCommands/RicNewEnsembleJobFeature.cpp @@ -0,0 +1,77 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RicNewEnsembleJobFeature.h" + +#include "RiaApplication.h" +#include "RiaPreferencesOpm.h" +#include "Summary/RiaSummaryDefines.h" + +#include "EnsembleFileSet/RimEnsembleFileSet.h" +#include "Jobs/RimEnsembleJob.h" +#include "Jobs/RimJobCollection.h" +#include "RimReservoirGridEnsemble.h" +#include "RimTools.h" + +#include "Riu3DMainWindowTools.h" +#include "RiuFileDialogTools.h" + +#include "cafSelectionManager.h" + +#include +#include +#include + +CAF_CMD_SOURCE_INIT( RicNewEnsembleJobFeature, "RicNewEnsembleJobFeature" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RicNewEnsembleJobFeature::isCommandEnabled() const +{ + std::vector selectedEnsembles = caf::SelectionManager::instance()->objectsByType(); + return RiaPreferencesOpm::current()->validateFlowSettings() && selectedEnsembles.size() == 1 && + selectedEnsembles[0]->ensembleFileSet()->groupingMode() == RiaDefines::EnsembleGroupingMode::FMU_FOLDER_STRUCTURE; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicNewEnsembleJobFeature::onActionTriggered( bool isChecked ) +{ + std::vector selectedEnsembles = caf::SelectionManager::instance()->objectsByType(); + + if ( selectedEnsembles.empty() ) return; + + auto job = new RimEnsembleJob(); + job->setName( selectedEnsembles[0]->name() ); + job->setInputEnsemble( selectedEnsembles[0] ); + auto jobColl = RimTools::jobCollection(); + jobColl->addNewJob( job ); + + Riu3DMainWindowTools::selectAsCurrentItem( job ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicNewEnsembleJobFeature::setupActionLook( QAction* actionToSetup ) +{ + actionToSetup->setIcon( QIcon( ":/opm.png" ) ); + actionToSetup->setText( "New Ensemble Job... " ); +} diff --git a/ApplicationLibCode/Commands/JobCommands/RicNewEnsembleJobFeature.h b/ApplicationLibCode/Commands/JobCommands/RicNewEnsembleJobFeature.h new file mode 100644 index 0000000000..38de3a1919 --- /dev/null +++ b/ApplicationLibCode/Commands/JobCommands/RicNewEnsembleJobFeature.h @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "cafCmdFeature.h" + +//================================================================================================== +/// +//================================================================================================== +class RicNewEnsembleJobFeature : public caf::CmdFeature +{ + CAF_CMD_HEADER_INIT; + +protected: + bool isCommandEnabled() const override; + void onActionTriggered( bool isChecked ) override; + void setupActionLook( QAction* actionToSetup ) override; +}; diff --git a/ApplicationLibCode/FileInterface/CMakeLists_files.cmake b/ApplicationLibCode/FileInterface/CMakeLists_files.cmake index abda450ea1..ae5529023f 100644 --- a/ApplicationLibCode/FileInterface/CMakeLists_files.cmake +++ b/ApplicationLibCode/FileInterface/CMakeLists_files.cmake @@ -63,6 +63,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RifOpmCommonSummary.cpp ${CMAKE_CURRENT_LIST_DIR}/RifOpmFlowDeckFile.cpp ${CMAKE_CURRENT_LIST_DIR}/RifOpmDeckTools.cpp + ${CMAKE_CURRENT_LIST_DIR}/RifOpmDeckFileTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RifEventKeywordFormatter.cpp ${CMAKE_CURRENT_LIST_DIR}/RifEnsembleFractureStatisticsExporter.cpp ${CMAKE_CURRENT_LIST_DIR}/RifSummaryReaderAggregator.cpp diff --git a/ApplicationLibCode/FileInterface/RifOpmDeckFileTools.cpp b/ApplicationLibCode/FileInterface/RifOpmDeckFileTools.cpp new file mode 100644 index 0000000000..674738d498 --- /dev/null +++ b/ApplicationLibCode/FileInterface/RifOpmDeckFileTools.cpp @@ -0,0 +1,67 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RifOpmDeckFileTools.h" + +#include "RifOpmFlowDeckFile.h" + +namespace RifOpmDeckFileTools +{ + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector datesInDeckFile( const std::string& deckFileName ) +{ + RifOpmFlowDeckFile deckFile; + auto loadResult = deckFile.loadDeck( deckFileName ); + if ( !loadResult.has_value() ) + { + return {}; + } + std::vector dates; + // Extract dates from the deck file + for ( auto tt : deckFile.dates() ) + { + dates.push_back( QDateTime::fromSecsSinceEpoch( tt ) ); + } + + return dates; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector wellGroupsInFileDeck( const std::string& deckFileName ) +{ + RifOpmFlowDeckFile deckFile; + auto loadResult = deckFile.loadDeck( deckFileName ); + if ( !loadResult.has_value() ) + { + return {}; + } + + std::vector groups; + for ( auto& grp : deckFile.wellGroupsInFile() ) + { + groups.push_back( grp ); + } + return groups; +} + +} // namespace RifOpmDeckFileTools diff --git a/ApplicationLibCode/FileInterface/RifOpmDeckFileTools.h b/ApplicationLibCode/FileInterface/RifOpmDeckFileTools.h new file mode 100644 index 0000000000..880b11aa86 --- /dev/null +++ b/ApplicationLibCode/FileInterface/RifOpmDeckFileTools.h @@ -0,0 +1,30 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +#include + +#include + +namespace RifOpmDeckFileTools +{ +std::vector datesInDeckFile( const std::string& deckFileName ); +std::vector wellGroupsInFileDeck( const std::string& deckFileName ); +} // namespace RifOpmDeckFileTools diff --git a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp index 111177f0e0..748d43424c 100644 --- a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp +++ b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp @@ -373,6 +373,7 @@ bool RifOpmFlowDeckFile::saveDeckInline( std::string folder, std::string filenam int RifOpmFlowDeckFile::addKeywordAtPosition( int position, const Opm::DeckKeyword& keyword ) { if ( m_fileDeck.get() == nullptr ) return -1; + if ( keyword.empty() ) return position; // locate position auto insertIdx = internal::positionToIndex( position, m_fileDeck ); @@ -395,6 +396,7 @@ int RifOpmFlowDeckFile::addKeywordAtPosition( int position, const Opm::DeckKeywo int RifOpmFlowDeckFile::mergeKeywordAtPosition( int position, const Opm::DeckKeyword& keyword ) { if ( m_fileDeck.get() == nullptr ) return -1; + if ( keyword.empty() ) return position; // locate position auto insertIdx = internal::positionToIndex( position, m_fileDeck ); @@ -438,6 +440,7 @@ int RifOpmFlowDeckFile::mergeKeywordAtPosition( int position, const Opm::DeckKey bool RifOpmFlowDeckFile::addKeywordAtTimeStep( int timeStep, const Opm::DeckKeyword& keyword, std::string insertAfterKeyword ) { if ( m_fileDeck.get() == nullptr ) return false; + if ( keyword.empty() ) return true; auto dateIdx = internal::locateTimeStep( m_fileDeck, timeStep ); if ( dateIdx.has_value() ) @@ -472,6 +475,7 @@ bool RifOpmFlowDeckFile::addKeywordAtTimeStep( int timeStep, const Opm::DeckKeyw bool RifOpmFlowDeckFile::mergeKeywordAtTimeStep( int timeStep, const Opm::DeckKeyword& keyword, std::string insertAfterKeyword ) { if ( m_fileDeck.get() == nullptr ) return false; + if ( keyword.empty() ) return true; auto dateIdx = internal::locateTimeStep( m_fileDeck, timeStep ); if ( dateIdx.has_value() ) diff --git a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.cpp b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.cpp index 5e38232f3c..e2bd6e4f30 100644 --- a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.cpp +++ b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.cpp @@ -455,6 +455,14 @@ QString RimEnsembleFileSet::nameTemplateText() const return text; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleFileSet::setRealizationSubSet( QString filterText ) +{ + m_realizationSubSet = filterText; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.h b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.h index 417c71eda5..3e3e3b4604 100644 --- a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.h +++ b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSet.h @@ -59,6 +59,8 @@ public: void setGroupingMode( RiaDefines::EnsembleGroupingMode groupingMode ); RiaDefines::EnsembleGroupingMode groupingMode() const; + void setRealizationSubSet( QString filterText ); + void reload(); private: diff --git a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.cpp b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.cpp index 23fa670f0a..d93f928a00 100644 --- a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.cpp +++ b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.cpp @@ -58,6 +58,20 @@ std::vector createSummaryEnsemblesFromFileSets( const std:: return ensembles; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector createEnsembleFileSets( const std::vector& fileNames, + RiaDefines::EnsembleGroupingMode groupingMode ) +{ + QStringList qFileNames; + for ( const auto& fileName : fileNames ) + { + qFileNames.push_back( QString::fromStdString( fileName ) ); + } + return createEnsembleFileSets( qFileNames, groupingMode ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.h b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.h index 35434c8b5e..a0357dcde5 100644 --- a/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.h +++ b/ApplicationLibCode/ProjectDataModel/EnsembleFileSet/RimEnsembleFileSetTools.h @@ -34,6 +34,9 @@ namespace RimEnsembleFileSetTools { std::vector createSummaryEnsemblesFromFileSets( const std::vector fileSets ); std::vector createEnsembleFileSets( const QStringList& fileNames, RiaDefines::EnsembleGroupingMode groupingMode ); +std::vector + createEnsembleFileSets( const std::vector& fileNames, + RiaDefines::EnsembleGroupingMode groupingMode = RiaDefines::EnsembleGroupingMode::FMU_FOLDER_STRUCTURE ); std::vector createGridEnsemblesFromFileSets( const std::vector fileSets ); diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake index bed713b6fa..b4d755a8a3 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/Jobs/CMakeLists_files.cmake @@ -9,6 +9,9 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimKeywordWconinje.cpp ${CMAKE_CURRENT_LIST_DIR}/RimJobMonitor.cpp ${CMAKE_CURRENT_LIST_DIR}/RimKeywordBcprop.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimEnsembleJob.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimSingleJob.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimJobWellSettings.cpp ) list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES}) diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimEnsembleJob.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimEnsembleJob.cpp new file mode 100644 index 0000000000..f769d45d33 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimEnsembleJob.cpp @@ -0,0 +1,509 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimEnsembleJob.h" + +#include "RiaColorTools.h" +#include "RiaFilePathTools.h" +#include "RiaLogging.h" +#include "RiaPreferencesOpm.h" +#include "Summary/RiaSummaryTools.h" + +#include "JobCommands/RicRunJobFeature.h" +#include "JobCommands/RicStopJobFeature.h" + +#include "RifOpmDeckFileTools.h" + +#include "Ensemble/RimSummaryFileSetEnsemble.h" +#include "EnsembleFileSet/RimEnsembleFileSet.h" +#include "EnsembleFileSet/RimEnsembleFileSetTools.h" +#include "RimEclipseCase.h" +#include "RimEclipseCaseCollection.h" +#include "RimJobWellSettings.h" +#include "RimOilField.h" +#include "RimOpmFlowJob.h" +#include "RimOpmFlowJobSettings.h" +#include "RimProject.h" +#include "RimReservoirGridEnsemble.h" +#include "RimSummaryCaseMainCollection.h" +#include "RimSummaryEnsemble.h" +#include "RimSummaryEnsembleTools.h" +#include "RimTools.h" + +#include "RiuGuiTheme.h" + +#include "cafPdmUiButton.h" +#include "cafPdmUiCheckBoxEditor.h" +#include "cafPdmUiTreeAttributes.h" +#include "cafPdmUiTreeSelectionEditor.h" + +#include + +CAF_PDM_SOURCE_INIT( RimEnsembleJob, "EnsembleJob" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimEnsembleJob::RimEnsembleJob() + : m_subJobsCompleted( 0 ) + , m_subJobsStarted( 0 ) +{ + CAF_PDM_InitObject( "Ensemble Job", ":/opm.png" ); + + CAF_PDM_InitFieldNoDefault( &m_inputEnsemble, "Ensemble", "Input Ensemble" ); + m_inputEnsemble = nullptr; + m_inputEnsemble.uiCapability()->setUiReadOnly( true ); + + CAF_PDM_InitFieldNoDefault( &m_selectedRealizations, "SelectedRealizations", "Selected Realizations" ); + m_selectedRealizations.uiCapability()->setUiEditorTypeName( caf::PdmUiTreeSelectionEditor::uiEditorTypeName() ); + m_selectedRealizations.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::HIDDEN ); + + CAF_PDM_InitField( &m_outputIterationNumber, "OutputIterationNumber", 0, "Output Iteration Number" ); + CAF_PDM_InitFieldNoDefault( &m_subJobs, "SubJobs", "Jobs" ); + m_subJobs.xmlCapability()->disableIO(); + + CAF_PDM_InitFieldNoDefault( &m_jobSettings, "JobSettings", "Opm Flow Settings" ); + m_jobSettings = RiaPreferencesOpm::current()->createDefaultJobSettings(); + m_jobSettings.uiCapability()->setUiTreeChildrenHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_jobWellSettings, "JobWellSettings", "Job Well Settings" ); + m_jobWellSettings = new RimJobWellSettings(); + m_jobWellSettings.uiCapability()->setUiTreeChildrenHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_wellGroupsInInputDeck, "WellGroupsInInputDeck", "Well Groups in Input Deck" ); + m_wellGroupsInInputDeck.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_datesInInputDeck, "DatesInInputDeck", "Dates in Input Deck" ); + m_datesInInputDeck.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitField( &m_createGridEnsemble, "CreateGridEnsemble", false, "Create Grid Ensemble (if missing)" ); + CAF_PDM_InitField( &m_createSummaryEnsemble, "CreateSummaryEnsemble", true, "Create Summary Ensemble (if missing)" ); + + CAF_PDM_InitFieldNoDefault( &m_outputEnsembleFileSet, "OutputEnsembleFileSet", "Output Ensemble File Set" ); + m_outputEnsembleFileSet = nullptr; + m_outputEnsembleFileSet.uiCapability()->setUiTreeChildrenHidden( true ); + + caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_createGridEnsemble ); + caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_createSummaryEnsemble ); + + setDeletable( true ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimEnsembleJob::~RimEnsembleJob() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimEnsembleJob::jobInputFileKey() +{ + return "OpmFlowInputEnsemble"; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleJob::initAfterCopy() +{ + m_outputEnsembleFileSet = nullptr; + m_subJobs.deleteChildren(); + m_expectedOutputFiles.clear(); + m_jobLog.clear(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleJob::stop() +{ + if ( !isRunning() ) return false; + + for ( auto& subJob : m_subJobs() ) + { + subJob->stop(); + } + return true; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimEnsembleJob::percentageDone() const +{ + if ( m_subJobs().size() == 0 ) return 0.0; + + return 100.0 * m_subJobsCompleted / (int)m_subJobs.size(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const QStringList RimEnsembleJob::jobLog() const +{ + QStringList log; + for ( auto& line : m_jobLog ) + { + log.append( QString::fromStdString( line ) ); + } + return log; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleJob::matchesKeyValue( const QString& key, const QString& value ) const +{ + if ( key == jobInputFileKey() ) + { + return ( ( m_inputEnsemble() != nullptr ) && ( m_inputEnsemble()->ensembleName() == value ) ); + } + return false; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleJob::setInputEnsemble( RimReservoirGridEnsemble* ensemble ) +{ + m_inputEnsemble = ensemble; + + if ( ensemble == nullptr ) return; + if ( ensemble->cases().empty() ) return; + + auto deckName = RiaFilePathTools::replaceFileExtension( ensemble->cases()[0]->gridFileName().toStdString(), "DATA" ); + + m_datesInInputDeck = RifOpmDeckFileTools::datesInDeckFile( deckName ); + + std::vector newWellGroups; + for ( auto grp : RifOpmDeckFileTools::wellGroupsInFileDeck( deckName ) ) + { + newWellGroups.push_back( QString::fromStdString( grp ) ); + } + m_wellGroupsInInputDeck = newWellGroups; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QList RimEnsembleJob::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) +{ + QList options; + + if ( fieldNeedingOptions == &m_selectedRealizations ) + { + if ( m_inputEnsemble() != nullptr ) + { + for ( auto* realization : m_inputEnsemble->cases() ) + { + options.push_back( caf::PdmOptionItemInfo( realization->uiName(), realization ) ); + } + } + } + else if ( fieldNeedingOptions == &m_inputEnsemble ) + { + RimTools::reservoirGridEnsembleOptionItems( &options ); + } + + return options; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimEnsembleJob::selectedRealizationFileNames() const +{ + std::vector fileNames; + for ( auto& realization : m_selectedRealizations.value() ) + { + if ( realization.notNull() ) + { + auto locationOnDisk = realization->gridFileName().toStdString(); + if ( QFile::exists( QString::fromStdString( locationOnDisk ) ) ) fileNames.push_back( locationOnDisk ); + } + } + return fileNames; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimEnsembleJob::execute() +{ + auto realizations = setUpSelectedRealizations(); + + if ( realizations.empty() ) + { + RiaLogging::warning( "Ensemble Job: No realizations selected, nothing to do!" ); + return false; + } + + m_expectedOutputFiles.clear(); + m_subJobs.deleteChildren(); + m_subJobsCompleted = 0; + m_subJobsStarted = 0; + m_jobLog.clear(); + + setDeletable( false ); + + updateAllRequiredEditors(); + + for ( auto& real : realizations ) + { + std::string infoText = std::format( "Case {}: Input deck {} to output folder: {} ", + real.inputCase->uiName().toStdString(), + real.realizationInputDeckName, + real.realizationOutputDir ); + RiaLogging::info( infoText ); + m_jobLog.push_back( infoText ); + + RimOpmFlowJob* subJob = new RimOpmFlowJob(); + subJob->setEclipseCase( real.inputCase ); + subJob->setInputDataFile( QString::fromStdString( real.realizationInputDeckName ) ); + subJob->setWorkingDirectory( QString::fromStdString( real.realizationOutputDir ) ); + subJob->setName( QString::fromStdString( real.outputDeckName ) ); + subJob->setJobWellSettings( m_jobWellSettings.value() ); + subJob->setJobSettings( m_jobSettings.value() ); + subJob->setAutoLoadResults( false ); + subJob->setIsChildJob( true ); + m_subJobs.push_back( subJob ); + + m_expectedOutputFiles.push_back( real.realizationOutputDir + "/" + real.outputDeckName + ".DATA" ); + } + + updateAllRequiredEditors(); + + bool anyJobsStarted = false; + for ( auto& subJob : m_subJobs() ) + { + subJob->jobCompleted.connect( this, &RimEnsembleJob::subJobCompleted ); + if ( RicRunJobFeature::runJob( subJob ) ) + { + m_subJobsStarted++; + anyJobsStarted = true; + } + } + + m_inputEnsemble->reloadMetaDataIfNeeded(); + + setState( anyJobsStarted ? RimGenericJob::Running : RimGenericJob::Failed ); + updateConnectedEditors(); + + return anyJobsStarted; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleJob::subJobCompleted( const caf::SignalEmitter* emitter, bool runOk ) +{ + if ( const RimOpmFlowJob* job = dynamic_cast( emitter ) ) + { + std::string resultText = runOk ? std::string( "successfully." ) : std::string( "with errors." ); + std::string logText = std::format( "Job {} completed {}", job->name().toStdString(), resultText ); + m_jobLog.push_back( logText ); + } + m_subJobsCompleted++; + if ( m_subJobsCompleted >= m_subJobsStarted ) + { + m_jobLog.push_back( "All jobs completed!" ); + setFinished( true ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleJob::setFinished( bool runOk ) +{ + setDeletable( true ); + setState( RimGenericJob::Completed ); + updateConnectedEditors(); + + if ( runOk ) + { + if ( m_outputEnsembleFileSet() == nullptr ) + { + auto fileSets = RimEnsembleFileSetTools::createEnsembleFileSets( m_expectedOutputFiles ); + if ( fileSets.empty() ) return; + m_outputEnsembleFileSet = fileSets[0]; + + if ( m_createGridEnsemble() ) + { + RimEnsembleFileSetTools::createGridEnsemblesFromFileSets( { m_outputEnsembleFileSet() } ); + } + + if ( m_createSummaryEnsemble() ) + { + RimEnsembleFileSetTools::createSummaryEnsemblesFromFileSets( { m_outputEnsembleFileSet() } ); + } + } + else + { + m_outputEnsembleFileSet()->setRealizationSubSet( "*" ); + m_outputEnsembleFileSet()->reload(); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimEnsembleJob::setUpSelectedRealizations() const +{ + if ( !m_inputEnsemble() || !m_inputEnsemble()->ensembleFileSet() ) return {}; + + auto [key1, key2] = m_inputEnsemble()->ensembleFileSet()->nameKeys(); + + std::vector realizationInfos; + for ( RimEclipseCase* realization : m_selectedRealizations.value() ) + { + RealizationInfo info; + + // the case to use for well data extraction + info.inputCase = realization; + // the deck file to use as input when creating new input deck for job + info.realizationInputDeckName = RiaFilePathTools::replaceFileExtension( realization->gridFileName().toStdString(), "DATA" ); + if ( !QFile::exists( QString::fromStdString( info.realizationInputDeckName ) ) ) + { + RiaLogging::error( std::format( "Deck file {} does not exist, skipping realization.", info.realizationInputDeckName ) ); + continue; + } + + // replace the output iteration folder (key2 for FMU) with the well planning iteration folder + auto outputFileName = RiaFilePathTools::replaceSubFolderInPath( info.realizationInputDeckName, key2, outputIteration() ); + std::filesystem::path outputPath( outputFileName ); + info.realizationOutputDir = outputPath.parent_path().string(); + info.outputDeckName = outputPath.stem().string(); + + // make sure the output folder exists + if ( !std::filesystem::exists( info.realizationOutputDir ) ) + { + std::filesystem::create_directories( info.realizationOutputDir ); + } + + realizationInfos.push_back( info ); + } + return realizationInfos; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::string RimEnsembleJob::outputIteration() const +{ + return std::format( "wp-{}", m_outputIterationNumber.value() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + if ( isRunning() ) + { + auto runGrp = uiOrdering.addNewGroup( "Running" ); + auto stopButton = runGrp->addNewButton( "Stop", [this]() { RicStopJobFeature::stopJob( this ); } ); + stopButton->setUiIconFromResourceString( ":/stop.svg" ); + stopButton->setAlignment( Qt::AlignCenter ); + uiOrdering.skipRemainingFields(); + return; + } + + auto genGrp = uiOrdering.addNewGroup( "General" ); + genGrp->add( nameField() ); + genGrp->add( &m_inputEnsemble ); + genGrp->add( &m_createSummaryEnsemble ); + genGrp->add( &m_createGridEnsemble ); + + auto runButton = genGrp->addNewButton( "Run", [this]() { RicRunJobFeature::runJob( this ); } ); + runButton->setUiIconFromResourceString( ":/Play.svg" ); + runButton->setAlignment( Qt::AlignCenter ); + + auto realGrp = uiOrdering.addNewGroup( "Realizations" ); + realGrp->add( &m_selectedRealizations ); + + m_jobWellSettings->useDateStrings( dateStrings() ); + m_jobWellSettings->useWellGroups( m_wellGroupsInInputDeck.value() ); + + auto wellGrp = uiOrdering.addNewGroup( "New Well Settings" ); + m_jobWellSettings->uiOrdering( wellGrp ); + + auto opmGrp = uiOrdering.addNewGroup( "OPM Flow" ); + opmGrp->setCollapsedByDefault(); + + m_jobSettings->uiOrdering( opmGrp, false /* expand by default */ ); + + auto advGrp = uiOrdering.addNewGroup( "Advanced" ); + advGrp->setCollapsedByDefault(); + advGrp->add( &m_outputIterationNumber ); + + uiOrdering.skipRemainingFields(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimEnsembleJob::dateStrings() const +{ + std::vector dates; + for ( auto& dt : m_datesInInputDeck.value() ) + { + dates.push_back( dt.toString( "yyyy-MM-dd" ) ); + } + return dates; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleJob::defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) +{ + if ( auto* treeItemAttribute = dynamic_cast( attribute ) ) + { + if ( state() == JobState::Failed ) + { + auto txt = "Failed"; + auto tag = + caf::PdmUiTreeViewItemAttribute::createTag( QColor( Qt::red ), RiuGuiTheme::getColorByVariableName( "backgroundColor1" ), txt ); + treeItemAttribute->tags.push_back( std::move( tag ) ); + } + else if ( ( state() == JobState::Running ) || ( state() == JobState::Completed ) ) + { + auto tag = caf::PdmUiTreeViewItemAttribute::createTag(); + + if ( state() == JobState::Running ) + { + tag->text = "Running"; + } + else + { + tag->text = "Done"; + } + + cvf::Color3f viewColor = cvf::Color3f( cvf::Color3f::GREEN ); + cvf::Color3f viewTextColor = RiaColorTools::contrastColor( viewColor ); + tag->bgColor = QColor( RiaColorTools::toQColor( viewColor ) ); + tag->fgColor = QColor( RiaColorTools::toQColor( viewTextColor ) ); + treeItemAttribute->tags.push_back( std::move( tag ) ); + } + } +} diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimEnsembleJob.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimEnsembleJob.h new file mode 100644 index 0000000000..d30aaa96ec --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimEnsembleJob.h @@ -0,0 +1,107 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimGenericJob.h" + +#include "cafPdmPtrArrayField.h" +#include "cafPdmPtrField.h" + +#include + +#include +#include + +class RimReservoirGridEnsemble; +class RimEclipseCase; +class RimOpmFlowJob; +class RimOpmFlowJobSettings; +class RimJobWellSettings; +class RimEnsembleFileSet; + +//================================================================================================== +/// +//================================================================================================== +class RimEnsembleJob : public RimGenericJob +{ + CAF_PDM_HEADER_INIT; + +public: + RimEnsembleJob(); + ~RimEnsembleJob() override; + + void setInputEnsemble( RimReservoirGridEnsemble* ensemble ); + + bool execute() override; + bool stop() override; + double percentageDone() const override; + const QStringList jobLog() const override; + bool matchesKeyValue( const QString& key, const QString& value ) const override; + + static QString jobInputFileKey(); + + void setFinished( bool runOk ) override; + + void initAfterCopy(); + +protected: + struct RealizationInfo + { + RimEclipseCase* inputCase; + std::string realizationInputDeckName; + std::string realizationOutputDir; + std::string outputDeckName; + }; + + QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; + + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; + + std::vector setUpSelectedRealizations() const; + std::string outputIteration() const; + + std::vector dateStrings() const; + + void subJobCompleted( const caf::SignalEmitter* emitter, bool runOk ); + +private: + std::vector selectedRealizationFileNames() const; + +private: + caf::PdmPtrField m_inputEnsemble; + caf::PdmPtrArrayField m_selectedRealizations; + caf::PdmField m_outputIterationNumber; + caf::PdmChildArrayField m_subJobs; + caf::PdmChildField m_jobSettings; + caf::PdmChildField m_jobWellSettings; + caf::PdmPtrField m_outputEnsembleFileSet; + + caf::PdmField> m_datesInInputDeck; + caf::PdmField> m_wellGroupsInInputDeck; + + caf::PdmField m_createGridEnsemble; + caf::PdmField m_createSummaryEnsemble; + + std::vector m_expectedOutputFiles; + + std::vector m_jobLog; + int m_subJobsCompleted; + int m_subJobsStarted; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp index 434011d87c..badedd1b54 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.cpp @@ -18,22 +18,7 @@ #include "RimGenericJob.h" -#include "RiaColorTools.h" -#include "RiaGuiApplication.h" -#include "RiaLogging.h" -#include "RiaQStringFormatter.h" - -#include "RimJobMonitor.h" -#include "RimProcess.h" -#include "RimProcessQueue.h" - -#include "RiuGuiTheme.h" - #include "cafCmdFeatureMenuBuilder.h" -#include "cafPdmUiTreeAttributes.h" -#include "cafProgressInfo.h" - -#include CAF_PDM_XML_ABSTRACT_SOURCE_INIT( RimGenericJob, "GenericJob" ); // Do not use. Abstract class @@ -41,11 +26,7 @@ CAF_PDM_XML_ABSTRACT_SOURCE_INIT( RimGenericJob, "GenericJob" ); // Do not use. /// //-------------------------------------------------------------------------------------------------- RimGenericJob::RimGenericJob() - : m_percentageDone( 0.0 ) - , m_jobState( JobState::Idle ) - , m_errorsDetected( 0 ) - , m_warningsDetected( 0 ) - , m_process( nullptr ) + : m_jobState( JobState::Idle ) { CAF_PDM_InitObject( "Generic Job" ); } @@ -57,39 +38,6 @@ RimGenericJob::~RimGenericJob() { } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -double RimGenericJob::percentageDone() const -{ - return m_percentageDone; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -void RimGenericJob::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const -{ - if ( isRunning() ) - { - menuBuilder << "RicStopJobFeature"; - } - else - { - menuBuilder << "RicRunJobFeature"; - } - menuBuilder << "RicDuplicateJobFeature"; - menuBuilder << "RicViewJobLogFeature"; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -QString RimGenericJob::workingDirectory() const -{ - return ""; -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -109,182 +57,24 @@ RimGenericJob::JobState RimGenericJob::state() const //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -bool RimGenericJob::stop() +void RimGenericJob::setState( RimGenericJob::JobState state ) { - if ( !m_process.isNull() ) + m_jobState = state; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGenericJob::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const +{ + if ( isRunning() ) { - RimProcessQueue::stopProcess( m_process->ID() ); - RiaLogging::info( std::format( "Job \"{}\" stopped by user.", name() ) ); - return true; + menuBuilder << "RicStopJobFeature"; } - return false; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -bool RimGenericJob::execute() -{ - if ( isRunning() ) return false; - - m_errorsDetected = 0; - m_warningsDetected = 0; - m_percentageDone = 0.0; - m_process = nullptr; - m_jobState = JobState::Idle; - - onProgress( m_percentageDone ); - - // job preparations + else { - caf::ProgressInfo prepProgress( 1, name(), false ); - - auto prepRun = prepProgress.task( "Preparing for run, please wait..." ); - - if ( !onPrepare() ) - { - m_jobState = JobState::Failed; - onProgress( m_percentageDone ); - return false; - } + menuBuilder << "RicRunJobFeature"; } - - // check if we should run - if ( !onRun() ) return false; - - QStringList cmdLine = command(); - if ( cmdLine.isEmpty() ) - { - m_jobState = JobState::Failed; - onProgress( m_percentageDone ); - return false; - } - - // cannot delete job while running - setDeletable( false ); - m_jobState = JobState::Queued; - - m_process = new RimProcess( true, new RimJobMonitor( this ) ); - - // build process to run - QString cmd = cmdLine.takeFirst(); - m_process->setCommand( cmd ); - if ( !cmdLine.isEmpty() ) m_process->addParameters( cmdLine ); - m_process->setWorkingDirectory( workingDirectory() ); - for ( const auto& [name, value] : environment() ) - { - m_process->addEnvironmentVariable( name, value ); - } - - RimProcessQueue::queueProcess( m_process ); - onProgress( m_percentageDone ); - - return true; + menuBuilder << "RicDuplicateJobFeature"; + menuBuilder << "RicViewJobLogFeature"; } - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -bool RimGenericJob::setFinished( bool runOk ) -{ - m_jobState = runOk ? JobState::Completed : JobState::Failed; - - m_percentageDone = 100.0; - onProgress( m_percentageDone ); - setDeletable( true ); - - onCompleted( runOk ); - - return runOk; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -void RimGenericJob::setStarted() -{ - m_jobState = RimGenericJob::JobState::Running; - onProgress( m_percentageDone ); -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -void RimGenericJob::defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) -{ - static auto warnColor = QColor( RiaColorTools::toQColor( cvf::Color3f( cvf::Color3f::DARK_YELLOW ) ) ); - static auto contrastWarnColor = - QColor( RiaColorTools::toQColor( RiaColorTools::contrastColor( cvf::Color3f( cvf::Color3f::DARK_YELLOW ) ) ) ); - - static auto waitColor = QColor( RiaColorTools::toQColor( cvf::Color3f( cvf::Color3f::LIGHT_GRAY ) ) ); - static auto contrastWaitColor = - QColor( RiaColorTools::toQColor( RiaColorTools::contrastColor( cvf::Color3f( cvf::Color3f::LIGHT_GRAY ) ) ) ); - - if ( auto* treeItemAttribute = dynamic_cast( attribute ) ) - { - if ( m_jobState == JobState::Failed ) - { - auto txt = m_errorsDetected > 0 ? QString( "[%1]" ).arg( m_errorsDetected ) : "!!!"; - auto tag = - caf::PdmUiTreeViewItemAttribute::createTag( QColor( Qt::red ), RiuGuiTheme::getColorByVariableName( "backgroundColor1" ), txt ); - treeItemAttribute->tags.push_back( std::move( tag ) ); - } - else if ( m_jobState == JobState::Queued ) - { - auto tag = caf::PdmUiTreeViewItemAttribute::createTag(); - tag->text = "Waiting..."; - tag->bgColor = waitColor; - tag->fgColor = contrastWaitColor; - treeItemAttribute->tags.push_back( std::move( tag ) ); - } - else if ( ( m_jobState == JobState::Running ) || ( m_jobState == JobState::Completed ) ) - { - auto tag = caf::PdmUiTreeViewItemAttribute::createTag(); - - if ( m_jobState == JobState::Running ) - { - tag->text = QString( "%1 %" ).arg( m_percentageDone, 0, 'f', 1 ); - } - else - { - tag->text = "Done"; - } - - double factor = m_percentageDone / 100.0; - - cvf::Color3f viewColor = cvf::Color3f( cvf::Color3f::GREEN ); - viewColor.set( viewColor.r() * factor, viewColor.g() * factor, viewColor.b() * factor ); - cvf::Color3f viewTextColor = RiaColorTools::contrastColor( viewColor ); - tag->bgColor = QColor( RiaColorTools::toQColor( viewColor ) ); - tag->fgColor = QColor( RiaColorTools::toQColor( viewTextColor ) ); - treeItemAttribute->tags.push_back( std::move( tag ) ); - } - if ( m_warningsDetected > 0 ) - { - auto warnTxt = QString( "[%1]" ).arg( m_warningsDetected ); - - auto warnTag = caf::PdmUiTreeViewItemAttribute::createTag( warnColor, contrastWarnColor, warnTxt ); - treeItemAttribute->tags.push_back( std::move( warnTag ) ); - auto iconTag = caf::PdmUiTreeViewItemAttribute::createTag(); - iconTag->icon = caf::IconProvider( ":/warning.png" ); - treeItemAttribute->tags.push_back( std::move( iconTag ) ); - } - } -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -const QStringList RimGenericJob::jobLog() const -{ - if ( m_process.isNull() ) return QStringList(); - return m_process->stdOut(); -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -bool RimGenericJob::matchesKeyValue( const QString& key, const QString& value ) const -{ - return false; -} \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h index da11d2b97f..349ff2c7d6 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimGenericJob.h @@ -50,40 +50,23 @@ public: RimGenericJob(); ~RimGenericJob() override; - bool execute(); - bool setFinished( bool runOk ); - void setStarted(); + virtual bool execute() = 0; + virtual bool stop() = 0; + virtual double percentageDone() const = 0; + virtual const QStringList jobLog() const = 0; + virtual bool matchesKeyValue( const QString& key, const QString& value ) const = 0; + virtual void processLogOutput( const QString& logLine ) {}; - bool isRunning() const; - bool stop(); + virtual void setFinished( bool runOk ) = 0; + virtual void setStarted() {}; + bool isRunning() const; JobState state() const; - - double percentageDone() const; - const QStringList jobLog() const; - - virtual bool matchesKeyValue( const QString& key, const QString& value ) const; - - virtual void processLogOutput( const QString& logLine ) = 0; + void setState( JobState state ); protected: void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; - void defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; - - virtual QStringList command() = 0; - virtual std::map environment() = 0; - virtual QString workingDirectory() const; - virtual bool onPrepare() = 0; - virtual bool onRun() = 0; - virtual void onCompleted( bool success ) = 0; - virtual void onProgress( double percentageDone ) = 0; - -protected: - double m_percentageDone; - int m_warningsDetected; - int m_errorsDetected; private: - JobState m_jobState; - caf::PdmPointer m_process; + JobState m_jobState; }; diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobWellSettings.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobWellSettings.cpp new file mode 100644 index 0000000000..31ccba5318 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobWellSettings.cpp @@ -0,0 +1,307 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimJobWellSettings.h" + +#include "RimKeywordWconinje.h" +#include "RimKeywordWconprod.h" +#include "RimTools.h" +#include "RimWellPath.h" + +#include "cafPdmFieldCapability.h" +#include "cafPdmUiCheckBoxEditor.h" +#include "cafPdmUiComboBoxEditor.h" + +CAF_PDM_SOURCE_INIT( RimJobWellSettings, "JobWellSettings" ); + +namespace caf +{ +template <> +void caf::AppEnum::setUp() +{ + addItem( RimJobWellSettings::WellOpenType::OPEN_BY_POSITION, "OpenByPosition", "By Position in File" ); + addItem( RimJobWellSettings::WellOpenType::OPEN_AT_DATE, "AtSelectedDate", "By Date" ); + + setDefault( RimJobWellSettings::WellOpenType::OPEN_AT_DATE ); +} + +} // namespace caf + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimJobWellSettings::RimJobWellSettings() +{ + CAF_PDM_InitObject( "Job Well Settings" ); + + CAF_PDM_InitFieldNoDefault( &m_wellPath, "WellPath", "Well Path for New Well" ); + + CAF_PDM_InitField( &m_addNewWell, "AddNewWell", true, "Add New Well" ); + CAF_PDM_InitField( &m_includeMSWData, "IncludeMswData", false, "Include MSW Data" ); + + CAF_PDM_InitFieldNoDefault( &m_wellGroupName, "WellGroupName", "Well Group Name" ); + m_wellGroupName.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() ); + + CAF_PDM_InitField( &m_wellOpenType, "WellOpenType", caf::AppEnum( WellOpenType::OPEN_AT_DATE ), "Open Well" ); + m_wellOpenType.uiCapability()->setUiReadOnly( true ); + + CAF_PDM_InitField( &m_wellOpenKeyword, "WellOpenKeyword", QString( "WCONPROD" ), "Open Well Keyword" ); + m_wellOpenKeyword.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() ); + m_wellOpenKeyword.xmlCapability()->disableIO(); + + CAF_PDM_InitFieldNoDefault( &m_wconprodKeyword, "WconprodKeyword", "WCONPROD Settings" ); + m_wconprodKeyword = new RimKeywordWconprod(); + m_wconprodKeyword.uiCapability()->setUiTreeChildrenHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_wconinjeKeyword, "WconinjeKeyword", "WCONINJE Settings" ); + m_wconinjeKeyword = new RimKeywordWconinje(); + m_wconinjeKeyword.uiCapability()->setUiTreeChildrenHidden( true ); + + CAF_PDM_InitField( &m_openTimeStep, "OpenTimeStep", 0, " " ); + + caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_includeMSWData ); + caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_addNewWell ); + + setDeletable( true ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimJobWellSettings::~RimJobWellSettings() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setWellPath( RimWellPath* wellPath ) +{ + m_wellPath = wellPath; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setWellGroupName( const QString& wellGroupName ) +{ + m_wellGroupName = wellGroupName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setOpenTimeStep( int openTimeStep ) +{ + m_openTimeStep = openTimeStep; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setWellOpenType( WellOpenType wellOpenType ) +{ + m_wellOpenType = caf::AppEnum( wellOpenType ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setWellOpenKeyword( const QString& wellOpenKeyword ) +{ + m_wellOpenKeyword = wellOpenKeyword; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setWconprodKeyword( RimKeywordWconprod* wconprodKeyword ) +{ + m_wconprodKeyword = wconprodKeyword; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setWconinjeKeyword( RimKeywordWconinje* wconinjeKeyword ) +{ + m_wconinjeKeyword = wconinjeKeyword; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::setIncludeMSWData( bool includeMSWData ) +{ + m_includeMSWData = includeMSWData; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimJobWellSettings::addNewWell() const +{ + return m_addNewWell; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellPath* RimJobWellSettings::wellPath() const +{ + return m_wellPath(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimJobWellSettings::wellGroupName() const +{ + return m_wellGroupName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RimJobWellSettings::openTimeStep() const +{ + return m_openTimeStep; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimJobWellSettings::WellOpenType RimJobWellSettings::wellOpenType() const +{ + return m_wellOpenType(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimJobWellSettings::wellOpenKeyword() const +{ + return m_wellOpenKeyword; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimKeywordWconprod* RimJobWellSettings::wconprodKeyword() const +{ + auto copy = m_wconprodKeyword()->copyByXmlSerialization( caf::PdmDefaultObjectFactory::instance() ); + return dynamic_cast( copy ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimKeywordWconinje* RimJobWellSettings::wconinjeKeyword() const +{ + auto copy = m_wconinjeKeyword()->copyByXmlSerialization( caf::PdmDefaultObjectFactory::instance() ); + return dynamic_cast( copy ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimJobWellSettings::includeMSWData() const +{ + return m_includeMSWData; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::useDateStrings( const std::vector& dateStrings ) +{ + m_dateStrings = dateStrings; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::useWellGroups( const std::vector& wellGroups ) +{ + m_wellGroups = wellGroups; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimJobWellSettings::uiOrdering( caf::PdmUiGroup* wellGrp ) +{ + wellGrp->add( &m_addNewWell ); + + if ( m_addNewWell() ) + { + wellGrp->add( &m_wellPath ); + wellGrp->add( &m_wellGroupName ); + wellGrp->add( &m_wellOpenKeyword ); + if ( m_wellOpenKeyword() == "WCONPROD" ) + { + auto wconGrp = wellGrp->addNewGroup( "WCONPROD Settings" ); + m_wconprodKeyword->uiOrdering( wconGrp ); + wconGrp->setCollapsedByDefault(); + } + else + { + auto wconGrp = wellGrp->addNewGroup( "WCONINJE Settings" ); + m_wconinjeKeyword->uiOrdering( wconGrp ); + wconGrp->setCollapsedByDefault(); + } + + wellGrp->add( &m_wellOpenType ); + wellGrp->add( &m_openTimeStep ); + wellGrp->add( &m_includeMSWData ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QList RimJobWellSettings::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) +{ + QList options; + + if ( fieldNeedingOptions == &m_wellPath ) + { + RimTools::wellPathOptionItems( &options ); + } + else if ( fieldNeedingOptions == &m_openTimeStep ) + { + for ( int i = 0; i < static_cast( m_dateStrings.size() - 1 ); i++ ) + { + options.push_back( caf::PdmOptionItemInfo( m_dateStrings[i], QVariant::fromValue( i ) ) ); + } + } + else if ( fieldNeedingOptions == &m_wellOpenKeyword ) + { + options.push_back( caf::PdmOptionItemInfo( "WCONPROD", QVariant::fromValue( QString( "WCONPROD" ) ) ) ); + options.push_back( caf::PdmOptionItemInfo( "WCONINJE", QVariant::fromValue( QString( "WCONINJE" ) ) ) ); + } + else if ( fieldNeedingOptions == &m_wellGroupName ) + { + for ( auto& grp : m_wellGroups ) + { + options.push_back( caf::PdmOptionItemInfo( grp, QVariant::fromValue( grp ) ) ); + } + } + + return options; +} diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimJobWellSettings.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobWellSettings.h new file mode 100644 index 0000000000..657042e808 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimJobWellSettings.h @@ -0,0 +1,89 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "cafPdmField.h" +#include "cafPdmObject.h" +#include "cafPdmPtrField.h" + +#include + +class RimWellPath; +class RimKeywordWconprod; +class RimKeywordWconinje; + +//================================================================================================== +/// +/// +//================================================================================================== +class RimJobWellSettings : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + RimJobWellSettings(); + ~RimJobWellSettings() override; + + enum class WellOpenType + { + OPEN_BY_POSITION, + OPEN_AT_DATE + }; + + void uiOrdering( caf::PdmUiGroup* uiGroup ); + + void setWellPath( RimWellPath* wellPath ); + void setWellGroupName( const QString& wellGroupName ); + void setOpenTimeStep( int openTimeStep ); + void setWellOpenType( WellOpenType wellOpenType ); + void setWellOpenKeyword( const QString& wellOpenKeyword ); + void setWconprodKeyword( RimKeywordWconprod* wconprodKeyword ); + void setWconinjeKeyword( RimKeywordWconinje* wconinjeKeyword ); + void setIncludeMSWData( bool includeMSWData ); + + bool addNewWell() const; + RimWellPath* wellPath() const; + QString wellGroupName() const; + int openTimeStep() const; + WellOpenType wellOpenType() const; + QString wellOpenKeyword() const; + RimKeywordWconprod* wconprodKeyword() const; + RimKeywordWconinje* wconinjeKeyword() const; + bool includeMSWData() const; + + void useDateStrings( const std::vector& dateStrings ); + void useWellGroups( const std::vector& wellGroups ); + +protected: + QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; + +private: + caf::PdmField m_addNewWell; + caf::PdmPtrField m_wellPath; + caf::PdmField m_wellGroupName; + caf::PdmField m_openTimeStep; + caf::PdmField> m_wellOpenType; + caf::PdmField m_wellOpenKeyword; + caf::PdmChildField m_wconprodKeyword; + caf::PdmChildField m_wconinjeKeyword; + caf::PdmField m_includeMSWData; + + std::vector m_dateStrings; + std::vector m_wellGroups; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp index 2a9a0fde38..fc0d6cbd42 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.cpp @@ -45,6 +45,7 @@ #include "RimEclipseCaseCollection.h" #include "RimEclipseCaseEnsemble.h" #include "RimFishbones.h" +#include "RimJobWellSettings.h" #include "RimKeywordFactory.h" #include "RimKeywordWconinje.h" #include "RimKeywordWconprod.h" @@ -78,15 +79,6 @@ CAF_PDM_SOURCE_INIT( RimOpmFlowJob, "OpmFlowJob" ); namespace caf { -template <> -void caf::AppEnum::setUp() -{ - addItem( RimOpmFlowJob::WellOpenType::OPEN_BY_POSITION, "OpenByPosition", "By Position in File" ); - addItem( RimOpmFlowJob::WellOpenType::OPEN_AT_DATE, "AtSelectedDate", "By Date" ); - - setDefault( RimOpmFlowJob::WellOpenType::OPEN_AT_DATE ); -} - template <> void caf::AppEnum::setUp() { @@ -105,6 +97,8 @@ RimOpmFlowJob::RimOpmFlowJob() : m_fileDeckHasDates( false ) , m_fileDeckIsRestart( false ) , m_startStepForProgress( -1 ) + , jobCompleted( this ) + , progressUpdate( this ) { CAF_PDM_InitObject( "Opm Flow Simulation", ":/opm.png" ); @@ -129,7 +123,10 @@ RimOpmFlowJob::RimOpmFlowJob() CAF_PDM_InitFieldNoDefault( &m_wellGroupName, "WellGroupName", "Well Group Name" ); m_wellGroupName.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() ); - CAF_PDM_InitField( &m_wellOpenType, "WellOpenType", caf::AppEnum( WellOpenType::OPEN_AT_DATE ), "Open Well" ); + CAF_PDM_InitField( &m_wellOpenType, + "WellOpenType", + caf::AppEnum( RimJobWellSettings::WellOpenType::OPEN_AT_DATE ), + "Open Well" ); CAF_PDM_InitField( &m_wellOpenKeyword, "WellOpenKeyword", QString( "WCONPROD" ), "Open Well Keyword" ); m_wellOpenKeyword.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() ); m_wellOpenKeyword.xmlCapability()->disableIO(); @@ -155,6 +152,12 @@ RimOpmFlowJob::RimOpmFlowJob() CAF_PDM_InitField( &m_numberOfNewDates, "NumberOfNewDates", 12, "Number of Dates to Append" ); CAF_PDM_InitField( &m_dateAppendType, "DateAppendType", caf::AppEnum( DateAppendType::ADD_MONTHS ), " " ); + CAF_PDM_InitField( &m_autoLoadResults, "AutoLoadResults", true, "Automatic loading of results from job" ); + m_autoLoadResults.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitField( &m_isChildJob, "IsChildJob", false, "Is Child Job" ); + m_isChildJob.uiCapability()->setUiHidden( true ); + caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_addToEnsemble ); caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_pauseBeforeRun ); caf::PdmUiNativeCheckBoxEditor::configureFieldForEditor( &m_useRestart ); @@ -280,8 +283,8 @@ void RimOpmFlowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& { if ( isRunning() ) { - auto runGrp = uiOrdering.addNewGroup( "Running" ); m_workDir.uiCapability()->setUiReadOnly( true ); + auto runGrp = uiOrdering.addNewGroup( "Running" ); runGrp->add( &m_workDir ); auto stopButton = runGrp->addNewButton( "Stop", [this]() { RicStopJobFeature::stopJob( this ); } ); stopButton->setUiIconFromResourceString( ":/stop.svg" ); @@ -289,14 +292,26 @@ void RimOpmFlowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering.skipRemainingFields(); return; } - m_workDir.uiCapability()->setUiReadOnly( false ); + if ( m_isChildJob() ) + { + m_workDir.uiCapability()->setUiReadOnly( true ); + uiOrdering.add( &m_workDir ); + uiOrdering.skipRemainingFields(); + return; + } + + m_workDir.uiCapability()->setUiReadOnly( false ); auto genGrp = uiOrdering.addNewGroup( "General" ); genGrp->add( nameField() ); genGrp->add( &m_deckFileName ); genGrp->add( &m_workDir ); genGrp->add( &m_addToEnsemble ); + auto runButton = genGrp->addNewButton( "Run", [this]() { RicRunJobFeature::runJob( this ); } ); + runButton->setUiIconFromResourceString( ":/Play.svg" ); + runButton->setAlignment( Qt::AlignCenter ); + if ( m_eclipseCase() == nullptr ) { m_addNewWell = false; @@ -331,7 +346,7 @@ void RimOpmFlowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& if ( m_fileDeckHasDates ) { - if ( m_wellOpenType() == WellOpenType::OPEN_AT_DATE ) + if ( m_wellOpenType() == RimJobWellSettings::WellOpenType::OPEN_AT_DATE ) { wellGrp->add( &m_openTimeStep ); if ( !m_fileDeckIsRestart ) @@ -344,7 +359,7 @@ void RimOpmFlowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& } if ( m_eclipseCase() ) wellGrp->add( &m_includeMSWData ); } - else if ( m_wellOpenType == WellOpenType::OPEN_BY_POSITION ) + else if ( m_wellOpenType == RimJobWellSettings::WellOpenType::OPEN_BY_POSITION ) { createOpenPostionButton = true; } @@ -354,7 +369,7 @@ void RimOpmFlowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& { createOpenPostionButton = true; - m_wellOpenType = WellOpenType::OPEN_BY_POSITION; + m_wellOpenType = RimJobWellSettings::WellOpenType::OPEN_BY_POSITION; m_wellOpenType.uiCapability()->setUiReadOnly( true ); } @@ -386,11 +401,7 @@ void RimOpmFlowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& } auto opmGrp = uiOrdering.addNewGroup( "OPM Flow" ); - - auto runButton = opmGrp->addNewButton( "Run", [this]() { RicRunJobFeature::runJob( this ); } ); - runButton->setUiIconFromResourceString( ":/Play.svg" ); - runButton->setAlignment( Qt::AlignCenter ); - + opmGrp->setCollapsedByDefault(); opmGrp->add( &m_pauseBeforeRun ); m_jobSettings->uiOrdering( opmGrp, false /* expand by default */ ); @@ -562,6 +573,14 @@ void RimOpmFlowJob::setInputDataFile( QString filename ) openDeckFile(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::setJobSettings( RimOpmFlowJobSettings* jobSettings ) +{ + m_jobSettings = jobSettings->copyObject(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -811,28 +830,40 @@ bool RimOpmFlowJob::onPrepare() } m_wellPath->completionSettings()->setGroupName( m_wellGroupName() ); + // well data needs the case to be open, this is not necessarily true for ensemble + bool needToOpenCase = !m_eclipseCase->isReservoirCaseOpen(); + if ( needToOpenCase ) + { + m_eclipseCase->ensureReservoirCaseIsOpen(); + } + int mergePosition = mergeBasicWellSettings(); if ( mergePosition < 0 ) { + if ( needToOpenCase ) m_eclipseCase->closeReservoirCase(); RiaLogging::error( "Unable to merge new well data into DATA file. Please check file format." ); return false; } - if ( ( m_includeMSWData ) && ( m_wellOpenType == WellOpenType::OPEN_AT_DATE ) ) + if ( ( m_includeMSWData ) && ( m_wellOpenType == RimJobWellSettings::WellOpenType::OPEN_AT_DATE ) ) { mergePosition = mergeMswData( mergePosition ); if ( mergePosition < 0 ) { + if ( needToOpenCase ) m_eclipseCase->closeReservoirCase(); RiaLogging::error( "Failed to merge MSW data into file deck." ); return false; } } + // close cases that was previously closed to save memory + if ( needToOpenCase ) m_eclipseCase->closeReservoirCase(); + Opm::DeckKeyword openKeyword = ( m_wellOpenKeyword() == "WCONPROD" ) ? m_wconprodKeyword->keyword( wellNameInDeck ) : m_wconinjeKeyword->keyword( wellNameInDeck ); // open new well at selected timestep - if ( m_wellOpenType == WellOpenType::OPEN_AT_DATE ) + if ( m_wellOpenType == RimJobWellSettings::WellOpenType::OPEN_AT_DATE ) { if ( !m_deckFile->openWellAtTimeStep( m_openTimeStep(), openKeyword ) ) { @@ -911,6 +942,7 @@ void RimOpmFlowJob::onProgress( double percentageDone ) { m_percentageDone = percentageDone; updateConnectedEditors(); + progressUpdate.send( percentageDone ); } //-------------------------------------------------------------------------------------------------- @@ -918,8 +950,19 @@ void RimOpmFlowJob::onProgress( double percentageDone ) //-------------------------------------------------------------------------------------------------- void RimOpmFlowJob::onCompleted( bool success ) { - if ( !success ) return; + if ( success && m_autoLoadResults() ) + { + loadResults(); + } + jobCompleted.send( success ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::loadResults() +{ QString outputEgridFileName = workingDirectory() + "/" + deckName() + ".EGRID"; if ( !QFile::exists( outputEgridFileName ) ) return; @@ -1044,13 +1087,18 @@ int RimOpmFlowJob::mergeBasicWellSettings() auto complumpKw = RimKeywordFactory::complumpKeyword( compdata, wellName ); auto welspecsKw = RimKeywordFactory::welspecsKeyword( m_wellGroupName().toStdString(), m_eclipseCase(), m_wellPath() ); - if ( welspecsKw.empty() || compdatKw.empty() ) + if ( compdatKw.empty() ) { - RiaLogging::error( "Failed to create WELSPECS and COMPDAT keywords for selected well path. Do you have a valid case selected?" ); + RiaLogging::warning( "Failed to create COMPDAT keyword for selected well path. Do you have a valid case selected?" ); + } + + if ( welspecsKw.empty() ) + { + RiaLogging::error( "Failed to create WELSPECS keyword for selected well path. Do you have a valid case selected?" ); return failure; } - if ( m_wellOpenType == WellOpenType::OPEN_AT_DATE ) + if ( m_wellOpenType == RimJobWellSettings::WellOpenType::OPEN_AT_DATE ) { // reverse order for correct insertion order if ( !complumpKw.empty() ) @@ -1122,13 +1170,18 @@ int RimOpmFlowJob::mergeMswData( int mergePosition ) auto wsegaicdKw = RimKeywordFactory::wsegaicdKeyword( mswDataResult.value() ); auto wsegsicdKw = RimKeywordFactory::wsegsicdKeyword( mswDataResult.value() ); - if ( welsegsKw.empty() || compsegsKw.empty() ) + if ( compsegsKw.empty() ) { - RiaLogging::error( "Failed to create WELSEGS or COMPSEGS keyword from MSW data." ); - return failure; + RiaLogging::warning( "Unable to create COMPSEGS keyword from MSW data, skipping MSW export. Is the wellpath outside the grid?" ); + return ( m_wellOpenType() == RimJobWellSettings::WellOpenType::OPEN_AT_DATE ) ? 0 : mergePosition; } - if ( m_wellOpenType == WellOpenType::OPEN_AT_DATE ) + if ( welsegsKw.empty() ) + { + RiaLogging::warning( "Unable to create WELSEGS keyword from MSW data. Is the wellpath outside the grid?" ); + } + + if ( m_wellOpenType() == RimJobWellSettings::WellOpenType::OPEN_AT_DATE ) { // make sure we insert after COMPDAT kw if ( !m_deckFile->addKeywordAtTimeStep( m_openTimeStep(), welsegsKw, "COMPDAT" ) ) return failure; @@ -1280,3 +1333,39 @@ QString RimOpmFlowJob::jobInputFileKey() { return "OpmFlowInputFile"; } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::setJobWellSettings( RimJobWellSettings* jobWellSettings ) +{ + if ( jobWellSettings == nullptr ) return; + + m_addNewWell = jobWellSettings->addNewWell(); + m_wellPath = jobWellSettings->wellPath(); + m_wellGroupName = jobWellSettings->wellGroupName(); + m_openTimeStep = jobWellSettings->openTimeStep(); + m_wellOpenType = jobWellSettings->wellOpenType(); + m_wellOpenKeyword = jobWellSettings->wellOpenKeyword(); + m_wconinjeKeyword = jobWellSettings->wconinjeKeyword(); + m_wconprodKeyword = jobWellSettings->wconprodKeyword(); + m_includeMSWData = jobWellSettings->includeMSWData(); + + updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::setAutoLoadResults( bool autoLoadResults ) +{ + m_autoLoadResults = autoLoadResults; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimOpmFlowJob::setIsChildJob( bool isChildJob ) +{ + m_isChildJob = isChildJob; +} diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h index 31a7aee1e1..3041de92ba 100644 --- a/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimOpmFlowJob.h @@ -18,9 +18,12 @@ #pragma once -#include "RimGenericJob.h" +#include "RimSingleJob.h" + +#include "RimJobWellSettings.h" #include "cafPdmPtrField.h" +#include "cafSignal.h" #include @@ -37,23 +40,20 @@ class RimOpmFlowJobSettings; /// /// //================================================================================================== -class RimOpmFlowJob : public RimGenericJob +class RimOpmFlowJob : public RimSingleJob { CAF_PDM_HEADER_INIT; public: - enum class WellOpenType - { - OPEN_BY_POSITION, - OPEN_AT_DATE - }; - enum class DateAppendType { ADD_DAYS, ADD_MONTHS }; + caf::Signal jobCompleted; + caf::Signal progressUpdate; + public: RimOpmFlowJob(); ~RimOpmFlowJob() override; @@ -61,8 +61,14 @@ public: void setWorkingDirectory( QString workDir ); void setEclipseCase( RimEclipseCase* eCase ); void setInputDataFile( QString filename ); + void setJobSettings( RimOpmFlowJobSettings* jobSettings ); + void setJobWellSettings( RimJobWellSettings* jobWellSettings ); + void setIsChildJob( bool isChildJob ); + void initAfterCopy(); + void setAutoLoadResults( bool autoLoad ); + QString deckName(); QString mainWorkingDirectory() const; @@ -90,6 +96,8 @@ protected: void closeDeckFile(); bool copyUnrstFileToWorkDir(); + void loadResults(); + private: RimEclipseCase* findExistingCase( QString filename ); QString deckExtension() const; @@ -118,19 +126,20 @@ private: caf::PdmField m_addToEnsemble; caf::PdmField m_currentRunId; caf::PdmField m_useRestart; + caf::PdmField m_isChildJob; caf::PdmPtrField m_wellPath; caf::PdmPtrField m_eclipseCase; caf::PdmPtrField m_gridEnsemble; caf::PdmPtrField m_summaryEnsemble; - caf::PdmField m_openTimeStep; - caf::PdmField m_endTimeStepEnabled; - caf::PdmField m_endTimeStep; - caf::PdmField m_addNewWell; - caf::PdmField> m_wellOpenType; - caf::PdmField m_includeMSWData; - caf::PdmField m_wellGroupName; + caf::PdmField m_openTimeStep; + caf::PdmField m_endTimeStepEnabled; + caf::PdmField m_endTimeStep; + caf::PdmField m_addNewWell; + caf::PdmField> m_wellOpenType; + caf::PdmField m_includeMSWData; + caf::PdmField m_wellGroupName; caf::PdmField m_appendNewDates; caf::PdmField m_newDatesInterval; @@ -141,6 +150,8 @@ private: caf::PdmChildField m_wconinjeKeyword; caf::PdmChildField m_jobSettings; + caf::PdmField m_autoLoadResults; + caf::PdmField m_wellOpenKeyword; std::unique_ptr m_deckFile; diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimSingleJob.cpp b/ApplicationLibCode/ProjectDataModel/Jobs/RimSingleJob.cpp new file mode 100644 index 0000000000..5c794b44ad --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimSingleJob.cpp @@ -0,0 +1,235 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimSingleJob.h" + +#include "RiaColorTools.h" +#include "RiaGuiApplication.h" +#include "RiaLogging.h" +#include "RiaQStringFormatter.h" + +#include "RimJobMonitor.h" +#include "RimProcess.h" +#include "RimProcessQueue.h" + +#include "RiuGuiTheme.h" + +#include "cafPdmUiTreeAttributes.h" +#include "cafProgressInfo.h" + +#include + +CAF_PDM_XML_ABSTRACT_SOURCE_INIT( RimSingleJob, "SingleJob" ); // Do not use. Abstract class + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimSingleJob::RimSingleJob() + : m_percentageDone( 0.0 ) + , m_errorsDetected( 0 ) + , m_warningsDetected( 0 ) + , m_process( nullptr ) +{ + CAF_PDM_InitObject( "Generic Single Job" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimSingleJob::~RimSingleJob() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimSingleJob::percentageDone() const +{ + return m_percentageDone; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimSingleJob::stop() +{ + if ( !m_process.isNull() ) + { + RimProcessQueue::stopProcess( m_process->ID() ); + RiaLogging::info( std::format( "Job \"{}\" stopped by user.", name() ) ); + return true; + } + return false; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimSingleJob::execute() +{ + if ( isRunning() ) return false; + + m_errorsDetected = 0; + m_warningsDetected = 0; + m_percentageDone = 0.0; + m_process = nullptr; + setState( JobState::Idle ); + + onProgress( m_percentageDone ); + + // job preparations + { + caf::ProgressInfo prepProgress( 1, name(), false ); + + auto prepRun = prepProgress.task( "Preparing for run, please wait..." ); + + if ( !onPrepare() ) + { + setFinished( false ); + return false; + } + } + + // check if we should run + if ( !onRun() ) return false; + + QStringList cmdLine = command(); + if ( cmdLine.isEmpty() ) + { + setFinished( false ); + return false; + } + + // cannot delete job while running + setDeletable( false ); + setState( JobState::Queued ); + + m_process = new RimProcess( true, new RimJobMonitor( this ) ); + + // build process to run + QString cmd = cmdLine.takeFirst(); + m_process->setCommand( cmd ); + if ( !cmdLine.isEmpty() ) m_process->addParameters( cmdLine ); + m_process->setWorkingDirectory( workingDirectory() ); + for ( const auto& [name, value] : environment() ) + { + m_process->addEnvironmentVariable( name, value ); + } + + RimProcessQueue::queueProcess( m_process ); + onProgress( m_percentageDone ); + + return true; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSingleJob::setFinished( bool runOk ) +{ + setState( runOk ? JobState::Completed : JobState::Failed ); + + m_percentageDone = 100.0; + onProgress( m_percentageDone ); + setDeletable( true ); + + onCompleted( runOk ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSingleJob::setStarted() +{ + setState( RimSingleJob::JobState::Running ); + onProgress( m_percentageDone ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSingleJob::defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) +{ + static auto warnColor = QColor( RiaColorTools::toQColor( cvf::Color3f( cvf::Color3f::DARK_YELLOW ) ) ); + static auto contrastWarnColor = + QColor( RiaColorTools::toQColor( RiaColorTools::contrastColor( cvf::Color3f( cvf::Color3f::DARK_YELLOW ) ) ) ); + + static auto waitColor = QColor( RiaColorTools::toQColor( cvf::Color3f( cvf::Color3f::LIGHT_GRAY ) ) ); + static auto contrastWaitColor = + QColor( RiaColorTools::toQColor( RiaColorTools::contrastColor( cvf::Color3f( cvf::Color3f::LIGHT_GRAY ) ) ) ); + + if ( auto* treeItemAttribute = dynamic_cast( attribute ) ) + { + if ( state() == JobState::Failed ) + { + auto txt = m_errorsDetected > 0 ? QString( "[%1]" ).arg( m_errorsDetected ) : "!!!"; + auto tag = + caf::PdmUiTreeViewItemAttribute::createTag( QColor( Qt::red ), RiuGuiTheme::getColorByVariableName( "backgroundColor1" ), txt ); + treeItemAttribute->tags.push_back( std::move( tag ) ); + } + else if ( state() == JobState::Queued ) + { + auto tag = caf::PdmUiTreeViewItemAttribute::createTag(); + tag->text = "Waiting..."; + tag->bgColor = waitColor; + tag->fgColor = contrastWaitColor; + treeItemAttribute->tags.push_back( std::move( tag ) ); + } + else if ( ( state() == JobState::Running ) || ( state() == JobState::Completed ) ) + { + auto tag = caf::PdmUiTreeViewItemAttribute::createTag(); + + if ( state() == JobState::Running ) + { + tag->text = QString( "%1 %" ).arg( m_percentageDone, 0, 'f', 1 ); + } + else + { + tag->text = "Done"; + } + + double factor = m_percentageDone / 100.0; + + cvf::Color3f viewColor = cvf::Color3f( cvf::Color3f::GREEN ); + viewColor.set( viewColor.r() * factor, viewColor.g() * factor, viewColor.b() * factor ); + cvf::Color3f viewTextColor = RiaColorTools::contrastColor( viewColor ); + tag->bgColor = QColor( RiaColorTools::toQColor( viewColor ) ); + tag->fgColor = QColor( RiaColorTools::toQColor( viewTextColor ) ); + treeItemAttribute->tags.push_back( std::move( tag ) ); + } + if ( m_warningsDetected > 0 ) + { + auto warnTxt = QString( "[%1]" ).arg( m_warningsDetected ); + + auto warnTag = caf::PdmUiTreeViewItemAttribute::createTag( warnColor, contrastWarnColor, warnTxt ); + treeItemAttribute->tags.push_back( std::move( warnTag ) ); + auto iconTag = caf::PdmUiTreeViewItemAttribute::createTag(); + iconTag->icon = caf::IconProvider( ":/warning.png" ); + treeItemAttribute->tags.push_back( std::move( iconTag ) ); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const QStringList RimSingleJob::jobLog() const +{ + if ( m_process.isNull() ) return QStringList(); + return m_process->stdOut(); +} diff --git a/ApplicationLibCode/ProjectDataModel/Jobs/RimSingleJob.h b/ApplicationLibCode/ProjectDataModel/Jobs/RimSingleJob.h new file mode 100644 index 0000000000..eac678fda2 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Jobs/RimSingleJob.h @@ -0,0 +1,71 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimGenericJob.h" + +#include "cafPdmPointer.h" + +#include +#include + +#include + +class RimProcess; + +//================================================================================================== +/// +/// +//================================================================================================== +class RimSingleJob : public RimGenericJob +{ + CAF_PDM_HEADER_INIT; + +public: + RimSingleJob(); + ~RimSingleJob() override; + + bool execute() override; + bool stop() override; + double percentageDone() const override; + const QStringList jobLog() const override; + bool matchesKeyValue( const QString& key, const QString& value ) const override = 0; + + void setFinished( bool runOk ) override; + void setStarted() override; + +protected: + void defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; + + virtual QStringList command() = 0; + virtual std::map environment() = 0; + virtual QString workingDirectory() const = 0; + virtual bool onPrepare() = 0; + virtual bool onRun() = 0; + virtual void onCompleted( bool success ) = 0; + virtual void onProgress( double percentageDone ) = 0; + +protected: + double m_percentageDone; + int m_warningsDetected; + int m_errorsDetected; + +private: + caf::PdmPointer m_process; +}; diff --git a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp index a6b2bc801c..167808e30e 100644 --- a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp @@ -756,6 +756,7 @@ RimCaseCollection* RimReservoirGridEnsemble::caseCollection() const void RimReservoirGridEnsemble::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const { menuBuilder << "RicNewViewForGridEnsembleFeature"; + menuBuilder.addSeparator(); menuBuilder << "RicNewStatisticsContourMapFeature"; menuBuilder << "RicNewWellTargetMappingFeature"; @@ -770,6 +771,8 @@ void RimReservoirGridEnsemble::appendMenuItems( caf::CmdFeatureMenuBuilder& menu menuBuilder.subMenuStart( "Data Filters", QIcon( ":/CellFilter.png" ) ); m_dataFilterCollection->appendMenuItems( menuBuilder ); menuBuilder.subMenuEnd(); + menuBuilder.addSeparator(); + menuBuilder << "RicNewEnsembleJobFeature"; } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/RimTools.cpp b/ApplicationLibCode/ProjectDataModel/RimTools.cpp index f5ad272361..299f73210e 100644 --- a/ApplicationLibCode/ProjectDataModel/RimTools.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimTools.cpp @@ -32,11 +32,13 @@ #include "RimColorLegendCollection.h" #include "RimCompletionTemplateCollection.h" #include "RimEclipseCase.h" +#include "RimEclipseCaseCollection.h" #include "RimFaultInView.h" #include "RimFaultInViewCollection.h" #include "RimGeoMechCase.h" #include "RimOilField.h" #include "RimProject.h" +#include "RimReservoirGridEnsemble.h" #include "RimSeismicData.h" #include "RimSeismicDataCollection.h" #include "RimSeismicDifferenceData.h" @@ -377,6 +379,21 @@ void RimTools::eclipseCaseOptionItems( QList* options ) } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimTools::reservoirGridEnsembleOptionItems( QList* options ) +{ + if ( !options ) return; + if ( RimProject* proj = RimProject::current() ) + { + for ( auto ensemble : proj->activeOilField()->analysisModels()->reservoirGridEnsembles.childrenByType() ) + { + options->push_back( caf::PdmOptionItemInfo( ensemble->name(), ensemble ) ); + } + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/RimTools.h b/ApplicationLibCode/ProjectDataModel/RimTools.h index f3f760b4cb..fe36cf98c3 100644 --- a/ApplicationLibCode/ProjectDataModel/RimTools.h +++ b/ApplicationLibCode/ProjectDataModel/RimTools.h @@ -45,6 +45,7 @@ class RimFaultInViewCollection; class RimPolygonCollection; class RimAutomationSettings; class RimJobCollection; +class RimReservoirGridEnsemble; //-------------------------------------------------------------------------------------------------- /// @@ -71,6 +72,7 @@ public: static void seismicDataOptionItems( QList* options, cvf::BoundingBox worldBBox, bool basicDataOnly = false ); static void seismicDataOptionItems( QList* options ); static void polygonOptionItems( QList* options ); + static void reservoirGridEnsembleOptionItems( QList* options ); static void faultOptionItems( QList* options, RimFaultInViewCollection* coll ); diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.cpp index fd48ae8208..1f4ea416e4 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.cpp @@ -38,6 +38,7 @@ #include "RifHdf5SummaryExporter.h" #endif +#include "Ensemble/RimSummaryFileSetEnsemble.h" #include "RimCaseDisplayNameTools.h" #include "RimCsvSummaryCase.h" #include "RimDeltaSummaryEnsemble.h" @@ -407,6 +408,22 @@ std::vector RimSummaryCaseMainCollection::summaryEnsembles( return ensembles; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimSummaryCaseMainCollection::summaryFileSetEnsembles() const +{ + std::vector ensembles; + for ( auto ensemble : m_ensembles.childrenByType() ) + { + if ( auto fileSetEnsemble = dynamic_cast( ensemble ) ) + { + ensembles.push_back( fileSetEnsemble ); + } + } + return ensembles; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.h b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.h index b3c152464d..43df6b3e7f 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCaseMainCollection.h @@ -28,6 +28,7 @@ class RimSummaryCase; class RimFileSummaryCase; class RimEclipseResultCase; class RimSummaryEnsemble; +class RimSummaryFileSetEnsemble; class RifSummaryCaseFileResultInfo; //================================================================================================== @@ -47,9 +48,10 @@ public: RimSummaryCase* summaryCase( size_t idx ); size_t summaryCaseCount() const; - std::vector allSummaryCases() const; - std::vector topLevelSummaryCases() const; - std::vector summaryEnsembles() const; + std::vector allSummaryCases() const; + std::vector topLevelSummaryCases() const; + std::vector summaryEnsembles() const; + std::vector summaryFileSetEnsembles() const; std::vector createSummaryCasesFromFileInfos( const std::vector& summaryHeaderFileInfos, bool readStateFromFirstFile, diff --git a/ApplicationLibCode/UnitTests/RiaFilePathTools-Test.cpp b/ApplicationLibCode/UnitTests/RiaFilePathTools-Test.cpp index df99e84bd0..d4b5a41c9d 100644 --- a/ApplicationLibCode/UnitTests/RiaFilePathTools-Test.cpp +++ b/ApplicationLibCode/UnitTests/RiaFilePathTools-Test.cpp @@ -193,3 +193,50 @@ TEST( RiaFilePathTools, removeFileExtension ) // Empty path EXPECT_EQ( QString( "" ), RiaFilePathTools::removeFileExtension( "" ) ); } + +//-------------------------------------------------------------------------------------------------- +TEST( RiaFilePathTools, replaceSubFolderInPath ) +{ + { + std::string testPath0( "e:/models/from_equinor_sftp/drogon3d_ahm/realization-0/iter-3/eclipse/model/DROGON-0.SMSPEC" ); + std::string testPath1( "e:/models/from_equinor_sftp/drogon3d_ahm/realization-1/iter-3/eclipse/model/DROGON-1.SMSPEC" ); + std::string testPath2( "/models/from_equinor_sftp/drogon3d_ahm/realization-1/iter-3/eclipse/model/DROGON-1.SMSPEC" ); + + std::string expPath0( "e:/models/from_equinor_sftp/drogon3d_ahm/realization-0/wp-0/eclipse/model/DROGON-0.SMSPEC" ); + std::string expPath1( "e:/models/from_equinor_sftp/drogon3d_ahm/realization-1/wp-0/eclipse/model/DROGON-1.SMSPEC" ); + std::string expPath2( "/models/from_equinor_sftp/drogon3d_ahm/realization-1/wp-0/eclipse/model/DROGON-1.SMSPEC" ); + + auto newPath0 = RiaFilePathTools::replaceSubFolderInPath( testPath0, "iter-3", "wp-0" ); + auto newPath1 = RiaFilePathTools::replaceSubFolderInPath( testPath1, "iter-3", "wp-0" ); + auto newPath2 = RiaFilePathTools::replaceSubFolderInPath( testPath2, "iter-3", "wp-0" ); + + EXPECT_EQ( expPath0, newPath0 ); + EXPECT_EQ( expPath1, newPath1 ); + EXPECT_EQ( expPath2, newPath2 ); + } +} + +//-------------------------------------------------------------------------------------------------- +TEST( RiaFilePathTools, replaceExtension ) +{ + // A folder name can contain a dot. Only a file extension is removed, the folder name is left untouched. + const std::string pathWithExtension = "/scratch/fmu/user/drogon.2024/realization-$(INDEX)/iter-0/eclipse/model/DROGON-$(INDEX).ESMRY"; + EXPECT_EQ( std::string( "/scratch/fmu/user/drogon.2024/realization-$(INDEX)/iter-0/eclipse/model/DROGON-$(INDEX).DATA" ), + RiaFilePathTools::replaceFileExtension( pathWithExtension, "DATA" ) ); + + // A path without a file extension must be returned unmodified, see https://github.com/OPM/ResInsight/issues/14470 + const std::string pathWithoutExtension = "/scratch/fmu/user/drogon.2024/realization-$(INDEX)/iter-0/eclipse/model/DROGON-$(INDEX).DATA"; + EXPECT_EQ( pathWithoutExtension, RiaFilePathTools::replaceFileExtension( pathWithoutExtension, "DATA" ) ); + +#ifdef WIN32 + // A backslash is only recognized as a path separator on Windows + const std::string windowsPath = "d:\\scratch\\drogon.2024\\realization-0\\DROGON-0.DATA"; + EXPECT_EQ( windowsPath, RiaFilePathTools::replaceFileExtension( windowsPath, "DATA" ) ); +#endif + + // No folder part + EXPECT_EQ( std::string( "DROGON-0.DATA" ), RiaFilePathTools::replaceFileExtension( "DROGON-0.SMSPEC", "DATA" ) ); + + // Empty path + EXPECT_EQ( std::string( "" ), RiaFilePathTools::replaceFileExtension( "", "DATA" ) ); +}