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

@@ -28,6 +28,7 @@
#include "RigCaseCellResultsData.h" #include "RigCaseCellResultsData.h"
#include "RigEclipseCaseData.h" #include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h" #include "RigEclipseResultAddress.h"
#include "RigGridBase.h"
#include "RigGridExportAdapter.h" #include "RigGridExportAdapter.h"
#include "RigMainGrid.h" #include "RigMainGrid.h"
#include "RigNoRefinement.h" #include "RigNoRefinement.h"
@@ -36,6 +37,7 @@
#include "RigResultAccessorFactory.h" #include "RigResultAccessorFactory.h"
#include "RimCase.h" #include "RimCase.h"
#include "RimCellFilter.h"
#include "RimEclipseCase.h" #include "RimEclipseCase.h"
#include "RimEclipseResultCase.h" #include "RimEclipseResultCase.h"
#include "RimProject.h" #include "RimProject.h"
@@ -45,9 +47,12 @@
#include "cafPdmFieldScriptingCapability.h" #include "cafPdmFieldScriptingCapability.h"
#include "cvfArray.h"
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <algorithm>
#include <expected> #include <expected>
#include <limits> #include <limits>
#include <vector> #include <vector>
@@ -332,4 +337,73 @@ std::expected<caf::PdmObjectHandle*, QString> RimcEclipseCase_clearResultAliases
} }
eclipseCase->clearResultAliases(); eclipseCase->clearResultAliases();
return nullptr; return nullptr;
} }
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimEclipseCase, RimcEclipseCase_filteredCellsInternal, "filtered_cells_internal" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimcEclipseCase_filteredCellsInternal::RimcEclipseCase_filteredCellsInternal( caf::PdmObjectHandle* self )
: caf::PdmVoidObjectMethod( self )
{
CAF_PDM_InitObject( "Filtered Cells Internal",
"",
"",
"Apply a cell filter to this case for a grid + time step and write a per-cell 0/1 mask "
"to the key-value store. The vector length and ordering match grid_property(...) for the "
"same grid index." );
CAF_PDM_InitScriptableFieldNoDefault( &m_filter, "Filter", "", "", "", "Cell Filter" );
CAF_PDM_InitScriptableFieldNoDefault( &m_maskKey, "MaskKey", "" );
CAF_PDM_InitScriptableField( &m_timeStep, "TimeStep", 0, "" );
CAF_PDM_InitScriptableField( &m_gridIndex, "GridIndex", 0, "" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<caf::PdmObjectHandle*, QString> RimcEclipseCase_filteredCellsInternal::execute()
{
auto* eclipseCase = self<RimEclipseCase>();
if ( !eclipseCase || !eclipseCase->eclipseCaseData() )
{
return std::unexpected( QString( "Self is not an Eclipse case with case data." ) );
}
if ( m_maskKey().isEmpty() )
{
return std::unexpected( QString( "Mask key is empty." ) );
}
RimCellFilter* filter = m_filter();
if ( !filter )
{
return std::unexpected( QString( "Filter argument is null." ) );
}
RigEclipseCaseData* caseData = eclipseCase->eclipseCaseData();
const size_t gridIdx = static_cast<size_t>( std::max( 0, m_gridIndex() ) );
if ( gridIdx >= caseData->gridCount() )
{
return std::unexpected( QString( "Grid index out of range." ) );
}
const RigGridBase* grid = caseData->grid( gridIdx );
const size_t n = grid->cellCount();
cvf::UByteArray cellVis( n );
cellVis.setAll( 1 );
filter->applyToCellVisibility( &cellVis, grid, static_cast<size_t>( std::max( 0, m_timeStep() ) ) );
std::vector<float> values;
values.reserve( n );
for ( size_t i = 0; i < n; ++i )
{
values.push_back( cellVis[i] ? 1.0f : 0.0f );
}
auto keyValueStore = RiaApplication::instance()->keyValueStore();
keyValueStore->set( m_maskKey().toStdString(), RiaKeyValueStoreUtil::convertToByteVector( values ) );
return nullptr;
}
@@ -21,9 +21,12 @@
#include "cafPdmField.h" #include "cafPdmField.h"
#include "cafPdmObjectHandle.h" #include "cafPdmObjectHandle.h"
#include "cafPdmObjectMethod.h" #include "cafPdmObjectMethod.h"
#include "cafPdmPtrField.h"
#include <QString> #include <QString>
class RimCellFilter;
//================================================================================================== //==================================================================================================
/// ///
//================================================================================================== //==================================================================================================
@@ -112,3 +115,25 @@ public:
std::expected<caf::PdmObjectHandle*, QString> execute() override; std::expected<caf::PdmObjectHandle*, QString> execute() override;
}; };
//==================================================================================================
/// Apply a cell filter to this case for a given grid + time step and write a per-cell 0/1 mask
/// (1 = passes the filter, 0 = filtered out) to the key-value store under m_maskKey. The vector
/// length and ordering match case.grid_property(..., grid_index) so the two can be combined
/// element-wise.
//==================================================================================================
class RimcEclipseCase_filteredCellsInternal : public caf::PdmVoidObjectMethod
{
CAF_PDM_HEADER_INIT;
public:
RimcEclipseCase_filteredCellsInternal( caf::PdmObjectHandle* self );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
private:
caf::PdmPtrField<RimCellFilter*> m_filter;
caf::PdmField<QString> m_maskKey;
caf::PdmField<int> m_timeStep;
caf::PdmField<int> m_gridIndex;
};
+33
View File
@@ -1579,3 +1579,36 @@ def replace_corner_point_grid(
zcorn_key=zcorn_key, zcorn_key=zcorn_key,
actnum_key=actnum_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
)