diff --git a/surface_apps/driver.py b/surface_apps/driver.py index 29f06f3..b7a690d 100644 --- a/surface_apps/driver.py +++ b/surface_apps/driver.py @@ -13,30 +13,28 @@ import sys import tempfile from abc import abstractmethod -from json import load from pathlib import Path -from geoapps_utils.driver.data import BaseData -from geoapps_utils.driver.driver import BaseDriver +from geoapps_utils.base import Driver, Options from geoh5py.groups import UIJsonGroup from geoh5py.objects import ObjectBase -from geoh5py.shared.utils import fetch_active_workspace +from geoh5py.shared.utils import fetch_active_workspace, stringify from geoh5py.ui_json import InputFile logger = logging.getLogger(__name__) -class BaseSurfaceDriver(BaseDriver): +class BaseSurfaceDriver(Driver): """ Driver for the surface application. :param parameters: Application parameters. """ - _parameter_class: type[BaseData] + _parameter_class: type[Options] - def __init__(self, parameters: BaseData | InputFile): + def __init__(self, parameters: Options | InputFile): self._out_group: UIJsonGroup | None = None if isinstance(parameters, InputFile): @@ -59,9 +57,7 @@ def out_group(self) -> UIJsonGroup | None: workspace=workspace, name=self.params.title, ) - self._out_group.options = InputFile.stringify( # type: ignore - InputFile.demote(self.params.input_file.ui_json) - ) + self._out_group.options = stringify(self.params.input_file.ui_json) return self._out_group @@ -91,27 +87,16 @@ def run(self): self.store() @property - def params(self) -> BaseData: + def params(self) -> Options: """Application parameters.""" return self._params @params.setter - def params(self, val: BaseData): - if not isinstance(val, BaseData): - raise TypeError("Parameters must be a BaseData subclass.") + def params(self, val: Options): + if not isinstance(val, Options): + raise TypeError("Parameters must be an Options 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. diff --git a/surface_apps/iso_surfaces/params.py b/surface_apps/iso_surfaces/params.py index ebf392c..959ce0a 100644 --- a/surface_apps/iso_surfaces/params.py +++ b/surface_apps/iso_surfaces/params.py @@ -13,19 +13,19 @@ from typing import ClassVar import numpy as np -from geoapps_utils.driver.data import BaseData +from geoapps_utils.base import Options from geoh5py.data import Data from geoh5py.groups import UIJsonGroup -from geoh5py.objects import Points, Surface +from geoh5py.objects import BlockModel, Points, Surface from geoh5py.objects.cell_object import CellObject from geoh5py.objects.grid_object import GridObject from geoh5py.ui_json.utils import str2list -from pydantic import ConfigDict, field_validator +from pydantic import BaseModel, ConfigDict, field_validator from surface_apps import assets_path -class IsoSurfaceSourceParameters(BaseData): +class IsoSurfaceSourceParameters(BaseModel): """ Source parameters providing input data to the driver. @@ -41,8 +41,22 @@ class IsoSurfaceSourceParameters(BaseData): data: Data horizon: Surface | None = None + @field_validator("objects", mode="before") + @classmethod + def no_single_layer_grids(cls, value): + """Ensure a grid has more than a single layer in any dimension.""" + + if isinstance(value, BlockModel): + n_cells = [len(getattr(value, f"{k}_cell_delimiters")) - 1 for k in "uvz"] + if any(n == 1 for n in n_cells): + raise ValueError( + "Grid source cannot be a single layer in any dimension." + ) + + return value + -class IsoSurfaceDetectionParameters(BaseData): +class IsoSurfaceDetectionParameters(BaseModel): """ Contour specification parameters. @@ -123,7 +137,7 @@ def contours(self) -> list[float]: return contours -class IsoSurfaceParameters(BaseData): +class IsoSurfaceParameters(Options): """ Contour parameters for use with `contours.driver`. diff --git a/tests/run_tests/iso_surfaces_test.py b/tests/run_tests/iso_surfaces_test.py index 49351ca..900a90a 100644 --- a/tests/run_tests/iso_surfaces_test.py +++ b/tests/run_tests/iso_surfaces_test.py @@ -12,10 +12,13 @@ from pathlib import Path import numpy as np +import pytest +from geoapps_utils.utils.importing import GeoAppsError from geoh5py.objects import BlockModel, Points, Surface from geoh5py.workspace import Workspace from surface_apps.iso_surfaces.driver import Driver as IsoSurfacesDriver +from surface_apps.iso_surfaces.params import IsoSurfaceParameters # pylint: disable=too-many-locals @@ -215,3 +218,38 @@ def test_clipping_horizon(tmp_path: Path): ) assert np.all(surface.vertices[:, -1] <= 30) + + +def test_single_layer_grid(tmp_path): + with Workspace(tmp_path / "iso_test.geoh5") as ws: + grid = BlockModel.create( + ws, + name="single_layer_grid", + u_cell_delimiters=np.linspace(0, 10, 11), + v_cell_delimiters=np.linspace(0, 10, 11), + z_cell_delimiters=np.array([0.0, 1.0]), + origin=[0, 0, 0], + ) + data = grid.add_data( + { + "elevation": { + "values": np.random.rand(grid.n_cells) * 100, + "data_type": "Float", + "association": "Cell", + } + } + ) + + with pytest.raises(GeoAppsError, match="cannot be a single layer"): + IsoSurfaceParameters.build( + { + "geoh5": ws, + "objects": grid, + "data": data, + "interval_min": 0.0, + "interval_max": 100.0, + "interval_spacing": 20.0, + "max_distance": 50.0, + "resolution": 5.0, + } + )