#12569 Python: Add API for creating well paths from XYZ coordinates

Add import_fixed_trajectory_well_path() method to WellPathCollection that enables
creation of well paths from coordinate lists in Python. Implements feature request
for programmatic well path creation without file-based imports.

- Add RimFixedTrajectoryWellPath class for coordinate-based well paths
- Add GRPC interface for coordinate data transfer via key-value store
- Add comprehensive Python API with input validation and error handling
- Add example script importing well on Norne.
- Add 7 unit tests covering all functionality and edge cases

The API allows creating well paths like:
well_path = collection.import_fixed_trajectory_well_path(
    name="MyWell",
    coordinates=[[x1,y1,z1], [x2,y2,z2], ...]
)
This commit is contained in:
Kristian Bendiksen committed 2025-08-25 13:27:31 +02:00
1 parent 66756ff21e
commit 83ae9fb7ac
10 files changed
+601

No files matched your search

@@ -0,0 +1,64 @@
"""
Example demonstrating how to create well paths from XYZ coordinates
using the import_fixed_trajectory_well_path API.
"""
import sys
import os
import rips
def create_norne_well():
coordinates = [
[457121.858, 7322122.992, -371.7],
[457121.820, 7322122.956, -404.999],
[457121.767, 7322122.946, -434.999],
[457123.250, 7322120.048, -644.946],
[457132.573, 7322112.750, -914.534],
[457132.853, 7322111.663, -1184.492],
[457141.541, 7322111.158, -1484.359],
[457147.535, 7322109.991, -1694.269],
[457152.835, 7322107.659, -1934.198],
[457142.201, 7322088.313, -2182.14],
[457108.159, 7322025.317, -2335.345],
[457077.300, 7321905.985, -2464.557],
[457100.629, 7321698.916, -2584.388],
[457226.480, 7321457.656, -2629.045],
[457374.455, 7321253.227, -2633.377],
[457499.332, 7321103.741, -2630.816],
[457581.202, 7321019.105, -2630.42],
[457661.101, 7320934.536, -2630.212],
[457727.784, 7320862.819, -2632.240],
]
return coordinates
resinsight = rips.Instance.find()
# Get the well path collection
well_path_coll = resinsight.project.well_path_collection()
# Create different types of well paths
well_paths_data = [
("B 2-H", create_norne_well()),
]
created_wells = []
for name, coordinates in well_paths_data:
print(f"\nCreating well path: {name}")
print(f" Number of coordinate points: {len(coordinates)}")
print(
f" Start: [{coordinates[0][0]:.1f}, {coordinates[0][1]:.1f}, {coordinates[0][2]:.1f}]"
)
print(
f" End: [{coordinates[-1][0]:.1f}, {coordinates[-1][1]:.1f}, {coordinates[-1][2]:.1f}]"
)
# Create the well path using the new API
well_path = well_path_coll.import_fixed_trajectory_well_path(
name=name, coordinates=coordinates
)
created_wells.append(well_path)
+1
View File
@@ -17,6 +17,7 @@ from .contour_map import (
)
from .well_log_plot import WellLogPlot as WellLogPlot
from .well_path import WellPath as WellPath
from . import well_path_collection
from .simulation_well import SimulationWell as SimulationWell
from .exception import RipsError as RipsError
from .surface import RegularSurface as RegularSurface
+3
View File
@@ -46,6 +46,9 @@ disable_error_code = no-untyped-def, no-any-return, attr-defined
[mypy-rips.well_log_plot.*]
disable_error_code = no-any-return
[mypy-rips.well_path_collection.*]
disable_error_code = attr-defined
[mypy-rips.contour_map.*]
disable_error_code = no-redef
@@ -0,0 +1,162 @@
import sys
import os
import pytest
sys.path.insert(1, os.path.join(sys.path[0], "../../"))
def test_import_fixed_trajectory_well_path_basic(rips_instance, initialize_test):
"""Test basic creation of a fixed trajectory well path"""
well_path_coll = rips_instance.project.well_path_collection()
# Define a simple vertical well path
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
]
well_path = well_path_coll.import_fixed_trajectory_well_path(
name="TestFixedWell", coordinates=coordinates
)
assert well_path is not None
assert well_path.name == "TestFixedWell"
def test_import_fixed_trajectory_well_path_deviated(rips_instance, initialize_test):
"""Test creation of a deviated well path"""
well_path_coll = rips_instance.project.well_path_collection()
# Define a deviated well path
coordinates = [
[1000.0, 2000.0, 0.0], # Surface point
[1020.0, 2010.0, -200.0], # Slight deviation
[1050.0, 2030.0, -500.0], # More deviation
[1100.0, 2080.0, -1000.0], # Final target
]
well_path = well_path_coll.import_fixed_trajectory_well_path(
name="TestDeviatedWell", coordinates=coordinates
)
assert well_path is not None
assert well_path.name == "TestDeviatedWell"
def test_import_fixed_trajectory_well_path_validation(rips_instance, initialize_test):
"""Test input validation for fixed trajectory well path"""
well_path_coll = rips_instance.project.well_path_collection()
# Test empty name
with pytest.raises(ValueError, match="Name cannot be empty"):
well_path_coll.import_fixed_trajectory_well_path(
name="", coordinates=[[1000, 2000, 0]]
)
# Test empty coordinates
with pytest.raises(ValueError, match="Coordinates list cannot be empty"):
well_path_coll.import_fixed_trajectory_well_path(
name="TestWell", coordinates=[]
)
# Test invalid coordinate format - wrong number of values
with pytest.raises(ValueError, match="must be a list/tuple of 3 values"):
well_path_coll.import_fixed_trajectory_well_path(
name="TestWell",
coordinates=[[1000, 2000]], # Missing Z coordinate
)
# Test invalid coordinate format - non-numeric values
with pytest.raises(ValueError, match="All coordinate values must be numeric"):
well_path_coll.import_fixed_trajectory_well_path(
name="TestWell", coordinates=[[1000, "invalid", 0]]
)
def test_import_fixed_trajectory_well_path_multiple(rips_instance, initialize_test):
"""Test creating multiple fixed trajectory well paths"""
well_path_coll = rips_instance.project.well_path_collection()
# Create first well path
coordinates1 = [
[1000.0, 2000.0, 0.0],
[1000.0, 2000.0, -500.0],
]
well_path1 = well_path_coll.import_fixed_trajectory_well_path(
name="Well_1", coordinates=coordinates1
)
# Create second well path
coordinates2 = [
[1500.0, 2500.0, 0.0],
[1500.0, 2500.0, -300.0],
]
well_path2 = well_path_coll.import_fixed_trajectory_well_path(
name="Well_2", coordinates=coordinates2
)
assert well_path1.name == "Well_1"
assert well_path2.name == "Well_2"
assert well_path1 != well_path2
def test_import_fixed_trajectory_well_path_single_point(rips_instance, initialize_test):
"""Test creating a well path with a single point"""
well_path_coll = rips_instance.project.well_path_collection()
# Single point well path
coordinates = [
[1000.0, 2000.0, -100.0],
]
well_path = well_path_coll.import_fixed_trajectory_well_path(
name="SinglePointWell", coordinates=coordinates
)
assert well_path is not None
assert well_path.name == "SinglePointWell"
def test_import_fixed_trajectory_well_path_various_types(
rips_instance, initialize_test
):
"""Test with various coordinate input types (list, tuple, mixed)"""
well_path_coll = rips_instance.project.well_path_collection()
# Mix of list and tuple coordinates
coordinates = [
[1000.0, 2000.0, 0.0], # List
(1010.0, 2010.0, -100.0), # Tuple
[1020, 2020, -200], # Integers
]
well_path = well_path_coll.import_fixed_trajectory_well_path(
name="MixedTypesWell", coordinates=coordinates
)
assert well_path is not None
assert well_path.name == "MixedTypesWell"
def test_import_fixed_trajectory_well_path_large_coordinates(
rips_instance, initialize_test
):
"""Test with large coordinate values (typical UTM coordinates)"""
well_path_coll = rips_instance.project.well_path_collection()
# Large UTM-style coordinates
coordinates = [
[456789.0, 6789012.0, 50.0], # Surface
[456790.0, 6789013.0, -1500.0], # Deep point
]
well_path = well_path_coll.import_fixed_trajectory_well_path(
name="UTMCoordinatesWell", coordinates=coordinates
)
assert well_path is not None
assert well_path.name == "UTMCoordinatesWell"
@@ -0,0 +1,84 @@
import uuid
from typing import List
from .pdmobject import add_method
from .resinsight_classes import WellPathCollection
from .project import Project
@add_method(WellPathCollection)
def import_fixed_trajectory_well_path(
self: WellPathCollection, name: str, coordinates: List[List[float]]
) -> object:
"""Create a well path from a list of XYZ coordinates.
Arguments:
name (str): Name of the well path
coordinates (List[List[float]]): List of [x, y, z] coordinate triplets
Returns:
RimFixedTrajectoryWellPath: The created well path object
Raises:
ValueError: If coordinates are invalid or have wrong dimensions
"""
if not name:
raise ValueError("Name cannot be empty")
if not coordinates:
raise ValueError("Coordinates list cannot be empty")
# Validate coordinate format
for i, coord in enumerate(coordinates):
if not isinstance(coord, (list, tuple)) or len(coord) != 3:
raise ValueError(
f"Coordinate at index {i} must be a list/tuple of 3 values [x, y, z]"
)
# Convert to float to validate numeric values
try:
float(coord[0])
float(coord[1])
float(coord[2])
except (ValueError, TypeError):
raise ValueError(
f"All coordinate values must be numeric. Invalid coordinate at index {i}: {coord}"
)
# Extract separate x, y, z arrays
x_coords = [float(coord[0]) for coord in coordinates]
y_coords = [float(coord[1]) for coord in coordinates]
z_coords = [float(coord[2]) for coord in coordinates]
# Generate temporary keys with shared UUID for key-value store
shared_uuid = uuid.uuid4()
x_key = f"{shared_uuid}_coordinate_x"
y_key = f"{shared_uuid}_coordinate_y"
z_key = f"{shared_uuid}_coordinate_z"
# Store coordinates in key-value store
project = self.ancestor(Project)
if not project:
raise RuntimeError("Could not find parent project")
project.set_key_values(x_key, x_coords)
project.set_key_values(y_key, y_coords)
project.set_key_values(z_key, z_coords)
try:
# Call internal method to create the well path
well_path = self.import_fixed_trajectory_well_path_internal(
name=name,
coordinate_x_key=x_key,
coordinate_y_key=y_key,
coordinate_z_key=z_key,
)
return well_path
finally:
# Clean up temporary keys from key-value store
project.remove_key_values(x_key)
project.remove_key_values(y_key)
project.remove_key_values(z_key)