From 6c42ba5ae0456311399eeecf71422350481b01cb Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Thu, 8 Oct 2026 09:02:09 +0200 Subject: [PATCH] #14878: Add well-pick zone filtering to RFT cross plot and tornado plot Let the RFT parameter cross plot reference a RimWellFormationsFile and use its well-pick formations to filter/group data by zone, pushed down to the tornado plot and RFT correlation sub plots. --- .../RimParameterRftCrossPlot.cpp | 152 ++++++++++++++---- .../RimParameterRftCrossPlot.h | 48 +++--- .../RimRftCorrelationReportPlot.cpp | 20 +++ .../RimRftCorrelationReportPlot.h | 1 + .../CorrelationPlots/RimRftCrossPlotTools.cpp | 138 +++++++++++++--- .../CorrelationPlots/RimRftCrossPlotTools.h | 80 +++++++-- .../CorrelationPlots/RimRftTornadoPlot.cpp | 65 ++++++-- .../CorrelationPlots/RimRftTornadoPlot.h | 26 ++- .../UnitTests/RimRftCrossPlotTools-Test.cpp | 106 ++++++++++++ 9 files changed, 535 insertions(+), 101 deletions(-) diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp index b511aadfe3..4fc269170f 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp @@ -26,8 +26,12 @@ #include "RigEnsembleParameter.h" #include "RigStatisticsTools.h" +#include "Formations/RimWellFormationsCollection.h" +#include "Formations/RimWellFormationsFile.h" + #include "RimEclipseCase.h" #include "RimEclipseResultCase.h" +#include "RimOilField.h" #include "RimProject.h" #include "RimRftCrossPlotTools.h" #include "RimSummaryCase.h" @@ -44,6 +48,7 @@ #include "cafPdmPointer.h" #include "cafPdmUiComboBoxEditor.h" +#include "cafPdmUiTreeSelectionEditor.h" #include "qwt_picker_machine.h" #include "qwt_plot.h" @@ -86,9 +91,15 @@ RimParameterRftCrossPlot::RimParameterRftCrossPlot() CAF_PDM_InitFieldNoDefault( &m_selectedTimeStep, "TimeStep", "Time Step" ); m_selectedTimeStep.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() ); CAF_PDM_InitFieldNoDefault( &m_eclipseCase, "EclipseCase", "Eclipse Case (MD fallback)" ); - CAF_PDM_InitField( &m_useDepthRange, "UseDepthRange", false, "Filter by Depth Range" ); + CAF_PDM_InitField( &m_filterMode, + "FilterMode", + RimRftCrossPlotTools::DepthFilterModeEnum( RimRftCrossPlotTools::DepthFilterMode::NONE ), + "Depth Filter" ); CAF_PDM_InitField( &m_depthRangeMin, "DepthRangeMin", 0.0, "Min Depth" ); CAF_PDM_InitField( &m_depthRangeMax, "DepthRangeMax", 5000.0, "Max Depth" ); + CAF_PDM_InitFieldNoDefault( &m_wellFormations, "WellFormations", "Well Formations File" ); + CAF_PDM_InitFieldNoDefault( &m_selectedZones, "SelectedZones", "Zones" ); + m_selectedZones.uiCapability()->setUiEditorTypeName( caf::PdmUiTreeSelectionEditor::uiEditorTypeName() ); CAF_PDM_InitField( &m_depthType, "DepthType", caf::AppEnum( RiaDefines::DepthType::MEASURED_DEPTH ), "Depth Type" ); m_depthType.uiCapability()->setUiHidden( true ); // driven by the parent RimRftCorrelationReportPlot CAF_PDM_InitField( &m_samplingMode, "SamplingMode", SamplingModeEnum( SamplingMode::MEAN_PER_REALIZATION ), "Sampling" ); @@ -164,6 +175,14 @@ void RimParameterRftCrossPlot::setEnsembleParameter( const QString& paramName ) m_ensembleParameter = paramName; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimParameterRftCrossPlot::setWellFormations( RimWellFormationsFile* wellFormationsFile ) +{ + m_wellFormations = wellFormationsFile; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -209,7 +228,7 @@ RimEclipseResultCase* RimParameterRftCrossPlot::eclipseCase() const //-------------------------------------------------------------------------------------------------- bool RimParameterRftCrossPlot::useDepthRange() const { - return m_useDepthRange(); + return m_filterMode() != RimRftCrossPlotTools::DepthFilterMode::NONE; } //-------------------------------------------------------------------------------------------------- @@ -236,6 +255,30 @@ RiaDefines::DepthType RimParameterRftCrossPlot::depthType() const return m_depthType(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimRftCrossPlotTools::DepthFilterMode RimParameterRftCrossPlot::filterMode() const +{ + return m_filterMode(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimParameterRftCrossPlot::selectedZones() const +{ + return m_selectedZones(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimWellFormationsFile* RimParameterRftCrossPlot::wellFormationsFile() const +{ + return m_wellFormations(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -244,6 +287,21 @@ RiuQwtPlotWidget* RimParameterRftCrossPlot::viewer() return m_plotWidget; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimParameterRftCrossPlot::depthIntervals() const +{ + return RimRftCrossPlotTools::buildDepthIntervals( useDepthRange(), + m_filterMode(), + m_depthRangeMin(), + m_depthRangeMax(), + m_wellFormations(), + m_wellName(), + m_selectedZones(), + m_depthType() ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -268,9 +326,7 @@ std::vector RimParameterRftCrossPlot::create m_wellName(), m_selectedTimeStep(), m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), + depthIntervals(), m_depthType() ); if ( samplesPerCase.size() != allCases.size() ) return {}; @@ -292,9 +348,7 @@ std::vector RimParameterRftCrossPlot::create m_wellName(), m_selectedTimeStep(), m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), + depthIntervals(), m_depthType() ); if ( pressurePerCase.size() != allCases.size() ) return {}; @@ -333,13 +387,15 @@ void RimParameterRftCrossPlot::updateAxes() const int axisTitleSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_axisTitleFontSize() ); const int axisValueSize = caf::FontTools::absolutePointSize( RiaPreferences::current()->defaultPlotFontSize(), m_axisValueFontSize() ); - const QString pressureLabel = m_samplingMode() == SamplingMode::ALL_SAMPLES ? "Pressure" : "Mean Pressure"; - const QString depthLabel = m_useDepthRange() ? QString( "%1 [%2 %3 - %4]" ) - .arg( pressureLabel ) - .arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) - .arg( m_depthRangeMin() ) - .arg( m_depthRangeMax() ) - : pressureLabel; + const QString pressureLabel = m_samplingMode() == SamplingMode::ALL_SAMPLES ? "Pressure" : "Mean Pressure"; + const QString filterDescription = RimRftCrossPlotTools::depthFilterDescription( useDepthRange(), + m_filterMode(), + m_depthType(), + m_depthRangeMin(), + m_depthRangeMax(), + m_selectedZones() ); + const QString depthLabel = filterDescription.isEmpty() ? pressureLabel + : QString( "%1 [%2]" ).arg( pressureLabel ).arg( filterDescription ); m_plotWidget->setAxisTitleText( RiuPlotAxis::defaultLeft(), depthLabel ); m_plotWidget->setAxisTitleEnabled( RiuPlotAxis::defaultLeft(), true ); @@ -518,11 +574,18 @@ void RimParameterRftCrossPlot::defineUiOrdering( QString uiConfigName, caf::PdmU auto* depthGroup = uiOrdering.addNewGroup( QString( "Depth Range (%1)" ).arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) ); - depthGroup->add( &m_useDepthRange ); + depthGroup->add( &m_filterMode ); depthGroup->add( &m_depthRangeMin ); depthGroup->add( &m_depthRangeMax ); - m_depthRangeMin.uiCapability()->setUiReadOnly( !m_useDepthRange() ); - m_depthRangeMax.uiCapability()->setUiReadOnly( !m_useDepthRange() ); + depthGroup->add( &m_wellFormations ); + depthGroup->add( &m_selectedZones ); + + const bool useRange = m_filterMode() == RimRftCrossPlotTools::DepthFilterMode::DEPTH_RANGE; + const bool useZones = m_filterMode() == RimRftCrossPlotTools::DepthFilterMode::ZONES; + m_depthRangeMin.uiCapability()->setUiHidden( !useRange ); + m_depthRangeMax.uiCapability()->setUiHidden( !useRange ); + m_wellFormations.uiCapability()->setUiHidden( !useZones ); + m_selectedZones.uiCapability()->setUiHidden( !useZones || !m_wellFormations() ); auto* crossPlotGroup = uiOrdering.addNewGroup( "Cross Plot Parameter" ); crossPlotGroup->add( &m_ensembleParameter ); @@ -564,6 +627,13 @@ void RimParameterRftCrossPlot::fieldChangedByUi( const caf::PdmFieldHandle* chan m_selectedTimeStep = timeSteps.empty() ? QDateTime() : *timeSteps.begin(); } + if ( changedField == &m_wellName || changedField == &m_wellFormations ) + { + // The selected zones belong to the previous well/formations file; clear them so stale zone + // names are not silently applied as a filter. + m_selectedZones = std::vector(); + } + RimPlot::fieldChangedByUi( changedField, oldValue, newValue ); loadDataAndUpdate(); } @@ -626,6 +696,32 @@ QList RimParameterRftCrossPlot::calculateValueOptions( c options.push_back( caf::PdmOptionItemInfo( rc->caseUserDescription(), rc ) ); } } + else if ( fieldNeedingOptions == &m_wellFormations ) + { + options.push_back( caf::PdmOptionItemInfo( "None", static_cast( nullptr ) ) ); + auto* project = RimProject::current(); + if ( project && project->activeOilField() && project->activeOilField()->wellFormationsCollection() ) + { + for ( RimWellFormationsFile* file : project->activeOilField()->wellFormationsCollection()->wellFormationsFiles() ) + options.push_back( caf::PdmOptionItemInfo( file->shortName(), file ) ); + } + } + else if ( fieldNeedingOptions == &m_filterMode ) + { + using DepthFilterMode = RimRftCrossPlotTools::DepthFilterMode; + for ( auto mode : { DepthFilterMode::NONE, DepthFilterMode::DEPTH_RANGE, DepthFilterMode::ZONES } ) + { + options.push_back( caf::PdmOptionItemInfo( RimRftCrossPlotTools::DepthFilterModeEnum::uiText( mode ), mode ) ); + } + } + else if ( fieldNeedingOptions == &m_selectedZones ) + { + if ( m_wellFormations() ) + { + for ( const QString& zoneName : m_wellFormations->zoneNames( m_wellName() ) ) + options.push_back( caf::PdmOptionItemInfo( zoneName, zoneName ) ); + } + } else if ( fieldNeedingOptions == &m_ensembleParameter ) { if ( m_ensemble() ) @@ -641,9 +737,7 @@ QList RimParameterRftCrossPlot::calculateValueOptions( c m_wellName(), m_selectedTimeStep(), m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), + depthIntervals(), m_depthType() ); } @@ -750,14 +844,16 @@ void RimParameterRftCrossPlot::updatePlotTitle() if ( m_useAutoPlotTitle && m_ensemble() ) { - if ( m_useDepthRange() ) + const QString filterDescription = RimRftCrossPlotTools::depthFilterDescription( useDepthRange(), + m_filterMode(), + m_depthType(), + m_depthRangeMin(), + m_depthRangeMax(), + m_selectedZones() ); + if ( !filterDescription.isEmpty() ) { - m_description = QString( "%1 vs RFT Pressure [%2 %3 - %4 m], %5" ) - .arg( m_ensembleParameter() ) - .arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) - .arg( m_depthRangeMin() ) - .arg( m_depthRangeMax() ) - .arg( m_ensemble->name() ); + m_description = + QString( "%1 vs RFT Pressure [%2], %3" ).arg( m_ensembleParameter() ).arg( filterDescription ).arg( m_ensemble->name() ); } else { diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h index 76a875aec4..18f4968a4c 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h @@ -20,6 +20,7 @@ #include "Appearance/RimFontSizeField.h" #include "RiaDefines.h" #include "RimPlot.h" +#include "RimRftCrossPlotTools.h" #include "cafAppEnum.h" #include "cafPdmField.h" @@ -35,6 +36,7 @@ class RimEclipseResultCase; class RimSummaryEnsemble; class RimSummaryCase; +class RimWellFormationsFile; class RiuQwtPlotWidget; //================================================================================================== @@ -74,16 +76,20 @@ public: void setDepthRange( double minMd, double maxMd ); void setDepthType( RiaDefines::DepthType depthType ); void setEnsembleParameter( const QString& paramName ); + void setWellFormations( RimWellFormationsFile* wellFormationsFile ); - QString ensembleParameter() const; - QString wellName() const; - QDateTime selectedTimeStep() const; - RimSummaryEnsemble* ensemble() const; - RimEclipseResultCase* eclipseCase() const; - bool useDepthRange() const; - double depthRangeMin() const; - double depthRangeMax() const; - RiaDefines::DepthType depthType() const; + QString ensembleParameter() const; + QString wellName() const; + QDateTime selectedTimeStep() const; + RimSummaryEnsemble* ensemble() const; + RimEclipseResultCase* eclipseCase() const; + bool useDepthRange() const; + double depthRangeMin() const; + double depthRangeMax() const; + RiaDefines::DepthType depthType() const; + RimRftCrossPlotTools::DepthFilterMode filterMode() const; + std::vector selectedZones() const; + RimWellFormationsFile* wellFormationsFile() const; RiuQwtPlotWidget* viewer(); @@ -120,17 +126,21 @@ private: void cleanupBeforeClose(); RimSummaryCase* findClosestCase( const QPoint& canvasPos ); + std::vector depthIntervals() const; + private: - caf::PdmPtrField m_ensemble; - caf::PdmField m_wellName; - caf::PdmField m_selectedTimeStep; - caf::PdmPtrField m_eclipseCase; - caf::PdmField m_useDepthRange; - caf::PdmField m_depthRangeMin; - caf::PdmField m_depthRangeMax; - caf::PdmField> m_depthType; - caf::PdmField m_samplingMode; - caf::PdmField m_ensembleParameter; + caf::PdmPtrField m_ensemble; + caf::PdmField m_wellName; + caf::PdmField m_selectedTimeStep; + caf::PdmPtrField m_eclipseCase; + caf::PdmField m_filterMode; + caf::PdmField m_depthRangeMin; + caf::PdmField m_depthRangeMax; + caf::PdmPtrField m_wellFormations; + caf::PdmField> m_selectedZones; + caf::PdmField> m_depthType; + caf::PdmField m_samplingMode; + caf::PdmField m_ensembleParameter; caf::PdmField m_useAutoPlotTitle; caf::PdmField m_description; diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.cpp b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.cpp index 938545cad9..86d0d631b1 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.cpp @@ -23,6 +23,8 @@ #include "RimWellLogTrack.h" #include "RimWellRftPlot.h" +#include "Formations/RimWellFormationsFile.h" + #include "RiuInterfaceToViewWindow.h" #include "RiuPlotWidget.h" #include "RiuQwtPlotWidget.h" @@ -528,6 +530,8 @@ void RimRftCorrelationReportPlot::syncTornadoInputsFromCrossPlot() { if ( !m_tornadoPlot() || !m_parameterRftCrossPlot() ) return; + applyWellFormationsToSubPlots(); + m_tornadoPlot->setEnsemble( m_parameterRftCrossPlot->ensemble() ); m_tornadoPlot->setWellName( m_parameterRftCrossPlot->wellName() ); m_tornadoPlot->setTimeStep( m_parameterRftCrossPlot->selectedTimeStep() ); @@ -536,6 +540,22 @@ void RimRftCorrelationReportPlot::syncTornadoInputsFromCrossPlot() m_tornadoPlot->setUseDepthRange( m_parameterRftCrossPlot->useDepthRange() ); m_tornadoPlot->setDepthRange( m_parameterRftCrossPlot->depthRangeMin(), m_parameterRftCrossPlot->depthRangeMax() ); m_tornadoPlot->setDepthType( m_parameterRftCrossPlot->depthType() ); + m_tornadoPlot->setFilterMode( m_parameterRftCrossPlot->filterMode() ); + m_tornadoPlot->setSelectedZones( m_parameterRftCrossPlot->selectedZones() ); +} + +//-------------------------------------------------------------------------------------------------- +/// Pushes the user's manual "Well Formations File" selection in the cross plot down to the sub +/// plots so their "Filter By: Zones" option becomes available. +//-------------------------------------------------------------------------------------------------- +void RimRftCorrelationReportPlot::applyWellFormationsToSubPlots() +{ + if ( !m_parameterRftCrossPlot() ) return; + + RimWellFormationsFile* wellFormationsFile = m_parameterRftCrossPlot->wellFormationsFile(); + + m_parameterRftCrossPlot->setWellFormations( wellFormationsFile ); + if ( m_tornadoPlot() ) m_tornadoPlot->setWellFormations( wellFormationsFile ); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.h b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.h index c162a11fb8..862ef9ccef 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.h +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCorrelationReportPlot.h @@ -90,6 +90,7 @@ private: void onTornadoParameterSelected( const QString& paramName ); void syncTornadoInputsFromCrossPlot(); void applyDepthTypeToSubPlots(); + void applyWellFormationsToSubPlots(); private: caf::PdmProxyValueField m_name; diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp index a6dcf47566..9fc0cf9f68 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp @@ -23,6 +23,9 @@ #include "RiaExtractionTools.h" #include "Well/RigEclipseWellLogExtractor.h" +#include "Well/RigWellPathFormations.h" + +#include "Formations/RimWellFormationsFile.h" #include "RimEclipseResultCase.h" #include "RimProject.h" @@ -30,9 +33,22 @@ #include "RimSummaryEnsemble.h" #include "RimWellPath.h" +#include #include #include +namespace caf +{ +template <> +void caf::AppEnum::setUp() +{ + addItem( RimRftCrossPlotTools::DepthFilterMode::NONE, "NONE", "None" ); + addItem( RimRftCrossPlotTools::DepthFilterMode::DEPTH_RANGE, "DEPTH_RANGE", "Depth Range" ); + addItem( RimRftCrossPlotTools::DepthFilterMode::ZONES, "ZONES", "Formation" ); + setDefault( RimRftCrossPlotTools::DepthFilterMode::NONE ); +} +} // namespace caf + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -76,7 +92,59 @@ std::vector RimRftCrossPlotTools::filterPressuresByDepthRange( const std double depthRangeMin, double depthRangeMax ) { - if ( !useDepthRange ) return pressures; + std::vector intervals; + if ( useDepthRange ) intervals.push_back( { depthRangeMin, depthRangeMax } ); + + return filterPressuresByDepthIntervals( depths, pressures, intervals ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimRftCrossPlotTools::buildDepthIntervals( bool useFilter, + DepthFilterMode mode, + double depthRangeMin, + double depthRangeMax, + RimWellFormationsFile* wellFormationsFile, + const QString& wellName, + const std::vector& selectedZones, + RiaDefines::DepthType depthType ) +{ + if ( !useFilter || mode == DepthFilterMode::NONE ) return {}; + + if ( mode == DepthFilterMode::DEPTH_RANGE ) return { DepthInterval{ depthRangeMin, depthRangeMax } }; + + // ZONES mode + if ( !wellFormationsFile || selectedZones.empty() ) return {}; + + std::optional formations = wellFormationsFile->formationsForWell( wellName ); + if ( !formations.has_value() ) return {}; + + std::vector intervals; + for ( size_t i = 0; i < formations->formationCount(); ++i ) + { + const RigWellPathFormation& formation = formations->formationAt( i ); + + bool isSelected = std::find( selectedZones.begin(), selectedZones.end(), formation.formationName ) != selectedZones.end(); + if ( !isSelected ) continue; + + if ( depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ) + intervals.push_back( { formation.tvdTop, formation.tvdBase } ); + else + intervals.push_back( { formation.mdTop, formation.mdBase } ); + } + + return intervals; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimRftCrossPlotTools::filterPressuresByDepthIntervals( const std::vector& depths, + const std::vector& pressures, + const std::vector& depthIntervals ) +{ + if ( depthIntervals.empty() ) return pressures; // Depth filter requested but no aligned depth data is available; exclude rather than // silently return an unfiltered set of samples. @@ -84,11 +152,45 @@ std::vector RimRftCrossPlotTools::filterPressuresByDepthRange( const std std::vector samplesInRange; for ( size_t i = 0; i < depths.size(); ++i ) - if ( depths[i] >= depthRangeMin && depths[i] <= depthRangeMax ) samplesInRange.push_back( pressures[i] ); + { + for ( const DepthInterval& interval : depthIntervals ) + { + if ( depths[i] >= interval.top && depths[i] <= interval.base ) + { + samplesInRange.push_back( pressures[i] ); + break; + } + } + } return samplesInRange; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimRftCrossPlotTools::depthFilterDescription( bool useFilter, + DepthFilterMode mode, + RiaDefines::DepthType depthType, + double depthRangeMin, + double depthRangeMax, + const std::vector& selectedZones ) +{ + if ( !useFilter || mode == DepthFilterMode::NONE ) return {}; + + if ( mode == DepthFilterMode::ZONES ) + { + if ( selectedZones.empty() ) return {}; + + QStringList zoneList; + for ( const QString& zone : selectedZones ) + zoneList << zone; + return QString( "Zones: %1" ).arg( zoneList.join( ", " ) ); + } + + return QString( "%1 %2 - %3 m" ).arg( depthTypeAbbreviation( depthType ) ).arg( depthRangeMin ).arg( depthRangeMax ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -102,14 +204,12 @@ double RimRftCrossPlotTools::computeMean( const std::vector& samples ) //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::vector> RimRftCrossPlotTools::computePressureSamplesPerCase( RimSummaryEnsemble* ensemble, - const QString& wellName, - const QDateTime& timeStep, - RimEclipseResultCase* eclipseCase, - bool useDepthRange, - double depthRangeMin, - double depthRangeMax, - RiaDefines::DepthType depthType ) +std::vector> RimRftCrossPlotTools::computePressureSamplesPerCase( RimSummaryEnsemble* ensemble, + const QString& wellName, + const QDateTime& timeStep, + RimEclipseResultCase* eclipseCase, + const std::vector& depthIntervals, + RiaDefines::DepthType depthType ) { if ( !ensemble || wellName.isEmpty() || !timeStep.isValid() ) return {}; @@ -154,7 +254,7 @@ std::vector> RimRftCrossPlotTools::computePressureSamplesPer // operates on values consistent with what the user sees in the RFT plot. std::vector depths = rftCurveDepthValues( reader, wellName, timeStep, extractor, depthType ); - samplesPerCase.push_back( filterPressuresByDepthRange( depths, pressures, useDepthRange, depthRangeMin, depthRangeMax ) ); + samplesPerCase.push_back( filterPressuresByDepthIntervals( depths, pressures, depthIntervals ) ); } return samplesPerCase; @@ -163,17 +263,15 @@ std::vector> RimRftCrossPlotTools::computePressureSamplesPer //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::vector RimRftCrossPlotTools::computeMeanPressurePerCase( RimSummaryEnsemble* ensemble, - const QString& wellName, - const QDateTime& timeStep, - RimEclipseResultCase* eclipseCase, - bool useDepthRange, - double depthRangeMin, - double depthRangeMax, - RiaDefines::DepthType depthType ) +std::vector RimRftCrossPlotTools::computeMeanPressurePerCase( RimSummaryEnsemble* ensemble, + const QString& wellName, + const QDateTime& timeStep, + RimEclipseResultCase* eclipseCase, + const std::vector& depthIntervals, + RiaDefines::DepthType depthType ) { const std::vector> samplesPerCase = - computePressureSamplesPerCase( ensemble, wellName, timeStep, eclipseCase, useDepthRange, depthRangeMin, depthRangeMax, depthType ); + computePressureSamplesPerCase( ensemble, wellName, timeStep, eclipseCase, depthIntervals, depthType ); std::vector pressurePerCase; pressurePerCase.reserve( samplesPerCase.size() ); diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h index 772488087b..828dc078d2 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h @@ -19,8 +19,11 @@ #include "RiaDefines.h" +#include "cafAppEnum.h" + #include #include +#include #include @@ -28,6 +31,7 @@ class RifReaderRftInterface; class RigEclipseWellLogExtractor; class RimEclipseResultCase; class RimSummaryEnsemble; +class RimWellFormationsFile; //================================================================================================== /// @@ -37,6 +41,23 @@ class RimSummaryEnsemble; //================================================================================================== namespace RimRftCrossPlotTools { +// How the active depth filter selects samples: a single depth range, or a set of named zones +// looked up in a well formations file. +enum class DepthFilterMode +{ + NONE, + DEPTH_RANGE, + ZONES +}; +using DepthFilterModeEnum = caf::AppEnum; + +// A single inclusive depth interval, in the unit/datum of whichever depth type it was built for. +struct DepthInterval +{ + double top = 0.0; + double base = 0.0; +}; + // Short abbreviation used in axis titles and plot titles for the active depth type. QString depthTypeAbbreviation( RiaDefines::DepthType depthType ); @@ -61,28 +82,53 @@ std::vector filterPressuresByDepthRange( const std::vector& dept double depthRangeMin, double depthRangeMax ); +// Builds the active depth filter intervals from either a depth range or a set of selected zones. +// Returns an empty vector (meaning "no filter") when useFilter is false, or when ZONES mode is +// selected but no well formations file, well name, or matching zones are available. +std::vector buildDepthIntervals( bool useFilter, + DepthFilterMode mode, + double depthRangeMin, + double depthRangeMax, + RimWellFormationsFile* wellFormationsFile, + const QString& wellName, + const std::vector& selectedZones, + RiaDefines::DepthType depthType ); + +// Returns the subset of pressures whose corresponding depth lies within any of depthIntervals +// (inclusive). An empty depthIntervals list means "no filter", returning pressures unchanged; a +// size mismatch between depths/pressures returns an empty vector (no aligned depth data). +std::vector filterPressuresByDepthIntervals( const std::vector& depths, + const std::vector& pressures, + const std::vector& depthIntervals ); + +// Short description of the active depth filter for titles/axis labels/group text, e.g. +// "MD 1000 - 2000 m" or "Zones: Valysar, Therys". Empty when filtering is disabled. +QString depthFilterDescription( bool useFilter, + DepthFilterMode mode, + RiaDefines::DepthType depthType, + double depthRangeMin, + double depthRangeMax, + const std::vector& selectedZones ); + // Arithmetic mean of the given samples, or infinity if samples is empty. double computeMean( const std::vector& samples ); -// Computes the individual RFT pressure samples within the depth range per ensemble case. -// Indices match ensemble->allSummaryCases(). A case with no data gets an empty vector. -std::vector> computePressureSamplesPerCase( RimSummaryEnsemble* ensemble, - const QString& wellName, - const QDateTime& timeStep, - RimEclipseResultCase* eclipseCase, - bool useDepthRange, - double depthRangeMin, - double depthRangeMax, +// Computes the individual RFT pressure samples within depthIntervals per ensemble case. +// Indices match ensemble->allSummaryCases(). A case with no data gets an empty vector. An empty +// depthIntervals list means no filtering. +std::vector> computePressureSamplesPerCase( RimSummaryEnsemble* ensemble, + const QString& wellName, + const QDateTime& timeStep, + RimEclipseResultCase* eclipseCase, + const std::vector& depthIntervals, RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH ); // Computes mean RFT pressure per ensemble case (one entry per case, infinity = no data). // Indices match ensemble->allSummaryCases(). -std::vector computeMeanPressurePerCase( RimSummaryEnsemble* ensemble, - const QString& wellName, - const QDateTime& timeStep, - RimEclipseResultCase* eclipseCase, - bool useDepthRange, - double depthRangeMin, - double depthRangeMax, - RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH ); +std::vector computeMeanPressurePerCase( RimSummaryEnsemble* ensemble, + const QString& wellName, + const QDateTime& timeStep, + RimEclipseResultCase* eclipseCase, + const std::vector& depthIntervals, + RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH ); } // namespace RimRftCrossPlotTools diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp index d5505a435d..4e45c97d5d 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp @@ -30,6 +30,8 @@ #include "RimSummaryEnsemble.h" #include "RimSummaryEnsembleTools.h" +#include "Formations/RimWellFormationsFile.h" + #include "RiuContextMenuLauncher.h" #include "RiuGroupedBarChartBuilder.h" #include "RiuPlotItem.h" @@ -60,8 +62,14 @@ RimRftTornadoPlot::RimRftTornadoPlot() CAF_PDM_InitFieldNoDefault( &m_selectedTimeStep, "TimeStep", "Time Step" ); CAF_PDM_InitFieldNoDefault( &m_eclipseCase, "EclipseCase", "Eclipse Case (MD fallback)" ); CAF_PDM_InitField( &m_useDepthRange, "UseDepthRange", false, "Filter by Depth Range" ); + CAF_PDM_InitField( &m_filterMode, + "FilterMode", + RimRftCrossPlotTools::DepthFilterModeEnum( RimRftCrossPlotTools::DepthFilterMode::DEPTH_RANGE ), + "Filter By" ); CAF_PDM_InitField( &m_depthRangeMin, "DepthRangeMin", 0.0, "Min Depth" ); CAF_PDM_InitField( &m_depthRangeMax, "DepthRangeMax", 5000.0, "Max Depth" ); + CAF_PDM_InitFieldNoDefault( &m_wellFormations, "WellFormations", "Well Formations File" ); + CAF_PDM_InitFieldNoDefault( &m_selectedZones, "SelectedZones", "Zones" ); CAF_PDM_InitField( &m_depthType, "DepthType", caf::AppEnum( RiaDefines::DepthType::MEASURED_DEPTH ), "Depth Type" ); m_depthType.uiCapability()->setUiHidden( true ); // driven by the parent RimRftCorrelationReportPlot @@ -155,6 +163,30 @@ void RimRftTornadoPlot::setDepthType( RiaDefines::DepthType depthType ) m_depthType = depthType; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimRftTornadoPlot::setFilterMode( RimRftCrossPlotTools::DepthFilterMode filterMode ) +{ + m_filterMode = filterMode; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimRftTornadoPlot::setWellFormations( RimWellFormationsFile* wellFormationsFile ) +{ + m_wellFormations = wellFormationsFile; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimRftTornadoPlot::setSelectedZones( const std::vector& zones ) +{ + m_selectedZones = zones; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -360,6 +392,21 @@ void RimRftTornadoPlot::onPlotItemSelected( std::shared_ptr plotIte if ( !paramName.isEmpty() && m_parameterSelectedCallback ) m_parameterSelectedCallback( paramName ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimRftTornadoPlot::depthIntervals() const +{ + return RimRftCrossPlotTools::buildDepthIntervals( m_useDepthRange(), + m_filterMode(), + m_depthRangeMin(), + m_depthRangeMax(), + m_wellFormations(), + m_wellName(), + m_selectedZones(), + m_depthType() ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -376,9 +423,7 @@ std::map RimRftTornadoPlot::addDataToChartBuilder( RiuGroupedBa m_wellName(), m_selectedTimeStep(), m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), + depthIntervals(), m_depthType() ); // For each numeric parameter, compute Pearson correlation against pressurePerCase @@ -421,12 +466,14 @@ void RimRftTornadoPlot::updatePlotTitle() if ( m_useAutoPlotTitle() && m_ensemble() ) { - const QString rangeStr = m_useDepthRange() ? QString( " [%1 %2 - %3 m]" ) - .arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) - .arg( m_depthRangeMin() ) - .arg( m_depthRangeMax() ) - : QString(); - m_description = QString( "Parameter Correlation vs RFT Pressure%1, %2" ).arg( rangeStr ).arg( m_ensemble->name() ); + const QString filterDescription = RimRftCrossPlotTools::depthFilterDescription( m_useDepthRange(), + m_filterMode(), + m_depthType(), + m_depthRangeMin(), + m_depthRangeMax(), + m_selectedZones() ); + const QString rangeStr = filterDescription.isEmpty() ? QString() : QString( " [%1]" ).arg( filterDescription ); + m_description = QString( "Parameter Correlation vs RFT Pressure%1, %2" ).arg( rangeStr ).arg( m_ensemble->name() ); } m_plotWidget->setPlotTitle( m_description() ); diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.h b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.h index a8c354e5e9..b72fd8d360 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.h +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.h @@ -21,6 +21,7 @@ #include "Appearance/RimFontSizeField.h" #include "RiaDefines.h" +#include "RimRftCrossPlotTools.h" #include "cafAppEnum.h" #include "cafPdmField.h" @@ -35,6 +36,7 @@ class RimEclipseResultCase; class RimSummaryEnsemble; +class RimWellFormationsFile; class RiuQwtPlotWidget; //================================================================================================== @@ -63,6 +65,9 @@ public: void setUseDepthRange( bool useDepthRange ); void setDepthRange( double minMd, double maxMd ); void setDepthType( RiaDefines::DepthType depthType ); + void setFilterMode( RimRftCrossPlotTools::DepthFilterMode filterMode ); + void setWellFormations( RimWellFormationsFile* wellFormationsFile ); + void setSelectedZones( const std::vector& zones ); void setSelectedParameter( const QString& paramName ); RiuQwtPlotWidget* viewer(); @@ -94,16 +99,21 @@ private: void updatePlotTitle(); void cleanupBeforeClose(); + std::vector depthIntervals() const; + private: // Data source inputs - caf::PdmPtrField m_ensemble; - caf::PdmField m_wellName; - caf::PdmField m_selectedTimeStep; - caf::PdmPtrField m_eclipseCase; - caf::PdmField m_useDepthRange; - caf::PdmField m_depthRangeMin; - caf::PdmField m_depthRangeMax; - caf::PdmField> m_depthType; + caf::PdmPtrField m_ensemble; + caf::PdmField m_wellName; + caf::PdmField m_selectedTimeStep; + caf::PdmPtrField m_eclipseCase; + caf::PdmField m_useDepthRange; + caf::PdmField m_filterMode; + caf::PdmField m_depthRangeMin; + caf::PdmField m_depthRangeMax; + caf::PdmPtrField m_wellFormations; + caf::PdmField> m_selectedZones; + caf::PdmField> m_depthType; // Tornado settings caf::PdmField m_showAbsoluteValues; diff --git a/ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp b/ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp index 8d8abcdb06..e44670c505 100644 --- a/ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp +++ b/ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp @@ -103,3 +103,109 @@ TEST( RimRftCrossPlotToolsTest, DepthTypeAbbreviation ) EXPECT_EQ( QString( "MD" ), RimRftCrossPlotTools::depthTypeAbbreviation( RiaDefines::DepthType::MEASURED_DEPTH ) ); EXPECT_EQ( QString( "TVD" ), RimRftCrossPlotTools::depthTypeAbbreviation( RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ) ); } + +//-------------------------------------------------------------------------------------------------- +/// Disabled filtering returns no intervals regardless of mode. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, BuildDepthIntervals_DisabledReturnsEmpty ) +{ + auto intervals = RimRftCrossPlotTools::buildDepthIntervals( false, + RimRftCrossPlotTools::DepthFilterMode::DEPTH_RANGE, + 1000.0, + 2000.0, + nullptr, + QString(), + {}, + RiaDefines::DepthType::MEASURED_DEPTH ); + EXPECT_TRUE( intervals.empty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// Depth range mode produces a single interval matching the requested range. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, BuildDepthIntervals_DepthRangeProducesSingleInterval ) +{ + auto intervals = RimRftCrossPlotTools::buildDepthIntervals( true, + RimRftCrossPlotTools::DepthFilterMode::DEPTH_RANGE, + 1000.0, + 2000.0, + nullptr, + QString(), + {}, + RiaDefines::DepthType::MEASURED_DEPTH ); + ASSERT_EQ( size_t( 1 ), intervals.size() ); + EXPECT_DOUBLE_EQ( 1000.0, intervals[0].top ); + EXPECT_DOUBLE_EQ( 2000.0, intervals[0].base ); +} + +//-------------------------------------------------------------------------------------------------- +/// Zones mode with no well formations file available produces no intervals. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, BuildDepthIntervals_ZonesWithoutFileReturnsEmpty ) +{ + auto intervals = RimRftCrossPlotTools::buildDepthIntervals( true, + RimRftCrossPlotTools::DepthFilterMode::ZONES, + 1000.0, + 2000.0, + nullptr, + "WELL-A", + { "ZoneA" }, + RiaDefines::DepthType::MEASURED_DEPTH ); + EXPECT_TRUE( intervals.empty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// An empty interval list means no filtering, so all pressures pass through. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, FilterPressuresByDepthIntervals_EmptyIntervalsReturnsAllSamples ) +{ + std::vector depths{ 1000.0, 2000.0, 3000.0 }; + std::vector pressures{ 10.0, 20.0, 30.0 }; + + auto filtered = RimRftCrossPlotTools::filterPressuresByDepthIntervals( depths, pressures, {} ); + + EXPECT_EQ( pressures, filtered ); +} + +//-------------------------------------------------------------------------------------------------- +/// Only samples within any of the given (possibly non-contiguous) intervals are returned. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, FilterPressuresByDepthIntervals_FiltersByMultipleIntervals ) +{ + std::vector depths{ 1000.0, 1500.0, 2000.0, 2500.0, 3000.0 }; + std::vector pressures{ 10.0, 15.0, 20.0, 25.0, 30.0 }; + + std::vector intervals{ { 1000.0, 1500.0 }, { 2800.0, 3200.0 } }; + auto filtered = RimRftCrossPlotTools::filterPressuresByDepthIntervals( depths, pressures, intervals ); + + std::vector expected{ 10.0, 15.0, 30.0 }; + EXPECT_EQ( expected, filtered ); +} + +//-------------------------------------------------------------------------------------------------- +/// A filter description is empty when filtering is disabled. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, DepthFilterDescription_DisabledReturnsEmpty ) +{ + QString description = RimRftCrossPlotTools::depthFilterDescription( false, + RimRftCrossPlotTools::DepthFilterMode::DEPTH_RANGE, + RiaDefines::DepthType::MEASURED_DEPTH, + 1000.0, + 2000.0, + {} ); + EXPECT_TRUE( description.isEmpty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// Zone mode description lists the selected zone names. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, DepthFilterDescription_ZonesListsSelectedZoneNames ) +{ + QString description = RimRftCrossPlotTools::depthFilterDescription( true, + RimRftCrossPlotTools::DepthFilterMode::ZONES, + RiaDefines::DepthType::MEASURED_DEPTH, + 1000.0, + 2000.0, + { "Valysar", "Therys" } ); + EXPECT_EQ( QString( "Zones: Valysar, Therys" ), description ); +}