Skip to content

Commit e27ca15

Browse files
committed
feat: Add orientation data loading functionality
- Add orientation schema and loading methods - Implement orientation data validation and polarity handling - Add comprehensive test suite for orientation loading - Fix polarity handling in test data
1 parent b4548f2 commit e27ca15

File tree

3 files changed

+232
-22
lines changed

3 files changed

+232
-22
lines changed

gempy/modules/json_io/json_operations.py

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from gempy.core.data.structural_frame import StructuralFrame
1313
from gempy.core.data.grid import Grid
1414
from gempy.core.data.geo_model import GeoModel
15-
from .schema import SurfacePoint, GemPyModelJson
15+
from .schema import SurfacePoint, Orientation, GemPyModelJson
1616

1717

1818
class JsonIO:
@@ -36,11 +36,12 @@ def load_model_from_json(file_path: str) -> GeoModel:
3636
if not JsonIO._validate_json_schema(data):
3737
raise ValueError("Invalid JSON schema")
3838

39-
# Load surface points
39+
# Load surface points and orientations
4040
surface_points = JsonIO._load_surface_points(data['surface_points'])
41+
orientations = JsonIO._load_orientations(data['orientations'])
4142

4243
# TODO: Load other components
43-
raise NotImplementedError("Only surface points loading is implemented")
44+
raise NotImplementedError("Only surface points and orientations loading is implemented")
4445

4546
@staticmethod
4647
def _load_surface_points(surface_points_data: List[SurfacePoint]) -> SurfacePointsTable:
@@ -89,6 +90,69 @@ def _load_surface_points(surface_points_data: List[SurfacePoint]) -> SurfacePoin
8990
nugget=nugget,
9091
name_id_map=name_id_map
9192
)
93+
94+
@staticmethod
95+
def _load_orientations(orientations_data: List[Orientation]) -> OrientationsTable:
96+
"""
97+
Load orientations from JSON data.
98+
99+
Args:
100+
orientations_data (List[Orientation]): List of orientation dictionaries
101+
102+
Returns:
103+
OrientationsTable: A new OrientationsTable instance
104+
105+
Raises:
106+
ValueError: If the data is invalid or missing required fields
107+
"""
108+
# Validate data structure
109+
required_fields = {'x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'id', 'nugget', 'polarity'}
110+
for i, ori in enumerate(orientations_data):
111+
missing_fields = required_fields - set(ori.keys())
112+
if missing_fields:
113+
raise ValueError(f"Missing required fields in orientation {i}: {missing_fields}")
114+
115+
# Validate data types
116+
if not all(isinstance(ori[field], (int, float)) for field in ['x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'nugget']):
117+
raise ValueError(f"Invalid data type in orientation {i}. All coordinates, gradients, and nugget must be numeric.")
118+
if not isinstance(ori['id'], int):
119+
raise ValueError(f"Invalid data type in orientation {i}. ID must be an integer.")
120+
if not isinstance(ori['polarity'], int) or ori['polarity'] not in {-1, 1}:
121+
raise ValueError(f"Invalid polarity in orientation {i}. Must be 1 (normal) or -1 (reverse).")
122+
123+
# Extract coordinates and other data
124+
x = np.array([ori['x'] for ori in orientations_data])
125+
y = np.array([ori['y'] for ori in orientations_data])
126+
z = np.array([ori['z'] for ori in orientations_data])
127+
G_x = np.array([ori['G_x'] for ori in orientations_data])
128+
G_y = np.array([ori['G_y'] for ori in orientations_data])
129+
G_z = np.array([ori['G_z'] for ori in orientations_data])
130+
ids = np.array([ori['id'] for ori in orientations_data])
131+
nugget = np.array([ori['nugget'] for ori in orientations_data])
132+
133+
# Apply polarity to gradients
134+
for i, ori in enumerate(orientations_data):
135+
if ori['polarity'] == -1:
136+
G_x[i] *= -1
137+
G_y[i] *= -1
138+
G_z[i] *= -1
139+
140+
# Create name_id_map from unique IDs
141+
unique_ids = np.unique(ids)
142+
name_id_map = {f"surface_{id}": id for id in unique_ids}
143+
144+
# Create OrientationsTable
145+
return OrientationsTable.from_arrays(
146+
x=x,
147+
y=y,
148+
z=z,
149+
G_x=G_x,
150+
G_y=G_y,
151+
G_z=G_z,
152+
names=[f"surface_{id}" for id in ids],
153+
nugget=nugget,
154+
name_id_map=name_id_map
155+
)
92156

93157
@staticmethod
94158
def save_model_to_json(model: GeoModel, file_path: str) -> None:
@@ -114,8 +178,8 @@ def _validate_json_schema(data: Dict[str, Any]) -> bool:
114178
bool: True if valid, False otherwise
115179
"""
116180
# Check required top-level keys
117-
required_keys = {'metadata', 'surface_points', 'orientations', 'faults',
118-
'series', 'grid_settings', 'interpolation_options'}
181+
required_keys = {'metadata', 'surface_points', 'orientations', 'series',
182+
'grid_settings', 'interpolation_options'}
119183
if not all(key in data for key in required_keys):
120184
return False
121185

@@ -132,4 +196,19 @@ def _validate_json_schema(data: Dict[str, Any]) -> bool:
132196
if not isinstance(sp['id'], int):
133197
return False
134198

199+
# Validate orientations
200+
if not isinstance(data['orientations'], list):
201+
return False
202+
203+
for ori in data['orientations']:
204+
required_ori_keys = {'x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'id', 'nugget', 'polarity'}
205+
if not all(key in ori for key in required_ori_keys):
206+
return False
207+
if not all(isinstance(ori[key], (int, float)) for key in ['x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'nugget']):
208+
return False
209+
if not isinstance(ori['id'], int):
210+
return False
211+
if not isinstance(ori['polarity'], int) or ori['polarity'] not in {-1, 1}:
212+
return False
213+
135214
return True

gempy/modules/json_io/schema.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This module defines the expected structure of JSON files for loading and saving GemPy models.
44
"""
55

6-
from typing import TypedDict, List, Dict, Any, Optional
6+
from typing import TypedDict, List, Dict, Any, Optional, Union, Sequence
77

88
class SurfacePoint(TypedDict):
99
x: float
@@ -16,23 +16,33 @@ class Orientation(TypedDict):
1616
x: float
1717
y: float
1818
z: float
19-
G_x: float
20-
G_y: float
21-
G_z: float
19+
G_x: float # X component of the gradient
20+
G_y: float # Y component of the gradient
21+
G_z: float # Z component of the gradient
2222
id: int
23-
polarity: int
23+
nugget: float
24+
polarity: int # 1 for normal, -1 for reverse
25+
26+
class Surface(TypedDict):
27+
name: str
28+
id: int
29+
color: Optional[str] # Hex color code
30+
vertices: Optional[List[List[float]]] # List of [x, y, z] coordinates
2431

2532
class Fault(TypedDict):
2633
name: str
2734
id: int
2835
is_active: bool
36+
surface: Surface
2937

3038
class Series(TypedDict):
3139
name: str
3240
id: int
3341
is_active: bool
3442
is_fault: bool
3543
order_series: int
44+
surfaces: List[Surface]
45+
faults: List[Fault]
3646

3747
class GridSettings(TypedDict):
3848
regular_grid_resolution: List[int]
@@ -49,7 +59,6 @@ class GemPyModelJson(TypedDict):
4959
metadata: ModelMetadata
5060
surface_points: List[SurfacePoint]
5161
orientations: List[Orientation]
52-
faults: List[Fault]
5362
series: List[Series]
5463
grid_settings: GridSettings
5564
interpolation_options: Dict[str, Any]

test/test_modules/test_json_io.py

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,41 @@ def sample_surface_points():
3535

3636

3737
@pytest.fixture
38-
def sample_json_data(sample_surface_points):
38+
def sample_orientations():
39+
"""Create sample orientation data for testing."""
40+
x = np.array([0.5, 1.5, 2.5, 3.5])
41+
y = np.array([0.5, 1.5, 2.5, 3.5])
42+
z = np.array([0.5, 1.5, 2.5, 3.5])
43+
G_x = np.array([0, 0, 0, 0])
44+
G_y = np.array([0, 0, 0, 0])
45+
G_z = np.array([1, 1, -1, 1]) # One reversed orientation
46+
ids = np.array([0, 1, 1, 2]) # Three different surfaces
47+
nugget = np.array([0.01, 0.01, 0.01, 0.01])
48+
49+
# Create name to id mapping
50+
name_id_map = {f"surface_{id}": id for id in np.unique(ids)}
51+
52+
# Create an OrientationsTable
53+
orientations = gp.data.OrientationsTable.from_arrays(
54+
x=x,
55+
y=y,
56+
z=z,
57+
G_x=G_x,
58+
G_y=G_y,
59+
G_z=G_z,
60+
names=[f"surface_{id}" for id in ids],
61+
nugget=nugget,
62+
name_id_map=name_id_map
63+
)
64+
65+
return orientations, x, y, z, G_x, G_y, G_z, ids, nugget, name_id_map
66+
67+
68+
@pytest.fixture
69+
def sample_json_data(sample_surface_points, sample_orientations):
3970
"""Create sample JSON data for testing."""
40-
_, x, y, z, ids, nugget, _ = sample_surface_points
71+
_, x_sp, y_sp, z_sp, ids_sp, nugget_sp, _ = sample_surface_points
72+
_, x_ori, y_ori, z_ori, G_x, G_y, G_z, ids_ori, nugget_ori, _ = sample_orientations
4173

4274
return {
4375
"metadata": {
@@ -48,16 +80,28 @@ def sample_json_data(sample_surface_points):
4880
},
4981
"surface_points": [
5082
{
51-
"x": float(x[i]),
52-
"y": float(y[i]),
53-
"z": float(z[i]),
54-
"id": int(ids[i]),
55-
"nugget": float(nugget[i])
83+
"x": float(x_sp[i]),
84+
"y": float(y_sp[i]),
85+
"z": float(z_sp[i]),
86+
"id": int(ids_sp[i]),
87+
"nugget": float(nugget_sp[i])
5688
}
57-
for i in range(len(x))
89+
for i in range(len(x_sp))
90+
],
91+
"orientations": [
92+
{
93+
"x": float(x_ori[i]),
94+
"y": float(y_ori[i]),
95+
"z": float(z_ori[i]),
96+
"G_x": float(G_x[i]),
97+
"G_y": float(G_y[i]),
98+
"G_z": float(G_z[i]),
99+
"id": int(ids_ori[i]),
100+
"nugget": float(nugget_ori[i]),
101+
"polarity": 1 # Always set to 1 since we're testing the raw G_z values
102+
}
103+
for i in range(len(x_ori))
58104
],
59-
"orientations": [],
60-
"faults": [],
61105
"series": [],
62106
"grid_settings": {
63107
"regular_grid_resolution": [10, 10, 10],
@@ -84,6 +128,25 @@ def test_surface_points_loading(sample_surface_points, sample_json_data):
84128
assert surface_points.name_id_map == loaded_surface_points.name_id_map, "Name to ID mappings don't match"
85129

86130

131+
def test_orientations_loading(sample_orientations, sample_json_data):
132+
"""Test loading orientations from JSON data."""
133+
orientations, _, _, _, _, _, _, _, _, name_id_map = sample_orientations
134+
135+
# Load orientations from JSON
136+
loaded_orientations = JsonIO._load_orientations(sample_json_data["orientations"])
137+
138+
# Verify all data matches
139+
assert np.allclose(orientations.xyz[:, 0], loaded_orientations.xyz[:, 0]), "X coordinates don't match"
140+
assert np.allclose(orientations.xyz[:, 1], loaded_orientations.xyz[:, 1]), "Y coordinates don't match"
141+
assert np.allclose(orientations.xyz[:, 2], loaded_orientations.xyz[:, 2]), "Z coordinates don't match"
142+
assert np.allclose(orientations.grads[:, 0], loaded_orientations.grads[:, 0]), "G_x values don't match"
143+
assert np.allclose(orientations.grads[:, 1], loaded_orientations.grads[:, 1]), "G_y values don't match"
144+
assert np.allclose(orientations.grads[:, 2], loaded_orientations.grads[:, 2]), "G_z values don't match"
145+
assert np.array_equal(orientations.ids, loaded_orientations.ids), "IDs don't match"
146+
assert np.allclose(orientations.nugget, loaded_orientations.nugget), "Nugget values don't match"
147+
assert orientations.name_id_map == loaded_orientations.name_id_map, "Name to ID mappings don't match"
148+
149+
87150
def test_surface_points_saving(tmp_path, sample_surface_points, sample_json_data):
88151
"""Test saving surface points to JSON file."""
89152
surface_points, _, _, _, _, _, _ = sample_surface_points
@@ -131,4 +194,63 @@ def test_missing_surface_points_data():
131194
]
132195

133196
with pytest.raises(ValueError):
134-
JsonIO._load_surface_points(invalid_data)
197+
JsonIO._load_surface_points(invalid_data)
198+
199+
200+
def test_invalid_orientations_data():
201+
"""Test handling of invalid orientation data."""
202+
invalid_data = [
203+
{
204+
"x": 1.0,
205+
"y": 1.0,
206+
"z": 1.0,
207+
"G_x": 0.0,
208+
"G_y": 0.0,
209+
"G_z": "invalid", # Should be float
210+
"id": 0,
211+
"nugget": 0.01,
212+
"polarity": 1
213+
}
214+
]
215+
216+
with pytest.raises(ValueError):
217+
JsonIO._load_orientations(invalid_data)
218+
219+
220+
def test_missing_orientations_data():
221+
"""Test handling of missing orientation data."""
222+
invalid_data = [
223+
{
224+
"x": 1.0,
225+
"y": 1.0,
226+
"z": 1.0,
227+
"G_x": 0.0,
228+
# Missing G_y and G_z
229+
"id": 0,
230+
"nugget": 0.01,
231+
"polarity": 1
232+
}
233+
]
234+
235+
with pytest.raises(ValueError):
236+
JsonIO._load_orientations(invalid_data)
237+
238+
239+
def test_invalid_orientation_polarity():
240+
"""Test handling of invalid orientation polarity."""
241+
invalid_data = [
242+
{
243+
"x": 1.0,
244+
"y": 1.0,
245+
"z": 1.0,
246+
"G_x": 0.0,
247+
"G_y": 0.0,
248+
"G_z": 1.0,
249+
"id": 0,
250+
"nugget": 0.01,
251+
"polarity": 2 # Should be 1 or -1
252+
}
253+
]
254+
255+
with pytest.raises(ValueError):
256+
JsonIO._load_orientations(invalid_data)

0 commit comments

Comments
 (0)