From b35b481ff12200a05bab9efc376b776509e00ac5 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Tue, 6 Oct 2026 09:58:07 +0200 Subject: [PATCH] Extract RFT cross plot static helpers into RimRftCrossPlotTools, add unit tests - Move depthTypeAbbreviation(), computePressureSamplesPerCase() and computeMeanPressurePerCase() out of RimParameterRftCrossPlot into a new standalone RimRftCrossPlotTools namespace, shared by RimParameterRftCrossPlot and RimRftTornadoPlot (removing duplicated code in the latter). - Extract pure, reader-independent helpers filterPressuresByDepthRange() and computeMean() so the depth filtering and averaging logic can be unit tested without RFT reader/ensemble mocks. - Add RimRftCrossPlotTools-Test.cpp with gtest cases covering depth range filtering (disabled, inclusive bounds, out-of-range, mismatched sizes) and mean computation (empty/normal), plus depth type abbreviation. Move RimWellLogRftCurve::rftCurveDepthValues into RimRftCrossPlotTools This static helper's only caller was already RimRftCrossPlotTools::computePressureSamplesPerCase, so move it alongside the other RFT cross plot/tornado plot helpers for consistency. --- .../CorrelationPlots/CMakeLists_files.cmake | 1 + .../RimParameterRftCrossPlot.cpp | 182 +++-------------- .../RimParameterRftCrossPlot.h | 23 --- .../CorrelationPlots/RimRftCrossPlotTools.cpp | 185 ++++++++++++++++++ .../CorrelationPlots/RimRftCrossPlotTools.h | 88 +++++++++ .../CorrelationPlots/RimRftTornadoPlot.cpp | 40 ++-- .../WellLog/RimWellLogRftCurve.cpp | 26 --- .../WellLog/RimWellLogRftCurve.h | 12 -- ApplicationLibCode/UnitTests/CMakeLists.txt | 1 + .../UnitTests/RimRftCrossPlotTools-Test.cpp | 105 ++++++++++ 10 files changed, 424 insertions(+), 239 deletions(-) create mode 100644 ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h create mode 100644 ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/CMakeLists_files.cmake index 1ab1036b48..cd057f249e 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/CMakeLists_files.cmake @@ -9,6 +9,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimRftCorrelationReportPlot.cpp ${CMAKE_CURRENT_LIST_DIR}/RimRftTornadoPlot.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCorrelationBarChartTools.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimRftCrossPlotTools.cpp ) list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES}) diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp index a9381fb81a..b511aadfe3 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.cpp @@ -21,24 +21,18 @@ #include "RiaColorTables.h" #include "RiaPreferences.h" -#include "RifEclipseRftAddress.h" #include "RifReaderRftInterface.h" #include "RigEnsembleParameter.h" #include "RigStatisticsTools.h" -#include "RiaExtractionTools.h" - -#include "Well/RigEclipseWellLogExtractor.h" - #include "RimEclipseCase.h" #include "RimEclipseResultCase.h" #include "RimProject.h" +#include "RimRftCrossPlotTools.h" #include "RimSummaryCase.h" #include "RimSummaryEnsemble.h" #include "RimSummaryEnsembleTools.h" -#include "RimWellLogRftCurve.h" -#include "RimWellPath.h" #include "RiuContextMenuLauncher.h" #include "RiuDockWidgetTools.h" @@ -66,17 +60,6 @@ #include #include -namespace -{ -//-------------------------------------------------------------------------------------------------- -/// Short abbreviation used in axis titles and plot titles for the active depth type. -//-------------------------------------------------------------------------------------------------- -QString depthTypeAbbreviation( RiaDefines::DepthType depthType ) -{ - return depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ? "TVD" : "MD"; -} -} // namespace - namespace caf { template <> @@ -261,114 +244,6 @@ RiuQwtPlotWidget* RimParameterRftCrossPlot::viewer() return m_plotWidget; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector> RimParameterRftCrossPlot::computePressureSamplesPerCase( RimSummaryEnsemble* ensemble, - const QString& wellName, - const QDateTime& timeStep, - RimEclipseResultCase* eclipseCase, - bool useDepthRange, - double depthRangeMin, - double depthRangeMax, - RiaDefines::DepthType depthType ) -{ - if ( !ensemble || wellName.isEmpty() || !timeStep.isValid() ) return {}; - - RigEclipseWellLogExtractor* extractor = nullptr; - if ( eclipseCase ) - { - RimWellPath* wellPath = RimProject::current()->wellPathFromSimWellName( wellName ); - extractor = RiaExtractionTools::findOrCreateWellLogExtractor( wellPath, eclipseCase ); - if ( !extractor ) extractor = RiaExtractionTools::findOrCreateSimWellExtractor( eclipseCase, wellName, false, 0 ); - } - - const auto& allCases = ensemble->allSummaryCases(); - - std::vector> samplesPerCase; - samplesPerCase.reserve( allCases.size() ); - - for ( RimSummaryCase* summaryCase : allCases ) - { - if ( !summaryCase ) - { - samplesPerCase.push_back( {} ); - continue; - } - - RifReaderRftInterface* reader = summaryCase->rftReader(); - if ( !reader ) - { - samplesPerCase.push_back( {} ); - continue; - } - - auto pressureAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::PRESSURE ); - std::vector pressures; - reader->values( pressureAddress, &pressures ); - if ( pressures.empty() ) - { - samplesPerCase.push_back( {} ); - continue; - } - - // Use the same depth values the RFT curves use for their depth axis, so the filter - // operates on values consistent with what the user sees in the RFT plot. - std::vector depths = RimWellLogRftCurve::rftCurveDepthValues( reader, wellName, timeStep, extractor, depthType ); - - std::vector samplesInRange; - if ( useDepthRange ) - { - if ( depths.size() != pressures.size() ) - { - // Depth filter requested but no aligned depth data is available for this case; - // exclude rather than silently return an unfiltered set of samples. - samplesPerCase.push_back( {} ); - continue; - } - for ( size_t i = 0; i < depths.size(); ++i ) - if ( depths[i] >= depthRangeMin && depths[i] <= depthRangeMax ) samplesInRange.push_back( pressures[i] ); - } - else - { - samplesInRange = pressures; - } - - samplesPerCase.push_back( samplesInRange ); - } - - return samplesPerCase; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector RimParameterRftCrossPlot::computeMeanPressurePerCase( RimSummaryEnsemble* ensemble, - const QString& wellName, - const QDateTime& timeStep, - RimEclipseResultCase* eclipseCase, - bool useDepthRange, - double depthRangeMin, - double depthRangeMax, - RiaDefines::DepthType depthType ) -{ - const std::vector> samplesPerCase = - computePressureSamplesPerCase( ensemble, wellName, timeStep, eclipseCase, useDepthRange, depthRangeMin, depthRangeMax, depthType ); - - std::vector pressurePerCase; - pressurePerCase.reserve( samplesPerCase.size() ); - - for ( const std::vector& samples : samplesPerCase ) - { - if ( samples.empty() ) - pressurePerCase.push_back( std::numeric_limits::infinity() ); - else - pressurePerCase.push_back( std::accumulate( samples.begin(), samples.end(), 0.0 ) / samples.size() ); - } - - return pressurePerCase; -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -389,14 +264,14 @@ std::vector RimParameterRftCrossPlot::create if ( m_samplingMode() == SamplingMode::ALL_SAMPLES ) { - const std::vector> samplesPerCase = computePressureSamplesPerCase( m_ensemble(), - m_wellName(), - m_selectedTimeStep(), - m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), - m_depthType() ); + const std::vector> samplesPerCase = RimRftCrossPlotTools::computePressureSamplesPerCase( m_ensemble(), + m_wellName(), + m_selectedTimeStep(), + m_eclipseCase(), + m_useDepthRange(), + m_depthRangeMin(), + m_depthRangeMax(), + m_depthType() ); if ( samplesPerCase.size() != allCases.size() ) return {}; @@ -413,14 +288,14 @@ std::vector RimParameterRftCrossPlot::create } else { - const std::vector pressurePerCase = computeMeanPressurePerCase( m_ensemble(), - m_wellName(), - m_selectedTimeStep(), - m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), - m_depthType() ); + const std::vector pressurePerCase = RimRftCrossPlotTools::computeMeanPressurePerCase( m_ensemble(), + m_wellName(), + m_selectedTimeStep(), + m_eclipseCase(), + m_useDepthRange(), + m_depthRangeMin(), + m_depthRangeMax(), + m_depthType() ); if ( pressurePerCase.size() != allCases.size() ) return {}; @@ -461,7 +336,7 @@ void RimParameterRftCrossPlot::updateAxes() const QString pressureLabel = m_samplingMode() == SamplingMode::ALL_SAMPLES ? "Pressure" : "Mean Pressure"; const QString depthLabel = m_useDepthRange() ? QString( "%1 [%2 %3 - %4]" ) .arg( pressureLabel ) - .arg( depthTypeAbbreviation( m_depthType() ) ) + .arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) .arg( m_depthRangeMin() ) .arg( m_depthRangeMax() ) : pressureLabel; @@ -641,7 +516,8 @@ void RimParameterRftCrossPlot::defineUiOrdering( QString uiConfigName, caf::PdmU dataGroup->add( &m_selectedTimeStep ); dataGroup->add( &m_eclipseCase ); - auto* depthGroup = uiOrdering.addNewGroup( QString( "Depth Range (%1)" ).arg( depthTypeAbbreviation( m_depthType() ) ) ); + auto* depthGroup = + uiOrdering.addNewGroup( QString( "Depth Range (%1)" ).arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) ); depthGroup->add( &m_useDepthRange ); depthGroup->add( &m_depthRangeMin ); depthGroup->add( &m_depthRangeMax ); @@ -761,14 +637,14 @@ QList RimParameterRftCrossPlot::calculateValueOptions( c std::vector pressurePerCase; if ( canComputeCorrelation ) { - pressurePerCase = computeMeanPressurePerCase( m_ensemble(), - m_wellName(), - m_selectedTimeStep(), - m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), - m_depthType() ); + pressurePerCase = RimRftCrossPlotTools::computeMeanPressurePerCase( m_ensemble(), + m_wellName(), + m_selectedTimeStep(), + m_eclipseCase(), + m_useDepthRange(), + m_depthRangeMin(), + m_depthRangeMax(), + m_depthType() ); } // Compute correlation for each numeric parameter, then sort by abs value descending @@ -878,7 +754,7 @@ void RimParameterRftCrossPlot::updatePlotTitle() { m_description = QString( "%1 vs RFT Pressure [%2 %3 - %4 m], %5" ) .arg( m_ensembleParameter() ) - .arg( depthTypeAbbreviation( m_depthType() ) ) + .arg( RimRftCrossPlotTools::depthTypeAbbreviation( m_depthType() ) ) .arg( m_depthRangeMin() ) .arg( m_depthRangeMax() ) .arg( m_ensemble->name() ); diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h index bf5e3ff476..76a875aec4 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimParameterRftCrossPlot.h @@ -89,29 +89,6 @@ public: std::vector createCaseData() const; - // Computes mean RFT pressure per ensemble case (one entry per case, infinity = no data). - // Indices match ensemble->allSummaryCases(). - static 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 ); - - // 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. - static std::vector> - computePressureSamplesPerCase( RimSummaryEnsemble* ensemble, - const QString& wellName, - const QDateTime& timeStep, - RimEclipseResultCase* eclipseCase, - bool useDepthRange, - double depthRangeMin, - double depthRangeMax, - RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH ); - // RimPlot pure virtual overrides RiuPlotWidget* plotWidget() override; void setAutoScaleXEnabled( bool ) override {} diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp new file mode 100644 index 0000000000..a6dcf47566 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.cpp @@ -0,0 +1,185 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimRftCrossPlotTools.h" + +#include "RifEclipseRftAddress.h" +#include "RifReaderRftInterface.h" + +#include "RiaExtractionTools.h" + +#include "Well/RigEclipseWellLogExtractor.h" + +#include "RimEclipseResultCase.h" +#include "RimProject.h" +#include "RimSummaryCase.h" +#include "RimSummaryEnsemble.h" +#include "RimWellPath.h" + +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimRftCrossPlotTools::depthTypeAbbreviation( RiaDefines::DepthType depthType ) +{ + return depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ? "TVD" : "MD"; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimRftCrossPlotTools::rftCurveDepthValues( RifReaderRftInterface* reader, + const QString& wellName, + const QDateTime& timeStep, + RigEclipseWellLogExtractor* extractor, + RiaDefines::DepthType depthType ) +{ + if ( !reader ) return {}; + + if ( depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ) + { + auto tvdAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::TVD ); + std::vector tvdDepths; + reader->values( tvdAddress, &tvdDepths ); + return tvdDepths; + } + + auto mdAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::MD ); + std::vector depths; + reader->values( mdAddress, &depths ); + if ( depths.empty() && extractor ) depths = reader->computeMeasuredDepth( wellName, timeStep, extractor ); + return depths; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimRftCrossPlotTools::filterPressuresByDepthRange( const std::vector& depths, + const std::vector& pressures, + bool useDepthRange, + double depthRangeMin, + double depthRangeMax ) +{ + if ( !useDepthRange ) return pressures; + + // Depth filter requested but no aligned depth data is available; exclude rather than + // silently return an unfiltered set of samples. + if ( depths.size() != pressures.size() ) return {}; + + std::vector samplesInRange; + for ( size_t i = 0; i < depths.size(); ++i ) + if ( depths[i] >= depthRangeMin && depths[i] <= depthRangeMax ) samplesInRange.push_back( pressures[i] ); + + return samplesInRange; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +double RimRftCrossPlotTools::computeMean( const std::vector& samples ) +{ + if ( samples.empty() ) return std::numeric_limits::infinity(); + + return std::accumulate( samples.begin(), samples.end(), 0.0 ) / samples.size(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector> RimRftCrossPlotTools::computePressureSamplesPerCase( RimSummaryEnsemble* ensemble, + const QString& wellName, + const QDateTime& timeStep, + RimEclipseResultCase* eclipseCase, + bool useDepthRange, + double depthRangeMin, + double depthRangeMax, + RiaDefines::DepthType depthType ) +{ + if ( !ensemble || wellName.isEmpty() || !timeStep.isValid() ) return {}; + + RigEclipseWellLogExtractor* extractor = nullptr; + if ( eclipseCase ) + { + RimWellPath* wellPath = RimProject::current()->wellPathFromSimWellName( wellName ); + extractor = RiaExtractionTools::findOrCreateWellLogExtractor( wellPath, eclipseCase ); + if ( !extractor ) extractor = RiaExtractionTools::findOrCreateSimWellExtractor( eclipseCase, wellName, false, 0 ); + } + + const auto& allCases = ensemble->allSummaryCases(); + + std::vector> samplesPerCase; + samplesPerCase.reserve( allCases.size() ); + + for ( RimSummaryCase* summaryCase : allCases ) + { + if ( !summaryCase ) + { + samplesPerCase.push_back( {} ); + continue; + } + + RifReaderRftInterface* reader = summaryCase->rftReader(); + if ( !reader ) + { + samplesPerCase.push_back( {} ); + continue; + } + + auto pressureAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::PRESSURE ); + std::vector pressures; + reader->values( pressureAddress, &pressures ); + if ( pressures.empty() ) + { + samplesPerCase.push_back( {} ); + continue; + } + + // Use the same depth values the RFT curves use for their depth axis, so the filter + // 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 ) ); + } + + return samplesPerCase; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimRftCrossPlotTools::computeMeanPressurePerCase( RimSummaryEnsemble* ensemble, + const QString& wellName, + const QDateTime& timeStep, + RimEclipseResultCase* eclipseCase, + bool useDepthRange, + double depthRangeMin, + double depthRangeMax, + RiaDefines::DepthType depthType ) +{ + const std::vector> samplesPerCase = + computePressureSamplesPerCase( ensemble, wellName, timeStep, eclipseCase, useDepthRange, depthRangeMin, depthRangeMax, depthType ); + + std::vector pressurePerCase; + pressurePerCase.reserve( samplesPerCase.size() ); + + for ( const std::vector& samples : samplesPerCase ) + pressurePerCase.push_back( computeMean( samples ) ); + + return pressurePerCase; +} diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h new file mode 100644 index 0000000000..772488087b --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftCrossPlotTools.h @@ -0,0 +1,88 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RiaDefines.h" + +#include +#include + +#include + +class RifReaderRftInterface; +class RigEclipseWellLogExtractor; +class RimEclipseResultCase; +class RimSummaryEnsemble; + +//================================================================================================== +/// +/// Helpers for filtering and reducing RFT pressure data by depth range, shared by the RFT +/// correlation parameter cross plot (RimParameterRftCrossPlot) and tornado plot (RimRftTornadoPlot). +/// +//================================================================================================== +namespace RimRftCrossPlotTools +{ +// Short abbreviation used in axis titles and plot titles for the active depth type. +QString depthTypeAbbreviation( RiaDefines::DepthType depthType ); + +// Returns the depth values for the given well/time step, strictly matching depthType: MD +// (native RFT MD channel, falling back to extractor-derived MD if the channel is missing) or +// TVD (native RFT TVD channel). Returns an empty vector if the requested depth type is not +// available, rather than silently substituting the other depth type — callers that filter by +// depth range rely on this to exclude/blank out data instead of comparing a range against the +// wrong unit. +std::vector rftCurveDepthValues( RifReaderRftInterface* reader, + const QString& wellName, + const QDateTime& timeStep, + RigEclipseWellLogExtractor* extractor, + RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH ); + +// Returns the subset of pressures whose corresponding depth is within [depthRangeMin, depthRangeMax]. +// If useDepthRange is false, all pressures are returned unfiltered. If useDepthRange is true and +// depths/pressures are not the same size, an empty vector is returned (no aligned depth data). +std::vector filterPressuresByDepthRange( const std::vector& depths, + const std::vector& pressures, + bool useDepthRange, + double depthRangeMin, + double depthRangeMax ); + +// 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, + 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 ); +} // namespace RimRftCrossPlotTools diff --git a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp index 17b211d034..d5505a435d 100644 --- a/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/CorrelationPlots/RimRftTornadoPlot.cpp @@ -26,7 +26,7 @@ #include "RimCorrelationBarChartTools.h" #include "RimEclipseResultCase.h" -#include "RimParameterRftCrossPlot.h" +#include "RimRftCrossPlotTools.h" #include "RimSummaryEnsemble.h" #include "RimSummaryEnsembleTools.h" @@ -46,17 +46,6 @@ #include #include -namespace -{ -//-------------------------------------------------------------------------------------------------- -/// Short abbreviation used in the plot title for the active depth type. -//-------------------------------------------------------------------------------------------------- -QString depthTypeAbbreviation( RiaDefines::DepthType depthType ) -{ - return depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ? "TVD" : "MD"; -} -} // namespace - CAF_PDM_SOURCE_INIT( RimRftTornadoPlot, "RftTornadoPlot" ); //-------------------------------------------------------------------------------------------------- @@ -383,14 +372,14 @@ std::map RimRftTornadoPlot::addDataToChartBuilder( RiuGroupedBa const auto& allCases = m_ensemble->allSummaryCases(); // Build pressure vector per case (indices match allCases order) - const std::vector pressurePerCase = RimParameterRftCrossPlot::computeMeanPressurePerCase( m_ensemble(), - m_wellName(), - m_selectedTimeStep(), - m_eclipseCase(), - m_useDepthRange(), - m_depthRangeMin(), - m_depthRangeMax(), - m_depthType() ); + const std::vector pressurePerCase = RimRftCrossPlotTools::computeMeanPressurePerCase( m_ensemble(), + m_wellName(), + m_selectedTimeStep(), + m_eclipseCase(), + m_useDepthRange(), + m_depthRangeMin(), + m_depthRangeMax(), + m_depthType() ); // For each numeric parameter, compute Pearson correlation against pressurePerCase for ( const auto& param : RimSummaryEnsembleTools::alphabeticEnsembleParameters( allCases ) ) @@ -432,11 +421,12 @@ void RimRftTornadoPlot::updatePlotTitle() if ( m_useAutoPlotTitle() && m_ensemble() ) { - const QString rangeStr = - m_useDepthRange() - ? QString( " [%1 %2 - %3 m]" ).arg( 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 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() ); } m_plotWidget->setPlotTitle( m_description() ); diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.cpp b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.cpp index e548a7cfbc..f20633ecc7 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.cpp @@ -1220,32 +1220,6 @@ std::vector RimWellLogRftCurve::measuredDepthValues( QString& prefixText return {}; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector RimWellLogRftCurve::rftCurveDepthValues( RifReaderRftInterface* reader, - const QString& wellName, - const QDateTime& timeStep, - RigEclipseWellLogExtractor* extractor, - RiaDefines::DepthType depthType ) -{ - if ( !reader ) return {}; - - if ( depthType == RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ) - { - auto tvdAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::TVD ); - std::vector tvdDepths; - reader->values( tvdAddress, &tvdDepths ); - return tvdDepths; - } - - auto mdAddress = RifEclipseRftAddress::createAddress( wellName, timeStep, RifEclipseRftAddress::RftWellLogChannelType::MD ); - std::vector depths; - reader->values( mdAddress, &depths ); - if ( depths.empty() && extractor ) depths = reader->computeMeasuredDepth( wellName, timeStep, extractor ); - return depths; -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.h b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.h index 670cc22c43..83cef38356 100644 --- a/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.h +++ b/ApplicationLibCode/ProjectDataModel/WellLog/RimWellLogRftCurve.h @@ -96,18 +96,6 @@ public: void setScaleFactor( double factor ); - // Returns the depth values for the given well/time step, strictly matching depthType: MD - // (native RFT MD channel, falling back to extractor-derived MD if the channel is missing) or - // TVD (native RFT TVD channel). Returns an empty vector if the requested depth type is not - // available, rather than silently substituting the other depth type — callers that filter by - // depth range rely on this to exclude/blank out data instead of comparing a range against the - // wrong unit. - static std::vector rftCurveDepthValues( RifReaderRftInterface* reader, - const QString& wellName, - const QDateTime& timeStep, - RigEclipseWellLogExtractor* extractor, - RiaDefines::DepthType depthType = RiaDefines::DepthType::MEASURED_DEPTH ); - protected: QString createCurveAutoName() override; QString createCurveNameFromTemplate( const QString& templateText ) override; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index a7ef5046fb..10f8a390b6 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -94,6 +94,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RimWellPathFracture-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellPathValve-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellPathGeometryDef-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimRftCrossPlotTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellRftPlot-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellTargetMapping-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWorkflowDescribeTools-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp b/ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp new file mode 100644 index 0000000000..8d8abcdb06 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimRftCrossPlotTools-Test.cpp @@ -0,0 +1,105 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "gtest/gtest.h" + +#include "RimRftCrossPlotTools.h" + +#include + +//-------------------------------------------------------------------------------------------------- +/// When depth-range filtering is disabled, all pressures are returned unmodified. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, FilterPressuresByDepthRange_DisabledReturnsAllSamples ) +{ + std::vector depths{ 1000.0, 2000.0, 3000.0 }; + std::vector pressures{ 10.0, 20.0, 30.0 }; + + auto filtered = RimRftCrossPlotTools::filterPressuresByDepthRange( depths, pressures, false, 1500.0, 2500.0 ); + + EXPECT_EQ( pressures, filtered ); +} + +//-------------------------------------------------------------------------------------------------- +/// When enabled, only pressures whose depth falls within [min, max] (inclusive) are returned. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, FilterPressuresByDepthRange_FiltersByInclusiveRange ) +{ + 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 }; + + auto filtered = RimRftCrossPlotTools::filterPressuresByDepthRange( depths, pressures, true, 1500.0, 2500.0 ); + + std::vector expected{ 15.0, 20.0, 25.0 }; + EXPECT_EQ( expected, filtered ); +} + +//-------------------------------------------------------------------------------------------------- +/// Depths outside the requested range are excluded. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, FilterPressuresByDepthRange_ExcludesOutsideRange ) +{ + std::vector depths{ 500.0, 4000.0 }; + std::vector pressures{ 5.0, 40.0 }; + + auto filtered = RimRftCrossPlotTools::filterPressuresByDepthRange( depths, pressures, true, 1000.0, 3000.0 ); + + EXPECT_TRUE( filtered.empty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// Mismatched depths/pressures sizes cannot be aligned, so filtering returns an empty result. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, FilterPressuresByDepthRange_MismatchedSizesReturnsEmpty ) +{ + std::vector depths{ 1000.0, 2000.0 }; + std::vector pressures{ 10.0 }; + + auto filtered = RimRftCrossPlotTools::filterPressuresByDepthRange( depths, pressures, true, 0.0, 5000.0 ); + + EXPECT_TRUE( filtered.empty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// The mean of an empty sample set is defined as infinity (used as a "no data" sentinel). +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, ComputeMean_EmptyReturnsInfinity ) +{ + std::vector samples; + + EXPECT_EQ( std::numeric_limits::infinity(), RimRftCrossPlotTools::computeMean( samples ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// The mean of a normal set of samples is the arithmetic average. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, ComputeMean_NormalAverage ) +{ + std::vector samples{ 10.0, 20.0, 30.0 }; + + EXPECT_DOUBLE_EQ( 20.0, RimRftCrossPlotTools::computeMean( samples ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// The depth type abbreviation used in axis/plot titles distinguishes MD from TVD. +//-------------------------------------------------------------------------------------------------- +TEST( RimRftCrossPlotToolsTest, DepthTypeAbbreviation ) +{ + EXPECT_EQ( QString( "MD" ), RimRftCrossPlotTools::depthTypeAbbreviation( RiaDefines::DepthType::MEASURED_DEPTH ) ); + EXPECT_EQ( QString( "TVD" ), RimRftCrossPlotTools::depthTypeAbbreviation( RiaDefines::DepthType::TRUE_VERTICAL_DEPTH ) ); +}