From 50e2de7a9868291c24b2878add570878fb2a18dd Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Tue, 23 Dec 2025 14:02:08 +0100 Subject: [PATCH] #13313 Python: Add support for setting geometry on an existing case --- .../ProjectDataModel/RimCornerPointCase.cpp | 35 ++++++ .../ProjectDataModel/RimCornerPointCase.h | 8 ++ .../ProjectDataModel/RimReloadCaseTools.h | 2 +- .../CMakeLists_files.cmake | 2 + .../RimcCornerPointCase.cpp | 117 ++++++++++++++++++ .../RimcCornerPointCase.h | 47 +++++++ GrpcInterface/Python/rips/case.py | 53 ++++++++ .../tests/test_export_corner_point_grid.py | 116 +++++++++++++++++ 8 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.cpp create mode 100644 ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.h diff --git a/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.cpp b/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.cpp index 678635b9b98..490dd7a41c0 100644 --- a/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.cpp @@ -29,6 +29,7 @@ #include "RigCaseCellResultsData.h" #include "RigEclipseCaseData.h" #include "RigMainGrid.h" +#include "RimReloadCaseTools.h" #include "RimEclipseInputProperty.h" #include "RimReservoirCellResultsStorage.h" @@ -108,6 +109,40 @@ std::expected RimCornerPointCase::createFromCoordi return cornerPointCase; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimCornerPointCase::replaceGridFromCoordinatesArray( RimCornerPointCase& cornerPointCase, + const int nx, + const int ny, + const int nz, + const std::vector& coord, + const std::vector& zcorn, + const std::vector& actnum ) +{ + RigActiveCellInfo* activeCellInfo = cornerPointCase.eclipseCaseData()->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL ); + CVF_ASSERT( activeCellInfo ); + activeCellInfo->clear(); + + RigActiveCellInfo* fractureActiveCellInfo = + cornerPointCase.eclipseCaseData()->activeCellInfo( RiaDefines::PorosityModelType::FRACTURE_MODEL ); + CVF_ASSERT( fractureActiveCellInfo ); + fractureActiveCellInfo->clear(); + + RimReloadCaseTools::clearAllGridData( cornerPointCase.eclipseCaseData() ); + + // Clear the existing grid geometry before building the new grid + RigMainGrid* mainGrid = cornerPointCase.eclipseCaseData()->mainGrid(); + CVF_ASSERT( mainGrid ); + mainGrid->reservoirCells().clear(); + mainGrid->nodes().clear(); + + buildGrid( *cornerPointCase.eclipseCaseData(), nx, ny, nz, coord, zcorn, actnum ); + cornerPointCase.computeCachedData(); + + return {}; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.h b/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.h index ef26b96e634..d342096c742 100644 --- a/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.h +++ b/ApplicationLibCode/ProjectDataModel/RimCornerPointCase.h @@ -50,6 +50,14 @@ class RimCornerPointCase : public RimEclipseCase const std::vector& zcorn, const std::vector& actnum ); + static std::expected replaceGridFromCoordinatesArray( RimCornerPointCase& cornerPointCase, + const int nx, + const int ny, + const int nz, + const std::vector& coord, + const std::vector& zcorn, + const std::vector& actnum ); + protected: void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; diff --git a/ApplicationLibCode/ProjectDataModel/RimReloadCaseTools.h b/ApplicationLibCode/ProjectDataModel/RimReloadCaseTools.h index b3ec14132a5..fdef595ad02 100644 --- a/ApplicationLibCode/ProjectDataModel/RimReloadCaseTools.h +++ b/ApplicationLibCode/ProjectDataModel/RimReloadCaseTools.h @@ -40,10 +40,10 @@ class RimReloadCaseTools static RimEclipseCase* gridModelFromSummaryCase( const RimSummaryCase* summaryCase ); static RimSummaryCase* findSummaryCaseFromEclipseResultCase( const RimEclipseResultCase* eclResCase ); static bool openOrImportGridModelFromSummaryCase( const RimSummaryCase* summaryCase ); + static void clearAllGridData( RigEclipseCaseData* eclipseCaseData ); private: static void reloadEclipseData( RimEclipseCase* eclipseCase, bool reloadSummaryData ); - static void clearAllGridData( RigEclipseCaseData* eclipseCaseData ); static void updateAllPlots(); static bool findGridModelAndActivateFirstView( const RimSummaryCase* summaryCase ); diff --git a/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake index 20b230684dc..c30a18ad455 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModelCommands/CMakeLists_files.cmake @@ -24,6 +24,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RimcFractureTemplate.h ${CMAKE_CURRENT_LIST_DIR}/RimcThermalFractureTemplate.h ${CMAKE_CURRENT_LIST_DIR}/RimcIntersection.h + ${CMAKE_CURRENT_LIST_DIR}/RimcCornerPointCase.h ${CMAKE_CURRENT_LIST_DIR}/RimcEclipseCase.h ${CMAKE_CURRENT_LIST_DIR}/RimcEclipseStatisticsCase.h ${CMAKE_CURRENT_LIST_DIR}/RimcGridView.h @@ -63,6 +64,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimcFractureTemplate.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcThermalFractureTemplate.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcIntersection.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimcCornerPointCase.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcEclipseCase.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcEclipseStatisticsCase.cpp ${CMAKE_CURRENT_LIST_DIR}/RimcGridView.cpp diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.cpp new file mode 100644 index 00000000000..7bf6d3033fb --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.cpp @@ -0,0 +1,117 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimcCornerPointCase.h" + +#include "RiaApplication.h" +#include "RiaKeyValueStoreUtil.h" +#include "RiaViewRedrawScheduler.h" + +#include "Rim3dView.h" +#include "RimCornerPointCase.h" +#include "RimEclipseView.h" + +#include "cafPdmFieldScriptingCapability.h" + +#include + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimCornerPointCase, RimcCornerPointCase_replaceCornerPointGridInternal, "replace_corner_point_grid_internal" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcCornerPointCase_replaceCornerPointGridInternal::RimcCornerPointCase_replaceCornerPointGridInternal( caf::PdmObjectHandle* self ) + : caf::PdmVoidObjectMethod( self ) +{ + CAF_PDM_InitObject( "Replace Corner Point Grid", "", "", "Replace Corner Point Grid" ); + + CAF_PDM_InitScriptableFieldNoDefault( &m_nx, "Nx", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_ny, "Ny", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_nz, "Nz", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_coordKey, "CoordKey", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_zcornKey, "ZcornKey", "" ); + CAF_PDM_InitScriptableFieldNoDefault( &m_actnumKey, "ActnumKey", "" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcCornerPointCase_replaceCornerPointGridInternal::execute() +{ + auto cornerPointCase = self(); + if ( !cornerPointCase ) + { + return std::unexpected( "Unable to get corner point case." ); + } + + int nx = m_nx(); + int ny = m_ny(); + int nz = m_nz(); + if ( nx <= 0 || ny <= 0 || nz <= 0 ) + { + return std::unexpected( "Invalid grid dimensions. nx, ny and nz must be positive." ); + } + + auto keyValueStore = RiaApplication::instance()->keyValueStore(); + + std::vector coord = RiaKeyValueStoreUtil::convertToFloatVector( keyValueStore->get( m_coordKey().toStdString() ) ); + std::vector zcorn = RiaKeyValueStoreUtil::convertToFloatVector( keyValueStore->get( m_zcornKey().toStdString() ) ); + std::vector actnum = RiaKeyValueStoreUtil::convertToFloatVector( keyValueStore->get( m_actnumKey().toStdString() ) ); + + if ( coord.empty() || zcorn.empty() || actnum.empty() ) + { + return std::unexpected( "Found unexpected empty coord, zcorn or actnum array." ); + } + + // Block view updates while the grid is being replaced. + RiaViewRedrawScheduler::instance()->clearViewsScheduledForUpdate(); + RiaViewRedrawScheduler::instance()->blockUpdate( true ); + + // Create a new corner point grid from the provided geometry + auto result = RimCornerPointCase::replaceGridFromCoordinatesArray( *cornerPointCase, nx, ny, nz, coord, zcorn, actnum ); + if ( !result.has_value() ) + { + RiaViewRedrawScheduler::instance()->blockUpdate( false ); + return std::unexpected( "Failed to replace grid" ); + } + + // Update all 3D views to reflect the new grid + // We must completely reinitialize the views because the grid size has changed + std::vector views = cornerPointCase->views(); + for ( Rim3dView* view : views ) + { + if ( RimEclipseView* eclView = dynamic_cast( view ) ) + { + // Force complete regeneration by calling loadDataAndUpdate + // This will reinitialize all geometry managers with the new grid size + eclView->loadDataAndUpdate(); + } + } + + RiaViewRedrawScheduler::instance()->blockUpdate( false ); + + // Update connected editors + cornerPointCase->updateConnectedEditors(); + + // Clean up key-value store + keyValueStore->remove( m_coordKey().toStdString() ); + keyValueStore->remove( m_zcornKey().toStdString() ); + keyValueStore->remove( m_actnumKey().toStdString() ); + + return nullptr; +} diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.h b/ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.h new file mode 100644 index 00000000000..d0c3bdf250b --- /dev/null +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcCornerPointCase.h @@ -0,0 +1,47 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafPdmField.h" +#include "cafPdmObjectMethod.h" + +#include + +#include + +//================================================================================================== +/// +//================================================================================================== +class RimcCornerPointCase_replaceCornerPointGridInternal : public caf::PdmVoidObjectMethod +{ + CAF_PDM_HEADER_INIT; + +public: + RimcCornerPointCase_replaceCornerPointGridInternal( caf::PdmObjectHandle* self ); + + std::expected execute() override; + +private: + caf::PdmField m_nx; + caf::PdmField m_ny; + caf::PdmField m_nz; + caf::PdmField m_coordKey; + caf::PdmField m_zcornKey; + caf::PdmField m_actnumKey; +}; diff --git a/GrpcInterface/Python/rips/case.py b/GrpcInterface/Python/rips/case.py index 9123c369a27..17ff1f62605 100644 --- a/GrpcInterface/Python/rips/case.py +++ b/GrpcInterface/Python/rips/case.py @@ -1435,3 +1435,56 @@ def export_corner_point_grid( return (zcorn, coord, actnum, nx, ny, nz) else: return ([], [], [], 0, 0, 0) + + +@add_method(Case) +def replace_corner_point_grid( + self, + nx: int, + ny: int, + nz: int, + coord: List[float], + zcorn: List[float], + actnum: List[int], +): + """Replace the current case grid with new corner point grid geometry + + This method modifies the geometry of an existing case by replacing it with + new grid parameters. All existing properties will be cleared. + + Arguments: + nx(int): Number of cells in x direction + ny(int): Number of cells in y direction + nz(int): Number of cells in z direction + coord(list[float]): Coordinate lines as COORD keyword in Eclipse. + Each coordinate line is defined by two points (top and bottom). + Size: (nx+1) * (ny+1) * 2 * 3. Points are ordered as in Eclipse. + zcorn(list[float]): Corner depths as defined by the Eclipse keyword ZCORN. + Size: nx * ny * nz * 8 + actnum(list[int]): Active cell info: cells with values > 0 are active. + Size: nx * ny * nz + """ + # Generate unique keys for three arrays + coord_key = "{}_{}".format(uuid.uuid4(), "coord") + zcorn_key = "{}_{}".format(uuid.uuid4(), "zcorn") + actnum_key = "{}_{}".format(uuid.uuid4(), "actnum") + + # Get project to access key-value store + project = self.ancestor(rips.project.Project) + if not project: + raise RuntimeError("Unable to get project from case") + + # Store arrays in key-value store + project.set_key_values(coord_key, coord) + project.set_key_values(zcorn_key, zcorn) + project.set_key_values(actnum_key, actnum) + + # Call internal C++ method to replace the grid + self.replace_corner_point_grid_internal( + nx=nx, + ny=ny, + nz=nz, + coord_key=coord_key, + zcorn_key=zcorn_key, + actnum_key=actnum_key, + ) diff --git a/GrpcInterface/Python/rips/tests/test_export_corner_point_grid.py b/GrpcInterface/Python/rips/tests/test_export_corner_point_grid.py index a047b722320..5490644f555 100644 --- a/GrpcInterface/Python/rips/tests/test_export_corner_point_grid.py +++ b/GrpcInterface/Python/rips/tests/test_export_corner_point_grid.py @@ -172,3 +172,119 @@ def test_export_corner_point_grid_roundtrip(rips_instance, initialize_test): max_diff = max(diff_x, diff_y, diff_z) assert max_diff < 1e-3 + + +def test_replace_corner_point_grid_basic(rips_instance, initialize_test): + """Test the replace_corner_point_grid method by creating a grid and replacing it with a larger one""" + + # Create initial grid with 2x2x2 cells + nx_init, ny_init, nz_init = 2, 2, 2 + + # Generate simple coordinate data for initial grid + coord_init = [] + for j in range(ny_init + 1): + for i in range(nx_init + 1): + x = i * 100.0 + y = j * 100.0 + # Top point + coord_init.extend([x, y, 1000.0]) + # Bottom point + coord_init.extend([x, y, 1100.0]) + + # Generate simple ZCORN data for initial grid + zcorn_init = [] + for k in range(nz_init): + for j in range(ny_init): + for i in range(nx_init): + base_depth = 1000.0 + k * 100.0 + # 8 corner depths for each cell + zcorn_init.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 for initial grid (all cells active) + actnum_init = [1] * (nx_init * ny_init * nz_init) + + # Create the initial grid case + case = rips_instance.project.create_corner_point_grid( + "InitialGrid", nx_init, ny_init, nz_init, coord_init, zcorn_init, actnum_init + ) + + # Verify initial grid dimensions + initial_cell_count = case.cell_count() + assert initial_cell_count.reservoir_cell_count == nx_init * ny_init * nz_init + + # Now create replacement grid with more cells (3x3x3) + nx_new, ny_new, nz_new = 3, 3, 3 + + # Generate coordinate data for new grid + coord_new = [] + for j in range(ny_new + 1): + for i in range(nx_new + 1): + x = i * 100.0 + y = j * 100.0 + # Top point + coord_new.extend([x, y, 1000.0]) + # Bottom point + coord_new.extend([x, y, 1200.0]) + + # Generate ZCORN data for new grid + zcorn_new = [] + for k in range(nz_new): + for j in range(ny_new): + for i in range(nx_new): + base_depth = 1000.0 + k * 100.0 + # 8 corner depths for each cell + zcorn_new.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 for new grid (all cells active) + actnum_new = [1] * (nx_new * ny_new * nz_new) + + # Replace the grid geometry + case.replace_corner_point_grid( + nx_new, ny_new, nz_new, coord_new, zcorn_new, actnum_new + ) + + # Verify that the grid now has the new dimensions + new_cell_count = case.cell_count() + assert new_cell_count.reservoir_cell_count == nx_new * ny_new * nz_new + assert new_cell_count.reservoir_cell_count == 27 # 3x3x3 + + # Verify the grid has more cells than before + assert new_cell_count.reservoir_cell_count > initial_cell_count.reservoir_cell_count + + # Export the replaced grid and verify dimensions + exported_zcorn, exported_coord, exported_actnum, export_nx, export_ny, export_nz = ( + case.export_corner_point_grid() + ) + + # Verify dimensions match + assert export_nx == nx_new + assert export_ny == ny_new + assert export_nz == nz_new + + # Verify array sizes + print(exported_actnum) + assert len(exported_actnum) == nx_new * ny_new * nz_new + # All cells should be active + assert sum(1 for x in exported_actnum if x > 0) == len(actnum_new)