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.
This commit is contained in:
Kristian Bendiksen committed 2026-05-15 08:37:46 +02:00
1 parent 4ed6d70b73
commit eb70d32eb1
13 files changed
+528 -24

No files matched your search

@@ -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}")
@@ -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"