///////////////////////////////////////////////////////////////////////////////// // // 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 "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 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 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(); if ( anyJobsStarted ) { setStarted(); } 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 ) { 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::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_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 RimEnsembleJob::dateStrings() const { std::vector dates; for ( auto& dt : m_datesInInputDeck.value() ) { dates.push_back( dt.toString( "yyyy-MM-dd" ) ); } return dates; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimEnsembleJob::treeItemTags() { std::vector tags; if ( state() == JobState::Failed ) { auto tag = caf::PdmUiTreeViewItemTag::create( QColor( Qt::red ), RiuGuiTheme::getColorByVariableName( "backgroundColor1" ), "Failed" ); 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 ); if ( state() == JobState::Running ) { auto timeTag = caf::PdmUiTreeViewItemTag::create( QColor( Qt::black ), QColor( Qt::white ), runTimeString() ); tags.push_back( timeTag ); } } return tags; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimEnsembleJob::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) { if ( changedField == &m_outputIterationNumber ) { m_outputEnsembleName = QString::fromStdString( outputIteration() ); } }