Skip to content
Merged
37 changes: 24 additions & 13 deletions .github/workflows/tester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,49 @@ on:
paths:
- '**.py'
- .github/workflows/tester.yml

pull_request:
branches:
- master
paths:
- '**.py'
- .github/workflows/tester.yml
workflow_dispatch:

jobs:
continuous-integration:
name: Continuous integration ${{ matrix.os }} python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ${{ fromJSON(vars.BUILD_OS)}}
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}}
os: ${{ fromJSON(vars.BUILD_OS) }}
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}

steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3

- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
python-version: ${{ matrix.python }}
- name: Installing dependencies
shell: bash -l {0}
run: |
conda install -c conda-forge numpy scipy scikit-image scikit-learn pyvista pandas pytest networkx osqp matplotlib -y
- name: Building and install
shell: bash -l {0}
version: "latest"

- name: Set up Python ${{ matrix.matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install dependencies and library
run: |
pip install . --user
uv pip install --system \
numpy \
scipy \
scikit-image \
scikit-learn \
pytest \
networkx \
osqp \
matplotlib \
geoh5py \
-e .[tests]

- name: pytest
shell: bash -l {0}
run: |
pytest
25 changes: 23 additions & 2 deletions LoopStructural/datatypes/_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ class ValuePoints:
values: np.ndarray = field(default_factory=lambda: np.array([0]))
name: str = "unnamed"
properties: Optional[dict] = None

def __post_init__(self):

self.values = np.asarray(self.values)
self.locations = np.asarray(self.locations)
if self.locations.shape[1] != 3:
raise ValueError('locations must be of shape (n, 3)')
if len(self.values) != len(self.locations):
raise ValueError('values must be the same length as locations')
for k, v in (self.properties or {}).items():
if len(v) != len(self.locations):
raise ValueError(f'Property {k} must be the same length as locations')
self.properties[k] = np.asarray(v)
def to_dict(self):
return {
"locations": self.locations,
Expand Down Expand Up @@ -112,7 +123,17 @@ class VectorPoints:
vectors: np.ndarray = field(default_factory=lambda: np.array([[0, 0, 0]]))
name: str = "unnamed"
properties: Optional[dict] = None

def __post_init__(self):
self.vectors = np.asarray(self.vectors)
self.locations = np.asarray(self.locations)
if self.locations.shape[1] != 3:
raise ValueError('locations must be of shape (n, 3)')
if len(self.vectors) != len(self.locations):
raise ValueError('vectors must be the same length as locations')
for k, v in (self.properties or {}).items():
if len(v) != len(self.locations):
raise ValueError(f'Property {k} must be the same length as locations')
self.properties[k] = np.asarray(v)
def to_dict(self):
return {
"locations": self.locations,
Expand Down
82 changes: 79 additions & 3 deletions LoopStructural/export/geoh5.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import geoh5py
import geoh5py.workspace
import numpy as np
from LoopStructural.datatypes import ValuePoints, VectorPoints
import pandas as pd

from LoopStructural.datatypes import ValuePoints, VectorPoints

def add_surface_to_geoh5(filename, surface, overwrite=True, groupname="Loop"):
def add_group_to_geoh5(filename, groupname="Loop", parent=None, overwrite=True):
with geoh5py.workspace.Workspace(filename) as workspace:

group = workspace.get_entity(groupname)[0]
if group and overwrite:
group.allow_delete = True
workspace.remove_entity(group)
if not group:
group = geoh5py.groups.ContainerGroup.create(
workspace, name=groupname, allow_delete=True
)
if parent is not None:
parent = workspace.get_entity(parent)[0]
if parent:
parent.add_children(group)
return group.uid
def add_surface_to_geoh5(filename, surface, overwrite=True, group="Loop"):
with geoh5py.workspace.Workspace(filename) as workspace:
group = workspace.get_entity(group)[0]
if not group:
group = geoh5py.groups.ContainerGroup.create(
workspace, name=group, allow_delete=True
)
if surface.name in workspace.list_entities_name.values():
existing_surf = workspace.get_entity(surface.name)
existing_surf[0].allow_delete = True
Expand All @@ -32,6 +49,7 @@ def add_surface_to_geoh5(filename, surface, overwrite=True, groupname="Loop"):

def add_points_to_geoh5(filename, point, overwrite=True, groupname="Loop"):
with geoh5py.workspace.Workspace(filename) as workspace:

group = workspace.get_entity(groupname)[0]
if not group:
group = geoh5py.groups.ContainerGroup.create(
Expand All @@ -52,15 +70,73 @@ def add_points_to_geoh5(filename, point, overwrite=True, groupname="Loop"):
data['vz'] = {'association': "VERTEX", "values": point.vectors[:, 2]}

if isinstance(point, ValuePoints):
data['val'] = {'association': "VERTEX", "values": point.values}
data['values'] = {'association': "VERTEX", "values": point.values}
point = geoh5py.objects.Points.create(
workspace,
name=point.name,
vertices=point.locations,
parent=group,
)
point.add_data(data)

def overwrite_object(workspace, name, overwrite):
if name in workspace.list_entities_name.values():
existing_entity = workspace.get_entity(name)
existing_entity[0].allow_delete = True
if overwrite:
workspace.remove_entity(existing_entity[0])

def add_points_from_df(filename, df, name='pointset', overwrite=True, columns=None, groupname="Loop", x_col='X', y_col='Y', z_col='Z'):
"""
Add points to a geoh5 file from a pandas DataFrame. The DataFrame must have columns 'name', 'X', 'Y', 'Z' for the point locations.
Additional columns can be added as data associated with the points.
Parameters
----------
filename: str
Path to the geoh5 file.
df: pandas.DataFrame
DataFrame containing point data. Must have columns 'name', 'X', 'Y', 'Z'. Additional columns will be added as data.
overwrite: bool, optional
Whether to overwrite existing points with the same name. Default is True.
columns: list of str, optional
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.

"""
if columns is None:
columns = df.columns.tolist()
if x_col not in columns or y_col not in columns or z_col not in columns:
raise ValueError("DataFrame must contain 'name', 'X', 'Y', 'Z' columns. " \
"Specify the column names using x_col, y_col, z_col parameters if they are different.")
with geoh5py.workspace.Workspace(filename) as workspace:
if groupname:
group = workspace.get_entity(groupname)
group = group[0] if group else None
if not group:
group = geoh5py.groups.ContainerGroup.create(
workspace, name=groupname, allow_delete=True,
)

location = np.array(df[[x_col, y_col, z_col]].values) # shape (n,3)

overwrite_object(workspace, name, overwrite)


pts = geoh5py.objects.Points.create(
workspace,
name=name,
vertices=location,
parent=group,
)
data = {}
for col in columns:
if col in ['name', x_col, y_col, z_col]:
continue
data[col] = {"association": "VERTEX", "values": np.array(df[col]).flatten()}


if data:
pts.add_data(data)


def add_structured_grid_to_geoh5(filename, structured_grid, overwrite=True, groupname="Loop"):
with geoh5py.workspace.Workspace(filename) as workspace:
Expand Down
1 change: 1 addition & 0 deletions LoopStructural/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
plungeazimuth2vector,
azimuthplunge2vector,
normal_vector_to_strike_and_dip,
normal_vector_to_dip_and_dip_direction,
rotate,
)
from .helper import create_surface, create_box
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ visualisation = ["matplotlib", "pyvista", "loopstructuralvisualisation>=0.1.14"]
export = ["geoh5py", "pyevtk", "dill"]
jupyter = ["pyvista[all]"]
inequalities = ["loopsolver"]
tests = ['pytest','all']
docs = [
"pyvista[all]",
"pydata-sphinx-theme",
Expand Down
90 changes: 90 additions & 0 deletions tests/unit/io/test_geoh5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from LoopStructural.export.geoh5 import add_group_to_geoh5, add_points_to_geoh5, add_points_from_df
import geoh5py
import pytest
from pathlib import Path
from LoopStructural.datatypes import ValuePoints, VectorPoints
import numpy as np
@pytest.fixture
def tmp_path():
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def test_setup(tmp_path):
filename = tmp_path / "test.geoh5"
with geoh5py.workspace.Workspace.create(filename) as workspace:
yield filename
workspace.close()

def test_add_group_to_geoh5(test_setup):
filename = test_setup
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")

with geoh5py.workspace.Workspace(filename) as workspace:
assert workspace.get_entity(group_uid)[0].name == "TestGroup"

def test_add_points_to_geoh5(test_setup):
filename = test_setup
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
points = ValuePoints(
name="TestPoints",
locations=[[0, 0, 0], [1, 1, 1], [2, 2, 2]],
values=[10., 20, 30],
)
add_points_to_geoh5(filename, points, groupname=group_uid)
with geoh5py.workspace.Workspace(filename) as workspace:
point_entity = workspace.get_entity("TestPoints")[0]
assert point_entity.name == "TestPoints"
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
assert np.sum(point_entity.get_data("values")[0].values-np.array([10., 20., 30.])) == 0

def test_add_vector_points_to_geoh5(test_setup):
filename = test_setup
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
points = VectorPoints(
name="TestVectorPoints",
locations=[[0, 0, 0], [1, 1, 1], [2, 2, 2]],
vectors=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
)
add_points_to_geoh5(filename, points, groupname=group_uid)
with geoh5py.workspace.Workspace(filename) as workspace:
point_entity = workspace.get_entity("TestVectorPoints")[0]
assert point_entity.name == "TestVectorPoints"
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
assert np.sum(point_entity.get_data("vx")[0].values-np.array([1., 0., 0.])) == 0
assert np.sum(point_entity.get_data("vy")[0].values-np.array([0., 1., 0.])) == 0
assert np.sum(point_entity.get_data("vz")[0].values-np.array([0., 0., 1.])) == 0

def test_add_df_to_geoh5(test_setup):
import pandas as pd
filename = test_setup
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
df = pd.DataFrame({
'X': [0, 1, 2],
'Y': [0, 1, 2],
'Z': [0, 1, 2],
'value': [10., 20., 30.],
})
add_points_from_df(filename, df, name='df_points', groupname=group_uid)
with geoh5py.workspace.Workspace(filename) as workspace:
point_entity = workspace.get_entity("TestGroup")[0].children[0]
assert point_entity.name == "df_points"
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
assert np.sum(point_entity.get_data("value")[0].values-np.array([10., 20., 30.])) == 0

def test_add_df_with_alternate_xyz_to_geoh5(test_setup):
import pandas as pd
filename = test_setup
group_uid = add_group_to_geoh5(filename, groupname="TestGroup")
df = pd.DataFrame({
'EAST': [0, 1, 2],
'NORTH': [0, 1, 2],
'RL': [0, 1, 2],
'value': [10., 20., 30.],
})
add_points_from_df(filename, df, name='df_points',groupname=group_uid, x_col='EAST', y_col='NORTH', z_col='RL')
with geoh5py.workspace.Workspace(filename) as workspace:
point_entity = workspace.get_entity("TestGroup")[0].children[0]
assert point_entity.name == "df_points"
assert point_entity.vertices.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2]]
assert np.sum(point_entity.get_data("value")[0].values-np.array([10., 20., 30.])) == 0
Loading