From 13725d0e691a7d83b96615aff61c86ef66efd336 Mon Sep 17 00:00:00 2001 From: benjamink Date: Fri, 21 Jun 2024 11:58:07 -0700 Subject: [PATCH] clean out iso-surfaces driver/params/constants --- geoapps/iso_surfaces/__init__.py | 3 - geoapps/iso_surfaces/constants.py | 132 ---------------- geoapps/iso_surfaces/driver.py | 212 -------------------------- geoapps/iso_surfaces/params.py | 140 ----------------- geoapps/utils/write_default_uijson.py | 7 - tests/iso_surfaces_test.py | 155 ------------------- 6 files changed, 649 deletions(-) delete mode 100644 geoapps/iso_surfaces/constants.py delete mode 100644 geoapps/iso_surfaces/driver.py delete mode 100644 geoapps/iso_surfaces/params.py delete mode 100644 tests/iso_surfaces_test.py diff --git a/geoapps/iso_surfaces/__init__.py b/geoapps/iso_surfaces/__init__.py index f3a17abf2..ad575ec1c 100644 --- a/geoapps/iso_surfaces/__init__.py +++ b/geoapps/iso_surfaces/__init__.py @@ -10,6 +10,3 @@ # flake8: noqa from __future__ import annotations - -from .constants import app_initializer -from .params import IsoSurfacesParams diff --git a/geoapps/iso_surfaces/constants.py b/geoapps/iso_surfaces/constants.py deleted file mode 100644 index ae8731ed8..000000000 --- a/geoapps/iso_surfaces/constants.py +++ /dev/null @@ -1,132 +0,0 @@ -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of geoapps. ' -# ' -# geoapps is distributed under the terms and conditions of the MIT License ' -# (see LICENSE file at the root of this source code package). ' -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -from __future__ import annotations - -from copy import deepcopy -from uuid import UUID - -from geoh5py.ui_json.constants import default_ui_json as base_ui_json - -import geoapps -from geoapps import assets_path - -defaults = { - "version": geoapps.__version__, - "title": "Create Iso Surfaces", - "geoh5": None, - "objects": None, - "data": None, - "interval_min": 0.005, - "interval_max": 0.02, - "interval_spacing": 0.005, - "fixed_contours": None, - "max_distance": 500.0, - "resolution": 50.0, - "generate_sweep": False, - "run_command": "geoapps.iso_surfaces.driver", - "monitoring_directory": None, - "workspace_geoh5": None, - "conda_environment": "geoapps", - "conda_environment_boolean": False, -} - -default_ui_json = deepcopy(base_ui_json) -default_ui_json.update( - { - "version": geoapps.__version__, - "title": "Create Iso Surfaces", - "geoh5": "", - "run_command": "geoapps.iso_surfaces.driver", - "monitoring_directory": "", - "conda_environment": "geoapps", - "conda_environment_boolean": False, - "objects": { - "meshType": [ - "{2e814779-c35f-4da0-ad6a-39a6912361f9}", - "{202C5DB1-A56D-4004-9CAD-BAAFD8899406}", - "{6A057FDC-B355-11E3-95BE-FD84A7FFCB88}", - "{F26FEBA3-ADED-494B-B9E9-B2BBCBE298E1}", - "{4EA87376-3ECE-438B-BF12-3479733DED46}", - ], - "main": True, - "group": "Data Selection", - "label": "Object", - "value": None, - }, - "data": { - "main": True, - "group": "Data Selection", - "association": ["Vertex", "Cell"], - "dataType": "Float", - "label": "Value fields", - "parent": "objects", - "value": None, - }, - "interval_min": { - "main": True, - "group": "Interval Contours", - "groupOptional": True, - "label": "Interval min", - "value": 0.0, - }, - "interval_max": { - "main": True, - "group": "Interval Contours", - "label": "Interval max", - "value": 0.0, - }, - "interval_spacing": { - "main": True, - "group": "Interval Contours", - "label": "Interval spacing", - "value": 0.0, - }, - "fixed_contours": { - "main": True, - "label": "Fixed Contours", - "value": "", - "optional": True, - "enabled": False, - }, - "max_distance": { - "enabled": True, - "label": "Max Interpolation Distance (m)", - "main": True, - "value": 500.0, - }, - "resolution": { - "enabled": True, - "label": "Base grid resolution (m)", - "main": True, - "value": 50.0, - }, - "generate_sweep": { - "label": "Generate sweep file", - "group": "Python run preferences", - "main": True, - "value": False, - }, - "export_as": {"main": True, "label": "Name", "value": "Iso_"}, - } -) - -validations = {} - -app_initializer = { - "geoh5": str(assets_path() / "FlinFlon.geoh5"), - "objects": UUID("{2e814779-c35f-4da0-ad6a-39a6912361f9}"), - "data": UUID("{f3e36334-be0a-4210-b13e-06933279de25}"), - "max_distance": 500.0, - "resolution": 50.0, - "interval_min": 0.005, - "interval_max": 0.02, - "interval_spacing": 0.005, - "export_as": "Iso_Iteration_7_model", -} diff --git a/geoapps/iso_surfaces/driver.py b/geoapps/iso_surfaces/driver.py deleted file mode 100644 index 2f6983580..000000000 --- a/geoapps/iso_surfaces/driver.py +++ /dev/null @@ -1,212 +0,0 @@ -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of geoapps. ' -# ' -# geoapps is distributed under the terms and conditions of the MIT License ' -# (see LICENSE file at the root of this source code package). ' -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -from __future__ import annotations - -import sys -import warnings - -import numpy as np -from geoapps_utils.driver.driver import BaseDriver -from geoh5py.groups import ContainerGroup -from geoh5py.objects import BlockModel, ObjectBase, Surface -from geoh5py.shared.utils import fetch_active_workspace -from scipy.interpolate import interp1d -from skimage.measure import marching_cubes -from tqdm import tqdm - -from geoapps.iso_surfaces.constants import validations -from geoapps.iso_surfaces.params import IsoSurfacesParams -from geoapps.shared_utils.utils import get_contours, rotate_xyz, weighted_average -from geoapps.utils.formatters import string_name - - -class IsoSurfacesDriver(BaseDriver): - _params_class = IsoSurfacesParams - _validations = validations - - def __init__(self, params: IsoSurfacesParams): - super().__init__(params) - self.params: IsoSurfacesParams = params - - def run(self): - """ - Create iso surfaces from input values. - """ - levels = get_contours( - self.params.interval_min, - self.params.interval_max, - self.params.interval_spacing, - self.params.fixed_contours, - ) - - if len(levels) < 1: - return - - with fetch_active_workspace(self.params.geoh5, mode="r+"): - print("Starting the isosurface creation.") - surfaces = self.iso_surface( - self.params.objects, - self.params.data.values, - levels, - resolution=self.params.resolution, - max_distance=self.params.max_distance, - ) - - container = ContainerGroup.create(self.params.geoh5, name="Isosurface") - result = [] - for surface, level in zip(surfaces, levels): - if len(surface[0]) > 0 and len(surface[1]) > 0: - result += [ - Surface.create( - self.params.geoh5, - name=string_name(self.params.export_as + f"_{level:.2e}"), - vertices=surface[0], - cells=surface[1], - parent=container, - ) - ] - self.update_monitoring_directory(container) - - print("Isosurface completed. " f"-> {len(surfaces)} surface(s) created.") - - return result - - @staticmethod - def iso_surface( - entity: ObjectBase, - values: np.ndarray, - levels: list, - resolution: float = 100, - max_distance: float = np.inf, - ): - """ - Generate 3D iso surface from an entity vertices or centroids and values. - - Parameters - ---------- - entity: geoh5py.objects - Any entity with 'vertices' or 'centroids' attribute. - - values: numpy.ndarray - Array of values to create iso-surfaces from. - - levels: list of floats - List of iso values - - max_distance: float, default=numpy.inf - Maximum distance from input data to generate iso surface. - Only used for input entities other than BlockModel. - - resolution: int, default=100 - Grid size used to generate the iso surface. - Only used for input entities other than BlockModel. - - Returns - ------- - surfaces: list of numpy.ndarrays - List of surfaces (one per levels) defined by - vertices and cell indices. - [(vertices, cells)_level_1, ..., (vertices, cells)_level_n] - """ - if getattr(entity, "vertices", None) is not None: - locations = entity.vertices - elif getattr(entity, "centroids", None) is not None: - locations = entity.centroids - else: - print("Input 'entity' must have 'vertices' or 'centroids'.") - return None - - if isinstance(entity, BlockModel): - values = values.reshape( - (entity.shape[2], entity.shape[0], entity.shape[1]), order="F" - ).transpose((1, 2, 0)) - - grid = [] - for i in ["u", "v", "z"]: - cell_delimiters = getattr(entity, i + "_cell_delimiters") - dx = cell_delimiters[1:] - cell_delimiters[:-1] - grid.append(cell_delimiters[:-1] + dx / 2) - - else: - print("Interpolating the model onto a regular grid...") - grid = [] - for i in range(3): - grid += [ - np.arange( - locations[:, i].min(), - locations[:, i].max() + resolution, - resolution, - ) - ] - - y, x, z = np.meshgrid(grid[1], grid[0], grid[2]) - values = weighted_average( - locations, - np.c_[x.flatten(), y.flatten(), z.flatten()], - [values], - threshold=resolution / 2.0, - n=8, - max_distance=max_distance, - ) - values = values[0].reshape(x.shape) - - lower, upper = np.nanmin(values), np.nanmax(values) - surfaces = [] - print("Running marching cubes on levels.") - skip = [] - for level in tqdm(levels): - try: - if level < lower or level > upper: - skip += [level] - continue - verts, faces, _, _ = marching_cubes(values, level=level) - - # Remove all vertices and cells with nan - nan_verts = np.any(np.isnan(verts), axis=1) - rem_cells = np.any(nan_verts[faces], axis=1) - - active = np.arange(nan_verts.shape[0]) - active[nan_verts] = nan_verts.shape[0] - _, inv_map = np.unique(active, return_inverse=True) - - verts = verts[~nan_verts, :] - faces = faces[~rem_cells, :] - faces = inv_map[faces].astype("uint32") - - vertices = [] - for i in range(3): - F = interp1d( - np.arange(grid[i].shape[0]), grid[i], fill_value="extrapolate" - ) - vertices += [F(verts[:, i])] - - if isinstance(entity, BlockModel): - vertices = rotate_xyz( - np.vstack(vertices).T, [0, 0, 0], entity.rotation - ) - vertices[:, 0] += entity.origin["x"] - vertices[:, 1] += entity.origin["y"] - vertices[:, 2] += entity.origin["z"] - - else: - vertices = np.vstack(vertices).T - except RuntimeError: - vertices, faces = [], [] - - surfaces += [[vertices, faces]] - - if any(skip): - warnings.warn(f"The following levels were out of bound and ignored: {skip}") - return surfaces - - -if __name__ == "__main__": - file = sys.argv[1] - IsoSurfacesDriver.start(file) diff --git a/geoapps/iso_surfaces/params.py b/geoapps/iso_surfaces/params.py deleted file mode 100644 index 4ebb766b8..000000000 --- a/geoapps/iso_surfaces/params.py +++ /dev/null @@ -1,140 +0,0 @@ -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of geoapps. ' -# ' -# geoapps is distributed under the terms and conditions of the MIT License ' -# (see LICENSE file at the root of this source code package). ' -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -from __future__ import annotations - -from copy import deepcopy - -from geoapps_utils.driver.params import BaseParams -from geoh5py.data import Data -from geoh5py.objects import ObjectBase - -from geoapps.iso_surfaces.constants import default_ui_json, defaults, validations - - -class IsoSurfacesParams(BaseParams): - """ - Parameter class for iso surfaces creation application. - """ - - def __init__(self, input_file=None, **kwargs): - self._default_ui_json = deepcopy(default_ui_json) - self._defaults = deepcopy(defaults) - self._validations = validations - self._objects = None - self._data = None - self._interval_min = None - self._interval_max = None - self._interval_spacing = None - self._fixed_contours = None - self._max_distance = None - self._resolution = None - self._export_as = None - - super().__init__(input_file=input_file, **kwargs) - - @property - def objects(self) -> ObjectBase | None: - """ - Input object - """ - return self._objects - - @objects.setter - def objects(self, val): - self.setter_validator("objects", val, fun=self._uuid_promoter) - - @property - def data(self) -> Data | None: - """ - Input data - """ - return self._data - - @data.setter - def data(self, val): - self.setter_validator("data", val) - - @property - def interval_min(self) -> float | None: - """ - Minimum value for contours. - """ - - return self._interval_min - - @interval_min.setter - def interval_min(self, val): - self.setter_validator("interval_min", val) - - @property - def interval_max(self) -> float | None: - """ - Maximum value for contours. - """ - return self._interval_max - - @interval_max.setter - def interval_max(self, val): - self.setter_validator("interval_max", val) - - @property - def interval_spacing(self) -> float | None: - """ - Step size for contours. - """ - return self._interval_spacing - - @interval_spacing.setter - def interval_spacing(self, val): - self.setter_validator("interval_spacing", val) - - @property - def fixed_contours(self) -> str | None: - """ - String defining list of fixed contours. - """ - return self._fixed_contours - - @fixed_contours.setter - def fixed_contours(self, val): - self.setter_validator("fixed_contours", val) - - @property - def max_distance(self) -> float | None: - """ - Maximum distance from input data to generate iso surface. - """ - return self._max_distance - - @max_distance.setter - def max_distance(self, val): - self.setter_validator("max_distance", val) - - @property - def resolution(self) -> float | None: - """ - Grid size used to generate the iso surface. - """ - return self._resolution - - @resolution.setter - def resolution(self, val): - self.setter_validator("resolution", val) - - @property - def export_as(self) -> str | None: - """ - Name to save surface as. - """ - return self._export_as - - @export_as.setter - def export_as(self, val): - self.setter_validator("export_as", val) diff --git a/geoapps/utils/write_default_uijson.py b/geoapps/utils/write_default_uijson.py index 354282392..d06dcaafd 100644 --- a/geoapps/utils/write_default_uijson.py +++ b/geoapps/utils/write_default_uijson.py @@ -48,7 +48,6 @@ from geoapps.block_model_creation.params import BlockModelParams from geoapps.clustering.params import ClusteringParams from geoapps.interpolation.params import DataInterpolationParams -from geoapps.iso_surfaces.params import IsoSurfacesParams from geoapps.peak_finder.params import PeakFinderParams from geoapps.scatter_plot.params import ScatterPlotParams @@ -200,11 +199,6 @@ def write_default_uijson(path: str | Path, use_initializers=False): peak_init["geoh5"] = str(assets_path() / "FlinFlon.geoh5") peak_init = peak_init if use_initializers else {} - from geoapps.iso_surfaces.constants import app_initializer as iso_init - - iso_init["geoh5"] = str(assets_path() / "FlinFlon.geoh5") - iso_init = iso_init if use_initializers else {} - from geoapps.inversion.joint.joint_surveys.constants import ( app_initializer as joint_surveys_init, ) @@ -304,7 +298,6 @@ def write_default_uijson(path: str | Path, use_initializers=False): "interpolation.ui.json": DataInterpolationParams(validate=False, **interp_init), "block_model_creation.ui.json": BlockModelParams(validate=False, **block_init), "cluster.ui.json": ClusteringParams(validate=False, **cluster_init), - "iso_surfaces.ui.json": IsoSurfacesParams(validate=False, **iso_init), } for filename, params in filedict.items(): diff --git a/tests/iso_surfaces_test.py b/tests/iso_surfaces_test.py deleted file mode 100644 index 3cc749971..000000000 --- a/tests/iso_surfaces_test.py +++ /dev/null @@ -1,155 +0,0 @@ -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -# Copyright (c) 2024 Mira Geoscience Ltd. ' -# ' -# This file is part of geoapps. ' -# ' -# geoapps is distributed under the terms and conditions of the MIT License ' -# (see LICENSE file at the root of this source code package). ' -# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -from __future__ import annotations - -from pathlib import Path - -import numpy as np -from geoh5py.objects import BlockModel, Points, Surface -from geoh5py.workspace import Workspace - -from geoapps.iso_surfaces.driver import IsoSurfacesDriver - - -def test_centroids(tmp_path: Path): - """ - Test iso_surface with a block model. Data values are the distance from a point. - """ - ws = Workspace(tmp_path / "iso_test.geoh5") - np.random.seed(0) - n = 70 - length = 10 - - # Axes for block model - x = np.linspace(0, length, n) - y = np.linspace(0, length, n) - z = np.linspace(0, length, n) - - origin = np.random.uniform(-100, 100, 3) - - # Create test block model - block_model = BlockModel.create( - ws, - origin=origin, - u_cell_delimiters=x, - v_cell_delimiters=y, - z_cell_delimiters=z, - name="test_block_model", - allow_move=False, - ) - - # Sphere test data for the block model - sphere_radius = np.random.uniform(length * 0.15, length * 0.3) - offset = np.random.uniform(0, (length / 2) - sphere_radius, 3) - sphere_center = ((length - 2) / 2, (length - 2) / 2, (length - 2) / 2) + offset - - # The value at each point is its distance from the center of the sphere - x_coords, y_coords, z_coords = np.meshgrid( - np.linspace(0, length, n - 1), - np.linspace(0, length, n - 1), - np.linspace(0, length, n - 1), - ) - verts = np.stack( - (x_coords.flatten(), y_coords.flatten(), z_coords.flatten()), axis=1 - ) - - values = np.linalg.norm( - np.subtract(verts, np.asarray(sphere_center)), - axis=1, - ) - - # Generate surface - func_surface = IsoSurfacesDriver.iso_surface( - block_model, values, [sphere_radius], max_distance=np.inf - ) - - # Compare surface center with sphere center - surf_center = np.mean(func_surface[0][0], axis=0) - center_error = np.abs( - ((sphere_center + origin) - surf_center) / (sphere_center + origin) - ) - - assert np.all(center_error < 0.02) - - # Radius of sphere - surf_distance = np.linalg.norm(np.subtract(func_surface[0][0], surf_center), axis=1) - surf_radius = np.mean(surf_distance, axis=0) - radius_error = np.abs((surf_radius - sphere_radius) / sphere_radius) - - assert radius_error < 0.02 - - # For user validation only - Surface.create( - ws, name="surface", vertices=func_surface[0][0], cells=func_surface[0][1] - ) - block_model.add_data( - { - "DataValues": { - "values": values, - } - } - ) - ws.close() - - -def test_vertices(tmp_path: Path): - """ - Test iso_surface with a points object. Data values are the distance from a point. - """ - ws = Workspace(tmp_path / "iso_test.geoh5") - np.random.seed(0) - length = 10 - origin = np.random.uniform(-100, 100, 3) - verts = np.random.randn(5000, 3) * length + origin - sphere_radius = np.random.uniform(length * 0.2, length * 0.5, 1) - offset = np.random.uniform(0, (length / 2), 3) - sphere_center = origin + offset - - values = np.linalg.norm(verts - sphere_center, axis=1) - - points = Points.create( - ws, - name="test_points", - vertices=verts, - ) - - func_surface = IsoSurfacesDriver.iso_surface( - points, - values, - [sphere_radius], - resolution=sphere_radius / 8.0, - max_distance=np.inf, - ) - - # For user validation only - Surface.create( - ws, name="surface", vertices=func_surface[0][0], cells=func_surface[0][1] - ) - points.add_data( - { - "DataValues": { - "values": values, - } - } - ) - ws.close() - - # Compare surface center with sphere center - surf_center = np.mean(func_surface[0][0], axis=0) - center_error = np.abs((sphere_center - surf_center) / (sphere_center)) - - assert np.all(center_error < 0.25) - - # Radius of sphere - surf_distance = np.linalg.norm(np.subtract(func_surface[0][0], surf_center), axis=1) - surf_radius = np.mean(surf_distance, axis=0) - radius_error = np.abs((surf_radius - sphere_radius) / sphere_radius) - - assert radius_error < 0.06