mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 22:50:25 -05:00
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:
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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
)
|
||||||
Reference in new issue
Block a user