Files
ResInsight/ApplicationLibCode/ProjectDataModel/Jobs/RimEnsembleJob.cpp
T
Jon Jenssen d9d7507bcd Preferences framework
Add preferences for HPC/Batch scheduler
Add slurm and LSF support
Add batch job monitor
Support launcher specific user settings
Use internal timer, not global view timer
Add some more options to batch monitor
Improve naming
Add tests for job lists
2026-10-19 12:09:52 +02:00

526 lines
20 KiB
C++

/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "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 "EnsembleFileSet/RimEnsembleFileSet.h"
#include "EnsembleFileSet/RimEnsembleFileSetTools.h"
#include "RimEclipseCase.h"
#include "RimJobWellSettings.h"
#include "RimOpmFlowJob.h"
#include "RimOpmFlowJobSettings.h"
#include "RimReservoirGridEnsemble.h"
#include "RimTools.h"
#include "RiuGuiTheme.h"
#include "cafPdmUiButton.h"
#include "cafPdmUiCheckBoxEditor.h"
#include "cafPdmUiTreeAttributes.h"
#include "cafPdmUiTreeSelectionEditor.h"
#include "cafPdmUiTreeViewEditor.h"
#include <QFile>
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_InitField( &m_outputEnsembleName, "OutputEnsembleName", QString( "wp-0" ), "Output Ensemble Name" );
m_outputEnsembleName.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::initAfterRead()
{
m_outputEnsembleName = QString( "wp-%1" ).arg( m_outputIterationNumber.value() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimEnsembleJob::initAfterCopy()
{
m_outputEnsembleFileSet = nullptr;
m_subJobs.deleteChildren();
m_expectedOutputFiles.clear();
m_jobLog.clear();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimEnsembleJob::stop()
{
if ( isRunning() )
{
for ( auto& subJob : m_subJobs() )
{
subJob->stop();
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
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<QString> newWellGroups;
for ( auto grp : RifOpmDeckFileTools::wellGroupsInFileDeck( deckName ) )
{
newWellGroups.push_back( QString::fromStdString( grp ) );
}
m_wellGroupsInInputDeck = newWellGroups;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QList<caf::PdmOptionItemInfo> RimEnsembleJob::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions )
{
QList<caf::PdmOptionItemInfo> 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<std::string> RimEnsembleJob::selectedRealizationFileNames() const
{
std::vector<std::string> 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<const RimOpmFlowJob*>( 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 )
{
RimGenericJob::setFinished( runOk );
setDeletable( true );
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::RealizationInfo> RimEnsembleJob::setUpSelectedRealizations() const
{
if ( !m_inputEnsemble() || !m_inputEnsemble()->ensembleFileSet() ) return {};
auto [key1, key2] = m_inputEnsemble()->ensembleFileSet()->nameKeys();
std::vector<RealizationInfo> 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_outputEnsembleName );
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<QString> RimEnsembleJob::dateStrings() const
{
std::vector<QString> dates;
for ( auto& dt : m_datesInInputDeck.value() )
{
dates.push_back( dt.toString( "yyyy-MM-dd" ) );
}
return dates;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<caf::PdmUiTreeViewItemTag> RimEnsembleJob::treeItemTags()
{
std::vector<caf::PdmUiTreeViewItemTag> tags;
if ( state() == JobState::Failed )
{
auto txt = "Failed";
auto tag = caf::PdmUiTreeViewItemTag::create( QColor( Qt::red ), RiuGuiTheme::getColorByVariableName( "backgroundColor1" ), txt );
tags.push_back( tag );
}
else if ( ( state() == JobState::Running ) || ( state() == JobState::Completed ) )
{
auto tag = caf::PdmUiTreeViewItemTag::create();
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 ) );
tags.push_back( tag );
}
return tags;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimEnsembleJob::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
{
if ( changedField == &m_outputIterationNumber )
{
m_outputEnsembleName = QString::fromStdString( outputIteration() );
}
}