mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 06:38:13 -05:00
Keep the removed formation filter fields as obsolete so old projects still parse, and warn that the filter is no longer applied.
299 lines
13 KiB
C++
299 lines
13 KiB
C++
/////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Copyright (C) 2026 Equinor ASA
|
|
//
|
|
// ResInsight is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
// FITNESS FOR A PARTICULAR PURPOSE.
|
|
//
|
|
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
|
// for more details.
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "gtest/gtest.h"
|
|
|
|
#include "RiaDefines.h"
|
|
#include "RiaLogging.h"
|
|
#include "RiaTestDataDirectory.h"
|
|
|
|
#include "ContourMap/RigContourMapCalculator.h"
|
|
#include "ContourMap/RigContourMapGrid.h"
|
|
#include "RigEclipseCaseData.h"
|
|
#include "RigMainGrid.h"
|
|
|
|
#include "ContourMap/RimStatisticsContourMap.h"
|
|
#include "RimCellRangeFilter.h"
|
|
#include "RimDataFilterCollection.h"
|
|
#include "RimEclipseResultCase.h"
|
|
#include "RimEclipseResultDefinition.h"
|
|
#include "RimProject.h"
|
|
#include "RimReservoirGridEnsemble.h"
|
|
|
|
#include "cafPdmChildField.h"
|
|
#include "cafPdmDefaultObjectFactory.h"
|
|
#include "cafPdmField.h"
|
|
#include "cafPdmPtrField.h"
|
|
#include "cafPdmXmlObjectHandle.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
|
|
#include <cmath>
|
|
#include <memory>
|
|
|
|
namespace
|
|
{
|
|
using GridModeType = RimReservoirGridEnsembleBase::GridModeType;
|
|
using GridImportMode = RimStatisticsContourMap::GridImportMode;
|
|
|
|
RimEclipseResultCase* openBruggeRealization( const QString& realizationFolder, const QString& fileName )
|
|
{
|
|
QDir baseFolder( TEST_MODEL_DIR );
|
|
if ( !baseFolder.cd( QString( "Case_with_10_timesteps/%1" ).arg( realizationFolder ) ) ) return nullptr;
|
|
|
|
QString filePath = baseFolder.absoluteFilePath( fileName );
|
|
if ( !QFile::exists( filePath ) ) return nullptr;
|
|
|
|
auto eclipseCase = std::make_unique<RimEclipseResultCase>();
|
|
eclipseCase->setCaseInfo( realizationFolder, filePath );
|
|
if ( !eclipseCase->openEclipseGridFile() ) return nullptr;
|
|
|
|
return eclipseCase.release();
|
|
}
|
|
|
|
std::unique_ptr<RimReservoirGridEnsemble> createBruggeEnsemble( GridModeType gridMode )
|
|
{
|
|
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
|
|
|
|
for ( const auto& [folder, fileName] : std::vector<std::pair<QString, QString>>{ { "Real0", "BRUGGE_0000.EGRID" },
|
|
{ "Real10", "BRUGGE_0010.EGRID" },
|
|
{ "Real30", "BRUGGE_0030.EGRID" },
|
|
{ "Real40", "BRUGGE_0040.EGRID" } } )
|
|
{
|
|
auto* eclipseCase = openBruggeRealization( folder, fileName );
|
|
if ( !eclipseCase ) return nullptr;
|
|
ensemble->addCase( eclipseCase );
|
|
}
|
|
|
|
auto* autoDetectField = dynamic_cast<caf::PdmField<bool>*>( ensemble->findField( "AutoDetectGridType" ) );
|
|
auto* gridModeField = dynamic_cast<caf::PdmField<caf::AppEnum<GridModeType>>*>( ensemble->findField( "GridMode" ) );
|
|
if ( !autoDetectField || !gridModeField ) return nullptr;
|
|
|
|
autoDetectField->setValue( false );
|
|
gridModeField->setValue( gridMode );
|
|
|
|
return ensemble;
|
|
}
|
|
|
|
RimStatisticsContourMap* addPoroContourMap( RimReservoirGridEnsemble* ensemble, GridImportMode importMode )
|
|
{
|
|
auto* map = new RimStatisticsContourMap;
|
|
ensemble->addStatisticsContourMap( map );
|
|
map->setEclipseCase( ensemble->cases().front() );
|
|
map->setGridImportMode( importMode );
|
|
|
|
auto* aggregationField =
|
|
dynamic_cast<caf::PdmField<caf::AppEnum<RigContourMapCalculator::ResultAggregationType>>*>( map->findField( "ResultAggregation" ) );
|
|
auto* resultDefinitionField = dynamic_cast<caf::PdmChildField<RimEclipseResultDefinition*>*>( map->findField( "ResultDefinition" ) );
|
|
if ( !aggregationField || !resultDefinitionField ) return nullptr;
|
|
|
|
aggregationField->setValue( RigContourMapCalculator::MEAN );
|
|
( *resultDefinitionField )()->setResultType( RiaDefines::ResultCatType::STATIC_NATIVE );
|
|
( *resultDefinitionField )()->setResultVariable( "PORO" );
|
|
|
|
return map;
|
|
}
|
|
|
|
void setDataFilter( RimStatisticsContourMap* map, RimCellFilter* filter )
|
|
{
|
|
auto* dataFilterField = dynamic_cast<caf::PdmPtrField<RimCellFilter*>*>( map->findField( "DataFilter" ) );
|
|
ASSERT_TRUE( dataFilterField != nullptr );
|
|
dataFilterField->setValue( filter );
|
|
}
|
|
|
|
// Filter including the top K layer of the grid only
|
|
RimCellRangeFilter* addTopLayerFilter( RimReservoirGridEnsemble* ensemble )
|
|
{
|
|
const RigMainGrid* mainGrid = ensemble->cases().front()->eclipseCaseData()->mainGrid();
|
|
|
|
auto* rangeFilter = ensemble->dataFilterCollection()->addNewRangeFilter();
|
|
rangeFilter->startIndexI = 1;
|
|
rangeFilter->startIndexJ = 1;
|
|
rangeFilter->startIndexK = 1;
|
|
rangeFilter->cellCountI = static_cast<int>( mainGrid->cellCountI() );
|
|
rangeFilter->cellCountJ = static_cast<int>( mainGrid->cellCountJ() );
|
|
rangeFilter->cellCountK = 1;
|
|
|
|
return rangeFilter;
|
|
}
|
|
|
|
size_t countValidValues( const std::vector<double>& values )
|
|
{
|
|
return std::count_if( values.begin(), values.end(), []( double v ) { return std::isfinite( v ); } );
|
|
}
|
|
|
|
size_t countDifferentValues( const std::vector<double>& lhs, const std::vector<double>& rhs )
|
|
{
|
|
size_t differentCount = 0;
|
|
for ( size_t i = 0; i < lhs.size(); i++ )
|
|
{
|
|
const bool lhsValid = std::isfinite( lhs[i] );
|
|
const bool rhsValid = std::isfinite( rhs[i] );
|
|
if ( lhsValid != rhsValid || ( lhsValid && std::abs( lhs[i] - rhs[i] ) > 1.0e-9 ) ) differentCount++;
|
|
}
|
|
return differentCount;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Compute the mean PORO contour map with and without a top layer data filter, and verify that the
|
|
/// filter changes the result
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::vector<double> verifyDataFilterIsApplied( GridModeType gridMode, GridImportMode importMode )
|
|
{
|
|
auto ensemble = createBruggeEnsemble( gridMode );
|
|
if ( !ensemble ) ADD_FAILURE() << "Failed to create Brugge ensemble";
|
|
if ( !ensemble ) return {};
|
|
|
|
auto* map = addPoroContourMap( ensemble.get(), importMode );
|
|
if ( !map ) ADD_FAILURE() << "Failed to create contour map";
|
|
if ( !map ) return {};
|
|
|
|
RimStatisticsContourMap::computeStatisticsForMaps( { map } );
|
|
const auto unfiltered = map->result( 0, RimStatisticsContourMap::StatisticsType::MEAN );
|
|
EXPECT_GT( countValidValues( unfiltered ), 0u );
|
|
|
|
setDataFilter( map, addTopLayerFilter( ensemble.get() ) );
|
|
RimStatisticsContourMap::computeStatisticsForMaps( { map } );
|
|
const auto filtered = map->result( 0, RimStatisticsContourMap::StatisticsType::MEAN );
|
|
|
|
EXPECT_EQ( unfiltered.size(), filtered.size() );
|
|
if ( unfiltered.size() != filtered.size() ) return {};
|
|
|
|
EXPECT_GT( countValidValues( filtered ), 0u );
|
|
EXPECT_GT( countDifferentValues( unfiltered, filtered ), 0u );
|
|
|
|
// An EXCLUDE filter of the same layer must give yet another result
|
|
auto* excludeFilter = addTopLayerFilter( ensemble.get() );
|
|
excludeFilter->setFilterMode( RimCellFilter::EXCLUDE );
|
|
setDataFilter( map, excludeFilter );
|
|
RimStatisticsContourMap::computeStatisticsForMaps( { map } );
|
|
const auto excluded = map->result( 0, RimStatisticsContourMap::StatisticsType::MEAN );
|
|
|
|
EXPECT_GT( countDifferentValues( filtered, excluded ), 0u );
|
|
|
|
return filtered;
|
|
}
|
|
} // namespace
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimStatisticsContourMapTest, DataFilterSharedGrid )
|
|
{
|
|
verifyDataFilterIsApplied( GridModeType::SHARED_GRID, GridImportMode::SHARED_GRID );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimStatisticsContourMapTest, DataFilterIndividualGridImport )
|
|
{
|
|
verifyDataFilterIsApplied( GridModeType::SHARED_GRID, GridImportMode::INDIVIDUAL_GRIDS );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimStatisticsContourMapTest, DataFilterIndividualGridEnsemble )
|
|
{
|
|
verifyDataFilterIsApplied( GridModeType::INDIVIDUAL_GRIDS, GridImportMode::SHARED_GRID );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// The Brugge realizations have identical grids, so the shared and individual grid code paths must
|
|
/// give the same filtered result
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimStatisticsContourMapTest, DataFilterSharedAndIndividualGridsAgree )
|
|
{
|
|
const auto shared = verifyDataFilterIsApplied( GridModeType::SHARED_GRID, GridImportMode::SHARED_GRID );
|
|
const auto individual = verifyDataFilterIsApplied( GridModeType::INDIVIDUAL_GRIDS, GridImportMode::INDIVIDUAL_GRIDS );
|
|
|
|
ASSERT_FALSE( shared.empty() );
|
|
ASSERT_EQ( shared.size(), individual.size() );
|
|
EXPECT_EQ( 0u, countDifferentValues( shared, individual ) );
|
|
}
|
|
|
|
namespace
|
|
{
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Minimal logger capturing warning messages, used to verify the obsolete formation filter warning.
|
|
//--------------------------------------------------------------------------------------------------
|
|
class RecordingLogger : public RiaLogger
|
|
{
|
|
public:
|
|
int level() const override { return static_cast<int>( RILogLevel::RI_LL_WARNING ); }
|
|
void setLevel( int ) override {}
|
|
|
|
void error( const char* ) override {}
|
|
void warning( const char* message ) override { warnings.push_back( message ); }
|
|
void info( const char* ) override {}
|
|
void debug( const char* ) override {}
|
|
|
|
std::vector<std::string> warnings;
|
|
};
|
|
} // namespace
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Loading a project with the obsolete formation filter fields set (#14710) should warn, not silently drop them.
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimStatisticsContourMapTest, ObsoleteFormationFilterWarnsOnLoad )
|
|
{
|
|
auto sourceMap = std::make_unique<RimStatisticsContourMap>();
|
|
sourceMap->setName( "MyContourMap" );
|
|
|
|
auto* enableField = dynamic_cast<caf::PdmField<bool>*>( sourceMap->findField( "EnableFormationFilter" ) );
|
|
auto* formationsField = dynamic_cast<caf::PdmField<std::vector<QString>>*>( sourceMap->findField( "Formations" ) );
|
|
ASSERT_TRUE( enableField != nullptr );
|
|
ASSERT_TRUE( formationsField != nullptr );
|
|
|
|
enableField->setValue( true );
|
|
formationsField->setValue( { "Zone1", "Zone2" } );
|
|
|
|
// The obsolete fields are marked non-writable so current projects never persist them again; force
|
|
// writing here to simulate an XML string produced by an older ResInsight version.
|
|
enableField->xmlCapability()->setIOWritable( true );
|
|
formationsField->xmlCapability()->setIOWritable( true );
|
|
|
|
QString xml = sourceMap->xmlCapability()->writeObjectToXmlString();
|
|
|
|
auto* recordingLogger = new RecordingLogger();
|
|
RiaLogging::appendLoggerInstance( std::unique_ptr<RiaLogger>( recordingLogger ) );
|
|
|
|
// Simulate the project having been saved by an older ResInsight version, so the version guard in
|
|
// initAfterRead() lets the obsolete-field warning through.
|
|
auto* versionField = dynamic_cast<caf::PdmField<QString>*>( RimProject::current()->findField( "ProjectFileVersionString" ) );
|
|
ASSERT_TRUE( versionField != nullptr );
|
|
QString originalVersion = versionField->value();
|
|
versionField->setValue( "2026.06.0" );
|
|
|
|
auto readMap = std::make_unique<RimStatisticsContourMap>();
|
|
readMap->xmlCapability()->readObjectFromXmlString( xml, caf::PdmDefaultObjectFactory::instance() );
|
|
readMap->xmlCapability()->resolveReferencesRecursively();
|
|
readMap->xmlCapability()->initAfterReadRecursively();
|
|
|
|
versionField->setValue( originalVersion );
|
|
|
|
bool foundWarning = false;
|
|
for ( const auto& message : recordingLogger->warnings )
|
|
{
|
|
if ( message.find( "Zone1, Zone2" ) != std::string::npos ) foundWarning = true;
|
|
}
|
|
EXPECT_TRUE( foundWarning );
|
|
}
|