Add support for connecting a lateral to a parent well from Python (#12613)

* Updates well path connection logic.

Refactors well path connection to ensure tie-in location is updated correctly after connecting to a parent well. This ensures the visualization and data consistency are maintained.

* Updates connected editors after target manipulation

Ensures that connected editors are updated whenever the target is manipulated through the 3D manipulator.

* #12613 Adds functionality to append lateral well paths
This commit is contained in:
Magne Sjaastad authored and GitHub committed 2025-09-08 15:33:16 +02:00
1 parent 0255d98a2b
commit 01ad456a3b
8 files changed
+315 -1

No files matched your search

@@ -234,6 +234,7 @@ void RicWellTarget3dEditor::slotUpdated( const cvf::Vec3d& origin, const cvf::Ve
relativePositionXYD.z() = -relativePositionXYD.z();
manipulatedTarget->updateFrom3DManipulator( relativePositionXYD );
manipulatedTarget->updateConnectedEditors();
return;
}
@@ -267,6 +267,8 @@ void RimModeledWellPath::updateTieInLocationFromParentWell()
m_geometryDefinition->setIsAttachedToParentWell( false );
m_geometryDefinition->setFixedWellPathPoints( {} );
m_geometryDefinition->setFixedMeasuredDepths( {} );
updateWellPathVisualization();
}
}
@@ -286,3 +288,13 @@ void RimModeledWellPath::updateReferencePoint()
auto refPoint = topLevelModelledWell->geometryDefinition()->anchorPointXyz();
m_geometryDefinition->setReferencePointXyz( refPoint );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimModeledWellPath::connectWellPaths( RimWellPath* parentWell, double tieInMeasuredDepth )
{
RimWellPath::connectWellPaths( parentWell, tieInMeasuredDepth );
updateTieInLocationFromParentWell();
}
@@ -42,6 +42,8 @@ public:
void updateTieInLocationFromParentWell();
void updateReferencePoint();
virtual void connectWellPaths( RimWellPath* parentWell, double tieInMeasuredDepth ) override;
private:
void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName ) override;
void onGeometryDefinitionChanged( const caf::SignalEmitter* emitter, bool fullUpdate );
@@ -172,7 +172,7 @@ public:
std::vector<RimWellPath*> wellPathLaterals() const;
RimWellPathTieIn* wellPathTieIn() const;
void connectWellPaths( RimWellPath* parentWell, double tieInMeasuredDepth );
virtual void connectWellPaths( RimWellPath* parentWell, double tieInMeasuredDepth );
protected:
// Override PdmObject
@@ -48,6 +48,7 @@
#include "RigDoglegTools.h"
#include "RigStimPlanModelTools.h"
#include "Well/RigWellPath.h"
#include "Well/RigWellPathGeometryExporter.h"
#include "Well/RigWellPathGeometryTools.h"
@@ -614,3 +615,70 @@ QString RimcWellPath_addWellLogInternal::classKeywordReturnedType() const
{
return RimImportedWellLog::classKeywordStatic();
}
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimWellPath, RimcWellPath_appendLateral, "AppendLateral" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimcWellPath_appendLateral::RimcWellPath_appendLateral( caf::PdmObjectHandle* self )
: caf::PdmVoidObjectMethod( self )
{
CAF_PDM_InitObject( "Append Well Path Lateral" );
CAF_PDM_InitScriptableFieldNoDefault( &m_lateral, "WellPathLateral", "", "", "", "Well Path Lateral" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<caf::PdmObjectHandle*, QString> RimcWellPath_appendLateral::execute()
{
auto wellPath = self<RimWellPath>();
const auto mainWellPathGeometry = wellPath->wellPathGeometry();
if ( !mainWellPathGeometry )
{
QString txt = "No geometry available for main well path. Cannot append lateral to main well path.";
return std::unexpected( txt );
}
if ( !m_lateral )
{
QString txt = "No lateral specified. Cannot append lateral to well path.";
return std::unexpected( txt );
}
auto lateralGeometry = m_lateral->wellPathGeometry();
if ( !lateralGeometry )
{
QString txt = "No geometry available for lateral. Cannot append lateral to main well path.";
return std::unexpected( txt );
}
const double sharedWellPathLength = lateralGeometry->identicalTubeLength( *mainWellPathGeometry );
const double epsilon = 1.0e-8;
if ( sharedWellPathLength > epsilon )
{
m_lateral->connectWellPaths( wellPath, sharedWellPathLength );
return nullptr;
}
if ( lateralGeometry->wellPathPoints().empty() )
{
QString txt = "No points available for lateral. Cannot append lateral to main well path.";
return std::unexpected( txt );
}
auto lateralStartPoint = lateralGeometry->wellPathPoints().front();
auto closestMdOnMainWell = mainWellPathGeometry->closestMeasuredDepth( lateralStartPoint );
if ( closestMdOnMainWell < 0.0 )
{
QString txt = "Failed to find closest point on main well path. Cannot append lateral to main well path.";
return std::unexpected( txt );
}
m_lateral->connectWellPaths( wellPath, closestMdOnMainWell );
return nullptr;
}
@@ -30,6 +30,7 @@
class RimStimPlanFractureTemplate;
class RimThermalFractureTemplate;
class RimEclipseCase;
class RimWellPath;
//==================================================================================================
///
@@ -165,3 +166,19 @@ private:
caf::PdmField<QString> m_tvdMslKey;
caf::PdmField<QString> m_tvdRkbKey;
};
//==================================================================================================
///
//==================================================================================================
class RimcWellPath_appendLateral : public caf::PdmVoidObjectMethod
{
CAF_PDM_HEADER_INIT;
public:
RimcWellPath_appendLateral( caf::PdmObjectHandle* self );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
private:
caf::PdmPtrField<RimWellPath*> m_lateral;
};
@@ -0,0 +1,63 @@
import rips
resinsight = rips.Instance.find()
if resinsight is None:
raise RuntimeError("No running ResInsight instance found on the expected ports.")
# Create a modeled well path and add well path targets
# The coordinates are based on the Norne case
well_path_coll = resinsight.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "Test Well-1"
well_path.update()
geometry = well_path.well_path_geometry()
reference_point = geometry.reference_point
reference_point[0] = 457196
reference_point[1] = 7322270
reference_point[2] = 2742
geometry.update() # Commit updates back to ResInsight
# Create the first well target at the reference point
coord = [0, 0, 0]
geometry.append_well_target(coord)
# Append new well targets relative the the reference point
coord = [454.28, 250, -10]
target = geometry.append_well_target(coord)
coord = [1054.28, 250, -50]
target = geometry.append_well_target(coord)
################################
## Create a lateral well path and append it to the main well path
################################
lateral_well = well_path_coll.add_new_object(rips.ModeledWellPath)
lateral_well.name = "Lateral-1"
lateral_well.update()
lateral_geometry = lateral_well.well_path_geometry()
# Only create geometry starting from the first target of the lateral
lateral_geometry.use_auto_generated_target_at_sea_level = False
reference_point = lateral_geometry.reference_point
reference_point[0] = 457550
reference_point[1] = 7322481
reference_point[2] = 2735
lateral_geometry.update() # Commit updates back to ResInsight
coord = [0, 0, 0]
lateral_geometry.append_well_target(coord)
coord = [454.28, 250, -10]
target = lateral_geometry.append_well_target(coord)
coord = [1054.28, 250, -50]
target = lateral_geometry.append_well_target(coord)
# Append the lateral to the main well path
well_path.append_lateral(lateral_well)
@@ -236,3 +236,154 @@ def test_perforation_settings(rips_instance, initialize_test):
assert non_darcy_parameters_updated.porosity_scaling_factor == 0.59
assert non_darcy_parameters_updated.relative_gas_density == 0.23
assert non_darcy_parameters_updated.well_radius == 12.12
def test_append_lateral(rips_instance, initialize_test):
well_path_coll = rips_instance.project.well_path_collection()
# Create main well path
main_well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
main_well_path.name = "main_well"
main_well_path.update()
# Create lateral well path
lateral_well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
lateral_well_path.name = "lateral_well"
lateral_well_path.update()
# Test appending lateral to main well path
# This should fail because well paths don't have geometry defined
try:
main_well_path.append_lateral(well_path_lateral=lateral_well_path)
# If we get here, the operation succeeded unexpectedly
assert False, "Expected append_lateral to fail due to missing geometry"
except rips.RipsError as e:
# Expected error message when no geometry is available
expected_msg = "No geometry available for main well path. Cannot append lateral to main well path."
assert str(e) == expected_msg
# Verify the lateral well path objects still exist and have correct names
assert lateral_well_path is not None
assert lateral_well_path.name == "lateral_well"
assert main_well_path is not None
assert main_well_path.name == "main_well"
def test_append_lateral_with_geometry(rips_instance, initialize_test):
well_path_coll = rips_instance.project.well_path_collection()
# Create main well path with geometry using coordinates
main_coordinates = [
[1000.0, 2000.0, 0.0], # Surface point
[1000.0, 2000.0, -100.0], # 100m down
[1000.0, 2000.0, -500.0], # 500m down
[1000.0, 2000.0, -1000.0], # 1000m down
]
main_well_path = well_path_coll.import_well_path_from_points(
name="main_well_with_geometry", coordinates=main_coordinates
)
# Create lateral well path with geometry that starts along the main well path
lateral_coordinates = [
[1000.0, 2000.0, -300.0], # Start along main well at 300m depth
[1020.0, 2020.0, -320.0], # Deviate slightly
[1050.0, 2050.0, -350.0], # Continue deviation
[1100.0, 2100.0, -400.0], # Final lateral point
]
lateral_well_path = well_path_coll.import_well_path_from_points(
name="lateral_well_with_geometry", coordinates=lateral_coordinates
)
# Test appending lateral to main well path - this should work with geometry
# Note: The actual connection may still fail if the geometries don't align properly,
# but we're testing that the function can be called with valid geometry objects
try:
main_well_path.append_lateral(well_path_lateral=lateral_well_path)
# If successful, verify both well paths still exist
assert main_well_path.name == "main_well_with_geometry"
assert lateral_well_path.name == "lateral_well_with_geometry"
except rips.RipsError as e:
# If it fails, it should be for geometric reasons, not missing geometry
error_msg = str(e)
assert (
"No geometry available" not in error_msg
), f"Unexpected geometry error: {error_msg}"
# Other geometric errors (like alignment issues) are acceptable for this test
print(f"Geometric alignment error (expected): {error_msg}")
# Verify the well path objects are still valid regardless of connection result
assert main_well_path is not None
assert lateral_well_path is not None
def test_append_lateral_check_connection(rips_instance, initialize_test):
well_path_coll = rips_instance.project.well_path_collection()
# Create main well path using ModeledWellPath with targets
main_well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
main_well_path.name = "main_well_check_connection"
main_well_path.update()
# Add geometry targets to main well path
main_geometry = main_well_path.well_path_geometry()
main_geometry.append_well_target([1000.0, 2000.0, 0.0]) # Surface
main_geometry.append_well_target([1000.0, 2000.0, -100.0]) # 100m down
main_geometry.append_well_target([1000.0, 2000.0, -300.0]) # 300m down
main_geometry.append_well_target([1000.0, 2000.0, -500.0]) # 500m down
main_geometry.update()
# Create lateral well path using ModeledWellPath with targets
lateral_well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
lateral_well_path.name = "lateral_well_check_connection"
lateral_well_path.update()
# Add geometry targets to lateral well path
lateral_geometry = lateral_well_path.well_path_geometry()
lateral_geometry.append_well_target(
[1000.0, 2000.0, -150.0]
) # Start at 150m on main
lateral_geometry.append_well_target([1020.0, 2020.0, -200.0]) # Deviate
lateral_geometry.append_well_target([1050.0, 2050.0, -250.0]) # Continue
lateral_geometry.update()
# Get initial geometry state
initial_attached_state = lateral_geometry.attached_to_parent_well
# Test appending lateral to main well path
try:
main_well_path.append_lateral(well_path_lateral=lateral_well_path)
# Check if connection was successful by examining the geometry properties
lateral_geometry.update() # Refresh from server
final_attached_state = lateral_geometry.attached_to_parent_well
# If connection was successful, verify the parent relationship
if final_attached_state and not initial_attached_state:
print("Successfully connected lateral to parent well path")
assert lateral_well_path.name == "lateral_well_check_connection"
assert main_well_path.name == "main_well_check_connection"
# The lateral should now be marked as attached to parent
assert lateral_geometry.attached_to_parent_well
else:
print("Connection did not change attachment state - may be geometric issue")
# Still a valid test if the function completed without error
except rips.RipsError as e:
# Log the error for debugging but don't fail the test
# Different geometric configurations may result in different connection outcomes
error_msg = str(e)
print(f"Connection attempt resulted in error: {error_msg}")
# Ensure it's not a "no geometry" error since we provided geometry
assert (
"No geometry available" not in error_msg
), f"Unexpected geometry error: {error_msg}"
# Verify both well paths are still valid
assert main_well_path is not None
assert lateral_well_path is not None
assert main_well_path.name == "main_well_check_connection"
assert lateral_well_path.name == "lateral_well_check_connection"