Python: Add filtered_cells API for evaluating a data filter on a case

Adds case.filtered_cells(filter, time_step, grid_index) which applies any
RimCellFilter to an Eclipse case and returns a per-cell 0/1 mask. The mask
length and ordering match case.grid_property(..., grid_index), so callers
can combine the two vectors element-wise.

Implemented as a scriptable Rimc method (PdmVoidObjectMethod) on
RimEclipseCase that writes the mask via the existing key-value store, with
a thin Python wrapper mirroring the visible_cells() pattern. No proto or
gRPC service changes.
This commit is contained in:
Kristian Bendiksen committed 2026-05-15 08:37:46 +02:00
1 parent c9456bba62
commit ddcfef736b
4 files changed
+233 -1

No files matched your search

+33
View File
@@ -1579,3 +1579,36 @@ def replace_corner_point_grid(
zcorn_key=zcorn_key,
actnum_key=actnum_key,
)
@add_method(EclipseCase)
def filtered_cells(self, filter, time_step: int = 0, grid_index: int = 0) -> List[int]:
"""Apply a cell filter to this case and return a per-cell 0/1 mask.
The returned list has the same length and ordering as
``case.grid_property("STATIC_NATIVE", "PERMX", grid_index)``, so the two
vectors can be combined element-wise.
Arguments:
filter (CellFilter): The cell filter to apply (e.g. a CombinedFilter
obtained from ``case.data_filter_collection().filters()``).
time_step (int): Time step index used by property-filter children.
grid_index (int): Grid index (0 = main grid, >0 = LGRs).
Returns:
List[int]: 1 if the cell passes the filter, 0 otherwise.
"""
mask_key = "{}_{}".format(uuid.uuid4(), "filtered_cells")
project = self.ancestor(rips.project.Project)
if not project:
raise RuntimeError("Unable to get project from case")
try:
self.filtered_cells_internal(
filter=filter,
mask_key=mask_key,
time_step=time_step,
grid_index=grid_index,
)
return [int(v) for v in project.key_values(mask_key)]
finally:
project.remove_key_values(mask_key)
@@ -0,0 +1,100 @@
import sys
import os
import pytest
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_filtered_cells_length_matches_grid_property(rips_instance, initialize_test):
"""Returned mask length matches grid_property length for the same grid."""
case = _open_case(rips_instance)
combined = case.data_filter_collection().add_combined_filter(combine_mode="AND")
mask = case.filtered_cells(filter=combined, time_step=0, grid_index=0)
permx = case.grid_property("STATIC_NATIVE", "PERMX", 0)
assert len(mask) == len(permx)
assert len(mask) > 0
def test_filtered_cells_empty_combined_passes_all(rips_instance, initialize_test):
"""An empty combined filter (no children) keeps every cell visible."""
case = _open_case(rips_instance)
combined = case.data_filter_collection().add_combined_filter(combine_mode="AND")
mask = case.filtered_cells(filter=combined)
assert all(v == 1 for v in mask)
def test_filtered_cells_range_filter_narrows(rips_instance, initialize_test):
"""A K-slice range filter restricts the mask to that slab."""
case = _open_case(rips_instance)
combined = case.data_filter_collection().add_combined_filter(combine_mode="AND")
rf = combined.add_range_filter(name="K=5", start_k=5, cell_count_k=1)
rf.update()
dims = case.grid(0).dimensions()
expected_in_slab = dims.i * dims.j
mask = case.filtered_cells(filter=combined, grid_index=0)
assert sum(mask) == expected_in_slab
def test_filtered_cells_property_filter_pins_ordering(rips_instance, initialize_test):
"""Property-filter mask agrees with the grid_property values cell-by-cell.
For a PERMX in [lower, upper] filter, mask[i]==1 iff lower <= permx[i] <= upper.
Identical ordering across the two vectors is the whole point of this API.
"""
case = _open_case(rips_instance)
permx = case.grid_property("STATIC_NATIVE", "PERMX", 0)
finite = [v for v in permx if v == v] # NaN-safe
assert finite, "PERMX has no finite values in this case"
lower = sorted(finite)[len(finite) // 4]
upper = sorted(finite)[(3 * len(finite)) // 4]
combined = case.data_filter_collection().add_combined_filter(combine_mode="AND")
pf = combined.add_property_filter(
result_variable="PERMX", result_type="STATIC_NATIVE"
)
pf.lower_bound = lower
pf.upper_bound = upper
pf.update()
mask = case.filtered_cells(filter=combined, grid_index=0)
assert len(mask) == len(permx)
mismatches = 0
for i, (m, v) in enumerate(zip(mask, permx)):
in_range = lower <= v <= upper
if bool(m) != bool(in_range):
mismatches += 1
if mismatches <= 3:
print(
f" mismatch at i={i}: mask={m}, permx={v}, range=[{lower},{upper}]"
)
assert mismatches == 0
def test_filtered_cells_null_filter_rejected(rips_instance, initialize_test):
"""Calling the internal method with a null filter is rejected by C++."""
case = _open_case(rips_instance)
with pytest.raises(Exception):
case.filtered_cells_internal(
filter=None, mask_key="ignored", time_step=0, grid_index=0
)