///////////////////////////////////////////////////////////////////////////////// // // 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 "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 #include #include #include #include #include static std::unique_ptr 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(); 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(); // 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*>( propertyFilter->findField( "LowerBound" ) ); auto* upperField = dynamic_cast*>( 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(); 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*>( propertyFilter->findField( "LowerBound" ) ); auto* upperField = dynamic_cast*>( 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*>( propertyFilter.findField( "LowerBound" ) ); auto* upperField = dynamic_cast*>( 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(); 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*>( propertyFilter.findField( "CategorySelection" ) ); auto* selectedValuesField = dynamic_cast>*>( 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 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( { 0, 1 } ), visibleKLayers( visibilityA.p(), caseA.get() ) ); EXPECT_EQ( std::set( { 0, 1, 2 } ), visibleKLayers( visibilityB.p(), caseB.get() ) ); }