Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions surface_apps-assets/uijson/surface_normals.ui.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"version": "0.1.0",
"title": "Compute normals",
"conda_environment": "surface_apps",
"run_command": "surface_apps.surface_normals.driver",
"geoh5": "",
"monitoring_directory": "",
"workspace_geoh5": "",
"conda_environment_boolean": false,
"surfaces": {
"group": "Data",
"main": true,
"meshType": "{f26feba3-aded-494b-b9e9-b2bbcbe298e1}",
"label": "Surfaces",
"value": "",
"multiSelect": true
},
"merge_points": {
"group": "Output",
"main": true,
"label": "Merge",
"value": true
},
"out_name": {
"group": "Output",
"main": true,
"label": "Name",
"value": "surface normals",
"tooltip": "Name of the output points object."
}
}
18 changes: 18 additions & 0 deletions surface_apps/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
from __future__ import annotations

import logging
import sys
import tempfile
from abc import abstractmethod
from json import load
from pathlib import Path

from geoapps_utils.driver.data import BaseData
Expand Down Expand Up @@ -99,6 +101,17 @@ def params(self, val: BaseData):
raise TypeError("Parameters must be a BaseData subclass.")
self._params = val

@classmethod
def start(cls, filepath: str | Path, driver_class=None, **kwargs):
with open(filepath, encoding="utf-8") as jsonfile:
uijson = load(jsonfile)

if driver_class is None:
module = __import__(uijson["run_command"], fromlist=["Driver"])
driver_class = module.Driver

super().start(filepath, driver_class=driver_class, **kwargs)

def add_ui_json(self, entity: ObjectBase | UIJsonGroup) -> None:
"""
Add ui.json file to entity.
Expand All @@ -111,3 +124,8 @@ def add_ui_json(self, entity: ObjectBase | UIJsonGroup) -> None:
self.params.write_ui_json(filepath)

entity.add_file(str(filepath))


if __name__ == "__main__":
file = Path(sys.argv[1]).resolve()
BaseSurfaceDriver.start(file)
11 changes: 5 additions & 6 deletions surface_apps/iso_surfaces/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import logging
import sys
from pathlib import Path

import numpy as np
from geoapps_utils.utils.formatters import string_name
Expand All @@ -27,14 +28,14 @@
logger = logging.getLogger(__name__)


class IsoSurfacesDriver(BaseSurfaceDriver):
class Driver(BaseSurfaceDriver):
"""
Driver for the detection of iso-surfaces within geoh5py objects.

:param parameters: Application parameters.
"""

_parameter_class = IsoSurfaceParameters
_params_class = IsoSurfaceParameters

def __init__(self, parameters: IsoSurfaceParameters | InputFile):
super().__init__(parameters)
Expand Down Expand Up @@ -109,7 +110,5 @@


if __name__ == "__main__":
file = sys.argv[1]
ifile = InputFile.read_ui_json(file)
driver = IsoSurfacesDriver(ifile)
driver.run()
file = Path(sys.argv[1]).resolve()
Driver.start(file)

Check warning on line 114 in surface_apps/iso_surfaces/driver.py

View check run for this annotation

Codecov / codecov/patch

surface_apps/iso_surfaces/driver.py#L113-L114

Added lines #L113 - L114 were not covered by tests
8 changes: 8 additions & 0 deletions surface_apps/surface_normals/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
# '
# This file is part of surface-apps package. '
# '
# surface-apps is distributed under the terms and conditions of the MIT License
# (see LICENSE file at the root of this source code package). '
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
77 changes: 77 additions & 0 deletions surface_apps/surface_normals/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
# '
# This file is part of surface-apps package. '
# '
# surface-apps is distributed under the terms and conditions of the MIT License
# (see LICENSE file at the root of this source code package). '
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

import logging
import sys
from pathlib import Path

from geoapps_utils.driver.driver import BaseDriver
from geoapps_utils.utils.transformations import (
compute_normals,
)
from geoh5py.groups.property_group_type import GroupTypeEnum
from geoh5py.shared.conversion.base import CellObjectConversion
from geoh5py.shared.merging.points import PointsMerger
from geoh5py.shared.utils import fetch_active_workspace

from surface_apps.surface_normals.options import SurfaceNormalsOptions


logger = logging.getLogger(__name__)


class Driver(BaseDriver):
"""
Driver for computing surface normals.

:param options: Application options
"""

_params_class = SurfaceNormalsOptions

def run(self):
with fetch_active_workspace(self.params.geoh5, mode="r+") as geoh5:
points = []
for surface in self.params.surfaces:
normals = compute_normals(surface)
pts = CellObjectConversion.to_points(
surface, name=f"{surface.name} {self.params.out_name}"
)
pts.add_data(
{
"Nx": {"values": normals[:, 0], "association": "VERTEX"},
"Ny": {"values": normals[:, 1], "association": "VERTEX"},
"Nz": {"values": normals[:, 2], "association": "VERTEX"},
}
)
points.append(pts)

merge_points = self.params.merge_points and len(points) > 1
if merge_points:
points = [

Check warning on line 57 in surface_apps/surface_normals/driver.py

View check run for this annotation

Codecov / codecov/patch

surface_apps/surface_normals/driver.py#L57

Added line #L57 was not covered by tests
PointsMerger.merge_objects(
geoh5,
points, # type: ignore
add_data=True,
name=f"merged {self.params.out_name}",
)
]

for pts in points:
pts.create_property_group(
properties=[pts.get_data(k)[0] for k in ["Nx", "Ny", "Nz"]],
name="Normals",
property_group_type=GroupTypeEnum.VECTOR,
)
self.update_monitoring_directory(pts)


if __name__ == "__main__":
file = Path(sys.argv[1]).resolve()
driver = Driver.start(file)

Check warning on line 77 in surface_apps/surface_normals/driver.py

View check run for this annotation

Codecov / codecov/patch

surface_apps/surface_normals/driver.py#L76-L77

Added lines #L76 - L77 were not covered by tests
38 changes: 38 additions & 0 deletions surface_apps/surface_normals/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
# '
# This file is part of surface-apps package. '
# '
# surface-apps is distributed under the terms and conditions of the MIT License
# (see LICENSE file at the root of this source code package). '
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

from pathlib import Path
from typing import ClassVar

from geoapps_utils.driver.data import BaseData
from geoh5py.objects import Surface
from pydantic import ConfigDict

from surface_apps import assets_path


class SurfaceNormalsOptions(BaseData):
"""
Options for surface normals.

:param surface: Triangulation on which normals will be computed
at triangle centers.
"""

model_config = ConfigDict(arbitrary_types_allowed=True)

name: ClassVar[str] = "surface_normals"
default_ui_json: ClassVar[Path] = assets_path() / "uijson/surface_normals.ui.json"
title: ClassVar[str] = "Surface Normals"
run_command: ClassVar[str] = "surface_apps.surface_normals.driver"

conda_environment: str = "surface_apps"
surfaces: list[Surface]
merge_points: bool = True
out_name: str = "surface normals"
2 changes: 1 addition & 1 deletion tests/run_tests/iso_surfaces_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from geoh5py.objects import BlockModel, Points, Surface
from geoh5py.workspace import Workspace

from surface_apps.iso_surfaces.driver import IsoSurfacesDriver
from surface_apps.iso_surfaces.driver import Driver as IsoSurfacesDriver


# pylint: disable=too-many-locals
Expand Down
79 changes: 79 additions & 0 deletions tests/run_tests/surface_normals_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
# Copyright (c) 2025 Mira Geoscience Ltd. '
# '
# This file is part of surface-apps package. '
# '
# surface-apps is distributed under the terms and conditions of the MIT License
# (see LICENSE file at the root of this source code package). '
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

import numpy as np
from geoh5py import Workspace
from geoh5py.objects import Surface

from surface_apps.surface_normals.driver import Driver as SurfaceNormalsDriver
from surface_apps.surface_normals.options import SurfaceNormalsOptions


def create_surface(workspace: Workspace):
h = np.sqrt(2)
alpha = np.deg2rad(30)
vertices = np.array(
[
[-h * np.cos(alpha), -h * np.sin(alpha), 0],
[h * np.sin(alpha), -h * np.cos(alpha), 0],
[h * np.cos(alpha), h * np.sin(alpha), 0],
[-h * np.sin(alpha), h * np.cos(alpha), 0],
[0, 0, -1],
[0, 0, 1],
]
)
cells = np.array(
[
[1, 2, 5],
[0, 1, 5],
[3, 0, 5],
[2, 3, 5],
[1, 4, 2],
[0, 4, 1],
[3, 4, 0],
[2, 4, 3],
]
)

surf = Surface.create(workspace, name="diamond", vertices=vertices, cells=cells)
return surf


def test_surface_normals(tmp_path):
with Workspace(tmp_path / "test.geoh5") as ws:
surf = create_surface(ws)

opts = SurfaceNormalsOptions(geoh5=ws, surfaces=[surf])
opts.write_ui_json(tmp_path / "test.ui.json")
normals = SurfaceNormalsDriver.start(tmp_path / "test.ui.json")

with Workspace(tmp_path / "test.geoh5") as ws:
pts = ws.get_entity("diamond surface normals")[0]
pg = pts.fetch_property_group("Normals")

normals = np.column_stack([ws.get_entity(k)[0].values for k in pg.properties])
h = np.sqrt(2)
beta = np.deg2rad(15)
validation = (
np.array(
[
[h * np.cos(beta), -h * np.sin(beta), h],
[-h * np.sin(beta), -h * np.cos(beta), h],
[-h * np.cos(beta), h * np.sin(beta), h],
[h * np.sin(beta), h * np.cos(beta), h],
[h * np.cos(beta), -h * np.sin(beta), -h],
[-h * np.sin(beta), -h * np.cos(beta), -h],
[-h * np.cos(beta), h * np.sin(beta), -h],
[h * np.sin(beta), h * np.cos(beta), -h],
]
)
/ 2
)

assert np.allclose(normals, validation)
1 change: 1 addition & 0 deletions tests/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# (see LICENSE file at the root of this source code package). '
# ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''


import numpy as np
from geoh5py import Workspace
from geoh5py.objects import Curve, Points
Expand Down