Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b4548f2
feat: Add JSON I/O functionality for surface points
flohorovicic Mar 19, 2025
e27ca15
feat: Add orientation data loading functionality
flohorovicic Mar 19, 2025
cb34e62
feat: Add horizontal stratigraphic model tutorial
flohorovicic Mar 19, 2025
f91fa03
fix: Update JSON loading to use surface names from series data - Add …
flohorovicic Mar 19, 2025
9b78ac2
fix: Update horizontal stratigraphic tutorial with correct data and m…
flohorovicic Mar 19, 2025
49b4f25
fix: correct IDs and positions for fault and rock1 in multiple series…
flohorovicic Mar 19, 2025
1e7b405
Added .json input file
flohorovicic Mar 19, 2025
c926296
Updated .json input file
flohorovicic Mar 19, 2025
f82abf2
Adjustments in stack-mapping for more flexible handling of faults
flohorovicic Mar 19, 2025
0f1734b
Added modules __init__ and minor changes in json module
flohorovicic Mar 19, 2025
6cf9a44
fix: Fix metadata handling in JSON I/O for proper preservation when l…
flohorovicic Mar 22, 2025
4c1d177
Updated .gitignore (only to ignore files generated by new tutorial)
flohorovicic Mar 22, 2025
61b7dec
Extended functionality to save .json and adjusted tests. Simple model…
flohorovicic Mar 23, 2025
6a40125
Added structural relations to .json and fixed error in second example
flohorovicic Mar 23, 2025
2a7d8f8
Fixed problem with loading of surface layer stack
flohorovicic Mar 23, 2025
e6fade1
Fixed stratigraphic pile handling in JSON I/O by reverting to working…
flohorovicic Mar 23, 2025
660ae65
Included name-id mapping in .json
flohorovicic Mar 24, 2025
1637ddb
Fix JSON serialization for NumPy types and update example data
flohorovicic Mar 24, 2025
e31bda4
Adjusted date format
flohorovicic Mar 24, 2025
c92878b
Simplified required json input further and added "minimal working exa…
flohorovicic Mar 25, 2025
6d1e029
Simplified minimal input even further: now only points and orientatio…
flohorovicic Mar 25, 2025
84d3332
Updated minimal json examples and comparison to minimal GemPy model
flohorovicic Mar 25, 2025
0386163
Additional fixes to get defaults right
flohorovicic Mar 25, 2025
360a103
Added default nugget value to minimize input even further
flohorovicic Mar 25, 2025
36fec0d
Updated tests and fixed code to pass tests.
flohorovicic Mar 28, 2025
949f7e0
fix: Update fault model example with correct series mapping and visua…
flohorovicic Apr 5, 2025
601e523
Improve scalar field visualization in fault model example - Add prope…
flohorovicic Apr 6, 2025
94f6fd3
Example model for a combination of series and faults from json
flohorovicic Apr 6, 2025
593dbbd
Add combination model JSON files to gitignore
flohorovicic Apr 6, 2025
a4f4264
fix: preserve colors when loading models from JSON - Added color pres…
flohorovicic Apr 6, 2025
cb5693c
test: update JSON I/O tests to verify color preservation - Added colo…
flohorovicic Apr 6, 2025
0e95fb7
Added TODOs for PR.
javoha Apr 11, 2025
369ef46
Added TODOs for PR.
javoha Apr 11, 2025
20ad605
fix: ensure NotRequired import works for both Python 3.11+ and earlie…
flohorovicic Apr 27, 2025
9d9f304
[BUG] Ensure compatibility with older Python versions
Leguark May 1, 2025
040d84a
Merge branch 'main' into fork/flohorovicic/feature/json_io
Leguark May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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
  • Loading branch information
flohorovicic committed Mar 19, 2025
commit e27ca1512ba92296979323331570db368c9536a4
89 changes: 84 additions & 5 deletions gempy/modules/json_io/json_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from gempy.core.data.structural_frame import StructuralFrame
from gempy.core.data.grid import Grid
from gempy.core.data.geo_model import GeoModel
from .schema import SurfacePoint, GemPyModelJson
from .schema import SurfacePoint, Orientation, GemPyModelJson


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

# Load surface points
# Load surface points and orientations
surface_points = JsonIO._load_surface_points(data['surface_points'])
orientations = JsonIO._load_orientations(data['orientations'])

# TODO: Load other components
raise NotImplementedError("Only surface points loading is implemented")
raise NotImplementedError("Only surface points and orientations loading is implemented")

@staticmethod
def _load_surface_points(surface_points_data: List[SurfacePoint]) -> SurfacePointsTable:
Expand Down Expand Up @@ -89,6 +90,69 @@ def _load_surface_points(surface_points_data: List[SurfacePoint]) -> SurfacePoin
nugget=nugget,
name_id_map=name_id_map
)

@staticmethod
def _load_orientations(orientations_data: List[Orientation]) -> OrientationsTable:
"""
Load orientations from JSON data.

Args:
orientations_data (List[Orientation]): List of orientation dictionaries

Returns:
OrientationsTable: A new OrientationsTable instance

Raises:
ValueError: If the data is invalid or missing required fields
"""
# Validate data structure
required_fields = {'x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'id', 'nugget', 'polarity'}
for i, ori in enumerate(orientations_data):
missing_fields = required_fields - set(ori.keys())
if missing_fields:
raise ValueError(f"Missing required fields in orientation {i}: {missing_fields}")

# Validate data types
if not all(isinstance(ori[field], (int, float)) for field in ['x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'nugget']):
raise ValueError(f"Invalid data type in orientation {i}. All coordinates, gradients, and nugget must be numeric.")
if not isinstance(ori['id'], int):
raise ValueError(f"Invalid data type in orientation {i}. ID must be an integer.")
if not isinstance(ori['polarity'], int) or ori['polarity'] not in {-1, 1}:
raise ValueError(f"Invalid polarity in orientation {i}. Must be 1 (normal) or -1 (reverse).")

# Extract coordinates and other data
x = np.array([ori['x'] for ori in orientations_data])
y = np.array([ori['y'] for ori in orientations_data])
z = np.array([ori['z'] for ori in orientations_data])
G_x = np.array([ori['G_x'] for ori in orientations_data])
G_y = np.array([ori['G_y'] for ori in orientations_data])
G_z = np.array([ori['G_z'] for ori in orientations_data])
ids = np.array([ori['id'] for ori in orientations_data])
nugget = np.array([ori['nugget'] for ori in orientations_data])

# Apply polarity to gradients
for i, ori in enumerate(orientations_data):
if ori['polarity'] == -1:
G_x[i] *= -1
G_y[i] *= -1
G_z[i] *= -1

# Create name_id_map from unique IDs
unique_ids = np.unique(ids)
name_id_map = {f"surface_{id}": id for id in unique_ids}

# Create OrientationsTable
return OrientationsTable.from_arrays(
x=x,
y=y,
z=z,
G_x=G_x,
G_y=G_y,
G_z=G_z,
names=[f"surface_{id}" for id in ids],
nugget=nugget,
name_id_map=name_id_map
)

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

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

# Validate orientations
if not isinstance(data['orientations'], list):
return False

for ori in data['orientations']:
required_ori_keys = {'x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'id', 'nugget', 'polarity'}
if not all(key in ori for key in required_ori_keys):
return False
if not all(isinstance(ori[key], (int, float)) for key in ['x', 'y', 'z', 'G_x', 'G_y', 'G_z', 'nugget']):
return False
if not isinstance(ori['id'], int):
return False
if not isinstance(ori['polarity'], int) or ori['polarity'] not in {-1, 1}:
return False

return True
21 changes: 15 additions & 6 deletions gempy/modules/json_io/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This module defines the expected structure of JSON files for loading and saving GemPy models.
"""

from typing import TypedDict, List, Dict, Any, Optional
from typing import TypedDict, List, Dict, Any, Optional, Union, Sequence

class SurfacePoint(TypedDict):
x: float
Expand All @@ -16,23 +16,33 @@ class Orientation(TypedDict):
x: float
y: float
z: float
G_x: float
G_y: float
G_z: float
G_x: float # X component of the gradient
G_y: float # Y component of the gradient
G_z: float # Z component of the gradient
id: int
polarity: int
nugget: float
polarity: int # 1 for normal, -1 for reverse

class Surface(TypedDict):
name: str
id: int
color: Optional[str] # Hex color code
vertices: Optional[List[List[float]]] # List of [x, y, z] coordinates

class Fault(TypedDict):
name: str
id: int
is_active: bool
surface: Surface

class Series(TypedDict):
name: str
id: int
is_active: bool
is_fault: bool
order_series: int
surfaces: List[Surface]
faults: List[Fault]

class GridSettings(TypedDict):
regular_grid_resolution: List[int]
Expand All @@ -49,7 +59,6 @@ class GemPyModelJson(TypedDict):
metadata: ModelMetadata
surface_points: List[SurfacePoint]
orientations: List[Orientation]
faults: List[Fault]
series: List[Series]
grid_settings: GridSettings
interpolation_options: Dict[str, Any]
144 changes: 133 additions & 11 deletions test/test_modules/test_json_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,41 @@ def sample_surface_points():


@pytest.fixture
def sample_json_data(sample_surface_points):
def sample_orientations():
"""Create sample orientation data for testing."""
x = np.array([0.5, 1.5, 2.5, 3.5])
y = np.array([0.5, 1.5, 2.5, 3.5])
z = np.array([0.5, 1.5, 2.5, 3.5])
G_x = np.array([0, 0, 0, 0])
G_y = np.array([0, 0, 0, 0])
G_z = np.array([1, 1, -1, 1]) # One reversed orientation
ids = np.array([0, 1, 1, 2]) # Three different surfaces
nugget = np.array([0.01, 0.01, 0.01, 0.01])

# Create name to id mapping
name_id_map = {f"surface_{id}": id for id in np.unique(ids)}

# Create an OrientationsTable
orientations = gp.data.OrientationsTable.from_arrays(
x=x,
y=y,
z=z,
G_x=G_x,
G_y=G_y,
G_z=G_z,
names=[f"surface_{id}" for id in ids],
nugget=nugget,
name_id_map=name_id_map
)

return orientations, x, y, z, G_x, G_y, G_z, ids, nugget, name_id_map


@pytest.fixture
def sample_json_data(sample_surface_points, sample_orientations):
"""Create sample JSON data for testing."""
_, x, y, z, ids, nugget, _ = sample_surface_points
_, x_sp, y_sp, z_sp, ids_sp, nugget_sp, _ = sample_surface_points
_, x_ori, y_ori, z_ori, G_x, G_y, G_z, ids_ori, nugget_ori, _ = sample_orientations

return {
"metadata": {
Expand All @@ -48,16 +80,28 @@ def sample_json_data(sample_surface_points):
},
"surface_points": [
{
"x": float(x[i]),
"y": float(y[i]),
"z": float(z[i]),
"id": int(ids[i]),
"nugget": float(nugget[i])
"x": float(x_sp[i]),
"y": float(y_sp[i]),
"z": float(z_sp[i]),
"id": int(ids_sp[i]),
"nugget": float(nugget_sp[i])
}
for i in range(len(x))
for i in range(len(x_sp))
],
"orientations": [
{
"x": float(x_ori[i]),
"y": float(y_ori[i]),
"z": float(z_ori[i]),
"G_x": float(G_x[i]),
"G_y": float(G_y[i]),
"G_z": float(G_z[i]),
"id": int(ids_ori[i]),
"nugget": float(nugget_ori[i]),
"polarity": 1 # Always set to 1 since we're testing the raw G_z values
}
for i in range(len(x_ori))
],
"orientations": [],
"faults": [],
"series": [],
"grid_settings": {
"regular_grid_resolution": [10, 10, 10],
Expand All @@ -84,6 +128,25 @@ def test_surface_points_loading(sample_surface_points, sample_json_data):
assert surface_points.name_id_map == loaded_surface_points.name_id_map, "Name to ID mappings don't match"


def test_orientations_loading(sample_orientations, sample_json_data):
"""Test loading orientations from JSON data."""
orientations, _, _, _, _, _, _, _, _, name_id_map = sample_orientations

# Load orientations from JSON
loaded_orientations = JsonIO._load_orientations(sample_json_data["orientations"])

# Verify all data matches
assert np.allclose(orientations.xyz[:, 0], loaded_orientations.xyz[:, 0]), "X coordinates don't match"
assert np.allclose(orientations.xyz[:, 1], loaded_orientations.xyz[:, 1]), "Y coordinates don't match"
assert np.allclose(orientations.xyz[:, 2], loaded_orientations.xyz[:, 2]), "Z coordinates don't match"
assert np.allclose(orientations.grads[:, 0], loaded_orientations.grads[:, 0]), "G_x values don't match"
assert np.allclose(orientations.grads[:, 1], loaded_orientations.grads[:, 1]), "G_y values don't match"
assert np.allclose(orientations.grads[:, 2], loaded_orientations.grads[:, 2]), "G_z values don't match"
assert np.array_equal(orientations.ids, loaded_orientations.ids), "IDs don't match"
assert np.allclose(orientations.nugget, loaded_orientations.nugget), "Nugget values don't match"
assert orientations.name_id_map == loaded_orientations.name_id_map, "Name to ID mappings don't match"


def test_surface_points_saving(tmp_path, sample_surface_points, sample_json_data):
"""Test saving surface points to JSON file."""
surface_points, _, _, _, _, _, _ = sample_surface_points
Expand Down Expand Up @@ -131,4 +194,63 @@ def test_missing_surface_points_data():
]

with pytest.raises(ValueError):
JsonIO._load_surface_points(invalid_data)
JsonIO._load_surface_points(invalid_data)


def test_invalid_orientations_data():
"""Test handling of invalid orientation data."""
invalid_data = [
{
"x": 1.0,
"y": 1.0,
"z": 1.0,
"G_x": 0.0,
"G_y": 0.0,
"G_z": "invalid", # Should be float
"id": 0,
"nugget": 0.01,
"polarity": 1
}
]

with pytest.raises(ValueError):
JsonIO._load_orientations(invalid_data)


def test_missing_orientations_data():
"""Test handling of missing orientation data."""
invalid_data = [
{
"x": 1.0,
"y": 1.0,
"z": 1.0,
"G_x": 0.0,
# Missing G_y and G_z
"id": 0,
"nugget": 0.01,
"polarity": 1
}
]

with pytest.raises(ValueError):
JsonIO._load_orientations(invalid_data)


def test_invalid_orientation_polarity():
"""Test handling of invalid orientation polarity."""
invalid_data = [
{
"x": 1.0,
"y": 1.0,
"z": 1.0,
"G_x": 0.0,
"G_y": 0.0,
"G_z": 1.0,
"id": 0,
"nugget": 0.01,
"polarity": 2 # Should be 1 or -1
}
]

with pytest.raises(ValueError):
JsonIO._load_orientations(invalid_data)