Python: Update well log import API to accept measured depth and multiple channels

- Updated Python API to accept measured_depth list and channel_data dictionary
- Modified C++ GRPC implementation to handle CSV-formatted channel mappings
- Replaced single values parameter with dictionary of channels
- Added comprehensive validation for all input parameters
- Updated parameter name from channel_keys_json to channel_keys_csv
- Enhanced tests to cover 16 different scenarios including edge cases
- Updated example to demonstrate new dictionary-based API functionality
This commit is contained in:
Kristian Bendiksen committed 2025-09-04 09:04:56 +02:00
1 parent 8089739add
commit 160cb71168
16 files changed
+1211 -126

No files matched your search

@@ -1,62 +1,86 @@
"""
Example: Import Well Log Data to Well Paths
This example demonstrates how to import well log data from Python arrays
into ResInsight well paths using the new well log import API.
This example demonstrates how to import well log data with measured depth
and multiple channels from Python arrays into ResInsight well paths using
the new well log import API.
"""
import rips
def create_example_well_log_data():
"""Create example well log data"""
# Simulated gamma ray log data
gamma_ray = [75.2, 82.1, 78.5, 80.3, 85.7, 88.2, 90.1, 87.3, 84.6, 82.8]
"""Create example well log data with measured depth and multiple channels"""
# Measured depth values in meters
measured_depth = [1000, 1200, 1400, 1600, 1800, 2000, 2200, 2400, 2600, 2800]
# Simulated porosity log data
porosity = [0.12, 0.15, 0.18, 0.22, 0.25, 0.28, 0.24, 0.20, 0.16, 0.14]
# Multiple channel data corresponding to each measured depth
channel_data = {
"gamma_ray": [75.2, 82.1, 78.5, 80.3, 85.7, 88.2, 90.1, 87.3, 84.6, 82.8],
"porosity": [0.12, 0.15, 0.18, 0.22, 0.25, 0.28, 0.24, 0.20, 0.16, 0.14],
"permeability": [15.5, 22.3, 35.8, 48.2, 65.1, 78.4, 52.9, 38.7, 25.6, 18.9],
"resistivity": [2.1, 1.8, 3.2, 2.5, 1.9, 1.4, 2.8, 3.1, 2.7, 2.3],
}
# Simulated permeability log data
permeability = [15.5, 22.3, 35.8, 48.2, 65.1, 78.4, 52.9, 38.7, 25.6, 18.9]
return {"Gamma_Ray": gamma_ray, "Porosity": porosity, "Permeability": permeability}
return measured_depth, channel_data
def main():
# Connect to ResInsight
resinsight = rips.Instance()
resinsight = rips.Instance.find()
project = resinsight.project
# Create a well path from coordinates
coordinates = [
[458000, 5934000, 1000], # X, Y, Z coordinates
[458100, 5934100, 1500],
[458200, 5934200, 2000],
[458300, 5934300, 2500],
[458400, 5934400, 3000],
[458000, 5934000, -1000], # X, Y, Z coordinates
[458100, 5934100, -1500],
[458200, 5934200, -2000],
[458300, 5934300, -2500],
[458400, 5934400, -3000],
]
well_path = project.well_path_collection.import_fixed_trajectory_well_path(
well_path = project.well_path_collection().import_well_path_from_points(
name="ExampleWell", coordinates=coordinates
)
print(f"Created well path: {well_path.name}")
# Get example well log data
log_data = create_example_well_log_data()
measured_depth, channel_data = create_example_well_log_data()
# Import each well log channel
for log_name, values in log_data.items():
well_log = well_path.add_well_log(name=log_name, values=values)
print(f"Added well log: {well_log.name} with {len(values)} data points")
# Import well log with multiple channels using the new API
well_log = well_path.add_well_log(
name="ComprehensiveWellLog",
measured_depth=measured_depth,
channel_data=channel_data,
)
# Verify all logs were added
well_logs = well_path.well_logs()
print(f"\\nWell path now has {len(well_logs)} well logs:")
for log in well_logs:
print(f" - {log.name}")
print(f"Added well log: {well_log.name}")
print(
f" - Measured depth range: {min(measured_depth):.1f} - {max(measured_depth):.1f} m"
)
print(f" - Number of data points: {len(measured_depth)}")
print(f" - Channels imported: {', '.join(channel_data.keys())}")
print("\\nWell log import completed successfully!")
# You can also import additional well logs with different data
# For example, a well log with different measured depth intervals
additional_measured_depth = [1100, 1300, 1500, 1700, 1900, 2100, 2300, 2500, 2700]
additional_channel_data = {
"caliper": [8.5, 8.2, 8.7, 8.3, 8.1, 8.4, 8.6, 8.2, 8.3],
"neutron": [0.18, 0.22, 0.25, 0.28, 0.31, 0.29, 0.26, 0.23, 0.20],
}
additional_well_log = well_path.add_well_log(
name="AdditionalWellLog",
measured_depth=additional_measured_depth,
channel_data=additional_channel_data,
)
print(f"Added additional well log: {additional_well_log.name}")
print(
f" - MD range: {min(additional_measured_depth):.1f} - {max(additional_measured_depth):.1f} m"
)
print(f" - Channels imported: {', '.join(additional_channel_data.keys())}")
if __name__ == "__main__":
@@ -1,7 +1,4 @@
import pytest
import rips
import os
@pytest.fixture
@@ -30,10 +27,16 @@ def test_add_well_log_basic(project):
# Test data
name = "TestLog"
values = [10.5, 20.3, 30.8, 40.2, 15.7]
measured_depth = [1000, 1500, 2000, 2500, 3000]
channel_data = {
"porosity": [0.10, 0.15, 0.20, 0.18, 0.12],
"permeability": [10.5, 25.3, 45.8, 35.2, 18.7],
}
# Import well log
well_log = well_path.add_well_log(name=name, values=values)
well_log = well_path.add_well_log(
name=name, measured_depth=measured_depth, channel_data=channel_data
)
# Verify well log was created
assert well_log is not None
@@ -44,60 +47,176 @@ def test_add_well_log_empty_name(project):
"""Test that empty name raises ValueError"""
proj, well_path = project
values = [1.0, 2.0, 3.0]
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
with pytest.raises(ValueError, match="Name cannot be empty"):
well_path.add_well_log(name="", values=values)
well_path.add_well_log(
name="", measured_depth=measured_depth, channel_data=channel_data
)
def test_add_well_log_empty_values(project):
"""Test that empty values raises ValueError"""
def test_add_well_log_empty_measured_depth(project):
"""Test that empty measured depth raises ValueError"""
proj, well_path = project
with pytest.raises(ValueError, match="Values list cannot be empty"):
well_path.add_well_log(name="TestLog", values=[])
channel_data = {"porosity": [0.1, 0.2, 0.3]}
with pytest.raises(ValueError, match="Measured depth list cannot be empty"):
well_path.add_well_log(
name="TestLog", measured_depth=[], channel_data=channel_data
)
def test_add_well_log_invalid_values_type(project):
"""Test that non-list values raises TypeError"""
def test_add_well_log_empty_channel_data(project):
"""Test that empty channel data raises ValueError"""
proj, well_path = project
with pytest.raises(TypeError, match="Values must be a list"):
well_path.add_well_log(name="TestLog", values="not a list")
measured_depth = [1000, 1500, 2000]
with pytest.raises(ValueError, match="Channel data dictionary cannot be empty"):
well_path.add_well_log(
name="TestLog", measured_depth=measured_depth, channel_data={}
)
def test_add_well_log_non_numeric_values(project):
"""Test that non-numeric values raises TypeError"""
def test_add_well_log_invalid_measured_depth_type(project):
"""Test that non-list measured depth raises TypeError"""
proj, well_path = project
values = [1.0, "invalid", 3.0]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
with pytest.raises(TypeError, match="All values must be numeric"):
well_path.add_well_log(name="TestLog", values=values)
with pytest.raises(TypeError, match="Measured depth must be a list"):
well_path.add_well_log(
name="TestLog", measured_depth="not a list", channel_data=channel_data
)
def test_add_well_log_invalid_channel_data_type(project):
"""Test that non-dict channel data raises TypeError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
with pytest.raises(TypeError, match="Channel data must be a dictionary"):
well_path.add_well_log(
name="TestLog", measured_depth=measured_depth, channel_data="not a dict"
)
def test_add_well_log_non_numeric_measured_depth(project):
"""Test that non-numeric measured depth values raise TypeError"""
proj, well_path = project
measured_depth = [1000, "invalid", 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
with pytest.raises(TypeError, match="All measured depth values must be numeric"):
well_path.add_well_log(
name="TestLog", measured_depth=measured_depth, channel_data=channel_data
)
def test_add_well_log_non_numeric_channel_values(project):
"""Test that non-numeric channel values raise TypeError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, "invalid", 0.3]}
with pytest.raises(
TypeError, match="All values in channel 'porosity' must be numeric"
):
well_path.add_well_log(
name="TestLog", measured_depth=measured_depth, channel_data=channel_data
)
def test_add_well_log_mismatched_lengths(project):
"""Test that mismatched lengths raise ValueError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2]} # Only 2 values
with pytest.raises(
ValueError,
match="Channel 'porosity' has 2 values but measured depth has 3 values",
):
well_path.add_well_log(
name="TestLog", measured_depth=measured_depth, channel_data=channel_data
)
def test_add_well_log_empty_channel_name(project):
"""Test that empty channel name raises ValueError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"": [0.1, 0.2, 0.3]} # Empty channel name
with pytest.raises(ValueError, match="Channel name cannot be empty"):
well_path.add_well_log(
name="TestLog", measured_depth=measured_depth, channel_data=channel_data
)
def test_add_well_log_mixed_numeric_types(project):
"""Test that mixed numeric types work correctly"""
proj, well_path = project
# Mix of int, float
values = [1, 2.5, 3, 4.8, 5]
# Mix of int, float in both measured depth and channel data
measured_depth = [1000, 1500.5, 2000, 2500.25, 3000]
channel_data = {
"porosity": [0.1, 0.15, 0.2, 0.18, 0.12],
"gamma_ray": [75, 80.5, 85, 82.3, 78],
}
well_log = well_path.add_well_log(name="MixedLog", values=values)
well_log = well_path.add_well_log(
name="MixedLog", measured_depth=measured_depth, channel_data=channel_data
)
assert well_log is not None
assert well_log.name == "MixedLog"
def test_add_well_log_multiple_channels(project):
"""Test adding well log with multiple channels"""
proj, well_path = project
measured_depth = [1000, 1500, 2000, 2500, 3000]
channel_data = {
"porosity": [0.10, 0.15, 0.20, 0.18, 0.12],
"permeability": [10.5, 25.3, 45.8, 35.2, 18.7],
"gamma_ray": [75.2, 82.1, 88.5, 85.3, 79.8],
"resistivity": [2.5, 3.2, 1.8, 2.1, 2.8],
}
well_log = well_path.add_well_log(
name="MultiChannelLog", measured_depth=measured_depth, channel_data=channel_data
)
assert well_log is not None
assert well_log.name == "MultiChannelLog"
def test_add_multiple_well_logs(project):
"""Test adding multiple well logs to same well path"""
proj, well_path = project
# Add first well log
log1 = well_path.add_well_log(name="Log1", values=[1.0, 2.0, 3.0])
measured_depth1 = [1000, 1500, 2000]
channel_data1 = {"porosity": [0.1, 0.2, 0.3]}
log1 = well_path.add_well_log(
name="Log1", measured_depth=measured_depth1, channel_data=channel_data1
)
# Add second well log
log2 = well_path.add_well_log(name="Log2", values=[10.0, 20.0, 30.0])
measured_depth2 = [1200, 1700, 2200, 2700]
channel_data2 = {"gamma_ray": [75, 80, 85, 82]}
log2 = well_path.add_well_log(
name="Log2", measured_depth=measured_depth2, channel_data=channel_data2
)
# Verify both logs exist
assert log1 is not None
@@ -105,18 +224,21 @@ def test_add_multiple_well_logs(project):
assert log1.name == "Log1"
assert log2.name == "Log2"
# Both logs were created successfully (verified by assertions above)
# Note: well_logs() method not currently exposed in Python API
def test_add_well_log_large_dataset(project):
"""Test importing large dataset"""
proj, well_path = project
# Generate large dataset
values = [float(i) * 0.1 for i in range(1000)]
measured_depth = [float(i * 10) for i in range(1000)] # 0, 10, 20, ..., 9990
channel_data = {
"porosity": [0.1 + (i % 100) * 0.001 for i in range(1000)],
"gamma_ray": [70.0 + (i % 50) * 0.5 for i in range(1000)],
}
well_log = well_path.add_well_log(name="LargeLog", values=values)
well_log = well_path.add_well_log(
name="LargeLog", measured_depth=measured_depth, channel_data=channel_data
)
assert well_log is not None
assert well_log.name == "LargeLog"
@@ -126,9 +248,12 @@ def test_add_well_log_single_value(project):
"""Test adding well log with single value"""
proj, well_path = project
values = [42.0]
measured_depth = [1000.0]
channel_data = {"porosity": [0.15]}
well_log = well_path.add_well_log(name="SingleValue", values=values)
well_log = well_path.add_well_log(
name="SingleValue", measured_depth=measured_depth, channel_data=channel_data
)
assert well_log is not None
assert well_log.name == "SingleValue"
@@ -138,9 +263,180 @@ def test_add_well_log_special_numeric_values(project):
"""Test adding well log with special numeric values"""
proj, well_path = project
values = [0.0, -5.5, 1e6, 1.23e-4, 99.999]
measured_depth = [0.0, 1000.5, 2000.0, 3000.25, 4000.0]
channel_data = {
"special_values": [0.0, -5.5, 1e6, 1.23e-4, 99.999],
"porosity": [0.0, 0.15, 0.25, 0.18, 0.0],
}
well_log = well_path.add_well_log(name="SpecialValues", values=values)
well_log = well_path.add_well_log(
name="SpecialValues", measured_depth=measured_depth, channel_data=channel_data
)
assert well_log is not None
assert well_log.name == "SpecialValues"
def test_add_well_log_with_tvd_msl(project):
"""Test adding well log with TVD MSL values"""
proj, well_path = project
measured_depth = [1000, 1500, 2000, 2500, 3000]
channel_data = {"porosity": [0.10, 0.15, 0.20, 0.18, 0.12]}
tvd_msl = [980, 1480, 1980, 2480, 2980] # TVD MSL values
well_log = well_path.add_well_log(
name="WithTvdMsl",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_msl=tvd_msl,
)
assert well_log is not None
assert well_log.name == "WithTvdMsl"
def test_add_well_log_with_tvd_rkb(project):
"""Test adding well log with TVD RKB values"""
proj, well_path = project
measured_depth = [1000, 1500, 2000, 2500, 3000]
channel_data = {"porosity": [0.10, 0.15, 0.20, 0.18, 0.12]}
tvd_rkb = [1020, 1520, 2020, 2520, 3020] # TVD RKB values
well_log = well_path.add_well_log(
name="WithTvdRkb",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_rkb=tvd_rkb,
)
assert well_log is not None
assert well_log.name == "WithTvdRkb"
def test_add_well_log_with_both_tvd(project):
"""Test adding well log with both TVD MSL and TVD RKB values"""
proj, well_path = project
measured_depth = [1000, 1500, 2000, 2500, 3000]
channel_data = {"porosity": [0.10, 0.15, 0.20, 0.18, 0.12]}
tvd_msl = [980, 1480, 1980, 2480, 2980] # TVD MSL values
tvd_rkb = [1020, 1520, 2020, 2520, 3020] # TVD RKB values
well_log = well_path.add_well_log(
name="WithBothTvd",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_msl=tvd_msl,
tvd_rkb=tvd_rkb,
)
assert well_log is not None
assert well_log.name == "WithBothTvd"
def test_add_well_log_tvd_msl_length_mismatch(project):
"""Test that TVD MSL length mismatch raises ValueError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
tvd_msl = [980, 1480] # Only 2 values for 3 measured depths
with pytest.raises(
ValueError, match="tvd_msl has 2 values but measured depth has 3 values"
):
well_path.add_well_log(
name="TestLog",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_msl=tvd_msl,
)
def test_add_well_log_tvd_rkb_length_mismatch(project):
"""Test that TVD RKB length mismatch raises ValueError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
tvd_rkb = [1020, 1520, 2020, 2520] # 4 values for 3 measured depths
with pytest.raises(
ValueError, match="tvd_rkb has 4 values but measured depth has 3 values"
):
well_path.add_well_log(
name="TestLog",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_rkb=tvd_rkb,
)
def test_add_well_log_tvd_msl_non_numeric(project):
"""Test that non-numeric TVD MSL values raise TypeError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
tvd_msl = [980, "invalid", 1980]
with pytest.raises(TypeError, match="All tvd_msl values must be numeric"):
well_path.add_well_log(
name="TestLog",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_msl=tvd_msl,
)
def test_add_well_log_tvd_rkb_non_numeric(project):
"""Test that non-numeric TVD RKB values raise TypeError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
tvd_rkb = [1020, 1520, "invalid"]
with pytest.raises(TypeError, match="All tvd_rkb values must be numeric"):
well_path.add_well_log(
name="TestLog",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_rkb=tvd_rkb,
)
def test_add_well_log_tvd_msl_invalid_type(project):
"""Test that non-list TVD MSL raises TypeError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
tvd_msl = "not a list"
with pytest.raises(TypeError, match="tvd_msl must be a list"):
well_path.add_well_log(
name="TestLog",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_msl=tvd_msl,
)
def test_add_well_log_tvd_rkb_invalid_type(project):
"""Test that non-list TVD RKB raises TypeError"""
proj, well_path = project
measured_depth = [1000, 1500, 2000]
channel_data = {"porosity": [0.1, 0.2, 0.3]}
tvd_rkb = "not a list"
with pytest.raises(TypeError, match="tvd_rkb must be a list"):
well_path.add_well_log(
name="TestLog",
measured_depth=measured_depth,
channel_data=channel_data,
tvd_rkb=tvd_rkb,
)
@@ -0,0 +1,73 @@
import pytest
import tempfile
import os
@pytest.fixture
def project(rips_instance):
"""Create a test project with a well path"""
# Create well path from coordinates
coordinates = [
[1000, 2000, 0],
[1000, 2000, 1000],
[1100, 2100, 2000],
[1200, 2200, 3000],
]
well_path = (
rips_instance.project.well_path_collection().import_well_path_from_points(
name="TestWell", coordinates=coordinates
)
)
return rips_instance.project, well_path
def test_well_log_project_file_persistence(project):
"""Test that well log data is persisted in project files"""
proj, well_path = project
# Test data with TVD values
name = "PersistenceTestLog"
measured_depth = [1000, 1500, 2000, 2500, 3000]
channel_data = {
"porosity": [0.10, 0.15, 0.20, 0.18, 0.12],
"permeability": [10.5, 25.3, 45.8, 35.2, 18.7],
}
tvd_msl = [980, 1480, 1980, 2480, 2980]
tvd_rkb = [1020, 1520, 2020, 2520, 3020]
# Import well log
well_log = well_path.add_well_log(
name=name,
measured_depth=measured_depth,
channel_data=channel_data,
tvd_msl=tvd_msl,
tvd_rkb=tvd_rkb,
)
# Verify well log was created
assert well_log is not None
assert well_log.name == name
# Save project to temporary file
with tempfile.NamedTemporaryFile(suffix=".rsp", delete=False) as temp_file:
temp_path = temp_file.name
try:
# Save the project
proj.save(temp_path)
# The project should save without errors, indicating PDM persistence is working
# This test primarily verifies that the PDM objects can be serialized
assert os.path.exists(temp_path)
assert os.path.getsize(temp_path) > 0
print(
f"Project saved successfully to {temp_path} ({os.path.getsize(temp_path)} bytes)"
)
finally:
# Clean up temp file
if os.path.exists(temp_path):
os.remove(temp_path)
+130 -18
View File
@@ -4,7 +4,7 @@ from .pdmobject import add_method
from .project import Project
from .resinsight_classes import WellPath
from typing import Dict, List
from typing import Dict, List, Optional
@add_method(WellPath)
@@ -63,12 +63,22 @@ def trajectory_properties(
@add_method(WellPath)
def add_well_log(self: WellPath, name: str, values: List[float]) -> object:
def add_well_log(
self: WellPath,
name: str,
measured_depth: List[float],
channel_data: Dict[str, List[float]],
tvd_msl: Optional[List[float]] = None,
tvd_rkb: Optional[List[float]] = None,
) -> object:
"""Add imported well log data to well path.
Arguments:
name (str): Name of the well log channel.
values (List[float]): Array of well log values.
name (str): Name of the well log.
measured_depth (List[float]): Array of measured depth values.
channel_data (Dict[str, List[float]]): Dictionary mapping channel names to their values.
tvd_msl (List[float], optional): Array of TVD MSL values. Must match measured_depth length if provided.
tvd_rkb (List[float], optional): Array of TVD RKB values. Must match measured_depth length if provided.
Returns:
The created well log object.
@@ -77,33 +87,135 @@ def add_well_log(self: WellPath, name: str, values: List[float]) -> object:
if not name:
raise ValueError("Name cannot be empty")
if not values:
raise ValueError("Values list cannot be empty")
if not measured_depth:
raise ValueError("Measured depth list cannot be empty")
if not isinstance(values, list):
raise TypeError("Values must be a list")
if not isinstance(measured_depth, list):
raise TypeError("Measured depth must be a list")
# Validate that all values are numeric
if not channel_data:
raise ValueError("Channel data dictionary cannot be empty")
if not isinstance(channel_data, dict):
raise TypeError("Channel data must be a dictionary")
# Validate measured depth values are numeric
try:
float_values = [float(v) for v in values]
float_measured_depth = [float(v) for v in measured_depth]
except (ValueError, TypeError) as e:
raise TypeError("All values must be numeric") from e
raise TypeError("All measured depth values must be numeric") from e
# Generate temporary key for values
temp_key = f"{uuid.uuid4()}_welllog_values"
# Validate optional TVD parameters
validated_tvd_msl = None
validated_tvd_rkb = None
# Store values in key-value store
if tvd_msl is not None:
if not isinstance(tvd_msl, list):
raise TypeError("tvd_msl must be a list")
if len(tvd_msl) != len(measured_depth):
raise ValueError(
f"tvd_msl has {len(tvd_msl)} values but measured depth has {len(measured_depth)} values"
)
try:
validated_tvd_msl = [float(v) for v in tvd_msl]
except (ValueError, TypeError) as e:
raise TypeError("All tvd_msl values must be numeric") from e
if tvd_rkb is not None:
if not isinstance(tvd_rkb, list):
raise TypeError("tvd_rkb must be a list")
if len(tvd_rkb) != len(measured_depth):
raise ValueError(
f"tvd_rkb has {len(tvd_rkb)} values but measured depth has {len(measured_depth)} values"
)
try:
validated_tvd_rkb = [float(v) for v in tvd_rkb]
except (ValueError, TypeError) as e:
raise TypeError("All tvd_rkb values must be numeric") from e
# Validate channel data
validated_channels = {}
for channel_name, values in channel_data.items():
if not channel_name:
raise ValueError("Channel name cannot be empty")
if not isinstance(values, list):
raise TypeError(f"Values for channel '{channel_name}' must be a list")
if not values:
raise ValueError(
f"Values list for channel '{channel_name}' cannot be empty"
)
if len(values) != len(measured_depth):
raise ValueError(
f"Channel '{channel_name}' has {len(values)} values but measured depth has {len(measured_depth)} values"
)
# Validate that all values are numeric
try:
validated_channels[channel_name] = [float(v) for v in values]
except (ValueError, TypeError) as e:
raise TypeError(
f"All values in channel '{channel_name}' must be numeric"
) from e
# Store data in key-value store and prepare for GRPC call
project = self.ancestor(Project)
if not project:
raise RuntimeError("Could not find project")
project.set_key_values(temp_key, float_values)
# Generate unique keys for data transfer
shared_uuid = uuid.uuid4()
measured_depth_key = f"{shared_uuid}_measured_depth"
tvd_msl_key = f"{shared_uuid}_tvd_msl" if validated_tvd_msl is not None else ""
tvd_rkb_key = f"{shared_uuid}_tvd_rkb" if validated_tvd_rkb is not None else ""
# Store measured depth
project.set_key_values(measured_depth_key, float_measured_depth)
# Store optional TVD data
if validated_tvd_msl is not None:
project.set_key_values(tvd_msl_key, validated_tvd_msl)
if validated_tvd_rkb is not None:
project.set_key_values(tvd_rkb_key, validated_tvd_rkb)
# Store all channels and build channel mappings
temp_keys = [measured_depth_key]
if validated_tvd_msl is not None:
temp_keys.append(tvd_msl_key)
if validated_tvd_rkb is not None:
temp_keys.append(tvd_rkb_key)
channel_mappings = []
try:
for channel_name, values in validated_channels.items():
channel_key = f"{shared_uuid}_channel_{channel_name}"
project.set_key_values(channel_key, values)
temp_keys.append(channel_key)
channel_mappings.append(f"{channel_name}:{channel_key}")
# Create comma-separated string for channel mappings
channel_keys_csv = ",".join(channel_mappings)
# Call the internal GRPC method
well_log = self.add_well_log_internal(name=name, values_key=temp_key)
well_log = self.add_well_log_internal(
name=name,
measured_depth_key=measured_depth_key,
channel_keys_csv=channel_keys_csv,
tvd_msl_key=tvd_msl_key,
tvd_rkb_key=tvd_rkb_key,
)
return well_log
except Exception as e:
# Clean up key-value store on failure
project.remove_key_values(temp_key)
# Clean up all temporary keys on failure
for temp_key in temp_keys:
try:
project.remove_key_values(temp_key)
except Exception:
pass # Ignore cleanup errors
raise RuntimeError(f"Failed to create well log: {str(e)}") from e