mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
Replace the built-in formation filter with ensemble data filters. Load ensemble data filters after project read, and parent new views before assigning the case so the ensemble filter collection is resolved correctly.
402 lines
17 KiB
C++
402 lines
17 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 "RiaResultNames.h"
|
|
#include "RiaTestDataDirectory.h"
|
|
|
|
#include "RigActiveCellInfo.h"
|
|
#include "RigCaseCellResultsData.h"
|
|
#include "RigEclipseCaseData.h"
|
|
#include "RigEclipseResultAddress.h"
|
|
#include "RigMainGrid.h"
|
|
|
|
#include "Formations/RimFormationNames.h"
|
|
#include "RimCellFilterTools.h"
|
|
#include "RimCellRangeFilter.h"
|
|
#include "RimDataFilterCollection.h"
|
|
#include "RimDataFilterInView.h"
|
|
#include "RimDataFilterInViewCollection.h"
|
|
#include "RimEclipsePropertyFilter.h"
|
|
#include "RimEclipseResultCase.h"
|
|
#include "RimEclipseResultDefinition.h"
|
|
#include "RimEclipseView.h"
|
|
#include "RimReservoirGridEnsemble.h"
|
|
|
|
#include "cafPdmField.h"
|
|
#include "cafPdmUiDoubleSliderEditor.h"
|
|
#include "cafPdmUiLineEditor.h"
|
|
#include "cafPdmUiObjectHandle.h"
|
|
#include "cafPdmUiOrdering.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QTemporaryDir>
|
|
|
|
#include <cmath>
|
|
#include <memory>
|
|
#include <set>
|
|
|
|
static std::unique_ptr<RimEclipseResultCase> openBruggeCase( 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;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimCellFilterToolsTest, RangeFilterVisibility )
|
|
{
|
|
auto eclipseCase = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
|
|
ASSERT_TRUE( eclipseCase != nullptr );
|
|
|
|
RimCellRangeFilter rangeFilter;
|
|
rangeFilter.setCase( eclipseCase.get() );
|
|
rangeFilter.startIndexI = 10;
|
|
rangeFilter.startIndexJ = 20;
|
|
rangeFilter.startIndexK = 1;
|
|
rangeFilter.cellCountI = 5;
|
|
rangeFilter.cellCountJ = 4;
|
|
rangeFilter.cellCountK = 3;
|
|
|
|
auto visibility = RimCellFilterTools::computeReservoirCellVisibility( &rangeFilter, eclipseCase.get(), 0 );
|
|
ASSERT_TRUE( visibility.notNull() );
|
|
|
|
const RigMainGrid* mainGrid = eclipseCase->eclipseCaseData()->mainGrid();
|
|
ASSERT_EQ( mainGrid->totalCellCount(), visibility->size() );
|
|
|
|
size_t visibleCount = 0;
|
|
for ( size_t i = 0; i < visibility->size(); i++ )
|
|
{
|
|
if ( visibility->val( i ) ) visibleCount++;
|
|
}
|
|
|
|
// The Brugge grid has no LGRs, so the geometric mask is exactly the IJK box
|
|
EXPECT_EQ( size_t( 5 * 4 * 3 ), visibleCount );
|
|
|
|
// An EXCLUDE filter must select the complement
|
|
rangeFilter.setFilterMode( RimCellFilter::EXCLUDE );
|
|
|
|
auto excludeVisibility = RimCellFilterTools::computeReservoirCellVisibility( &rangeFilter, eclipseCase.get(), 0 );
|
|
ASSERT_TRUE( excludeVisibility.notNull() );
|
|
|
|
size_t excludeVisibleCount = 0;
|
|
for ( size_t i = 0; i < excludeVisibility->size(); i++ )
|
|
{
|
|
if ( excludeVisibility->val( i ) ) excludeVisibleCount++;
|
|
}
|
|
|
|
EXPECT_EQ( visibility->size() - visibleCount, excludeVisibleCount );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Data filters in a grid ensemble collection are bound to the main case of the ensemble, and can
|
|
/// be evaluated per case without any view
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimCellFilterToolsTest, GridEnsembleDataFilterCollection )
|
|
{
|
|
auto eclipseCase = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
|
|
ASSERT_TRUE( eclipseCase != nullptr );
|
|
|
|
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
|
|
|
|
// The ensemble takes ownership of the case
|
|
RimEclipseResultCase* mainCase = eclipseCase.release();
|
|
ensemble->addCase( mainCase );
|
|
|
|
ASSERT_TRUE( ensemble->dataFilterCollection() != nullptr );
|
|
|
|
// New filters in the ensemble collection are bound to the main case of the ensemble
|
|
auto* propertyFilter = ensemble->dataFilterCollection()->addNewPropertyFilter();
|
|
ASSERT_TRUE( propertyFilter != nullptr );
|
|
EXPECT_EQ( mainCase, propertyFilter->resultDefinition()->eclipseCase() );
|
|
|
|
auto* lowerField = dynamic_cast<caf::PdmField<double>*>( propertyFilter->findField( "LowerBound" ) );
|
|
auto* upperField = dynamic_cast<caf::PdmField<double>*>( propertyFilter->findField( "UpperBound" ) );
|
|
ASSERT_TRUE( lowerField && upperField );
|
|
|
|
caf::PdmUiOrdering uiOrdering;
|
|
propertyFilter->uiCapability()->uiOrdering( "", uiOrdering );
|
|
|
|
EXPECT_DOUBLE_EQ( 0.0, lowerField->value() );
|
|
EXPECT_DOUBLE_EQ( 1.0, upperField->value() );
|
|
EXPECT_EQ( caf::PdmUiLineEditor::uiEditorTypeName(), lowerField->uiCapability()->uiEditorTypeName( "" ) );
|
|
EXPECT_EQ( caf::PdmUiLineEditor::uiEditorTypeName(), upperField->uiCapability()->uiEditorTypeName( "" ) );
|
|
|
|
lowerField->setValue( -100.0 );
|
|
upperField->setValue( 100.0 );
|
|
propertyFilter->resultDefinition()->setResultType( RiaDefines::ResultCatType::DYNAMIC_NATIVE );
|
|
propertyFilter->resultDefinition()->setResultVariable( RiaResultNames::swat() );
|
|
propertyFilter->computeResultValueRange();
|
|
EXPECT_DOUBLE_EQ( -100.0, lowerField->value() );
|
|
EXPECT_DOUBLE_EQ( 100.0, upperField->value() );
|
|
|
|
propertyFilter->resultDefinition()->setResultVariable( "DX" );
|
|
propertyFilter->setToDefaultValues();
|
|
EXPECT_DOUBLE_EQ( 0.0, lowerField->value() );
|
|
EXPECT_DOUBLE_EQ( 1.0, upperField->value() );
|
|
|
|
auto* rangeFilter = ensemble->dataFilterCollection()->addNewRangeFilter();
|
|
ASSERT_TRUE( rangeFilter != nullptr );
|
|
rangeFilter->startIndexI = 1;
|
|
rangeFilter->startIndexJ = 1;
|
|
rangeFilter->startIndexK = 1;
|
|
rangeFilter->cellCountI = 10;
|
|
rangeFilter->cellCountJ = 10;
|
|
rangeFilter->cellCountK = 1;
|
|
|
|
auto visibility = RimCellFilterTools::computeReservoirCellVisibility( rangeFilter, mainCase, 0 );
|
|
ASSERT_TRUE( visibility.notNull() );
|
|
|
|
size_t visibleCount = 0;
|
|
for ( size_t i = 0; i < visibility->size(); i++ )
|
|
{
|
|
if ( visibility->val( i ) ) visibleCount++;
|
|
}
|
|
EXPECT_EQ( size_t( 10 * 10 * 1 ), visibleCount );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimCellFilterToolsTest, EnsembleDataFilterInViewUsesViewCase )
|
|
{
|
|
auto caseA = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
|
|
auto caseB = openBruggeCase( "Real40", "BRUGGE_0040.EGRID" );
|
|
ASSERT_TRUE( caseA != nullptr );
|
|
ASSERT_TRUE( caseB != nullptr );
|
|
|
|
auto ensemble = std::make_unique<RimReservoirGridEnsemble>();
|
|
|
|
RimEclipseResultCase* mainCase = caseA.release();
|
|
RimEclipseResultCase* viewCase = caseB.release();
|
|
ensemble->addCase( mainCase );
|
|
ensemble->addCase( viewCase );
|
|
|
|
auto* view = new RimEclipseView();
|
|
view->setEclipseCase( viewCase );
|
|
ensemble->addView( view );
|
|
|
|
auto* propertyFilter = ensemble->dataFilterCollection()->addNewPropertyFilter();
|
|
ASSERT_TRUE( propertyFilter != nullptr );
|
|
EXPECT_EQ( mainCase, propertyFilter->resultDefinition()->eclipseCase() );
|
|
|
|
propertyFilter->resultDefinition()->setResultType( RiaDefines::ResultCatType::DYNAMIC_NATIVE );
|
|
propertyFilter->resultDefinition()->setResultVariable( RiaResultNames::swat() );
|
|
|
|
auto* lowerField = dynamic_cast<caf::PdmField<double>*>( propertyFilter->findField( "LowerBound" ) );
|
|
auto* upperField = dynamic_cast<caf::PdmField<double>*>( propertyFilter->findField( "UpperBound" ) );
|
|
ASSERT_TRUE( lowerField && upperField );
|
|
lowerField->setValue( 0.3 );
|
|
upperField->setValue( 0.6 );
|
|
|
|
auto wrappers = view->dataFiltersInView()->wrappers();
|
|
ASSERT_EQ( 1u, wrappers.size() );
|
|
wrappers.front()->setCheckState( true );
|
|
|
|
const size_t timeStepIndex = 5;
|
|
auto expectedViewCaseVisibility = RimCellFilterTools::computeReservoirCellVisibility( propertyFilter, viewCase, timeStepIndex );
|
|
auto mainCaseVisibility = RimCellFilterTools::computeReservoirCellVisibility( propertyFilter, mainCase, timeStepIndex );
|
|
ASSERT_TRUE( expectedViewCaseVisibility.notNull() );
|
|
ASSERT_TRUE( mainCaseVisibility.notNull() );
|
|
|
|
const RigMainGrid* viewGrid = viewCase->eclipseCaseData()->mainGrid();
|
|
cvf::UByteArray viewVisibility( viewGrid->cellCount() );
|
|
viewVisibility.setAll( true );
|
|
wrappers.front()->applyToCellVisibility( &viewVisibility, viewGrid, timeStepIndex );
|
|
|
|
ASSERT_EQ( expectedViewCaseVisibility->size(), viewVisibility.size() );
|
|
ASSERT_EQ( mainCaseVisibility->size(), viewVisibility.size() );
|
|
|
|
size_t differingFromMainCase = 0;
|
|
for ( size_t i = 0; i < viewVisibility.size(); i++ )
|
|
{
|
|
EXPECT_EQ( expectedViewCaseVisibility->val( i ), viewVisibility.val( i ) );
|
|
if ( mainCaseVisibility->val( i ) != viewVisibility.val( i ) ) differingFromMainCase++;
|
|
}
|
|
EXPECT_GT( differingFromMainCase, 0u );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimCellFilterToolsTest, PropertyFilterVisibilityPerCase )
|
|
{
|
|
auto caseA = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
|
|
auto caseB = openBruggeCase( "Real40", "BRUGGE_0040.EGRID" );
|
|
ASSERT_TRUE( caseA != nullptr );
|
|
ASSERT_TRUE( caseB != nullptr );
|
|
|
|
const double lowerBound = 0.3;
|
|
const double upperBound = 0.6;
|
|
|
|
// Property filter bound to case A
|
|
RimEclipsePropertyFilter propertyFilter;
|
|
propertyFilter.setCase( caseA.get() );
|
|
propertyFilter.resultDefinition()->setEclipseCase( caseA.get() );
|
|
propertyFilter.resultDefinition()->setResultType( RiaDefines::ResultCatType::DYNAMIC_NATIVE );
|
|
propertyFilter.resultDefinition()->setResultVariable( RiaResultNames::swat() );
|
|
|
|
auto* lowerField = dynamic_cast<caf::PdmField<double>*>( propertyFilter.findField( "LowerBound" ) );
|
|
auto* upperField = dynamic_cast<caf::PdmField<double>*>( propertyFilter.findField( "UpperBound" ) );
|
|
ASSERT_TRUE( lowerField && upperField );
|
|
|
|
propertyFilter.setToDefaultValues();
|
|
caf::PdmUiOrdering uiOrdering;
|
|
propertyFilter.uiCapability()->uiOrdering( "", uiOrdering );
|
|
EXPECT_LE( lowerField->value(), upperField->value() );
|
|
EXPECT_EQ( caf::PdmUiDoubleSliderEditor::uiEditorTypeName(), lowerField->uiCapability()->uiEditorTypeName( "" ) );
|
|
EXPECT_EQ( caf::PdmUiDoubleSliderEditor::uiEditorTypeName(), upperField->uiCapability()->uiEditorTypeName( "" ) );
|
|
|
|
lowerField->setValue( lowerBound );
|
|
upperField->setValue( upperBound );
|
|
|
|
const size_t timeStepIndex = 5;
|
|
|
|
auto countVisible = []( const cvf::UByteArray* visibility )
|
|
{
|
|
size_t count = 0;
|
|
for ( size_t i = 0; i < visibility->size(); i++ )
|
|
{
|
|
if ( visibility->val( i ) ) count++;
|
|
}
|
|
return count;
|
|
};
|
|
|
|
// Reference: count active cells with SWAT inside the bounds, using the case data directly
|
|
auto expectedVisibleCount = [&]( RimEclipseCase* eclipseCase )
|
|
{
|
|
auto porosityModel = RiaDefines::PorosityModelType::MATRIX_MODEL;
|
|
auto results = eclipseCase->results( porosityModel );
|
|
|
|
RigEclipseResultAddress swatAddress( RiaDefines::ResultCatType::DYNAMIC_NATIVE, RiaResultNames::swat() );
|
|
results->ensureKnownResultLoaded( swatAddress );
|
|
if ( !results->hasResultEntry( swatAddress ) || results->timeStepCount( swatAddress ) <= timeStepIndex )
|
|
{
|
|
ADD_FAILURE() << "SWAT is not available for time step " << timeStepIndex;
|
|
return size_t( 0 );
|
|
}
|
|
const auto& values = results->cellScalarResults( swatAddress, timeStepIndex );
|
|
|
|
size_t count = 0;
|
|
for ( double value : values )
|
|
{
|
|
if ( lowerBound <= value && value <= upperBound ) count++;
|
|
}
|
|
return count;
|
|
};
|
|
|
|
auto visibilityA = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseA.get(), timeStepIndex );
|
|
auto visibilityB = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseB.get(), timeStepIndex );
|
|
ASSERT_TRUE( visibilityA.notNull() );
|
|
ASSERT_TRUE( visibilityB.notNull() );
|
|
|
|
// The same filter definition must be evaluated against each case's own result values
|
|
EXPECT_EQ( expectedVisibleCount( caseA.get() ), countVisible( visibilityA.p() ) );
|
|
EXPECT_EQ( expectedVisibleCount( caseB.get() ), countVisible( visibilityB.p() ) );
|
|
|
|
// The realizations have different SWAT fields, so the masks must differ
|
|
ASSERT_EQ( visibilityA->size(), visibilityB->size() );
|
|
size_t differingCells = 0;
|
|
for ( size_t i = 0; i < visibilityA->size(); i++ )
|
|
{
|
|
if ( visibilityA->val( i ) != visibilityB->val( i ) ) differingCells++;
|
|
}
|
|
EXPECT_GT( differingCells, 0u );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Realizations can have their own formation names file, with the same formations covering different
|
|
/// K layers. A formation filter must use the K ranges of each realization.
|
|
//--------------------------------------------------------------------------------------------------
|
|
TEST( RimCellFilterToolsTest, FormationFilterUsesKLayersOfEachCase )
|
|
{
|
|
auto caseA = openBruggeCase( "Real0", "BRUGGE_0000.EGRID" );
|
|
auto caseB = openBruggeCase( "Real40", "BRUGGE_0040.EGRID" );
|
|
ASSERT_TRUE( caseA != nullptr );
|
|
ASSERT_TRUE( caseB != nullptr );
|
|
|
|
QTemporaryDir tempDir;
|
|
ASSERT_TRUE( tempDir.isValid() );
|
|
|
|
auto createFormationNames = [&tempDir]( const QString& fileName, const QString& content )
|
|
{
|
|
QFile file( tempDir.filePath( fileName ) );
|
|
EXPECT_TRUE( file.open( QIODevice::WriteOnly | QIODevice::Text ) );
|
|
file.write( content.toUtf8() );
|
|
file.close();
|
|
|
|
auto formationNames = std::make_unique<RimFormationNames>();
|
|
formationNames->setFileName( file.fileName() );
|
|
EXPECT_TRUE( formationNames->readFormationNamesFile().has_value() );
|
|
return formationNames;
|
|
};
|
|
|
|
auto formationsA = createFormationNames( "a.lyr", "'Upper' 1 - 2\n'Lower' 3 - 4\n" );
|
|
auto formationsB = createFormationNames( "b.lyr", "'Upper' 1 - 3\n'Lower' 4 - 4\n" );
|
|
caseA->eclipseCaseData()->setActiveFormationNames( formationsA->formationNamesData() );
|
|
caseB->eclipseCaseData()->setActiveFormationNames( formationsB->formationNamesData() );
|
|
|
|
RimEclipsePropertyFilter propertyFilter;
|
|
propertyFilter.setCase( caseA.get() );
|
|
propertyFilter.setActive( true );
|
|
propertyFilter.resultDefinition()->setResultType( RiaDefines::ResultCatType::FORMATION_NAMES );
|
|
propertyFilter.resultDefinition()->setResultVariable( RiaResultNames::activeFormationNamesResultName() );
|
|
propertyFilter.computeResultValueRange();
|
|
|
|
auto* categorySelectionField = dynamic_cast<caf::PdmField<bool>*>( propertyFilter.findField( "CategorySelection" ) );
|
|
auto* selectedValuesField = dynamic_cast<caf::PdmField<std::vector<int>>*>( propertyFilter.findField( "SelectedValues" ) );
|
|
ASSERT_TRUE( categorySelectionField && selectedValuesField );
|
|
categorySelectionField->setValue( true );
|
|
selectedValuesField->setValue( { 0 } );
|
|
ASSERT_TRUE( propertyFilter.isCategorySelectionActive() );
|
|
|
|
auto visibleKLayers = []( const cvf::UByteArray* visibility, const RimEclipseCase* eclipseCase )
|
|
{
|
|
const RigMainGrid* mainGrid = eclipseCase->eclipseCaseData()->mainGrid();
|
|
|
|
std::set<size_t> kLayers;
|
|
for ( size_t i = 0; i < visibility->size(); i++ )
|
|
{
|
|
size_t ci = 0, cj = 0, ck = 0;
|
|
if ( visibility->val( i ) && mainGrid->ijkFromCellIndex( i, &ci, &cj, &ck ) ) kLayers.insert( ck );
|
|
}
|
|
return kLayers;
|
|
};
|
|
|
|
auto visibilityA = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseA.get(), 0 );
|
|
auto visibilityB = RimCellFilterTools::computeReservoirCellVisibility( &propertyFilter, caseB.get(), 0 );
|
|
ASSERT_TRUE( visibilityA.notNull() );
|
|
ASSERT_TRUE( visibilityB.notNull() );
|
|
|
|
EXPECT_EQ( std::set<size_t>( { 0, 1 } ), visibleKLayers( visibilityA.p(), caseA.get() ) );
|
|
EXPECT_EQ( std::set<size_t>( { 0, 1, 2 } ), visibleKLayers( visibilityB.p(), caseB.get() ) );
|
|
}
|