Skip to content

Commit 040d84a

Browse files
committed
Merge branch 'main' into fork/flohorovicic/feature/json_io
2 parents 9d9f304 + a002410 commit 040d84a

File tree

13 files changed

+354
-31
lines changed

13 files changed

+354
-31
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ Please include a summary of the changes.
44
Relates to <issue>
55

66
# Checklist
7-
- [ ] My code follows the [PEP 8 style guidelines](https://www.python.org/dev/peps/pep-0008/).
87
- [ ] My code uses type hinting for function and method arguments and return values.
9-
- [ ] My code contains descriptive and helpful docstrings
10-
which are formatted per the [Google Python Style Guidelines](http://google.github.io/styleguide/pyguide.html).
11-
- [ ] I have created tests which entirely cover my code.
8+
- [ ] I have created tests which cover my code.
129
- [ ] The test code either 1. demonstrates at least one valuable use case (e.g. integration tests)
1310
or 2. verifies that outputs are as expected for given inputs (e.g. unit tests).
14-
- [ ] New and existing tests pass locally with my changes.
11+
- [ ] New tests pass locally with my changes.
1512

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
[![GitHub Stars](https://img.shields.io/github/stars/cgre-aachen/gempy.svg)](https://github.com/cgre-aachen/gempy/stargazers)
44
[![GitHub Forks](https://img.shields.io/github/forks/cgre-aachen/gempy.svg)](https://github.com/cgre-aachen/gempy/network)
5+
[![Documentation Status](https://assets.readthedocs.org/static/projects/badges/passing-flat.svg)](http://docs.gempy.org)
6+
[![Build Status](http://terranigma-solutions.teamcity.com/app/rest/builds/buildType:(id:Gempy_TestingGemPy)/statusIcon)](http://terranigma-solutions.teamcity.com/viewType.html?buildTypeId=Gempy_TestingGemPy&guest=1)
57
[![PyPI](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)](https://www.python.org/downloads/)
68
[![PyPI](https://img.shields.io/badge/pypi-1.0-blue.svg)](https://pypi.org/project/gempy/)
79
[![license: EUPL v1.2](https://img.shields.io/badge/license-EUPL%20v1.2-blue.svg)](https://github.com/cgre-aachen/gempy/blob/master/LICENSE)
8-
[![Documentation Status](https://assets.readthedocs.org/static/projects/badges/passing-flat.svg)](http://docs.gempy.org)
910
[![DOI](https://zenodo.org/badge/96211155.svg)](https://zenodo.org/badge/latestdoi/96211155)
1011

1112

Lines changed: 181 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
11
"""
2-
Model 1 - Horizontal stratigraphy
3-
==================================
2+
Model 1 - Horizontal Stratigraphy
3+
===================================
44
5-
A simple example with horizontal layers
5+
This example demonstrates how to build a basic geological model with horizontally stacked layers using GemPy,
6+
an open-source Python library for implicit geological modeling. In this script, we will:
67
7-
This script demonstrates how to create a basic model with horizontally stacked layers using GemPy,
8-
a Python-based, open-source library for implicit geological modeling.
9-
"""
8+
- Set up and compute a simple geological model using input data (orientations and surface points).
9+
- Visualize the model in 2D and 3D.
10+
- Export model components (formations, orientations, surface points, and the regular grid) to VTK files.
11+
- Extract mesh vertices and normals, save them to Excel files, and convert the vertex data to an XYZ file.
1012
13+
Each section includes detailed comments to explain its purpose and functionality.
14+
"""
1115

1216
# %%
1317
# Import necessary libraries
1418
import gempy as gp
1519
import gempy_viewer as gpv
16-
20+
import pyvista as pv
21+
import vtk
22+
import pandas as pd
1723

1824
# sphinx_gallery_thumbnail_number = 2
1925

2026
# %%
21-
# Generate the model
22-
# Define the path to data
27+
# Generate the Geological Model
28+
# -----------------------------
29+
# In this section, we define the data source and create a GeoModel instance.
30+
# The model is defined with a specific spatial extent and resolution (refinement).
31+
# Input data (orientations and surface points) are loaded from a remote repository.
32+
33+
# Define the path to the dataset hosted online
2334
data_path = 'https://raw.githubusercontent.com/cgre-aachen/gempy_data/master/'
24-
# Create a GeoModel instance
35+
36+
# Create a GeoModel instance with the given project name, extent, and refinement level.
2537
data = gp.create_geomodel(
2638
project_name='horizontal',
2739
extent=[0, 1000, 0, 1000, 0, 1000],
@@ -31,19 +43,174 @@
3143
path_to_surface_points=data_path + "/data/input_data/jan_models/model1_surface_points.csv"
3244
)
3345
)
34-
# Map geological series to surfaces
46+
47+
# Map a geological series to the corresponding surfaces.
48+
# Here, the "Strat_Series" is associated with two formations ('rock2' and 'rock1'),
49+
# which defines the stacking order of horizontal layers.
3550
gp.map_stack_to_surfaces(
3651
gempy_model=data,
3752
mapping_object={"Strat_Series": ('rock2', 'rock1')}
3853
)
39-
# Compute the geological model
54+
55+
# Compute the geological model using the provided data and mappings.
4056
gp.compute_model(data)
4157
geo_data = data
4258

4359
# %%
44-
# Plot the initial geological model in the y direction without results
60+
# 2D Visualization of the Geological Model
61+
# -----------------------------------------
62+
# The following plots provide 2D views of the model.
63+
# - The first plot shows the initial model without computed results.
64+
# - The subsequent plots display the computed geological data in the x and y directions.
65+
# Note: The boundaries are hidden in the latter plots for a cleaner visualization.
66+
67+
# Plot the initial model in the y direction (without displaying computed results)
4568
gpv.plot_2d(geo_data, direction=['y'], show_results=False)
4669

47-
# Plot the result of the model in the x and y direction with data and without boundaries
70+
# Plot the computed model results in the x and y directions, including data but excluding boundaries
4871
gpv.plot_2d(geo_data, direction=['x'], show_data=True, show_boundaries=False)
4972
gpv.plot_2d(geo_data, direction=['y'], show_data=True, show_boundaries=False)
73+
74+
# %%
75+
# 3D Visualization and Export to VTK
76+
# ----------------------------------
77+
# Visualize the model in 3D with data, results, and boundaries.
78+
# The generated 3D plot is also used to export various components of the model to VTK files.
79+
p = gpv.plot_3d(geo_data, show_data=True, show_results=True, show_boundaries=True, image=True)
80+
81+
# Export different components of the model to VTK files:
82+
p.surface_poly['rock1'].save('rock1.vtk') # Save formation 'rock1'
83+
p.surface_poly['rock2'].save('rock2.vtk') # Save formation 'rock2'
84+
p.orientations_mesh.save('orientations.vtk') # Save the orientations mesh
85+
p.surface_points_mesh.save('surface_points.vtk') # Save the surface points mesh
86+
87+
# Retrieve and export the regular grid (volume representation) of the model
88+
box = p.regular_grid_actor.GetMapper().GetInput()
89+
box.save('box.vtk')
90+
91+
# %%
92+
# Display the Exported VTK Files with PyVista
93+
# -------------------------------------------
94+
# Load and plot each exported VTK file to verify the export.
95+
pv.read('rock1.vtk').plot(show_edges=False)
96+
pv.read('rock2.vtk').plot(show_edges=False)
97+
pv.read('orientations.vtk').plot(show_edges=False)
98+
pv.read('surface_points.vtk').plot(show_edges=False)
99+
pv.read('box.vtk').plot(show_edges=False)
100+
101+
102+
# %%
103+
# Extract and Save Mesh Vertices and Normals
104+
# ------------------------------------------
105+
# The following functions extract vertex coordinates and corresponding normal vectors
106+
# from a mesh (vtkPolyData), and then save this data into Excel files for further analysis.
107+
108+
def generate_normals(polydata):
109+
"""
110+
Generate normals for the given polydata if they are not already computed.
111+
112+
Parameters:
113+
polydata (vtk.vtkPolyData): Input polydata for which normals are to be computed.
114+
115+
Returns:
116+
vtk.vtkPolyData: The polydata with computed point normals.
117+
"""
118+
normal_generator = vtk.vtkPolyDataNormals()
119+
normal_generator.SetInputData(polydata)
120+
normal_generator.ComputePointNormalsOn()
121+
normal_generator.ComputeCellNormalsOff()
122+
normal_generator.Update()
123+
return normal_generator.GetOutput()
124+
125+
126+
def get_vertices_and_normals(mesh):
127+
"""
128+
Extract vertices and normals from a mesh.
129+
130+
Parameters:
131+
mesh: The mesh object (typically from a formation) to extract surface vertices and normals.
132+
133+
Returns:
134+
tuple: Two lists containing vertices and normals respectively.
135+
"""
136+
# Extract the surface of the mesh
137+
surface_mesh = mesh.extract_surface()
138+
polydata = surface_mesh
139+
140+
# Ensure normals are computed for the polydata
141+
polydata_with_normals = generate_normals(polydata)
142+
143+
# Extract points (vertices)
144+
points = polydata_with_normals.GetPoints()
145+
vertices = [points.GetPoint(i) for i in range(points.GetNumberOfPoints())]
146+
147+
# Extract normals associated with the points
148+
normals_array = polydata_with_normals.GetPointData().GetNormals()
149+
normals = [normals_array.GetTuple(i) for i in range(normals_array.GetNumberOfTuples())]
150+
151+
return vertices, normals
152+
153+
154+
def save_to_excel(vertices, normals, vertices_file, normals_file):
155+
"""
156+
Save the vertices and normals to separate Excel files.
157+
158+
Parameters:
159+
vertices (list): List of vertex coordinates.
160+
normals (list): List of normal vectors.
161+
vertices_file (str): File name for saving vertices.
162+
normals_file (str): File name for saving normals.
163+
"""
164+
# Create DataFrames from the lists
165+
vertices_df = pd.DataFrame(vertices, columns=['X', 'Y', 'Z'])
166+
normals_df = pd.DataFrame(normals, columns=['x', 'y', 'z'])
167+
168+
# Write the DataFrames to Excel files
169+
vertices_df.to_excel(vertices_file, index=False)
170+
normals_df.to_excel(normals_file, index=False)
171+
172+
173+
# Extract vertices and normals from the mesh of formation 'rock1'
174+
mesh = p.surface_poly['rock1']
175+
vertices, normals = get_vertices_and_normals(mesh)
176+
177+
# Create DataFrames for later processing or verification
178+
vertices_df = pd.DataFrame(vertices, columns=['X', 'Y', 'Z'])
179+
normals_df = pd.DataFrame(normals, columns=['x', 'y', 'z'])
180+
181+
# Define file names for the Excel outputs
182+
vertices_file = "rock1_vertices.xlsx"
183+
normals_file = "rock1_norms.xlsx"
184+
185+
# Save the extracted data to Excel files
186+
save_to_excel(vertices, normals, vertices_file, normals_file)
187+
188+
# Optionally, read back the Excel files to verify their content
189+
pd.read_excel(vertices_file)
190+
pd.read_excel(normals_file)
191+
192+
193+
# %%
194+
# Convert Vertices DataFrame to an XYZ File
195+
# -----------------------------------------
196+
# This function converts a DataFrame containing vertex coordinates to an XYZ format file,
197+
# which is a simple text file with one point per line.
198+
199+
def dataframe_to_xyz(df, filename):
200+
"""
201+
Write vertex coordinates from a DataFrame to an XYZ file.
202+
203+
Parameters:
204+
df (pandas.DataFrame): DataFrame containing 'X', 'Y', and 'Z' columns.
205+
filename (str): Output filename for the XYZ file.
206+
"""
207+
with open(filename, 'w') as f:
208+
for index, row in df.iterrows():
209+
f.write(f"{row['X']} {row['Y']} {row['Z']}\n")
210+
211+
212+
# Specify the output file name for the XYZ file
213+
filename = "output.xyz"
214+
215+
# Convert the vertices DataFrame to an XYZ file
216+
dataframe_to_xyz(vertices_df, filename)

examples/examples/real/Perth_basin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
geo_model: gp.data.GeoModel = gp.create_geomodel(
2626
project_name='Perth_Basin',
2727
extent=[337000, 400000, 6640000, 6710000, -18000, 1000],
28-
resolution=[100, 100, 100],
29-
refinement=4,
28+
refinement=6,
3029
importer_helper=gp.data.ImporterHelper(
3130
path_to_orientations=data_path + "/data/input_data/perth_basin/Paper_GU2F_sc_faults_topo_Foliations.csv",
3231
path_to_surface_points=data_path + "/data/input_data/perth_basin/Paper_GU2F_sc_faults_topo_Points.csv",

gempy/core/data/geo_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ def solutions(self) -> Solutions:
147147

148148
@solutions.setter
149149
def solutions(self, value):
150+
# * This is set from the gempy engine
151+
150152
self._solutions = value
151153

152154
# * Set solutions per group

gempy/core/data/grid.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,11 @@ def _update_values(self):
254254
if self.GridTypes.CENTERED in self.active_grids:
255255
if self.centered_grid is None: raise AttributeError('Centered grid is active but not defined')
256256
values.append(self.centered_grid.values)
257-
257+
258+
# make sure values is not empty
259+
if len(values) == 0:
260+
return self.values
261+
258262
self.values = np.concatenate(values)
259263

260264
return self.values

gempy/modules/mesh_extranction/__init__.py

Whitespace-only changes.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import numpy as np
2+
from typing import Optional
3+
from skimage import measure
4+
from gempy_engine.core.data.interp_output import InterpOutput
5+
from gempy_engine.core.data.raw_arrays_solution import RawArraysSolution
6+
7+
from gempy.core.data import GeoModel, StructuralElement, StructuralGroup
8+
from gempy.core.data.grid_modules import RegularGrid
9+
10+
11+
def set_meshes_with_marching_cubes(model: GeoModel) -> None:
12+
"""Extract meshes for all structural elements using the marching cubes algorithm.
13+
14+
Parameters
15+
----------
16+
model : GeoModel
17+
The geological model containing solutions and structural elements.
18+
19+
Raises
20+
------
21+
ValueError
22+
If the model solutions do not contain dense grid data.
23+
"""
24+
# Verify that solutions contain dense grid data
25+
if (model.solutions is None or
26+
model.solutions.block_solution_type != RawArraysSolution.BlockSolutionType.DENSE_GRID):
27+
raise ValueError("Model solutions must contain dense grid data for mesh extraction.")
28+
29+
regular_grid: RegularGrid = model.grid.regular_grid
30+
structural_groups: list[StructuralGroup] = model.structural_frame.structural_groups
31+
32+
if not model.solutions.octrees_output or not model.solutions.octrees_output[0].outputs_centers:
33+
raise ValueError("No interpolation outputs available for mesh extraction.")
34+
35+
output_lvl0: list[InterpOutput] = model.solutions.octrees_output[0].outputs_centers
36+
37+
for e, structural_group in enumerate(structural_groups):
38+
if e >= len(output_lvl0):
39+
continue
40+
41+
output_group: InterpOutput = output_lvl0[e]
42+
scalar_field_matrix = output_group.exported_fields_dense_grid.scalar_field
43+
44+
for element in structural_group.elements:
45+
extract_mesh_for_element(
46+
structural_element=element,
47+
regular_grid=regular_grid,
48+
scalar_field=scalar_field_matrix
49+
)
50+
51+
52+
def extract_mesh_for_element(structural_element: StructuralElement,
53+
regular_grid: RegularGrid,
54+
scalar_field: np.ndarray,
55+
mask: Optional[np.ndarray] = None) -> None:
56+
"""Extract a mesh for a single structural element using marching cubes.
57+
58+
Parameters
59+
----------
60+
structural_element : StructuralElement
61+
The structural element for which to extract a mesh.
62+
regular_grid : RegularGrid
63+
The regular grid defining the spatial discretization.
64+
scalar_field : np.ndarray
65+
The scalar field used for isosurface extraction.
66+
mask : np.ndarray, optional
67+
Optional mask to restrict the mesh extraction to specific regions.
68+
"""
69+
# Apply mask if provided
70+
volume = scalar_field.reshape(regular_grid.resolution)
71+
if mask is not None:
72+
volume = volume * mask
73+
74+
# Extract mesh using marching cubes
75+
verts, faces, _, _ = measure.marching_cubes(
76+
volume=volume,
77+
level=structural_element.scalar_field,
78+
spacing=(regular_grid.dx, regular_grid.dy, regular_grid.dz)
79+
)
80+
81+
# Adjust vertices to correct coordinates in the model's extent
82+
verts = (verts + [regular_grid.extent[0],
83+
regular_grid.extent[2],
84+
regular_grid.extent[4]])
85+
86+
# Store mesh in the structural element
87+
structural_element.vertices = verts
88+
structural_element.edges = faces

requirements/dev-requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
-r optional-requirements.txt
22

33
pytest
4-
subsurface~=2024.2.0
4+
pyvista
5+
git+https://github.com/terranigma-solutions/[email protected]
56

67
# Testing
78
pytest-approvaltests

0 commit comments

Comments
 (0)