From eb70d32eb1f4d389aca38d16e6ebb5c5795fcfa1 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Wed, 6 May 2026 09:33:44 +0200 Subject: [PATCH] Python: Add scriptable Data Filters API for combined filters Expose the case-level RimDataFilterCollection (introduced for case-shared cell filters) to Python so scripts can build combined filters with property-filter and IJK-range children without going through a view. Scriptable surface: - RimEclipseCase.m_dataFilterCollection -> case.data_filter_collection() - RimDataFilterCollection (object + Filters child array) -> filters() - RimEclipsePropertyFilter (object + m_lowerBound + m_upperBound) -> pf.lower_bound, pf.upper_bound - RimCellRangeFilter (object + start_index_{i,j,k}, cell_count_{i,j,k}) New CAF object methods under ProjectDataModelCommands/: - RimcDataFilterCollection: add_combined_filter(name, combine_mode) - RimcCombinedFilter: add_property_filter(result_variable, result_type) and add_range_filter(name, start_{i,j,k}, cell_count_{i,j,k}). Bounds are not part of add_property_filter because the Python class generator emits a bare nan literal as the default for double fields initialised to quiet_NaN, which fails to import. Bounds are set via the now-scriptable lower_bound/upper_bound fields after creation, matching the update() pattern used elsewhere in the API. RicEclipsePropertyFilterFeatureImpl::addPropertyFilterToCombinedFilter now returns the new RimEclipsePropertyFilter* so the CAF method can hand it back to Python. setDefaults is relaxed to be view-tolerant: case-level filters have no Rim3dView ancestor and the eclipse case is already wired by RimEclipsePropertyFilter::setCase via the owning collection's onItemsChanged hook. End-to-end coverage in tests/test_combined_filter.py: round-trip create, mode toggle, property-filter add/mutate, range-filter add/mutate. Example script in PythonExamples builds an AND combined filter that gates PERMX between 100 and 20000 against a K=5..10 slice. --- .../RicEclipsePropertyFilterFeatureImpl.cpp | 22 ++- .../RicEclipsePropertyFilterFeatureImpl.h | 6 +- .../CellFilters/RimCellRangeFilter.cpp | 16 +- .../CellFilters/RimDataFilterCollection.cpp | 6 +- .../CellFilters/RimEclipsePropertyFilter.cpp | 8 +- .../ProjectDataModel/RimEclipseCase.cpp | 2 +- .../CMakeLists_files.cmake | 4 + .../RimcCombinedFilter.cpp | 149 ++++++++++++++++++ .../RimcCombinedFilter.h | 66 ++++++++ .../RimcDataFilterCollection.cpp | 79 ++++++++++ .../RimcDataFilterCollection.h | 43 +++++ .../add_combined_data_filter.py | 55 +++++++ .../Python/rips/tests/test_combined_filter.py | 96 +++++++++++ 13 files changed, 528 insertions(+), 24 deletions(-) create mode 100644 ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.cpp create mode 100644 ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.h create mode 100644 ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.cpp create mode 100644 ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.h create mode 100644 GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/add_combined_data_filter.py create mode 100644 GrpcInterface/Python/rips/tests/test_combined_filter.py diff --git a/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.cpp b/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.cpp index 89d2f3e3ca..77a18454a0 100644 --- a/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.cpp +++ b/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.cpp @@ -75,9 +75,9 @@ void RicEclipsePropertyFilterFeatureImpl::addPropertyFilter( RimEclipsePropertyF //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -void RicEclipsePropertyFilterFeatureImpl::addPropertyFilterToCombinedFilter( RimCombinedFilter* combined ) +RimEclipsePropertyFilter* RicEclipsePropertyFilterFeatureImpl::addPropertyFilterToCombinedFilter( RimCombinedFilter* combined ) { - if ( !combined ) return; + if ( !combined ) return nullptr; auto* propertyFilter = new RimEclipsePropertyFilter(); combined->addFilter( propertyFilter ); @@ -94,6 +94,8 @@ void RicEclipsePropertyFilterFeatureImpl::addPropertyFilterToCombinedFilter( Rim combined->updateConnectedEditors(); Riu3DMainWindowTools::selectAsCurrentItem( propertyFilter, false ); + + return propertyFilter; } //-------------------------------------------------------------------------------------------------- @@ -139,13 +141,17 @@ void RicEclipsePropertyFilterFeatureImpl::setDefaults( RimEclipsePropertyFilter* { CVF_ASSERT( propertyFilter ); - auto reservoirView = propertyFilter->firstAncestorOrThisOfTypeAsserted(); - - propertyFilter->resultDefinition()->setEclipseCase( reservoirView->eclipseCase() ); - - if ( !RiaResultNames::isPerCellFaceResult( reservoirView->cellResult()->resultVariable() ) ) + // View-tolerant: case-level data filters have no RimEclipseView ancestor; the result + // definition's eclipse case is already wired by RimEclipsePropertyFilter::setCase via the + // owning collection's onItemsChanged hook. + if ( auto* reservoirView = propertyFilter->firstAncestorOrThisOfType() ) { - propertyFilter->resultDefinition()->simpleCopy( reservoirView->cellResult() ); + propertyFilter->resultDefinition()->setEclipseCase( reservoirView->eclipseCase() ); + + if ( !RiaResultNames::isPerCellFaceResult( reservoirView->cellResult()->resultVariable() ) ) + { + propertyFilter->resultDefinition()->simpleCopy( reservoirView->cellResult() ); + } } propertyFilter->resultDefinition()->loadResult(); diff --git a/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.h b/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.h index 1b890d3631..3523c1d2a1 100644 --- a/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.h +++ b/ApplicationLibCode/Commands/EclipseCommands/RicEclipsePropertyFilterFeatureImpl.h @@ -40,9 +40,9 @@ public: static std::vector selectedPropertyFilters(); static std::vector selectedPropertyFilterCollections(); - static void addPropertyFilter( RimEclipsePropertyFilterCollection* propertyFilterCollection ); - static void addPropertyFilterToCombinedFilter( RimCombinedFilter* combined ); - static void insertPropertyFilter( RimEclipsePropertyFilterCollection* propertyFilterCollection, size_t index ); + static void addPropertyFilter( RimEclipsePropertyFilterCollection* propertyFilterCollection ); + static RimEclipsePropertyFilter* addPropertyFilterToCombinedFilter( RimCombinedFilter* combined ); + static void insertPropertyFilter( RimEclipsePropertyFilterCollection* propertyFilterCollection, size_t index ); static bool isPropertyFilterCommandAvailable( caf::PdmObjectHandle* object ); diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellRangeFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellRangeFilter.cpp index 048e4fdd0a..dfa8021440 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellRangeFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimCellRangeFilter.cpp @@ -28,6 +28,8 @@ #include "RimCase.h" #include "RimTools.h" +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" #include "cafPdmUiLabelEditor.h" #include "cafPdmUiSliderEditor.h" @@ -42,36 +44,36 @@ CAF_PDM_SOURCE_INIT( RimCellRangeFilter, "CellRangeFilter" ); RimCellRangeFilter::RimCellRangeFilter() : RimCellFilter( RimCellFilter::RANGE ) { - CAF_PDM_InitObject( "Cell Range Filter", ":/CellFilter_Range.png" ); + CAF_PDM_InitScriptableObject( "Cell Range Filter", ":/CellFilter_Range.png" ); CAF_PDM_InitField( &m_labelI, "LabelI", QString( "I" ), "I" ); m_labelI.uiCapability()->setUiEditorTypeName( caf::PdmUiLabelEditor::uiEditorTypeName() ); m_labelI.xmlCapability()->disableIO(); - CAF_PDM_InitField( &startIndexI, "StartIndexI", 1, "I Start" ); + CAF_PDM_InitScriptableField( &startIndexI, "StartIndexI", 1, "I Start" ); startIndexI.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); - CAF_PDM_InitField( &cellCountI, "CellCountI", 1, " Width" ); + CAF_PDM_InitScriptableField( &cellCountI, "CellCountI", 1, " Width" ); cellCountI.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); CAF_PDM_InitField( &m_labelJ, "LabelJ", QString( "J" ), "J" ); m_labelJ.uiCapability()->setUiEditorTypeName( caf::PdmUiLabelEditor::uiEditorTypeName() ); m_labelJ.xmlCapability()->disableIO(); - CAF_PDM_InitField( &startIndexJ, "StartIndexJ", 1, "J Start" ); + CAF_PDM_InitScriptableField( &startIndexJ, "StartIndexJ", 1, "J Start" ); startIndexJ.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); - CAF_PDM_InitField( &cellCountJ, "CellCountJ", 1, " Width" ); + CAF_PDM_InitScriptableField( &cellCountJ, "CellCountJ", 1, " Width" ); cellCountJ.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); CAF_PDM_InitField( &m_labelK, "LabelK", QString( "K" ), "K" ); m_labelK.uiCapability()->setUiEditorTypeName( caf::PdmUiLabelEditor::uiEditorTypeName() ); m_labelK.xmlCapability()->disableIO(); - CAF_PDM_InitField( &startIndexK, "StartIndexK", 1, "K Start" ); + CAF_PDM_InitScriptableField( &startIndexK, "StartIndexK", 1, "K Start" ); startIndexK.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); - CAF_PDM_InitField( &cellCountK, "CellCountK", 1, " Width" ); + CAF_PDM_InitScriptableField( &cellCountK, "CellCountK", 1, " Width" ); cellCountK.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); m_propagateToSubGrids = true; diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp index 4ddc002a8f..3adb5262a9 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimDataFilterCollection.cpp @@ -24,6 +24,8 @@ #include "RimEclipsePropertyFilter.h" #include "cafCmdFeatureMenuBuilder.h" +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" CAF_PDM_SOURCE_INIT( RimDataFilterCollection, "DataFilterCollection" ); @@ -33,9 +35,9 @@ CAF_PDM_SOURCE_INIT( RimDataFilterCollection, "DataFilterCollection" ); RimDataFilterCollection::RimDataFilterCollection() : filtersChanged( this ) { - CAF_PDM_InitObject( "Data Filters", ":/CellFilter.png" ); + CAF_PDM_InitScriptableObject( "Data Filters", ":/CellFilter.png" ); - CAF_PDM_InitFieldNoDefault( &m_items, "Filters", "Filters" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_items, "Filters", "Filters" ); CAF_PDM_InitFieldNoDefault( &m_srcCase, "SourceCase", "Source Case" ); m_srcCase.uiCapability()->setUiHidden( true ); diff --git a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp index feeaee68f1..da3b47c303 100644 --- a/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp +++ b/ApplicationLibCode/ProjectDataModel/CellFilters/RimEclipsePropertyFilter.cpp @@ -44,6 +44,8 @@ #include "RiuMainWindow.h" +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" #include "cafPdmUiCheckBoxEditor.h" #include "cafPdmUiDoubleSliderEditor.h" #include "cafPdmUiSliderEditor.h" @@ -60,7 +62,7 @@ CAF_PDM_SOURCE_INIT( RimEclipsePropertyFilter, "CellPropertyFilter" ); //-------------------------------------------------------------------------------------------------- RimEclipsePropertyFilter::RimEclipsePropertyFilter() { - CAF_PDM_InitObject( "Cell Property Filter", ":/CellFilter_Values.png" ); + CAF_PDM_InitScriptableObject( "Cell Property Filter", ":/CellFilter_Values.png" ); CAF_PDM_InitFieldNoDefault( &m_resultDefinition, "ResultDefinition", "Result Definition" ); m_resultDefinition = new RimEclipseResultDefinition(); @@ -81,10 +83,10 @@ RimEclipsePropertyFilter::RimEclipsePropertyFilter() m_rangeLabelText.xmlCapability()->disableIO(); m_rangeLabelText.uiCapability()->setUiReadOnly( true ); - CAF_PDM_InitField( &m_lowerBound, "LowerBound", 0.0, "Min" ); + CAF_PDM_InitScriptableField( &m_lowerBound, "LowerBound", 0.0, "Min" ); m_lowerBound.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() ); - CAF_PDM_InitField( &m_upperBound, "UpperBound", 0.0, "Max" ); + CAF_PDM_InitScriptableField( &m_upperBound, "UpperBound", 0.0, "Max" ); m_upperBound.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() ); CAF_PDM_InitFieldNoDefault( &m_integerLowerBound, "IntegerLowerBound", "Min" ); diff --git a/ApplicationLibCode/ProjectDataModel/RimEclipseCase.cpp b/ApplicationLibCode/ProjectDataModel/RimEclipseCase.cpp index 0f6d0d0b55..3d1afc7936 100644 --- a/ApplicationLibCode/ProjectDataModel/RimEclipseCase.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimEclipseCase.cpp @@ -125,7 +125,7 @@ RimEclipseCase::RimEclipseCase() CAF_PDM_InitFieldNoDefault( &m_viewCollection, "ViewCollection", "Views" ); m_viewCollection = new RimEclipseViewCollection; - CAF_PDM_InitFieldNoDefault( &m_dataFilterCollection, "DataFilterCollection", "Data Filters" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_dataFilterCollection, "DataFilterCollection", "Data Filters" ); m_dataFilterCollection = new RimDataFilterCollection; m_dataFilterCollection->setCase( this ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake index 4b245180fb..e383bd60a8 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake @@ -33,6 +33,8 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimcPressureTable.h ${CMAKE_CURRENT_LIST_DIR}/RimcFishbonesCollection.h ${CMAKE_CURRENT_LIST_DIR}/RimcPolygonCollection.h + ${CMAKE_CURRENT_LIST_DIR}/RimcDataFilterCollection.h + ${CMAKE_CURRENT_LIST_DIR}/RimcCombinedFilter.h ${CMAKE_CURRENT_LIST_DIR}/RimcRegularSurface.h ${CMAKE_CURRENT_LIST_DIR}/RimcPerforationInterval.h ${CMAKE_CURRENT_LIST_DIR}/RimcValveTemplateCollection.h @@ -78,6 +80,8 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimcPressureTable.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcFishbonesCollection.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcPolygonCollection.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimcDataFilterCollection.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimcCombinedFilter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcRegularSurface.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcPerforationInterval.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcValveTemplateCollection.cpp diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.cpp new file mode 100644 index 0000000000..41efa40cd5 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.cpp @@ -0,0 +1,149 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimcCombinedFilter.h" + +#include "RiaDefines.h" + +#include "RicEclipsePropertyFilterFeatureImpl.h" + +#include "RimCellRangeFilter.h" +#include "RimCombinedFilter.h" +#include "RimEclipsePropertyFilter.h" +#include "RimEclipseResultDefinition.h" + +#include "Rim3dView.h" + +#include "cafAppEnum.h" +#include "cafPdmAbstractFieldScriptingCapability.h" +#include "cafPdmFieldScriptingCapability.h" + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimCombinedFilter, RimcCombinedFilter_addPropertyFilter, "AddPropertyFilter" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcCombinedFilter_addPropertyFilter::RimcCombinedFilter_addPropertyFilter( caf::PdmObjectHandle* self ) + : caf::PdmObjectCreationMethod( self ) +{ + CAF_PDM_InitObject( "Add Property Filter", "", "", "Add a new property filter as a child of this combined filter" ); + + CAF_PDM_InitScriptableFieldNoDefault( &m_resultVariable, "ResultVariable", "Result Variable" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_resultType, "ResultType", "Result Type" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcCombinedFilter_addPropertyFilter::execute() +{ + auto* combined = self(); + if ( !combined ) return std::unexpected( QString( "Self is not a RimCombinedFilter" ) ); + + RimEclipsePropertyFilter* pf = RicEclipsePropertyFilterFeatureImpl::addPropertyFilterToCombinedFilter( combined ); + if ( !pf ) return std::unexpected( QString( "Failed to create property filter" ) ); + + const bool hasType = !m_resultType().isEmpty(); + const bool hasVar = !m_resultVariable().isEmpty(); + + if ( hasType ) + { + const auto cat = caf::AppEnum::fromText( m_resultType() ); + pf->resultDefinition()->setResultType( cat ); + } + if ( hasVar ) + { + pf->resultDefinition()->setResultVariable( m_resultVariable() ); + } + if ( hasType || hasVar ) + { + pf->resultDefinition()->loadResult(); + pf->setToDefaultValues(); + pf->updateFilterName(); + } + + pf->updateConnectedEditors(); + + return pf; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimcCombinedFilter_addPropertyFilter::classKeywordReturnedType() const +{ + return RimEclipsePropertyFilter::classKeywordStatic(); +} + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimCombinedFilter, RimcCombinedFilter_addRangeFilter, "AddRangeFilter" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcCombinedFilter_addRangeFilter::RimcCombinedFilter_addRangeFilter( caf::PdmObjectHandle* self ) + : caf::PdmObjectCreationMethod( self ) +{ + CAF_PDM_InitObject( "Add Range Filter", "", "", "Add a new IJK range filter as a child of this combined filter" ); + + CAF_PDM_InitScriptableFieldNoDefault( &m_name, "Name", "Name" ); + CAF_PDM_InitScriptableField( &m_startI, "StartI", -1, "Start I" ); + CAF_PDM_InitScriptableField( &m_startJ, "StartJ", -1, "Start J" ); + CAF_PDM_InitScriptableField( &m_startK, "StartK", -1, "Start K" ); + CAF_PDM_InitScriptableField( &m_cellCountI, "CellCountI", -1, "Cell Count I" ); + CAF_PDM_InitScriptableField( &m_cellCountJ, "CellCountJ", -1, "Cell Count J" ); + CAF_PDM_InitScriptableField( &m_cellCountK, "CellCountK", -1, "Cell Count K" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcCombinedFilter_addRangeFilter::execute() +{ + auto* combined = self(); + if ( !combined ) return std::unexpected( QString( "Self is not a RimCombinedFilter" ) ); + + auto* rf = combined->addNewFilter( + []( RimCellRangeFilter* f ) + { + f->setGridIndex( 0 ); + f->setDefaultValues( -1, -1 ); + } ); + + if ( !rf ) return std::unexpected( QString( "Failed to create range filter" ) ); + + if ( !m_name().isEmpty() ) rf->setName( m_name() ); + + if ( m_startI() > 0 ) rf->startIndexI = m_startI(); + if ( m_startJ() > 0 ) rf->startIndexJ = m_startJ(); + if ( m_startK() > 0 ) rf->startIndexK = m_startK(); + if ( m_cellCountI() > 0 ) rf->cellCountI = m_cellCountI(); + if ( m_cellCountJ() > 0 ) rf->cellCountJ = m_cellCountJ(); + if ( m_cellCountK() > 0 ) rf->cellCountK = m_cellCountK(); + + rf->updateConnectedEditors(); + + return rf; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimcCombinedFilter_addRangeFilter::classKeywordReturnedType() const +{ + return RimCellRangeFilter::classKeywordStatic(); +} diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.h b/ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.h new file mode 100644 index 0000000000..542c47b665 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcCombinedFilter.h @@ -0,0 +1,66 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "cafPdmField.h" +#include "cafPdmObjectHandle.h" +#include "cafPdmObjectMethod.h" + +#include + +//================================================================================================== +/// Adds a new property filter as a child of a RimCombinedFilter. +//================================================================================================== +class RimcCombinedFilter_addPropertyFilter : public caf::PdmObjectCreationMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcCombinedFilter_addPropertyFilter( caf::PdmObjectHandle* self ); + + std::expected execute() override; + QString classKeywordReturnedType() const override; + +private: + caf::PdmField m_resultVariable; + caf::PdmField m_resultType; +}; + +//================================================================================================== +/// Adds a new IJK range filter as a child of a RimCombinedFilter. +//================================================================================================== +class RimcCombinedFilter_addRangeFilter : public caf::PdmObjectCreationMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcCombinedFilter_addRangeFilter( caf::PdmObjectHandle* self ); + + std::expected execute() override; + QString classKeywordReturnedType() const override; + +private: + caf::PdmField m_name; + caf::PdmField m_startI; + caf::PdmField m_startJ; + caf::PdmField m_startK; + caf::PdmField m_cellCountI; + caf::PdmField m_cellCountJ; + caf::PdmField m_cellCountK; +}; diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.cpp new file mode 100644 index 0000000000..b1ed628caf --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.cpp @@ -0,0 +1,79 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimcDataFilterCollection.h" + +#include "RimCellFilterCollection.h" +#include "RimCombinedFilter.h" +#include "RimDataFilterCollection.h" + +#include "cafPdmAbstractFieldScriptingCapability.h" +#include "cafPdmFieldScriptingCapability.h" + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimDataFilterCollection, RimcDataFilterCollection_addCombinedFilter, "AddCombinedFilter" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcDataFilterCollection_addCombinedFilter::RimcDataFilterCollection_addCombinedFilter( caf::PdmObjectHandle* self ) + : caf::PdmObjectCreationMethod( self ) +{ + CAF_PDM_InitObject( "Add Combined Filter", "", "", "Add a new combined filter to the data filter collection" ); + + CAF_PDM_InitScriptableFieldNoDefault( &m_name, "Name", "Name" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_combineMode, "CombineMode", "Combine Mode (AND/OR)" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcDataFilterCollection_addCombinedFilter::execute() +{ + auto* coll = self(); + if ( !coll ) return std::unexpected( QString( "Self is not a RimDataFilterCollection" ) ); + + RimCombinedFilter* combined = coll->addNewCombinedFilter(); + if ( !combined ) return std::unexpected( QString( "Failed to create combined filter" ) ); + + if ( !m_name().isEmpty() ) + { + combined->setName( m_name() ); + } + + const QString modeStr = m_combineMode().trimmed().toUpper(); + if ( modeStr == "AND" ) + { + combined->setCombineMode( RimCellFilterCollection::AND ); + } + else if ( modeStr == "OR" ) + { + combined->setCombineMode( RimCellFilterCollection::OR ); + } + + coll->updateConnectedEditors(); + + return combined; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimcDataFilterCollection_addCombinedFilter::classKeywordReturnedType() const +{ + return RimCombinedFilter::classKeywordStatic(); +} diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.h b/ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.h new file mode 100644 index 0000000000..8c110d5a58 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcDataFilterCollection.h @@ -0,0 +1,43 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "cafPdmField.h" +#include "cafPdmObjectHandle.h" +#include "cafPdmObjectMethod.h" + +#include + +//================================================================================================== +/// Adds a new combined filter to a case-level RimDataFilterCollection. +//================================================================================================== +class RimcDataFilterCollection_addCombinedFilter : public caf::PdmObjectCreationMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcDataFilterCollection_addCombinedFilter( caf::PdmObjectHandle* self ); + + std::expected execute() override; + QString classKeywordReturnedType() const override; + +private: + caf::PdmField m_name; + caf::PdmField m_combineMode; +}; diff --git a/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/add_combined_data_filter.py b/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/add_combined_data_filter.py new file mode 100644 index 0000000000..ea99653a0a --- /dev/null +++ b/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/add_combined_data_filter.py @@ -0,0 +1,55 @@ +####################################################### +# +# Adds a case-level "Data Filters" combined filter that +# AND-combines a PERMX property filter with a K-slice +# range filter (K = 5..10), then shows how to mutate +# the property filter's lower bound after creation. +# +# Assumes a case is already loaded in the running +# ResInsight instance. +# +####################################################### + +import rips + +resinsight = rips.Instance.find() + +# Use the first open case in the project. Add a guard so the script fails +# loudly if the user runs it against an empty project. +cases = resinsight.project.cases() +assert cases, "No case loaded in ResInsight. Open a case before running this script." +case = cases[0] + +# Case-level Data Filters: shared by every view of the case. +data_filters = case.data_filter_collection() + +# AND-combined parent. +combined = data_filters.add_combined_filter( + name="PERMX 100..20000 AND K=5..10", + combine_mode="AND", +) + +# PERMX property filter, then override the bounds populated by setToDefaultValues. +permx = combined.add_property_filter( + result_variable="PERMX", + result_type="STATIC_NATIVE", +) +permx.lower_bound = 100.0 +permx.upper_bound = 20000.0 +permx.update() + +# K-slice filter for K = 5..10. 1-based Eclipse indexing — start_k=5, count=6 covers 5..10. +combined.add_range_filter( + name="K=5..10", + start_k=5, + cell_count_k=6, +) + +# Mutate the lower bound later via field assignment. Re-read from the project +# tree to confirm the change round-trips. +permx.lower_bound = 250.0 +permx.update() + +permx_refreshed = combined.filters()[0] +print(f"PERMX lower_bound after update: {permx_refreshed.lower_bound}") +print(f"PERMX upper_bound after update: {permx_refreshed.upper_bound}") diff --git a/GrpcInterface/Python/rips/tests/test_combined_filter.py b/GrpcInterface/Python/rips/tests/test_combined_filter.py new file mode 100644 index 0000000000..69115acfb2 --- /dev/null +++ b/GrpcInterface/Python/rips/tests/test_combined_filter.py @@ -0,0 +1,96 @@ +import sys +import os + +sys.path.insert(1, os.path.join(sys.path[0], "../../")) + +import dataroot + + +def _open_case(rips_instance): + project = rips_instance.project.open( + dataroot.PATH + "/TEST10K_FLT_LGR_NNC/10KWithWellLog.rsp" + ) + return project.cases()[0] + + +def test_combined_filter_round_trip(rips_instance, initialize_test): + case = _open_case(rips_instance) + + data_filters = case.data_filter_collection() + assert data_filters is not None + assert len(data_filters.filters()) == 0 + + combined = data_filters.add_combined_filter(name="My Combined", combine_mode="OR") + assert combined is not None + assert combined.name == "My Combined" + assert combined.combine_mode == "OR" + assert len(data_filters.filters()) == 1 + + +def test_combined_filter_add_property_filter(rips_instance, initialize_test): + case = _open_case(rips_instance) + data_filters = case.data_filter_collection() + + combined = data_filters.add_combined_filter(combine_mode="AND") + + pf = combined.add_property_filter( + result_variable="SOIL", result_type="DYNAMIC_NATIVE" + ) + assert pf is not None + assert len(combined.filters()) == 1 + + pf2 = combined.add_property_filter( + result_variable="PRESSURE", result_type="DYNAMIC_NATIVE" + ) + assert pf2 is not None + assert len(combined.filters()) == 2 + + # Bounds are populated by setToDefaultValues based on the result range; override + # them via the scriptable fields. + pf2.lower_bound = 150.0 + pf2.upper_bound = 250.0 + pf2.update() + + refreshed = combined.filters()[1] + assert refreshed.lower_bound == 150.0 + assert refreshed.upper_bound == 250.0 + + +def test_combined_filter_add_range_filter(rips_instance, initialize_test): + case = _open_case(rips_instance) + data_filters = case.data_filter_collection() + + combined = data_filters.add_combined_filter() + + rf = combined.add_range_filter(name="K-slice 5", start_k=5, cell_count_k=1) + assert rf is not None + assert "K-slice 5" in rf.name + assert rf.start_index_k == 5 + assert rf.cell_count_k == 1 + + # Defaults from setDefaultValues should populate I/J using grid extent (>= 1). + assert rf.start_index_i >= 1 + assert rf.start_index_j >= 1 + assert rf.cell_count_i >= 1 + assert rf.cell_count_j >= 1 + + # Mutate via field assignment + update. + rf.cell_count_k = 3 + rf.update() + + refreshed = combined.filters()[0] + assert refreshed.cell_count_k == 3 + + +def test_combined_filter_mode_toggle(rips_instance, initialize_test): + case = _open_case(rips_instance) + data_filters = case.data_filter_collection() + + combined = data_filters.add_combined_filter(combine_mode="OR") + assert combined.combine_mode == "OR" + + combined.combine_mode = "AND" + combined.update() + + refreshed = data_filters.filters()[0] + assert refreshed.combine_mode == "AND"