Add LGR support to MSW tables

Add grid_name to relevant tables
Add support for WPIMULT
This commit is contained in:
Magne Sjaastad committed 2025-12-03 09:48:50 +01:00
1 parent e47a52b3d4
commit 88b0094dc8
9 files changed
+1105 -89

No files matched your search

@@ -0,0 +1,95 @@
# Reference Data for Completion Tests
This directory contains reference JSON files used for regression testing of the ResInsight Python gRPC API.
## completion_data_reference.json
Reference data for testing the `GetCompletionData` gRPC service method. Contains expected COMPDAT and WELSPECS table entries for a well with fixed completion parameters:
- **Well**: Imported from `wellpath_a.dev` (TEST10K dataset)
- **Start MD**: 2200.0m
- **End MD**: 2600.0m
- **Interval Length**: 400m
- **Diameter**: 0.25m
- **Skin Factor**: 0.1
- **Grid**: TEST10K_FLT_LGR_NNC
### Test Coverage
The 400m perforation interval generates **6 COMPDAT entries** covering:
- Multiple grid cells: (39,54), (39,55), (39,56)
- Multiple K layers: 15-18
- Various completion directions: Y, Z
- Different transmissibility and KH values
### Usage
The reference file is automatically used by `test_completion_data_regression_reference()` to validate that completion data generation remains consistent across code changes.
```python
# Test automatically compares current results against reference
pytest test_get_completion_data.py::test_completion_data_regression_reference
```
### Updating Reference Data
If intentional changes are made to completion data generation:
1. Delete the reference file: `rm completion_data_reference.json`
2. Run the test to regenerate: `pytest test_get_completion_data.py::test_completion_data_regression_reference`
3. Review the new reference data for correctness
4. Commit the new reference file to git
### Cross-Platform Compatibility
The tests are designed to produce identical results on Windows and Linux:
- **File Paths**: Uses `pathlib.Path` for cross-platform file paths
- **JSON Encoding**: UTF-8 encoding with `ensure_ascii=True` for consistent serialization
- **Field Ordering**: Alphabetical field ordering in dictionaries
- **Number Formatting**: Explicit `int()` and `float()` conversion with consistent rounding (6-8 decimal places)
- **JSON Serialization**: Fixed separators `(',', ':')` to eliminate whitespace variations
- **Hash Stability**: SHA256 with UTF-8 encoding for reproducible hashes across platforms
- **Data Types**: Explicit type casting to avoid platform-specific numeric representations
### Reference Data Structure
```json
{
"compdat": [
{
"comment": "Perforation Completion: MD In: 2274.13 - MD Out: 2290.66 ...",
"diameter": 0.25,
"direction": "Z",
"end_md": 2290.660636,
"grid_i": 39,
"grid_j": 54,
"kh": 8.39888987,
"lower_k": 16,
"open_shut_flag": "OPEN",
"skin_factor": 0.1,
"start_md": 2274.130017,
"transmissibility": 0.09835443,
"upper_k": 16,
"well_name": "ReferenceTestWell"
},
// ... 5 more COMPDAT entries
],
"welspecs": [
{
"well_name": "ReferenceTestWell",
"group_name": "1*",
"grid_i": 29,
"grid_j": 41,
"phase": "OIL",
// ... other WELSPECS fields
}
]
}
```
## Maintenance
- Reference data should be updated whenever the completion data generation algorithm changes intentionally
- Failed regression tests indicate unexpected changes in completion data generation
- The test includes validation that modified data properly fails comparison (failure simulation)
@@ -0,0 +1,122 @@
{
"compdat": [
{
"comment": "Perforation Completion: MD In: 2274.13 - MD Out: 2290.66 Transmissibility: 0.0983544",
"diameter": 0.25,
"direction": "Z",
"end_md": 2290.660636,
"grid_i": 39,
"grid_j": 54,
"grid_name": "",
"kh": 8.39888987,
"lower_k": 16,
"open_shut_flag": "OPEN",
"skin_factor": 0.1,
"start_md": 2274.130017,
"transmissibility": 0.09835443,
"upper_k": 16,
"well_name": "ReferenceTestWell"
},
{
"comment": "Perforation Completion: MD In: 2240.91 - MD Out: 2274.13 Transmissibility: 0.993938",
"diameter": 0.25,
"direction": "Z",
"end_md": 2274.130017,
"grid_i": 39,
"grid_j": 54,
"grid_name": "",
"kh": 84.97793886,
"lower_k": 17,
"open_shut_flag": "OPEN",
"skin_factor": 0.1,
"start_md": 2240.908558,
"transmissibility": 0.9939382,
"upper_k": 17,
"well_name": "ReferenceTestWell"
},
{
"comment": "Perforation Completion: MD In: 2200 - MD Out: 2240.91 Transmissibility: 0.0863079",
"diameter": 0.25,
"direction": "Z",
"end_md": 2240.908558,
"grid_i": 39,
"grid_j": 54,
"grid_name": "",
"kh": 7.38157699,
"lower_k": 18,
"open_shut_flag": "OPEN",
"skin_factor": 0.1,
"start_md": 2200.0,
"transmissibility": 0.08630795,
"upper_k": 18,
"well_name": "ReferenceTestWell"
},
{
"comment": "Perforation Completion: MD In: 2341.04 - MD Out: 2391.47 Transmissibility: 0.00885377",
"diameter": 0.25,
"direction": "Z",
"end_md": 2391.469453,
"grid_i": 39,
"grid_j": 55,
"grid_name": "",
"kh": 0.51920334,
"lower_k": 15,
"open_shut_flag": "OPEN",
"skin_factor": 0.1,
"start_md": 2341.037598,
"transmissibility": 0.00885377,
"upper_k": 15,
"well_name": "ReferenceTestWell"
},
{
"comment": "Perforation Completion: MD In: 2290.66 - MD Out: 2341.04 Transmissibility: 0.104071",
"diameter": 0.25,
"direction": "Z",
"end_md": 2341.037598,
"grid_i": 39,
"grid_j": 55,
"grid_name": "",
"kh": 8.88321679,
"lower_k": 16,
"open_shut_flag": "OPEN",
"skin_factor": 0.1,
"start_md": 2290.660636,
"transmissibility": 0.10407092,
"upper_k": 16,
"well_name": "ReferenceTestWell"
},
{
"comment": "Perforation Completion: MD In: 2391.47 - MD Out: 2600 Transmissibility: 0.0132193",
"diameter": 0.25,
"direction": "Y",
"end_md": 2600.0,
"grid_i": 39,
"grid_j": 56,
"grid_name": "",
"kh": 0.76175274,
"lower_k": 15,
"open_shut_flag": "OPEN",
"skin_factor": 0.1,
"start_md": 2391.469453,
"transmissibility": 0.01321926,
"upper_k": 15,
"well_name": "ReferenceTestWell"
}
],
"welspecs": [
{
"auto_shut_in": "STOP",
"cross_flow": "YES",
"fip_region": 0,
"grid_i": 29,
"grid_j": 41,
"grid_name": "",
"group_name": "1*",
"hydrostatic_density_calc": "SEG",
"inflow_equation": "STD",
"phase": "OIL",
"pvt_num": 0,
"well_name": "ReferenceTestWell"
}
]
}
@@ -0,0 +1,787 @@
import sys
import os
import json
import hashlib
from pathlib import Path
sys.path.insert(1, os.path.join(sys.path[0], "../../"))
import rips
import dataroot
import pytest
def test_get_completion_data_basic(rips_instance, initialize_test):
"""Test GetCompletionData with a well path and completions"""
# Load a grid case and import existing well path
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
assert len(case.grids()) == 2
# Import existing well path that intersects the grid properly
well_path_file = case_root_path + "/wellpath_a.dev"
well_path_names = rips_instance.project.import_well_paths([well_path_file])
assert len(well_path_names) == 1
wells = rips_instance.project.well_paths()
assert len(wells) == 1
well_path = wells[0]
well_path.name = "TestWell" # Rename for our test
well_path.update()
# Get the trajectory properties to find valid MD range
result = well_path.trajectory_properties(resampling_interval=10.0)
measured_depths = result["measured_depth"]
# Use measured depth range from the actual well trajectory
start_md = measured_depths[len(measured_depths) // 2] # Middle of well
end_md = (
measured_depths[len(measured_depths) // 2 + 10]
if len(measured_depths) > 20
else measured_depths[-1]
)
# Add perforation intervals (completions)
perf_interval = well_path.append_perforation_interval(
start_md=start_md, end_md=end_md, diameter=0.25, skin_factor=0.1
)
assert perf_interval is not None
# Test the completion_data method
completion_data = well_path.completion_data(case.id)
# Verify basic structure exists
assert completion_data is not None
# Should have WELSPECS data for the well
assert len(completion_data.welspecs) > 0
welspec = completion_data.welspecs[0]
assert welspec.well_name == "TestWell"
assert welspec.grid_i > 0
assert welspec.grid_j > 0
# Should have COMPDAT data for the perforation intervals
assert len(completion_data.compdat) > 0
compdat = completion_data.compdat[0]
assert compdat.well_name == "TestWell"
assert compdat.grid_i > 0
assert compdat.grid_j > 0
assert compdat.upper_k > 0
assert compdat.lower_k > 0
assert compdat.open_shut_flag in ["OPEN", "SHUT"]
# Check measured depth information in COMPDAT
assert compdat.HasField("start_md")
assert compdat.HasField("end_md")
assert compdat.start_md >= start_md - 10 # Allow some tolerance
assert compdat.end_md <= end_md + 10
def test_get_completion_data_with_valves(rips_instance, initialize_test):
"""Test GetCompletionData with valves/ICDs"""
# Load a grid case and import well path
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
# Import existing well path
well_path_file = case_root_path + "/wellpath_b.dev"
well_path_names = rips_instance.project.import_well_paths([well_path_file])
assert len(well_path_names) == 1
wells = rips_instance.project.well_paths()
well_path = wells[0]
well_path.name = "TestWellWithValves"
well_path.update()
# Get trajectory properties for valid MD range
result = well_path.trajectory_properties(resampling_interval=10.0)
measured_depths = result["measured_depth"]
start_md = measured_depths[len(measured_depths) // 2]
end_md = (
measured_depths[len(measured_depths) // 2 + 10]
if len(measured_depths) > 20
else measured_depths[-1]
)
# Add perforation interval with valve
perf_interval = well_path.append_perforation_interval(
start_md=start_md, end_md=end_md, diameter=0.25, skin_factor=0.1
)
# Add a valve to the perforation interval
valve_templates = rips_instance.project.valve_templates()
valve_defs = valve_templates.valve_definitions()
assert len(valve_defs) >= 1
valve_start_md = start_md + (end_md - start_md) * 0.2
valve_end_md = start_md + (end_md - start_md) * 0.8
valve = perf_interval.add_valve(
template=valve_defs[0],
start_md=valve_start_md,
end_md=valve_end_md,
valve_count=2,
)
assert valve is not None
# Test completion data
completion_data = well_path.completion_data(case.id)
assert completion_data is not None
# Should have WELSPECS
assert len(completion_data.welspecs) > 0
welspec = completion_data.welspecs[0]
assert welspec.well_name == "TestWellWithValves"
# Should have COMPDAT
assert len(completion_data.compdat) > 0
def test_get_completion_data_multiple_intervals(rips_instance, initialize_test):
"""Test GetCompletionData with multiple perforation intervals"""
# Load a grid case and import well path
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
# Import existing well path
well_path_file = case_root_path + "/wellpath_a.dev"
well_path_names = rips_instance.project.import_well_paths([well_path_file])
wells = rips_instance.project.well_paths()
well_path = wells[0]
well_path.name = "TestMultiInterval"
well_path.update()
# Get trajectory properties for valid MD ranges
result = well_path.trajectory_properties(resampling_interval=10.0)
measured_depths = result["measured_depth"]
# Create three intervals along the well path
total_length = len(measured_depths)
if total_length >= 30:
# Interval 1: Early part of well
start_md1 = measured_depths[total_length // 4]
end_md1 = measured_depths[total_length // 4 + 5]
# Interval 2: Middle part of well
start_md2 = measured_depths[total_length // 2]
end_md2 = measured_depths[total_length // 2 + 5]
# Interval 3: Later part of well
start_md3 = measured_depths[3 * total_length // 4]
end_md3 = measured_depths[3 * total_length // 4 + 5]
# Add multiple perforation intervals
interval1 = well_path.append_perforation_interval(
start_md=start_md1, end_md=end_md1, diameter=0.2, skin_factor=0.0
)
interval2 = well_path.append_perforation_interval(
start_md=start_md2, end_md=end_md2, diameter=0.25, skin_factor=0.1
)
interval3 = well_path.append_perforation_interval(
start_md=start_md3, end_md=end_md3, diameter=0.3, skin_factor=0.2
)
assert interval1 is not None
assert interval2 is not None
assert interval3 is not None
# Test completion data
completion_data = well_path.completion_data(case.id)
assert completion_data is not None
# Should have WELSPECS
assert len(completion_data.welspecs) > 0
welspec = completion_data.welspecs[0]
assert welspec.well_name == "TestMultiInterval"
# Should have multiple COMPDAT entries for different intervals
assert len(completion_data.compdat) >= 1 # At least some COMPDAT data
# Check that we have MD information in COMPDAT entries
has_md_data = False
for compdat in completion_data.compdat:
if compdat.HasField("start_md") and compdat.HasField("end_md"):
has_md_data = True
assert compdat.start_md >= 0
assert compdat.end_md >= compdat.start_md
assert has_md_data, "Should have measured depth data in COMPDAT"
else:
# Skip test if well is too short for multiple intervals
assert True # Test passes if well is too short
def test_get_completion_data_field_validation(rips_instance, initialize_test):
"""Test optional field handling in completion data"""
# Load a grid case and import well path
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
# Import existing well path
well_path_file = case_root_path + "/wellpath_a.dev"
well_path_names = rips_instance.project.import_well_paths([well_path_file])
wells = rips_instance.project.well_paths()
well_path = wells[0]
well_path.name = "FieldValidationTest"
well_path.update()
# Get valid MD range
result = well_path.trajectory_properties(resampling_interval=10.0)
measured_depths = result["measured_depth"]
start_md = measured_depths[len(measured_depths) // 2]
end_md = (
measured_depths[len(measured_depths) // 2 + 5]
if len(measured_depths) > 10
else measured_depths[-1]
)
perf_interval = well_path.append_perforation_interval(start_md, end_md, 0.25, 0.1)
assert perf_interval is not None
# Get completion data
completion_data = well_path.completion_data(case.id)
assert completion_data is not None
# Validate COMPDAT optional fields
if len(completion_data.compdat) > 0:
comp = completion_data.compdat[0]
# Test required fields
assert comp.well_name == "FieldValidationTest"
assert comp.grid_i > 0
assert comp.grid_j > 0
assert comp.upper_k > 0
assert comp.lower_k > 0
assert comp.open_shut_flag in ["OPEN", "SHUT"]
# Test optional double fields - verify type and range when present
if comp.HasField("transmissibility"):
assert comp.transmissibility >= 0.0
if comp.HasField("diameter"):
assert comp.diameter > 0.0
if comp.HasField("skin_factor"):
assert isinstance(comp.skin_factor, float)
if comp.HasField("start_md") and comp.HasField("end_md"):
assert comp.end_md >= comp.start_md
# Validate WELSPECS required and optional fields
if len(completion_data.welspecs) > 0:
welspec = completion_data.welspecs[0]
# Required fields
assert welspec.well_name == "FieldValidationTest"
assert welspec.group_name
assert welspec.grid_i > 0
assert welspec.grid_j > 0
assert welspec.phase in ["OIL", "GAS", "WATER", "LIQ"]
# Optional fields
if welspec.HasField("bhp_depth"):
assert welspec.bhp_depth >= 0.0
if welspec.HasField("drainage_radius"):
assert welspec.drainage_radius > 0.0
def test_get_completion_data_invalid_case_id(rips_instance, initialize_test):
"""Test error handling for invalid case ID"""
# Load a grid case
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
rips_instance.project.load_case(path=case_path)
# Create well path
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "ErrorTestWell"
well_path.update()
geometry = well_path.well_path_geometry()
surface_coord = [460000.0, 5930000.0, 1600.0]
bottom_coord = [461000.0, 5931000.0, 1900.0]
geometry.append_well_target(surface_coord)
geometry.append_well_target(bottom_coord)
well_path.update()
perf_interval = well_path.append_perforation_interval(1000.0, 1200.0, 0.25, 0.1)
assert perf_interval is not None
# Test with invalid case ID
try:
# Using a negative case ID that shouldn't exist
completion_data = well_path.completion_data(-1)
# Should return empty data or handle gracefully
assert completion_data is not None
# With invalid case, completion data should be empty or minimal
except Exception as e:
# Acceptable if it throws an error for invalid case ID
assert (
"case" in str(e).lower()
or "invalid" in str(e).lower()
or "id" in str(e).lower()
)
def test_get_completion_data_empty_well(rips_instance, initialize_test):
"""Test GetCompletionData with a well that has no completions"""
# Load a grid case
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
# Create well path without completions
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "EmptyWell"
well_path.update()
geometry = well_path.well_path_geometry()
surface_coord = [460000.0, 5930000.0, 1600.0]
bottom_coord = [461000.0, 5931000.0, 1900.0]
geometry.append_well_target(surface_coord)
geometry.append_well_target(bottom_coord)
well_path.update()
# Don't add any perforation intervals
# Test completion data - should return valid response but with empty/minimal data
completion_data = well_path.completion_data(case.id)
assert completion_data is not None
# May or may not have WELSPECS depending on implementation
# But should not have COMPDAT entries without perforations
assert len(completion_data.compdat) == 0
def test_get_completion_data_stability_comprehensive(rips_instance, initialize_test):
"""Comprehensive stability test for GetCompletionData - ensures deterministic results"""
# 1. Use deterministic test setup
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
well_path_file = case_root_path + "/wellpath_a.dev"
rips_instance.project.import_well_paths([well_path_file])
well_path = rips_instance.project.well_paths()[0]
well_path.name = "StabilityTestWell"
well_path.update()
# 2. Fixed completion parameters (no dynamic MD calculation) - 400m interval
perf_interval = well_path.append_perforation_interval(
start_md=2200.0, end_md=2600.0, diameter=0.25, skin_factor=0.1
)
assert perf_interval is not None
# 3. Get completion data multiple times
data_run1 = well_path.completion_data(case.id)
data_run2 = well_path.completion_data(case.id)
# 4. Serialize and compare
compdat1 = serialize_compdat_for_comparison(data_run1.compdat)
compdat2 = serialize_compdat_for_comparison(data_run2.compdat)
# 5. Verify deterministic behavior
assert compdat1 == compdat2, "COMPDAT data should be identical between runs"
assert len(compdat1) > 0, "Should have COMPDAT entries"
# 6. Validate expected properties
validate_compdat_properties(
data_run1.compdat, expected_diameter=0.25, expected_skin=0.1
)
# 7. Validate WELSPECS stability
welspecs1 = serialize_welspecs_for_comparison(data_run1.welspecs)
welspecs2 = serialize_welspecs_for_comparison(data_run2.welspecs)
assert welspecs1 == welspecs2, "WELSPECS data should be identical between runs"
# 8. Cross-validate with perforation interval properties
for compdat in data_run1.compdat:
assert compdat.well_name == "StabilityTestWell"
if compdat.HasField("diameter"):
assert abs(compdat.diameter - 0.25) < 0.001, "Diameter should match input"
if compdat.HasField("skin_factor"):
assert (
abs(compdat.skin_factor - 0.1) < 0.001
), "Skin factor should match input"
if compdat.HasField("start_md") and compdat.HasField("end_md"):
assert (
2190.0 <= compdat.start_md <= 2610.0
), "Start MD should be in expected range"
assert (
2190.0 <= compdat.end_md <= 2610.0
), "End MD should be in expected range"
assert compdat.end_md >= compdat.start_md, "End MD should be >= start MD"
def test_completion_data_regression_reference(rips_instance, initialize_test):
"""Regression test against stored reference JSON file"""
# Generate completion data with fixed parameters
completion_data = generate_reference_completion_data(rips_instance)
# Serialize current results
current_results = {
"compdat": serialize_compdat_for_comparison(completion_data.compdat),
"welspecs": serialize_welspecs_for_comparison(completion_data.welspecs),
}
# Path to reference data (cross-platform)
test_dir = Path(__file__).parent
reference_dir = test_dir / "reference_data"
reference_file = reference_dir / "completion_data_reference.json"
# Ensure reference directory exists
reference_dir.mkdir(exist_ok=True)
if reference_file.exists():
# Load reference and compare
with open(reference_file, "r", encoding="utf-8") as f:
reference_results = json.load(f)
# Compare COMPDAT data
assert current_results["compdat"] == reference_results["compdat"], (
"COMPDAT data differs from reference. "
"If this is expected, delete the reference file and run again to regenerate."
)
# Compare WELSPECS data
assert current_results["welspecs"] == reference_results["welspecs"], (
"WELSPECS data differs from reference. "
"If this is expected, delete the reference file and run again to regenerate."
)
print("Completion data matches reference file successfully")
else:
# Generate reference file (run once to establish baseline)
with open(reference_file, "w", encoding="utf-8") as f:
json.dump(current_results, f, indent=2, sort_keys=True)
print(f"Generated reference data at: {reference_file}")
print("Re-run the test to validate against the reference")
# Validate the generated data has expected structure
assert (
len(current_results["compdat"]) > 0
), "Should have COMPDAT entries in reference"
assert (
len(current_results["welspecs"]) > 0
), "Should have WELSPECS entries in reference"
def generate_reference_completion_data(rips_instance):
"""Generate completion data with fixed, deterministic parameters for reference testing"""
# Load case
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
# Import well path
well_path_file = case_root_path + "/wellpath_a.dev"
rips_instance.project.import_well_paths([well_path_file])
well_path = rips_instance.project.well_paths()[0]
well_path.name = "ReferenceTestWell"
well_path.update()
# Create completion with fixed parameters - 400m interval to cover multiple cells
perf_interval = well_path.append_perforation_interval(
start_md=2200.0, end_md=2600.0, diameter=0.25, skin_factor=0.1
)
assert perf_interval is not None
# Return completion data
return well_path.completion_data(case.id)
def serialize_compdat_for_comparison(compdat_entries):
"""Convert COMPDAT to stable comparison format - cross-platform consistent"""
entries = []
for comp in compdat_entries:
# Create ordered dict to ensure consistent field ordering
entry = {}
# Add fields in alphabetical order for consistency
if comp.HasField("comment"):
entry["comment"] = comp.comment
if comp.HasField("d_factor"):
entry["d_factor"] = round(float(comp.d_factor), 8)
if comp.HasField("diameter"):
entry["diameter"] = round(float(comp.diameter), 8)
if comp.HasField("direction"):
entry["direction"] = comp.direction
if comp.HasField("end_md"):
entry["end_md"] = round(float(comp.end_md), 6)
entry["grid_i"] = int(comp.grid_i)
entry["grid_j"] = int(comp.grid_j)
if comp.HasField("grid_name"):
entry["grid_name"] = comp.grid_name
if comp.HasField("kh"):
entry["kh"] = round(float(comp.kh), 8)
entry["lower_k"] = int(comp.lower_k)
entry["open_shut_flag"] = comp.open_shut_flag
if comp.HasField("saturation"):
entry["saturation"] = round(float(comp.saturation), 8)
if comp.HasField("skin_factor"):
entry["skin_factor"] = round(float(comp.skin_factor), 8)
if comp.HasField("start_md"):
entry["start_md"] = round(float(comp.start_md), 6)
if comp.HasField("transmissibility"):
entry["transmissibility"] = round(float(comp.transmissibility), 8)
entry["upper_k"] = int(comp.upper_k)
entry["well_name"] = comp.well_name
entries.append(entry)
# Sort for consistent ordering across platforms
entries.sort(key=lambda x: (x["grid_i"], x["grid_j"], x["upper_k"], x["lower_k"]))
return entries
def serialize_welspecs_for_comparison(welspecs_entries):
"""Convert WELSPECS to stable comparison format - cross-platform consistent"""
entries = []
for welspec in welspecs_entries:
# Create ordered dict with alphabetical field ordering
entry = {}
if welspec.HasField("auto_shut_in"):
entry["auto_shut_in"] = welspec.auto_shut_in
if welspec.HasField("bhp_depth"):
entry["bhp_depth"] = round(float(welspec.bhp_depth), 6)
if welspec.HasField("cross_flow"):
entry["cross_flow"] = welspec.cross_flow
if welspec.HasField("drainage_radius"):
entry["drainage_radius"] = round(float(welspec.drainage_radius), 6)
if welspec.HasField("fip_region"):
entry["fip_region"] = int(welspec.fip_region)
entry["grid_i"] = int(welspec.grid_i)
entry["grid_j"] = int(welspec.grid_j)
if welspec.HasField("grid_name"):
entry["grid_name"] = welspec.grid_name
entry["group_name"] = welspec.group_name
if welspec.HasField("hydrostatic_density_calc"):
entry["hydrostatic_density_calc"] = welspec.hydrostatic_density_calc
if welspec.HasField("inflow_equation"):
entry["inflow_equation"] = welspec.inflow_equation
entry["phase"] = welspec.phase
if welspec.HasField("pvt_num"):
entry["pvt_num"] = int(welspec.pvt_num)
entry["well_name"] = welspec.well_name
entries.append(entry)
# Sort for consistent ordering across platforms
entries.sort(key=lambda x: (x["well_name"], x["grid_i"], x["grid_j"]))
return entries
def validate_compdat_properties(
compdat_entries, expected_diameter=None, expected_skin=None
):
"""Validate that COMPDAT entries have consistent and expected properties"""
assert len(compdat_entries) > 0, "Should have at least one COMPDAT entry"
for i, compdat in enumerate(compdat_entries):
# Validate required fields
assert compdat.well_name, f"Entry {i}: Well name should not be empty"
assert compdat.grid_i > 0, f"Entry {i}: Grid I should be positive"
assert compdat.grid_j > 0, f"Entry {i}: Grid J should be positive"
assert compdat.upper_k > 0, f"Entry {i}: Upper K should be positive"
assert (
compdat.lower_k >= compdat.upper_k
), f"Entry {i}: Lower K should be >= upper K"
assert compdat.open_shut_flag in [
"OPEN",
"SHUT",
], f"Entry {i}: Invalid open/shut flag"
# Validate optional fields when present
if compdat.HasField("transmissibility"):
assert (
compdat.transmissibility >= 0
), f"Entry {i}: Transmissibility should be non-negative"
if compdat.HasField("diameter") and expected_diameter is not None:
assert (
abs(compdat.diameter - expected_diameter) < 0.001
), f"Entry {i}: Diameter mismatch"
if compdat.HasField("skin_factor") and expected_skin is not None:
assert (
abs(compdat.skin_factor - expected_skin) < 0.001
), f"Entry {i}: Skin factor mismatch"
if compdat.HasField("start_md") and compdat.HasField("end_md"):
assert (
compdat.end_md >= compdat.start_md
), f"Entry {i}: End MD should be >= start MD"
assert compdat.start_md >= 0, f"Entry {i}: Start MD should be non-negative"
if compdat.HasField("kh"):
assert compdat.kh >= 0, f"Entry {i}: KH should be non-negative"
def generate_compdat_hash(compdat_data):
"""Generate hash for regression detection - cross-platform stable"""
# Ensure consistent JSON serialization across platforms
data_str = json.dumps(
compdat_data,
sort_keys=True,
separators=(",", ":"), # Remove whitespace variations
ensure_ascii=True, # Avoid unicode encoding differences
)
# Use UTF-8 encoding explicitly for cross-platform consistency
return hashlib.sha256(data_str.encode("utf-8")).hexdigest()
def test_completion_data_hash_cross_platform_stability(rips_instance, initialize_test):
"""Test that hash generation is stable across different platforms"""
# Generate test data
completion_data = generate_reference_completion_data(rips_instance)
# Serialize the same data multiple times
serialized_data1 = {
"compdat": serialize_compdat_for_comparison(completion_data.compdat),
"welspecs": serialize_welspecs_for_comparison(completion_data.welspecs),
}
serialized_data2 = {
"compdat": serialize_compdat_for_comparison(completion_data.compdat),
"welspecs": serialize_welspecs_for_comparison(completion_data.welspecs),
}
# Generate hashes
hash1 = generate_compdat_hash(serialized_data1)
hash2 = generate_compdat_hash(serialized_data2)
# Should be identical
assert hash1 == hash2, "Hash should be deterministic for same data"
# Test specific cross-platform concerns
test_data = [
{"float_val": 0.12345678901234567, "int_val": 42, "str_val": "test"},
{"float_val": 1.0000000000000001, "int_val": 0, "str_val": ""},
]
# Generate hash with explicit settings
json_str1 = json.dumps(
test_data, sort_keys=True, separators=(",", ":"), ensure_ascii=True
)
json_str2 = json.dumps(
test_data, sort_keys=True, separators=(",", ":"), ensure_ascii=True
)
hash_test1 = hashlib.sha256(json_str1.encode("utf-8")).hexdigest()
hash_test2 = hashlib.sha256(json_str2.encode("utf-8")).hexdigest()
assert hash_test1 == hash_test2, "JSON serialization should be deterministic"
print(f"Cross-platform stable hash: {hash1[:16]}...")
def test_completion_data_reference_failure_simulation(rips_instance, initialize_test):
"""Test that the reference test properly detects changes (simulate failure scenario)"""
# This test validates that our reference test would catch real changes
# by temporarily creating modified data and verifying it fails comparison
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
well_path_file = case_root_path + "/wellpath_a.dev"
rips_instance.project.import_well_paths([well_path_file])
well_path = rips_instance.project.well_paths()[0]
well_path.name = "ModifiedTestWell" # Different well name
well_path.update()
# Same completion parameters as reference
perf_interval = well_path.append_perforation_interval(
start_md=2200.0, end_md=2600.0, diameter=0.25, skin_factor=0.1
)
completion_data = well_path.completion_data(case.id)
modified_results = {
"compdat": serialize_compdat_for_comparison(completion_data.compdat),
"welspecs": serialize_welspecs_for_comparison(completion_data.welspecs),
}
# Load reference data
test_dir = Path(__file__).parent
reference_file = test_dir / "reference_data" / "completion_data_reference.json"
with open(reference_file, "r", encoding="utf-8") as f:
reference_results = json.load(f)
# This should fail because well names are different
assert (
modified_results["compdat"] != reference_results["compdat"]
), "Modified data should differ from reference (well name changed)"
assert (
modified_results["welspecs"] != reference_results["welspecs"]
), "Modified WELSPECS should differ from reference (well name changed)"
print("Reference test correctly detects data changes")
def test_completion_data_cross_validation(rips_instance, initialize_test):
"""Cross-validate completion data with perforation interval properties"""
case_root_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC"
case_path = case_root_path + "/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
well_path_file = case_root_path + "/wellpath_a.dev"
rips_instance.project.import_well_paths([well_path_file])
well_path = rips_instance.project.well_paths()[0]
well_path.name = "CrossValidationWell"
well_path.update()
# Create perforation with known parameters
diameter = 0.3
skin_factor = 0.05
start_md = 2400.0
end_md = 2420.0
perf_interval = well_path.append_perforation_interval(
start_md, end_md, diameter, skin_factor
)
# Get data via gRPC API
completion_data = well_path.completion_data(case.id)
# Cross-validate with perforation interval properties
assert len(completion_data.compdat) > 0, "Should have COMPDAT entries"
# Validate against original interval properties
found_matching_entry = False
for compdat in completion_data.compdat:
assert compdat.well_name == "CrossValidationWell", "Well name should match"
# Check diameter matches
if compdat.HasField("diameter"):
if abs(compdat.diameter - diameter) < 0.001:
found_matching_entry = True
# Also check skin factor for this entry
if compdat.HasField("skin_factor"):
assert (
abs(compdat.skin_factor - skin_factor) < 0.001
), "Skin factor should match"
# Check MD range
if compdat.HasField("start_md") and compdat.HasField("end_md"):
assert (
start_md <= compdat.start_md <= end_md + 5
), "Start MD should be in range"
assert (
start_md <= compdat.end_md <= end_md + 5
), "End MD should be in range"
assert found_matching_entry, f"Should find COMPDAT entry with diameter {diameter}"