diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp index b5cd3102eb..c7c21f9add 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp @@ -22,13 +22,17 @@ #include "RiaGuiApplication.h" #include "RiaKeyValueStoreUtil.h" +#include "RigActiveCellInfo.h" #include "RigCaseCellResultsData.h" #include "RigEclipseCaseData.h" #include "RigEclipseResultAddress.h" +#include "RigGridExportAdapter.h" #include "RigMainGrid.h" +#include "RigResdataGridConverter.h" #include "RigResultAccessor.h" #include "RigResultAccessorFactory.h" +#include "RimCase.h" #include "RimEclipseCase.h" #include "RimProject.h" @@ -181,3 +185,70 @@ std::expected RimcEclipseCase_exportValuesIntern return nullptr; } + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimEclipseCase, RimcEclipseCase_exportCornerPointGridInternal, "export_corner_point_grid_internal" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcEclipseCase_exportCornerPointGridInternal::RimcEclipseCase_exportCornerPointGridInternal( caf::PdmObjectHandle* self ) + : caf::PdmVoidObjectMethod( self ) +{ + CAF_PDM_InitObject( "Export Corner Point Grid Internal", "", "", "Export Corner Point Grid Internal" ); + + CAF_PDM_InitScriptableFieldNoDefault( &m_zcornKey, "ZcornKey", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_coordKey, "CoordKey", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_actnumKey, "ActnumKey", "" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcEclipseCase_exportCornerPointGridInternal::execute() +{ + auto eclipseCase = self(); + if ( !eclipseCase ) + { + return std::unexpected( "Unable to get Eclipse case." ); + } + + RigEclipseCaseData* eclipseCaseData = eclipseCase->eclipseCaseData(); + if ( !eclipseCaseData ) + { + return std::unexpected( "Unable to load eclipse case data." ); + } + + auto keyValueStore = RiaApplication::instance()->keyValueStore(); + + // Use RigGridExportAdapter to handle full grid export + // Export the full grid (no min/max bounds, no refinement, no visibility override) + RigGridExportAdapter gridAdapter( eclipseCaseData, cvf::Vec3st::ZERO, cvf::Vec3st::UNDEFINED, cvf::Vec3st( 1, 1, 1 ), nullptr ); + + // Use the existing conversion utility to get the arrays in the correct format + std::vector coordArray; + std::vector zcornArray; + std::vector actnumArray; + + RigResdataGridConverter::convertGridToCornerPointArrays( gridAdapter, coordArray, zcornArray, actnumArray ); + + // Convert actnum from int to float for key-value store + std::vector actnumFloat; + actnumFloat.reserve( actnumArray.size() ); + for ( int value : actnumArray ) + { + actnumFloat.push_back( static_cast( value ) ); + } + + // Store arrays in key-value store + keyValueStore->set( m_zcornKey().toStdString(), RiaKeyValueStoreUtil::convertToByteVector( zcornArray ) ); + keyValueStore->set( m_coordKey().toStdString(), RiaKeyValueStoreUtil::convertToByteVector( coordArray ) ); + keyValueStore->set( m_actnumKey().toStdString(), RiaKeyValueStoreUtil::convertToByteVector( actnumFloat ) ); + + // Store grid dimensions as well + std::vector dimensions = { static_cast( gridAdapter.cellCountI() ), + static_cast( gridAdapter.cellCountJ() ), + static_cast( gridAdapter.cellCountK() ) }; + keyValueStore->set( ( m_zcornKey().toStdString() + "_dimensions" ), RiaKeyValueStoreUtil::convertToByteVector( dimensions ) ); + + return nullptr; +} diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h index 03003842b4..83be4f340f 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h @@ -24,8 +24,6 @@ #include -#include - //================================================================================================== /// //================================================================================================== @@ -64,3 +62,21 @@ private: caf::PdmField m_porosityModel; caf::PdmField m_resultKey; }; + +//================================================================================================== +/// +//================================================================================================== +class RimcEclipseCase_exportCornerPointGridInternal : public caf::PdmVoidObjectMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcEclipseCase_exportCornerPointGridInternal( caf::PdmObjectHandle* self ); + + std::expected execute() override; + +private: + caf::PdmField m_zcornKey; + caf::PdmField m_coordKey; + caf::PdmField m_actnumKey; +}; diff --git a/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/export_corner_point_grid.py b/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/export_corner_point_grid.py new file mode 100644 index 0000000000..f15f806698 --- /dev/null +++ b/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/export_corner_point_grid.py @@ -0,0 +1,126 @@ +###################################################################### +# This script demonstrates how to export corner point grid data from +# ResInsight cases and recreate new grids from the exported data. +# +# This is the inverse operation of create_corner_point_grid.py - it +# extracts COORD, ZCORN, and ACTNUM arrays from existing Eclipse cases. +###################################################################### + +import rips + + +def validate_grid_dimensions(coord_array, zcorn_array, actnum_array, nx, ny, nz): + """ + Validate that the exported arrays match the expected dimensions. + + Args: + coord_array: COORD array (coordinate lines) + zcorn_array: ZCORN array (corner depths) + actnum_array: ACTNUM array (active cell flags) + nx, ny, nz: Grid dimensions + + Returns: + bool: True if arrays match expected dimensions + """ + total_cells = nx * ny * nz + expected_coord_size = (nx + 1) * (ny + 1) * 6 # 6 values per coordinate line + expected_zcorn_size = nx * ny * nz * 8 # 8 corner depths per cell + + return ( + len(actnum_array) == total_cells + and len(coord_array) == expected_coord_size + and len(zcorn_array) == expected_zcorn_size + ) + + +def main(): + # Connect to ResInsight + resinsight = rips.Instance.find() + if resinsight is None: + print("Starting ResInsight...") + resinsight = rips.Instance.launch(console=True) + + project = resinsight.project + if not project.cases(): + print("No cases in current project") + return + + original_case = project.cases()[0] + if original_case is None: + print("Failed to load case") + return + + print(f"Loaded case: {original_case.name}") + + # Get basic case information + cell_count = original_case.cell_count() + print("Case information:") + print(f" Total cells: {cell_count.reservoir_cell_count}") + print(f" Active cells: {cell_count.active_cell_count}") + + # Export corner point grid data + print("\nExporting corner point grid data...") + zcorn, coord, actnum, nx, ny, nz = original_case.export_corner_point_grid() + + print("Exported arrays:") + print(f" ZCORN: {len(zcorn):,} values (corner depths)") + print(f" COORD: {len(coord):,} values (coordinate lines)") + print(f" ACTNUM: {len(actnum):,} values (active cell flags)") + + # Grid dimensions are now returned directly from the export method! + print(f"\nGrid dimensions from export: {nx} x {ny} x {nz} = {nx * ny * nz:,} cells") + + # Validate that the arrays match the expected dimensions + if validate_grid_dimensions(coord, zcorn, actnum, nx, ny, nz): + print("✓ Array sizes match expected dimensions perfectly") + else: + print("⚠ Array sizes don't match expected dimensions - this shouldn't happen") + return + + # Create a new corner point grid from the exported data + print("\nCreating new corner point grid from exported data...") + recreated_name = f"{original_case.name}_Recreated" + recreated_case = project.create_corner_point_grid( + recreated_name, nx, ny, nz, coord, zcorn, actnum + ) + + if recreated_case is None: + print("Failed to create recreated case") + return + + print(f"Created recreated case: {recreated_case.name}") + + # Compare the two grids + print("\nComparing original and recreated grids:") + + orig_cell_count = original_case.cell_count() + recreated_cell_count = recreated_case.cell_count() + + print("Original grid:") + print(f" Total cells: {orig_cell_count.reservoir_cell_count:,}") + print(f" Active cells: {orig_cell_count.active_cell_count:,}") + + print("Recreated grid:") + print(f" Total cells: {recreated_cell_count.reservoir_cell_count:,}") + print(f" Active cells: {recreated_cell_count.active_cell_count:,}") + + # Get coordinate ranges for comparison + print("\nCoordinate ranges:") + + # Sample some coordinates for comparison + orig_bbox = original_case.reservoir_boundingbox() + recreated_bbox = recreated_case.reservoir_boundingbox() + + print("Original bounding box:") + print(f" X: {orig_bbox.min_x:.2f} to {orig_bbox.max_x:.2f}") + print(f" Y: {orig_bbox.min_y:.2f} to {orig_bbox.max_y:.2f}") + print(f" Z: {orig_bbox.min_z:.2f} to {orig_bbox.max_z:.2f}") + + print("Recreated bounding box:") + print(f" X: {recreated_bbox.min_x:.2f} to {recreated_bbox.max_x:.2f}") + print(f" Y: {recreated_bbox.min_y:.2f} to {recreated_bbox.max_y:.2f}") + print(f" Z: {recreated_bbox.min_z:.2f} to {recreated_bbox.max_z:.2f}") + + +if __name__ == "__main__": + main() diff --git a/GrpcInterface/Python/rips/case.py b/GrpcInterface/Python/rips/case.py index 830846c641..f32f5c7c1a 100644 --- a/GrpcInterface/Python/rips/case.py +++ b/GrpcInterface/Python/rips/case.py @@ -52,6 +52,7 @@ from .resinsight_classes import ( Case as Case, EclipseCase as EclipseCase, GeoMechCase as GeoMechCase, + Reservoir as Reservoir, WellBoreStabilityPlot as WellBoreStabilityPlot, WbsParameters as WbsParameters, ) @@ -1386,3 +1387,50 @@ def grid_property_for_positions( return result else: return [] + + +@add_method(Reservoir) +def export_corner_point_grid( + self, +) -> Tuple[List[float], List[float], List[int], int, int, int]: + """Export corner point grid data from case + + Returns: + Tuple of (zcorn, coord, actnum, nx, ny, nz) where: + - zcorn: Corner depths as defined by the Eclipse keyword ZCORN + - coord: Coordinate lines as COORD keyword in Eclipse + - actnum: Active cell info: cells with values > 0 are active + - nx, ny, nz: Grid dimensions in I, J, K directions + """ + shared_uuid = uuid.uuid4() + zcorn_key = "{}_{}".format(shared_uuid, "zcorn") + coord_key = "{}_{}".format(shared_uuid, "coord") + actnum_key = "{}_{}".format(shared_uuid, "actnum") + + # Call internal C++ method to populate key-value store + self.export_corner_point_grid_internal( + zcorn_key=zcorn_key, coord_key=coord_key, actnum_key=actnum_key + ) + + # Retrieve results from key-value store + project = self.ancestor(Project) + if project: + zcorn = project.key_values(zcorn_key) + coord = project.key_values(coord_key) + actnum_raw = project.key_values(actnum_key) + actnum = [int(x) for x in actnum_raw] + + # Retrieve grid dimensions + dimensions_key = zcorn_key + "_dimensions" + dimensions = project.key_values(dimensions_key) + nx, ny, nz = int(dimensions[0]), int(dimensions[1]), int(dimensions[2]) + + # Clean up + project.remove_key_values(zcorn_key) + project.remove_key_values(coord_key) + project.remove_key_values(actnum_key) + project.remove_key_values(dimensions_key) + + return (zcorn, coord, actnum, nx, ny, nz) + else: + return ([], [], [], 0, 0, 0) diff --git a/GrpcInterface/Python/rips/tests/test_export_corner_point_grid.py b/GrpcInterface/Python/rips/tests/test_export_corner_point_grid.py new file mode 100644 index 0000000000..a047b72232 --- /dev/null +++ b/GrpcInterface/Python/rips/tests/test_export_corner_point_grid.py @@ -0,0 +1,174 @@ +import sys +import os + +sys.path.insert(1, os.path.join(sys.path[0], "../../")) +import dataroot + + +def test_export_corner_point_grid_basic(rips_instance, initialize_test): + """Test the export_corner_point_grid method with a simple created grid""" + + # Create a simple test case + nx, ny, nz = 2, 2, 2 + + # Generate simple coordinate data + coord = [] + for j in range(ny + 1): + for i in range(nx + 1): + x = i * 100.0 + y = j * 100.0 + # Top point + coord.extend([x, y, 1000.0]) + # Bottom point + coord.extend([x, y, 1100.0]) + + # Generate simple ZCORN data (8 values per cell) + zcorn = [] + for k in range(nz): + for j in range(ny): + for i in range(nx): + base_depth = 1000.0 + k * 100.0 + # 8 corner depths for each cell + zcorn.extend( + [ + base_depth, + base_depth, + base_depth, + base_depth, + base_depth + 100.0, + base_depth + 100.0, + base_depth + 100.0, + base_depth + 100.0, + ] + ) + + # Generate ACTNUM (all cells active) + actnum = [1] * (nx * ny * nz) + + # Create the grid case + case = rips_instance.project.create_corner_point_grid( + "TestGrid", nx, ny, nz, coord, zcorn, actnum + ) + + # Test our export function + exported_zcorn, exported_coord, exported_actnum, export_nx, export_ny, export_nz = ( + case.export_corner_point_grid() + ) + + # Basic validation + assert len(exported_zcorn) == len(zcorn) + assert len(exported_coord) == len(coord) + assert len(exported_actnum) == len(actnum) + + # Check that all cells are active + active_count = sum(1 for x in exported_actnum if x > 0) + assert active_count == sum(actnum) + + # Verify dimensions are returned correctly + assert export_nx == nx, f"Dimension mismatch: nx={export_nx} vs expected {nx}" + assert export_ny == ny, f"Dimension mismatch: ny={export_ny} vs expected {ny}" + assert export_nz == nz, f"Dimension mismatch: nz={export_nz} vs expected {nz}" + + +def test_export_corner_point_grid_return_types(rips_instance, initialize_test): + """Test that export_corner_point_grid returns the correct types""" + + # Create a minimal test case + nx, ny, nz = 1, 1, 1 + coord = [0, 0, 1000, 0, 0, 1100] * 4 # 4 coordinate lines for 1x1 grid + zcorn = [1000] * 8 # 8 corners + actnum = [1] # 1 active cell + + case = rips_instance.project.create_corner_point_grid( + "MinimalGrid", nx, ny, nz, coord, zcorn, actnum + ) + + exported_zcorn, exported_coord, exported_actnum, export_nx, export_ny, export_nz = ( + case.export_corner_point_grid() + ) + + # Check return types + assert isinstance(exported_zcorn, list), "ZCORN should be a list" + assert isinstance(exported_coord, list), "COORD should be a list" + assert isinstance(exported_actnum, list), "ACTNUM should be a list" + + # Check element data types + assert all(isinstance(x, float) for x in exported_zcorn) + assert all(isinstance(x, float) for x in exported_coord) + assert all(isinstance(x, int) for x in exported_actnum) + + # Check dimension types + assert isinstance(export_nx, int), "nx dimension should be integer" + assert isinstance(export_ny, int), "ny dimension should be integer" + assert isinstance(export_nz, int), "nz dimension should be integer" + + +def test_export_corner_point_grid_roundtrip(rips_instance, initialize_test): + """Test round-trip: load Eclipse grid, export data, recreate grid, and compare geometry""" + + # Load the Eclipse grid file (using BRUGGE case without LGR complications) + original_case = rips_instance.project.load_case( + dataroot.PATH + "/Case_with_10_timesteps/Real0/BRUGGE_0000.EGRID" + ) + + assert original_case is not None, "Failed to load test case" + assert ( + original_case.name == "BRUGGE_0000" + ), f"Expected case name BRUGGE_0000, got {original_case.name}" + + # Get original geometry data for comparison + original_active_count = original_case.cell_count().active_cell_count + original_cell_corners = original_case.active_cell_corners() + + # Export corner point grid data + exported_zcorn, exported_coord, exported_actnum, nx, ny, nz = ( + original_case.export_corner_point_grid() + ) + + # Validate that the returned dimensions match the exported array sizes + total_cells = len(exported_actnum) + assert nx * ny * nz == total_cells + + # Validate array sizes make sense for these dimensions + expected_coord_size = (nx + 1) * (ny + 1) * 6 # 6 values per coordinate line + expected_zcorn_size = nx * ny * nz * 8 # 8 corner depths per cell + expected_actnum_size = nx * ny * nz # 1 value per cell + + assert len(exported_coord) == expected_coord_size + assert len(exported_zcorn) == expected_zcorn_size + assert len(exported_actnum) == expected_actnum_size + + # Create new corner point grid from exported data + recreated_case = rips_instance.project.create_corner_point_grid( + "Recreated_BRUGGE", nx, ny, nz, exported_coord, exported_zcorn, exported_actnum + ) + + assert recreated_case is not None, "Failed to create recreated case" + + # Compare geometry between original and recreated cases + recreated_active_count = recreated_case.cell_count().active_cell_count + recreated_cell_corners = recreated_case.active_cell_corners() + + # Compare active cell counts + assert recreated_active_count == original_active_count + + # Compare number of corner points (should be same for active cells) + assert len(recreated_cell_corners) == len(original_cell_corners) + + # Now do a focused comparison on a smaller sample + sample_size = min(200, len(original_cell_corners)) + + for i in range(sample_size): + orig_cell = original_cell_corners[i] + recreated_cell = recreated_cell_corners[i] + + for corner_idx in range(8): + orig_corner = getattr(orig_cell, f"c{corner_idx}") + recreated_corner = getattr(recreated_cell, f"c{corner_idx}") + + diff_x = abs(orig_corner.x - recreated_corner.x) + diff_y = abs(orig_corner.y - recreated_corner.y) + diff_z = abs(orig_corner.z - recreated_corner.z) + + max_diff = max(diff_x, diff_y, diff_z) + assert max_diff < 1e-3