Skip to content

Commit c621c0b

Browse files
authored
Merge pull request #276 from Loop3D/laurent
Adding the function to add dataframe as pts with nesting in geohy5.py
2 parents 228ca78 + 3054c56 commit c621c0b

6 files changed

Lines changed: 218 additions & 18 deletions

File tree

.github/workflows/tester.yml

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,49 @@ on:
77
paths:
88
- '**.py'
99
- .github/workflows/tester.yml
10-
1110
pull_request:
1211
branches:
1312
- master
1413
paths:
1514
- '**.py'
1615
- .github/workflows/tester.yml
1716
workflow_dispatch:
17+
1818
jobs:
1919
continuous-integration:
2020
name: Continuous integration ${{ matrix.os }} python ${{ matrix.python-version }}
2121
runs-on: ${{ matrix.os }}
2222
strategy:
2323
fail-fast: false
2424
matrix:
25-
os: ${{ fromJSON(vars.BUILD_OS)}}
26-
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}}
25+
os: ${{ fromJSON(vars.BUILD_OS) }}
26+
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
27+
2728
steps:
2829
- uses: actions/checkout@v4
29-
- uses: conda-incubator/setup-miniconda@v3
3030

31+
- name: Set up uv
32+
uses: astral-sh/setup-uv@v3
3133
with:
32-
python-version: ${{ matrix.python }}
33-
- name: Installing dependencies
34-
shell: bash -l {0}
35-
run: |
36-
conda install -c conda-forge numpy scipy scikit-image scikit-learn pyvista pandas pytest networkx osqp matplotlib -y
37-
- name: Building and install
38-
shell: bash -l {0}
34+
version: "latest"
35+
36+
- name: Set up Python ${{ matrix.matrix.python-version }}
37+
run: uv python install ${{ matrix.python-version }}
38+
39+
- name: Install dependencies and library
3940
run: |
40-
pip install . --user
41+
uv pip install --system \
42+
numpy \
43+
scipy \
44+
scikit-image \
45+
scikit-learn \
46+
pytest \
47+
networkx \
48+
osqp \
49+
matplotlib \
50+
geoh5py \
51+
-e .[tests]
52+
4153
- name: pytest
42-
shell: bash -l {0}
4354
run: |
4455
pytest

LoopStructural/datatypes/_point.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,18 @@ class ValuePoints:
1414
values: np.ndarray = field(default_factory=lambda: np.array([0]))
1515
name: str = "unnamed"
1616
properties: Optional[dict] = None
17-
17+
def __post_init__(self):
18+
19+
self.values = np.asarray(self.values)
20+
self.locations = np.asarray(self.locations)
21+
if self.locations.shape[1] != 3:
22+
raise ValueError('locations must be of shape (n, 3)')
23+
if len(self.values) != len(self.locations):
24+
raise ValueError('values must be the same length as locations')
25+
for k, v in (self.properties or {}).items():
26+
if len(v) != len(self.locations):
27+
raise ValueError(f'Property {k} must be the same length as locations')
28+
self.properties[k] = np.asarray(v)
1829
def to_dict(self):
1930
return {
2031
"locations": self.locations,
@@ -112,7 +123,17 @@ class VectorPoints:
112123
vectors: np.ndarray = field(default_factory=lambda: np.array([[0, 0, 0]]))
113124
name: str = "unnamed"
114125
properties: Optional[dict] = None
115-
126+
def __post_init__(self):
127+
self.vectors = np.asarray(self.vectors)
128+
self.locations = np.asarray(self.locations)
129+
if self.locations.shape[1] != 3:
130+
raise ValueError('locations must be of shape (n, 3)')
131+
if len(self.vectors) != len(self.locations):
132+
raise ValueError('vectors must be the same length as locations')
133+
for k, v in (self.properties or {}).items():
134+
if len(v) != len(self.locations):
135+
raise ValueError(f'Property {k} must be the same length as locations')
136+
self.properties[k] = np.asarray(v)
116137
def to_dict(self):
117138
return {
118139
"locations": self.locations,

LoopStructural/export/geoh5.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
import geoh5py
22
import geoh5py.workspace
33
import numpy as np
4-
from LoopStructural.datatypes import ValuePoints, VectorPoints
4+
import pandas as pd
55

6+
from LoopStructural.datatypes import ValuePoints, VectorPoints
67

7-
def add_surface_to_geoh5(filename, surface, overwrite=True, groupname="Loop"):
8+
def add_group_to_geoh5(filename, groupname="Loop", parent=None, overwrite=True):
89
with geoh5py.workspace.Workspace(filename) as workspace:
10+
911
group = workspace.get_entity(groupname)[0]
12+
if group and overwrite:
13+
group.allow_delete = True
14+
workspace.remove_entity(group)
1015
if not group:
1116
group = geoh5py.groups.ContainerGroup.create(
1217
workspace, name=groupname, allow_delete=True
1318
)
19+
if parent is not None:
20+
parent = workspace.get_entity(parent)[0]
21+
if parent:
22+
parent.add_children(group)
23+
return group.uid
24+
def add_surface_to_geoh5(filename, surface, overwrite=True, group="Loop"):
25+
with geoh5py.workspace.Workspace(filename) as workspace:
26+
group = workspace.get_entity(group)[0]
27+
if not group:
28+
group = geoh5py.groups.ContainerGroup.create(
29+
workspace, name=group, allow_delete=True
30+
)
1431
if surface.name in workspace.list_entities_name.values():
1532
existing_surf = workspace.get_entity(surface.name)
1633
existing_surf[0].allow_delete = True
@@ -32,6 +49,7 @@ def add_surface_to_geoh5(filename, surface, overwrite=True, groupname="Loop"):
3249

3350
def add_points_to_geoh5(filename, point, overwrite=True, groupname="Loop"):
3451
with geoh5py.workspace.Workspace(filename) as workspace:
52+
3553
group = workspace.get_entity(groupname)[0]
3654
if not group:
3755
group = geoh5py.groups.ContainerGroup.create(
@@ -52,15 +70,73 @@ def add_points_to_geoh5(filename, point, overwrite=True, groupname="Loop"):
5270
data['vz'] = {'association': "VERTEX", "values": point.vectors[:, 2]}
5371

5472
if isinstance(point, ValuePoints):
55-
data['val'] = {'association': "VERTEX", "values": point.values}
73+
data['values'] = {'association': "VERTEX", "values": point.values}
5674
point = geoh5py.objects.Points.create(
5775
workspace,
5876
name=point.name,
5977
vertices=point.locations,
6078
parent=group,
6179
)
6280
point.add_data(data)
81+
82+
def overwrite_object(workspace, name, overwrite):
83+
if name in workspace.list_entities_name.values():
84+
existing_entity = workspace.get_entity(name)
85+
existing_entity[0].allow_delete = True
86+
if overwrite:
87+
workspace.remove_entity(existing_entity[0])
6388

89+
def add_points_from_df(filename, df, name='pointset', overwrite=True, columns=None, groupname="Loop", x_col='X', y_col='Y', z_col='Z'):
90+
"""
91+
Add points to a geoh5 file from a pandas DataFrame. The DataFrame must have columns 'name', 'X', 'Y', 'Z' for the point locations.
92+
Additional columns can be added as data associated with the points.
93+
Parameters
94+
----------
95+
filename: str
96+
Path to the geoh5 file.
97+
df: pandas.DataFrame
98+
DataFrame containing point data. Must have columns 'name', 'X', 'Y', 'Z'. Additional columns will be added as data.
99+
overwrite: bool, optional
100+
Whether to overwrite existing points with the same name. Default is True.
101+
columns: list of str, optional
102+
List of columns in the DataFrame to add as data. If None, all columns except 'name', 'X', 'Y', 'Z' will be added. Default is None.
103+
104+
"""
105+
if columns is None:
106+
columns = df.columns.tolist()
107+
if x_col not in columns or y_col not in columns or z_col not in columns:
108+
raise ValueError("DataFrame must contain 'name', 'X', 'Y', 'Z' columns. " \
109+
"Specify the column names using x_col, y_col, z_col parameters if they are different.")
110+
with geoh5py.workspace.Workspace(filename) as workspace:
111+
if groupname:
112+
group = workspace.get_entity(groupname)
113+
group = group[0] if group else None
114+
if not group:
115+
group = geoh5py.groups.ContainerGroup.create(
116+
workspace, name=groupname, allow_delete=True,
117+
)
118+
119+
location = np.array(df[[x_col, y_col, z_col]].values) # shape (n,3)
120+
121+
overwrite_object(workspace, name, overwrite)
122+
123+
124+
pts = geoh5py.objects.Points.create(
125+
workspace,
126+
name=name,
127+
vertices=location,
128+
parent=group,
129+
)
130+
data = {}
131+
for col in columns:
132+
if col in ['name', x_col, y_col, z_col]:
133+
continue
134+
data[col] = {"association": "VERTEX", "values": np.array(df[col]).flatten()}
135+
136+
137+
if data:
138+
pts.add_data(data)
139+
64140

65141
def add_structured_grid_to_geoh5(filename, structured_grid, overwrite=True, groupname="Loop"):
66142
with geoh5py.workspace.Workspace(filename) as workspace:

LoopStructural/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
plungeazimuth2vector,
2727
azimuthplunge2vector,
2828
normal_vector_to_strike_and_dip,
29+
normal_vector_to_dip_and_dip_direction,
2930
rotate,
3031
)
3132
from .helper import create_surface, create_box

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ visualisation = ["matplotlib", "pyvista", "loopstructuralvisualisation>=0.1.14"]
4646
export = ["geoh5py", "pyevtk", "dill"]
4747
jupyter = ["pyvista[all]"]
4848
inequalities = ["loopsolver"]
49+
tests = ['pytest','all']
4950
docs = [
5051
"pyvista[all]",
5152
"pydata-sphinx-theme",

tests/unit/io/test_geoh5.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from LoopStructural.export.geoh5 import add_group_to_geoh5, add_points_to_geoh5, add_points_from_df
2+
import geoh5py
3+
import pytest
4+
from pathlib import Path
5+
from LoopStructural.datatypes import ValuePoints, VectorPoints
6+
import numpy as np
7+
@pytest.fixture
8+
def tmp_path():
9+
import tempfile
10+
with tempfile.TemporaryDirectory() as tmpdir:
11+
yield Path(tmpdir)
12+
@pytest.fixture
13+
def test_setup(tmp_path):
14+
filename = tmp_path / "test.geoh5"
15+
with geoh5py.workspace.Workspace.create(filename) as workspace:
16+
yield filename
17+
workspace.close()
18+
19+
def test_add_group_to_geoh5(test_setup):
20+
filename = test_setup
21+
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
22+
23+
with geoh5py.workspace.Workspace(filename) as workspace:
24+
assert workspace.get_entity(group_uid)[0].name == "TestGroup"
25+
26+
def test_add_points_to_geoh5(test_setup):
27+
filename = test_setup
28+
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
29+
points = ValuePoints(
30+
name="TestPoints",
31+
locations=[[0, 0, 0], [1, 1, 1], [2, 2, 2]],
32+
values=[10., 20, 30],
33+
)
34+
add_points_to_geoh5(filename, points, groupname=group_uid)
35+
with geoh5py.workspace.Workspace(filename) as workspace:
36+
point_entity = workspace.get_entity("TestPoints")[0]
37+
assert point_entity.name == "TestPoints"
38+
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
39+
assert np.sum(point_entity.get_data("values")[0].values-np.array([10., 20., 30.])) == 0
40+
41+
def test_add_vector_points_to_geoh5(test_setup):
42+
filename = test_setup
43+
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
44+
points = VectorPoints(
45+
name="TestVectorPoints",
46+
locations=[[0, 0, 0], [1, 1, 1], [2, 2, 2]],
47+
vectors=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
48+
)
49+
add_points_to_geoh5(filename, points, groupname=group_uid)
50+
with geoh5py.workspace.Workspace(filename) as workspace:
51+
point_entity = workspace.get_entity("TestVectorPoints")[0]
52+
assert point_entity.name == "TestVectorPoints"
53+
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
54+
assert np.sum(point_entity.get_data("vx")[0].values-np.array([1., 0., 0.])) == 0
55+
assert np.sum(point_entity.get_data("vy")[0].values-np.array([0., 1., 0.])) == 0
56+
assert np.sum(point_entity.get_data("vz")[0].values-np.array([0., 0., 1.])) == 0
57+
58+
def test_add_df_to_geoh5(test_setup):
59+
import pandas as pd
60+
filename = test_setup
61+
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
62+
df = pd.DataFrame({
63+
'X': [0, 1, 2],
64+
'Y': [0, 1, 2],
65+
'Z': [0, 1, 2],
66+
'value': [10., 20., 30.],
67+
})
68+
add_points_from_df(filename, df, name='df_points', groupname=group_uid)
69+
with geoh5py.workspace.Workspace(filename) as workspace:
70+
point_entity = workspace.get_entity("TestGroup")[0].children[0]
71+
assert point_entity.name == "df_points"
72+
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
73+
assert np.sum(point_entity.get_data("value")[0].values-np.array([10., 20., 30.])) == 0
74+
75+
def test_add_df_with_alternate_xyz_to_geoh5(test_setup):
76+
import pandas as pd
77+
filename = test_setup
78+
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
79+
df = pd.DataFrame({
80+
'EAST': [0, 1, 2],
81+
'NORTH': [0, 1, 2],
82+
'RL': [0, 1, 2],
83+
'value': [10., 20., 30.],
84+
})
85+
add_points_from_df(filename, df, name='df_points',groupname=group_uid, x_col='EAST', y_col='NORTH', z_col='RL')
86+
with geoh5py.workspace.Workspace(filename) as workspace:
87+
point_entity = workspace.get_entity("TestGroup")[0].children[0]
88+
assert point_entity.name == "df_points"
89+
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
90+
assert np.sum(point_entity.get_data("value")[0].values-np.array([10., 20., 30.])) == 0

0 commit comments

Comments
 (0)