diff --git a/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h b/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h index 963465c7e4..475b8f77f5 100644 --- a/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h +++ b/ApplicationLibCode/Commands/CellFilterCommands/RicCellFilterFeatureTools.h @@ -28,6 +28,7 @@ #include "RimEclipseCase.h" #include "RimFilterInViewCollection.h" #include "RimGridView.h" +#include "RimReservoirGridEnsemble.h" #include "Riu3DMainWindowTools.h" @@ -84,10 +85,10 @@ inline RimCellFilterCollection* resolveTargetCellFilterCollection() } //-------------------------------------------------------------------------------------------------- -/// Resolve the case-level RimDataFilterCollection to target from the current selection: either the -/// data-filter collection node itself, or a selected RimEclipseCase (whose "Data Filters" node is -/// hidden while empty, so the case node is right-clicked to create the first filter). Returns -/// nullptr if neither is selected. +/// Resolve the RimDataFilterCollection to target from the current selection: either the data-filter +/// collection node itself, or a selected RimEclipseCase or RimReservoirGridEnsemble (whose "Data +/// Filters" node is hidden while empty, so the owner node is right-clicked to create the first +/// filter). Returns nullptr if none is selected. //-------------------------------------------------------------------------------------------------- inline RimDataFilterCollection* selectedDataFilterCollection() { @@ -97,6 +98,9 @@ inline RimDataFilterCollection* selectedDataFilterCollection() auto cases = caf::selectedObjectsByTypeStrict(); if ( !cases.empty() && cases.front() ) return cases.front()->dataFilterCollection(); + auto ensembles = caf::selectedObjectsByTypeStrict(); + if ( !ensembles.empty() && ensembles.front() ) return ensembles.front()->dataFilterCollection(); + return nullptr; } diff --git a/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp b/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp index 9997c72c25..3896bf8f7f 100644 --- a/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp +++ b/ApplicationLibCode/Commands/CellFilterCommands/RicNewPolygonFilterFeature.cpp @@ -104,8 +104,9 @@ void RicNewPolygonFilterFeature::onActionTriggered( bool isChecked ) return; } - // If a case-level Data Filter Collection is selected, add polygon filters there. - auto* dataCollection = caf::SelectionManager::instance()->selectedItemOfType(); + // If a case or ensemble level Data Filter Collection is selected (or its owner node), add + // polygon filters there. + auto* dataCollection = RicCellFilterFeatureTools::selectedDataFilterCollection(); if ( dataCollection ) { if ( polygons.empty() ) polygons.push_back( nullptr ); diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake index 41dd8e82fc..288b829048 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/CMakeLists_files.cmake @@ -1,6 +1,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimCellFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCellFilterCollection.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCellFilterTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCellRangeFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimCombinedFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimDataFilterCollection.cpp diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp index 37ad312a4f..534d3f4f81 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.cpp @@ -199,6 +199,14 @@ caf::AppEnum RimCellFilter::filterMode() const return m_filterMode(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCellFilter::setFilterMode( FilterModeType filterMode ) +{ + m_filterMode = filterMode; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -322,7 +330,10 @@ QList RimCellFilter::calculateValueOptions( const caf::P //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RimCellFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t /*timeStepIndex*/ ) +void RimCellFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t /*timeStepIndex*/, + RimEclipseCase* /*sourceCaseOverride*/ ) { if ( cellVisibility == nullptr || grid == nullptr ) return; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h index dfcab766b0..14ae3bd7ec 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilter.h @@ -79,6 +79,7 @@ class RimCellFilter : public RimCheckableNamedObject virtual bool isFilterEnabled() const; caf::AppEnum filterMode() const; + void setFilterMode( FilterModeType filterMode ); QString modeString() const; bool propagateToSubGrids() const; @@ -97,7 +98,13 @@ class RimCellFilter : public RimCheckableNamedObject // Unified evaluation: take an incoming per-cell visibility mask and hide the cells that // this filter rejects (respecting its own INCLUDE/EXCLUDE mode). The default implementation // bridges the legacy range/index dispatch so existing subclasses do not need to override. - virtual void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ); + // When sourceCaseOverride is set, property filters evaluate against that case's results instead + // of the case the filter is bound to, allowing the same filter definition to be evaluated + // against each case in an ensemble. + virtual void applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride = nullptr ); protected: caf::PdmFieldHandle* userDescriptionField() override; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp new file mode 100644 index 0000000000..4465ba84bd --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.cpp @@ -0,0 +1,107 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimCellFilterTools.h" + +#include "RigEclipseCaseData.h" +#include "RigGridBase.h" +#include "RigLocalGrid.h" +#include "RigMainGrid.h" + +#include "RimCellFilter.h" +#include "RimEclipseCase.h" + +//-------------------------------------------------------------------------------------------------- +/// Evaluate the filter against the given case for all grids, and return the visibility indexed by +/// reservoir cell index. Property filters are evaluated against the given case's own result values. +//-------------------------------------------------------------------------------------------------- +cvf::ref + RimCellFilterTools::computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex ) +{ + if ( !filter || !eclipseCase || !eclipseCase->eclipseCaseData() || !eclipseCase->eclipseCaseData()->mainGrid() ) return nullptr; + + RigEclipseCaseData* caseData = eclipseCase->eclipseCaseData(); + + cvf::ref reservoirVisibility = new cvf::UByteArray( caseData->mainGrid()->totalCellCount() ); + reservoirVisibility->setAll( false ); + + const bool isInclude = ( filter->filterMode() == RimCellFilter::INCLUDE ); + + // Grid local masks are kept to allow propagation of parent grid visibility into LGRs + std::vector> gridMasks( caseData->gridCount() ); + + for ( size_t gridIndex = 0; gridIndex < caseData->gridCount(); gridIndex++ ) + { + RigGridBase* grid = caseData->grid( gridIndex ); + + gridMasks[gridIndex] = new cvf::UByteArray( grid->cellCount() ); + cvf::UByteArray& gridMask = *gridMasks[gridIndex]; + gridMask.setAll( true ); + + if ( filter->isRangeFilter() ) + { + // Range filters evaluate only on their target grid. On other grids an INCLUDE filter + // contributes no cells, while an EXCLUDE filter removes none. + const bool isTargetGrid = ( filter->gridIndex() == static_cast( gridIndex ) ); + if ( isTargetGrid ) + { + filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase ); + } + else + { + gridMask.setAll( !isInclude ); + } + } + else + { + // Index filters (polygon, user defined) and property filters evaluate on all grids + filter->applyToCellVisibility( &gridMask, grid, timeStepIndex, eclipseCase ); + } + + // Cells in LGRs follow the visibility of their parent grid cell, as in the filtered geometry + // of a 3d view. Geometry based filters (range and index, e.g. an INDEX_K polygon) can fail to + // select the refined cells directly on a fine LGR, so propagate the parent grid visibility. + // Property filters evaluate each cell against its own result value and must be left untouched. + const bool isGeometryFilter = filter->isRangeFilter() || filter->isIndexFilter(); + if ( isGeometryFilter && !grid->isMainGrid() ) + { + auto localGrid = static_cast( grid ); + const cvf::UByteArray& parentMask = *gridMasks[localGrid->parentGrid()->gridIndex()]; + + for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ ) + { + const size_t parentCellIndex = grid->cell( localIdx ).parentCellIndex(); + if ( isInclude ) + { + gridMask[localIdx] = gridMask[localIdx] || parentMask[parentCellIndex]; + } + else + { + gridMask[localIdx] = gridMask[localIdx] && parentMask[parentCellIndex]; + } + } + } + + for ( size_t localIdx = 0; localIdx < grid->cellCount(); localIdx++ ) + { + reservoirVisibility->set( grid->reservoirCellIndex( localIdx ), gridMask[localIdx] ); + } + } + + return reservoirVisibility; +} diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.h new file mode 100644 index 0000000000..56849d2ee6 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellFilterTools.h @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "cvfArray.h" +#include "cvfObject.h" + +class RimCellFilter; +class RimEclipseCase; + +//================================================================================================== +/// +//================================================================================================== +class RimCellFilterTools +{ +public: + static cvf::ref computeReservoirCellVisibility( RimCellFilter* filter, RimEclipseCase* eclipseCase, size_t timeStepIndex ); +}; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp index a1952faef0..8f6ffc3ac0 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.cpp @@ -104,7 +104,10 @@ void RimCombinedFilter::onGridChanged() /// INCLUDE/EXCLUDE mode inside applyToCellVisibility), AND/OR combine the masks, then apply this /// combined filter's INCLUDE/EXCLUDE mode onto the incoming cellVisibility. //-------------------------------------------------------------------------------------------------- -void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) +void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride ) { if ( cellVisibility == nullptr || grid == nullptr ) return; @@ -125,7 +128,7 @@ void RimCombinedFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, { cvf::UByteArray childMask( n ); childMask.setAll( 1 ); - child->applyToCellVisibility( &childMask, grid, timeStepIndex ); + child->applyToCellVisibility( &childMask, grid, timeStepIndex, sourceCaseOverride ); if ( m_combineMode() == CombineMode::AND ) { diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h index 20f0fc3530..964c37335c 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCombinedFilter.h @@ -49,7 +49,10 @@ class RimCombinedFilter : public RimCellFilter bool isFilterEnabled() const override; void onGridChanged() override; - void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) override; + void applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride = nullptr ) override; // Bridge from legacy collection dispatch: combined filter is declared INDEX-type, so the cell // filter collection's evaluation path (if ever called) would route through updateCellIndexFilter. diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp index 1df8136143..82fd2c8ca5 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp @@ -18,11 +18,15 @@ #include "RimDataFilterCollection.h" +#include "Polygons/RimPolygon.h" +#include "Polygons/RimPolygonCollection.h" + #include "RimCase.h" #include "RimCellRangeFilter.h" #include "RimCombinedFilter.h" #include "RimEclipsePropertyFilter.h" #include "RimEclipseResultDefinition.h" +#include "RimTools.h" #include "cafCmdFeatureMenuBuilder.h" #include "cafPdmFieldScriptingCapability.h" @@ -80,6 +84,7 @@ void RimDataFilterCollection::removeFilter( RimCellFilter* f ) if ( !f ) return; deleteItem( f ); filtersChanged.send(); + updateOwnerEditors(); } //-------------------------------------------------------------------------------------------------- @@ -157,9 +162,9 @@ void RimDataFilterCollection::onItemsChanged() } filtersChanged.send(); - // The collection node is hidden from the case tree while empty, so refresh the owner case to + // The collection node is hidden from the owner's tree while empty, so refresh the owner to // re-run its defineUiTreeOrdering and add the node once the first filter appears. - if ( auto* c = ownerCase() ) c->updateConnectedEditors(); + updateOwnerEditors(); } //-------------------------------------------------------------------------------------------------- @@ -171,8 +176,22 @@ void RimDataFilterCollection::onChildDeleted( caf::PdmChildArrayFieldHandle* chi updateConnectedEditors(); filtersChanged.send(); - // Refresh the owner case so its defineUiTreeOrdering re-runs and drops the node once empty. - if ( auto* c = ownerCase() ) c->updateConnectedEditors(); + // Refresh the owner so its defineUiTreeOrdering re-runs and drops the node once empty. + updateOwnerEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// Refresh the object owning this collection in the project tree. Note that the owner is not +/// necessarily the source case: for a collection in a grid ensemble, the source case is the main +/// case of the ensemble, while the tree node is owned by the ensemble. +//-------------------------------------------------------------------------------------------------- +void RimDataFilterCollection::updateOwnerEditors() +{ + caf::PdmObjectHandle* owner = parentField() ? parentField()->ownerObject() : nullptr; + if ( owner && owner->uiCapability() ) + { + owner->uiCapability()->updateConnectedEditors(); + } } //-------------------------------------------------------------------------------------------------- @@ -193,6 +212,21 @@ void RimDataFilterCollection::appendMenuItems( caf::CmdFeatureMenuBuilder& menuB { menuBuilder << "RicEclipsePropertyFilterNewFeature"; menuBuilder << "Separator"; + + menuBuilder.subMenuStart( "Polygon Filter", QIcon( ":/CellFilter_Polygon.png" ) ); + { + auto polygonCollection = RimTools::polygonCollection(); + for ( auto p : polygonCollection->allPolygons() ) + { + if ( !p ) continue; + + menuBuilder.addCmdFeatureWithUserData( "RicNewPolygonFilterFeature", p->name(), QVariant::fromValue( static_cast( p ) ) ); + } + } + menuBuilder.subMenuEnd(); + + menuBuilder << "RicNewPolygonFilterFeature"; + menuBuilder << "Separator"; menuBuilder.subMenuStart( "Range Filter" ); menuBuilder << "RicNewRangeFilterSliceIFeature"; menuBuilder << "RicNewRangeFilterSliceJFeature"; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h index d9a84252f1..c0e9c43f13 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.h @@ -72,6 +72,7 @@ class RimDataFilterCollection : public caf::PdmObjectCollection private: void connectChildSignal( RimCellFilter* child ); void onChildFilterChanged( const caf::SignalEmitter* emitter ); + void updateOwnerEditors(); caf::PdmPtrField m_srcCase; }; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp index eb5181e188..097e2ed6fe 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp @@ -724,21 +724,36 @@ void RimEclipsePropertyFilter::initAfterRead() /// uniformly; the body is extracted from the former inline loop in /// RivReservoirViewPartMgr::computePropertyVisibility. //-------------------------------------------------------------------------------------------------- -void RimEclipsePropertyFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) +void RimEclipsePropertyFilter::applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride ) { if ( cellVisibility == nullptr || grid == nullptr ) return; if ( !isActive() || !resultDefinition()->hasResult() ) return; - resultDefinition()->loadResult(); - - // The result definition carries its own eclipse case binding (set via setEclipseCase). Prefer - // that, since case-level data filters have no view-side property-filter-collection ancestor. - RimEclipseCase* ec = resultDefinition()->eclipseCase(); - if ( !ec ) + RimEclipseCase* ec = sourceCaseOverride; + if ( ec ) { - auto* container = parentContainer(); - auto* view = container ? container->reservoirView() : nullptr; - ec = view ? view->eclipseCase() : nullptr; + // Evaluate against the override case's results. Make sure the result is loaded for that case. + if ( auto cellResultsData = ec->results( resultDefinition()->porosityModel() ) ) + { + cellResultsData->ensureKnownResultLoaded( resultDefinition()->eclipseResultAddress() ); + } + } + else + { + resultDefinition()->loadResult(); + + // The result definition carries its own eclipse case binding (set via setEclipseCase). Prefer + // that, since case-level data filters have no view-side property-filter-collection ancestor. + ec = resultDefinition()->eclipseCase(); + if ( !ec ) + { + auto* container = parentContainer(); + auto* view = container ? container->reservoirView() : nullptr; + ec = view ? view->eclipseCase() : nullptr; + } } if ( !ec ) return; RigEclipseCaseData* eclipseCase = ec->eclipseCaseData(); diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h index fb900cb3fe..ed04eb15a7 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.h @@ -59,7 +59,10 @@ class RimEclipsePropertyFilter : public RimPropertyFilter, public RimFieldQuickA void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; void initAfterRead() override; - void applyToCellVisibility( cvf::UByteArray* cellVisibility, const RigGridBase* grid, size_t timeStepIndex ) override; + void applyToCellVisibility( cvf::UByteArray* cellVisibility, + const RigGridBase* grid, + size_t timeStepIndex, + RimEclipseCase* sourceCaseOverride = nullptr ) override; void updateUiFieldsFromActiveResult(); diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp index 4ba2d3810c..c7bfd09799 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.cpp @@ -35,8 +35,12 @@ #include "RigResultAccessor.h" #include "RigResultAccessorFactory.h" #include "RigStatisticsMath.h" +#include "RigStatisticsTools.h" #include "RimCaseCollection.h" +#include "RimCellFilter.h" +#include "RimCellFilterTools.h" +#include "RimDataFilterCollection.h" #include "RimEclipseCase.h" #include "RimEclipseCaseCollection.h" #include "RimEclipseCaseEnsemble.h" @@ -51,6 +55,7 @@ #include "RimOilField.h" #include "RimProject.h" #include "RimReloadCaseTools.h" +#include "RimReservoirGridEnsemble.h" #include "RimResultSelectionUi.h" #include "RimTools.h" @@ -83,6 +88,14 @@ void caf::AppEnum::setUp() addItem( RimGridCalculation::AdditionalCasesType::ALL_CASES, "NONE", "All Cases" ); setDefault( RimGridCalculation::AdditionalCasesType::NONE ); } +template <> +void caf::AppEnum::setUp() +{ + addItem( RimGridCalculation::FilterType::NO_FILTER, "NO_FILTER", "None" ); + addItem( RimGridCalculation::FilterType::CELL_FILTER_VIEW, "CELL_FILTER_VIEW", "Cell Filter View" ); + addItem( RimGridCalculation::FilterType::DATA_FILTER, "DATA_FILTER", "Data Filter" ); + setDefault( RimGridCalculation::FilterType::NO_FILTER ); +} }; // namespace caf //-------------------------------------------------------------------------------------------------- @@ -91,7 +104,9 @@ void caf::AppEnum::setUp() RimGridCalculation::RimGridCalculation() { CAF_PDM_InitObject( "RimGridCalculation", ":/octave.png", "Calculation", "" ); + CAF_PDM_InitFieldNoDefault( &m_filterType, "FilterType", "Filter Type" ); CAF_PDM_InitFieldNoDefault( &m_cellFilterView, "VisibleCellView", "Filter by 3d View Visibility" ); + CAF_PDM_InitFieldNoDefault( &m_dataFilter, "DataFilter", "Data Filter" ); CAF_PDM_InitFieldNoDefault( &m_defaultValueType, "DefaultValueType", "Non-visible Cell Value" ); CAF_PDM_InitField( &m_defaultValue, "DefaultValue", 0.0, "Custom Value" ); CAF_PDM_InitFieldNoDefault( &m_destinationCase, "DestinationCase", "Destination Case" ); @@ -173,9 +188,18 @@ RimGridCalculationVariable* RimGridCalculation::createVariable() //-------------------------------------------------------------------------------------------------- bool RimGridCalculation::calculate() { + const bool useCellFilterView = ( m_filterType() == FilterType::CELL_FILTER_VIEW ) && m_cellFilterView() != nullptr; + const bool useDataFilter = ( m_filterType() == FilterType::DATA_FILTER ); + + if ( useDataFilter && m_dataFilter() == nullptr ) + { + RiuMessageDialog::showError( nullptr, "Grid Property Calculator", "The filter type is 'Data Filter', but no data filter is selected." ); + return false; + } + // Equal grid size is required if there is more than one grid case in the expression. If a cell filter view is active, the visibility is - // based on one view and reused for all other grid models, and requires equal grid size. - bool checkIfGridSizeIsEqual = ( !allSourceCasesAreEqualToDestinationCase() || m_cellFilterView != nullptr ) && + // based on one view and reused for all other grid models, and requires equal grid size. A data filter is evaluated per case. + bool checkIfGridSizeIsEqual = ( !allSourceCasesAreEqualToDestinationCase() || useCellFilterView ) && m_additionalCasesType != AdditionalCasesType::ENSEMBLE; for ( auto calculationCase : outputEclipseCases() ) @@ -242,10 +266,13 @@ bool RimGridCalculation::calculate() } } - cvf::UByteArray* inputValueVisibilityFilter = nullptr; - if ( m_cellFilterView() ) + // The visible cells of the view, including the effect of property filters evaluated on the view's + // case. Cells inside the filters that are inactive in the view's case are not part of the mask; use + // the data filter type to evaluate filters per calculation case. + cvf::ref inputValueVisibilityFilter; + if ( useCellFilterView ) { - inputValueVisibilityFilter = m_cellFilterView()->currentTotalCellVisibility().p(); + inputValueVisibilityFilter = m_cellFilterView()->currentTotalCellVisibility(); } std::optional> timeSteps = std::nullopt; @@ -262,7 +289,7 @@ bool RimGridCalculation::calculate() } bool evaluateDependentCalculations = true; - return calculateForCases( outputEclipseCases(), inputValueVisibilityFilter, timeSteps, evaluateDependentCalculations ); + return calculateForCases( outputEclipseCases(), inputValueVisibilityFilter.p(), timeSteps, evaluateDependentCalculations ); } //-------------------------------------------------------------------------------------------------- @@ -351,9 +378,14 @@ void RimGridCalculation::defineUiOrdering( QString uiConfigName, caf::PdmUiOrder caf::PdmUiGroup* filterGroup = uiOrdering.addNewGroup( "Cell Filter" ); filterGroup->setCollapsedByDefault(); - filterGroup->add( &m_cellFilterView ); + filterGroup->add( &m_filterType ); - if ( m_cellFilterView() != nullptr ) + if ( m_filterType() == FilterType::CELL_FILTER_VIEW ) filterGroup->add( &m_cellFilterView ); + if ( m_filterType() == FilterType::DATA_FILTER ) filterGroup->add( &m_dataFilter ); + + const bool hasFilter = ( m_filterType() == FilterType::CELL_FILTER_VIEW && m_cellFilterView() != nullptr ) || + ( m_filterType() == FilterType::DATA_FILTER && m_dataFilter() != nullptr ); + if ( hasFilter ) { filterGroup->add( &m_defaultValueType ); @@ -409,6 +441,38 @@ QList RimGridCalculation::calculateValueOptions( const c } } } + else if ( fieldNeedingOptions == &m_dataFilter ) + { + options.push_back( caf::PdmOptionItemInfo( "None", nullptr ) ); + + if ( m_destinationCase() ) + { + if ( auto dataFilterCollection = m_destinationCase()->dataFilterCollection() ) + { + for ( RimCellFilter* filter : dataFilterCollection->filters() ) + { + if ( !filter ) continue; + options.push_back( caf::PdmOptionItemInfo( filter->fullName(), filter, false, filter->uiIconProvider() ) ); + } + } + + // Also offer the data filters of the grid ensemble the destination case belongs to, if any + if ( auto gridEnsemble = m_destinationCase()->firstAncestorOfType() ) + { + if ( auto dataFilterCollection = gridEnsemble->dataFilterCollection() ) + { + for ( RimCellFilter* filter : dataFilterCollection->filters() ) + { + if ( !filter ) continue; + options.push_back( caf::PdmOptionItemInfo( QString( "%1 : %2" ).arg( gridEnsemble->name(), filter->fullName() ), + filter, + false, + filter->uiIconProvider() ) ); + } + } + } + } + } else if ( fieldNeedingOptions == &m_destinationCase ) { RimEclipseCase* firstInputCase = nullptr; @@ -493,6 +557,27 @@ void RimGridCalculation::initAfterRead() } if ( m_applyToAllCases_OBSOLETE ) m_additionalCasesType = RimGridCalculation::AdditionalCasesType::ALL_CASES; + + // Projects from before the filter type field was introduced only have the cell filter view. The view + // pointer is cleared when the filter type is changed away from CELL_FILTER_VIEW, so a set view pointer + // implies the cell filter view mode. + if ( m_cellFilterView() != nullptr ) m_filterType = FilterType::CELL_FILTER_VIEW; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimGridCalculation::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) +{ + RimUserDefinedCalculation::fieldChangedByUi( changedField, oldValue, newValue ); + + if ( changedField == &m_filterType ) + { + if ( m_filterType() != FilterType::CELL_FILTER_VIEW ) m_cellFilterView = nullptr; + if ( m_filterType() != FilterType::DATA_FILTER ) m_dataFilter = nullptr; + + updateConnectedEditors(); + } } //-------------------------------------------------------------------------------------------------- @@ -680,12 +765,13 @@ void RimGridCalculation::replaceFilteredValuesWithVector( const std::vector visibility, - std::vector& resultValues, - RigActiveCellInfo* activeCellInfo ) +size_t RimGridCalculation::replaceFilteredValuesWithDefaultValue( double defaultValue, + cvf::ref visibility, + std::vector& resultValues, + RigActiveCellInfo* activeCellInfo ) { auto activeReservoirCellIndices = activeCellInfo->activeReservoirCellIndices(); @@ -693,21 +779,47 @@ void RimGridCalculation::replaceFilteredValuesWithDefaultValue( double CAF_ASSERT( numActiveCells == (int)resultValues.size() ); -#pragma omp parallel for + size_t replacedCount = 0; + +#pragma omp parallel for reduction( + : replacedCount ) for ( int i = 0; i < numActiveCells; i++ ) { const auto reservoirCellIndex = activeReservoirCellIndices[i]; if ( !visibility->val( reservoirCellIndex.value() ) ) { resultValues[i] = defaultValue; + replacedCount++; + } + } + + return replacedCount; +} + +//-------------------------------------------------------------------------------------------------- +/// Replace undefined values (infinity and NaN) with the given default value. Returns the number of +/// replaced values. +//-------------------------------------------------------------------------------------------------- +size_t RimGridCalculation::replaceInvalidValuesWithDefaultValue( double defaultValue, std::vector& values ) +{ + size_t replacedCount = 0; + +#pragma omp parallel for reduction( + : replacedCount ) + for ( int i = 0; i < static_cast( values.size() ); i++ ) + { + if ( RigStatisticsTools::isInvalidNumber( values[i] ) ) + { + values[i] = defaultValue; + replacedCount++; } } + + return replacedCount; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RimGridCalculation::filterResults( RimGridView* cellFilterView, +void RimGridCalculation::filterResults( cvf::UByteArray* visibility, const std::vector>& values, size_t timeStep, RimGridCalculation::DefaultValueType defaultValueType, @@ -717,8 +829,6 @@ void RimGridCalculation::filterResults( RimGridView* RimEclipseCase* outputEclipseCase ) const { - auto visibility = cellFilterView->currentTotalCellVisibility(); - auto activeCellInfo = outputEclipseCase->eclipseCaseData()->activeCellInfo( porosityModel ); if ( defaultValueType == RimGridCalculation::DefaultValueType::FROM_PROPERTY ) @@ -898,12 +1008,23 @@ bool RimGridCalculation::calculateForCases( const std::vector& scalarResultFrames->resize( timeStepCount ); std::vector aggregatedValuesOneTimeStep; + std::vector invalidValueCountPerVariable( m_variables.size(), 0 ); for ( size_t tsId = 0; tsId < timeStepCount; tsId++ ) { // Skip time steps that are not in the list of time steps to calculate if ( timeSteps && std::find( timeSteps->begin(), timeSteps->end(), tsId ) == timeSteps->end() ) continue; + // A data filter is evaluated per calculation case and time step, using the case's own active + // cells and result values. The visibility filter provided by the caller is used as-is. + cvf::ref dataFilterVisibility; + cvf::UByteArray* visibilityFilter = inputValueVisibilityFilter; + if ( m_filterType() == FilterType::DATA_FILTER && m_dataFilter() ) + { + dataFilterVisibility = RimCellFilterTools::computeReservoirCellVisibility( m_dataFilter(), calculationCase, tsId ); + visibilityFilter = dataFilterVisibility.p(); + } + std::vector> dataForAllVariables; for ( size_t i = 0; i < m_variables.size(); i++ ) { @@ -919,11 +1040,32 @@ bool RimGridCalculation::calculateForCases( const std::vector& { RiaLogging::error( std::format( " No data found for variable '{}'.", v->name() ) ); } - else if ( inputValueVisibilityFilter && hasAggregationExpression ) + else if ( hasAggregationExpression ) { - const double defaultValue = 0.0; - auto activeCellInfo = calculationCase->eclipseCaseData()->activeCellInfo( porosityModel ); - replaceFilteredValuesWithDefaultValue( defaultValue, inputValueVisibilityFilter, dataForVariable, activeCellInfo ); + const double defaultValue = 0.0; + size_t nonVisibleCount = 0; + if ( visibilityFilter ) + { + auto activeCellInfo = calculationCase->eclipseCaseData()->activeCellInfo( porosityModel ); + nonVisibleCount = + replaceFilteredValuesWithDefaultValue( defaultValue, visibilityFilter, dataForVariable, activeCellInfo ); + } + + // Aggregation functions include all values in the vector. Replace undefined values with the + // default value to avoid contaminating aggregated values like sum() with infinity. Undefined + // values are ignored by other statistics computations, like the histogram in the 3d view. + const size_t invalidCount = replaceInvalidValuesWithDefaultValue( defaultValue, dataForVariable ); + invalidValueCountPerVariable[i] += invalidCount; + + RiaLogging::debug( std::format( " Case '{}', time step {}: variable '{}': {} of {} values used in aggregation " + "({} non-visible and {} undefined values replaced with 0.0)", + calculationCase->caseUserDescription().toStdString(), + tsId, + v->name().toStdString(), + dataForVariable.size() - nonVisibleCount - invalidCount, + dataForVariable.size(), + nonVisibleCount, + invalidCount ) ); } dataForAllVariables.push_back( dataForVariable ); @@ -967,9 +1109,9 @@ bool RimGridCalculation::calculateForCases( const std::vector& } } - if ( m_cellFilterView() && !resultValues.empty() ) + if ( visibilityFilter && !resultValues.empty() ) { - filterResults( m_cellFilterView(), + filterResults( visibilityFilter, dataForAllVariables, tsId, m_defaultValueType(), @@ -995,6 +1137,18 @@ bool RimGridCalculation::calculateForCases( const std::vector& calculationCase->updateResultAddressCollection(); } + for ( size_t i = 0; i < m_variables.size(); i++ ) + { + if ( invalidValueCountPerVariable[i] > 0 ) + { + RiaLogging::warning( std::format( " Variable '{}': {} undefined input values were replaced with 0.0 for case '{}'. The " + "input result may be missing data for some cells or time steps.", + m_variables[i]->name().toStdString(), + invalidValueCountPerVariable[i], + calculationCase->caseUserDescription().toStdString() ) ); + } + } + if ( hasAggregationExpression ) { QString txt = " " + calculationCase->caseUserDescription(); diff --git a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h index fc162ceeb6..ecd096cb7f 100644 --- a/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h +++ b/ApplicationLibCode/ProjectDataModel/RimGridCalculation.h @@ -30,6 +30,7 @@ #include +class RimCellFilter; class RimEclipseCase; class RimGridView; class RigEclipseResultAddress; @@ -61,6 +62,13 @@ class RimGridCalculation : public RimUserDefinedCalculation ALL_CASES }; + enum class FilterType + { + NO_FILTER, + CELL_FILTER_VIEW, + DATA_FILTER + }; + RimGridCalculation(); bool preCalculate() const override; @@ -86,6 +94,8 @@ class RimGridCalculation : public RimUserDefinedCalculation RimGridCalculationVariable* createVariable() override; + static size_t replaceInvalidValuesWithDefaultValue( double defaultValue, std::vector& values ); + protected: void onChildrenUpdated( caf::PdmChildArrayFieldHandle* childArray, std::vector& updatedObjects ) override; @@ -104,7 +114,7 @@ class RimGridCalculation : public RimUserDefinedCalculation RimEclipseCase* sourceCase, RimEclipseCase* destinationCase ) const; - void filterResults( RimGridView* cellFilterView, + void filterResults( cvf::UByteArray* visibility, const std::vector>& values, size_t timeStep, RimGridCalculation::DefaultValueType defaultValueType, @@ -118,10 +128,10 @@ class RimGridCalculation : public RimUserDefinedCalculation std::vector& resultValues, RigActiveCellInfo* activeCellInfo ); - static void replaceFilteredValuesWithDefaultValue( double defaultValue, - cvf::ref visibility, - std::vector& resultValues, - RigActiveCellInfo* activeCellInfo ); + static size_t replaceFilteredValuesWithDefaultValue( double defaultValue, + cvf::ref visibility, + std::vector& resultValues, + RigActiveCellInfo* activeCellInfo ); using DefaultValueConfig = std::pair; DefaultValueConfig defaultValueConfiguration() const; @@ -131,6 +141,7 @@ class RimGridCalculation : public RimUserDefinedCalculation void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; void initAfterRead() override; + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; private: void onVariableUpdated( const SignalEmitter* emitter ); @@ -140,7 +151,9 @@ class RimGridCalculation : public RimUserDefinedCalculation static std::pair createStatisticsText( const std::vector>& values ); private: + caf::PdmField> m_filterType; caf::PdmPtrField m_cellFilterView; + caf::PdmPtrField m_dataFilter; caf::PdmField> m_defaultValueType; caf::PdmField m_defaultValue; caf::PdmPtrField m_destinationCase; diff --git a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp index 1c878e4a2b..e58e2832c5 100644 --- a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.cpp @@ -18,6 +18,8 @@ #include "RimReservoirGridEnsemble.h" +#include "cafPdmUiTreeOrdering.h" + #include "RiaLogging.h" #include "RiaQStringFormatter.h" #include "RiaResultNames.h" @@ -37,6 +39,7 @@ #include "Formations/RimFormationNamesCollection.h" #include "Rim2dIntersectionViewCollection.h" #include "RimCaseCollection.h" +#include "RimDataFilterCollection.h" #include "RimEclipseCase.h" #include "RimEclipseCellColors.h" #include "RimEclipseResultCase.h" @@ -108,6 +111,9 @@ RimReservoirGridEnsemble::RimReservoirGridEnsemble() m_statisticsCaseCollection->uiCapability()->setUiName( "Derived Statistics" ); m_statisticsCaseCollection->uiCapability()->setUiIconFromResourceString( ":/Histograms16x16.png" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_dataFilterCollection, "DataFilterCollection", "Data Filters" ); + m_dataFilterCollection = new RimDataFilterCollection; + CAF_PDM_InitFieldNoDefault( &m_viewCollection, "ViewCollection", "Views" ); m_viewCollection = new RimEclipseViewCollection; m_viewCollection->setEclipseCaseProvider( [this]() { return this->cases(); } ); @@ -192,6 +198,7 @@ void RimReservoirGridEnsemble::addCase( RimEclipseCase* reservoir ) clearActiveCellUnions(); clearStatisticsResults(); updateMainGridAndActiveCellsForStatisticsCases(); + updateDataFilterCollectionCase(); } //-------------------------------------------------------------------------------------------------- @@ -211,6 +218,7 @@ void RimReservoirGridEnsemble::removeCase( RimEclipseCase* reservoir ) clearActiveCellUnions(); clearStatisticsResults(); updateMainGridAndActiveCellsForStatisticsCases(); + updateDataFilterCollectionCase(); } //-------------------------------------------------------------------------------------------------- @@ -634,6 +642,13 @@ void RimReservoirGridEnsemble::appendMenuItems( caf::CmdFeatureMenuBuilder& menu { menuBuilder << "RicNewStatisticsCaseFeature"; } + + // The "Data Filters" collection node is hidden while empty, so expose the filter creation + // commands on the ensemble node to allow creating the first filter. + menuBuilder << "Separator"; + menuBuilder.subMenuStart( "Data Filters", QIcon( ":/CellFilter.png" ) ); + m_dataFilterCollection->appendMenuItems( menuBuilder ); + menuBuilder.subMenuEnd(); } //-------------------------------------------------------------------------------------------------- @@ -645,6 +660,24 @@ RimFormationNames* RimReservoirGridEnsemble::activeFormationNames() const return m_activeFormationNames(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimDataFilterCollection* RimReservoirGridEnsemble::dataFilterCollection() const +{ + return m_dataFilterCollection(); +} + +//-------------------------------------------------------------------------------------------------- +/// The filters in the collection need a source case for configuration (result meta data, grid +/// geometry). Use the main case of the ensemble. Evaluation of the filters is done per case, see +/// RimCellFilterTools::computeReservoirCellVisibility. +//-------------------------------------------------------------------------------------------------- +void RimReservoirGridEnsemble::updateDataFilterCollectionCase() +{ + if ( m_dataFilterCollection() ) m_dataFilterCollection->setCase( mainCase() ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -711,6 +744,12 @@ void RimReservoirGridEnsemble::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiT uiTreeOrdering.add( &m_caseCollection ); uiTreeOrdering.add( &m_statisticsCaseCollection ); + + if ( m_dataFilterCollection() && m_dataFilterCollection()->shouldBeVisibleInTree() ) + { + uiTreeOrdering.add( m_dataFilterCollection() ); + } + uiTreeOrdering.add( &m_viewCollection ); for ( auto eclipseCase : cases() ) @@ -779,6 +818,7 @@ void RimReservoirGridEnsemble::initAfterRead() m_viewCollection->setEclipseCaseProvider( [this]() { return this->cases(); } ); } + updateDataFilterCollectionCase(); updateStatisticsVisibility(); } diff --git a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h index a4e04e490f..b62c0ac029 100644 --- a/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h +++ b/ApplicationLibCode/ProjectDataModel/RimReservoirGridEnsemble.h @@ -35,6 +35,7 @@ class RigActiveCellInfo; class RigMainGrid; class RimCaseCollection; +class RimDataFilterCollection; class RimEclipseCase; class RimEclipseStatisticsCase; class RimEclipseView; @@ -97,6 +98,9 @@ class RimReservoirGridEnsemble : public RimNamedObject, public RimReservoirGridE // Formation names RimFormationNames* activeFormationNames() const override; + // Data filters + RimDataFilterCollection* dataFilterCollection() const; + // Statistics RimCaseCollection* statisticsCaseCollection() const override; RimEclipseStatisticsCase* createAndAppendStatisticsCase() override; @@ -143,6 +147,7 @@ class RimReservoirGridEnsemble : public RimNamedObject, public RimReservoirGridE void loadGridsInSharedMode(); void loadGridsInIndividualMode(); void updateGridModeToolTip(); + void updateDataFilterCollectionCase(); private: // File set reference @@ -158,6 +163,9 @@ class RimReservoirGridEnsemble : public RimNamedObject, public RimReservoirGridE caf::PdmChildField m_caseCollection; caf::PdmChildField m_statisticsCaseCollection; + // Data filters + caf::PdmChildField m_dataFilterCollection; + // Grid mode caf::PdmField m_autoDetectGridType; caf::PdmField> m_gridMode; diff --git a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp index 5f3a664120..c3266618bd 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.cpp @@ -1504,7 +1504,15 @@ size_t RigCaseCellResultsData::findOrLoadKnownScalarResult( const RigEclipseResu if ( isDataPresent( scalarResultIndex ) ) { - return scalarResultIndex; + // Data for some time steps can have been released to reduce memory usage, see + // RimGridCalculation::getActiveCellValues(). Results computed by ResInsight can not be reloaded from + // file, so continue to recompute them if data is missing for any time step. + const bool mustRecompute = isResultComputedByResInsight( resultName, scalarResultIndex ) && + hasEmptyDataForAnyTimeStep( scalarResultIndex ); + if ( !mustRecompute ) + { + return scalarResultIndex; + } } if ( resultName == RiaResultNames::soil() ) @@ -1762,6 +1770,21 @@ size_t RigCaseCellResultsData::findOrLoadKnownScalarResultForTimeStep( const Rig if ( scalarResultIndex == cvf::UNDEFINED_SIZE_T ) return cvf::UNDEFINED_SIZE_T; if ( type == RiaDefines::ResultCatType::GENERATED ) return scalarResultIndex; + // Results computed by ResInsight (e.g. riOILVOLUME) can not be read from file for a single time step. + // Delegate to findOrLoadKnownScalarResult(), which computes all time steps using the result + // calculators. Skip the delegation if data is already present for the requested time step, to avoid + // repeated recomputation when data for other time steps is released during a calculation. + if ( isResultComputedByResInsight( resultName, scalarResultIndex ) ) + { + const auto& valuesAllTimeSteps = m_cellScalarResults[scalarResultIndex]; + if ( timeStepIndex < valuesAllTimeSteps.size() && !valuesAllTimeSteps[timeStepIndex].empty() ) + { + return scalarResultIndex; + } + + return findOrLoadKnownScalarResult( resVarAddr ); + } + if ( m_readerInterface.notNull() ) { size_t timeStepCount = infoForEachResultIndex()[scalarResultIndex].timeStepInfos().size(); @@ -3143,6 +3166,28 @@ bool RigCaseCellResultsData::isDataPresent( size_t scalarResultIndex ) const return allocatedValueCount( scalarResultIndex ) > 0; } +//-------------------------------------------------------------------------------------------------- +/// Returns true for results that are computed by ResInsight and can not be read from file +//-------------------------------------------------------------------------------------------------- +bool RigCaseCellResultsData::isResultComputedByResInsight( const QString& resultName, size_t scalarResultIndex ) const +{ + return mustBeCalculated( scalarResultIndex ) || resultName == RiaResultNames::riCellVolumeResultName() || + resultName == RiaResultNames::riOilVolumeResultName() || resultName == RiaResultNames::riPorvSoil() || + resultName == RiaResultNames::riPorvSgas() || resultName == RiaResultNames::riPorvSoilSgas(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RigCaseCellResultsData::hasEmptyDataForAnyTimeStep( size_t scalarResultIndex ) const +{ + if ( scalarResultIndex >= resultCount() ) return false; + + const std::vector>& valuesAllTimeSteps = m_cellScalarResults[scalarResultIndex]; + + return std::any_of( valuesAllTimeSteps.begin(), valuesAllTimeSteps.end(), []( const auto& values ) { return values.empty(); } ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h index 2108510267..c181cefaab 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h +++ b/ApplicationLibCode/ReservoirDataModel/RigCaseCellResultsData.h @@ -215,6 +215,9 @@ class RigCaseCellResultsData : public cvf::Object bool mustBeCalculated( size_t scalarResultIndex ) const; void setMustBeCalculated( size_t scalarResultIndex ); + bool isResultComputedByResInsight( const QString& resultName, size_t scalarResultIndex ) const; + bool hasEmptyDataForAnyTimeStep( size_t scalarResultIndex ) const; + void computeSOILForTimeStep( size_t timeStepIndex ); void testAndComputeSgasForTimeStep( size_t timeStepIndex ); diff --git a/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp b/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp index 6d7ee8aa87..94a515a6b7 100644 --- a/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp +++ b/ApplicationLibCode/ReservoirDataModel/RigEclipseNativeVisibleCellsStatCalc.cpp @@ -19,6 +19,7 @@ #include "RigEclipseNativeVisibleCellsStatCalc.h" +#include "RiaLogging.h" #include "RiaResultNames.h" #include "RigActiveCellInfo.h" @@ -27,6 +28,7 @@ #include "RigWeightedMeanCalc.h" #include +#include //-------------------------------------------------------------------------------------------------- /// @@ -104,6 +106,12 @@ void RigEclipseNativeVisibleCellsStatCalc::valueSumAndSampleCount( size_t timeSt traverseCells( acc, timeStepIndex ); valueSum = acc.valueSum; sampleCount = acc.sampleCount; + + RiaLogging::debug( std::format( "Visible cells statistics for '{}', time step {}: sum = {}, number of values used = {}", + m_resultAddress.resultName().toStdString(), + timeStepIndex, + valueSum, + sampleCount ) ); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 6a9e74ea07..64a20c33ea 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -129,6 +129,8 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/opm-import-well-data-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifInpExportTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifGridCalculationIO-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCellFilterTools-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimGridCalculation-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifSummaryCalculationIO-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimEmReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimFormationNames-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp b/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp new file mode 100644 index 0000000000..25578f08c7 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimCellFilterTools-Test.cpp @@ -0,0 +1,224 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimCellFilterTools.h" +#include "RimCellRangeFilter.h" +#include "RimDataFilterCollection.h" +#include "RimEclipsePropertyFilter.h" +#include "RimEclipseResultCase.h" +#include "RimEclipseResultDefinition.h" +#include "RimReservoirGridEnsemble.h" + +#include "cafPdmField.h" + +#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* 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, 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 ); + 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 ); + 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 ); +} diff --git a/ApplicationLibCode/UnitTests/RimGridCalculation-Test.cpp b/ApplicationLibCode/UnitTests/RimGridCalculation-Test.cpp new file mode 100644 index 0000000000..db65d7e984 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimGridCalculation-Test.cpp @@ -0,0 +1,62 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimGridCalculation.h" + +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimGridCalculationTest, ReplaceInvalidValuesWithDefaultValue ) +{ + const double defaultValue = 0.0; + std::vector values = + { 1.0, HUGE_VAL, 2.0, -HUGE_VAL, std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -3.0, 0.0 }; + + size_t replacedCount = RimGridCalculation::replaceInvalidValuesWithDefaultValue( defaultValue, values ); + + EXPECT_EQ( 4u, replacedCount ); + + const std::vector expected = { 1.0, 0.0, 2.0, 0.0, 0.0, 0.0, -3.0, 0.0 }; + ASSERT_EQ( expected.size(), values.size() ); + for ( size_t i = 0; i < expected.size(); i++ ) + { + EXPECT_DOUBLE_EQ( expected[i], values[i] ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimGridCalculationTest, ReplaceInvalidValuesWithDefaultValueNoInvalidValues ) +{ + const std::vector original = { 1.0, 2.0, -3.0, 0.0 }; + + std::vector values = original; + size_t replacedCount = RimGridCalculation::replaceInvalidValuesWithDefaultValue( 42.0, values ); + + EXPECT_EQ( 0u, replacedCount ); + EXPECT_EQ( original, values ); + + std::vector emptyValues; + EXPECT_EQ( 0u, RimGridCalculation::replaceInvalidValuesWithDefaultValue( 42.0, emptyValues ) ); +}