#14772 Python: Add Case.replace_grid and forward replaceCase to it

First step in modernizing the command file interface: move the
implementation of legacy Ricf commands into object-oriented Rimc
methods, and let the Ricf command forward to them.

- Add RimCase_replaceGrid (PdmVoidObjectMethod on RimCase) with
  NewGridFile and optional ProjectFile parameters. Works for both
  Eclipse result cases and GeoMech cases. Generated in Python as
  Case.replace_grid().
- Add RicfCommandForwarding helpers for resolving ids to objects and
  converting std::expected results to PdmScriptResponse.
- RicfSingleCaseReplace::execute() now resolves the case and the
  executor's last project path, then forwards to RimCase_replaceGrid.
  Command file syntax and Commands.proto are unchanged.
- Case.replace() in Python is kept as a thin wrapper over
  replace_grid() and emits a DeprecationWarning.
This commit is contained in:
Kristian Bendiksen committed 2026-09-22 14:19:23 +02:00
1 parent 3437cf9b94
commit 3eb63c0b8e
9 files changed
+355 -32

No files matched your search

@@ -36,6 +36,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RicfImportWellLogFiles.cpp
${CMAKE_CURRENT_LIST_DIR}/RicfImportFormationNames.cpp
${CMAKE_CURRENT_LIST_DIR}/RicfExportWellLogPlotData.cpp
${CMAKE_CURRENT_LIST_DIR}/RicfCommandForwarding.cpp
)
list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
@@ -0,0 +1,90 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicfCommandForwarding.h"
#include "RiaLogging.h"
#include "Rim3dView.h"
#include "RimCase.h"
#include "RimEclipseResultCase.h"
#include "RimProject.h"
#include "cafPdmObjectHandle.h"
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<RimCase*, QString> RicfForwarding::findCase( int caseId )
{
RimProject* project = RimProject::current();
if ( !project ) return std::unexpected( "No project is available." );
std::vector<RimCase*> allCases = project->allGridCases();
if ( caseId < 0 )
{
// Matches RiaProjectModifier::firstCaseId(): the first occurrence is the first Eclipse result case
for ( RimCase* rimCase : allCases )
{
if ( dynamic_cast<RimEclipseResultCase*>( rimCase ) ) return rimCase;
}
return std::unexpected( "No Eclipse result cases found in project." );
}
for ( RimCase* rimCase : allCases )
{
if ( rimCase && rimCase->caseId() == caseId ) return rimCase;
}
return std::unexpected( QString( "Could not find case with ID %1" ).arg( caseId ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<Rim3dView*, QString> RicfForwarding::findView( RimCase* rimCase, int viewId )
{
if ( !rimCase ) return std::unexpected( "No case given when looking up view." );
for ( Rim3dView* view : rimCase->views() )
{
if ( view && view->id() == viewId ) return view;
}
return std::unexpected( QString( "Could not find view with ID %1 in case %2" ).arg( viewId ).arg( rimCase->caseId() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
caf::PdmScriptResponse RicfForwarding::toScriptResponse( const std::expected<caf::PdmObjectHandle*, QString>& result, const QString& commandName )
{
if ( !result.has_value() ) return errorResponse( result.error(), commandName );
return caf::PdmScriptResponse();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
caf::PdmScriptResponse RicfForwarding::errorResponse( const QString& message, const QString& commandName )
{
QString fullMessage = QString( "%1: %2" ).arg( commandName ).arg( message );
RiaLogging::error( fullMessage.toStdString() );
return caf::PdmScriptResponse( caf::PdmScriptResponse::COMMAND_ERROR, fullMessage );
}
@@ -0,0 +1,54 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafPdmScriptResponse.h"
#include <QString>
#include <expected>
class RimCase;
class Rim3dView;
namespace caf
{
class PdmObjectHandle;
}
//==================================================================================================
/// Helpers used by the legacy command file interface (Ricf*) to forward to the object-oriented
/// command methods (Rimc*). The legacy interface identifies objects by integer ids, while the
/// object methods operate directly on objects. These helpers translate between the two.
//==================================================================================================
namespace RicfForwarding
{
/// Find a grid case by id. A negative id resolves to the first Eclipse result case in the project.
std::expected<RimCase*, QString> findCase( int caseId );
/// Find a view by id in the given case. A negative id is not accepted.
std::expected<Rim3dView*, QString> findView( RimCase* rimCase, int viewId );
/// Convert the result of a Rimc method execution to a script response, logging errors with the command name.
caf::PdmScriptResponse toScriptResponse( const std::expected<caf::PdmObjectHandle*, QString>& result, const QString& commandName );
/// Create an error response and log the message with the command name.
caf::PdmScriptResponse errorResponse( const QString& message, const QString& commandName );
} // namespace RicfForwarding
@@ -23,8 +23,11 @@
#include "RiaProjectModifier.h"
#include "RicfCommandFileExecutor.h"
#include "RicfCommandForwarding.h"
#include "RimCase.h"
#include "RimProject.h"
#include "RimcCase.h"
#include "cafPdmFieldScriptingCapability.h"
@@ -63,46 +66,29 @@ QString RicfSingleCaseReplace::filePath() const
//--------------------------------------------------------------------------------------------------
caf::PdmScriptResponse RicfSingleCaseReplace::execute()
{
const QString commandName = classKeyword();
// The legacy command file interface allows replacing a case in a project that has been opened by
// 'openProject' but is not saved. Resolve the project path here and pass it on explicitly.
QString projectPath = RimProject::current()->fileName();
if ( projectPath.isEmpty() )
{
QString lastProjectPath = RicfCommandFileExecutor::instance()->getLastProjectPath();
if ( lastProjectPath.isNull() )
{
QString errMsg( "replaceCase: The project must be saved as a file before calling 'replaceCase'." );
RiaLogging::error( errMsg.toStdString() );
return caf::PdmScriptResponse( caf::PdmScriptResponse::COMMAND_ERROR, errMsg );
return RicfForwarding::errorResponse( "The project must be saved as a file before calling 'replaceCase'.", commandName );
}
projectPath = lastProjectPath;
}
cvf::ref<RiaProjectModifier> projectModifier = cvf::make_ref<RiaProjectModifier>();
auto rimCase = RicfForwarding::findCase( m_caseId() );
if ( !rimCase ) return RicfForwarding::errorResponse( rimCase.error(), commandName );
QString filePath = m_newGridFile();
QFileInfo casePathInfo( filePath );
if ( !casePathInfo.exists() )
{
QDir startDir( RiaApplication::instance()->startDir() );
filePath = startDir.absoluteFilePath( m_newGridFile() );
}
RimCase_replaceGrid method( rimCase.value() );
method.setNewGridFile( m_newGridFile() );
method.setProjectFile( projectPath );
if ( m_caseId() < 0 )
{
projectModifier->setReplaceCaseFirstOccurrence( filePath );
}
else
{
projectModifier->setReplaceCase( m_caseId(), filePath );
}
if ( !RiaApplication::instance()->loadProject( projectPath, RiaApplication::ProjectLoadAction::PLA_NONE, projectModifier.p() ) )
{
QString errMsg( "Could not reload project" );
RiaLogging::error( errMsg.toStdString() );
return caf::PdmScriptResponse( caf::PdmScriptResponse::COMMAND_ERROR, errMsg );
}
return caf::PdmScriptResponse();
return RicfForwarding::toScriptResponse( method.execute(), commandName );
}
CAF_PDM_SOURCE_INIT( RicfMultiCaseReplace, "replaceMultipleCases" );
@@ -45,6 +45,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RimcWellEventPerf.cpp
${CMAKE_CURRENT_LIST_DIR}/RimcColorLegend.cpp
${CMAKE_CURRENT_LIST_DIR}/RimcNestedCollectionBase.cpp
${CMAKE_CURRENT_LIST_DIR}/RimcCase.cpp
)
list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
@@ -0,0 +1,106 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimcCase.h"
#include "RiaApplication.h"
#include "RiaProjectModifier.h"
#include "RimCase.h"
#include "RimProject.h"
#include "cafPdmFieldScriptingCapability.h"
#include <QDir>
#include <QFileInfo>
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimCase, RimCase_replaceGrid, "replaceGrid" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimCase_replaceGrid::RimCase_replaceGrid( caf::PdmObjectHandle* self )
: caf::PdmVoidObjectMethod( self )
{
CAF_PDM_InitObject( "Replace Grid", "", "", "Replace the grid file of the case and reload the project" );
CAF_PDM_InitScriptableField( &m_newGridFile, "NewGridFile", QString(), "New Grid File", "", "Path to the new grid file (EGRID, GRID, GRDECL or ODB)" );
CAF_PDM_InitScriptableField( &m_projectFile,
"ProjectFile",
QString(),
"Project File",
"",
"Optional project file to reload. Defaults to the current project file." );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimCase_replaceGrid::setNewGridFile( const QString& newGridFile )
{
m_newGridFile = newGridFile;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimCase_replaceGrid::setProjectFile( const QString& projectFile )
{
m_projectFile = projectFile;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<caf::PdmObjectHandle*, QString> RimCase_replaceGrid::execute()
{
auto* rimCase = self<RimCase>();
if ( !rimCase ) return std::unexpected( "No case is available." );
if ( m_newGridFile().isEmpty() ) return std::unexpected( "No new grid file specified." );
QString projectPath = m_projectFile();
if ( projectPath.isEmpty() )
{
RimProject* project = RimProject::current();
if ( project ) projectPath = project->fileName();
}
if ( projectPath.isEmpty() )
{
return std::unexpected( "The project must be saved as a file before replacing the grid of a case." );
}
QString filePath = m_newGridFile();
QFileInfo casePathInfo( filePath );
if ( !casePathInfo.exists() )
{
QDir startDir( RiaApplication::instance()->startDir() );
filePath = startDir.absoluteFilePath( m_newGridFile() );
}
cvf::ref<RiaProjectModifier> projectModifier = cvf::make_ref<RiaProjectModifier>();
projectModifier->setReplaceCase( rimCase->caseId(), filePath );
if ( !RiaApplication::instance()->loadProject( projectPath, RiaApplication::ProjectLoadAction::PLA_NONE, projectModifier.p() ) )
{
return std::unexpected( "Could not reload project" );
}
return nullptr;
}
@@ -0,0 +1,49 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafPdmField.h"
#include "cafPdmObjectHandle.h"
#include "cafPdmObjectMethod.h"
#include <QString>
//==================================================================================================
/// Replace the grid file of a case (Eclipse result case or GeoMech case) and reload the project.
///
/// The project must be saved to file, as the replacement is performed by reloading the project
/// through a RiaProjectModifier. An explicit project file can be given to override the current
/// project file name (used by the legacy command file interface).
//==================================================================================================
class RimCase_replaceGrid : public caf::PdmVoidObjectMethod
{
CAF_PDM_HEADER_INIT;
public:
RimCase_replaceGrid( caf::PdmObjectHandle* self );
void setNewGridFile( const QString& newGridFile );
void setProjectFile( const QString& projectFile );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
private:
caf::PdmField<QString> m_newGridFile;
caf::PdmField<QString> m_projectFile;
};
+10 -4
View File
@@ -37,6 +37,7 @@ result
import grpc
import uuid
import warnings
from typing import Any, Iterable, Iterator, List, Optional, Tuple, Union
import Case_pb2
@@ -170,13 +171,18 @@ def grids(self) -> List[Grid]:
def replace(self, new_grid_file: str) -> None:
"""Replace the current case grid with a new grid loaded from file
Deprecated: use replace_grid(new_grid_file) instead.
Arguments:
new_egrid_file (str): Path to EGRID file
new_grid_file (str): Path to EGRID file
"""
project = self.ancestor(rips.project.Project)
self._execute_command(
replaceCase=Cmd.ReplaceCaseRequest(newGridFile=new_grid_file, caseId=self.id)
warnings.warn(
"Case.replace() is deprecated, use Case.replace_grid() instead",
DeprecationWarning,
stacklevel=3,
)
project = self.ancestor(rips.project.Project)
self.replace_grid(new_grid_file=new_grid_file)
new_case = project.case(self.id)
self.copy_from(new_case)
@@ -177,6 +177,36 @@ def test_replaceCase(rips_instance, initialize_test):
assert case.id == 0
def test_replace_grid(rips_instance, initialize_test):
project = rips_instance.project.open(
dataroot.PATH + "/TEST10K_FLT_LGR_NNC/10KWithWellLog.rsp"
)
case_path = dataroot.PATH + "/Case_with_10_timesteps/Real0/BRUGGE_0000.EGRID"
case = project.case(case_id=0)
assert case is not None
assert case.name == "TEST10K_FLT_LGR_NNC"
case.replace_grid(new_grid_file=case_path)
# The project is reloaded, so retrieve the case object again
cases = rips_instance.project.cases()
assert len(cases) == 1
case = rips_instance.project.case(case_id=0)
assert case.name == "BRUGGE_0000"
assert case.id == 0
def test_replace_deprecated(rips_instance, initialize_test):
project = rips_instance.project.open(
dataroot.PATH + "/TEST10K_FLT_LGR_NNC/10KWithWellLog.rsp"
)
case_path = dataroot.PATH + "/Case_with_10_timesteps/Real0/BRUGGE_0000.EGRID"
case = project.case(case_id=0)
with pytest.warns(DeprecationWarning):
case.replace(new_grid_file=case_path)
assert case.name == "BRUGGE_0000"
def test_loadNonExistingCase(rips_instance, initialize_test):
case_path = "Nonsense/Nonsense/Nonsense"
with pytest.raises(rips.RipsError):