mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 23:20:29 -05:00
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:
1 parent
8089739add
commit
160cb71168
16 files changed
+1211
-126
No files matched your search
+53
-29
@@ -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)
|
||||
@@ -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
|
||||
Reference in new issue
Block a user