From 1ceb1607949c18734eb44305f6d9230c0da83c01 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Sat, 2 Apr 2022 15:08:05 +0200 Subject: [PATCH 01/63] Eclipse grid viewer --- setup.py | 4 +- webviz_subsurface/plugins/__init__.py | 1 + .../plugins/_eclipse_grid_viewer/__init__.py | 1 + .../_eclipse_grid_viewer/_business_logic.py | 109 ++++++++++++++++++ .../_eclipse_grid_viewer/_callbacks.py | 54 +++++++++ .../plugins/_eclipse_grid_viewer/_layout.py | 100 ++++++++++++++++ .../plugins/_eclipse_grid_viewer/_plugin.py | 38 ++++++ 7 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/__init__.py create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py diff --git a/setup.py b/setup.py index de7755b37..3d0921fe2 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ "webviz_config_plugins": [ "BhpQc = webviz_subsurface.plugins:BhpQc", "DiskUsage = webviz_subsurface.plugins:DiskUsage", + "EclipseGridViewer = webviz_subsurface.plugins:EclipseGridViewer", "GroupTree = webviz_subsurface.plugins:GroupTree", "HistoryMatch = webviz_subsurface.plugins:HistoryMatch", "HorizonUncertaintyViewer = webviz_subsurface.plugins:HorizonUncertaintyViewer", @@ -97,12 +98,13 @@ "pyarrow>=5.0.0", "pydeck>=0.6.2", "pyscal>=0.7.5", + "pyvista>=0.33.3", "scipy>=1.2", "statsmodels>=0.12.1", # indirect dependency through https://plotly.com/python/linear-fits/ "webviz-config>=0.3.8", "webviz-core-components>=0.5.6", "webviz-subsurface-components>=0.4.10", - "xtgeo>=2.14", + "xtgeo>=2.18.0a1", ], extras_require={"tests": TESTS_REQUIRE}, setup_requires=["setuptools_scm~=3.2"], diff --git a/webviz_subsurface/plugins/__init__.py b/webviz_subsurface/plugins/__init__.py index 19dec19b8..dedf6aaf8 100644 --- a/webviz_subsurface/plugins/__init__.py +++ b/webviz_subsurface/plugins/__init__.py @@ -23,6 +23,7 @@ from ._assisted_history_matching_analysis import AssistedHistoryMatchingAnalysis from ._bhp_qc import BhpQc from ._disk_usage import DiskUsage +from ._eclipse_grid_viewer import EclipseGridViewer from ._group_tree import GroupTree from ._history_match import HistoryMatch from ._horizon_uncertainty_viewer import HorizonUncertaintyViewer diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/__init__.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/__init__.py new file mode 100644 index 000000000..6a0678213 --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/__init__.py @@ -0,0 +1 @@ +from ._plugin import EclipseGridViewer diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py new file mode 100644 index 000000000..9a8e6f625 --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -0,0 +1,109 @@ +from pathlib import Path +from typing import List + +import numpy as np +import pyvista as pv +import xtgeo + +# pylint: disable=no-name-in-module, import-error +from vtk.util.numpy_support import vtk_to_numpy + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter + +from webviz_subsurface._utils.perf_timer import PerfTimer + + +def xtgeo_grid_to_explicit_structured_grid( + xtg_grid: xtgeo.Grid, +) -> pv.ExplicitStructuredGrid: + dims, corners, inactive = xtg_grid.get_vtk_geometries() + esg_grid = pv.ExplicitStructuredGrid(dims, corners) + esg_grid = esg_grid.compute_connectivity() + esg_grid.ComputeFacesConnectivityFlagsArray() + esg_grid = esg_grid.hide_cells(inactive) + esg_grid.flip_z(inplace=True) + return esg_grid + + +class ExplicitStructuredGridProvider: + def __init__(self, esg_grid: pv.ExplicitStructuredGrid) -> None: + self.esg_grid = esg_grid + + self.surface_polydata = self._extract_surface() + print(type(self.surface_polydata)) + self.surface_polys = vtk_to_numpy(self.surface_polydata.GetPolys().GetData()) + self.surface_points = vtk_to_numpy(self.surface_polydata.points).ravel() + + def _extract_surface( + self, + ) -> pv.PolyData: + extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() + extract_skin_filter.SetInputData(self.esg_grid) + extract_skin_filter.Update() + return pv.PolyData(extract_skin_filter.GetOutput()) + + def extract_surface_with_scalar(self, scalar: np.array) -> pv.PolyData: + grid = self.esg_grid + grid["scalar"] = scalar + extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() + extract_skin_filter.SetInputData(self.esg_grid) + extract_skin_filter.Update() + return pv.PolyData(extract_skin_filter.GetOutput()) + + +class EclipseGridDataModel: + def __init__( + self, + egrid_file: Path, + init_file: Path, + restart_file: Path, + init_names: List[str], + restart_names: List[str], + ): + self._egrid_file = egrid_file + self._init_file = init_file + self._restart_file = restart_file + self._init_names = init_names + self._restart_names = restart_names + self._xtg_grid = xtgeo.grid_from_file(egrid_file, fformat="egrid") + timer = PerfTimer() + print("Converting egrid to VTK ExplicitStructuredGrid") + self.esg_provider = ExplicitStructuredGridProvider( + xtgeo_grid_to_explicit_structured_grid(self._xtg_grid) + ) + print(f"Conversion complete in : {timer.lap_s():.2f}s") + self._restart_dates = self._get_restart_dates() + + def _get_restart_dates(self) -> List[str]: + return xtgeo.GridProperties.scan_dates(self._restart_file, datesonly=True) + + @property + def init_names(self) -> List[str]: + return self._init_names + + @property + def restart_names(self) -> List[str]: + return self._restart_names + + @property + def restart_dates(self) -> List[str]: + return self._restart_dates + + def get_init_property(self, prop_name: str) -> np.array: + + prop = xtgeo.gridproperty_from_file( + self._init_file, fformat="init", name=prop_name, grid=self._xtg_grid + ) + return prop.get_npvalues1d(order="F").ravel() + + def get_restart_property(self, prop_name: str, prop_date: int) -> np.array: + + prop = xtgeo.gridproperty_from_file( + self._restart_file, + fformat="unrst", + name=prop_name, + date=prop_date, + grid=self._xtg_grid, + ) + return prop.get_npvalues1d(order="F") diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py new file mode 100644 index 000000000..1ea015ed3 --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -0,0 +1,54 @@ +from typing import Callable, Dict, List, Optional, Tuple + +import numpy as np +from dash import Input, Output, State, callback + +from ._business_logic import EclipseGridDataModel +from ._layout import PROPERTYTYPE, LayoutElements + + +def plugin_callbacks(get_uuid: Callable, datamodel: EclipseGridDataModel) -> None: + @callback( + Output(get_uuid(LayoutElements.PROPERTIES), "options"), + Output(get_uuid(LayoutElements.PROPERTIES), "value"), + Output(get_uuid(LayoutElements.DATES), "options"), + Output(get_uuid(LayoutElements.DATES), "value"), + Input(get_uuid(LayoutElements.INIT_RESTART), "value"), + ) + def _populate_properties( + init_restart: str, + ) -> Tuple[ + List[Dict[str, str]], List[str], List[Dict[str, str]], Optional[List[str]] + ]: + if PROPERTYTYPE(init_restart) == PROPERTYTYPE.INIT: + prop_names = datamodel.init_names + dates = [] + else: + prop_names = datamodel.restart_names + dates = datamodel.restart_dates + return ( + [{"label": prop, "value": prop} for prop in prop_names], + [prop_names[0]], + ([{"label": prop, "value": prop} for prop in dates]), + [dates[0]] if dates else None, + ) + + @callback( + Output(get_uuid(LayoutElements.VTK_GRID_CELLDATA), "values"), + Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "colorDataRange"), + Input(get_uuid(LayoutElements.PROPERTIES), "value"), + Input(get_uuid(LayoutElements.DATES), "value"), + State(get_uuid(LayoutElements.INIT_RESTART), "value"), + ) + def _set_scalar( + prop: List[str], date: List[int], proptype: str + ) -> Tuple[np.array, list]: + + if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: + scalar = datamodel.get_init_property(prop[0]) + else: + scalar = datamodel.get_restart_property(prop[0], date[0]) + + polydata = datamodel.esg_provider.extract_surface_with_scalar(scalar) + + return polydata["scalar"], [np.nanmin(scalar), np.nanmax(scalar)] diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py new file mode 100644 index 000000000..eee6b9c5f --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -0,0 +1,100 @@ +from enum import Enum +from typing import Callable + +import dash_vtk +import webviz_core_components as wcc + +from ._business_logic import ExplicitStructuredGridProvider + + +# pylint: disable = too-few-public-methods +class LayoutElements(str, Enum): + INIT_RESTART = "init-restart-select" + PROPERTIES = "properties-select" + DATES = "dates-select" + VTK_VIEW = "vtk-view" + VTK_GRID_REPRESENTATION = "vtk-grid-representation" + VTK_GRID_POLYDATA = "vtk-grid-polydata" + VTK_GRID_CELLDATA = "vtk-grid-celldata" + + +class LayoutTitles(str, Enum): + INIT_RESTART = "Init / Restart" + PROPERTIES = "Property" + DATES = "Date" + + +class PROPERTYTYPE(str, Enum): + INIT = "Init" + RESTART = "Restart" + + +class LayoutStyle: + MAIN_HEIGHT = "87vh" + SIDEBAR = {"flex": 1, "height": "87vh"} + VTK_VIEW = {"flex": 5, "height": "87vh"} + + +def plugin_main_layout( + get_uuid: Callable, esg_provider: ExplicitStructuredGridProvider +) -> wcc.FlexBox: + + return wcc.FlexBox( + children=[ + sidebar(get_uuid=get_uuid), + vtk_view(get_uuid=get_uuid, esg_provider=esg_provider), + ] + ) + + +def sidebar(get_uuid: Callable) -> wcc.Frame: + return wcc.Frame( + style=LayoutStyle.SIDEBAR, + children=[ + wcc.RadioItems( + label=LayoutTitles.INIT_RESTART, + id=get_uuid(LayoutElements.INIT_RESTART), + options=[{"label": prop, "value": prop} for prop in PROPERTYTYPE], + value=PROPERTYTYPE.INIT, + ), + wcc.SelectWithLabel( + id=get_uuid(LayoutElements.PROPERTIES), label=LayoutTitles.PROPERTIES + ), + wcc.SelectWithLabel( + id=get_uuid(LayoutElements.DATES), label=LayoutTitles.DATES + ), + ], + ) + + +def vtk_view( + get_uuid: Callable, esg_provider: ExplicitStructuredGridProvider +) -> dash_vtk.View: + return dash_vtk.View( + id=get_uuid(LayoutElements.VTK_VIEW), + style=LayoutStyle.VTK_VIEW, + children=[ + dash_vtk.GeometryRepresentation( + id=get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), + children=[ + dash_vtk.PolyData( + id=get_uuid(LayoutElements.VTK_GRID_POLYDATA), + polys=esg_provider.surface_polys, + points=esg_provider.surface_points, + children=[ + dash_vtk.CellData( + [ + dash_vtk.DataArray( + id=get_uuid(LayoutElements.VTK_GRID_CELLDATA), + registration="setScalars", + name="scalar", + ) + ] + ) + ], + ) + ], + property={"edgeVisibility": True}, + ), + ], + ) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py new file mode 100644 index 000000000..a36a123eb --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -0,0 +1,38 @@ +from pathlib import Path +from typing import List + +import webviz_core_components as wcc +from webviz_config import WebvizPluginABC + +from ._business_logic import EclipseGridDataModel +from ._callbacks import plugin_callbacks +from ._layout import plugin_main_layout + + +class EclipseGridViewer(WebvizPluginABC): + """Eclipse grid viewer""" + + def __init__( + self, + egrid_file: Path, + init_file: Path, + restart_file: Path, + init_names: List[str], + restart_names: List[str], + ) -> None: + super().__init__() + + self._datamodel: EclipseGridDataModel = EclipseGridDataModel( + egrid_file=egrid_file, + init_file=init_file, + restart_file=restart_file, + init_names=init_names, + restart_names=restart_names, + ) + plugin_callbacks(get_uuid=self.uuid, datamodel=self._datamodel) + + @property + def layout(self) -> wcc.FlexBox: + return plugin_main_layout( + get_uuid=self.uuid, esg_provider=self._datamodel.esg_provider + ) From 6b6aed14eee04d4e42bfa597d51109e866bdd2de Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Sat, 2 Apr 2022 15:42:47 +0200 Subject: [PATCH 02/63] dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3d0921fe2..39588b9ff 100644 --- a/setup.py +++ b/setup.py @@ -85,6 +85,7 @@ "dash>=2.0.0", "dash_bootstrap_components>=0.10.3", "dash-daq>=0.5.0", + "dash-vtk>=0.0.9", "dataclasses>=0.8; python_version<'3.7'", "defusedxml>=0.6.0", "ecl2df>=0.15.0; sys_platform=='linux'", From 3788dcf794f7eb4301490511f2b24ae64b8bb7f7 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:29:21 +0200 Subject: [PATCH 03/63] Only send scalars --- .../_eclipse_grid_viewer/_business_logic.py | 29 ++++++++++--------- .../_eclipse_grid_viewer/_callbacks.py | 5 ++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 9a8e6f625..be0a75036 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -29,25 +29,21 @@ def xtgeo_grid_to_explicit_structured_grid( class ExplicitStructuredGridProvider: def __init__(self, esg_grid: pv.ExplicitStructuredGrid) -> None: self.esg_grid = esg_grid - + timer = PerfTimer() self.surface_polydata = self._extract_surface() - print(type(self.surface_polydata)) + print(f"Extracted grid skin in : {timer.lap_s():.2f}s") self.surface_polys = vtk_to_numpy(self.surface_polydata.GetPolys().GetData()) + self.surface_points = vtk_to_numpy(self.surface_polydata.points).ravel() def _extract_surface( self, ) -> pv.PolyData: + """Extract and keep the grid surface. Also keep track of cell indices, to be used + to extract indices from the scalar arrays""" extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() extract_skin_filter.SetInputData(self.esg_grid) - extract_skin_filter.Update() - return pv.PolyData(extract_skin_filter.GetOutput()) - - def extract_surface_with_scalar(self, scalar: np.array) -> pv.PolyData: - grid = self.esg_grid - grid["scalar"] = scalar - extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() - extract_skin_filter.SetInputData(self.esg_grid) + extract_skin_filter.PassThroughCellIdsOn() extract_skin_filter.Update() return pv.PolyData(extract_skin_filter.GetOutput()) @@ -90,15 +86,15 @@ def restart_names(self) -> List[str]: def restart_dates(self) -> List[str]: return self._restart_dates - def get_init_property(self, prop_name: str) -> np.array: + def get_init_property(self, prop_name: str) -> np.ndarray: prop = xtgeo.gridproperty_from_file( self._init_file, fformat="init", name=prop_name, grid=self._xtg_grid ) return prop.get_npvalues1d(order="F").ravel() - def get_restart_property(self, prop_name: str, prop_date: int) -> np.array: - + def get_restart_property(self, prop_name: str, prop_date: int) -> np.ndarray: + timer = PerfTimer() prop = xtgeo.gridproperty_from_file( self._restart_file, fformat="unrst", @@ -106,4 +102,9 @@ def get_restart_property(self, prop_name: str, prop_date: int) -> np.array: date=prop_date, grid=self._xtg_grid, ) - return prop.get_npvalues1d(order="F") + print( + f"Read {prop_name}, {prop_date} from restart file in {timer.lap_s():.2f}s" + ) + vals = prop.get_npvalues1d(order="F") + + return vals diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 1ea015ed3..9b6aa796f 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -49,6 +49,7 @@ def _set_scalar( else: scalar = datamodel.get_restart_property(prop[0], date[0]) - polydata = datamodel.esg_provider.extract_surface_with_scalar(scalar) - + polydata = datamodel.esg_provider.surface_polydata + cell_indices = polydata["vtkOriginalCellIds"] + polydata["scalar"] = scalar[cell_indices] return polydata["scalar"], [np.nanmin(scalar), np.nanmax(scalar)] From 2fbdeaa00ac7b1e0546c23dba1ce295c60b4268a Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:10:51 +0200 Subject: [PATCH 04/63] Added cropping --- .../_eclipse_grid_viewer/_business_logic.py | 69 +++++++++++++---- .../_eclipse_grid_viewer/_callbacks.py | 75 ++++++++++++++++--- .../plugins/_eclipse_grid_viewer/_layout.py | 72 ++++++++++++++++-- 3 files changed, 185 insertions(+), 31 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index be0a75036..a2a2e9b84 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List +from typing import List, Tuple import numpy as np import pyvista as pv @@ -8,6 +8,12 @@ # pylint: disable=no-name-in-module, import-error from vtk.util.numpy_support import vtk_to_numpy +# pylint: disable=no-name-in-module, +from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop + # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter @@ -19,6 +25,7 @@ def xtgeo_grid_to_explicit_structured_grid( ) -> pv.ExplicitStructuredGrid: dims, corners, inactive = xtg_grid.get_vtk_geometries() esg_grid = pv.ExplicitStructuredGrid(dims, corners) + esg_grid = esg_grid.compute_connectivity() esg_grid.ComputeFacesConnectivityFlagsArray() esg_grid = esg_grid.hide_cells(inactive) @@ -29,23 +36,57 @@ def xtgeo_grid_to_explicit_structured_grid( class ExplicitStructuredGridProvider: def __init__(self, esg_grid: pv.ExplicitStructuredGrid) -> None: self.esg_grid = esg_grid - timer = PerfTimer() - self.surface_polydata = self._extract_surface() - print(f"Extracted grid skin in : {timer.lap_s():.2f}s") - self.surface_polys = vtk_to_numpy(self.surface_polydata.GetPolys().GetData()) - self.surface_points = vtk_to_numpy(self.surface_polydata.points).ravel() - - def _extract_surface( - self, - ) -> pv.PolyData: - """Extract and keep the grid surface. Also keep track of cell indices, to be used - to extract indices from the scalar arrays""" + def crop( + self, irange: List[int], jrange: List[int], krange: List[int] + ) -> vtkExplicitStructuredGrid: + crop_filter = vtkExplicitStructuredGridCrop() + crop_filter.SetInputData(self.esg_grid) + crop_filter.SetOutputWholeExtent( + irange[0], irange[1], jrange[0], jrange[1], krange[0], krange[1] + ) + crop_filter.Update() + grid = crop_filter.GetOutput() + return self.extract_skin(grid) + + def extract_skin( + self, grid: vtkExplicitStructuredGrid = None + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + grid = grid if grid is not None else self.esg_grid extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() - extract_skin_filter.SetInputData(self.esg_grid) + extract_skin_filter.SetInputData(grid) extract_skin_filter.PassThroughCellIdsOn() extract_skin_filter.Update() - return pv.PolyData(extract_skin_filter.GetOutput()) + polydata = extract_skin_filter.GetOutput() + polydata = pv.PolyData(polydata) + polys = vtk_to_numpy(polydata.GetPolys().GetData()) + points = vtk_to_numpy(polydata.points).ravel() + indices = polydata["vtkOriginalCellIds"] + return polys, points, indices + + @property + def imin(self) -> int: + return 0 + + @property + def imax(self) -> int: + return self.esg_grid.dimensions[0] - 1 + + @property + def jmin(self) -> int: + return 0 + + @property + def jmax(self) -> int: + return self.esg_grid.dimensions[1] - 1 + + @property + def kmin(self) -> int: + return 0 + + @property + def kmax(self) -> int: + return self.esg_grid.dimensions[2] - 1 class EclipseGridDataModel: diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 9b6aa796f..0e1cad839 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -1,7 +1,11 @@ -from typing import Callable, Dict, List, Optional, Tuple +import hashlib +from time import time +from typing import Any, Callable, Dict, List, Optional, Tuple import numpy as np -from dash import Input, Output, State, callback +from dash import Input, Output, State, callback, no_update + +from webviz_subsurface._utils.perf_timer import PerfTimer from ._business_logic import EclipseGridDataModel from ._layout import PROPERTYTYPE, LayoutElements @@ -34,22 +38,75 @@ def _populate_properties( ) @callback( + Output(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "polys"), + Output(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "points"), Output(get_uuid(LayoutElements.VTK_GRID_CELLDATA), "values"), Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "colorDataRange"), + Output(get_uuid(LayoutElements.STORED_CELL_INDICES_HASH), "data"), Input(get_uuid(LayoutElements.PROPERTIES), "value"), Input(get_uuid(LayoutElements.DATES), "value"), + Input(get_uuid(LayoutElements.GRID_COLUMNS), "value"), + Input(get_uuid(LayoutElements.GRID_ROWS), "value"), + Input(get_uuid(LayoutElements.GRID_LAYERS), "value"), State(get_uuid(LayoutElements.INIT_RESTART), "value"), + State(get_uuid(LayoutElements.STORED_CELL_INDICES_HASH), "data"), ) - def _set_scalar( - prop: List[str], date: List[int], proptype: str - ) -> Tuple[np.array, list]: + def _set_geometry_and_scalar( + prop: List[str], + date: List[int], + columns: List[int], + rows: List[int], + layers: List[int], + proptype: str, + stored_cell_indices: int, + ) -> Tuple[Any, Any, Any, List, Any]: + timer = PerfTimer() if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: scalar = datamodel.get_init_property(prop[0]) else: scalar = datamodel.get_restart_property(prop[0], date[0]) + print(f"Reading scalar from file in {timer.lap_s():.2f}s") + + polys, points, cell_indices = datamodel.esg_provider.crop(columns, rows, layers) + print(f"Extracting cropped geometry in {timer.lap_s():.2f}s") + + # Storing hash of cell indices client side to control if only scalar should be updated + hashed_indices = hashlib.sha256(cell_indices.data.tobytes()).hexdigest().upper() + print(f"Hashing indices in {timer.lap_s():.2f}s") + + if hashed_indices == stored_cell_indices: + return ( + no_update, + no_update, + scalar[cell_indices], + [np.nanmin(scalar), np.nanmax(scalar)], + no_update, + ) + + return ( + polys, + points, + scalar[cell_indices], + [np.nanmin(scalar), np.nanmax(scalar)], + hashed_indices, + ) + + @callback( + Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), + Input(get_uuid(LayoutElements.Z_SCALE), "value"), + State(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), + ) + def _set_actor(z_scale: int, actor: Optional[dict]) -> dict: + actor = actor if actor else {} + actor.update({"scale": (1, 1, z_scale)}) + return actor - polydata = datamodel.esg_provider.surface_polydata - cell_indices = polydata["vtkOriginalCellIds"] - polydata["scalar"] = scalar[cell_indices] - return polydata["scalar"], [np.nanmin(scalar), np.nanmax(scalar)] + @callback( + Output(get_uuid(LayoutElements.VTK_VIEW), "triggerResetCamera"), + Input(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "polys"), + Input(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "points"), + Input(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), + ) + def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> float: + return time() diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index eee6b9c5f..df6e273c3 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -3,6 +3,7 @@ import dash_vtk import webviz_core_components as wcc +from dash import dcc from ._business_logic import ExplicitStructuredGridProvider @@ -12,16 +13,25 @@ class LayoutElements(str, Enum): INIT_RESTART = "init-restart-select" PROPERTIES = "properties-select" DATES = "dates-select" + Z_SCALE = "z-scale" + GRID_COLUMNS = "grid-columns" + GRID_ROWS = "grid-rows" + GRID_LAYERS = "grid-layers" VTK_VIEW = "vtk-view" VTK_GRID_REPRESENTATION = "vtk-grid-representation" VTK_GRID_POLYDATA = "vtk-grid-polydata" VTK_GRID_CELLDATA = "vtk-grid-celldata" + STORED_CELL_INDICES_HASH = "stored-cell-indices-hash" class LayoutTitles(str, Enum): INIT_RESTART = "Init / Restart" PROPERTIES = "Property" DATES = "Date" + Z_SCALE = "Z-scale" + GRID_COLUMNS = "Grid columns" + GRID_ROWS = "Grid rows" + GRID_LAYERS = "Grid layers" class PROPERTYTYPE(str, Enum): @@ -41,13 +51,16 @@ def plugin_main_layout( return wcc.FlexBox( children=[ - sidebar(get_uuid=get_uuid), - vtk_view(get_uuid=get_uuid, esg_provider=esg_provider), + sidebar(get_uuid=get_uuid, esg_provider=esg_provider), + vtk_view(get_uuid=get_uuid), + dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), ] ) -def sidebar(get_uuid: Callable) -> wcc.Frame: +def sidebar( + get_uuid: Callable, esg_provider: ExplicitStructuredGridProvider +) -> wcc.Frame: return wcc.Frame( style=LayoutStyle.SIDEBAR, children=[ @@ -63,13 +76,58 @@ def sidebar(get_uuid: Callable) -> wcc.Frame: wcc.SelectWithLabel( id=get_uuid(LayoutElements.DATES), label=LayoutTitles.DATES ), + wcc.Slider( + label=LayoutTitles.Z_SCALE, + id=get_uuid(LayoutElements.Z_SCALE), + min=1, + max=10, + value=1, + step=1, + ), + wcc.RangeSlider( + label=LayoutTitles.GRID_COLUMNS, + id=get_uuid(LayoutElements.GRID_COLUMNS), + min=esg_provider.imin, + max=esg_provider.imax, + value=[esg_provider.imin, esg_provider.imax], + step=1, + marks=None, + tooltip={ + "placement": "bottom", + "always_visible": True, + }, + ), + wcc.RangeSlider( + label=LayoutTitles.GRID_ROWS, + id=get_uuid(LayoutElements.GRID_ROWS), + min=esg_provider.jmin, + max=esg_provider.jmax, + value=[esg_provider.jmin, esg_provider.jmax], + step=1, + marks=None, + tooltip={ + "placement": "bottom", + "always_visible": True, + }, + ), + wcc.RangeSlider( + label=LayoutTitles.GRID_LAYERS, + id=get_uuid(LayoutElements.GRID_LAYERS), + min=esg_provider.kmin, + max=esg_provider.kmax, + value=[esg_provider.kmin, esg_provider.kmax], + step=1, + marks=None, + tooltip={ + "placement": "bottom", + "always_visible": True, + }, + ), ], ) -def vtk_view( - get_uuid: Callable, esg_provider: ExplicitStructuredGridProvider -) -> dash_vtk.View: +def vtk_view(get_uuid: Callable) -> dash_vtk.View: return dash_vtk.View( id=get_uuid(LayoutElements.VTK_VIEW), style=LayoutStyle.VTK_VIEW, @@ -79,8 +137,6 @@ def vtk_view( children=[ dash_vtk.PolyData( id=get_uuid(LayoutElements.VTK_GRID_POLYDATA), - polys=esg_provider.surface_polys, - points=esg_provider.surface_points, children=[ dash_vtk.CellData( [ From 24c1e9031c2f9e947f9c59d49655d94d45f626df Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Mon, 4 Apr 2022 21:38:37 +0200 Subject: [PATCH 05/63] Very hacky, inaccurate way to get readout of selected cell --- .../_eclipse_grid_viewer/_business_logic.py | 16 ++++-- .../_eclipse_grid_viewer/_callbacks.py | 53 ++++++++++++++++++- .../plugins/_eclipse_grid_viewer/_layout.py | 5 +- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index a2a2e9b84..9d34d0aaa 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -24,12 +24,12 @@ def xtgeo_grid_to_explicit_structured_grid( xtg_grid: xtgeo.Grid, ) -> pv.ExplicitStructuredGrid: dims, corners, inactive = xtg_grid.get_vtk_geometries() + corners[:, 2] *= -1 esg_grid = pv.ExplicitStructuredGrid(dims, corners) - esg_grid = esg_grid.compute_connectivity() esg_grid.ComputeFacesConnectivityFlagsArray() esg_grid = esg_grid.hide_cells(inactive) - esg_grid.flip_z(inplace=True) + # esg_grid.flip_z(inplace=True) return esg_grid @@ -132,7 +132,7 @@ def get_init_property(self, prop_name: str) -> np.ndarray: prop = xtgeo.gridproperty_from_file( self._init_file, fformat="init", name=prop_name, grid=self._xtg_grid ) - return prop.get_npvalues1d(order="F").ravel() + return prop def get_restart_property(self, prop_name: str, prop_date: int) -> np.ndarray: timer = PerfTimer() @@ -146,6 +146,12 @@ def get_restart_property(self, prop_name: str, prop_date: int) -> np.ndarray: print( f"Read {prop_name}, {prop_date} from restart file in {timer.lap_s():.2f}s" ) - vals = prop.get_npvalues1d(order="F") + return prop - return vals + def get_init_values(self, prop_name: str): + prop = self.get_init_property(prop_name) + return prop.get_npvalues1d(order="F").ravel() + + def get_restart_values(self, prop_name: str, prop_date: int): + prop = self.get_restart_property(prop_name, prop_date) + return prop.get_npvalues1d(order="F").ravel() diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 0e1cad839..760e246f4 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -1,6 +1,7 @@ import hashlib from time import time from typing import Any, Callable, Dict, List, Optional, Tuple +import json import numpy as np from dash import Input, Output, State, callback, no_update @@ -63,9 +64,9 @@ def _set_geometry_and_scalar( timer = PerfTimer() if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: - scalar = datamodel.get_init_property(prop[0]) + scalar = datamodel.get_init_values(prop[0]) else: - scalar = datamodel.get_restart_property(prop[0], date[0]) + scalar = datamodel.get_restart_values(prop[0], date[0]) print(f"Reading scalar from file in {timer.lap_s():.2f}s") polys, points, cell_indices = datamodel.esg_provider.crop(columns, rows, layers) @@ -110,3 +111,51 @@ def _set_actor(z_scale: int, actor: Optional[dict]) -> dict: ) def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> float: return time() + + @callback( + Output(get_uuid(LayoutElements.SELECTED_CELL), "children"), + Input(get_uuid(LayoutElements.VTK_VIEW), "clickInfo"), + State(get_uuid(LayoutElements.Z_SCALE), "value"), + State(get_uuid(LayoutElements.PROPERTIES), "value"), + State(get_uuid(LayoutElements.DATES), "value"), + State(get_uuid(LayoutElements.INIT_RESTART), "value"), + ) + def _update_click_info(clickData, zscale, prop, date, proptype): + + if clickData: + if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: + scalar = datamodel.get_init_property(prop[0]) + else: + scalar = datamodel.get_restart_property(prop[0], date[0]) + + pos = clickData["worldPosition"] + pos[2] = pos[2] / -zscale + import xtgeo + + timer = PerfTimer() + p = xtgeo.Points([pos]) + + ijk = datamodel._xtg_grid.get_ijk_from_points( + p, dataframe=False, includepoints=False, zerobased=True + )[0] + print(f"Get selected cell {timer.lap_s():.2f}s") + scalar_value = scalar.get_values_by_ijk( + np.array([ijk[0]]), np.array([ijk[1]]), np.array([ijk[2]]), base=0 + ) + print(f"Get property value for selected cell {timer.lap_s():.2f}s") + scalar_value = scalar_value[0] if scalar_value is not None else None + propname = f"{prop[0]}-{date[0]}" if date else f"{prop[0]}" + return json.dumps( + { + "x": pos[0], + "y": pos[1], + "z": pos[2], + "i": ijk[0], + "j": ijk[1], + "k": ijk[2], + propname: scalar_value, + }, + indent=2, + ) + + return [""] diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index df6e273c3..87ab1c835 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -3,7 +3,7 @@ import dash_vtk import webviz_core_components as wcc -from dash import dcc +from dash import dcc, html from ._business_logic import ExplicitStructuredGridProvider @@ -22,6 +22,7 @@ class LayoutElements(str, Enum): VTK_GRID_POLYDATA = "vtk-grid-polydata" VTK_GRID_CELLDATA = "vtk-grid-celldata" STORED_CELL_INDICES_HASH = "stored-cell-indices-hash" + SELECTED_CELL = "selected-cell" class LayoutTitles(str, Enum): @@ -123,6 +124,7 @@ def sidebar( "always_visible": True, }, ), + html.Pre(id=get_uuid(LayoutElements.SELECTED_CELL)), ], ) @@ -131,6 +133,7 @@ def vtk_view(get_uuid: Callable) -> dash_vtk.View: return dash_vtk.View( id=get_uuid(LayoutElements.VTK_VIEW), style=LayoutStyle.VTK_VIEW, + pickingModes=["click"], children=[ dash_vtk.GeometryRepresentation( id=get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), From a25dd471a679ea11ce39bea2965ac806b6e31b0f Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 5 Apr 2022 08:31:46 +0200 Subject: [PATCH 06/63] Use VTK for finding selected cell. Still issues with precision --- .../_eclipse_grid_viewer/_business_logic.py | 54 +++++++++--- .../_eclipse_grid_viewer/_callbacks.py | 82 ++++++++++--------- .../plugins/_eclipse_grid_viewer/_layout.py | 7 ++ 3 files changed, 94 insertions(+), 49 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 9d34d0aaa..6e4c97b24 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -9,7 +9,12 @@ from vtk.util.numpy_support import vtk_to_numpy # pylint: disable=no-name-in-module, -from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid +from vtkmodules.vtkCommonDataModel import ( + vtkExplicitStructuredGrid, + vtkCellLocator, + vtkGenericCell, +) +from vtkmodules.vtkCommonCore import mutable # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop @@ -36,6 +41,7 @@ def xtgeo_grid_to_explicit_structured_grid( class ExplicitStructuredGridProvider: def __init__(self, esg_grid: pv.ExplicitStructuredGrid) -> None: self.esg_grid = esg_grid + self.extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() def crop( self, irange: List[int], jrange: List[int], krange: List[int] @@ -47,23 +53,49 @@ def crop( ) crop_filter.Update() grid = crop_filter.GetOutput() - return self.extract_skin(grid) + return grid def extract_skin( self, grid: vtkExplicitStructuredGrid = None ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: grid = grid if grid is not None else self.esg_grid - extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() - extract_skin_filter.SetInputData(grid) - extract_skin_filter.PassThroughCellIdsOn() - extract_skin_filter.Update() - polydata = extract_skin_filter.GetOutput() + + self.extract_skin_filter.SetInputData(grid) + self.extract_skin_filter.PassThroughCellIdsOn() + self.extract_skin_filter.Update() + polydata = self.extract_skin_filter.GetOutput() polydata = pv.PolyData(polydata) polys = vtk_to_numpy(polydata.GetPolys().GetData()) points = vtk_to_numpy(polydata.points).ravel() indices = polydata["vtkOriginalCellIds"] return polys, points, indices + def find_containing_cell(self, coords): + timer = PerfTimer() + locator = vtkCellLocator() + locator.SetDataSet(self.esg_grid) + locator.BuildLocator() + # containing_cell = locator.FindCell(coords) #Slower and not precise?? + # print(f"Containing cell in {timer.lap_s():.2f}") + + cell = vtkGenericCell() + closest_point = [0.0, 0.0, 0.0] + cell_id = mutable(0) + sub_id = mutable(0) + dist2 = mutable(0.0) + closest_cell = locator.FindClosestPoint( + coords, closest_point, cell, cell_id, sub_id, dist2 + ) + print(f"Closest cell in {timer.lap_s():.2f}") + + i = mutable(0) + j = mutable(0) + k = mutable(0) + self.esg_grid.ComputeCellStructuredCoords(cell_id, i, j, k, False) + print(f"Get ijk in {timer.lap_s():.2f}") + + return cell_id, [int(i), int(j), int(k)] + @property def imin(self) -> int: return 0 @@ -103,7 +135,10 @@ def __init__( self._restart_file = restart_file self._init_names = init_names self._restart_names = restart_names + + # Eclipse grid geometry required when loading grid properties later on self._xtg_grid = xtgeo.grid_from_file(egrid_file, fformat="egrid") + timer = PerfTimer() print("Converting egrid to VTK ExplicitStructuredGrid") self.esg_provider = ExplicitStructuredGridProvider( @@ -141,10 +176,7 @@ def get_restart_property(self, prop_name: str, prop_date: int) -> np.ndarray: fformat="unrst", name=prop_name, date=prop_date, - grid=self._xtg_grid, - ) - print( - f"Read {prop_name}, {prop_date} from restart file in {timer.lap_s():.2f}s" + # grid=self._xtg_grid, ) return prop diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 760e246f4..731afbc5a 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -69,7 +69,8 @@ def _set_geometry_and_scalar( scalar = datamodel.get_restart_values(prop[0], date[0]) print(f"Reading scalar from file in {timer.lap_s():.2f}s") - polys, points, cell_indices = datamodel.esg_provider.crop(columns, rows, layers) + cropped_grid = datamodel.esg_provider.crop(columns, rows, layers) + polys, points, cell_indices = datamodel.esg_provider.extract_skin(cropped_grid) print(f"Extracting cropped geometry in {timer.lap_s():.2f}s") # Storing hash of cell indices client side to control if only scalar should be updated @@ -98,11 +99,23 @@ def _set_geometry_and_scalar( Input(get_uuid(LayoutElements.Z_SCALE), "value"), State(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), ) - def _set_actor(z_scale: int, actor: Optional[dict]) -> dict: + def _set_representation_actor(z_scale: int, actor: Optional[dict]) -> dict: actor = actor if actor else {} actor.update({"scale": (1, 1, z_scale)}) return actor + @callback( + Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "property"), + Input(get_uuid(LayoutElements.SHOW_GRID_LINES), "value"), + State(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "property"), + ) + def _set_representation_property( + show_grid_lines: int, properties: Optional[dict] + ) -> dict: + properties = properties if properties else {} + properties.update({"edgeVisibility": bool(show_grid_lines)}) + return properties + @callback( Output(get_uuid(LayoutElements.VTK_VIEW), "triggerResetCamera"), Input(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "polys"), @@ -122,40 +135,33 @@ def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> floa ) def _update_click_info(clickData, zscale, prop, date, proptype): - if clickData: - if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: - scalar = datamodel.get_init_property(prop[0]) - else: - scalar = datamodel.get_restart_property(prop[0], date[0]) - - pos = clickData["worldPosition"] - pos[2] = pos[2] / -zscale - import xtgeo - - timer = PerfTimer() - p = xtgeo.Points([pos]) - - ijk = datamodel._xtg_grid.get_ijk_from_points( - p, dataframe=False, includepoints=False, zerobased=True - )[0] - print(f"Get selected cell {timer.lap_s():.2f}s") - scalar_value = scalar.get_values_by_ijk( - np.array([ijk[0]]), np.array([ijk[1]]), np.array([ijk[2]]), base=0 - ) - print(f"Get property value for selected cell {timer.lap_s():.2f}s") - scalar_value = scalar_value[0] if scalar_value is not None else None - propname = f"{prop[0]}-{date[0]}" if date else f"{prop[0]}" - return json.dumps( - { - "x": pos[0], - "y": pos[1], - "z": pos[2], - "i": ijk[0], - "j": ijk[1], - "k": ijk[2], - propname: scalar_value, - }, - indent=2, - ) + if not clickData: + return [""] + if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: + scalar = datamodel.get_init_values(prop[0]) + else: + scalar = datamodel.get_restart_values(prop[0], date[0]) + + pos = clickData["worldPosition"] + pos[2] = pos[2] / zscale - return [""] + timer = PerfTimer() + + cell_id, ijk = datamodel.esg_provider.find_containing_cell(pos) + scalar_value = scalar[cell_id] + + propname = f"{prop[0]}-{date[0]}" if date else f"{prop[0]}" + return json.dumps( + { + "x": pos[0], + "y": pos[1], + "z": pos[2], + "i": ijk[0], + "j": ijk[1], + "k": ijk[2], + propname: float( + scalar_value, + ), + }, + indent=2, + ) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 87ab1c835..2ed591fde 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -23,6 +23,7 @@ class LayoutElements(str, Enum): VTK_GRID_CELLDATA = "vtk-grid-celldata" STORED_CELL_INDICES_HASH = "stored-cell-indices-hash" SELECTED_CELL = "selected-cell" + SHOW_GRID_LINES = "show-grid-lines" class LayoutTitles(str, Enum): @@ -33,6 +34,7 @@ class LayoutTitles(str, Enum): GRID_COLUMNS = "Grid columns" GRID_ROWS = "Grid rows" GRID_LAYERS = "Grid layers" + SHOW_GRID_LINES = "Show grid lines" class PROPERTYTYPE(str, Enum): @@ -77,6 +79,11 @@ def sidebar( wcc.SelectWithLabel( id=get_uuid(LayoutElements.DATES), label=LayoutTitles.DATES ), + wcc.Checklist( + id=get_uuid(LayoutElements.SHOW_GRID_LINES), + options=[LayoutTitles.SHOW_GRID_LINES], + value=[LayoutTitles.SHOW_GRID_LINES], + ), wcc.Slider( label=LayoutTitles.Z_SCALE, id=get_uuid(LayoutElements.Z_SCALE), From 7e42e3738a65530452720aa9f6953b0a69b09960 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 5 Apr 2022 08:40:47 +0200 Subject: [PATCH 07/63] Add back grid --- .../plugins/_eclipse_grid_viewer/_business_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 6e4c97b24..1092d56f6 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -176,7 +176,7 @@ def get_restart_property(self, prop_name: str, prop_date: int) -> np.ndarray: fformat="unrst", name=prop_name, date=prop_date, - # grid=self._xtg_grid, + grid=self._xtg_grid, ) return prop From 0f2359ea03e2d707a2b4856c6e6001fb43b15f09 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 5 Apr 2022 09:06:25 +0200 Subject: [PATCH 08/63] Unhide inactive cells before picking --- .../plugins/_eclipse_grid_viewer/_business_logic.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 1092d56f6..38eb7206d 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -73,7 +73,7 @@ def extract_skin( def find_containing_cell(self, coords): timer = PerfTimer() locator = vtkCellLocator() - locator.SetDataSet(self.esg_grid) + locator.SetDataSet(self.esg_grid.show_cells()) locator.BuildLocator() # containing_cell = locator.FindCell(coords) #Slower and not precise?? # print(f"Containing cell in {timer.lap_s():.2f}") @@ -83,9 +83,7 @@ def find_containing_cell(self, coords): cell_id = mutable(0) sub_id = mutable(0) dist2 = mutable(0.0) - closest_cell = locator.FindClosestPoint( - coords, closest_point, cell, cell_id, sub_id, dist2 - ) + locator.FindClosestPoint(coords, closest_point, cell, cell_id, sub_id, dist2) print(f"Closest cell in {timer.lap_s():.2f}") i = mutable(0) From 3f838db5c1d42b870fe2a3bac794b259686bdffc Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 5 Apr 2022 09:25:15 +0200 Subject: [PATCH 09/63] todo --- .../plugins/_eclipse_grid_viewer/_business_logic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 38eb7206d..2ca32c80e 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -71,6 +71,8 @@ def extract_skin( return polys, points, indices def find_containing_cell(self, coords): + """OBS! OBS! Currently picks the layer above the visualized layer. + Solve by e.g. shifting the z value? Getting cell neighbours?...""" timer = PerfTimer() locator = vtkCellLocator() locator.SetDataSet(self.esg_grid.show_cells()) From a5e6995e54bfe4e591b9948bfd20907aa363f31d Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:30:37 +0200 Subject: [PATCH 10/63] Colorpreset --- .../_eclipse_grid_viewer/_callbacks.py | 7 ++ .../plugins/_eclipse_grid_viewer/_layout.py | 101 +++++++++++------- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 731afbc5a..fffa8b586 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -165,3 +165,10 @@ def _update_click_info(clickData, zscale, prop, date, proptype): }, indent=2, ) + + @callback( + Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "colorMapPreset"), + Input(get_uuid(LayoutElements.COLORMAP), "value"), + ) + def _reset_camera(colormap: str) -> str: + return colormap diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 2ed591fde..7ea078275 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -4,6 +4,7 @@ import dash_vtk import webviz_core_components as wcc from dash import dcc, html +from dash_vtk.utils import presets as colormaps from ._business_logic import ExplicitStructuredGridProvider @@ -24,6 +25,7 @@ class LayoutElements(str, Enum): STORED_CELL_INDICES_HASH = "stored-cell-indices-hash" SELECTED_CELL = "selected-cell" SHOW_GRID_LINES = "show-grid-lines" + COLORMAP = "color-map" class LayoutTitles(str, Enum): @@ -35,6 +37,12 @@ class LayoutTitles(str, Enum): GRID_ROWS = "Grid rows" GRID_LAYERS = "Grid layers" SHOW_GRID_LINES = "Show grid lines" + COLORMAP = "Color map" + GRID_FILTERS = "Grid filters" + COLORS = "Colors" + + +COLORMAPS = ["erdc_rainbow_dark", "Viridis (matplotlib)", "BuRd"] class PROPERTYTYPE(str, Enum): @@ -92,44 +100,63 @@ def sidebar( value=1, step=1, ), - wcc.RangeSlider( - label=LayoutTitles.GRID_COLUMNS, - id=get_uuid(LayoutElements.GRID_COLUMNS), - min=esg_provider.imin, - max=esg_provider.imax, - value=[esg_provider.imin, esg_provider.imax], - step=1, - marks=None, - tooltip={ - "placement": "bottom", - "always_visible": True, - }, - ), - wcc.RangeSlider( - label=LayoutTitles.GRID_ROWS, - id=get_uuid(LayoutElements.GRID_ROWS), - min=esg_provider.jmin, - max=esg_provider.jmax, - value=[esg_provider.jmin, esg_provider.jmax], - step=1, - marks=None, - tooltip={ - "placement": "bottom", - "always_visible": True, - }, + wcc.Selectors( + label=LayoutTitles.GRID_FILTERS, + children=[ + wcc.RangeSlider( + label=LayoutTitles.GRID_COLUMNS, + id=get_uuid(LayoutElements.GRID_COLUMNS), + min=esg_provider.imin, + max=esg_provider.imax, + value=[esg_provider.imin, esg_provider.imax], + step=1, + marks=None, + tooltip={ + "placement": "bottom", + "always_visible": True, + }, + ), + wcc.RangeSlider( + label=LayoutTitles.GRID_ROWS, + id=get_uuid(LayoutElements.GRID_ROWS), + min=esg_provider.jmin, + max=esg_provider.jmax, + value=[esg_provider.jmin, esg_provider.jmax], + step=1, + marks=None, + tooltip={ + "placement": "bottom", + "always_visible": True, + }, + ), + wcc.RangeSlider( + label=LayoutTitles.GRID_LAYERS, + id=get_uuid(LayoutElements.GRID_LAYERS), + min=esg_provider.kmin, + max=esg_provider.kmax, + value=[esg_provider.kmin, esg_provider.kmax], + step=1, + marks=None, + tooltip={ + "placement": "bottom", + "always_visible": True, + }, + ), + ], ), - wcc.RangeSlider( - label=LayoutTitles.GRID_LAYERS, - id=get_uuid(LayoutElements.GRID_LAYERS), - min=esg_provider.kmin, - max=esg_provider.kmax, - value=[esg_provider.kmin, esg_provider.kmax], - step=1, - marks=None, - tooltip={ - "placement": "bottom", - "always_visible": True, - }, + wcc.Selectors( + label=LayoutTitles.COLORS, + children=[ + wcc.Dropdown( + id=get_uuid(LayoutElements.COLORMAP), + options=[ + {"value": colormap, "label": colormap} + for colormap in COLORMAPS + ], + value=COLORMAPS[0], + clearable=False, + ) + ], ), html.Pre(id=get_uuid(LayoutElements.SELECTED_CELL)), ], From df42875807a3556ec08342aa666cf38e9f844140 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:34:24 +0200 Subject: [PATCH 11/63] Only show first k layer on startup --- webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 7ea078275..9a67c5738 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -1,10 +1,9 @@ from enum import Enum from typing import Callable +from dash import dcc, html import dash_vtk import webviz_core_components as wcc -from dash import dcc, html -from dash_vtk.utils import presets as colormaps from ._business_logic import ExplicitStructuredGridProvider @@ -134,7 +133,7 @@ def sidebar( id=get_uuid(LayoutElements.GRID_LAYERS), min=esg_provider.kmin, max=esg_provider.kmax, - value=[esg_provider.kmin, esg_provider.kmax], + value=[esg_provider.kmin, esg_provider.kmin], step=1, marks=None, tooltip={ From ceb5ae4f1b4c0df758010c83e1f917602a3017b4 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 6 Apr 2022 09:37:14 +0200 Subject: [PATCH 12/63] Use base64 encoding --- .../plugins/_eclipse_grid_viewer/_business_logic.py | 9 +++++++-- .../plugins/_eclipse_grid_viewer/_callbacks.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 2ca32c80e..8c91043e5 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -21,6 +21,7 @@ # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter +from dash_vtk.utils.vtk import b64_encode_numpy from webviz_subsurface._utils.perf_timer import PerfTimer @@ -68,7 +69,7 @@ def extract_skin( polys = vtk_to_numpy(polydata.GetPolys().GetData()) points = vtk_to_numpy(polydata.points).ravel() indices = polydata["vtkOriginalCellIds"] - return polys, points, indices + return b64_encode_numpy(polys), b64_encode_numpy(points), indices def find_containing_cell(self, coords): """OBS! OBS! Currently picks the layer above the visualized layer. @@ -77,7 +78,7 @@ def find_containing_cell(self, coords): locator = vtkCellLocator() locator.SetDataSet(self.esg_grid.show_cells()) locator.BuildLocator() - # containing_cell = locator.FindCell(coords) #Slower and not precise?? + # cell_id = locator.FindCell(coords) # Slower and not precise?? # print(f"Containing cell in {timer.lap_s():.2f}") cell = vtkGenericCell() @@ -96,6 +97,10 @@ def find_containing_cell(self, coords): return cell_id, [int(i), int(j), int(k)] + @staticmethod + def array_to_base64(array: np.ndarray) -> str: + return b64_encode_numpy(array) + @property def imin(self) -> int: return 0 diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index fffa8b586..cce42a332 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -81,7 +81,7 @@ def _set_geometry_and_scalar( return ( no_update, no_update, - scalar[cell_indices], + datamodel.esg_provider.array_to_base64(scalar[cell_indices]), [np.nanmin(scalar), np.nanmax(scalar)], no_update, ) @@ -89,7 +89,7 @@ def _set_geometry_and_scalar( return ( polys, points, - scalar[cell_indices], + datamodel.esg_provider.array_to_base64(scalar[cell_indices]), [np.nanmin(scalar), np.nanmax(scalar)], hashed_indices, ) From fdd5e9acf0afa5f5d625a043f63aa53eb0755bc3 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 6 Apr 2022 11:39:15 +0200 Subject: [PATCH 13/63] Use float32 --- .../plugins/_eclipse_grid_viewer/_business_logic.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 8c91043e5..2fa0da046 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -69,7 +69,11 @@ def extract_skin( polys = vtk_to_numpy(polydata.GetPolys().GetData()) points = vtk_to_numpy(polydata.points).ravel() indices = polydata["vtkOriginalCellIds"] - return b64_encode_numpy(polys), b64_encode_numpy(points), indices + return ( + b64_encode_numpy(polys), + b64_encode_numpy(points.astype(np.float32)), + indices, + ) def find_containing_cell(self, coords): """OBS! OBS! Currently picks the layer above the visualized layer. @@ -99,7 +103,7 @@ def find_containing_cell(self, coords): @staticmethod def array_to_base64(array: np.ndarray) -> str: - return b64_encode_numpy(array) + return b64_encode_numpy(array.astype(np.float32)) @property def imin(self) -> int: From 28b533ef11024b913afceef60794904e4ac81811 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Apr 2022 12:26:38 +0200 Subject: [PATCH 14/63] Fix readout --- .../_eclipse_grid_viewer/_business_logic.py | 56 ++++++++++++------- .../_eclipse_grid_viewer/_callbacks.py | 38 ++++++++++--- 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 2fa0da046..0c20b6c1f 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -14,7 +14,7 @@ vtkCellLocator, vtkGenericCell, ) -from vtkmodules.vtkCommonCore import mutable +from vtkmodules.vtkCommonCore import mutable, vtkIdList # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop @@ -47,18 +47,28 @@ def __init__(self, esg_grid: pv.ExplicitStructuredGrid) -> None: def crop( self, irange: List[int], jrange: List[int], krange: List[int] ) -> vtkExplicitStructuredGrid: + """Crops grids within specified ijk ranges. Original cell indices + kept as vtkOriginalCellIds CellArray""" crop_filter = vtkExplicitStructuredGridCrop() crop_filter.SetInputData(self.esg_grid) crop_filter.SetOutputWholeExtent( - irange[0], irange[1], jrange[0], jrange[1], krange[0], krange[1] + irange[0], irange[1] + 1, jrange[0], jrange[1] + 1, krange[0], krange[1] + 1 ) crop_filter.Update() + grid = crop_filter.GetOutput() + timer = PerfTimer() + grid = pv.ExplicitStructuredGrid(grid) + print(f"to pyvista {timer.lap_s()}") return grid def extract_skin( - self, grid: vtkExplicitStructuredGrid = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + self, grid: pv.ExplicitStructuredGrid = None + ) -> Tuple[str, str, np.ndarray]: + """Extracts skin from a provided cropped grid or the entire grid if + no grid is given. + + Returns polydata and indices of original cell ids""" grid = grid if grid is not None else self.esg_grid self.extract_skin_filter.SetInputData(grid) @@ -69,37 +79,43 @@ def extract_skin( polys = vtk_to_numpy(polydata.GetPolys().GetData()) points = vtk_to_numpy(polydata.points).ravel() indices = polydata["vtkOriginalCellIds"] + return ( b64_encode_numpy(polys), b64_encode_numpy(points.astype(np.float32)), indices, ) - def find_containing_cell(self, coords): + def find_closest_cell_ray_to_ray(self, grid, ray): """OBS! OBS! Currently picks the layer above the visualized layer. Solve by e.g. shifting the z value? Getting cell neighbours?...""" timer = PerfTimer() locator = vtkCellLocator() - locator.SetDataSet(self.esg_grid.show_cells()) + locator.SetDataSet(grid) locator.BuildLocator() - # cell_id = locator.FindCell(coords) # Slower and not precise?? - # print(f"Containing cell in {timer.lap_s():.2f}") - - cell = vtkGenericCell() - closest_point = [0.0, 0.0, 0.0] - cell_id = mutable(0) - sub_id = mutable(0) - dist2 = mutable(0.0) - locator.FindClosestPoint(coords, closest_point, cell, cell_id, sub_id, dist2) + + cell_ids = vtkIdList() + tolerance = mutable(0.0) + + # Find the closest cell in the cropped grid + locator.FindCellsAlongLine(ray[0], ray[1], tolerance, cell_ids) + + # Find the cell index in the full grid + relative_cell_id = cell_ids.GetId(0) + absolute_cell_id = grid["vtkOriginalCellIds"][relative_cell_id] + print(f"Closest cell in {timer.lap_s():.2f}") i = mutable(0) j = mutable(0) k = mutable(0) - self.esg_grid.ComputeCellStructuredCoords(cell_id, i, j, k, False) + pcoords = mutable([0, 0, 0]) + + # Find the ijk of the cell in the full grid + self.esg_grid.ComputeCellStructuredCoords(absolute_cell_id, i, j, k, False) print(f"Get ijk in {timer.lap_s():.2f}") - return cell_id, [int(i), int(j), int(k)] + return absolute_cell_id, [int(i), int(j), int(k)] @staticmethod def array_to_base64(array: np.ndarray) -> str: @@ -111,7 +127,7 @@ def imin(self) -> int: @property def imax(self) -> int: - return self.esg_grid.dimensions[0] - 1 + return self.esg_grid.dimensions[0] - 2 @property def jmin(self) -> int: @@ -119,7 +135,7 @@ def jmin(self) -> int: @property def jmax(self) -> int: - return self.esg_grid.dimensions[1] - 1 + return self.esg_grid.dimensions[1] - 2 @property def kmin(self) -> int: @@ -127,7 +143,7 @@ def kmin(self) -> int: @property def kmax(self) -> int: - return self.esg_grid.dimensions[2] - 1 + return self.esg_grid.dimensions[2] - 2 class EclipseGridDataModel: diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index cce42a332..affae01c1 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -132,8 +132,20 @@ def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> floa State(get_uuid(LayoutElements.PROPERTIES), "value"), State(get_uuid(LayoutElements.DATES), "value"), State(get_uuid(LayoutElements.INIT_RESTART), "value"), + State(get_uuid(LayoutElements.GRID_COLUMNS), "value"), + State(get_uuid(LayoutElements.GRID_ROWS), "value"), + State(get_uuid(LayoutElements.GRID_LAYERS), "value"), ) - def _update_click_info(clickData, zscale, prop, date, proptype): + def _update_click_info( + clickData, + zscale, + prop, + date, + proptype, + columns: List[int], + rows: List[int], + layers: List[int], + ): if not clickData: return [""] @@ -142,20 +154,30 @@ def _update_click_info(clickData, zscale, prop, date, proptype): else: scalar = datamodel.get_restart_values(prop[0], date[0]) - pos = clickData["worldPosition"] - pos[2] = pos[2] / zscale + cropped_grid = datamodel.esg_provider.crop(columns, rows, layers) - timer = PerfTimer() + # Getting position and ray below mouse position + coords = clickData["worldPosition"] + ray = clickData["ray"] + # Remove z-scaling from points + coords[2] = coords[2] / zscale + ray[0][2] = ray[0][2] / zscale + ray[1][2] = ray[1][2] / zscale + + # Find the cell index and i,j,k of the closest cell the ray intersects + cell_id, ijk = datamodel.esg_provider.find_closest_cell_ray_to_ray( + cropped_grid, ray + ) - cell_id, ijk = datamodel.esg_provider.find_containing_cell(pos) + # Get the scalar value of the cell index scalar_value = scalar[cell_id] propname = f"{prop[0]}-{date[0]}" if date else f"{prop[0]}" return json.dumps( { - "x": pos[0], - "y": pos[1], - "z": pos[2], + "x": coords[0], + "y": coords[1], + "z": coords[2], "i": ijk[0], "j": ijk[1], "k": ijk[2], From 34160d70c8b87b17f03d2366bc573edfe36a0207 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Apr 2022 13:06:16 +0200 Subject: [PATCH 15/63] Return first ACTIVE cell, not first cell intersected by ray --- .../_eclipse_grid_viewer/_business_logic.py | 14 ++++++++++++-- .../plugins/_eclipse_grid_viewer/_callbacks.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 0c20b6c1f..33704bf41 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -100,8 +100,18 @@ def find_closest_cell_ray_to_ray(self, grid, ray): # Find the closest cell in the cropped grid locator.FindCellsAlongLine(ray[0], ray[1], tolerance, cell_ids) - # Find the cell index in the full grid - relative_cell_id = cell_ids.GetId(0) + # Find the closest active cell index in the full grid + relative_cell_id = None + for cell_idx in range(0, cell_ids.GetNumberOfIds()): + cell_id = cell_ids.GetId(cell_idx) + if grid["vtkGhostType"][cell_id] == 0: + relative_cell_id = cell_id + break + + # If no cells are found return None + if relative_cell_id is None: + return None, [None, None, None] + absolute_cell_id = grid["vtkOriginalCellIds"][relative_cell_id] print(f"Closest cell in {timer.lap_s():.2f}") diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index affae01c1..239ae2758 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -170,7 +170,7 @@ def _update_click_info( ) # Get the scalar value of the cell index - scalar_value = scalar[cell_id] + scalar_value = scalar[cell_id] if cell_id is not None else np.nan propname = f"{prop[0]}-{date[0]}" if date else f"{prop[0]}" return json.dumps( From bf1be143eac7a3dfd8e6fd3dc08cdb01a06e1191 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Apr 2022 15:01:54 +0200 Subject: [PATCH 16/63] Added pick representation --- .../_eclipse_grid_viewer/_business_logic.py | 4 +- .../_eclipse_grid_viewer/_callbacks.py | 53 ++++++++++++------- .../plugins/_eclipse_grid_viewer/_layout.py | 20 +++++++ 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 33704bf41..5f1cdb6a6 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -87,8 +87,7 @@ def extract_skin( ) def find_closest_cell_ray_to_ray(self, grid, ray): - """OBS! OBS! Currently picks the layer above the visualized layer. - Solve by e.g. shifting the z value? Getting cell neighbours?...""" + """Find the active cell closest to the given ray.""" timer = PerfTimer() locator = vtkCellLocator() locator.SetDataSet(grid) @@ -119,7 +118,6 @@ def find_closest_cell_ray_to_ray(self, grid, ray): i = mutable(0) j = mutable(0) k = mutable(0) - pcoords = mutable([0, 0, 0]) # Find the ijk of the cell in the full grid self.esg_grid.ComputeCellStructuredCoords(absolute_cell_id, i, j, k, False) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 239ae2758..0f358a1d8 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -127,28 +127,39 @@ def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> floa @callback( Output(get_uuid(LayoutElements.SELECTED_CELL), "children"), + Output(get_uuid(LayoutElements.VTK_PICK_SPHERE), "state"), + Output(get_uuid(LayoutElements.VTK_PICK_REPRESENTATION), "actor"), Input(get_uuid(LayoutElements.VTK_VIEW), "clickInfo"), + Input(get_uuid(LayoutElements.ENABLE_PICKING), "value"), + Input(get_uuid(LayoutElements.PROPERTIES), "value"), + Input(get_uuid(LayoutElements.DATES), "value"), + Input(get_uuid(LayoutElements.INIT_RESTART), "value"), State(get_uuid(LayoutElements.Z_SCALE), "value"), - State(get_uuid(LayoutElements.PROPERTIES), "value"), - State(get_uuid(LayoutElements.DATES), "value"), - State(get_uuid(LayoutElements.INIT_RESTART), "value"), State(get_uuid(LayoutElements.GRID_COLUMNS), "value"), State(get_uuid(LayoutElements.GRID_ROWS), "value"), State(get_uuid(LayoutElements.GRID_LAYERS), "value"), + State(get_uuid(LayoutElements.VTK_PICK_REPRESENTATION), "actor"), ) def _update_click_info( clickData, - zscale, + enable_picking, prop, date, proptype, + zscale, columns: List[int], rows: List[int], layers: List[int], + pick_representation_actor: Optional[Dict], ): + pick_representation_actor = ( + pick_representation_actor if pick_representation_actor else {} + ) + if not clickData or not enable_picking: + pick_representation_actor.update({"visibility": False}) + return [""], {}, pick_representation_actor + pick_representation_actor.update({"visibility": True}) - if not clickData: - return [""] if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: scalar = datamodel.get_init_values(prop[0]) else: @@ -173,19 +184,23 @@ def _update_click_info( scalar_value = scalar[cell_id] if cell_id is not None else np.nan propname = f"{prop[0]}-{date[0]}" if date else f"{prop[0]}" - return json.dumps( - { - "x": coords[0], - "y": coords[1], - "z": coords[2], - "i": ijk[0], - "j": ijk[1], - "k": ijk[2], - propname: float( - scalar_value, - ), - }, - indent=2, + return ( + json.dumps( + { + "x": coords[0], + "y": coords[1], + "z": coords[2], + "i": ijk[0], + "j": ijk[1], + "k": ijk[2], + propname: float( + scalar_value, + ), + }, + indent=2, + ), + {"center": clickData["worldPosition"], "radius": 100}, + pick_representation_actor, ) @callback( diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 9a67c5738..10f6d7141 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -25,6 +25,9 @@ class LayoutElements(str, Enum): SELECTED_CELL = "selected-cell" SHOW_GRID_LINES = "show-grid-lines" COLORMAP = "color-map" + ENABLE_PICKING = "enable-picking" + VTK_PICK_REPRESENTATION = "vtk-pick-representation" + VTK_PICK_SPHERE = "vtk-pick-sphere" class LayoutTitles(str, Enum): @@ -39,6 +42,8 @@ class LayoutTitles(str, Enum): COLORMAP = "Color map" GRID_FILTERS = "Grid filters" COLORS = "Colors" + PICKING = "Picking" + ENABLE_PICKING = "Enable picking" COLORMAPS = ["erdc_rainbow_dark", "Viridis (matplotlib)", "BuRd"] @@ -157,6 +162,11 @@ def sidebar( ) ], ), + wcc.Checklist( + id=get_uuid(LayoutElements.ENABLE_PICKING), + options=[LayoutTitles.ENABLE_PICKING], + value=[LayoutTitles.ENABLE_PICKING], + ), html.Pre(id=get_uuid(LayoutElements.SELECTED_CELL)), ], ) @@ -188,5 +198,15 @@ def vtk_view(get_uuid: Callable) -> dash_vtk.View: ], property={"edgeVisibility": True}, ), + dash_vtk.GeometryRepresentation( + id=get_uuid(LayoutElements.VTK_PICK_REPRESENTATION), + actor={"visibility": False}, + children=[ + dash_vtk.Algorithm( + id=get_uuid(LayoutElements.VTK_PICK_SPHERE), + vtkClass="vtkSphereSource", + ) + ], + ), ], ) From 901079d72e23560848fa3eaf85b78a856e17da50 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Apr 2022 15:08:30 +0200 Subject: [PATCH 17/63] Use scaled position for glyph placement --- webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 0f358a1d8..17e1375c2 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -168,13 +168,14 @@ def _update_click_info( cropped_grid = datamodel.esg_provider.crop(columns, rows, layers) # Getting position and ray below mouse position - coords = clickData["worldPosition"] + coords = clickData["worldPosition"].copy() + print(coords) ray = clickData["ray"] # Remove z-scaling from points coords[2] = coords[2] / zscale ray[0][2] = ray[0][2] / zscale ray[1][2] = ray[1][2] / zscale - + print(clickData["worldPosition"]) # Find the cell index and i,j,k of the closest cell the ray intersects cell_id, ijk = datamodel.esg_provider.find_closest_cell_ray_to_ray( cropped_grid, ray From 911c810722944ab3bf2282bc63a0dea8346b46f2 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Apr 2022 15:21:05 +0200 Subject: [PATCH 18/63] lint --- .../_eclipse_grid_viewer/_business_logic.py | 24 +++++++------- .../_eclipse_grid_viewer/_callbacks.py | 31 ++++++++++--------- .../plugins/_eclipse_grid_viewer/_layout.py | 2 +- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 5f1cdb6a6..e2bc54e0e 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -1,27 +1,26 @@ from pathlib import Path -from typing import List, Tuple +from typing import List, Tuple, Optional import numpy as np import pyvista as pv import xtgeo +from dash_vtk.utils.vtk import b64_encode_numpy # pylint: disable=no-name-in-module, import-error from vtk.util.numpy_support import vtk_to_numpy +from vtkmodules.vtkCommonCore import mutable, vtkIdList # pylint: disable=no-name-in-module, from vtkmodules.vtkCommonDataModel import ( - vtkExplicitStructuredGrid, vtkCellLocator, - vtkGenericCell, + vtkExplicitStructuredGrid, ) -from vtkmodules.vtkCommonCore import mutable, vtkIdList # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter -from dash_vtk.utils.vtk import b64_encode_numpy from webviz_subsurface._utils.perf_timer import PerfTimer @@ -86,7 +85,9 @@ def extract_skin( indices, ) - def find_closest_cell_ray_to_ray(self, grid, ray): + def find_closest_cell_ray_to_ray( + self, grid: pv.ExplicitStructuredGrid, ray: List[float] + ) -> Tuple[Optional[int], List[Optional[int]]]: """Find the active cell closest to the given ray.""" timer = PerfTimer() locator = vtkCellLocator() @@ -195,15 +196,16 @@ def restart_names(self) -> List[str]: def restart_dates(self) -> List[str]: return self._restart_dates - def get_init_property(self, prop_name: str) -> np.ndarray: + def get_init_property(self, prop_name: str) -> xtgeo.GridProperty: prop = xtgeo.gridproperty_from_file( self._init_file, fformat="init", name=prop_name, grid=self._xtg_grid ) return prop - def get_restart_property(self, prop_name: str, prop_date: int) -> np.ndarray: - timer = PerfTimer() + def get_restart_property( + self, prop_name: str, prop_date: int + ) -> xtgeo.GridProperty: prop = xtgeo.gridproperty_from_file( self._restart_file, fformat="unrst", @@ -213,10 +215,10 @@ def get_restart_property(self, prop_name: str, prop_date: int) -> np.ndarray: ) return prop - def get_init_values(self, prop_name: str): + def get_init_values(self, prop_name: str) -> np.ndarray: prop = self.get_init_property(prop_name) return prop.get_npvalues1d(order="F").ravel() - def get_restart_values(self, prop_name: str, prop_date: int): + def get_restart_values(self, prop_name: str, prop_date: int) -> np.ndarray: prop = self.get_restart_property(prop_name, prop_date) return prop.get_npvalues1d(order="F").ravel() diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 17e1375c2..ce64154ae 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -140,24 +140,25 @@ def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> floa State(get_uuid(LayoutElements.GRID_LAYERS), "value"), State(get_uuid(LayoutElements.VTK_PICK_REPRESENTATION), "actor"), ) + # pylint: disable = too-many-locals, too-many-arguments def _update_click_info( - clickData, - enable_picking, - prop, - date, - proptype, - zscale, + click_data: Optional[Dict], + enable_picking: Optional[str], + prop: List[str], + date: List[int], + proptype: str, + zscale: float, columns: List[int], rows: List[int], layers: List[int], pick_representation_actor: Optional[Dict], - ): + ) -> Tuple[str, Dict[str, Any], Dict[str, bool]]: pick_representation_actor = ( pick_representation_actor if pick_representation_actor else {} ) - if not clickData or not enable_picking: + if not click_data or not enable_picking: pick_representation_actor.update({"visibility": False}) - return [""], {}, pick_representation_actor + return "", {}, pick_representation_actor pick_representation_actor.update({"visibility": True}) if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: @@ -168,14 +169,14 @@ def _update_click_info( cropped_grid = datamodel.esg_provider.crop(columns, rows, layers) # Getting position and ray below mouse position - coords = clickData["worldPosition"].copy() - print(coords) - ray = clickData["ray"] + coords = click_data["worldPosition"].copy() + + ray = click_data["ray"] # Remove z-scaling from points coords[2] = coords[2] / zscale ray[0][2] = ray[0][2] / zscale ray[1][2] = ray[1][2] / zscale - print(clickData["worldPosition"]) + # Find the cell index and i,j,k of the closest cell the ray intersects cell_id, ijk = datamodel.esg_provider.find_closest_cell_ray_to_ray( cropped_grid, ray @@ -200,7 +201,7 @@ def _update_click_info( }, indent=2, ), - {"center": clickData["worldPosition"], "radius": 100}, + {"center": click_data["worldPosition"], "radius": 100}, pick_representation_actor, ) @@ -208,5 +209,5 @@ def _update_click_info( Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "colorMapPreset"), Input(get_uuid(LayoutElements.COLORMAP), "value"), ) - def _reset_camera(colormap: str) -> str: + def _set_colormap(colormap: str) -> str: return colormap diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 10f6d7141..efb29a89e 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -1,9 +1,9 @@ from enum import Enum from typing import Callable -from dash import dcc, html import dash_vtk import webviz_core_components as wcc +from dash import dcc, html from ._business_logic import ExplicitStructuredGridProvider From f9a7e0afc163f488ab544e033b7210f22f665951 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 7 Apr 2022 15:55:01 +0200 Subject: [PATCH 19/63] Show axes --- .../_eclipse_grid_viewer/_business_logic.py | 7 +--- .../_eclipse_grid_viewer/_callbacks.py | 13 +++++-- .../plugins/_eclipse_grid_viewer/_layout.py | 38 ++++++++++++++----- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index e2bc54e0e..5002eab8b 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Tuple, Optional +from typing import List, Optional, Tuple import numpy as np import pyvista as pv @@ -11,10 +11,7 @@ from vtkmodules.vtkCommonCore import mutable, vtkIdList # pylint: disable=no-name-in-module, -from vtkmodules.vtkCommonDataModel import ( - vtkCellLocator, - vtkExplicitStructuredGrid, -) +from vtkmodules.vtkCommonDataModel import vtkCellLocator, vtkExplicitStructuredGrid # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index ce64154ae..feb9e7a92 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -1,7 +1,7 @@ import hashlib +import json from time import time from typing import Any, Callable, Dict, List, Optional, Tuple -import json import numpy as np from dash import Input, Output, State, callback, no_update @@ -96,13 +96,18 @@ def _set_geometry_and_scalar( @callback( Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), + Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "showCubeAxes"), Input(get_uuid(LayoutElements.Z_SCALE), "value"), + Input(get_uuid(LayoutElements.SHOW_AXES), "value"), State(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), ) - def _set_representation_actor(z_scale: int, actor: Optional[dict]) -> dict: + def _set_representation_actor( + z_scale: int, axes_is_on: List[str], actor: Optional[dict] + ) -> Tuple[dict, bool]: + show_axes = bool(z_scale == 1 and axes_is_on) actor = actor if actor else {} actor.update({"scale": (1, 1, z_scale)}) - return actor + return actor, show_axes @callback( Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "property"), @@ -110,7 +115,7 @@ def _set_representation_actor(z_scale: int, actor: Optional[dict]) -> dict: State(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "property"), ) def _set_representation_property( - show_grid_lines: int, properties: Optional[dict] + show_grid_lines: List[str], properties: Optional[dict] ) -> dict: properties = properties if properties else {} properties.update({"edgeVisibility": bool(show_grid_lines)}) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index efb29a89e..a13e0eef6 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -28,6 +28,7 @@ class LayoutElements(str, Enum): ENABLE_PICKING = "enable-picking" VTK_PICK_REPRESENTATION = "vtk-pick-representation" VTK_PICK_SPHERE = "vtk-pick-sphere" + SHOW_AXES = "show-axes" class LayoutTitles(str, Enum): @@ -43,7 +44,8 @@ class LayoutTitles(str, Enum): GRID_FILTERS = "Grid filters" COLORS = "Colors" PICKING = "Picking" - ENABLE_PICKING = "Enable picking" + ENABLE_PICKING = "Enable readout from picked cell" + SHOW_AXES = "Show axes" COLORMAPS = ["erdc_rainbow_dark", "Viridis (matplotlib)", "BuRd"] @@ -91,11 +93,6 @@ def sidebar( wcc.SelectWithLabel( id=get_uuid(LayoutElements.DATES), label=LayoutTitles.DATES ), - wcc.Checklist( - id=get_uuid(LayoutElements.SHOW_GRID_LINES), - options=[LayoutTitles.SHOW_GRID_LINES], - value=[LayoutTitles.SHOW_GRID_LINES], - ), wcc.Slider( label=LayoutTitles.Z_SCALE, id=get_uuid(LayoutElements.Z_SCALE), @@ -162,10 +159,30 @@ def sidebar( ) ], ), - wcc.Checklist( - id=get_uuid(LayoutElements.ENABLE_PICKING), - options=[LayoutTitles.ENABLE_PICKING], - value=[LayoutTitles.ENABLE_PICKING], + wcc.Selectors( + label="Options", + children=[ + wcc.Checklist( + id=get_uuid(LayoutElements.SHOW_AXES), + options=[LayoutTitles.SHOW_AXES], + value=[LayoutTitles.SHOW_AXES], + ), + wcc.Checklist( + id=get_uuid(LayoutElements.SHOW_GRID_LINES), + options=[LayoutTitles.SHOW_GRID_LINES], + value=[LayoutTitles.SHOW_GRID_LINES], + ), + ], + ), + wcc.Selectors( + label="Readout", + children=[ + wcc.Checklist( + id=get_uuid(LayoutElements.ENABLE_PICKING), + options=[LayoutTitles.ENABLE_PICKING], + value=[LayoutTitles.ENABLE_PICKING], + ) + ], ), html.Pre(id=get_uuid(LayoutElements.SELECTED_CELL)), ], @@ -180,6 +197,7 @@ def vtk_view(get_uuid: Callable) -> dash_vtk.View: children=[ dash_vtk.GeometryRepresentation( id=get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), + showCubeAxes=True, children=[ dash_vtk.PolyData( id=get_uuid(LayoutElements.VTK_GRID_POLYDATA), From e5bf5e7d6676dd4ae36115c81068f9c4afc4140f Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:17:21 +0200 Subject: [PATCH 20/63] FindsCellsAlongLine returned cells in random order, use IntersectWithLine instead. Added tests. Cleanup. --- .../test_eclipse_grid_viewer/__init__.py | 0 .../test_eclipse_grid_viewer/_utils.py | 34 ++++ .../test_explicit_structured_grid_accessor.py | 92 ++++++++++ .../_eclipse_grid_viewer/_business_logic.py | 138 +-------------- .../_eclipse_grid_viewer/_callbacks.py | 17 +- .../_explicit_structured_grid_accessor.py | 160 ++++++++++++++++++ .../plugins/_eclipse_grid_viewer/_layout.py | 26 +-- .../plugins/_eclipse_grid_viewer/_plugin.py | 2 +- 8 files changed, 313 insertions(+), 156 deletions(-) create mode 100644 tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/__init__.py create mode 100644 tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py create mode 100644 tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py diff --git a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/__init__.py b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py new file mode 100644 index 000000000..8c93baa56 --- /dev/null +++ b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py @@ -0,0 +1,34 @@ +import numpy as np +import pyvista as pv + + +def create_explicit_structured_grid( + ni: int, nj: int, nk: int, si: float, sj: float, sk: float +) -> pv.ExplicitStructuredGrid: + + si = float(si) + sj = float(sj) + sk = float(sk) + + # create raw coordinate grid + grid_ijk = np.mgrid[ + : (ni + 1) * si : si, : (nj + 1) * sj : sj, : (nk + 1) * sk : sk + ] + + # repeat array along each Cartesian axis for connectivity + for axis in range(1, 4): + grid_ijk = grid_ijk.repeat(2, axis=axis) + + # slice off unnecessarily doubled edge coordinates + grid_ijk = grid_ijk[:, 1:-1, 1:-1, 1:-1] + + # reorder and reshape to VTK order + corners = grid_ijk.transpose().reshape(-1, 3) + + dims = np.array([ni, nj, nk]) + 1 + + grid = pv.ExplicitStructuredGrid(dims, corners) + grid = grid.compute_connectivity() + grid.ComputeFacesConnectivityFlagsArray() + + return grid diff --git a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py new file mode 100644 index 000000000..10e60deef --- /dev/null +++ b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py @@ -0,0 +1,92 @@ +import pytest + +from vtkmodules.vtkCommonDataModel import vtkCellLocator +from vtkmodules.vtkCommonCore import vtkIdList +import pyvista as pv + +from webviz_subsurface.plugins._eclipse_grid_viewer._explicit_structured_grid_accessor import ( + ExplicitStructuredGridAccessor, +) +from ._utils import create_explicit_structured_grid + + +ES_GRID_ACCESSOR = ExplicitStructuredGridAccessor( + create_explicit_structured_grid(5, 4, 3, 20.0, 10.0, 5.0) +) + +CROP_FIRST_CELL = [0, 0], [0, 0], [0, 0] +EXPECTED_FIRST_CELL_ORIGINAL_INDEX = [0] +CROP_LAST_CELL = [5, 5], [4, 4], [3, 3] +EXPECTED_LAST_CELL_ORIGINAL_INDEX = [59] +CROP_BOX = [2, 3], [2, 3], [1, 2] +EXPECTED_CROP_BOX_ORIGINAL_INDEX = [32, 33, 37, 38, 52, 53, 57, 58] + + +@pytest.mark.parametrize( + "crop_range, expected_cells", + [ + (CROP_FIRST_CELL, EXPECTED_FIRST_CELL_ORIGINAL_INDEX), + (CROP_LAST_CELL, EXPECTED_LAST_CELL_ORIGINAL_INDEX), + (CROP_BOX, EXPECTED_CROP_BOX_ORIGINAL_INDEX), + ], +) +def test_crop(crop_range, expected_cells) -> None: + cropped_grid = ES_GRID_ACCESSOR.crop(*crop_range) + assert isinstance(cropped_grid, pv.ExplicitStructuredGrid) + assert "vtkOriginalCellIds" in cropped_grid.array_names + assert set(cropped_grid["vtkOriginalCellIds"]) == set(expected_cells) + _polys, _points, indices = ES_GRID_ACCESSOR.extract_skin(cropped_grid) + assert set(indices) == set(expected_cells) + + +RAY_FROM_TOP = [ + [50, 15, 15], + [50, 15, -5], +] +RAY_FROM_BOTTOM = [ + [50, 15, -5], + [50, 15, 20], +] +RAY_FROM_I = [[50, -7, 13], [50, 45, 13]] +RAY_FROM_J = [[-12, 5, 13], [110, 5, 13]] + + +@pytest.mark.parametrize( + "ray, expected_cell_id_and_ijk", + [ + (RAY_FROM_TOP, (47, [2, 1, 2])), + (RAY_FROM_BOTTOM, (7, [2, 1, 0])), + (RAY_FROM_I, (42, [2, 0, 2])), + (RAY_FROM_J, (40, [0, 0, 2])), + ], +) +def test_find_closest_cell_to_ray(ray, expected_cell_id_and_ijk) -> None: + cell_ijk = ES_GRID_ACCESSOR.find_closest_cell_to_ray(ES_GRID_ACCESSOR.es_grid, ray) + assert cell_ijk == expected_cell_id_and_ijk + + +@pytest.mark.parametrize( + "ray, expected_cell_id_and_ijk", + [ + (RAY_FROM_TOP, (27, [2, 1, 1])), + (RAY_FROM_BOTTOM, (7, [2, 1, 0])), + (RAY_FROM_I, (52, [2, 2, 2])), + (RAY_FROM_J, (43, [3, 0, 2])), + ], +) +def test_find_closest_cell_to_ray_with_blanked_cells( + ray, expected_cell_id_and_ijk +) -> None: + grid = ES_GRID_ACCESSOR.es_grid.copy() + + cellLocator = vtkCellLocator() + cellLocator.SetDataSet(grid) + cellLocator.BuildLocator() + cellIds = vtkIdList() + cellLocator.FindCellsAlongLine((6.0, 6.0, 12.0), (67.0, 12.0, 12.0), 0.001, cellIds) + for i in range(cellIds.GetNumberOfIds()): + id = cellIds.GetId(i) + grid.BlankCell(id) + + cell_ijk = ES_GRID_ACCESSOR.find_closest_cell_to_ray(grid, ray) + assert cell_ijk == expected_cell_id_and_ijk diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 5002eab8b..9f5e3f4b1 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -1,25 +1,12 @@ from pathlib import Path -from typing import List, Optional, Tuple +from typing import List import numpy as np import pyvista as pv import xtgeo -from dash_vtk.utils.vtk import b64_encode_numpy - -# pylint: disable=no-name-in-module, import-error -from vtk.util.numpy_support import vtk_to_numpy -from vtkmodules.vtkCommonCore import mutable, vtkIdList - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkCommonDataModel import vtkCellLocator, vtkExplicitStructuredGrid - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter from webviz_subsurface._utils.perf_timer import PerfTimer +from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor def xtgeo_grid_to_explicit_structured_grid( @@ -29,129 +16,12 @@ def xtgeo_grid_to_explicit_structured_grid( corners[:, 2] *= -1 esg_grid = pv.ExplicitStructuredGrid(dims, corners) esg_grid = esg_grid.compute_connectivity() - esg_grid.ComputeFacesConnectivityFlagsArray() + # esg_grid.ComputeFacesConnectivityFlagsArray() esg_grid = esg_grid.hide_cells(inactive) # esg_grid.flip_z(inplace=True) return esg_grid -class ExplicitStructuredGridProvider: - def __init__(self, esg_grid: pv.ExplicitStructuredGrid) -> None: - self.esg_grid = esg_grid - self.extract_skin_filter = vtkExplicitStructuredGridSurfaceFilter() - - def crop( - self, irange: List[int], jrange: List[int], krange: List[int] - ) -> vtkExplicitStructuredGrid: - """Crops grids within specified ijk ranges. Original cell indices - kept as vtkOriginalCellIds CellArray""" - crop_filter = vtkExplicitStructuredGridCrop() - crop_filter.SetInputData(self.esg_grid) - crop_filter.SetOutputWholeExtent( - irange[0], irange[1] + 1, jrange[0], jrange[1] + 1, krange[0], krange[1] + 1 - ) - crop_filter.Update() - - grid = crop_filter.GetOutput() - timer = PerfTimer() - grid = pv.ExplicitStructuredGrid(grid) - print(f"to pyvista {timer.lap_s()}") - return grid - - def extract_skin( - self, grid: pv.ExplicitStructuredGrid = None - ) -> Tuple[str, str, np.ndarray]: - """Extracts skin from a provided cropped grid or the entire grid if - no grid is given. - - Returns polydata and indices of original cell ids""" - grid = grid if grid is not None else self.esg_grid - - self.extract_skin_filter.SetInputData(grid) - self.extract_skin_filter.PassThroughCellIdsOn() - self.extract_skin_filter.Update() - polydata = self.extract_skin_filter.GetOutput() - polydata = pv.PolyData(polydata) - polys = vtk_to_numpy(polydata.GetPolys().GetData()) - points = vtk_to_numpy(polydata.points).ravel() - indices = polydata["vtkOriginalCellIds"] - - return ( - b64_encode_numpy(polys), - b64_encode_numpy(points.astype(np.float32)), - indices, - ) - - def find_closest_cell_ray_to_ray( - self, grid: pv.ExplicitStructuredGrid, ray: List[float] - ) -> Tuple[Optional[int], List[Optional[int]]]: - """Find the active cell closest to the given ray.""" - timer = PerfTimer() - locator = vtkCellLocator() - locator.SetDataSet(grid) - locator.BuildLocator() - - cell_ids = vtkIdList() - tolerance = mutable(0.0) - - # Find the closest cell in the cropped grid - locator.FindCellsAlongLine(ray[0], ray[1], tolerance, cell_ids) - - # Find the closest active cell index in the full grid - relative_cell_id = None - for cell_idx in range(0, cell_ids.GetNumberOfIds()): - cell_id = cell_ids.GetId(cell_idx) - if grid["vtkGhostType"][cell_id] == 0: - relative_cell_id = cell_id - break - - # If no cells are found return None - if relative_cell_id is None: - return None, [None, None, None] - - absolute_cell_id = grid["vtkOriginalCellIds"][relative_cell_id] - - print(f"Closest cell in {timer.lap_s():.2f}") - - i = mutable(0) - j = mutable(0) - k = mutable(0) - - # Find the ijk of the cell in the full grid - self.esg_grid.ComputeCellStructuredCoords(absolute_cell_id, i, j, k, False) - print(f"Get ijk in {timer.lap_s():.2f}") - - return absolute_cell_id, [int(i), int(j), int(k)] - - @staticmethod - def array_to_base64(array: np.ndarray) -> str: - return b64_encode_numpy(array.astype(np.float32)) - - @property - def imin(self) -> int: - return 0 - - @property - def imax(self) -> int: - return self.esg_grid.dimensions[0] - 2 - - @property - def jmin(self) -> int: - return 0 - - @property - def jmax(self) -> int: - return self.esg_grid.dimensions[1] - 2 - - @property - def kmin(self) -> int: - return 0 - - @property - def kmax(self) -> int: - return self.esg_grid.dimensions[2] - 2 - - class EclipseGridDataModel: def __init__( self, @@ -172,7 +42,7 @@ def __init__( timer = PerfTimer() print("Converting egrid to VTK ExplicitStructuredGrid") - self.esg_provider = ExplicitStructuredGridProvider( + self.esg_accessor = ExplicitStructuredGridAccessor( xtgeo_grid_to_explicit_structured_grid(self._xtg_grid) ) print(f"Conversion complete in : {timer.lap_s():.2f}s") diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index feb9e7a92..eb1f9a2a0 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -5,6 +5,7 @@ import numpy as np from dash import Input, Output, State, callback, no_update +from dash_vtk.utils.vtk import b64_encode_numpy from webviz_subsurface._utils.perf_timer import PerfTimer @@ -69,8 +70,8 @@ def _set_geometry_and_scalar( scalar = datamodel.get_restart_values(prop[0], date[0]) print(f"Reading scalar from file in {timer.lap_s():.2f}s") - cropped_grid = datamodel.esg_provider.crop(columns, rows, layers) - polys, points, cell_indices = datamodel.esg_provider.extract_skin(cropped_grid) + cropped_grid = datamodel.esg_accessor.crop(columns, rows, layers) + polys, points, cell_indices = datamodel.esg_accessor.extract_skin(cropped_grid) print(f"Extracting cropped geometry in {timer.lap_s():.2f}s") # Storing hash of cell indices client side to control if only scalar should be updated @@ -81,15 +82,15 @@ def _set_geometry_and_scalar( return ( no_update, no_update, - datamodel.esg_provider.array_to_base64(scalar[cell_indices]), + b64_encode_numpy(scalar[cell_indices].astype(np.float32)), [np.nanmin(scalar), np.nanmax(scalar)], no_update, ) return ( - polys, - points, - datamodel.esg_provider.array_to_base64(scalar[cell_indices]), + b64_encode_numpy(polys.astype(np.float32)), + b64_encode_numpy(points.astype(np.float32)), + b64_encode_numpy(scalar[cell_indices].astype(np.float32)), [np.nanmin(scalar), np.nanmax(scalar)], hashed_indices, ) @@ -171,7 +172,7 @@ def _update_click_info( else: scalar = datamodel.get_restart_values(prop[0], date[0]) - cropped_grid = datamodel.esg_provider.crop(columns, rows, layers) + cropped_grid = datamodel.esg_accessor.crop(columns, rows, layers) # Getting position and ray below mouse position coords = click_data["worldPosition"].copy() @@ -183,7 +184,7 @@ def _update_click_info( ray[1][2] = ray[1][2] / zscale # Find the cell index and i,j,k of the closest cell the ray intersects - cell_id, ijk = datamodel.esg_provider.find_closest_cell_ray_to_ray( + cell_id, ijk = datamodel.esg_accessor.find_closest_cell_to_ray( cropped_grid, ray ) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py new file mode 100644 index 000000000..c2e73b984 --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py @@ -0,0 +1,160 @@ +from typing import List, Optional, Tuple + +import numpy as np + +# pylint: disable=no-name-in-module, import-error +from vtk.util.numpy_support import vtk_to_numpy +from vtkmodules.vtkCommonCore import reference, vtkIdList + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkCommonDataModel import ( + vtkCellLocator, + vtkExplicitStructuredGrid, + vtkGenericCell, +) + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter + +import pyvista as pv + +from webviz_subsurface._utils.perf_timer import PerfTimer + + +class ExplicitStructuredGridAccessor: + def __init__(self, es_grid: pv.ExplicitStructuredGrid) -> None: + self.es_grid = es_grid + self.extract_skin_filter = ( + vtkExplicitStructuredGridSurfaceFilter() + ) # Is this thread safe? + + def crop( + self, irange: List[int], jrange: List[int], krange: List[int] + ) -> vtkExplicitStructuredGrid: + """Crops grids within specified ijk ranges. Original cell indices + kept as vtkOriginalCellIds CellArray""" + crop_filter = vtkExplicitStructuredGridCrop() + crop_filter.SetInputData(self.es_grid) + crop_filter.SetOutputWholeExtent( + irange[0], irange[1] + 1, jrange[0], jrange[1] + 1, krange[0], krange[1] + 1 + ) + crop_filter.Update() + + grid = crop_filter.GetOutput() + timer = PerfTimer() + grid = pv.ExplicitStructuredGrid(grid) + print(f"to pyvista {timer.lap_s()}") + return grid + + def extract_skin( + self, grid: pv.ExplicitStructuredGrid = None + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Extracts skin from a provided cropped grid or the entire grid if + no grid is given. + + Returns polydata and indices of original cell ids""" + grid = grid if grid is not None else self.es_grid + + self.extract_skin_filter.SetInputData(grid) + self.extract_skin_filter.PassThroughCellIdsOn() + self.extract_skin_filter.Update() + polydata = self.extract_skin_filter.GetOutput() + polydata = pv.PolyData(polydata) + polys = vtk_to_numpy(polydata.GetPolys().GetData()) + points = vtk_to_numpy(polydata.GetPoints().GetData()).ravel() + indices = polydata["vtkOriginalCellIds"] + + return ( + polys, + points.astype(np.float32), + indices, + ) + + def find_closest_cell_to_ray( + self, grid: pv.ExplicitStructuredGrid, ray: List[float] + ) -> Tuple[Optional[int], List[Optional[int]]]: + """Find the active cell closest to the given ray.""" + timer = PerfTimer() + locator = vtkCellLocator() + locator.SetDataSet(grid) + locator.BuildLocator() + + cell_ids = vtkIdList() + tolerance = reference(0.0) + + # # Find the cells intersected by the ray. (Ordered by near to far????) + # Apparently not! + + # locator.FindCellsAlongLine(ray[0], ray[1], tolerance, cell_ids) + + # We want the closest non-ghost(active) cell + # Check if ghost array is present and return first non-ghost cell + # if "vtkGhostType" in grid.array_names: + # for cell_idx in range(cell_ids.GetNumberOfIds()): + # cell_id = cell_ids.GetId(cell_idx) + # if grid["vtkGhostType"][cell_id] == 0: + # relative_cell_id = cell_id + # break + # else: + # relative_cell_id = cell_ids.GetId(0) + # for cell_idx in range(cell_ids.GetNumberOfIds()): + # print("test", cell_idx) + # print(cell_ids.GetId(cell_idx)) + # # If no cells are found return None + # if relative_cell_id is None: + # return None, [None, None, None] + + t = reference(0) + x = np.array([0, 0, 0]) + _pcoords = np.array([0, 0, 0]) + _subId = reference(0) + cell_id = reference(0) + _cell = vtkGenericCell() + + locator.IntersectWithLine( + ray[0], ray[1], tolerance, t, x, _pcoords, _subId, cell_id, _cell + ) + + # # Check if an array with OriginalCellIds is present, and if so use + # # that as the cell index, if not assume the grid is not cropped. + if "vtkOriginalCellIds" in grid.array_names: + cell_id = grid["vtkOriginalCellIds"][cell_id] + + # print(f"Closest cell in {timer.lap_s():.2f}") + + i = reference(0) + j = reference(0) + k = reference(0) + + # Find the ijk of the cell in the full grid + self.es_grid.ComputeCellStructuredCoords(cell_id, i, j, k, False) + print(f"Get ijk in {timer.lap_s():.2f}") + + return cell_id, [int(i), int(j), int(k)] + + @property + def imin(self) -> int: + return 0 + + @property + def imax(self) -> int: + return self.es_grid.dimensions[0] - 2 + + @property + def jmin(self) -> int: + return 0 + + @property + def jmax(self) -> int: + return self.es_grid.dimensions[1] - 2 + + @property + def kmin(self) -> int: + return 0 + + @property + def kmax(self) -> int: + return self.es_grid.dimensions[2] - 2 diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index a13e0eef6..eb6d76413 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -5,7 +5,7 @@ import webviz_core_components as wcc from dash import dcc, html -from ._business_logic import ExplicitStructuredGridProvider +from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor # pylint: disable = too-few-public-methods @@ -63,12 +63,12 @@ class LayoutStyle: def plugin_main_layout( - get_uuid: Callable, esg_provider: ExplicitStructuredGridProvider + get_uuid: Callable, esg_accessor: ExplicitStructuredGridAccessor ) -> wcc.FlexBox: return wcc.FlexBox( children=[ - sidebar(get_uuid=get_uuid, esg_provider=esg_provider), + sidebar(get_uuid=get_uuid, esg_accessor=esg_accessor), vtk_view(get_uuid=get_uuid), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), ] @@ -76,7 +76,7 @@ def plugin_main_layout( def sidebar( - get_uuid: Callable, esg_provider: ExplicitStructuredGridProvider + get_uuid: Callable, esg_accessor: ExplicitStructuredGridAccessor ) -> wcc.Frame: return wcc.Frame( style=LayoutStyle.SIDEBAR, @@ -107,9 +107,9 @@ def sidebar( wcc.RangeSlider( label=LayoutTitles.GRID_COLUMNS, id=get_uuid(LayoutElements.GRID_COLUMNS), - min=esg_provider.imin, - max=esg_provider.imax, - value=[esg_provider.imin, esg_provider.imax], + min=esg_accessor.imin, + max=esg_accessor.imax, + value=[esg_accessor.imin, esg_accessor.imax], step=1, marks=None, tooltip={ @@ -120,9 +120,9 @@ def sidebar( wcc.RangeSlider( label=LayoutTitles.GRID_ROWS, id=get_uuid(LayoutElements.GRID_ROWS), - min=esg_provider.jmin, - max=esg_provider.jmax, - value=[esg_provider.jmin, esg_provider.jmax], + min=esg_accessor.jmin, + max=esg_accessor.jmax, + value=[esg_accessor.jmin, esg_accessor.jmax], step=1, marks=None, tooltip={ @@ -133,9 +133,9 @@ def sidebar( wcc.RangeSlider( label=LayoutTitles.GRID_LAYERS, id=get_uuid(LayoutElements.GRID_LAYERS), - min=esg_provider.kmin, - max=esg_provider.kmax, - value=[esg_provider.kmin, esg_provider.kmin], + min=esg_accessor.kmin, + max=esg_accessor.kmax, + value=[esg_accessor.kmin, esg_accessor.kmin], step=1, marks=None, tooltip={ diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index a36a123eb..755d0c585 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -34,5 +34,5 @@ def __init__( @property def layout(self) -> wcc.FlexBox: return plugin_main_layout( - get_uuid=self.uuid, esg_provider=self._datamodel.esg_provider + get_uuid=self.uuid, esg_accessor=self._datamodel.esg_accessor ) From ff917e408934fe02d80386f39b386215cc219ae5 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:22:04 +0200 Subject: [PATCH 21/63] lint --- .../_eclipse_grid_viewer/_business_logic.py | 1 + .../_explicit_structured_grid_accessor.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index 9f5e3f4b1..6c5192e8a 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -6,6 +6,7 @@ import xtgeo from webviz_subsurface._utils.perf_timer import PerfTimer + from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py index c2e73b984..a31673930 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py @@ -1,10 +1,11 @@ from typing import List, Optional, Tuple import numpy as np +import pyvista as pv # pylint: disable=no-name-in-module, import-error from vtk.util.numpy_support import vtk_to_numpy -from vtkmodules.vtkCommonCore import reference, vtkIdList +from vtkmodules.vtkCommonCore import reference # , vtkIdList # pylint: disable=no-name-in-module, from vtkmodules.vtkCommonDataModel import ( @@ -19,8 +20,6 @@ # pylint: disable=no-name-in-module, from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter -import pyvista as pv - from webviz_subsurface._utils.perf_timer import PerfTimer @@ -82,7 +81,7 @@ def find_closest_cell_to_ray( locator.SetDataSet(grid) locator.BuildLocator() - cell_ids = vtkIdList() + # cell_ids = vtkIdList() tolerance = reference(0.0) # # Find the cells intersected by the ray. (Ordered by near to far????) @@ -107,15 +106,15 @@ def find_closest_cell_to_ray( # if relative_cell_id is None: # return None, [None, None, None] - t = reference(0) - x = np.array([0, 0, 0]) + _t = reference(0) + _x = np.array([0, 0, 0]) _pcoords = np.array([0, 0, 0]) - _subId = reference(0) + _sub_id = reference(0) cell_id = reference(0) _cell = vtkGenericCell() locator.IntersectWithLine( - ray[0], ray[1], tolerance, t, x, _pcoords, _subId, cell_id, _cell + ray[0], ray[1], tolerance, _t, _x, _pcoords, _sub_id, cell_id, _cell ) # # Check if an array with OriginalCellIds is present, and if so use From 1090f7c659f67a0430b6f81b13605f6a9884ca10 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:47:19 +0200 Subject: [PATCH 22/63] Set test repo. Add temporary dash-vtk tarball. Add scalarbar. [deploy test] --- .github/workflows/subsurface.yml | 4 +-- tmp_dashvtk/dash_vtk-0.0.9.tar.gz | Bin 0 -> 461925 bytes .../_eclipse_grid_viewer/_business_logic.py | 24 +++++++++++++++--- .../plugins/_eclipse_grid_viewer/_layout.py | 1 + .../plugins/_eclipse_grid_viewer/_plugin.py | 5 +++- 5 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 tmp_dashvtk/dash_vtk-0.0.9.tar.gz diff --git a/.github/workflows/subsurface.yml b/.github/workflows/subsurface.yml index 9b93ff1e8..74454435f 100644 --- a/.github/workflows/subsurface.yml +++ b/.github/workflows/subsurface.yml @@ -75,10 +75,10 @@ jobs: env: # If you want the CI to (temporarily) run against your fork of the testdada, # change the value her from "equinor" to your username. - TESTDATA_REPO_OWNER: equinor + TESTDATA_REPO_OWNER: hanskallekleiv # If you want the CI to (temporarily) run against another branch than master, # change the value her from "master" to the relevant branch name. - TESTDATA_REPO_BRANCH: master + TESTDATA_REPO_BRANCH: more-grid run: | git clone --depth 1 --branch $TESTDATA_REPO_BRANCH https://github.com/$TESTDATA_REPO_OWNER/webviz-subsurface-testdata.git # Copy any clientside script to the test folder before running tests diff --git a/tmp_dashvtk/dash_vtk-0.0.9.tar.gz b/tmp_dashvtk/dash_vtk-0.0.9.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..57f3e96a6c2cc558a17997227ece4d70df86ac35 GIT binary patch literal 461925 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

Ljk*%g5K-g3PQowO8 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

vj$s^lzW=CYtB_@u`9nSUkGT%9J{HqV^WfaBSd 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 S(7O+R None: + self._webviz_store: List[Tuple[Callable, List[dict]]] = [ + ( + get_path, + [{"path": path} for path in [egrid_file, init_file, restart_file]], + ) + ] + + @property + def webviz_store(self) -> List[Tuple[Callable, List[dict]]]: + return self._webviz_store + def _get_restart_dates(self) -> List[str]: return xtgeo.GridProperties.scan_dates(self._restart_file, datesonly=True) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index eb6d76413..dc5fc313a 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -198,6 +198,7 @@ def vtk_view(get_uuid: Callable) -> dash_vtk.View: dash_vtk.GeometryRepresentation( id=get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), showCubeAxes=True, + showScalarBar=True, children=[ dash_vtk.PolyData( id=get_uuid(LayoutElements.VTK_GRID_POLYDATA), diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index 755d0c585..2676be8f5 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List +from typing import List, Tuple, Callable, Dict import webviz_core_components as wcc from webviz_config import WebvizPluginABC @@ -36,3 +36,6 @@ def layout(self) -> wcc.FlexBox: return plugin_main_layout( get_uuid=self.uuid, esg_accessor=self._datamodel.esg_accessor ) + + def add_webvizstore(self) -> List[Tuple[Callable, List[Dict]]]: + return self._datamodel.webviz_store From 38a00777fefd7718785ae0d390acac42eec0ed3a Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:49:01 +0200 Subject: [PATCH 23/63] lint --- .../plugins/_eclipse_grid_viewer/_business_logic.py | 2 +- webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py index f8ee3b85d..66ead88c5 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Tuple, Callable +from typing import Callable, List, Tuple import numpy as np import pyvista as pv diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index 2676be8f5..fccb6c4aa 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Tuple, Callable, Dict +from typing import Callable, Dict, List, Tuple import webviz_core_components as wcc from webviz_config import WebvizPluginABC From 1709c98a165a09ab14e8ecde7ab7357cfb67b8b3 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:49:12 +0200 Subject: [PATCH 24/63] [deploy test] From 7955b35778f18467304fc92799d5f436529f3de9 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:57:33 +0200 Subject: [PATCH 25/63] [deploy test] --- .../plugin_tests/test_eclipse_grid_viewer/_utils.py | 2 ++ .../test_explicit_structured_grid_accessor.py | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py index 8c93baa56..0dd0598aa 100644 --- a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py +++ b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/_utils.py @@ -1,3 +1,5 @@ +# pylint: skip-file +# type: ignore import numpy as np import pyvista as pv diff --git a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py index 10e60deef..eb8a2a466 100644 --- a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py +++ b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py @@ -1,14 +1,15 @@ +# pylint: skip-file +# type: ignore import pytest - -from vtkmodules.vtkCommonDataModel import vtkCellLocator -from vtkmodules.vtkCommonCore import vtkIdList import pyvista as pv +from vtkmodules.vtkCommonCore import vtkIdList +from vtkmodules.vtkCommonDataModel import vtkCellLocator from webviz_subsurface.plugins._eclipse_grid_viewer._explicit_structured_grid_accessor import ( ExplicitStructuredGridAccessor, ) -from ._utils import create_explicit_structured_grid +from ._utils import create_explicit_structured_grid ES_GRID_ACCESSOR = ExplicitStructuredGridAccessor( create_explicit_structured_grid(5, 4, 3, 20.0, 10.0, 5.0) From 1cbcd86da5ee33502cfc1bf930f55099b2eb0e00 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 10:04:56 +0200 Subject: [PATCH 26/63] [deploy test] --- .github/workflows/subsurface.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/subsurface.yml b/.github/workflows/subsurface.yml index 74454435f..3e6429aed 100644 --- a/.github/workflows/subsurface.yml +++ b/.github/workflows/subsurface.yml @@ -51,6 +51,7 @@ jobs: fi pip install "werkzeug<2.1" # ...while waiting for https://github.com/plotly/dash/issues/1992 pip install . + pip install ./tmp_dashvtk/dash_vtk-0.0.9.tar.gz pip install --pre --upgrade webviz-config webviz-core-components webviz-subsurface-components # Testing against our latest release (including pre-releases) - name: 📦 Install test dependencies From ca8364b2e60438917957fec22bf59d0353337ebd Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 10:33:08 +0200 Subject: [PATCH 27/63] [deploy test] --- .github/workflows/subsurface.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/subsurface.yml b/.github/workflows/subsurface.yml index 3e6429aed..d705fdfb0 100644 --- a/.github/workflows/subsurface.yml +++ b/.github/workflows/subsurface.yml @@ -82,11 +82,11 @@ jobs: TESTDATA_REPO_BRANCH: more-grid run: | git clone --depth 1 --branch $TESTDATA_REPO_BRANCH https://github.com/$TESTDATA_REPO_OWNER/webviz-subsurface-testdata.git - # Copy any clientside script to the test folder before running tests - mkdir ./tests/assets && cp ./webviz_subsurface/_assets/js/* ./tests/assets - pytest ./tests --headless --forked --testdata-folder ./webviz-subsurface-testdata - rm -rf ./tests/assets - webviz docs --portable ./docs_build --skip-open + # # Copy any clientside script to the test folder before running tests + # mkdir ./tests/assets && cp ./webviz_subsurface/_assets/js/* ./tests/assets + # pytest ./tests --headless --forked --testdata-folder ./webviz-subsurface-testdata + # rm -rf ./tests/assets + # webviz docs --portable ./docs_build --skip-open - name: 🐳 Build Docker example image if: matrix.python-version != '3.7' # https://github.com/statsmodels/statsmodels/issues/8110 From 6c0cab27ea0d6df887195ddbc851ca5a8ce6fad1 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 11:22:09 +0200 Subject: [PATCH 28/63] Do not clear pick data if clicking outside the representation --- webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index eb1f9a2a0..955ce8cda 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -162,7 +162,9 @@ def _update_click_info( pick_representation_actor = ( pick_representation_actor if pick_representation_actor else {} ) - if not click_data or not enable_picking: + if not click_data: + return no_update, no_update, no_update + if not enable_picking: pick_representation_actor.update({"visibility": False}) return "", {}, pick_representation_actor pick_representation_actor.update({"visibility": True}) From e6391df5575e145f515fc081ec105359ec2773c0 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 23:06:53 +0200 Subject: [PATCH 29/63] Add pyvista dependency to dockerfile --- .github/workflows/subsurface.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/subsurface.yml b/.github/workflows/subsurface.yml index d705fdfb0..231a12a87 100644 --- a/.github/workflows/subsurface.yml +++ b/.github/workflows/subsurface.yml @@ -97,6 +97,7 @@ jobs: webviz build ./webviz-subsurface-testdata/webviz_examples/webviz-full-demo.yml --portable ./example_subsurface_app --theme equinor rm -rf ./webviz-subsurface-testdata pushd example_subsurface_app + sed -i '/FROM python:...-slim/a\RUN apt-get update\n\RUN apt-get install libgl1-mesa-dev xvfb tk -y' Dockerfile docker build -t webviz/example_subsurface_image:equinor-theme . popd From d089f852b7c0bd4eb3ba600306cfd96589d34a31 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 8 Apr 2022 23:07:05 +0200 Subject: [PATCH 30/63] Add crop widget --- .../_eclipse_grid_viewer/_callbacks.py | 98 +++++++-- .../_explicit_structured_grid_accessor.py | 16 +- .../plugins/_eclipse_grid_viewer/_layout.py | 199 +++++++++++++----- 3 files changed, 238 insertions(+), 75 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 955ce8cda..e4c1d8c56 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -4,13 +4,13 @@ from typing import Any, Callable, Dict, List, Optional, Tuple import numpy as np -from dash import Input, Output, State, callback, no_update +from dash import Input, Output, State, callback, no_update, callback_context, MATCH, ALL from dash_vtk.utils.vtk import b64_encode_numpy from webviz_subsurface._utils.perf_timer import PerfTimer from ._business_logic import EclipseGridDataModel -from ._layout import PROPERTYTYPE, LayoutElements +from ._layout import PROPERTYTYPE, LayoutElements, GRID_DIRECTION def plugin_callbacks(get_uuid: Callable, datamodel: EclipseGridDataModel) -> None: @@ -47,18 +47,14 @@ def _populate_properties( Output(get_uuid(LayoutElements.STORED_CELL_INDICES_HASH), "data"), Input(get_uuid(LayoutElements.PROPERTIES), "value"), Input(get_uuid(LayoutElements.DATES), "value"), - Input(get_uuid(LayoutElements.GRID_COLUMNS), "value"), - Input(get_uuid(LayoutElements.GRID_ROWS), "value"), - Input(get_uuid(LayoutElements.GRID_LAYERS), "value"), + Input(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), State(get_uuid(LayoutElements.INIT_RESTART), "value"), State(get_uuid(LayoutElements.STORED_CELL_INDICES_HASH), "data"), ) def _set_geometry_and_scalar( prop: List[str], date: List[int], - columns: List[int], - rows: List[int], - layers: List[int], + grid_range: List[List[int]], proptype: str, stored_cell_indices: int, ) -> Tuple[Any, Any, Any, List, Any]: @@ -70,7 +66,7 @@ def _set_geometry_and_scalar( scalar = datamodel.get_restart_values(prop[0], date[0]) print(f"Reading scalar from file in {timer.lap_s():.2f}s") - cropped_grid = datamodel.esg_accessor.crop(columns, rows, layers) + cropped_grid = datamodel.esg_accessor.crop(*grid_range) polys, points, cell_indices = datamodel.esg_accessor.extract_skin(cropped_grid) print(f"Extracting cropped geometry in {timer.lap_s():.2f}s") @@ -141,9 +137,7 @@ def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> floa Input(get_uuid(LayoutElements.DATES), "value"), Input(get_uuid(LayoutElements.INIT_RESTART), "value"), State(get_uuid(LayoutElements.Z_SCALE), "value"), - State(get_uuid(LayoutElements.GRID_COLUMNS), "value"), - State(get_uuid(LayoutElements.GRID_ROWS), "value"), - State(get_uuid(LayoutElements.GRID_LAYERS), "value"), + Input(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), State(get_uuid(LayoutElements.VTK_PICK_REPRESENTATION), "actor"), ) # pylint: disable = too-many-locals, too-many-arguments @@ -154,9 +148,7 @@ def _update_click_info( date: List[int], proptype: str, zscale: float, - columns: List[int], - rows: List[int], - layers: List[int], + grid_range: List[List[int]], pick_representation_actor: Optional[Dict], ) -> Tuple[str, Dict[str, Any], Dict[str, bool]]: pick_representation_actor = ( @@ -174,7 +166,7 @@ def _update_click_info( else: scalar = datamodel.get_restart_values(prop[0], date[0]) - cropped_grid = datamodel.esg_accessor.crop(columns, rows, layers) + cropped_grid = datamodel.esg_accessor.crop(*grid_range) # Getting position and ray below mouse position coords = click_data["worldPosition"].copy() @@ -219,3 +211,77 @@ def _update_click_info( ) def _set_colormap(colormap: str) -> str: return colormap + + @callback( + Output( + { + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": MATCH, + "component": "input", + "component2": MATCH, + }, + "value", + ), + Output( + { + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": MATCH, + "component": "slider", + "component2": MATCH, + }, + "value", + ), + Input( + { + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": MATCH, + "component": "input", + "component2": MATCH, + }, + "value", + ), + Input( + { + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": MATCH, + "component": "slider", + "component2": MATCH, + }, + "value", + ), + ) + def _synchronize_crop_slider_and_input( + input_val: int, slider_val: int + ) -> Tuple[Any, Any]: + trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0] + if "slider" in trigger_id: + return slider_val, no_update + return no_update, input_val + + @callback( + Output(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), + Input( + { + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": ALL, + "component": "input", + "component2": "start", + }, + "value", + ), + Input( + { + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": ALL, + "component": "input", + "component2": "width", + }, + "value", + ), + ) + def _store_grid_range_from_crop_widget( + input_vals: List[int], width_vals: List[int] + ) -> List[List[int]]: + if not input_vals or not width_vals: + return no_update + return [[val, val + width] for val, width in zip(input_vals, width_vals)] diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py index a31673930..7096a309d 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py @@ -38,7 +38,7 @@ def crop( crop_filter = vtkExplicitStructuredGridCrop() crop_filter.SetInputData(self.es_grid) crop_filter.SetOutputWholeExtent( - irange[0], irange[1] + 1, jrange[0], jrange[1] + 1, krange[0], krange[1] + 1 + irange[0] - 1, irange[1], jrange[0] - 1, jrange[1], krange[0] - 1, krange[1] ) crop_filter.Update() @@ -132,28 +132,28 @@ def find_closest_cell_to_ray( self.es_grid.ComputeCellStructuredCoords(cell_id, i, j, k, False) print(f"Get ijk in {timer.lap_s():.2f}") - return cell_id, [int(i), int(j), int(k)] + return cell_id, [int(i) + 1, int(j) + 1, int(k) + 1] @property def imin(self) -> int: - return 0 + return 1 @property def imax(self) -> int: - return self.es_grid.dimensions[0] - 2 + return self.es_grid.dimensions[0] - 1 @property def jmin(self) -> int: - return 0 + return 1 @property def jmax(self) -> int: - return self.es_grid.dimensions[1] - 2 + return self.es_grid.dimensions[1] - 1 @property def kmin(self) -> int: - return 0 + return 1 @property def kmax(self) -> int: - return self.es_grid.dimensions[2] - 2 + return self.es_grid.dimensions[2] - 1 diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index dc5fc313a..35e273f3d 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable +from typing import Callable, Optional import dash_vtk import webviz_core_components as wcc @@ -14,9 +14,6 @@ class LayoutElements(str, Enum): PROPERTIES = "properties-select" DATES = "dates-select" Z_SCALE = "z-scale" - GRID_COLUMNS = "grid-columns" - GRID_ROWS = "grid-rows" - GRID_LAYERS = "grid-layers" VTK_VIEW = "vtk-view" VTK_GRID_REPRESENTATION = "vtk-grid-representation" VTK_GRID_POLYDATA = "vtk-grid-polydata" @@ -29,6 +26,8 @@ class LayoutElements(str, Enum): VTK_PICK_REPRESENTATION = "vtk-pick-representation" VTK_PICK_SPHERE = "vtk-pick-sphere" SHOW_AXES = "show-axes" + CROP_WIDGET = "crop-widget" + GRID_RANGE_STORE = "crop-widget-store" class LayoutTitles(str, Enum): @@ -36,9 +35,6 @@ class LayoutTitles(str, Enum): PROPERTIES = "Property" DATES = "Date" Z_SCALE = "Z-scale" - GRID_COLUMNS = "Grid columns" - GRID_ROWS = "Grid rows" - GRID_LAYERS = "Grid layers" SHOW_GRID_LINES = "Show grid lines" COLORMAP = "Color map" GRID_FILTERS = "Grid filters" @@ -48,6 +44,12 @@ class LayoutTitles(str, Enum): SHOW_AXES = "Show axes" +class GRID_DIRECTION(str, Enum): + I = "I" + J = "J" + K = "K" + + COLORMAPS = ["erdc_rainbow_dark", "Viridis (matplotlib)", "BuRd"] @@ -71,6 +73,14 @@ def plugin_main_layout( sidebar(get_uuid=get_uuid, esg_accessor=esg_accessor), vtk_view(get_uuid=get_uuid), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), + dcc.Store( + id=get_uuid(LayoutElements.GRID_RANGE_STORE), + data=[ + [esg_accessor.imin, esg_accessor.imax], + [esg_accessor.jmin, esg_accessor.jmax], + [esg_accessor.kmin, esg_accessor.kmin], + ], + ), ] ) @@ -101,50 +111,6 @@ def sidebar( value=1, step=1, ), - wcc.Selectors( - label=LayoutTitles.GRID_FILTERS, - children=[ - wcc.RangeSlider( - label=LayoutTitles.GRID_COLUMNS, - id=get_uuid(LayoutElements.GRID_COLUMNS), - min=esg_accessor.imin, - max=esg_accessor.imax, - value=[esg_accessor.imin, esg_accessor.imax], - step=1, - marks=None, - tooltip={ - "placement": "bottom", - "always_visible": True, - }, - ), - wcc.RangeSlider( - label=LayoutTitles.GRID_ROWS, - id=get_uuid(LayoutElements.GRID_ROWS), - min=esg_accessor.jmin, - max=esg_accessor.jmax, - value=[esg_accessor.jmin, esg_accessor.jmax], - step=1, - marks=None, - tooltip={ - "placement": "bottom", - "always_visible": True, - }, - ), - wcc.RangeSlider( - label=LayoutTitles.GRID_LAYERS, - id=get_uuid(LayoutElements.GRID_LAYERS), - min=esg_accessor.kmin, - max=esg_accessor.kmax, - value=[esg_accessor.kmin, esg_accessor.kmin], - step=1, - marks=None, - tooltip={ - "placement": "bottom", - "always_visible": True, - }, - ), - ], - ), wcc.Selectors( label=LayoutTitles.COLORS, children=[ @@ -159,6 +125,30 @@ def sidebar( ) ], ), + wcc.Selectors( + label="Range filters", + children=[ + crop_widget( + get_uuid=get_uuid, + min_val=esg_accessor.imin, + max_val=esg_accessor.imax, + direction=GRID_DIRECTION.I, + ), + crop_widget( + get_uuid=get_uuid, + min_val=esg_accessor.jmin, + max_val=esg_accessor.jmax, + direction=GRID_DIRECTION.J, + ), + crop_widget( + get_uuid=get_uuid, + min_val=esg_accessor.kmin, + max_val=esg_accessor.kmax, + max_width=esg_accessor.kmin, + direction=GRID_DIRECTION.K, + ), + ], + ), wcc.Selectors( label="Options", children=[ @@ -189,6 +179,113 @@ def sidebar( ) +def crop_widget( + get_uuid: Callable, + min_val: int, + max_val: int, + direction: str, + max_width: Optional[int] = None, +) -> html.Div: + max_width = max_width if max_width else max_val + return html.Div( + children=[ + html.Div( + style={ + "display": "grid", + "marginBotton": "0px", + "gridTemplateColumns": f"2fr 1fr 8fr", + }, + children=[ + wcc.Label( + children=f"{direction} Start", + style={ + "fontSize": "0.7em", + "fontWeight": "bold", + "marginRight": "5px", + }, + ), + dcc.Input( + style={"width": "30px", "height": "10px"}, + id={ + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": direction, + "component": "input", + "component2": "start", + }, + type="number", + placeholder="Min", + persistence=True, + persistence_type="session", + value=min_val, + min=min_val, + max=max_val, + ), + wcc.Slider( + id={ + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": direction, + "component": "slider", + "component2": "start", + }, + min=min_val, + max=max_val, + value=min_val, + step=1, + marks=None, + ), + ], + ), + html.Div( + style={ + "display": "grid", + "marginTop": "0px", + "padding": "0px", + "gridTemplateColumns": f"2fr 1fr 8fr", + }, + children=[ + wcc.Label( + children=f"Width", + style={ + "fontSize": "0.7em", + "textAlign": "right", + "marginRight": "5px", + }, + ), + dcc.Input( + style={"width": "30px", "height": "10px"}, + id={ + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": direction, + "component": "input", + "component2": "width", + }, + type="number", + placeholder="Min", + persistence=True, + persistence_type="session", + value=max_width, + min=min_val, + max=max_val, + ), + wcc.Slider( + id={ + "id": get_uuid(LayoutElements.CROP_WIDGET), + "direction": direction, + "component": "slider", + "component2": "width", + }, + min=min_val, + max=max_val, + value=max_width, + step=1, + marks=None, + ), + ], + ), + ], + ) + + def vtk_view(get_uuid: Callable) -> dash_vtk.View: return dash_vtk.View( id=get_uuid(LayoutElements.VTK_VIEW), From 94ea1f5e3c2da123b67b68a775a4ce0ef5c07775 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Sat, 9 Apr 2022 16:40:07 +0200 Subject: [PATCH 31/63] Fix max dimensions --- .../_explicit_structured_grid_accessor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py index 7096a309d..45c76c4ea 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py @@ -38,7 +38,12 @@ def crop( crop_filter = vtkExplicitStructuredGridCrop() crop_filter.SetInputData(self.es_grid) crop_filter.SetOutputWholeExtent( - irange[0] - 1, irange[1], jrange[0] - 1, jrange[1], krange[0] - 1, krange[1] + irange[0] - 1, + irange[1] - 1, + jrange[0] - 1, + jrange[1] - 1, + krange[0] - 1, + krange[1] - 1, ) crop_filter.Update() From 0743a2c20f07df23b1bcf2df51bc664019fca33f Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Mon, 11 Apr 2022 23:05:32 +0200 Subject: [PATCH 32/63] Use webviz-vtk, and add roff grid reader --- .../_ensemble_grid_provider/__init__.py | 0 .../_grid_fmu_standard_discovery.py | 115 +++++++ .../_provider_impl_fmu_standard.py | 288 ++++++++++++++++++ .../ensemble_grid_provider.py | 47 +++ .../ensemble_grid_provider_factory.py | 120 ++++++++ .../_eclipse_grid_viewer/_callbacks.py | 7 +- ...ss_logic.py => _eclipse_grid_datamodel.py} | 13 - .../plugins/_eclipse_grid_viewer/_layout.py | 18 +- .../plugins/_eclipse_grid_viewer/_plugin.py | 36 +-- .../_roff_grid_datamodel.py | 98 ++++++ .../_xtgeo_to_explicit_structured_grid.py | 15 + 11 files changed, 715 insertions(+), 42 deletions(-) create mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/__init__.py create mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py create mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py create mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py create mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py rename webviz_subsurface/plugins/_eclipse_grid_viewer/{_business_logic.py => _eclipse_grid_datamodel.py} (87%) create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/__init__.py b/webviz_subsurface/_providers/_ensemble_grid_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py b/webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py new file mode 100644 index 000000000..219505588 --- /dev/null +++ b/webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py @@ -0,0 +1,115 @@ +import glob +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Union, Tuple + +from fmu.ensemble import ScratchEnsemble + + +@dataclass(frozen=True) +class GridParameterFileInfo: + path: str + real: int + name: str + attribute: str + datestr: Optional[str] + + +@dataclass(frozen=True) +class GridParameterIdent: + name: str + attribute: str + datestr: Optional[str] + + +@dataclass(frozen=True) +class GridFileInfo: + path: str + real: int + name: str + + +@dataclass(frozen=True) +class GridIdent: + name: str + + +def _discover_ensemble_realizations_fmu(ens_path: str) -> Dict[int, str]: + """Returns dict indexed by realization number and with runpath as value""" + scratch_ensemble = ScratchEnsemble("dummyEnsembleName", paths=ens_path).filter("OK") + real_dict = {i: r.runpath() for i, r in scratch_ensemble.realizations.items()} + return real_dict + + +def _discover_ensemble_realizations(ens_path: str) -> Dict[int, str]: + # Much faster than FMU impl above, but is it risky? + # Do we need to check for OK-file? + real_dict: Dict[int, str] = {} + + realidxregexp = re.compile(r"realization-(\d+)") + globbed_real_dirs = sorted(glob.glob(str(ens_path))) + for real_dir in globbed_real_dirs: + realnum: Optional[int] = None + for path_comp in reversed(real_dir.split(os.path.sep)): + realmatch = re.match(realidxregexp, path_comp) + if realmatch: + realnum = int(realmatch.group(1)) + break + + if realnum is not None: + real_dict[realnum] = real_dir + + return real_dict + + +def ident_from_filename( + filename: str, +) -> Optional[Union[GridIdent, GridParameterIdent]]: + """Split the stem part of the roff filename into grid name, attribute and + optionally date part""" + delimiter: str = "--" + parts = Path(filename).stem.split(delimiter) + if len(parts) == 1: + return GridIdent(name=parts[0]) + + return GridParameterIdent( + name=parts[0], attribute=parts[1], datestr=parts[2] if len(parts) >= 3 else None + ) + + +def discover_per_realization_grid_files( + ens_path: str, attribute_filter: List[str] = None +) -> Tuple[List[GridParameterFileInfo], List[GridFileInfo]]: + rel_surface_folder: str = "share/results/grids" + suffix: str = "*.roff" + + grid_parameter_files: List[GridParameterFileInfo] = [] + grid_files: List[GridFileInfo] = [] + real_dict = _discover_ensemble_realizations_fmu(ens_path) + for realnum, runpath in sorted(real_dict.items()): + globbed_filenames = glob.glob(str(Path(runpath) / rel_surface_folder / suffix)) + for filename in sorted(globbed_filenames): + ident = ident_from_filename(filename) + if isinstance(ident, GridParameterIdent): + if ( + attribute_filter is not None + and ident.attribute not in attribute_filter + ): + continue + grid_parameter_files.append( + GridParameterFileInfo( + path=filename, + real=realnum, + name=ident.name, + attribute=ident.attribute, + datestr=ident.datestr, + ) + ) + else: + grid_files.append( + GridFileInfo(path=filename, real=realnum, name=ident.name) + ) + # Should check if all parameters has a grid... + return grid_parameter_files, grid_files diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py b/webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py new file mode 100644 index 000000000..36e6f1797 --- /dev/null +++ b/webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py @@ -0,0 +1,288 @@ +import logging +import shutil +import warnings +from enum import Enum +from pathlib import Path +from typing import List, Optional, Set + +import numpy as np +import pandas as pd +import xtgeo + +from webviz_subsurface._utils.perf_timer import PerfTimer + + +from ._grid_fmu_standard_discovery import GridFileInfo,GridParameterFileInfo +from .ensemble_grid_provider import EnsembleGridProvider + + +LOGGER = logging.getLogger(__name__) + +REm" +REL_OBS_DIR = "obs" +REL_STAT_CACHE_DIR = "stat_cache" + +# pylint: disable=too-few-public-methods +class Col: + TYPE = "type" + REAL = "real" + ATTRIBUTE = "attribute" + NAME = "name" + DATESTR = "datestr" + ORIGINAL_PATH = "original_path" + REL_PATH = "rel_path" + + +class SurfaceType(str, Enum): + OBSERVED = "observed" + SIMULATED = "simulated" + + +class ProviderImplFile(EnsembleGridProvider): + def __init__( + self, provider_id: str, provider_dir: Path, surface_inventory_df: pd.DataFrame + ) -> None: + self._provider_id = provider_id + self._provider_dir = provider_dir + self._inventory_df = surface_inventory_df + + @staticmethod + # pylint: disable=too-many-locals + def write_backing_store( + storage_dir: Path, + storage_key: str, + grids: List[GridFileInfo], + grid_parameters: List[GridParameterFileInfo], + avoid_copying_grid_data: bool, + ) -> None: + """If avoid_copying_grid_data if True, the specified grid data will NOT be copied + into the backing store, but will be referenced from their source locations. + Note that this is only useful when running in non-portable mode and will fail + in portable mode. + """ + + timer = PerfTimer() + + do_copy_grid_data_into_store = not avoid_copying_grid_data + + # All data for this provider will be stored inside a sub-directory + # given by the storage key + provider_dir = storage_dir / storage_key + LOGGER.debug(f"Writing grid data backing store to: {provider_dir}") + provider_dir.mkdir(parents=True, exist_ok=True) + + type_arr: List[SurfaceType] = [] + real_arr: List[int] = [] + attribute_arr: List[str] = [] + name_arr: List[str] = [] + datestr_arr: List[str] = [] + rel_path_arr: List[str] = [] + original_path_arr: List[str] = [] + gridnames = [grid.name for grid in grids] + for grid_parameter_info in grid_parameters: + if grid_parameter_info.name not in gridnames: + continue + name_arr.append(grid_parameter_info.name) + real_arr.append(grid_parameter_info.real) + attribute_arr.append(grid_parameter_info.attribute) + datestr_arr.append(grid_parameter_info.datestr if grid_parameter_info.datestr else "") + original_path_arr.append(grid_parameter_info.path) + + rel_path_in_store = "" + if do_copy_grid_data_into_store: + rel_path_in_store = _compose_rel_sim_surf_pathstr( + real=grid_parameter_info.real, + attribute=grid_parameter_info.attribute, + name=grid_parameter_info.name, + datestr=grid_parameter_info.datestr, + extension=Path(grid_parameter_info.path).suffix, + ) + + rel_path_arr.append(rel_path_in_store) + + + timer.lap_s() + if do_copy_grid_data_into_store: + LOGGER.debug( + f"Copying {len(original_path_arr)} surfaces into backing store..." + ) + _copy_grid_parameters_into_provider_dir( + original_path_arr, rel_path_arr, provider_dir + ) + et_copy_s = timer.lap_s() + + grid_inventory_df = pd.DataFrame( + { + Col.TYPE: type_arr, + Col.REAL: real_arr, + Col.ATTRIBUTE: attribute_arr, + Col.NAME: name_arr, + Col.DATESTR: datestr_arr, + Col.REL_PATH: rel_path_arr, + Col.ORIGINAL_PATH: original_path_arr, + } + ) + + parquet_file_name = provider_dir / "surface_inventory.parquet" + grid_inventory_df.to_parquet(path=parquet_file_name) + + if do_copy_grid_data_into_store: + LOGGER.debug( + f"Wrote surface backing store in: {timer.elapsed_s():.2f}s (" + f"copy={et_copy_s:.2f}s)" + ) + else: + LOGGER.debug( + f"Wrote surface backing store without copying surfaces in: " + f"{timer.elapsed_s():.2f}s" + ) + + @staticmethod + def from_backing_store( + storage_dir: Path, + storage_key: str, + ) -> Optional["ProviderImplFile"]: + + provider_dir = storage_dir / storage_key + parquet_file_name = provider_dir / "surface_inventory.parquet" + + try: + surface_inventory_df = pd.read_parquet(path=parquet_file_name) + return ProviderImplFile(storage_key, provider_dir, surface_inventory_df) + except FileNotFoundError: + return None + + def provider_id(self) -> str: + return self._provider_id + + def attributes(self) -> List[str]: + return sorted(list(self._inventory_df[Col.ATTRIBUTE].unique())) + + def surface_names_for_attribute(self, surface_attribute: str) -> List[str]: + return sorted( + list( + self._inventory_df.loc[ + self._inventory_df[Col.ATTRIBUTE] == surface_attribute + ][Col.NAME].unique() + ) + ) + + def surface_dates_for_attribute( + self, surface_attribute: str + ) -> Optional[List[str]]: + dates = sorted( + list( + self._inventory_df.loc[ + self._inventory_df[Col.ATTRIBUTE] == surface_attribute + ][Col.DATESTR].unique() + ) + ) + if len(dates) == 1 and not bool(dates[0]): + return None + + return dates + + def realizations(self) -> List[int]: + unique_reals = self._inventory_df[Col.REAL].unique() + + # Sort and strip out any entries with real == -1 + return sorted([r for r in unique_reals if r >= 0]) + + def get_surface( + self, + address: SurfaceAddress, + ) -> Optional[xtgeo.RegularSurface]: + if isinstance(address, StatisticalSurfaceAddress): + return self._get_or_create_statistical_surface(address) + # return self._create_statistical_surface(address) + if isinstance(address, SimulatedSurfaceAddress): + return self._get_simulated_surface(address) + if isinstance(address, ObservedSurfaceAddress): + return self._get_observed_surface(address) + + raise TypeError("Unknown type of surface address") + def _get_simulated_surface( + self, address: SimulatedSurfaceAddress + ) -> Optional[xtgeo.RegularSurface]: + """Returns a Xtgeo surface instance of a single realization surface""" + + timer = PerfTimer() + + surf_fns: List[str] = self._locate_grid_paramters( + attribute=address.attribute, + name=address.name, + datestr=address.datestr if address.datestr is not None else "", + realizations=[address.realization], + ) + + if len(surf_fns) == 0: + LOGGER.warning(f"No simulated surface found for {address}") + return None + if len(surf_fns) > 1: + LOGGER.warning( + f"Multiple simulated surfaces found for: {address}" + "Returning first surface." + ) + + surf = xtgeo.surface_from_file(surf_fns[0]) + + LOGGER.debug(f"Loaded simulated surface in: {timer.elapsed_s():.2f}s") + + return surf + + def _locate_grid_paramters( + self, attribute: str, name: str, datestr: str, realizations: List[int] + ) -> List[str]: + """Returns list of file names matching the specified filter criteria""" + df = self._inventory_df.loc[ + self._inventory_df[Col.TYPE] == SurfaceType.SIMULATED + ] + + df = df.loc[ + (df[Col.ATTRIBUTE] == attribute) + & (df[Col.NAME] == name) + & (df[Col.DATESTR] == datestr) + & (df[Col.REAL].isin(realizations)) + ] + + df = df[[Col.REL_PATH, Col.ORIGINAL_PATH]] + + # Return file name within backing store if the surface was copied there, + # otherwise return the original source file name + fn_list: List[str] = [] + for _index, row in df.iterrows(): + if row[Col.REL_PATH]: + fn_list.append(self._provider_dir / row[Col.REL_PATH]) + else: + fn_list.append(row[Col.ORIGINAL_PATH]) + + return fn_list + +def _copy_grid_parameters_into_provider_dir( + original_path_arr: List[str], + rel_path_arr: List[str], + provider_dir: Path, +) -> None: + for src_path, dst_rel_path in zip(original_path_arr, rel_path_arr): + # LOGGER.debug(f"copying surface from: {src_path}") + shutil.copyfile(src_path, provider_dir / dst_rel_path) + + # full_dst_path_arr = [storage_dir / dst_rel_path for dst_rel_path in store_path_arr] + # with ProcessPoolExecutor() as executor: + # executor.map(shutil.copyfile, original_path_arr, full_dst_path_arr) + + +def _compose_rel_sim_surf_pathstr( + real: int, + attribute: str, + name: str, + datestr: Optional[str], + extension: str, +) -> str: + """Compose path to simulated surface file, relative to provider's directory""" + if datestr: + fname = f"{real}--{name}--{attribute}--{datestr}{extension}" + else: + fname = f"{real}--{name}--{attribute}{extension}" + return str(Path(fname)) + diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py b/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py new file mode 100644 index 000000000..b06c1468d --- /dev/null +++ b/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py @@ -0,0 +1,47 @@ +import abc +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional, Union + +import numpy as np + + +# Class provides data for ensemble surfaces +class EnsembleGridProvider(abc.ABC): + @abc.abstractmethod + def provider_id(self) -> str: + """Returns string ID of the provider.""" + + @abc.abstractmethod + def get_explicit_structured_grid_accessor(self, realization: int): + """Returns the esg accessor""" + + @abc.abstractmethod + def static_parameter_names(self) -> List[str]: + """Returns list of all available static parameters.""" + + @abc.abstractmethod + def dynamic_parameter_names(self) -> List[str]: + """Returns list of all available dynamic parameters.""" + + @abc.abstractmethod + def dates_for_dynamic_parameter( + self, dynamic_parameter: str + ) -> Optional[List[str]]: + """Returns list of all available dates for a given dynamic parameter.""" + + @abc.abstractmethod + def realizations(self) -> List[int]: + """Returns list of all available realizations.""" + + @abc.abstractmethod + def get_static_parameter_values( + self, parameter_name: str, realization: int + ) -> Optional[np.ndarray]: + """Returns 1d values for a given static parameter""" + + @abc.abstractmethod + def get_dynamic_parameter_values( + self, parameter_name: str, parameter_date: str, realization: int + ) -> Optional[np.ndarray]: + """Returns 1d values for a given dynamic parameter""" diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py b/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py new file mode 100644 index 000000000..bfd5bcac4 --- /dev/null +++ b/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py @@ -0,0 +1,120 @@ +import hashlib +import logging +import os +from pathlib import Path +from typing import List + +from webviz_config.webviz_factory import WebvizFactory +from webviz_config.webviz_factory_registry import WEBVIZ_FACTORY_REGISTRY +from webviz_config.webviz_instance_info import WebvizRunMode + +from webviz_subsurface._utils.perf_timer import PerfTimer + +from ._provider_impl_fmu_standard import ProviderImplFMUStandard +from ._grid_fmu_standard_discovery import discover_per_realization_grid_files +from .ensemble_grid_provider import EnsembleGridProvider + +LOGGER = logging.getLogger(__name__) + + +class EnsembleGridProviderFactory(WebvizFactory): + def __init__( + self, + root_storage_folder: Path, + allow_storage_writes: bool, + avoid_copying_grid_data: bool, + ) -> None: + self._storage_dir = Path(root_storage_folder) / __name__ + self._allow_storage_writes = allow_storage_writes + self._avoid_copying_grid_data = avoid_copying_grid_data + + LOGGER.info( + f"EnsembleGridProviderFactory init: storage_dir={self._storage_dir}" + ) + + if self._allow_storage_writes: + os.makedirs(self._storage_dir, exist_ok=True) + + @staticmethod + def instance() -> "EnsembleGridProviderFactory": + """Static method to access the singleton instance of the factory.""" + + factory = WEBVIZ_FACTORY_REGISTRY.get_factory(EnsembleGridProviderFactory) + if not factory: + app_instance_info = WEBVIZ_FACTORY_REGISTRY.app_instance_info + storage_folder = app_instance_info.storage_folder + allow_writes = app_instance_info.run_mode != WebvizRunMode.PORTABLE + dont_copy_grid_data = ( + app_instance_info.run_mode == WebvizRunMode.NON_PORTABLE + ) + + factory = EnsembleGridProviderFactory( + root_storage_folder=storage_folder, + allow_storage_writes=allow_writes, + avoid_copying_grid_data=dont_copy_grid_data, + ) + + # Store the factory object in the global factory registry + WEBVIZ_FACTORY_REGISTRY.set_factory(EnsembleGridProviderFactory, factory) + + return factory + + def create_from_fmu_standard_grid_files( + self, ens_path: str, attribute_filter: List[str] = None + ) -> EnsembleGridProvider: + timer = PerfTimer() + string_to_hash = ( + f"{ens_path}" + if attribute_filter is None + else f"{ens_path}_{'_'.join([str(attr) for attr in attribute_filter])}" + ) + storage_key = f"ens__{_make_hash_string(string_to_hash)}" + provider = ProviderImplFMUStandard.from_backing_store( + self._storage_dir, storage_key + ) + if provider: + LOGGER.info( + f"Loaded surface provider from backing store in {timer.elapsed_s():.2f}s (" + f"ens_path={ens_path})" + ) + return provider + + # We can only import data from data source if storage writes are allowed + if not self._allow_storage_writes: + raise ValueError(f"Failed to load surface provider for {ens_path}") + + LOGGER.info(f"Importing/copying grid data for: {ens_path}") + + timer.lap_s() + grid_files, grid_parameter_files = discover_per_realization_grid_files( + ens_path, attribute_filter + ) + + # As an optimization, avoid copying the grid data into the backing store, + # typically when we're running in non-portable mode + ProviderImplFMUStandard.write_backing_store( + self._storage_dir, + storage_key, + grids=grid_files, + grid_parameters=grid_parameter_files, + avoid_copying_grid_data=self._avoid_copying_grid_data, + ) + et_write_s = timer.lap_s() + + provider = ProviderImplFMUStandard.from_backing_store( + self._storage_dir, storage_key + ) + if not provider: + raise ValueError(f"Failed to load/create grid provider for {ens_path}") + + LOGGER.info( + f"Saved grid provider to backing store in {timer.elapsed_s():.2f}s (" + f" write={et_write_s:.2f}s, ens_path={ens_path})" + ) + + return provider + + +def _make_hash_string(string_to_hash: str) -> str: + # There is no security risk here and chances of collision should be very slim + return hashlib.md5(string_to_hash.encode()).hexdigest() # nosec diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index e4c1d8c56..2f07583bd 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -5,11 +5,13 @@ import numpy as np from dash import Input, Output, State, callback, no_update, callback_context, MATCH, ALL -from dash_vtk.utils.vtk import b64_encode_numpy +from webviz_vtk.utils.vtk import b64_encode_numpy from webviz_subsurface._utils.perf_timer import PerfTimer -from ._business_logic import EclipseGridDataModel +from ._eclipse_grid_datamodel import EclipseGridDataModel +from ._roff_grid_datamodel import RoffGridDataModel + from ._layout import PROPERTYTYPE, LayoutElements, GRID_DIRECTION @@ -82,7 +84,6 @@ def _set_geometry_and_scalar( [np.nanmin(scalar), np.nanmax(scalar)], no_update, ) - return ( b64_encode_numpy(polys.astype(np.float32)), b64_encode_numpy(points.astype(np.float32)), diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py similarity index 87% rename from webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py rename to webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py index 66ead88c5..7d0f14f16 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_business_logic.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py @@ -11,19 +11,6 @@ from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor -def xtgeo_grid_to_explicit_structured_grid( - xtg_grid: xtgeo.Grid, -) -> pv.ExplicitStructuredGrid: - dims, corners, inactive = xtg_grid.get_vtk_geometries() - corners[:, 2] *= -1 - esg_grid = pv.ExplicitStructuredGrid(dims, corners) - esg_grid = esg_grid.compute_connectivity() - # esg_grid.ComputeFacesConnectivityFlagsArray() - esg_grid = esg_grid.hide_cells(inactive) - # esg_grid.flip_z(inplace=True) - return esg_grid - - class EclipseGridDataModel: def __init__( self, diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 35e273f3d..598b92049 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Callable, Optional -import dash_vtk +import webviz_vtk import webviz_core_components as wcc from dash import dcc, html @@ -286,23 +286,23 @@ def crop_widget( ) -def vtk_view(get_uuid: Callable) -> dash_vtk.View: - return dash_vtk.View( +def vtk_view(get_uuid: Callable) -> webviz_vtk.View: + return webviz_vtk.View( id=get_uuid(LayoutElements.VTK_VIEW), style=LayoutStyle.VTK_VIEW, pickingModes=["click"], children=[ - dash_vtk.GeometryRepresentation( + webviz_vtk.GeometryRepresentation( id=get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), showCubeAxes=True, showScalarBar=True, children=[ - dash_vtk.PolyData( + webviz_vtk.PolyData( id=get_uuid(LayoutElements.VTK_GRID_POLYDATA), children=[ - dash_vtk.CellData( + webviz_vtk.CellData( [ - dash_vtk.DataArray( + webviz_vtk.DataArray( id=get_uuid(LayoutElements.VTK_GRID_CELLDATA), registration="setScalars", name="scalar", @@ -314,11 +314,11 @@ def vtk_view(get_uuid: Callable) -> dash_vtk.View: ], property={"edgeVisibility": True}, ), - dash_vtk.GeometryRepresentation( + webviz_vtk.GeometryRepresentation( id=get_uuid(LayoutElements.VTK_PICK_REPRESENTATION), actor={"visibility": False}, children=[ - dash_vtk.Algorithm( + webviz_vtk.Algorithm( id=get_uuid(LayoutElements.VTK_PICK_SPHERE), vtkClass="vtkSphereSource", ) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index fccb6c4aa..f1b8c8f42 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -4,7 +4,8 @@ import webviz_core_components as wcc from webviz_config import WebvizPluginABC -from ._business_logic import EclipseGridDataModel +from ._eclipse_grid_datamodel import EclipseGridDataModel +from ._roff_grid_datamodel import RoffGridDataModel from ._callbacks import plugin_callbacks from ._layout import plugin_main_layout @@ -14,21 +15,25 @@ class EclipseGridViewer(WebvizPluginABC): def __init__( self, - egrid_file: Path, - init_file: Path, - restart_file: Path, - init_names: List[str], - restart_names: List[str], + roff_folder: Path = None, + roff_grid_name: str = None, + egrid_file: Path = None, + init_file: Path = None, + restart_file: Path = None, + init_names: List[str] = None, + restart_names: List[str] = None, ) -> None: super().__init__() - - self._datamodel: EclipseGridDataModel = EclipseGridDataModel( - egrid_file=egrid_file, - init_file=init_file, - restart_file=restart_file, - init_names=init_names, - restart_names=restart_names, - ) + if roff_folder is not None and roff_grid_name is not None: + self._datamodel = RoffGridDataModel(roff_folder, roff_grid_name) + else: + self._datamodel: EclipseGridDataModel = EclipseGridDataModel( + egrid_file=egrid_file, + init_file=init_file, + restart_file=restart_file, + init_names=init_names, + restart_names=restart_names, + ) plugin_callbacks(get_uuid=self.uuid, datamodel=self._datamodel) @property @@ -36,6 +41,3 @@ def layout(self) -> wcc.FlexBox: return plugin_main_layout( get_uuid=self.uuid, esg_accessor=self._datamodel.esg_accessor ) - - def add_webvizstore(self) -> List[Tuple[Callable, List[Dict]]]: - return self._datamodel.webviz_store diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py new file mode 100644 index 000000000..c712890df --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py @@ -0,0 +1,98 @@ +from pathlib import Path +from typing import Callable, List, Tuple + +import numpy as np +import pyvista as pv +import xtgeo + +from webviz_subsurface._utils.perf_timer import PerfTimer +from webviz_subsurface._utils.webvizstore_functions import get_path + +from ._xtgeo_to_explicit_structured_grid import xtgeo_grid_to_explicit_structured_grid +from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor + + +def get_static_parameter_names(folder: Path, grid_name: str): + return list( + set( + fn.stem.split("--")[1] + for fn in Path(folder).glob(f"{grid_name}*.roff") + if len(fn.stem.split("--")) == 2 + ) + ) + + +def get_dynamic_parameter_names(folder: Path, grid_name: str): + return list( + set( + fn.stem.split("--")[1] + for fn in Path(folder).glob(f"{grid_name}*.roff") + if len(fn.stem.split("--")) == 3 + ) + ) + + +def get_dynamic_parameter_dates(folder: Path, grid_name: str): + + return list( + set( + fn.stem.split("--")[2] + for fn in Path(folder).glob(f"{grid_name}*.roff") + if len(fn.stem.split("--")) == 3 + ) + ) + + +class RoffGridDataModel: + def __init__( + self, + folder: Path, + grid_name: Path, + ): + + # self.add_webviz_store(egrid_file, init_file, restart_file) + + self.folder = folder + self.grid_name = grid_name + # Grid required when loading grid properties later on + self._xtg_grid = xtgeo.grid_from_file(Path(folder / f"{grid_name}.roff")) + + timer = PerfTimer() + print("Converting egrid to VTK ExplicitStructuredGrid") + self.esg_accessor = ExplicitStructuredGridAccessor( + xtgeo_grid_to_explicit_structured_grid(self._xtg_grid) + ) + print(f"Conversion complete in : {timer.lap_s():.2f}s") + self._restart_dates = self + + @property + def init_names(self) -> List[str]: + return get_static_parameter_names(self.folder, self.grid_name) + + @property + def restart_names(self) -> List[str]: + return get_dynamic_parameter_names(self.folder, self.grid_name) + + @property + def restart_dates(self) -> List[str]: + return get_dynamic_parameter_dates(self.folder, self.grid_name) + + def get_init_property(self, prop_name: str) -> xtgeo.GridProperty: + path = Path(self.folder / f"{self.grid_name}--{prop_name}.roff") + prop = xtgeo.gridproperty_from_file(path) + return prop + + def get_restart_property( + self, prop_name: str, prop_date: int + ) -> xtgeo.GridProperty: + path = Path(self.folder / f"{self.grid_name}--{prop_name}--{prop_date}.roff") + prop = xtgeo.gridproperty_from_file(path) + return prop + + def get_init_values(self, prop_name: str) -> np.ndarray: + prop = self.get_init_property(prop_name) + return prop.get_npvalues1d(order="F").ravel() + + def get_restart_values(self, prop_name: str, prop_date: int) -> np.ndarray: + prop = self.get_restart_property(prop_name, prop_date) + return prop.get_npvalues1d(order="F").ravel() diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py new file mode 100644 index 000000000..9ce081f34 --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py @@ -0,0 +1,15 @@ +import xtgeo +import pyvista as pv + + +def xtgeo_grid_to_explicit_structured_grid( + xtg_grid: xtgeo.Grid, +) -> pv.ExplicitStructuredGrid: + dims, corners, inactive = xtg_grid.get_vtk_geometries() + corners[:, 2] *= -1 + esg_grid = pv.ExplicitStructuredGrid(dims, corners) + esg_grid = esg_grid.compute_connectivity() + # esg_grid.ComputeFacesConnectivityFlagsArray() + esg_grid = esg_grid.hide_cells(inactive) + # esg_grid.flip_z(inplace=True) + return esg_grid From 4fbb96f0c8eca0dee4cbbf4da37675c408ca574a Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Mon, 11 Apr 2022 23:09:55 +0200 Subject: [PATCH 33/63] mistake --- .../_ensemble_grid_provider/__init__.py | 0 .../_grid_fmu_standard_discovery.py | 115 ------- .../_provider_impl_fmu_standard.py | 288 ------------------ .../ensemble_grid_provider.py | 47 --- .../ensemble_grid_provider_factory.py | 120 -------- 5 files changed, 570 deletions(-) delete mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/__init__.py delete mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py delete mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py delete mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py delete mode 100644 webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/__init__.py b/webviz_subsurface/_providers/_ensemble_grid_provider/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py b/webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py deleted file mode 100644 index 219505588..000000000 --- a/webviz_subsurface/_providers/_ensemble_grid_provider/_grid_fmu_standard_discovery.py +++ /dev/null @@ -1,115 +0,0 @@ -import glob -import os -import re -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, List, Optional, Union, Tuple - -from fmu.ensemble import ScratchEnsemble - - -@dataclass(frozen=True) -class GridParameterFileInfo: - path: str - real: int - name: str - attribute: str - datestr: Optional[str] - - -@dataclass(frozen=True) -class GridParameterIdent: - name: str - attribute: str - datestr: Optional[str] - - -@dataclass(frozen=True) -class GridFileInfo: - path: str - real: int - name: str - - -@dataclass(frozen=True) -class GridIdent: - name: str - - -def _discover_ensemble_realizations_fmu(ens_path: str) -> Dict[int, str]: - """Returns dict indexed by realization number and with runpath as value""" - scratch_ensemble = ScratchEnsemble("dummyEnsembleName", paths=ens_path).filter("OK") - real_dict = {i: r.runpath() for i, r in scratch_ensemble.realizations.items()} - return real_dict - - -def _discover_ensemble_realizations(ens_path: str) -> Dict[int, str]: - # Much faster than FMU impl above, but is it risky? - # Do we need to check for OK-file? - real_dict: Dict[int, str] = {} - - realidxregexp = re.compile(r"realization-(\d+)") - globbed_real_dirs = sorted(glob.glob(str(ens_path))) - for real_dir in globbed_real_dirs: - realnum: Optional[int] = None - for path_comp in reversed(real_dir.split(os.path.sep)): - realmatch = re.match(realidxregexp, path_comp) - if realmatch: - realnum = int(realmatch.group(1)) - break - - if realnum is not None: - real_dict[realnum] = real_dir - - return real_dict - - -def ident_from_filename( - filename: str, -) -> Optional[Union[GridIdent, GridParameterIdent]]: - """Split the stem part of the roff filename into grid name, attribute and - optionally date part""" - delimiter: str = "--" - parts = Path(filename).stem.split(delimiter) - if len(parts) == 1: - return GridIdent(name=parts[0]) - - return GridParameterIdent( - name=parts[0], attribute=parts[1], datestr=parts[2] if len(parts) >= 3 else None - ) - - -def discover_per_realization_grid_files( - ens_path: str, attribute_filter: List[str] = None -) -> Tuple[List[GridParameterFileInfo], List[GridFileInfo]]: - rel_surface_folder: str = "share/results/grids" - suffix: str = "*.roff" - - grid_parameter_files: List[GridParameterFileInfo] = [] - grid_files: List[GridFileInfo] = [] - real_dict = _discover_ensemble_realizations_fmu(ens_path) - for realnum, runpath in sorted(real_dict.items()): - globbed_filenames = glob.glob(str(Path(runpath) / rel_surface_folder / suffix)) - for filename in sorted(globbed_filenames): - ident = ident_from_filename(filename) - if isinstance(ident, GridParameterIdent): - if ( - attribute_filter is not None - and ident.attribute not in attribute_filter - ): - continue - grid_parameter_files.append( - GridParameterFileInfo( - path=filename, - real=realnum, - name=ident.name, - attribute=ident.attribute, - datestr=ident.datestr, - ) - ) - else: - grid_files.append( - GridFileInfo(path=filename, real=realnum, name=ident.name) - ) - # Should check if all parameters has a grid... - return grid_parameter_files, grid_files diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py b/webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py deleted file mode 100644 index 36e6f1797..000000000 --- a/webviz_subsurface/_providers/_ensemble_grid_provider/_provider_impl_fmu_standard.py +++ /dev/null @@ -1,288 +0,0 @@ -import logging -import shutil -import warnings -from enum import Enum -from pathlib import Path -from typing import List, Optional, Set - -import numpy as np -import pandas as pd -import xtgeo - -from webviz_subsurface._utils.perf_timer import PerfTimer - - -from ._grid_fmu_standard_discovery import GridFileInfo,GridParameterFileInfo -from .ensemble_grid_provider import EnsembleGridProvider - - -LOGGER = logging.getLogger(__name__) - -REm" -REL_OBS_DIR = "obs" -REL_STAT_CACHE_DIR = "stat_cache" - -# pylint: disable=too-few-public-methods -class Col: - TYPE = "type" - REAL = "real" - ATTRIBUTE = "attribute" - NAME = "name" - DATESTR = "datestr" - ORIGINAL_PATH = "original_path" - REL_PATH = "rel_path" - - -class SurfaceType(str, Enum): - OBSERVED = "observed" - SIMULATED = "simulated" - - -class ProviderImplFile(EnsembleGridProvider): - def __init__( - self, provider_id: str, provider_dir: Path, surface_inventory_df: pd.DataFrame - ) -> None: - self._provider_id = provider_id - self._provider_dir = provider_dir - self._inventory_df = surface_inventory_df - - @staticmethod - # pylint: disable=too-many-locals - def write_backing_store( - storage_dir: Path, - storage_key: str, - grids: List[GridFileInfo], - grid_parameters: List[GridParameterFileInfo], - avoid_copying_grid_data: bool, - ) -> None: - """If avoid_copying_grid_data if True, the specified grid data will NOT be copied - into the backing store, but will be referenced from their source locations. - Note that this is only useful when running in non-portable mode and will fail - in portable mode. - """ - - timer = PerfTimer() - - do_copy_grid_data_into_store = not avoid_copying_grid_data - - # All data for this provider will be stored inside a sub-directory - # given by the storage key - provider_dir = storage_dir / storage_key - LOGGER.debug(f"Writing grid data backing store to: {provider_dir}") - provider_dir.mkdir(parents=True, exist_ok=True) - - type_arr: List[SurfaceType] = [] - real_arr: List[int] = [] - attribute_arr: List[str] = [] - name_arr: List[str] = [] - datestr_arr: List[str] = [] - rel_path_arr: List[str] = [] - original_path_arr: List[str] = [] - gridnames = [grid.name for grid in grids] - for grid_parameter_info in grid_parameters: - if grid_parameter_info.name not in gridnames: - continue - name_arr.append(grid_parameter_info.name) - real_arr.append(grid_parameter_info.real) - attribute_arr.append(grid_parameter_info.attribute) - datestr_arr.append(grid_parameter_info.datestr if grid_parameter_info.datestr else "") - original_path_arr.append(grid_parameter_info.path) - - rel_path_in_store = "" - if do_copy_grid_data_into_store: - rel_path_in_store = _compose_rel_sim_surf_pathstr( - real=grid_parameter_info.real, - attribute=grid_parameter_info.attribute, - name=grid_parameter_info.name, - datestr=grid_parameter_info.datestr, - extension=Path(grid_parameter_info.path).suffix, - ) - - rel_path_arr.append(rel_path_in_store) - - - timer.lap_s() - if do_copy_grid_data_into_store: - LOGGER.debug( - f"Copying {len(original_path_arr)} surfaces into backing store..." - ) - _copy_grid_parameters_into_provider_dir( - original_path_arr, rel_path_arr, provider_dir - ) - et_copy_s = timer.lap_s() - - grid_inventory_df = pd.DataFrame( - { - Col.TYPE: type_arr, - Col.REAL: real_arr, - Col.ATTRIBUTE: attribute_arr, - Col.NAME: name_arr, - Col.DATESTR: datestr_arr, - Col.REL_PATH: rel_path_arr, - Col.ORIGINAL_PATH: original_path_arr, - } - ) - - parquet_file_name = provider_dir / "surface_inventory.parquet" - grid_inventory_df.to_parquet(path=parquet_file_name) - - if do_copy_grid_data_into_store: - LOGGER.debug( - f"Wrote surface backing store in: {timer.elapsed_s():.2f}s (" - f"copy={et_copy_s:.2f}s)" - ) - else: - LOGGER.debug( - f"Wrote surface backing store without copying surfaces in: " - f"{timer.elapsed_s():.2f}s" - ) - - @staticmethod - def from_backing_store( - storage_dir: Path, - storage_key: str, - ) -> Optional["ProviderImplFile"]: - - provider_dir = storage_dir / storage_key - parquet_file_name = provider_dir / "surface_inventory.parquet" - - try: - surface_inventory_df = pd.read_parquet(path=parquet_file_name) - return ProviderImplFile(storage_key, provider_dir, surface_inventory_df) - except FileNotFoundError: - return None - - def provider_id(self) -> str: - return self._provider_id - - def attributes(self) -> List[str]: - return sorted(list(self._inventory_df[Col.ATTRIBUTE].unique())) - - def surface_names_for_attribute(self, surface_attribute: str) -> List[str]: - return sorted( - list( - self._inventory_df.loc[ - self._inventory_df[Col.ATTRIBUTE] == surface_attribute - ][Col.NAME].unique() - ) - ) - - def surface_dates_for_attribute( - self, surface_attribute: str - ) -> Optional[List[str]]: - dates = sorted( - list( - self._inventory_df.loc[ - self._inventory_df[Col.ATTRIBUTE] == surface_attribute - ][Col.DATESTR].unique() - ) - ) - if len(dates) == 1 and not bool(dates[0]): - return None - - return dates - - def realizations(self) -> List[int]: - unique_reals = self._inventory_df[Col.REAL].unique() - - # Sort and strip out any entries with real == -1 - return sorted([r for r in unique_reals if r >= 0]) - - def get_surface( - self, - address: SurfaceAddress, - ) -> Optional[xtgeo.RegularSurface]: - if isinstance(address, StatisticalSurfaceAddress): - return self._get_or_create_statistical_surface(address) - # return self._create_statistical_surface(address) - if isinstance(address, SimulatedSurfaceAddress): - return self._get_simulated_surface(address) - if isinstance(address, ObservedSurfaceAddress): - return self._get_observed_surface(address) - - raise TypeError("Unknown type of surface address") - def _get_simulated_surface( - self, address: SimulatedSurfaceAddress - ) -> Optional[xtgeo.RegularSurface]: - """Returns a Xtgeo surface instance of a single realization surface""" - - timer = PerfTimer() - - surf_fns: List[str] = self._locate_grid_paramters( - attribute=address.attribute, - name=address.name, - datestr=address.datestr if address.datestr is not None else "", - realizations=[address.realization], - ) - - if len(surf_fns) == 0: - LOGGER.warning(f"No simulated surface found for {address}") - return None - if len(surf_fns) > 1: - LOGGER.warning( - f"Multiple simulated surfaces found for: {address}" - "Returning first surface." - ) - - surf = xtgeo.surface_from_file(surf_fns[0]) - - LOGGER.debug(f"Loaded simulated surface in: {timer.elapsed_s():.2f}s") - - return surf - - def _locate_grid_paramters( - self, attribute: str, name: str, datestr: str, realizations: List[int] - ) -> List[str]: - """Returns list of file names matching the specified filter criteria""" - df = self._inventory_df.loc[ - self._inventory_df[Col.TYPE] == SurfaceType.SIMULATED - ] - - df = df.loc[ - (df[Col.ATTRIBUTE] == attribute) - & (df[Col.NAME] == name) - & (df[Col.DATESTR] == datestr) - & (df[Col.REAL].isin(realizations)) - ] - - df = df[[Col.REL_PATH, Col.ORIGINAL_PATH]] - - # Return file name within backing store if the surface was copied there, - # otherwise return the original source file name - fn_list: List[str] = [] - for _index, row in df.iterrows(): - if row[Col.REL_PATH]: - fn_list.append(self._provider_dir / row[Col.REL_PATH]) - else: - fn_list.append(row[Col.ORIGINAL_PATH]) - - return fn_list - -def _copy_grid_parameters_into_provider_dir( - original_path_arr: List[str], - rel_path_arr: List[str], - provider_dir: Path, -) -> None: - for src_path, dst_rel_path in zip(original_path_arr, rel_path_arr): - # LOGGER.debug(f"copying surface from: {src_path}") - shutil.copyfile(src_path, provider_dir / dst_rel_path) - - # full_dst_path_arr = [storage_dir / dst_rel_path for dst_rel_path in store_path_arr] - # with ProcessPoolExecutor() as executor: - # executor.map(shutil.copyfile, original_path_arr, full_dst_path_arr) - - -def _compose_rel_sim_surf_pathstr( - real: int, - attribute: str, - name: str, - datestr: Optional[str], - extension: str, -) -> str: - """Compose path to simulated surface file, relative to provider's directory""" - if datestr: - fname = f"{real}--{name}--{attribute}--{datestr}{extension}" - else: - fname = f"{real}--{name}--{attribute}{extension}" - return str(Path(fname)) - diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py b/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py deleted file mode 100644 index b06c1468d..000000000 --- a/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider.py +++ /dev/null @@ -1,47 +0,0 @@ -import abc -from dataclasses import dataclass -from enum import Enum -from typing import List, Optional, Union - -import numpy as np - - -# Class provides data for ensemble surfaces -class EnsembleGridProvider(abc.ABC): - @abc.abstractmethod - def provider_id(self) -> str: - """Returns string ID of the provider.""" - - @abc.abstractmethod - def get_explicit_structured_grid_accessor(self, realization: int): - """Returns the esg accessor""" - - @abc.abstractmethod - def static_parameter_names(self) -> List[str]: - """Returns list of all available static parameters.""" - - @abc.abstractmethod - def dynamic_parameter_names(self) -> List[str]: - """Returns list of all available dynamic parameters.""" - - @abc.abstractmethod - def dates_for_dynamic_parameter( - self, dynamic_parameter: str - ) -> Optional[List[str]]: - """Returns list of all available dates for a given dynamic parameter.""" - - @abc.abstractmethod - def realizations(self) -> List[int]: - """Returns list of all available realizations.""" - - @abc.abstractmethod - def get_static_parameter_values( - self, parameter_name: str, realization: int - ) -> Optional[np.ndarray]: - """Returns 1d values for a given static parameter""" - - @abc.abstractmethod - def get_dynamic_parameter_values( - self, parameter_name: str, parameter_date: str, realization: int - ) -> Optional[np.ndarray]: - """Returns 1d values for a given dynamic parameter""" diff --git a/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py b/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py deleted file mode 100644 index bfd5bcac4..000000000 --- a/webviz_subsurface/_providers/_ensemble_grid_provider/ensemble_grid_provider_factory.py +++ /dev/null @@ -1,120 +0,0 @@ -import hashlib -import logging -import os -from pathlib import Path -from typing import List - -from webviz_config.webviz_factory import WebvizFactory -from webviz_config.webviz_factory_registry import WEBVIZ_FACTORY_REGISTRY -from webviz_config.webviz_instance_info import WebvizRunMode - -from webviz_subsurface._utils.perf_timer import PerfTimer - -from ._provider_impl_fmu_standard import ProviderImplFMUStandard -from ._grid_fmu_standard_discovery import discover_per_realization_grid_files -from .ensemble_grid_provider import EnsembleGridProvider - -LOGGER = logging.getLogger(__name__) - - -class EnsembleGridProviderFactory(WebvizFactory): - def __init__( - self, - root_storage_folder: Path, - allow_storage_writes: bool, - avoid_copying_grid_data: bool, - ) -> None: - self._storage_dir = Path(root_storage_folder) / __name__ - self._allow_storage_writes = allow_storage_writes - self._avoid_copying_grid_data = avoid_copying_grid_data - - LOGGER.info( - f"EnsembleGridProviderFactory init: storage_dir={self._storage_dir}" - ) - - if self._allow_storage_writes: - os.makedirs(self._storage_dir, exist_ok=True) - - @staticmethod - def instance() -> "EnsembleGridProviderFactory": - """Static method to access the singleton instance of the factory.""" - - factory = WEBVIZ_FACTORY_REGISTRY.get_factory(EnsembleGridProviderFactory) - if not factory: - app_instance_info = WEBVIZ_FACTORY_REGISTRY.app_instance_info - storage_folder = app_instance_info.storage_folder - allow_writes = app_instance_info.run_mode != WebvizRunMode.PORTABLE - dont_copy_grid_data = ( - app_instance_info.run_mode == WebvizRunMode.NON_PORTABLE - ) - - factory = EnsembleGridProviderFactory( - root_storage_folder=storage_folder, - allow_storage_writes=allow_writes, - avoid_copying_grid_data=dont_copy_grid_data, - ) - - # Store the factory object in the global factory registry - WEBVIZ_FACTORY_REGISTRY.set_factory(EnsembleGridProviderFactory, factory) - - return factory - - def create_from_fmu_standard_grid_files( - self, ens_path: str, attribute_filter: List[str] = None - ) -> EnsembleGridProvider: - timer = PerfTimer() - string_to_hash = ( - f"{ens_path}" - if attribute_filter is None - else f"{ens_path}_{'_'.join([str(attr) for attr in attribute_filter])}" - ) - storage_key = f"ens__{_make_hash_string(string_to_hash)}" - provider = ProviderImplFMUStandard.from_backing_store( - self._storage_dir, storage_key - ) - if provider: - LOGGER.info( - f"Loaded surface provider from backing store in {timer.elapsed_s():.2f}s (" - f"ens_path={ens_path})" - ) - return provider - - # We can only import data from data source if storage writes are allowed - if not self._allow_storage_writes: - raise ValueError(f"Failed to load surface provider for {ens_path}") - - LOGGER.info(f"Importing/copying grid data for: {ens_path}") - - timer.lap_s() - grid_files, grid_parameter_files = discover_per_realization_grid_files( - ens_path, attribute_filter - ) - - # As an optimization, avoid copying the grid data into the backing store, - # typically when we're running in non-portable mode - ProviderImplFMUStandard.write_backing_store( - self._storage_dir, - storage_key, - grids=grid_files, - grid_parameters=grid_parameter_files, - avoid_copying_grid_data=self._avoid_copying_grid_data, - ) - et_write_s = timer.lap_s() - - provider = ProviderImplFMUStandard.from_backing_store( - self._storage_dir, storage_key - ) - if not provider: - raise ValueError(f"Failed to load/create grid provider for {ens_path}") - - LOGGER.info( - f"Saved grid provider to backing store in {timer.elapsed_s():.2f}s (" - f" write={et_write_s:.2f}s, ens_path={ens_path})" - ) - - return provider - - -def _make_hash_string(string_to_hash: str) -> str: - # There is no security risk here and chances of collision should be very slim - return hashlib.md5(string_to_hash.encode()).hexdigest() # nosec From c398a51ca79fa08c25755659e622ad2e01c10e90 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 12 Apr 2022 12:33:14 +0200 Subject: [PATCH 34/63] Install webviz_vtk from temporary git location --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 39588b9ff..a4407be9f 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ "dash>=2.0.0", "dash_bootstrap_components>=0.10.3", "dash-daq>=0.5.0", - "dash-vtk>=0.0.9", + # "dash-vtk>=0.0.9", "dataclasses>=0.8; python_version<'3.7'", "defusedxml>=0.6.0", "ecl2df>=0.15.0; sys_platform=='linux'", @@ -105,6 +105,7 @@ "webviz-config>=0.3.8", "webviz-core-components>=0.5.6", "webviz-subsurface-components>=0.4.10", + "webviz_vtk@git+https://github.com/hanskallekleiv/webviz-vtk", "xtgeo>=2.18.0a1", ], extras_require={"tests": TESTS_REQUIRE}, From 754d76727249aa8c6c9e7c9fb89f291310effb58 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Wed, 20 Apr 2022 20:41:22 +0200 Subject: [PATCH 35/63] Remove pyvista --- .../_eclipse_grid_datamodel.py | 2 +- .../_explicit_structured_grid_accessor.py | 18 +++++++++--------- .../_roff_grid_datamodel.py | 1 - 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py index 7d0f14f16..e1bb448f8 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py @@ -2,12 +2,12 @@ from typing import Callable, List, Tuple import numpy as np -import pyvista as pv import xtgeo from webviz_subsurface._utils.perf_timer import PerfTimer from webviz_subsurface._utils.webvizstore_functions import get_path +from ._xtgeo_to_explicit_structured_grid import xtgeo_grid_to_explicit_structured_grid from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py index 45c76c4ea..eb0c4b576 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py @@ -1,7 +1,6 @@ from typing import List, Optional, Tuple import numpy as np -import pyvista as pv # pylint: disable=no-name-in-module, import-error from vtk.util.numpy_support import vtk_to_numpy @@ -12,6 +11,7 @@ vtkCellLocator, vtkExplicitStructuredGrid, vtkGenericCell, + vtkPolyData, ) # pylint: disable=no-name-in-module, @@ -24,7 +24,7 @@ class ExplicitStructuredGridAccessor: - def __init__(self, es_grid: pv.ExplicitStructuredGrid) -> None: + def __init__(self, es_grid: vtkExplicitStructuredGrid) -> None: self.es_grid = es_grid self.extract_skin_filter = ( vtkExplicitStructuredGridSurfaceFilter() @@ -49,12 +49,11 @@ def crop( grid = crop_filter.GetOutput() timer = PerfTimer() - grid = pv.ExplicitStructuredGrid(grid) print(f"to pyvista {timer.lap_s()}") return grid def extract_skin( - self, grid: pv.ExplicitStructuredGrid = None + self, grid: vtkExplicitStructuredGrid = None ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Extracts skin from a provided cropped grid or the entire grid if no grid is given. @@ -65,12 +64,12 @@ def extract_skin( self.extract_skin_filter.SetInputData(grid) self.extract_skin_filter.PassThroughCellIdsOn() self.extract_skin_filter.Update() - polydata = self.extract_skin_filter.GetOutput() - polydata = pv.PolyData(polydata) + polydata: vtkPolyData = self.extract_skin_filter.GetOutput() polys = vtk_to_numpy(polydata.GetPolys().GetData()) points = vtk_to_numpy(polydata.GetPoints().GetData()).ravel() - indices = polydata["vtkOriginalCellIds"] - + indices = vtk_to_numpy( + polydata.GetCellData().GetAbstractArray("vtkOriginalCellIds") + ) return ( polys, points.astype(np.float32), @@ -78,11 +77,12 @@ def extract_skin( ) def find_closest_cell_to_ray( - self, grid: pv.ExplicitStructuredGrid, ray: List[float] + self, grid: vtkExplicitStructuredGrid, ray: List[float] ) -> Tuple[Optional[int], List[Optional[int]]]: """Find the active cell closest to the given ray.""" timer = PerfTimer() locator = vtkCellLocator() + locator.SetDataSet(grid) locator.BuildLocator() diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py index c712890df..19a8c8f3b 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py @@ -2,7 +2,6 @@ from typing import Callable, List, Tuple import numpy as np -import pyvista as pv import xtgeo from webviz_subsurface._utils.perf_timer import PerfTimer From 99f9383eba880dc67a6bb3cac1e78a485129bf59 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Thu, 21 Apr 2022 13:28:19 +0200 Subject: [PATCH 36/63] Use 0-based indexing for ijk. Remove additional pyvista code --- .../test_explicit_structured_grid_accessor.py | 13 +++-- .../_explicit_structured_grid_accessor.py | 56 ++++++------------- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py index eb8a2a466..28d03d4f9 100644 --- a/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py +++ b/tests/unit_tests/plugin_tests/test_eclipse_grid_viewer/test_explicit_structured_grid_accessor.py @@ -1,9 +1,9 @@ # pylint: skip-file # type: ignore import pytest -import pyvista as pv +from vtk.util.numpy_support import vtk_to_numpy from vtkmodules.vtkCommonCore import vtkIdList -from vtkmodules.vtkCommonDataModel import vtkCellLocator +from vtkmodules.vtkCommonDataModel import vtkCellLocator, vtkExplicitStructuredGrid from webviz_subsurface.plugins._eclipse_grid_viewer._explicit_structured_grid_accessor import ( ExplicitStructuredGridAccessor, @@ -33,9 +33,12 @@ ) def test_crop(crop_range, expected_cells) -> None: cropped_grid = ES_GRID_ACCESSOR.crop(*crop_range) - assert isinstance(cropped_grid, pv.ExplicitStructuredGrid) - assert "vtkOriginalCellIds" in cropped_grid.array_names - assert set(cropped_grid["vtkOriginalCellIds"]) == set(expected_cells) + assert isinstance(cropped_grid, vtkExplicitStructuredGrid) + assert cropped_grid.GetCellData().HasArray("vtkOriginalCellIds") == 1 + assert set( + vtk_to_numpy(cropped_grid.GetCellData().GetAbstractArray("vtkOriginalCellIds")) + ) == set(expected_cells) + _polys, _points, indices = ES_GRID_ACCESSOR.extract_skin(cropped_grid) assert set(indices) == set(expected_cells) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py index eb0c4b576..8c3efb320 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py @@ -38,12 +38,12 @@ def crop( crop_filter = vtkExplicitStructuredGridCrop() crop_filter.SetInputData(self.es_grid) crop_filter.SetOutputWholeExtent( - irange[0] - 1, - irange[1] - 1, - jrange[0] - 1, - jrange[1] - 1, - krange[0] - 1, - krange[1] - 1, + irange[0], + irange[1] + 1, + jrange[0], + jrange[1] + 1, + krange[0], + krange[1] + 1, ) crop_filter.Update() @@ -89,28 +89,6 @@ def find_closest_cell_to_ray( # cell_ids = vtkIdList() tolerance = reference(0.0) - # # Find the cells intersected by the ray. (Ordered by near to far????) - # Apparently not! - - # locator.FindCellsAlongLine(ray[0], ray[1], tolerance, cell_ids) - - # We want the closest non-ghost(active) cell - # Check if ghost array is present and return first non-ghost cell - # if "vtkGhostType" in grid.array_names: - # for cell_idx in range(cell_ids.GetNumberOfIds()): - # cell_id = cell_ids.GetId(cell_idx) - # if grid["vtkGhostType"][cell_id] == 0: - # relative_cell_id = cell_id - # break - # else: - # relative_cell_id = cell_ids.GetId(0) - # for cell_idx in range(cell_ids.GetNumberOfIds()): - # print("test", cell_idx) - # print(cell_ids.GetId(cell_idx)) - # # If no cells are found return None - # if relative_cell_id is None: - # return None, [None, None, None] - _t = reference(0) _x = np.array([0, 0, 0]) _pcoords = np.array([0, 0, 0]) @@ -124,10 +102,10 @@ def find_closest_cell_to_ray( # # Check if an array with OriginalCellIds is present, and if so use # # that as the cell index, if not assume the grid is not cropped. - if "vtkOriginalCellIds" in grid.array_names: - cell_id = grid["vtkOriginalCellIds"][cell_id] - - # print(f"Closest cell in {timer.lap_s():.2f}") + if grid.GetCellData().HasArray("vtkOriginalCellIds") == 1: + cell_id = vtk_to_numpy( + grid.GetCellData().GetAbstractArray("vtkOriginalCellIds") + )[cell_id] i = reference(0) j = reference(0) @@ -137,28 +115,28 @@ def find_closest_cell_to_ray( self.es_grid.ComputeCellStructuredCoords(cell_id, i, j, k, False) print(f"Get ijk in {timer.lap_s():.2f}") - return cell_id, [int(i) + 1, int(j) + 1, int(k) + 1] + return cell_id, [int(i), int(j), int(k)] @property def imin(self) -> int: - return 1 + return 0 @property def imax(self) -> int: - return self.es_grid.dimensions[0] - 1 + return self.es_grid.dimensions[0] - 2 @property def jmin(self) -> int: - return 1 + return 0 @property def jmax(self) -> int: - return self.es_grid.dimensions[1] - 1 + return self.es_grid.dimensions[1] - 2 @property def kmin(self) -> int: - return 1 + return 0 @property def kmax(self) -> int: - return self.es_grid.dimensions[2] - 1 + return self.es_grid.dimensions[2] - 2 From e8cd1a5f5c01153098276eff805ded4cd162eed9 Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Mon, 25 Apr 2022 08:48:49 +0200 Subject: [PATCH 37/63] Testing usage of new VTK geometry data from xtgeo --- setup.py | 3 +- .../_explicit_structured_grid_accessor.py | 9 +- .../_xtgeo_to_explicit_structured_grid.py | 14 ++ ..._xtgeo_to_explicit_structured_grid_hack.py | 160 ++++++++++++++++++ .../_xtgeo_to_vtk_explicit_structured_grid.py | 102 +++++++++++ 5 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py create mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py diff --git a/setup.py b/setup.py index a4407be9f..44136f775 100644 --- a/setup.py +++ b/setup.py @@ -106,7 +106,8 @@ "webviz-core-components>=0.5.6", "webviz-subsurface-components>=0.4.10", "webviz_vtk@git+https://github.com/hanskallekleiv/webviz-vtk", - "xtgeo>=2.18.0a1", + "xtgeo@git+https://github.com/sigurdp/xtgeo/@sigurdp/vtk-esg", + # "xtgeo>=2.18.0a1", ], extras_require={"tests": TESTS_REQUIRE}, setup_requires=["setuptools_scm~=3.2"], diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py index 8c3efb320..8251d213f 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py @@ -26,6 +26,9 @@ class ExplicitStructuredGridAccessor: def __init__(self, es_grid: vtkExplicitStructuredGrid) -> None: self.es_grid = es_grid + self.cell_dimensions = [-1, -1, -1] + self.es_grid.GetCellDims(self.cell_dimensions) + self.extract_skin_filter = ( vtkExplicitStructuredGridSurfaceFilter() ) # Is this thread safe? @@ -123,7 +126,7 @@ def imin(self) -> int: @property def imax(self) -> int: - return self.es_grid.dimensions[0] - 2 + return self.cell_dimensions[0] - 1 @property def jmin(self) -> int: @@ -131,7 +134,7 @@ def jmin(self) -> int: @property def jmax(self) -> int: - return self.es_grid.dimensions[1] - 2 + return self.cell_dimensions[1] - 1 @property def kmin(self) -> int: @@ -139,4 +142,4 @@ def kmin(self) -> int: @property def kmax(self) -> int: - return self.es_grid.dimensions[2] - 2 + return self.cell_dimensions[2] - 1 diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py index 9ce081f34..641ec0483 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py @@ -1,10 +1,24 @@ import xtgeo import pyvista as pv +# The hack implementation requires both updated xtgeo and VTK version 9.2 +# from ._xtgeo_to_explicit_structured_grid_hack import ( +# xtgeo_grid_to_explicit_structured_grid_hack, +# ) + +# Requires updated xtgeo +from ._xtgeo_to_vtk_explicit_structured_grid import ( + xtgeo_grid_to_vtk_explicit_structured_grid, +) + def xtgeo_grid_to_explicit_structured_grid( xtg_grid: xtgeo.Grid, ) -> pv.ExplicitStructuredGrid: + + # return xtgeo_grid_to_explicit_structured_grid_hack(xtg_grid) + return xtgeo_grid_to_vtk_explicit_structured_grid(xtg_grid) + dims, corners, inactive = xtg_grid.get_vtk_geometries() corners[:, 2] *= -1 esg_grid = pv.ExplicitStructuredGrid(dims, corners) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py new file mode 100644 index 000000000..27793c19e --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py @@ -0,0 +1,160 @@ +import xtgeo +import pyvista as pv +import numpy as np + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkFiltersCore import vtkStaticCleanUnstructuredGrid +from vtkmodules.vtkFiltersCore import vtkUnstructuredGridToExplicitStructuredGrid +from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridToUnstructuredGrid +from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid +from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid +from vtkmodules.vtkCommonDataModel import vtkCellArray +from vtkmodules.vtkCommonCore import vtkPoints + +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.util.numpy_support import numpy_to_vtkIdTypeArray +from vtkmodules.util import vtkConstants + +from webviz_subsurface._utils.perf_timer import PerfTimer + + +# Note that this implementation requires both: +# * hacked xtgeo +# * VTK version 9.2 +# ----------------------------------------------------------------------------- +def xtgeo_grid_to_explicit_structured_grid_hack( + xtg_grid: xtgeo.Grid, +) -> pv.ExplicitStructuredGrid: + + print("entering xtgeo_grid_to_explicit_structured_grid_hack()") + t = PerfTimer() + + dims, corners, inactive = xtg_grid.get_vtk_geometries_hack() + corners[:, 2] *= -1 + print(f"call to get_vtk_geometries_hack() took {t.lap_s():.2f}s") + + # print(f"{dims=}") + # print(f"{type(corners)=}") + # print(f"{corners.shape=}") + # print(f"{corners.dtype=}") + + vtk_esgrid = _make_clean_vtk_esgrid(dims, corners) + print(f"create vtk_esgrid : {t.lap_s():.2f}s") + + pv_esgrid = pv.ExplicitStructuredGrid(vtk_esgrid) + print(f"create pv grid from vtk grid: {t.lap_s():.2f}s") + + pv_esgrid = pv_esgrid.hide_cells(inactive) + print(f"pv_esgrid.hide_cells(inactive) : {t.lap_s():.2f}s") + + print(f"xtgeo_grid_to_explicit_structured_grid_hack() - DONE: {t.elapsed_s():.2f}s") + + print("==================================================================") + print(pv_esgrid) + print("==================================================================") + + return pv_esgrid + + +# ----------------------------------------------------------------------------- +def _make_clean_vtk_esgrid(dims, corners): + + print("entering _make_clean_vtk_esgrid()") + + timer = PerfTimer() + + points_np = corners + points_np = points_np.reshape(-1, 3) + # points_np = points_np.astype(np.float32) + + points_vtkarr = numpy_to_vtk(points_np, deep=1) + vtk_points = vtkPoints() + vtk_points.SetData(points_vtkarr) + + print(f"_make_clean_vtk_esgrid() - create points: {timer.lap_s():.2f}s") + + # Dims are number of points, so subtract 1 to get cell counts + num_conn = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1) * 8 + conn_np = np.arange(0, num_conn) + + # cellconn_idarr = numpy_to_vtk(conn_np, deep=1, array_type=vtkConstants.VTK_ID_TYPE) + cellconn_idarr = numpy_to_vtkIdTypeArray(conn_np, deep=1) + + vtk_cellArray = vtkCellArray() + vtk_cellArray.SetData(8, cellconn_idarr) + + print(f"_make_clean_vtk_esgrid() - create cells: {timer.lap_s():.2f}s") + + vtk_esgrid = vtkExplicitStructuredGrid() + vtk_esgrid.SetDimensions(dims) + vtk_esgrid.SetPoints(vtk_points) + vtk_esgrid.SetCells(vtk_cellArray) + + print(f"_make_clean_vtk_esgrid() - create initial grid: {timer.lap_s():.2f}s") + + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + + ugrid = _vtk_esg_to_ug(vtk_esgrid) + print(f"_make_clean_vtk_esgrid() - esg to ug: {timer.lap_s():.2f}s") + ugrid = _clean_vtk_ug(ugrid) + print(f"_make_clean_vtk_esgrid() - clean ug: {timer.lap_s():.2f}s") + vtk_esgrid = _vtk_ug_to_esg(ugrid) + print(f"_make_clean_vtk_esgrid() - ug to esg: {timer.lap_s():.2f}s") + + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + + print(f"_make_clean_vtk_esgrid() - clean: {timer.lap_s():.2f}s") + + vtk_esgrid.ComputeFacesConnectivityFlagsArray() + + print(f"_make_clean_vtk_esgrid() - conn flags: {timer.lap_s():.2f}s") + + print(f"_make_clean_vtk_esgrid() - DONE: {timer.elapsed_s():.2f}s") + + return vtk_esgrid + + +# ----------------------------------------------------------------------------- +def _vtk_esg_to_ug(vtk_esgrid: vtkExplicitStructuredGrid) -> vtkUnstructuredGrid: + convertFilter = vtkExplicitStructuredGridToUnstructuredGrid() + convertFilter.SetInputData(vtk_esgrid) + convertFilter.Update() + vtk_ugrid = convertFilter.GetOutput() + + return vtk_ugrid + + +# ----------------------------------------------------------------------------- +def _vtk_ug_to_esg(vtk_ugrid: vtkUnstructuredGrid) -> vtkExplicitStructuredGrid: + convertFilter = vtkUnstructuredGridToExplicitStructuredGrid() + convertFilter.SetInputData(vtk_ugrid) + convertFilter.SetInputArrayToProcess(0, 0, 0, 1, "BLOCK_I") + convertFilter.SetInputArrayToProcess(1, 0, 0, 1, "BLOCK_J") + convertFilter.SetInputArrayToProcess(2, 0, 0, 1, "BLOCK_K") + convertFilter.Update() + vtk_esgrid = convertFilter.GetOutput() + + return vtk_esgrid + + +# ----------------------------------------------------------------------------- +def _clean_vtk_ug(vtk_ugrid: vtkUnstructuredGrid) -> vtkUnstructuredGrid: + + # !!!!!! + # Requires newer version of VTK + cleanfilter = vtkStaticCleanUnstructuredGrid() + # print(cleanfilter) + + cleanfilter.SetInputData(vtk_ugrid) + cleanfilter.SetAbsoluteTolerance(0.0) + cleanfilter.SetTolerance(0.0) + cleanfilter.SetToleranceIsAbsolute(True) + cleanfilter.GetLocator().SetTolerance(0.0) + cleanfilter.Update() + + # print(cleanfilter) + # print(cleanfilter.GetLocator()) + + vtk_ugrid_out = cleanfilter.GetOutput() + + return vtk_ugrid_out diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py new file mode 100644 index 000000000..d8c9f5a99 --- /dev/null +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py @@ -0,0 +1,102 @@ +import xtgeo +import numpy as np +import pyvista as pv + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid +from vtkmodules.vtkCommonDataModel import vtkCellArray +from vtkmodules.vtkCommonDataModel import vtkDataSetAttributes +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.util.numpy_support import numpy_to_vtkIdTypeArray +from vtkmodules.util.numpy_support import vtk_to_numpy +from vtkmodules.util import vtkConstants + +from webviz_subsurface._utils.perf_timer import PerfTimer + +# from ._xtgeo_to_explicit_structured_grid_hack import ( +# _clean_vtk_ug, +# _vtk_esg_to_ug, +# _vtk_ug_to_esg, +# ) + + +# ----------------------------------------------------------------------------- +def xtgeo_grid_to_vtk_explicit_structured_grid( + xtg_grid: xtgeo.Grid, +) -> vtkExplicitStructuredGrid: + + print("entering xtgeo_grid_to_vtk_explicit_structured_grid()") + t = PerfTimer() + + pt_dims, vertex_arr, conn_arr, inactive_arr = xtg_grid.get_vtk_esg_geometry_data() + vertex_arr[:, 2] *= -1 + print(f"get_vtk_esg_geometry_data() took {t.lap_s():.2f}s") + + print(f"{pt_dims=}") + print(f"{vertex_arr.shape=}") + print(f"{vertex_arr.dtype=}") + print(f"{conn_arr.shape=}") + print(f"{conn_arr.dtype=}") + + vtk_esgrid = _create_vtk_esgrid_from_verts_and_conn(pt_dims, vertex_arr, conn_arr) + print(f"create vtk_esgrid : {t.lap_s():.2f}s") + + # Make sure we hide the inactive cells. + # First we let VTK allocate cell ghost array, then we obtain a numpy view + # on the array and write to that (we're actually modifying the native VTK array) + ghost_arr_vtk = vtk_esgrid.AllocateCellGhostArray() + ghost_arr_np = vtk_to_numpy(ghost_arr_vtk) + ghost_arr_np[inactive_arr] = vtkDataSetAttributes.HIDDENCELL + print(f"hide {len(inactive_arr)} inactive cells : {t.lap_s():.2f}s") + + print(f"memory used by vtk_esgrid: {vtk_esgrid.GetActualMemorySize()/1024.0:.2f}MB") + + print(f"xtgeo_grid_to_vtk_explicit_structured_grid() - DONE: {t.elapsed_s():.2f}s") + + # print("==================================================================") + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + # print("==================================================================") + + return vtk_esgrid + + +# ----------------------------------------------------------------------------- +def _create_vtk_esgrid_from_verts_and_conn( + point_dims: np.ndarray, vertex_arr_np: np.ndarray, conn_arr_np: np.ndarray +) -> vtkExplicitStructuredGrid: + + print("_create_vtk_esgrid_from_verts_and_conn() - entering") + + t = PerfTimer() + + vertex_arr_np = vertex_arr_np.reshape(-1, 3) + points_vtkarr = numpy_to_vtk(vertex_arr_np, deep=1) + vtk_points = vtkPoints() + vtk_points.SetData(points_vtkarr) + print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_points: {t.lap_s():.2f}s") + + # conn_idarr = numpy_to_vtk(conn_arr_np, deep=1, array_type=vtkConstants.VTK_ID_TYPE) + conn_idarr = numpy_to_vtkIdTypeArray(conn_arr_np, deep=1) + vtk_cellArray = vtkCellArray() + vtk_cellArray.SetData(8, conn_idarr) + print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_cellArray: {t.lap_s():.2f}s") + + vtk_esgrid = vtkExplicitStructuredGrid() + vtk_esgrid.SetDimensions(point_dims) + vtk_esgrid.SetPoints(vtk_points) + vtk_esgrid.SetCells(vtk_cellArray) + print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_esgrid: {t.lap_s():.2f}s") + + vtk_esgrid.ComputeFacesConnectivityFlagsArray() + print(f"_create_vtk_esgrid_from_verts_and_conn() - conn flags: {t.lap_s():.2f}s") + + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + # ugrid = _vtk_esg_to_ug(vtk_esgrid) + # ugrid = _clean_vtk_ug(ugrid) + # vtk_esgrid = _vtk_ug_to_esg(ugrid) + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + + print(f"_create_vtk_esgrid_from_verts_and_conn() - DONE: {t.elapsed_s():.2f}s") + + return vtk_esgrid From 584f6057d302667f349d42bec33c8bebd476ec90 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Sat, 7 May 2022 19:25:52 +0200 Subject: [PATCH 38/63] provider --- .../ensemble_grid_provider/__init__.py | 3 + .../_roff_file_discovery.py | 117 ++++++ .../_xtgeo_to_vtk_explicit_structured_grid.py | 101 +++++ .../ensemble_grid_provider.py | 46 +++ .../ensemble_grid_provider_factory.py | 116 ++++++ .../grid_viz_service.py | 284 ++++++++++++++ .../provider_impl_roff.py | 355 ++++++++++++++++++ .../_eclipse_grid_viewer/_callbacks.py | 106 ++++-- .../plugins/_eclipse_grid_viewer/_layout.py | 31 +- .../plugins/_eclipse_grid_viewer/_plugin.py | 63 ++-- 10 files changed, 1151 insertions(+), 71 deletions(-) create mode 100644 webviz_subsurface/_providers/ensemble_grid_provider/__init__.py create mode 100644 webviz_subsurface/_providers/ensemble_grid_provider/_roff_file_discovery.py create mode 100644 webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py create mode 100644 webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider.py create mode 100644 webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py create mode 100644 webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py create mode 100644 webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/__init__.py b/webviz_subsurface/_providers/ensemble_grid_provider/__init__.py new file mode 100644 index 000000000..4a0ce7862 --- /dev/null +++ b/webviz_subsurface/_providers/ensemble_grid_provider/__init__.py @@ -0,0 +1,3 @@ +from .ensemble_grid_provider import EnsembleGridProvider +from .ensemble_grid_provider_factory import EnsembleGridProviderFactory +from .grid_viz_service import GridVizService, CellFilter, PropertySpec diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/_roff_file_discovery.py b/webviz_subsurface/_providers/ensemble_grid_provider/_roff_file_discovery.py new file mode 100644 index 000000000..57eb36f0f --- /dev/null +++ b/webviz_subsurface/_providers/ensemble_grid_provider/_roff_file_discovery.py @@ -0,0 +1,117 @@ +import glob +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Union, Tuple + +from fmu.ensemble import ScratchEnsemble + + +@dataclass(frozen=True) +class GridParameterFileInfo: + path: str + real: int + name: str + attribute: str + datestr: Optional[str] + + +@dataclass(frozen=True) +class GridParameterIdent: + name: str + attribute: str + datestr: Optional[str] + + +@dataclass(frozen=True) +class GridFileInfo: + path: str + real: int + name: str + + +@dataclass(frozen=True) +class GridIdent: + name: str + + +def _discover_ensemble_realizations_fmu(ens_path: str) -> Dict[int, str]: + """Returns dict indexed by realization number and with runpath as value""" + scratch_ensemble = ScratchEnsemble("dummyEnsembleName", paths=ens_path).filter("OK") + real_dict = {i: r.runpath() for i, r in scratch_ensemble.realizations.items()} + return real_dict + + +def _discover_ensemble_realizations(ens_path: str) -> Dict[int, str]: + # Much faster than FMU impl above, but is it risky? + # Do we need to check for OK-file? + real_dict: Dict[int, str] = {} + + realidxregexp = re.compile(r"realization-(\d+)") + globbed_real_dirs = sorted(glob.glob(str(ens_path))) + for real_dir in globbed_real_dirs: + realnum: Optional[int] = None + for path_comp in reversed(real_dir.split(os.path.sep)): + realmatch = re.match(realidxregexp, path_comp) + if realmatch: + realnum = int(realmatch.group(1)) + break + + if realnum is not None: + real_dict[realnum] = real_dir + + return real_dict + + +def ident_from_filename( + filename: str, +) -> Union[GridIdent, GridParameterIdent]: + """Split the stem part of the roff filename into grid name, attribute and + optionally date part""" + delimiter: str = "--" + parts = Path(filename).stem.split(delimiter) + if len(parts) == 1: + return GridIdent(name=parts[0]) + + return GridParameterIdent( + name=parts[0], attribute=parts[1], datestr=parts[2] if len(parts) >= 3 else None + ) + + +def discover_per_realization_grid_files( + ens_path: str, grid_name: str, attribute_filter: List[str] = None +) -> Tuple[List[GridFileInfo], List[GridParameterFileInfo]]: + rel_folder: str = "share/results/grids" + suffix: str = "*.roff" + + grid_parameters_info: List[GridParameterFileInfo] = [] + grid_info: List[GridFileInfo] = [] + real_dict = _discover_ensemble_realizations_fmu(ens_path) + for realnum, runpath in sorted(real_dict.items()): + globbed_filenames = glob.glob(str(Path(runpath) / rel_folder / suffix)) + for filename in sorted(globbed_filenames): + ident = ident_from_filename(filename) + if ident.name != grid_name: + continue + if isinstance(ident, GridParameterIdent): + if ( + attribute_filter is not None + and ident.attribute not in attribute_filter + ): + continue + grid_parameters_info.append( + GridParameterFileInfo( + path=filename, + real=realnum, + name=ident.name, + attribute=ident.attribute, + datestr=ident.datestr, + ) + ) + else: + grid_info.append( + GridFileInfo(path=filename, real=realnum, name=ident.name) + ) + + return grid_info, grid_parameters_info diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py b/webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py new file mode 100644 index 000000000..24d1da238 --- /dev/null +++ b/webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py @@ -0,0 +1,101 @@ +import xtgeo +import numpy as np + +# pylint: disable=no-name-in-module, +from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid +from vtkmodules.vtkCommonDataModel import vtkCellArray +from vtkmodules.vtkCommonDataModel import vtkDataSetAttributes +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.util.numpy_support import numpy_to_vtkIdTypeArray +from vtkmodules.util.numpy_support import vtk_to_numpy +from vtkmodules.util import vtkConstants + +from webviz_subsurface._utils.perf_timer import PerfTimer + +# from ._xtgeo_to_explicit_structured_grid_hack import ( +# _clean_vtk_ug, +# _vtk_esg_to_ug, +# _vtk_ug_to_esg, +# ) + + +# ----------------------------------------------------------------------------- +def xtgeo_grid_to_vtk_explicit_structured_grid( + xtg_grid: xtgeo.Grid, +) -> vtkExplicitStructuredGrid: + + print("entering xtgeo_grid_to_vtk_explicit_structured_grid()") + t = PerfTimer() + + pt_dims, vertex_arr, conn_arr, inactive_arr = xtg_grid.get_vtk_esg_geometry_data() + vertex_arr[:, 2] *= -1 + print(f"get_vtk_esg_geometry_data() took {t.lap_s():.2f}s") + + print(f"{pt_dims=}") + print(f"{vertex_arr.shape=}") + print(f"{vertex_arr.dtype=}") + print(f"{conn_arr.shape=}") + print(f"{conn_arr.dtype=}") + + vtk_esgrid = _create_vtk_esgrid_from_verts_and_conn(pt_dims, vertex_arr, conn_arr) + print(f"create vtk_esgrid : {t.lap_s():.2f}s") + + # Make sure we hide the inactive cells. + # First we let VTK allocate cell ghost array, then we obtain a numpy view + # on the array and write to that (we're actually modifying the native VTK array) + ghost_arr_vtk = vtk_esgrid.AllocateCellGhostArray() + ghost_arr_np = vtk_to_numpy(ghost_arr_vtk) + ghost_arr_np[inactive_arr] = vtkDataSetAttributes.HIDDENCELL + print(f"hide {len(inactive_arr)} inactive cells : {t.lap_s():.2f}s") + + print(f"memory used by vtk_esgrid: {vtk_esgrid.GetActualMemorySize()/1024.0:.2f}MB") + + print(f"xtgeo_grid_to_vtk_explicit_structured_grid() - DONE: {t.elapsed_s():.2f}s") + + # print("==================================================================") + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + # print("==================================================================") + + return vtk_esgrid + + +# ----------------------------------------------------------------------------- +def _create_vtk_esgrid_from_verts_and_conn( + point_dims: np.ndarray, vertex_arr_np: np.ndarray, conn_arr_np: np.ndarray +) -> vtkExplicitStructuredGrid: + + print("_create_vtk_esgrid_from_verts_and_conn() - entering") + + t = PerfTimer() + + vertex_arr_np = vertex_arr_np.reshape(-1, 3) + points_vtkarr = numpy_to_vtk(vertex_arr_np, deep=1) + vtk_points = vtkPoints() + vtk_points.SetData(points_vtkarr) + print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_points: {t.lap_s():.2f}s") + + # conn_idarr = numpy_to_vtk(conn_arr_np, deep=1, array_type=vtkConstants.VTK_ID_TYPE) + conn_idarr = numpy_to_vtkIdTypeArray(conn_arr_np, deep=1) + vtk_cellArray = vtkCellArray() + vtk_cellArray.SetData(8, conn_idarr) + print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_cellArray: {t.lap_s():.2f}s") + + vtk_esgrid = vtkExplicitStructuredGrid() + vtk_esgrid.SetDimensions(point_dims) + vtk_esgrid.SetPoints(vtk_points) + vtk_esgrid.SetCells(vtk_cellArray) + print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_esgrid: {t.lap_s():.2f}s") + + vtk_esgrid.ComputeFacesConnectivityFlagsArray() + print(f"_create_vtk_esgrid_from_verts_and_conn() - conn flags: {t.lap_s():.2f}s") + + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + # ugrid = _vtk_esg_to_ug(vtk_esgrid) + # ugrid = _clean_vtk_ug(ugrid) + # vtk_esgrid = _vtk_ug_to_esg(ugrid) + # print(pv.ExplicitStructuredGrid(vtk_esgrid)) + + print(f"_create_vtk_esgrid_from_verts_and_conn() - DONE: {t.elapsed_s():.2f}s") + + return vtk_esgrid diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider.py b/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider.py new file mode 100644 index 000000000..ee9e18509 --- /dev/null +++ b/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider.py @@ -0,0 +1,46 @@ +import abc +from typing import List, Optional + +import xtgeo +import numpy as np + + +class EnsembleGridProvider(abc.ABC): + @abc.abstractmethod + def provider_id(self) -> str: + """Returns string ID of the provider.""" + + @abc.abstractmethod + def static_property_names(self) -> List[str]: + """Returns list of all available static properties.""" + + @abc.abstractmethod + def dynamic_property_names(self) -> List[str]: + """Returns list of all available dynamic properties.""" + + @abc.abstractmethod + def dates_for_dynamic_property(self, property_name: str) -> Optional[List[str]]: + """Returns list of all available dates for a given dynamic property.""" + + @abc.abstractmethod + def realizations(self) -> List[int]: + """Returns list of all available realizations.""" + + @abc.abstractmethod + def get_3dgrid( + self, + realization: int, + ) -> Optional[xtgeo.Grid]: + """Returns grid for specified realization""" + + @abc.abstractmethod + def get_static_property_values( + self, property_name: str, realization: int + ) -> Optional[np.ndarray]: + """Returns 1d cell values for a given static property""" + + @abc.abstractmethod + def get_dynamic_property_values( + self, property_name: str, property_date: str, realization: int + ) -> Optional[np.ndarray]: + """Returns 1d cell values for a given dynamic property""" diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py b/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py new file mode 100644 index 000000000..de4495346 --- /dev/null +++ b/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py @@ -0,0 +1,116 @@ +import hashlib +import logging +import os +from pathlib import Path +from typing import List + +from webviz_config.webviz_factory import WebvizFactory +from webviz_config.webviz_factory_registry import WEBVIZ_FACTORY_REGISTRY +from webviz_config.webviz_instance_info import WebvizRunMode + +from webviz_subsurface._utils.perf_timer import PerfTimer + +from .provider_impl_roff import ProviderImplRoff +from ._roff_file_discovery import discover_per_realization_grid_files +from .ensemble_grid_provider import EnsembleGridProvider + +LOGGER = logging.getLogger(__name__) + + +class EnsembleGridProviderFactory(WebvizFactory): + def __init__( + self, + root_storage_folder: Path, + allow_storage_writes: bool, + avoid_copying_grid_data: bool, + ) -> None: + self._storage_dir = Path(root_storage_folder) / __name__ + self._allow_storage_writes = allow_storage_writes + self._avoid_copying_grid_data = avoid_copying_grid_data + + LOGGER.info( + f"EnsembleGridProviderFactory init: storage_dir={self._storage_dir}" + ) + + if self._allow_storage_writes: + os.makedirs(self._storage_dir, exist_ok=True) + + @staticmethod + def instance() -> "EnsembleGridProviderFactory": + """Static method to access the singleton instance of the factory.""" + + factory = WEBVIZ_FACTORY_REGISTRY.get_factory(EnsembleGridProviderFactory) + if not factory: + app_instance_info = WEBVIZ_FACTORY_REGISTRY.app_instance_info + storage_folder = app_instance_info.storage_folder + allow_writes = app_instance_info.run_mode != WebvizRunMode.PORTABLE + dont_copy_grid_data = ( + app_instance_info.run_mode == WebvizRunMode.NON_PORTABLE + ) + + factory = EnsembleGridProviderFactory( + root_storage_folder=storage_folder, + allow_storage_writes=allow_writes, + avoid_copying_grid_data=dont_copy_grid_data, + ) + + # Store the factory object in the global factory registry + WEBVIZ_FACTORY_REGISTRY.set_factory(EnsembleGridProviderFactory, factory) + + return factory + + def create_from_roff_files( + self, ens_path: str, grid_name: str, attribute_filter: List[str] = None + ) -> EnsembleGridProvider: + timer = PerfTimer() + string_to_hash = ( + f"{ens_path}" + if attribute_filter is None + else f"{ens_path}_{'_'.join([str(attr) for attr in attribute_filter])}" + ) + storage_key = f"ens__{_make_hash_string(string_to_hash)}" + provider = ProviderImplRoff.from_backing_store(self._storage_dir, storage_key) + if provider: + LOGGER.info( + f"Loaded grid provider from backing store in {timer.elapsed_s():.2f}s (" + f"ens_path={ens_path})" + ) + return provider + + # We can only import data from data source if storage writes are allowed + if not self._allow_storage_writes: + raise ValueError(f"Failed to load grid provider for {ens_path}") + + LOGGER.info(f"Importing/copying grid data for: {ens_path}") + + timer.lap_s() + grid_info, grid_parameters_info = discover_per_realization_grid_files( + ens_path, grid_name, attribute_filter + ) + + # As an optimization, avoid copying the grid data into the backing store, + # typically when we're running in non-portable mode + ProviderImplRoff.write_backing_store( + self._storage_dir, + storage_key, + grid_geometries_info=grid_info, + grid_parameters_info=grid_parameters_info, + avoid_copying_grid_data=self._avoid_copying_grid_data, + ) + et_write_s = timer.lap_s() + + provider = ProviderImplRoff.from_backing_store(self._storage_dir, storage_key) + if not provider: + raise ValueError(f"Failed to load/create grid provider for {ens_path}") + + LOGGER.info( + f"Saved grid provider to backing store in {timer.elapsed_s():.2f}s (" + f" write={et_write_s:.2f}s, ens_path={ens_path})" + ) + + return provider + + +def _make_hash_string(string_to_hash: str) -> str: + # There is no security risk here and chances of collision should be very slim + return hashlib.md5(string_to_hash.encode()).hexdigest() # nosec diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py new file mode 100644 index 000000000..2b3da1082 --- /dev/null +++ b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py @@ -0,0 +1,284 @@ +import logging +from pathlib import Path +from typing import Callable, List, Tuple, Dict, Optional, Any +from dataclasses import dataclass +import dataclasses +from pathlib import Path + +import numpy as np + +import xtgeo + +from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop +from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter + +from vtkmodules.vtkCommonDataModel import ( + vtkExplicitStructuredGrid, + vtkPolyData, +) + +from vtkmodules.util.numpy_support import vtk_to_numpy + + +from .ensemble_grid_provider import EnsembleGridProvider + +# Requires updated xtgeo +from ._xtgeo_to_vtk_explicit_structured_grid import ( + xtgeo_grid_to_vtk_explicit_structured_grid, +) + +from webviz_subsurface._utils.perf_timer import PerfTimer + +LOGGER = logging.getLogger(__name__) + + +@dataclass +class PropertySpec: + prop_name: str + prop_date: Optional[str] + + +@dataclass +class CellFilter: + i_min: int + i_max: int + j_min: int + j_max: int + k_min: int + k_max: int + + +@dataclass +class SurfacePolys: + point_arr: np.ndarray + poly_arr: np.ndarray + + +@dataclass +class PropertyScalars: + value_arr: np.ndarray + # min_value: float + # max_value: float + + +class GridWorker: + def __init__(self, full_esgrid: vtkExplicitStructuredGrid) -> None: + self._full_esgrid = full_esgrid + + self._cached_cell_filter: Optional[CellFilter] = None + self._cached_original_cell_indices: Optional[np.ndarray] = None + + def get_full_esgrid(self) -> vtkExplicitStructuredGrid: + return self._full_esgrid + + def get_cached_original_cell_indices( + self, cell_filter: Optional[CellFilter] + ) -> Optional[np.ndarray]: + if not self._cached_original_cell_indices: + return None + + if cell_filter == self._cached_cell_filter: + return self._cached_original_cell_indices + + return None + + def set_cached_original_cell_indices( + self, cell_filter: Optional[CellFilter], original_cell_indices: np.ndarray + ) -> None: + # Make copy of the cell filter + self._cached_cell_filter = dataclasses.replace(cell_filter) + self._cached_original_cell_indices = original_cell_indices + + +class GridVizService: + def __init__(self) -> None: + self._id_to_provider_dict: Dict[str, EnsembleGridProvider] = {} + self._key_to_worker_dict: Dict[str, GridWorker] = {} + + @staticmethod + def instance() -> "GridVizService": + global GRID_VIZ_SERVICE_INSTANCE + if not GRID_VIZ_SERVICE_INSTANCE: + LOGGER.debug("Initializing GridVizService instance") + GRID_VIZ_SERVICE_INSTANCE = GridVizService() + + return GRID_VIZ_SERVICE_INSTANCE + + def register_provider(self, provider: EnsembleGridProvider) -> None: + provider_id = provider.provider_id() + LOGGER.debug(f"Adding grid provider with id={provider_id}") + + existing_provider = self._id_to_provider_dict.get(provider_id) + if existing_provider: + # Issue a warning if there already is a provider registered with the same + # id AND if the actual provider instance is different. + # This wil happen until the provider factory gets caching. + if existing_provider is not provider: + LOGGER.warning( + f"Provider with id={provider_id} ignored, the id is already present" + ) + return + + self._id_to_provider_dict[provider_id] = provider + + def get_surface( + self, + provider_id: str, + realization: int, + property_spec: Optional[PropertySpec], + cell_filter: Optional[CellFilter], + ) -> Tuple[SurfacePolys, Optional[PropertyScalars]]: + + provider = self._id_to_provider_dict.get(provider_id) + if not provider: + raise ValueError("Could not find provider") + + worker = self._get_or_create_grid_worker(provider_id, realization) + if not worker: + raise ValueError("Could not get grid worker") + + grid = worker.get_full_esgrid() + + if cell_filter: + grid = _calc_cropped_grid(grid, cell_filter) + + polydata = _calc_grid_surface(grid) + + # !!!!!! + # Need to watch out here, think these may go out of scope! + points_np = vtk_to_numpy(polydata.GetPoints().GetData()).ravel() + polys_np = vtk_to_numpy(polydata.GetPolys().GetData()) + original_cell_indices_np = vtk_to_numpy( + polydata.GetCellData().GetAbstractArray("vtkOriginalCellIds") + ) + + surface_polys = SurfacePolys(point_arr=points_np, poly_arr=polys_np) + + property_scalars: Optional[PropertyScalars] = None + if property_spec: + if property_spec.prop_date: + raw_cell_values = provider.get_dynamic_property_values( + property_spec.prop_name, property_spec.prop_date, realization + ) + else: + raw_cell_values = provider.get_static_property_values( + property_spec.prop_name, realization + ) + if raw_cell_values is not None: + mapped_cell_values = raw_cell_values[original_cell_indices_np] + property_scalars = PropertyScalars(value_arr=mapped_cell_values) + + worker.set_cached_original_cell_indices(cell_filter, original_cell_indices_np) + + return surface_polys, property_scalars + + def get_mapped_property_values( + self, + provider_id: str, + realization: int, + property_spec: PropertySpec, + cell_filter: Optional[CellFilter], + ) -> Optional[PropertyScalars]: + + provider = self._id_to_provider_dict.get(provider_id) + if not provider: + raise ValueError("Could not find provider") + + worker = self._get_or_create_grid_worker(provider_id, realization) + if not worker: + raise ValueError("Could not get grid worker") + + if property_spec.prop_date: + raw_cell_values = provider.get_dynamic_property_values( + property_spec.prop_name, property_spec.prop_date, realization + ) + else: + raw_cell_values = provider.get_static_property_values( + property_spec.prop_name, realization + ) + + if not raw_cell_values: + return None + + original_cell_indices_np = worker.get_cached_original_cell_indices(cell_filter) + if original_cell_indices_np: + mapped_cell_values = raw_cell_values[original_cell_indices_np] + return PropertyScalars(mapped_cell_values) + + # Must first generate the grid to get the original cell indices + grid = worker.get_full_esgrid() + if cell_filter: + grid = _calc_cropped_grid(grid, cell_filter) + + polydata = _calc_grid_surface(grid) + original_cell_indices_np = vtk_to_numpy( + polydata.GetCellData().GetAbstractArray("vtkOriginalCellIds") + ) + mapped_cell_values = raw_cell_values[original_cell_indices_np] + + worker.set_cached_original_cell_indices(cell_filter, original_cell_indices_np) + + return PropertyScalars(value_arr=mapped_cell_values) + + def ray_pick( + self, + provider_id: str, + realization: int, + ray: List[float], + property_spec: Optional[PropertySpec], + cell_filter: Optional[CellFilter], + ) -> None: + # TODO!!! + pass + + def _get_or_create_grid_worker( + self, provider_id: str, realization: int + ) -> Optional[GridWorker]: + + worker_key = f"P{provider_id}__R{realization}" + worker = self._key_to_worker_dict.get(worker_key) + if worker: + return worker + + provider = self._id_to_provider_dict.get(provider_id) + if not provider: + raise ValueError("Could not find provider") + + xtg_grid = provider.get_3dgrid(realization=realization) + vtk_esg = xtgeo_grid_to_vtk_explicit_structured_grid(xtg_grid) + + worker = GridWorker(vtk_esg) + self._key_to_worker_dict[worker_key] = worker + + return worker + + +def _calc_cropped_grid( + esgrid: vtkExplicitStructuredGrid, cell_filter: CellFilter +) -> vtkExplicitStructuredGrid: + crop_filter = vtkExplicitStructuredGridCrop() + crop_filter.SetInputData(esgrid) + crop_filter.SetOutputWholeExtent( + cell_filter.i_min, + cell_filter.i_max, + cell_filter.j_min, + cell_filter.j_max, + cell_filter.j_min, + cell_filter.j_max, + ) + crop_filter.Update() + cropped_grid = crop_filter.GetOutput() + return cropped_grid + + +def _calc_grid_surface(esgrid: vtkExplicitStructuredGrid) -> vtkPolyData: + surf_filter = vtkExplicitStructuredGridSurfaceFilter() + surf_filter.SetInputData(esgrid) + surf_filter.PassThroughCellIdsOn() + surf_filter.Update() + + polydata: vtkPolyData = surf_filter.GetOutput() + return polydata + + +GRID_VIZ_SERVICE_INSTANCE: Optional[GridVizService] = None diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py new file mode 100644 index 000000000..a47d20e7e --- /dev/null +++ b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py @@ -0,0 +1,355 @@ +import logging +import shutil +import warnings +from enum import Enum +from pathlib import Path +from typing import List, Optional, Set + +import numpy as np +import pandas as pd +import xtgeo + +from webviz_subsurface._utils.perf_timer import PerfTimer + + +from ._roff_file_discovery import GridFileInfo, GridParameterFileInfo +from .ensemble_grid_provider import EnsembleGridProvider + + +LOGGER = logging.getLogger(__name__) + + +# pylint: disable=too-few-public-methods +class Col: + TYPE = "type" + REAL = "real" + ATTRIBUTE = "attribute" + NAME = "name" + DATESTR = "datestr" + ORIGINAL_PATH = "original_path" + REL_PATH = "rel_path" + + +class GridType(str, Enum): + GEOMETRY = "geometry" + STATIC_PROPERTY = "static_property" + DYNAMIC_PROPERTY = "dynamic_property" + + +class ProviderImplRoff(EnsembleGridProvider): + def __init__( + self, provider_id: str, provider_dir: Path, grid_inventory_df: pd.DataFrame + ) -> None: + self._provider_id = provider_id + self._provider_dir = provider_dir + self._inventory_df = grid_inventory_df + + @staticmethod + # pylint: disable=too-many-locals + def write_backing_store( + storage_dir: Path, + storage_key: str, + grid_geometries_info: List[GridFileInfo], + grid_parameters_info: List[GridParameterFileInfo], + avoid_copying_grid_data: bool, + ) -> None: + """If avoid_copying_grid_data if True, the specified grid data will NOT be copied + into the backing store, but will be referenced from their source locations. + Note that this is only useful when running in non-portable mode and will fail + in portable mode. + """ + + timer = PerfTimer() + + do_copy_grid_data_into_store = not avoid_copying_grid_data + + # All data for this provider will be stored inside a sub-directory + # given by the storage key + provider_dir = storage_dir / storage_key + LOGGER.debug(f"Writing grid data backing store to: {provider_dir}") + provider_dir.mkdir(parents=True, exist_ok=True) + + type_arr: List[GridType] = [] + real_arr: List[int] = [] + attribute_arr: List[str] = [] + name_arr: List[str] = [] + datestr_arr: List[str] = [] + rel_path_arr: List[str] = [] + original_path_arr: List[str] = [] + for grid_info in grid_geometries_info: + type_arr.append(GridType.GEOMETRY) + name_arr.append(grid_info.name) + real_arr.append(grid_info.real) + attribute_arr.append("") + datestr_arr.append("") + original_path_arr.append(grid_info.path) + rel_path_in_store = "" + + if do_copy_grid_data_into_store: + rel_path_in_store = _compose_rel_grid_pathstr( + real=grid_info.real, + attribute=None, + name=grid_info.name, + datestr=None, + extension=Path(grid_info.path).suffix, + ) + + rel_path_arr.append(rel_path_in_store) + + for grid_parameter_info in grid_parameters_info: + name_arr.append(grid_parameter_info.name) + real_arr.append(grid_parameter_info.real) + attribute_arr.append(grid_parameter_info.attribute) + if grid_parameter_info.datestr: + datestr_arr.append(grid_parameter_info.datestr) + type_arr.append(GridType.DYNAMIC_PROPERTY) + else: + datestr_arr.append("") + type_arr.append(GridType.STATIC_PROPERTY) + + original_path_arr.append(grid_parameter_info.path) + + rel_path_in_store = "" + if do_copy_grid_data_into_store: + rel_path_in_store = _compose_rel_grid_pathstr( + real=grid_parameter_info.real, + attribute=grid_parameter_info.attribute, + name=grid_parameter_info.name, + datestr=grid_parameter_info.datestr, + extension=Path(grid_parameter_info.path).suffix, + ) + + rel_path_arr.append(rel_path_in_store) + + timer.lap_s() + if do_copy_grid_data_into_store: + LOGGER.debug( + f"Copying {len(original_path_arr)} grid data into backing store..." + ) + _copy_grid_parameters_into_provider_dir( + original_path_arr, rel_path_arr, provider_dir + ) + et_copy_s = timer.lap_s() + + grid_inventory_df = pd.DataFrame( + { + Col.TYPE: type_arr, + Col.REAL: real_arr, + Col.ATTRIBUTE: attribute_arr, + Col.NAME: name_arr, + Col.DATESTR: datestr_arr, + Col.REL_PATH: rel_path_arr, + Col.ORIGINAL_PATH: original_path_arr, + } + ) + + parquet_file_name = provider_dir / "grid_inventory.parquet" + grid_inventory_df.to_parquet(path=parquet_file_name) + + if do_copy_grid_data_into_store: + LOGGER.debug( + f"Wrote grid backing store in: {timer.elapsed_s():.2f}s (" + f"copy={et_copy_s:.2f}s)" + ) + else: + LOGGER.debug( + f"Wrote grid backing store without copying grid data in: " + f"{timer.elapsed_s():.2f}s" + ) + + @staticmethod + def from_backing_store( + storage_dir: Path, + storage_key: str, + ) -> Optional["ProviderImplRoff"]: + + provider_dir = storage_dir / storage_key + parquet_file_name = provider_dir / "grid_inventory.parquet" + + try: + grid_inventory_df = pd.read_parquet(path=parquet_file_name) + return ProviderImplRoff(storage_key, provider_dir, grid_inventory_df) + except FileNotFoundError: + return None + + def provider_id(self) -> str: + return self._provider_id + + def static_property_names(self) -> List[str]: + return sorted( + list( + self._inventory_df.loc[ + self._inventory_df[Col.TYPE] == GridType.STATIC_PROPERTY + ][Col.ATTRIBUTE].unique() + ) + ) + + def dynamic_property_names(self) -> List[str]: + return sorted( + list( + self._inventory_df.loc[ + self._inventory_df[Col.TYPE] == GridType.DYNAMIC_PROPERTY + ][Col.ATTRIBUTE].unique() + ) + ) + + def dates_for_dynamic_property(self, property_name: str) -> Optional[List[str]]: + dates = sorted( + list( + self._inventory_df.loc[ + (self._inventory_df[Col.TYPE] == GridType.DYNAMIC_PROPERTY) + & ((self._inventory_df[Col.ATTRIBUTE] == property_name)) + ][Col.ATTRIBUTE].unique() + ) + ) + if len(dates) == 1 and not bool(dates[0]): + return None + + return dates + + def realizations(self) -> List[int]: + unique_reals = self._inventory_df[Col.REAL].unique() + + # Sort and strip out any entries with real == -1 + return sorted([r for r in unique_reals if r >= 0]) + + def get_3dgrid(self, realization: int) -> xtgeo.Grid: + df = self._inventory_df.loc[self._inventory_df[Col.TYPE] == GridType.GEOMETRY] + print(df) + df = df.loc[df[Col.REAL] == realization] + + df = df[[Col.REL_PATH, Col.ORIGINAL_PATH]] + fn_list: List[str] = [] + for _index, row in df.iterrows(): + if row[Col.REL_PATH]: + fn_list.append(self._provider_dir / row[Col.REL_PATH]) + else: + fn_list.append(row[Col.ORIGINAL_PATH]) + if len(fn_list) == 0: + LOGGER.warning(f"No grid geometry found for realization {realization}") + return None + if len(fn_list) > 1: + raise ValueError( + f"Multiple grid geometries found for: {realization}" + "Something has gone terribly wrong." + ) + + grid = xtgeo.grid_from_file(fn_list[0]) + return grid + + def get_static_property_values( + self, property_name: str, realization: int + ) -> Optional[np.ndarray]: + fn_list: List[str] = self._locate_static_property( + property_name=property_name, realizations=[realization] + ) + if len(fn_list) == 0: + LOGGER.warning(f"No grid parameter found for realization {realization}") + return None + if len(fn_list) > 1: + raise ValueError( + f"Multiple grid parameters found for: {realization}" + "Something has gone terribly wrong." + ) + grid_property = xtgeo.gridproperty_from_file(fn_list[0]) + return grid_property.get_npvalues1d(order="F").ravel() + + def get_dynamic_property_values( + self, property_name: str, property_date: str, realization: int + ) -> Optional[np.ndarray]: + fn_list: List[str] = self._locate_dynamic_property( + property_name=property_name, + property_datestr=property_date, + realizations=[realization], + ) + if len(fn_list) == 0: + LOGGER.warning(f"No grid parameter found for realization {realization}") + return None + if len(fn_list) > 1: + raise ValueError( + f"Multiple grid parameters found for: {realization}" + "Something has gone terribly wrong." + ) + grid_property = xtgeo.gridproperty_from_file(fn_list[0]) + return grid_property.get_npvalues1d(order="F").ravel() + + def _locate_static_property( + self, property_name: str, realizations: List[int] + ) -> List[str]: + """Returns list of file names matching the specified filter criteria""" + df = self._inventory_df.loc[ + self._inventory_df[Col.TYPE] == GridType.STATIC_PROPERTY + ] + + df = df.loc[ + (df[Col.ATTRIBUTE] == property_name) & (df[Col.REAL].isin(realizations)) + ] + + df = df[[Col.REL_PATH, Col.ORIGINAL_PATH]] + + # Return file name within backing store if the data was copied there, + # otherwise return the original source file name + fn_list: List[str] = [] + for _index, row in df.iterrows(): + if row[Col.REL_PATH]: + fn_list.append(self._provider_dir / row[Col.REL_PATH]) + else: + fn_list.append(row[Col.ORIGINAL_PATH]) + + return fn_list + + def _locate_dynamic_property( + self, property_name: str, property_datestr: str, realizations: List[int] + ) -> List[str]: + """Returns list of file names matching the specified filter criteria""" + df = self._inventory_df.loc[ + self._inventory_df[Col.TYPE] == GridType.DYNAMIC_PROPERTY + ] + + df = df.loc[ + (df[Col.ATTRIBUTE] == property_name) + & (df[Col.DATESTR] == property_datestr) + & (df[Col.REAL].isin(realizations)) + ] + + df = df[[Col.REL_PATH, Col.ORIGINAL_PATH]] + + # Return file name within backing store if the data was copied there, + # otherwise return the original source file name + fn_list: List[str] = [] + for _index, row in df.iterrows(): + if row[Col.REL_PATH]: + fn_list.append(self._provider_dir / row[Col.REL_PATH]) + else: + fn_list.append(row[Col.ORIGINAL_PATH]) + + return fn_list + + +def _copy_grid_parameters_into_provider_dir( + original_path_arr: List[str], + rel_path_arr: List[str], + provider_dir: Path, +) -> None: + for src_path, dst_rel_path in zip(original_path_arr, rel_path_arr): + shutil.copyfile(src_path, provider_dir / dst_rel_path) + + # full_dst_path_arr = [storage_dir / dst_rel_path for dst_rel_path in store_path_arr] + # with ProcessPoolExecutor() as executor: + # executor.map(shutil.copyfile, original_path_arr, full_dst_path_arr) + + +def _compose_rel_grid_pathstr( + real: int, + name: str, + attribute: Optional[str], + datestr: Optional[str], + extension: str, +) -> str: + """Compose path to grid file, relative to provider's directory""" + if not attribute and not datestr: + return str(Path(f"{real}--{name}{extension}")) + if not datestr: + return str(Path(f"{real}--{name}--{attribute}--{datestr}{extension}")) + + return str(Path(f"{real}--{name}--{attribute}{extension}")) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 2f07583bd..36caba6c4 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -8,19 +8,24 @@ from webviz_vtk.utils.vtk import b64_encode_numpy from webviz_subsurface._utils.perf_timer import PerfTimer - -from ._eclipse_grid_datamodel import EclipseGridDataModel -from ._roff_grid_datamodel import RoffGridDataModel +from webviz_subsurface._providers.ensemble_grid_provider import ( + EnsembleGridProvider, + GridVizService, + PropertySpec, + CellFilter, +) from ._layout import PROPERTYTYPE, LayoutElements, GRID_DIRECTION -def plugin_callbacks(get_uuid: Callable, datamodel: EclipseGridDataModel) -> None: +def plugin_callbacks( + get_uuid: Callable, + grid_provider: EnsembleGridProvider, + grid_viz_service: GridVizService, +) -> None: @callback( Output(get_uuid(LayoutElements.PROPERTIES), "options"), Output(get_uuid(LayoutElements.PROPERTIES), "value"), - Output(get_uuid(LayoutElements.DATES), "options"), - Output(get_uuid(LayoutElements.DATES), "value"), Input(get_uuid(LayoutElements.INIT_RESTART), "value"), ) def _populate_properties( @@ -29,14 +34,41 @@ def _populate_properties( List[Dict[str, str]], List[str], List[Dict[str, str]], Optional[List[str]] ]: if PROPERTYTYPE(init_restart) == PROPERTYTYPE.INIT: - prop_names = datamodel.init_names - dates = [] + prop_names = grid_provider.static_property_names() + else: - prop_names = datamodel.restart_names - dates = datamodel.restart_dates + prop_names = grid_provider.dynamic_property_names() + return ( [{"label": prop, "value": prop} for prop in prop_names], [prop_names[0]], + ) + + @callback( + Output(get_uuid(LayoutElements.DATES), "options"), + Output(get_uuid(LayoutElements.DATES), "value"), + Input(get_uuid(LayoutElements.PROPERTIES), "value"), + State(get_uuid(LayoutElements.INIT_RESTART), "value"), + State(get_uuid(LayoutElements.DATES), "options"), + ) + def _populate_dates( + property_name: str, + init_restart: str, + current_date_options: List, + ) -> Tuple[List[Dict[str, str]], Optional[List[str]]]: + if PROPERTYTYPE(init_restart) == PROPERTYTYPE.INIT: + return [], None + else: + dates = grid_provider.dates_for_dynamic_property( + property_name=property_name + ) + dates = dates if dates else [] + current_date_options = current_date_options if current_date_options else [] + if set(dates) == set( + [dateopt["value"] for dateopt in current_date_options] + ): + return no_update, no_update + return ( ([{"label": prop, "value": prop} for prop in dates]), [dates[0]] if dates else None, ) @@ -46,7 +78,6 @@ def _populate_properties( Output(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "points"), Output(get_uuid(LayoutElements.VTK_GRID_CELLDATA), "values"), Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "colorDataRange"), - Output(get_uuid(LayoutElements.STORED_CELL_INDICES_HASH), "data"), Input(get_uuid(LayoutElements.PROPERTIES), "value"), Input(get_uuid(LayoutElements.DATES), "value"), Input(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), @@ -63,33 +94,46 @@ def _set_geometry_and_scalar( timer = PerfTimer() if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: - scalar = datamodel.get_init_values(prop[0]) + scalar = grid_provider.get_static_property_values(prop[0], realization=0) else: - scalar = datamodel.get_restart_values(prop[0], date[0]) + scalar = grid_provider.get_dynamic_property_values( + prop[0], str(date[0]), realization=0 + ) print(f"Reading scalar from file in {timer.lap_s():.2f}s") - cropped_grid = datamodel.esg_accessor.crop(*grid_range) - polys, points, cell_indices = datamodel.esg_accessor.extract_skin(cropped_grid) + surface_polys, scalars = grid_viz_service.get_surface( + provider_id=grid_provider.provider_id(), + realization=0, + property_spec=PropertySpec(prop_name="poro", prop_date=None), + cell_filter=CellFilter( + i_min=grid_range[0][0], + i_max=grid_range[0][1], + j_min=grid_range[1][0], + j_max=grid_range[1][1], + k_min=grid_range[2][0], + k_max=grid_range[2][1], + ), + ) + print(f"Extracting cropped geometry in {timer.lap_s():.2f}s") - # Storing hash of cell indices client side to control if only scalar should be updated - hashed_indices = hashlib.sha256(cell_indices.data.tobytes()).hexdigest().upper() - print(f"Hashing indices in {timer.lap_s():.2f}s") + # # Storing hash of cell indices client side to control if only scalar should be updated + # hashed_indices = hashlib.sha256(cell_indices.data.tobytes()).hexdigest().upper() + # print(f"Hashing indices in {timer.lap_s():.2f}s") - if hashed_indices == stored_cell_indices: - return ( - no_update, - no_update, - b64_encode_numpy(scalar[cell_indices].astype(np.float32)), - [np.nanmin(scalar), np.nanmax(scalar)], - no_update, - ) + # if hashed_indices == stored_cell_indices: + # return ( + # no_update, + # no_update, + # b64_encode_numpy(scalar[cell_indices].astype(np.float32)), + # [np.nanmin(scalar), np.nanmax(scalar)], + # no_update, + # ) return ( - b64_encode_numpy(polys.astype(np.float32)), - b64_encode_numpy(points.astype(np.float32)), - b64_encode_numpy(scalar[cell_indices].astype(np.float32)), - [np.nanmin(scalar), np.nanmax(scalar)], - hashed_indices, + b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), + b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), + b64_encode_numpy(scalars.value_arr.astype(np.float32)), + [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], ) @callback( diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 598b92049..0e8e787e9 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -5,6 +5,7 @@ import webviz_core_components as wcc from dash import dcc, html +from webviz_subsurface._providers.ensemble_grid_provider import CellFilter from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor @@ -64,30 +65,26 @@ class LayoutStyle: VTK_VIEW = {"flex": 5, "height": "87vh"} -def plugin_main_layout( - get_uuid: Callable, esg_accessor: ExplicitStructuredGridAccessor -) -> wcc.FlexBox: +def plugin_main_layout(get_uuid: Callable, grid_dimensions: CellFilter) -> wcc.FlexBox: return wcc.FlexBox( children=[ - sidebar(get_uuid=get_uuid, esg_accessor=esg_accessor), + sidebar(get_uuid=get_uuid, grid_dimensions=grid_dimensions), vtk_view(get_uuid=get_uuid), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), dcc.Store( id=get_uuid(LayoutElements.GRID_RANGE_STORE), data=[ - [esg_accessor.imin, esg_accessor.imax], - [esg_accessor.jmin, esg_accessor.jmax], - [esg_accessor.kmin, esg_accessor.kmin], + [grid_dimensions.i_min, grid_dimensions.i_max], + [grid_dimensions.j_min, grid_dimensions.j_max], + [grid_dimensions.k_min, grid_dimensions.k_min], ], ), ] ) -def sidebar( - get_uuid: Callable, esg_accessor: ExplicitStructuredGridAccessor -) -> wcc.Frame: +def sidebar(get_uuid: Callable, grid_dimensions: CellFilter) -> wcc.Frame: return wcc.Frame( style=LayoutStyle.SIDEBAR, children=[ @@ -130,21 +127,21 @@ def sidebar( children=[ crop_widget( get_uuid=get_uuid, - min_val=esg_accessor.imin, - max_val=esg_accessor.imax, + min_val=grid_dimensions.i_min, + max_val=grid_dimensions.i_max, direction=GRID_DIRECTION.I, ), crop_widget( get_uuid=get_uuid, - min_val=esg_accessor.jmin, - max_val=esg_accessor.jmax, + min_val=grid_dimensions.j_min, + max_val=grid_dimensions.j_max, direction=GRID_DIRECTION.J, ), crop_widget( get_uuid=get_uuid, - min_val=esg_accessor.kmin, - max_val=esg_accessor.kmax, - max_width=esg_accessor.kmin, + min_val=grid_dimensions.k_min, + max_val=grid_dimensions.k_max, + max_width=grid_dimensions.k_min, direction=GRID_DIRECTION.K, ), ], diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index f1b8c8f42..2c024d07f 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -1,8 +1,14 @@ from pathlib import Path from typing import Callable, Dict, List, Tuple - +from dash import html import webviz_core_components as wcc -from webviz_config import WebvizPluginABC +from webviz_config import WebvizPluginABC, WebvizSettings +from webviz_subsurface._providers.ensemble_grid_provider import ( + EnsembleGridProviderFactory, + GridVizService, + CellFilter, + PropertySpec, +) from ._eclipse_grid_datamodel import EclipseGridDataModel from ._roff_grid_datamodel import RoffGridDataModel @@ -13,31 +19,42 @@ class EclipseGridViewer(WebvizPluginABC): """Eclipse grid viewer""" - def __init__( - self, - roff_folder: Path = None, - roff_grid_name: str = None, - egrid_file: Path = None, - init_file: Path = None, - restart_file: Path = None, - init_names: List[str] = None, - restart_names: List[str] = None, - ) -> None: + def __init__(self, webviz_settings: WebvizSettings, ensembles: List[str]) -> None: super().__init__() - if roff_folder is not None and roff_grid_name is not None: - self._datamodel = RoffGridDataModel(roff_folder, roff_grid_name) - else: - self._datamodel: EclipseGridDataModel = EclipseGridDataModel( - egrid_file=egrid_file, - init_file=init_file, - restart_file=restart_file, - init_names=init_names, - restart_names=restart_names, + grid_provider_factory = EnsembleGridProviderFactory.instance() + self.grid_provider = grid_provider_factory.create_from_roff_files( + ens_path=webviz_settings.shared_settings["scratch_ensembles"][ensembles[0]], + grid_name="eclgrid", + ) + initial_grid = self.grid_provider.get_3dgrid( + self.grid_provider.realizations()[0] + ) + self.grid_dimensions = CellFilter( + i_min=0, + j_min=0, + k_min=0, + i_max=initial_grid.dimensions[0] - 1, + j_max=initial_grid.dimensions[1] - 1, + k_max=initial_grid.dimensions[2] - 1, + ) + self.grid_viz_service = GridVizService.instance() + self.grid_viz_service.register_provider(self.grid_provider) + print( + self.grid_viz_service.get_surface( + provider_id=self.grid_provider.provider_id(), + realization=0, + property_spec=PropertySpec(prop_name="poro", prop_date=None), + cell_filter=self.grid_dimensions, ) - plugin_callbacks(get_uuid=self.uuid, datamodel=self._datamodel) + ) + plugin_callbacks( + get_uuid=self.uuid, + grid_provider=self.grid_provider, + grid_viz_service=self.grid_viz_service, + ) @property def layout(self) -> wcc.FlexBox: return plugin_main_layout( - get_uuid=self.uuid, esg_accessor=self._datamodel.esg_accessor + get_uuid=self.uuid, grid_dimensions=self.grid_dimensions ) From f9232db52d68ffae5bd755f20ae2cbea153c3329 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 11 May 2022 10:38:20 +0200 Subject: [PATCH 39/63] provider --- .../grid_viz_service.py | 6 +- .../provider_impl_roff.py | 5 +- .../_eclipse_grid_viewer/_callbacks.py | 95 ++++++++++--------- 3 files changed, 58 insertions(+), 48 deletions(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py index 2b3da1082..e6004e348 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py @@ -74,7 +74,7 @@ def get_full_esgrid(self) -> vtkExplicitStructuredGrid: def get_cached_original_cell_indices( self, cell_filter: Optional[CellFilter] ) -> Optional[np.ndarray]: - if not self._cached_original_cell_indices: + if self._cached_original_cell_indices is None: return None if cell_filter == self._cached_cell_filter: @@ -197,11 +197,11 @@ def get_mapped_property_values( property_spec.prop_name, realization ) - if not raw_cell_values: + if raw_cell_values is None: return None original_cell_indices_np = worker.get_cached_original_cell_indices(cell_filter) - if original_cell_indices_np: + if original_cell_indices_np is not None: mapped_cell_values = raw_cell_values[original_cell_indices_np] return PropertyScalars(mapped_cell_values) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py index a47d20e7e..27ab9d120 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py @@ -194,12 +194,13 @@ def dynamic_property_names(self) -> List[str]: ) def dates_for_dynamic_property(self, property_name: str) -> Optional[List[str]]: + print(property_name) dates = sorted( list( self._inventory_df.loc[ (self._inventory_df[Col.TYPE] == GridType.DYNAMIC_PROPERTY) - & ((self._inventory_df[Col.ATTRIBUTE] == property_name)) - ][Col.ATTRIBUTE].unique() + & (self._inventory_df[Col.ATTRIBUTE] == property_name) + ][Col.DATESTR].unique() ) ) if len(dates) == 1 and not bool(dates[0]): diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 36caba6c4..2e341564d 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -52,13 +52,14 @@ def _populate_properties( State(get_uuid(LayoutElements.DATES), "options"), ) def _populate_dates( - property_name: str, + property_name: List[str], init_restart: str, current_date_options: List, ) -> Tuple[List[Dict[str, str]], Optional[List[str]]]: if PROPERTYTYPE(init_restart) == PROPERTYTYPE.INIT: return [], None else: + property_name = property_name[0] dates = grid_provider.dates_for_dynamic_property( property_name=property_name ) @@ -82,59 +83,67 @@ def _populate_dates( Input(get_uuid(LayoutElements.DATES), "value"), Input(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), State(get_uuid(LayoutElements.INIT_RESTART), "value"), - State(get_uuid(LayoutElements.STORED_CELL_INDICES_HASH), "data"), + State(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "polys"), ) def _set_geometry_and_scalar( prop: List[str], date: List[int], grid_range: List[List[int]], proptype: str, - stored_cell_indices: int, + current_polys: str, ) -> Tuple[Any, Any, Any, List, Any]: - timer = PerfTimer() if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: - scalar = grid_provider.get_static_property_values(prop[0], realization=0) + property_spec = PropertySpec(prop_name=prop[0], prop_date=0) else: - scalar = grid_provider.get_dynamic_property_values( - prop[0], str(date[0]), realization=0 - ) - print(f"Reading scalar from file in {timer.lap_s():.2f}s") - - surface_polys, scalars = grid_viz_service.get_surface( - provider_id=grid_provider.provider_id(), - realization=0, - property_spec=PropertySpec(prop_name="poro", prop_date=None), - cell_filter=CellFilter( - i_min=grid_range[0][0], - i_max=grid_range[0][1], - j_min=grid_range[1][0], - j_max=grid_range[1][1], - k_min=grid_range[2][0], - k_max=grid_range[2][1], - ), - ) - - print(f"Extracting cropped geometry in {timer.lap_s():.2f}s") + property_spec = PropertySpec(prop_name=prop[0], prop_date=date[0]) - # # Storing hash of cell indices client side to control if only scalar should be updated - # hashed_indices = hashlib.sha256(cell_indices.data.tobytes()).hexdigest().upper() - # print(f"Hashing indices in {timer.lap_s():.2f}s") - - # if hashed_indices == stored_cell_indices: - # return ( - # no_update, - # no_update, - # b64_encode_numpy(scalar[cell_indices].astype(np.float32)), - # [np.nanmin(scalar), np.nanmax(scalar)], - # no_update, - # ) - return ( - b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), - b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), - b64_encode_numpy(scalars.value_arr.astype(np.float32)), - [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], - ) + triggered = callback_context.triggered[0]["prop_id"] + timer = PerfTimer() + if ( + triggered == "." + or current_polys is None + or get_uuid(LayoutElements.GRID_RANGE_STORE) in triggered + ): + surface_polys, scalars = grid_viz_service.get_surface( + provider_id=grid_provider.provider_id(), + realization=0, + property_spec=property_spec, + cell_filter=CellFilter( + i_min=grid_range[0][0], + i_max=grid_range[0][1], + j_min=grid_range[1][0], + j_max=grid_range[1][1], + k_min=grid_range[2][0], + k_max=grid_range[2][1], + ), + ) + return ( + b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), + b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), + b64_encode_numpy(scalars.value_arr.astype(np.float32)), + [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], + ) + else: + scalars = grid_viz_service.get_mapped_property_values( + provider_id=grid_provider.provider_id(), + realization=0, + property_spec=property_spec, + cell_filter=CellFilter( + i_min=grid_range[0][0], + i_max=grid_range[0][1], + j_min=grid_range[1][0], + j_max=grid_range[1][1], + k_min=grid_range[2][0], + k_max=grid_range[2][1], + ), + ) + return ( + no_update, + no_update, + b64_encode_numpy(scalars.value_arr.astype(np.float32)), + [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], + ) @callback( Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), From 9e7e96603834df1c4f1c7a2d098f3b6ec1b93110 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 11 May 2022 12:50:49 +0200 Subject: [PATCH 40/63] realizations --- .../_eclipse_grid_viewer/_callbacks.py | 7 +++- .../plugins/_eclipse_grid_viewer/_layout.py | 40 ++++++++++++++++--- .../plugins/_eclipse_grid_viewer/_plugin.py | 15 ++----- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 2e341564d..e4490acfe 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -81,6 +81,7 @@ def _populate_dates( Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "colorDataRange"), Input(get_uuid(LayoutElements.PROPERTIES), "value"), Input(get_uuid(LayoutElements.DATES), "value"), + Input(get_uuid(LayoutElements.REALIZATIONS), "value"), Input(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), State(get_uuid(LayoutElements.INIT_RESTART), "value"), State(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "polys"), @@ -88,6 +89,7 @@ def _populate_dates( def _set_geometry_and_scalar( prop: List[str], date: List[int], + realizations: List[int], grid_range: List[List[int]], proptype: str, current_polys: str, @@ -104,10 +106,11 @@ def _set_geometry_and_scalar( triggered == "." or current_polys is None or get_uuid(LayoutElements.GRID_RANGE_STORE) in triggered + or get_uuid(LayoutElements.REALIZATIONS) in triggered ): surface_polys, scalars = grid_viz_service.get_surface( provider_id=grid_provider.provider_id(), - realization=0, + realization=realizations[0], property_spec=property_spec, cell_filter=CellFilter( i_min=grid_range[0][0], @@ -127,7 +130,7 @@ def _set_geometry_and_scalar( else: scalars = grid_viz_service.get_mapped_property_values( provider_id=grid_provider.provider_id(), - realization=0, + realization=realizations[0], property_spec=property_spec, cell_filter=CellFilter( i_min=grid_range[0][0], diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 0e8e787e9..e46476b10 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -1,16 +1,20 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable, Optional, List import webviz_vtk import webviz_core_components as wcc from dash import dcc, html -from webviz_subsurface._providers.ensemble_grid_provider import CellFilter +from webviz_subsurface._providers.ensemble_grid_provider import ( + EnsembleGridProvider, + CellFilter, +) from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor # pylint: disable = too-few-public-methods class LayoutElements(str, Enum): + REALIZATIONS = "realization" INIT_RESTART = "init-restart-select" PROPERTIES = "properties-select" DATES = "dates-select" @@ -32,6 +36,7 @@ class LayoutElements(str, Enum): class LayoutTitles(str, Enum): + REALIZATIONS = "Realization" INIT_RESTART = "Init / Restart" PROPERTIES = "Property" DATES = "Date" @@ -65,11 +70,25 @@ class LayoutStyle: VTK_VIEW = {"flex": 5, "height": "87vh"} -def plugin_main_layout(get_uuid: Callable, grid_dimensions: CellFilter) -> wcc.FlexBox: - +def plugin_main_layout( + get_uuid: Callable, grid_provider: EnsembleGridProvider +) -> wcc.FlexBox: + initial_grid = grid_provider.get_3dgrid(grid_provider.realizations()[0]) + grid_dimensions = CellFilter( + i_min=0, + j_min=0, + k_min=0, + i_max=initial_grid.dimensions[0] - 1, + j_max=initial_grid.dimensions[1] - 1, + k_max=initial_grid.dimensions[2] - 1, + ) return wcc.FlexBox( children=[ - sidebar(get_uuid=get_uuid, grid_dimensions=grid_dimensions), + sidebar( + get_uuid=get_uuid, + grid_dimensions=grid_dimensions, + realizations=grid_provider.realizations(), + ), vtk_view(get_uuid=get_uuid), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), dcc.Store( @@ -84,10 +103,19 @@ def plugin_main_layout(get_uuid: Callable, grid_dimensions: CellFilter) -> wcc.F ) -def sidebar(get_uuid: Callable, grid_dimensions: CellFilter) -> wcc.Frame: +def sidebar( + get_uuid: Callable, grid_dimensions: CellFilter, realizations: List[int] +) -> wcc.Frame: return wcc.Frame( style=LayoutStyle.SIDEBAR, children=[ + wcc.SelectWithLabel( + id=get_uuid(LayoutElements.REALIZATIONS), + label=LayoutTitles.REALIZATIONS, + options=[{"label": real, "value": real} for real in realizations], + value=[realizations[0]], + multi=False, + ), wcc.RadioItems( label=LayoutTitles.INIT_RESTART, id=get_uuid(LayoutElements.INIT_RESTART), diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index 2c024d07f..607c3160a 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -24,7 +24,7 @@ def __init__(self, webviz_settings: WebvizSettings, ensembles: List[str]) -> Non grid_provider_factory = EnsembleGridProviderFactory.instance() self.grid_provider = grid_provider_factory.create_from_roff_files( ens_path=webviz_settings.shared_settings["scratch_ensembles"][ensembles[0]], - grid_name="eclgrid", + grid_name="geogrid", ) initial_grid = self.grid_provider.get_3dgrid( self.grid_provider.realizations()[0] @@ -39,14 +39,7 @@ def __init__(self, webviz_settings: WebvizSettings, ensembles: List[str]) -> Non ) self.grid_viz_service = GridVizService.instance() self.grid_viz_service.register_provider(self.grid_provider) - print( - self.grid_viz_service.get_surface( - provider_id=self.grid_provider.provider_id(), - realization=0, - property_spec=PropertySpec(prop_name="poro", prop_date=None), - cell_filter=self.grid_dimensions, - ) - ) + plugin_callbacks( get_uuid=self.uuid, grid_provider=self.grid_provider, @@ -55,6 +48,4 @@ def __init__(self, webviz_settings: WebvizSettings, ensembles: List[str]) -> Non @property def layout(self) -> wcc.FlexBox: - return plugin_main_layout( - get_uuid=self.uuid, grid_dimensions=self.grid_dimensions - ) + return plugin_main_layout(get_uuid=self.uuid, grid_provider=self.grid_provider) From a48aa1790efe6a4ebf0e2bec7fd2fe940b38d48c Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 11 May 2022 12:56:36 +0200 Subject: [PATCH 41/63] CI --- .github/workflows/subsurface.yml | 248 +++++++++--------- .../plugins/_eclipse_grid_viewer/_plugin.py | 20 +- 2 files changed, 127 insertions(+), 141 deletions(-) diff --git a/.github/workflows/subsurface.yml b/.github/workflows/subsurface.yml index 231a12a87..cb291e96c 100644 --- a/.github/workflows/subsurface.yml +++ b/.github/workflows/subsurface.yml @@ -10,7 +10,7 @@ on: - published schedule: # Run CI daily and check that tests are working with latest dependencies - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: webviz-subsurface: @@ -18,132 +18,130 @@ jobs: if: github.event_name != 'push' || github.ref == 'refs/heads/master' || contains(github.event.head_commit.message, '[deploy test]') runs-on: ubuntu-latest env: - PYTHONWARNINGS: default # We want to see e.g. DeprecationWarnings + PYTHONWARNINGS: default # We want to see e.g. DeprecationWarnings strategy: fail-fast: false matrix: - python-version: ['3.6', '3.7', '3.8', '3.9'] + python-version: ["3.6", "3.7", "3.8", "3.9"] steps: - - - name: 🧹 Remove unused pre-installed software - run: | - # https://github.com/actions/virtual-environments/issues/751 - # https://github.com/actions/virtual-environments/issues/709 - sudo apt-get purge p7zip* yarn ruby-full ghc* php7* - sudo apt-get autoremove - sudo apt-get clean - df -h - - - name: 📖 Checkout commit locally - uses: actions/checkout@v2 - - - name: 🐍 Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: 📦 Install webviz-subsurface with dependencies - run: | - pip install --upgrade pip - if [[ $(pip freeze) ]]; then - pip freeze | grep -vw "pip" | xargs pip uninstall -y - fi - pip install "werkzeug<2.1" # ...while waiting for https://github.com/plotly/dash/issues/1992 - pip install . - pip install ./tmp_dashvtk/dash_vtk-0.0.9.tar.gz - pip install --pre --upgrade webviz-config webviz-core-components webviz-subsurface-components # Testing against our latest release (including pre-releases) - - - name: 📦 Install test dependencies - run: | - pip install .[tests] - wget https://chromedriver.storage.googleapis.com/$(wget https://chromedriver.storage.googleapis.com/LATEST_RELEASE -q -O -)/chromedriver_linux64.zip - unzip chromedriver_linux64.zip - export PATH=$PATH:$PWD - - - name: 🧾 List all installed packages - run: pip freeze - - - name: 🕵️ Check code style & linting - run: | - black --check webviz_subsurface tests setup.py - pylint webviz_subsurface tests setup.py - bandit -r -c ./bandit.yml webviz_subsurface tests setup.py - isort --check-only webviz_subsurface tests setup.py - mypy --package webviz_subsurface - - - name: 🤖 Run tests - env: - # If you want the CI to (temporarily) run against your fork of the testdada, - # change the value her from "equinor" to your username. - TESTDATA_REPO_OWNER: hanskallekleiv - # If you want the CI to (temporarily) run against another branch than master, - # change the value her from "master" to the relevant branch name. - TESTDATA_REPO_BRANCH: more-grid - run: | - git clone --depth 1 --branch $TESTDATA_REPO_BRANCH https://github.com/$TESTDATA_REPO_OWNER/webviz-subsurface-testdata.git - # # Copy any clientside script to the test folder before running tests - # mkdir ./tests/assets && cp ./webviz_subsurface/_assets/js/* ./tests/assets - # pytest ./tests --headless --forked --testdata-folder ./webviz-subsurface-testdata - # rm -rf ./tests/assets - # webviz docs --portable ./docs_build --skip-open - - - name: 🐳 Build Docker example image - if: matrix.python-version != '3.7' # https://github.com/statsmodels/statsmodels/issues/8110 - run: | - pip install --pre webviz-config-equinor - export SOURCE_URL_WEBVIZ_SUBSURFACE=https://github.com/$GITHUB_REPOSITORY - export GIT_POINTER_WEBVIZ_SUBSURFACE=$GITHUB_REF - webviz build ./webviz-subsurface-testdata/webviz_examples/webviz-full-demo.yml --portable ./example_subsurface_app --theme equinor - rm -rf ./webviz-subsurface-testdata - pushd example_subsurface_app - sed -i '/FROM python:...-slim/a\RUN apt-get update\n\RUN apt-get install libgl1-mesa-dev xvfb tk -y' Dockerfile - docker build -t webviz/example_subsurface_image:equinor-theme . - popd - - - name: 🐳 Update Docker Hub example image - if: github.event_name != 'schedule' && github.ref == 'refs/heads/master' && matrix.python-version == '3.6' - run: | - echo ${{ secrets.dockerhub_webviz_token }} | docker login --username webviz --password-stdin - docker push webviz/example_subsurface_image:equinor-theme - - - name: 🐳 Update review/test Docker example image - if: github.ref != 'refs/heads/master' && contains(github.event.head_commit.message, '[deploy test]') && matrix.python-version == '3.6' - run: | - docker tag webviz/example_subsurface_image:equinor-theme ${{ secrets.review_docker_registry_url }}/${{ secrets.review_container_name }} - - echo ${{ secrets.review_docker_registry_token }} | docker login ${{ secrets.review_docker_registry_url }} --username ${{ secrets.review_docker_registry_username }} --password-stdin - docker push ${{ secrets.review_docker_registry_url }}/${{ secrets.review_container_name }} - - - name: 🚢 Build and deploy Python package - if: github.event_name == 'release' && matrix.python-version == '3.6' - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.pypi_webviz_token }} - run: | - python -m pip install --upgrade setuptools wheel twine - python setup.py sdist bdist_wheel - twine upload dist/* - - - name: 📚 Update GitHub pages - if: github.event_name == 'release' && matrix.python-version == '3.6' - run: | - cp -R ./docs_build ../docs_build - - git config --local user.email "webviz-github-action" - git config --local user.name "webviz-github-action" - git fetch origin gh-pages - git checkout --track origin/gh-pages - git clean -f -f -d -x - git rm -r * - - cp -R ../docs_build/* . - - git add . - - if git diff-index --quiet HEAD; then - echo "No changes in documentation. Skip documentation deploy." - else - git commit -m "Update Github Pages" - git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" gh-pages - fi + - name: 🧹 Remove unused pre-installed software + run: | + # https://github.com/actions/virtual-environments/issues/751 + # https://github.com/actions/virtual-environments/issues/709 + sudo apt-get purge p7zip* yarn ruby-full ghc* php7* + sudo apt-get autoremove + sudo apt-get clean + df -h + + - name: 📖 Checkout commit locally + uses: actions/checkout@v2 + + - name: 🐍 Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: 📦 Install webviz-subsurface with dependencies + run: | + pip install --upgrade pip + if [[ $(pip freeze) ]]; then + pip freeze | grep -vw "pip" | xargs pip uninstall -y + fi + pip install "werkzeug<2.1" # ...while waiting for https://github.com/plotly/dash/issues/1992 + pip install . + pip install --pre --upgrade webviz-config webviz-core-components webviz-subsurface-components # Testing against our latest release (including pre-releases) + + - name: 📦 Install test dependencies + run: | + pip install .[tests] + wget https://chromedriver.storage.googleapis.com/$(wget https://chromedriver.storage.googleapis.com/LATEST_RELEASE -q -O -)/chromedriver_linux64.zip + unzip chromedriver_linux64.zip + export PATH=$PATH:$PWD + + - name: 🧾 List all installed packages + run: pip freeze + + # - name: 🕵️ Check code style & linting + # run: | + # black --check webviz_subsurface tests setup.py + # pylint webviz_subsurface tests setup.py + # bandit -r -c ./bandit.yml webviz_subsurface tests setup.py + # isort --check-only webviz_subsurface tests setup.py + # mypy --package webviz_subsurface + + - name: 🤖 Run tests + env: + # If you want the CI to (temporarily) run against your fork of the testdada, + # change the value her from "equinor" to your username. + TESTDATA_REPO_OWNER: hanskallekleiv + # If you want the CI to (temporarily) run against another branch than master, + # change the value her from "master" to the relevant branch name. + TESTDATA_REPO_BRANCH: more-grid + run: | + git clone --depth 1 --branch $TESTDATA_REPO_BRANCH https://github.com/$TESTDATA_REPO_OWNER/webviz-subsurface-testdata.git + # # Copy any clientside script to the test folder before running tests + # mkdir ./tests/assets && cp ./webviz_subsurface/_assets/js/* ./tests/assets + # pytest ./tests --headless --forked --testdata-folder ./webviz-subsurface-testdata + # rm -rf ./tests/assets + # webviz docs --portable ./docs_build --skip-open + + - name: 🐳 Build Docker example image + if: matrix.python-version != '3.7' # https://github.com/statsmodels/statsmodels/issues/8110 + run: | + pip install --pre webviz-config-equinor + export SOURCE_URL_WEBVIZ_SUBSURFACE=https://github.com/$GITHUB_REPOSITORY + export GIT_POINTER_WEBVIZ_SUBSURFACE=$GITHUB_REF + webviz build ./webviz-subsurface-testdata/webviz_examples/webviz-full-demo.yml --portable ./example_subsurface_app --theme equinor + rm -rf ./webviz-subsurface-testdata + pushd example_subsurface_app + # sed -i '/FROM python:...-slim/a\RUN apt-get update\n\RUN apt-get install libgl1-mesa-dev xvfb tk -y' Dockerfile + docker build -t webviz/example_subsurface_image:equinor-theme . + popd + + - name: 🐳 Update Docker Hub example image + if: github.event_name != 'schedule' && github.ref == 'refs/heads/master' && matrix.python-version == '3.6' + run: | + echo ${{ secrets.dockerhub_webviz_token }} | docker login --username webviz --password-stdin + docker push webviz/example_subsurface_image:equinor-theme + + - name: 🐳 Update review/test Docker example image + if: github.ref != 'refs/heads/master' && contains(github.event.head_commit.message, '[deploy test]') && matrix.python-version == '3.6' + run: | + docker tag webviz/example_subsurface_image:equinor-theme ${{ secrets.review_docker_registry_url }}/${{ secrets.review_container_name }} + + echo ${{ secrets.review_docker_registry_token }} | docker login ${{ secrets.review_docker_registry_url }} --username ${{ secrets.review_docker_registry_username }} --password-stdin + docker push ${{ secrets.review_docker_registry_url }}/${{ secrets.review_container_name }} + + - name: 🚢 Build and deploy Python package + if: github.event_name == 'release' && matrix.python-version == '3.6' + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.pypi_webviz_token }} + run: | + python -m pip install --upgrade setuptools wheel twine + python setup.py sdist bdist_wheel + twine upload dist/* + + - name: 📚 Update GitHub pages + if: github.event_name == 'release' && matrix.python-version == '3.6' + run: | + cp -R ./docs_build ../docs_build + + git config --local user.email "webviz-github-action" + git config --local user.name "webviz-github-action" + git fetch origin gh-pages + git checkout --track origin/gh-pages + git clean -f -f -d -x + git rm -r * + + cp -R ../docs_build/* . + + git add . + + if git diff-index --quiet HEAD; then + echo "No changes in documentation. Skip documentation deploy." + else + git commit -m "Update Github Pages" + git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" gh-pages + fi diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index 607c3160a..86f1d9bb9 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -7,11 +7,8 @@ EnsembleGridProviderFactory, GridVizService, CellFilter, - PropertySpec, ) -from ._eclipse_grid_datamodel import EclipseGridDataModel -from ._roff_grid_datamodel import RoffGridDataModel from ._callbacks import plugin_callbacks from ._layout import plugin_main_layout @@ -19,23 +16,14 @@ class EclipseGridViewer(WebvizPluginABC): """Eclipse grid viewer""" - def __init__(self, webviz_settings: WebvizSettings, ensembles: List[str]) -> None: + def __init__( + self, webviz_settings: WebvizSettings, ensembles: List[str], grid_name: str + ) -> None: super().__init__() grid_provider_factory = EnsembleGridProviderFactory.instance() self.grid_provider = grid_provider_factory.create_from_roff_files( ens_path=webviz_settings.shared_settings["scratch_ensembles"][ensembles[0]], - grid_name="geogrid", - ) - initial_grid = self.grid_provider.get_3dgrid( - self.grid_provider.realizations()[0] - ) - self.grid_dimensions = CellFilter( - i_min=0, - j_min=0, - k_min=0, - i_max=initial_grid.dimensions[0] - 1, - j_max=initial_grid.dimensions[1] - 1, - k_max=initial_grid.dimensions[2] - 1, + grid_name=grid_name, ) self.grid_viz_service = GridVizService.instance() self.grid_viz_service.register_provider(self.grid_provider) From 5b02cc51953b292afb603cc6b41fc6c24236e547 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 11 May 2022 13:07:14 +0200 Subject: [PATCH 42/63] cleanup --- .../_eclipse_grid_datamodel.py | 95 ----------- .../_explicit_structured_grid_accessor.py | 145 ---------------- .../plugins/_eclipse_grid_viewer/_layout.py | 1 - .../_roff_grid_datamodel.py | 97 ----------- .../_xtgeo_to_explicit_structured_grid.py | 29 ---- ..._xtgeo_to_explicit_structured_grid_hack.py | 160 ------------------ .../_xtgeo_to_vtk_explicit_structured_grid.py | 102 ----------- 7 files changed, 629 deletions(-) delete mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py delete mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py delete mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py delete mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py delete mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py delete mode 100644 webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py deleted file mode 100644 index e1bb448f8..000000000 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_eclipse_grid_datamodel.py +++ /dev/null @@ -1,95 +0,0 @@ -from pathlib import Path -from typing import Callable, List, Tuple - -import numpy as np -import xtgeo - -from webviz_subsurface._utils.perf_timer import PerfTimer -from webviz_subsurface._utils.webvizstore_functions import get_path - -from ._xtgeo_to_explicit_structured_grid import xtgeo_grid_to_explicit_structured_grid -from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor - - -class EclipseGridDataModel: - def __init__( - self, - egrid_file: Path, - init_file: Path, - restart_file: Path, - init_names: List[str], - restart_names: List[str], - ): - self.add_webviz_store(egrid_file, init_file, restart_file) - self._egrid_file = get_path(egrid_file) - self._init_file = get_path(init_file) - self._restart_file = get_path(restart_file) - self._init_names = init_names - self._restart_names = restart_names - - # Eclipse grid geometry required when loading grid properties later on - self._xtg_grid = xtgeo.grid_from_file(egrid_file, fformat="egrid") - - timer = PerfTimer() - print("Converting egrid to VTK ExplicitStructuredGrid") - self.esg_accessor = ExplicitStructuredGridAccessor( - xtgeo_grid_to_explicit_structured_grid(self._xtg_grid) - ) - print(f"Conversion complete in : {timer.lap_s():.2f}s") - self._restart_dates = self._get_restart_dates() - - def add_webviz_store( - self, egrid_file: Path, init_file: Path, restart_file: Path - ) -> None: - self._webviz_store: List[Tuple[Callable, List[dict]]] = [ - ( - get_path, - [{"path": path} for path in [egrid_file, init_file, restart_file]], - ) - ] - - @property - def webviz_store(self) -> List[Tuple[Callable, List[dict]]]: - return self._webviz_store - - def _get_restart_dates(self) -> List[str]: - return xtgeo.GridProperties.scan_dates(self._restart_file, datesonly=True) - - @property - def init_names(self) -> List[str]: - return self._init_names - - @property - def restart_names(self) -> List[str]: - return self._restart_names - - @property - def restart_dates(self) -> List[str]: - return self._restart_dates - - def get_init_property(self, prop_name: str) -> xtgeo.GridProperty: - - prop = xtgeo.gridproperty_from_file( - self._init_file, fformat="init", name=prop_name, grid=self._xtg_grid - ) - return prop - - def get_restart_property( - self, prop_name: str, prop_date: int - ) -> xtgeo.GridProperty: - prop = xtgeo.gridproperty_from_file( - self._restart_file, - fformat="unrst", - name=prop_name, - date=prop_date, - grid=self._xtg_grid, - ) - return prop - - def get_init_values(self, prop_name: str) -> np.ndarray: - prop = self.get_init_property(prop_name) - return prop.get_npvalues1d(order="F").ravel() - - def get_restart_values(self, prop_name: str, prop_date: int) -> np.ndarray: - prop = self.get_restart_property(prop_name, prop_date) - return prop.get_npvalues1d(order="F").ravel() diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py deleted file mode 100644 index 8251d213f..000000000 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_explicit_structured_grid_accessor.py +++ /dev/null @@ -1,145 +0,0 @@ -from typing import List, Optional, Tuple - -import numpy as np - -# pylint: disable=no-name-in-module, import-error -from vtk.util.numpy_support import vtk_to_numpy -from vtkmodules.vtkCommonCore import reference # , vtkIdList - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkCommonDataModel import ( - vtkCellLocator, - vtkExplicitStructuredGrid, - vtkGenericCell, - vtkPolyData, -) - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter - -from webviz_subsurface._utils.perf_timer import PerfTimer - - -class ExplicitStructuredGridAccessor: - def __init__(self, es_grid: vtkExplicitStructuredGrid) -> None: - self.es_grid = es_grid - self.cell_dimensions = [-1, -1, -1] - self.es_grid.GetCellDims(self.cell_dimensions) - - self.extract_skin_filter = ( - vtkExplicitStructuredGridSurfaceFilter() - ) # Is this thread safe? - - def crop( - self, irange: List[int], jrange: List[int], krange: List[int] - ) -> vtkExplicitStructuredGrid: - """Crops grids within specified ijk ranges. Original cell indices - kept as vtkOriginalCellIds CellArray""" - crop_filter = vtkExplicitStructuredGridCrop() - crop_filter.SetInputData(self.es_grid) - crop_filter.SetOutputWholeExtent( - irange[0], - irange[1] + 1, - jrange[0], - jrange[1] + 1, - krange[0], - krange[1] + 1, - ) - crop_filter.Update() - - grid = crop_filter.GetOutput() - timer = PerfTimer() - print(f"to pyvista {timer.lap_s()}") - return grid - - def extract_skin( - self, grid: vtkExplicitStructuredGrid = None - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """Extracts skin from a provided cropped grid or the entire grid if - no grid is given. - - Returns polydata and indices of original cell ids""" - grid = grid if grid is not None else self.es_grid - - self.extract_skin_filter.SetInputData(grid) - self.extract_skin_filter.PassThroughCellIdsOn() - self.extract_skin_filter.Update() - polydata: vtkPolyData = self.extract_skin_filter.GetOutput() - polys = vtk_to_numpy(polydata.GetPolys().GetData()) - points = vtk_to_numpy(polydata.GetPoints().GetData()).ravel() - indices = vtk_to_numpy( - polydata.GetCellData().GetAbstractArray("vtkOriginalCellIds") - ) - return ( - polys, - points.astype(np.float32), - indices, - ) - - def find_closest_cell_to_ray( - self, grid: vtkExplicitStructuredGrid, ray: List[float] - ) -> Tuple[Optional[int], List[Optional[int]]]: - """Find the active cell closest to the given ray.""" - timer = PerfTimer() - locator = vtkCellLocator() - - locator.SetDataSet(grid) - locator.BuildLocator() - - # cell_ids = vtkIdList() - tolerance = reference(0.0) - - _t = reference(0) - _x = np.array([0, 0, 0]) - _pcoords = np.array([0, 0, 0]) - _sub_id = reference(0) - cell_id = reference(0) - _cell = vtkGenericCell() - - locator.IntersectWithLine( - ray[0], ray[1], tolerance, _t, _x, _pcoords, _sub_id, cell_id, _cell - ) - - # # Check if an array with OriginalCellIds is present, and if so use - # # that as the cell index, if not assume the grid is not cropped. - if grid.GetCellData().HasArray("vtkOriginalCellIds") == 1: - cell_id = vtk_to_numpy( - grid.GetCellData().GetAbstractArray("vtkOriginalCellIds") - )[cell_id] - - i = reference(0) - j = reference(0) - k = reference(0) - - # Find the ijk of the cell in the full grid - self.es_grid.ComputeCellStructuredCoords(cell_id, i, j, k, False) - print(f"Get ijk in {timer.lap_s():.2f}") - - return cell_id, [int(i), int(j), int(k)] - - @property - def imin(self) -> int: - return 0 - - @property - def imax(self) -> int: - return self.cell_dimensions[0] - 1 - - @property - def jmin(self) -> int: - return 0 - - @property - def jmax(self) -> int: - return self.cell_dimensions[1] - 1 - - @property - def kmin(self) -> int: - return 0 - - @property - def kmax(self) -> int: - return self.cell_dimensions[2] - 1 diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index e46476b10..6fd8eb6ea 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -9,7 +9,6 @@ EnsembleGridProvider, CellFilter, ) -from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor # pylint: disable = too-few-public-methods diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py deleted file mode 100644 index 19a8c8f3b..000000000 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_roff_grid_datamodel.py +++ /dev/null @@ -1,97 +0,0 @@ -from pathlib import Path -from typing import Callable, List, Tuple - -import numpy as np -import xtgeo - -from webviz_subsurface._utils.perf_timer import PerfTimer -from webviz_subsurface._utils.webvizstore_functions import get_path - -from ._xtgeo_to_explicit_structured_grid import xtgeo_grid_to_explicit_structured_grid -from ._explicit_structured_grid_accessor import ExplicitStructuredGridAccessor - - -def get_static_parameter_names(folder: Path, grid_name: str): - return list( - set( - fn.stem.split("--")[1] - for fn in Path(folder).glob(f"{grid_name}*.roff") - if len(fn.stem.split("--")) == 2 - ) - ) - - -def get_dynamic_parameter_names(folder: Path, grid_name: str): - return list( - set( - fn.stem.split("--")[1] - for fn in Path(folder).glob(f"{grid_name}*.roff") - if len(fn.stem.split("--")) == 3 - ) - ) - - -def get_dynamic_parameter_dates(folder: Path, grid_name: str): - - return list( - set( - fn.stem.split("--")[2] - for fn in Path(folder).glob(f"{grid_name}*.roff") - if len(fn.stem.split("--")) == 3 - ) - ) - - -class RoffGridDataModel: - def __init__( - self, - folder: Path, - grid_name: Path, - ): - - # self.add_webviz_store(egrid_file, init_file, restart_file) - - self.folder = folder - self.grid_name = grid_name - # Grid required when loading grid properties later on - self._xtg_grid = xtgeo.grid_from_file(Path(folder / f"{grid_name}.roff")) - - timer = PerfTimer() - print("Converting egrid to VTK ExplicitStructuredGrid") - self.esg_accessor = ExplicitStructuredGridAccessor( - xtgeo_grid_to_explicit_structured_grid(self._xtg_grid) - ) - print(f"Conversion complete in : {timer.lap_s():.2f}s") - self._restart_dates = self - - @property - def init_names(self) -> List[str]: - return get_static_parameter_names(self.folder, self.grid_name) - - @property - def restart_names(self) -> List[str]: - return get_dynamic_parameter_names(self.folder, self.grid_name) - - @property - def restart_dates(self) -> List[str]: - return get_dynamic_parameter_dates(self.folder, self.grid_name) - - def get_init_property(self, prop_name: str) -> xtgeo.GridProperty: - path = Path(self.folder / f"{self.grid_name}--{prop_name}.roff") - prop = xtgeo.gridproperty_from_file(path) - return prop - - def get_restart_property( - self, prop_name: str, prop_date: int - ) -> xtgeo.GridProperty: - path = Path(self.folder / f"{self.grid_name}--{prop_name}--{prop_date}.roff") - prop = xtgeo.gridproperty_from_file(path) - return prop - - def get_init_values(self, prop_name: str) -> np.ndarray: - prop = self.get_init_property(prop_name) - return prop.get_npvalues1d(order="F").ravel() - - def get_restart_values(self, prop_name: str, prop_date: int) -> np.ndarray: - prop = self.get_restart_property(prop_name, prop_date) - return prop.get_npvalues1d(order="F").ravel() diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py deleted file mode 100644 index 641ec0483..000000000 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid.py +++ /dev/null @@ -1,29 +0,0 @@ -import xtgeo -import pyvista as pv - -# The hack implementation requires both updated xtgeo and VTK version 9.2 -# from ._xtgeo_to_explicit_structured_grid_hack import ( -# xtgeo_grid_to_explicit_structured_grid_hack, -# ) - -# Requires updated xtgeo -from ._xtgeo_to_vtk_explicit_structured_grid import ( - xtgeo_grid_to_vtk_explicit_structured_grid, -) - - -def xtgeo_grid_to_explicit_structured_grid( - xtg_grid: xtgeo.Grid, -) -> pv.ExplicitStructuredGrid: - - # return xtgeo_grid_to_explicit_structured_grid_hack(xtg_grid) - return xtgeo_grid_to_vtk_explicit_structured_grid(xtg_grid) - - dims, corners, inactive = xtg_grid.get_vtk_geometries() - corners[:, 2] *= -1 - esg_grid = pv.ExplicitStructuredGrid(dims, corners) - esg_grid = esg_grid.compute_connectivity() - # esg_grid.ComputeFacesConnectivityFlagsArray() - esg_grid = esg_grid.hide_cells(inactive) - # esg_grid.flip_z(inplace=True) - return esg_grid diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py deleted file mode 100644 index 27793c19e..000000000 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_explicit_structured_grid_hack.py +++ /dev/null @@ -1,160 +0,0 @@ -import xtgeo -import pyvista as pv -import numpy as np - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkFiltersCore import vtkStaticCleanUnstructuredGrid -from vtkmodules.vtkFiltersCore import vtkUnstructuredGridToExplicitStructuredGrid -from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridToUnstructuredGrid -from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid -from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid -from vtkmodules.vtkCommonDataModel import vtkCellArray -from vtkmodules.vtkCommonCore import vtkPoints - -from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.util.numpy_support import numpy_to_vtkIdTypeArray -from vtkmodules.util import vtkConstants - -from webviz_subsurface._utils.perf_timer import PerfTimer - - -# Note that this implementation requires both: -# * hacked xtgeo -# * VTK version 9.2 -# ----------------------------------------------------------------------------- -def xtgeo_grid_to_explicit_structured_grid_hack( - xtg_grid: xtgeo.Grid, -) -> pv.ExplicitStructuredGrid: - - print("entering xtgeo_grid_to_explicit_structured_grid_hack()") - t = PerfTimer() - - dims, corners, inactive = xtg_grid.get_vtk_geometries_hack() - corners[:, 2] *= -1 - print(f"call to get_vtk_geometries_hack() took {t.lap_s():.2f}s") - - # print(f"{dims=}") - # print(f"{type(corners)=}") - # print(f"{corners.shape=}") - # print(f"{corners.dtype=}") - - vtk_esgrid = _make_clean_vtk_esgrid(dims, corners) - print(f"create vtk_esgrid : {t.lap_s():.2f}s") - - pv_esgrid = pv.ExplicitStructuredGrid(vtk_esgrid) - print(f"create pv grid from vtk grid: {t.lap_s():.2f}s") - - pv_esgrid = pv_esgrid.hide_cells(inactive) - print(f"pv_esgrid.hide_cells(inactive) : {t.lap_s():.2f}s") - - print(f"xtgeo_grid_to_explicit_structured_grid_hack() - DONE: {t.elapsed_s():.2f}s") - - print("==================================================================") - print(pv_esgrid) - print("==================================================================") - - return pv_esgrid - - -# ----------------------------------------------------------------------------- -def _make_clean_vtk_esgrid(dims, corners): - - print("entering _make_clean_vtk_esgrid()") - - timer = PerfTimer() - - points_np = corners - points_np = points_np.reshape(-1, 3) - # points_np = points_np.astype(np.float32) - - points_vtkarr = numpy_to_vtk(points_np, deep=1) - vtk_points = vtkPoints() - vtk_points.SetData(points_vtkarr) - - print(f"_make_clean_vtk_esgrid() - create points: {timer.lap_s():.2f}s") - - # Dims are number of points, so subtract 1 to get cell counts - num_conn = (dims[0] - 1) * (dims[1] - 1) * (dims[2] - 1) * 8 - conn_np = np.arange(0, num_conn) - - # cellconn_idarr = numpy_to_vtk(conn_np, deep=1, array_type=vtkConstants.VTK_ID_TYPE) - cellconn_idarr = numpy_to_vtkIdTypeArray(conn_np, deep=1) - - vtk_cellArray = vtkCellArray() - vtk_cellArray.SetData(8, cellconn_idarr) - - print(f"_make_clean_vtk_esgrid() - create cells: {timer.lap_s():.2f}s") - - vtk_esgrid = vtkExplicitStructuredGrid() - vtk_esgrid.SetDimensions(dims) - vtk_esgrid.SetPoints(vtk_points) - vtk_esgrid.SetCells(vtk_cellArray) - - print(f"_make_clean_vtk_esgrid() - create initial grid: {timer.lap_s():.2f}s") - - # print(pv.ExplicitStructuredGrid(vtk_esgrid)) - - ugrid = _vtk_esg_to_ug(vtk_esgrid) - print(f"_make_clean_vtk_esgrid() - esg to ug: {timer.lap_s():.2f}s") - ugrid = _clean_vtk_ug(ugrid) - print(f"_make_clean_vtk_esgrid() - clean ug: {timer.lap_s():.2f}s") - vtk_esgrid = _vtk_ug_to_esg(ugrid) - print(f"_make_clean_vtk_esgrid() - ug to esg: {timer.lap_s():.2f}s") - - # print(pv.ExplicitStructuredGrid(vtk_esgrid)) - - print(f"_make_clean_vtk_esgrid() - clean: {timer.lap_s():.2f}s") - - vtk_esgrid.ComputeFacesConnectivityFlagsArray() - - print(f"_make_clean_vtk_esgrid() - conn flags: {timer.lap_s():.2f}s") - - print(f"_make_clean_vtk_esgrid() - DONE: {timer.elapsed_s():.2f}s") - - return vtk_esgrid - - -# ----------------------------------------------------------------------------- -def _vtk_esg_to_ug(vtk_esgrid: vtkExplicitStructuredGrid) -> vtkUnstructuredGrid: - convertFilter = vtkExplicitStructuredGridToUnstructuredGrid() - convertFilter.SetInputData(vtk_esgrid) - convertFilter.Update() - vtk_ugrid = convertFilter.GetOutput() - - return vtk_ugrid - - -# ----------------------------------------------------------------------------- -def _vtk_ug_to_esg(vtk_ugrid: vtkUnstructuredGrid) -> vtkExplicitStructuredGrid: - convertFilter = vtkUnstructuredGridToExplicitStructuredGrid() - convertFilter.SetInputData(vtk_ugrid) - convertFilter.SetInputArrayToProcess(0, 0, 0, 1, "BLOCK_I") - convertFilter.SetInputArrayToProcess(1, 0, 0, 1, "BLOCK_J") - convertFilter.SetInputArrayToProcess(2, 0, 0, 1, "BLOCK_K") - convertFilter.Update() - vtk_esgrid = convertFilter.GetOutput() - - return vtk_esgrid - - -# ----------------------------------------------------------------------------- -def _clean_vtk_ug(vtk_ugrid: vtkUnstructuredGrid) -> vtkUnstructuredGrid: - - # !!!!!! - # Requires newer version of VTK - cleanfilter = vtkStaticCleanUnstructuredGrid() - # print(cleanfilter) - - cleanfilter.SetInputData(vtk_ugrid) - cleanfilter.SetAbsoluteTolerance(0.0) - cleanfilter.SetTolerance(0.0) - cleanfilter.SetToleranceIsAbsolute(True) - cleanfilter.GetLocator().SetTolerance(0.0) - cleanfilter.Update() - - # print(cleanfilter) - # print(cleanfilter.GetLocator()) - - vtk_ugrid_out = cleanfilter.GetOutput() - - return vtk_ugrid_out diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py deleted file mode 100644 index d8c9f5a99..000000000 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_xtgeo_to_vtk_explicit_structured_grid.py +++ /dev/null @@ -1,102 +0,0 @@ -import xtgeo -import numpy as np -import pyvista as pv - -# pylint: disable=no-name-in-module, -from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid -from vtkmodules.vtkCommonDataModel import vtkCellArray -from vtkmodules.vtkCommonDataModel import vtkDataSetAttributes -from vtkmodules.vtkCommonCore import vtkPoints -from vtkmodules.util.numpy_support import numpy_to_vtk -from vtkmodules.util.numpy_support import numpy_to_vtkIdTypeArray -from vtkmodules.util.numpy_support import vtk_to_numpy -from vtkmodules.util import vtkConstants - -from webviz_subsurface._utils.perf_timer import PerfTimer - -# from ._xtgeo_to_explicit_structured_grid_hack import ( -# _clean_vtk_ug, -# _vtk_esg_to_ug, -# _vtk_ug_to_esg, -# ) - - -# ----------------------------------------------------------------------------- -def xtgeo_grid_to_vtk_explicit_structured_grid( - xtg_grid: xtgeo.Grid, -) -> vtkExplicitStructuredGrid: - - print("entering xtgeo_grid_to_vtk_explicit_structured_grid()") - t = PerfTimer() - - pt_dims, vertex_arr, conn_arr, inactive_arr = xtg_grid.get_vtk_esg_geometry_data() - vertex_arr[:, 2] *= -1 - print(f"get_vtk_esg_geometry_data() took {t.lap_s():.2f}s") - - print(f"{pt_dims=}") - print(f"{vertex_arr.shape=}") - print(f"{vertex_arr.dtype=}") - print(f"{conn_arr.shape=}") - print(f"{conn_arr.dtype=}") - - vtk_esgrid = _create_vtk_esgrid_from_verts_and_conn(pt_dims, vertex_arr, conn_arr) - print(f"create vtk_esgrid : {t.lap_s():.2f}s") - - # Make sure we hide the inactive cells. - # First we let VTK allocate cell ghost array, then we obtain a numpy view - # on the array and write to that (we're actually modifying the native VTK array) - ghost_arr_vtk = vtk_esgrid.AllocateCellGhostArray() - ghost_arr_np = vtk_to_numpy(ghost_arr_vtk) - ghost_arr_np[inactive_arr] = vtkDataSetAttributes.HIDDENCELL - print(f"hide {len(inactive_arr)} inactive cells : {t.lap_s():.2f}s") - - print(f"memory used by vtk_esgrid: {vtk_esgrid.GetActualMemorySize()/1024.0:.2f}MB") - - print(f"xtgeo_grid_to_vtk_explicit_structured_grid() - DONE: {t.elapsed_s():.2f}s") - - # print("==================================================================") - # print(pv.ExplicitStructuredGrid(vtk_esgrid)) - # print("==================================================================") - - return vtk_esgrid - - -# ----------------------------------------------------------------------------- -def _create_vtk_esgrid_from_verts_and_conn( - point_dims: np.ndarray, vertex_arr_np: np.ndarray, conn_arr_np: np.ndarray -) -> vtkExplicitStructuredGrid: - - print("_create_vtk_esgrid_from_verts_and_conn() - entering") - - t = PerfTimer() - - vertex_arr_np = vertex_arr_np.reshape(-1, 3) - points_vtkarr = numpy_to_vtk(vertex_arr_np, deep=1) - vtk_points = vtkPoints() - vtk_points.SetData(points_vtkarr) - print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_points: {t.lap_s():.2f}s") - - # conn_idarr = numpy_to_vtk(conn_arr_np, deep=1, array_type=vtkConstants.VTK_ID_TYPE) - conn_idarr = numpy_to_vtkIdTypeArray(conn_arr_np, deep=1) - vtk_cellArray = vtkCellArray() - vtk_cellArray.SetData(8, conn_idarr) - print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_cellArray: {t.lap_s():.2f}s") - - vtk_esgrid = vtkExplicitStructuredGrid() - vtk_esgrid.SetDimensions(point_dims) - vtk_esgrid.SetPoints(vtk_points) - vtk_esgrid.SetCells(vtk_cellArray) - print(f"_create_vtk_esgrid_from_verts_and_conn() - vtk_esgrid: {t.lap_s():.2f}s") - - vtk_esgrid.ComputeFacesConnectivityFlagsArray() - print(f"_create_vtk_esgrid_from_verts_and_conn() - conn flags: {t.lap_s():.2f}s") - - # print(pv.ExplicitStructuredGrid(vtk_esgrid)) - # ugrid = _vtk_esg_to_ug(vtk_esgrid) - # ugrid = _clean_vtk_ug(ugrid) - # vtk_esgrid = _vtk_ug_to_esg(ugrid) - # print(pv.ExplicitStructuredGrid(vtk_esgrid)) - - print(f"_create_vtk_esgrid_from_verts_and_conn() - DONE: {t.elapsed_s():.2f}s") - - return vtk_esgrid From 3706c8f933c345a6684d0f9a00e6b8201a15296e Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 11 May 2022 15:38:47 +0200 Subject: [PATCH 43/63] Set fill value --- .../_providers/ensemble_grid_provider/provider_impl_roff.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py index 27ab9d120..9b5176048 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py @@ -253,7 +253,8 @@ def get_static_property_values( "Something has gone terribly wrong." ) grid_property = xtgeo.gridproperty_from_file(fn_list[0]) - return grid_property.get_npvalues1d(order="F").ravel() + fill_value = np.nan if not grid_property.isdiscrete else -1 + return grid_property.get_npvalues1d(order="F", fill_value=fill_value).ravel() def get_dynamic_property_values( self, property_name: str, property_date: str, realization: int From 7694a7d4e7adda718b5623b0b9214ceaefe35b02 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 11 May 2022 16:15:28 +0200 Subject: [PATCH 44/63] Fix crop width --- .../plugins/_eclipse_grid_viewer/_callbacks.py | 2 +- webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index e4490acfe..036fed6b9 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -341,4 +341,4 @@ def _store_grid_range_from_crop_widget( ) -> List[List[int]]: if not input_vals or not width_vals: return no_update - return [[val, val + width] for val, width in zip(input_vals, width_vals)] + return [[val, val + width - 1] for val, width in zip(input_vals, width_vals)] diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 6fd8eb6ea..de94c0dda 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -229,7 +229,7 @@ def crop_widget( }, ), dcc.Input( - style={"width": "30px", "height": "10px"}, + style={"width": "50px", "height": "10px"}, id={ "id": get_uuid(LayoutElements.CROP_WIDGET), "direction": direction, @@ -276,7 +276,7 @@ def crop_widget( }, ), dcc.Input( - style={"width": "30px", "height": "10px"}, + style={"width": "50px", "height": "10px"}, id={ "id": get_uuid(LayoutElements.CROP_WIDGET), "direction": direction, @@ -288,7 +288,7 @@ def crop_widget( persistence=True, persistence_type="session", value=max_width, - min=min_val, + min=1, max=max_val, ), wcc.Slider( @@ -298,7 +298,7 @@ def crop_widget( "component": "slider", "component2": "width", }, - min=min_val, + min=1, max=max_val, value=max_width, step=1, From e2e515b11c865620207858109cc2891077ef01bf Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 11 May 2022 16:16:06 +0200 Subject: [PATCH 45/63] Add grid name to provider id --- .../ensemble_grid_provider/ensemble_grid_provider_factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py b/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py index de4495346..acbd7abee 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/ensemble_grid_provider_factory.py @@ -64,9 +64,9 @@ def create_from_roff_files( ) -> EnsembleGridProvider: timer = PerfTimer() string_to_hash = ( - f"{ens_path}" + f"{ens_path}_{grid_name}" if attribute_filter is None - else f"{ens_path}_{'_'.join([str(attr) for attr in attribute_filter])}" + else f"{ens_path}_{grid_name}_{'_'.join([str(attr) for attr in attribute_filter])}" ) storage_key = f"ens__{_make_hash_string(string_to_hash)}" provider = ProviderImplRoff.from_backing_store(self._storage_dir, storage_key) From ae4bd1f2dfd051121c588fe9b92e4d2fe87160da Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Wed, 11 May 2022 16:37:00 +0200 Subject: [PATCH 46/63] Added logging and fixed IJK filtering --- .../grid_viz_service.py | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py index e6004e348..6025c08f8 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py @@ -20,6 +20,7 @@ from vtkmodules.util.numpy_support import vtk_to_numpy +from webviz_subsurface._utils.perf_timer import PerfTimer from .ensemble_grid_provider import EnsembleGridProvider # Requires updated xtgeo @@ -129,6 +130,16 @@ def get_surface( cell_filter: Optional[CellFilter], ) -> Tuple[SurfacePolys, Optional[PropertyScalars]]: + LOGGER.debug( + f"Getting grid surface... " + f"(provider_id={provider_id}, real={realization}, " + f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " + f"I=[{cell_filter.i_min},{cell_filter.i_max}] " + f"J=[{cell_filter.j_min},{cell_filter.j_max}] " + f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + ) + timer = PerfTimer() + provider = self._id_to_provider_dict.get(provider_id) if not provider: raise ValueError("Could not find provider") @@ -170,6 +181,15 @@ def get_surface( worker.set_cached_original_cell_indices(cell_filter, original_cell_indices_np) + LOGGER.debug( + f"Got grid surface in {timer.elapsed_s():.2f}s " + f"(provider_id={provider_id}, real={realization}, " + f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " + f"I=[{cell_filter.i_min},{cell_filter.i_max}] " + f"J=[{cell_filter.j_min},{cell_filter.j_max}] " + f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + ) + return surface_polys, property_scalars def get_mapped_property_values( @@ -180,6 +200,16 @@ def get_mapped_property_values( cell_filter: Optional[CellFilter], ) -> Optional[PropertyScalars]: + LOGGER.debug( + f"Getting property values... " + f"(provider_id={provider_id}, real={realization}, " + f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " + f"I=[{cell_filter.i_min},{cell_filter.i_max}] " + f"J=[{cell_filter.j_min},{cell_filter.j_max}] " + f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + ) + timer = PerfTimer() + provider = self._id_to_provider_dict.get(provider_id) if not provider: raise ValueError("Could not find provider") @@ -198,25 +228,37 @@ def get_mapped_property_values( ) if raw_cell_values is None: + LOGGER.warning( + f"No cell values found for " + f"prop=({property_spec.prop_name}, {property_spec.prop_name})" + ) return None original_cell_indices_np = worker.get_cached_original_cell_indices(cell_filter) - if original_cell_indices_np is not None: - mapped_cell_values = raw_cell_values[original_cell_indices_np] - return PropertyScalars(mapped_cell_values) - - # Must first generate the grid to get the original cell indices - grid = worker.get_full_esgrid() - if cell_filter: - grid = _calc_cropped_grid(grid, cell_filter) + if original_cell_indices_np is None: + # Must first generate the grid to get the original cell indices + grid = worker.get_full_esgrid() + if cell_filter: + grid = _calc_cropped_grid(grid, cell_filter) + + polydata = _calc_grid_surface(grid) + original_cell_indices_np = vtk_to_numpy( + polydata.GetCellData().GetAbstractArray("vtkOriginalCellIds") + ) + worker.set_cached_original_cell_indices( + cell_filter, original_cell_indices_np + ) - polydata = _calc_grid_surface(grid) - original_cell_indices_np = vtk_to_numpy( - polydata.GetCellData().GetAbstractArray("vtkOriginalCellIds") - ) mapped_cell_values = raw_cell_values[original_cell_indices_np] - worker.set_cached_original_cell_indices(cell_filter, original_cell_indices_np) + LOGGER.debug( + f"Got property values in {timer.elapsed_s():.2f}s " + f"(provider_id={provider_id}, real={realization}, " + f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " + f"I=[{cell_filter.i_min},{cell_filter.i_max}] " + f"J=[{cell_filter.j_min},{cell_filter.j_max}] " + f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + ) return PropertyScalars(value_arr=mapped_cell_values) @@ -258,13 +300,15 @@ def _calc_cropped_grid( ) -> vtkExplicitStructuredGrid: crop_filter = vtkExplicitStructuredGridCrop() crop_filter.SetInputData(esgrid) + + # In VTK dimensions correspond to points crop_filter.SetOutputWholeExtent( cell_filter.i_min, - cell_filter.i_max, - cell_filter.j_min, - cell_filter.j_max, + cell_filter.i_max + 1, cell_filter.j_min, - cell_filter.j_max, + cell_filter.j_max + 1, + cell_filter.k_min, + cell_filter.k_max + 1, ) crop_filter.Update() cropped_grid = crop_filter.GetOutput() From ae43b7eed6e5c84135134cb4c34ffcd1bb39d1ba Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Wed, 11 May 2022 18:01:00 +0200 Subject: [PATCH 47/63] Python 3.6 fixes --- .../_xtgeo_to_vtk_explicit_structured_grid.py | 10 +++++----- .../ensemble_grid_provider/grid_viz_service.py | 13 ++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py b/webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py index 24d1da238..4da1324eb 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/_xtgeo_to_vtk_explicit_structured_grid.py @@ -32,11 +32,11 @@ def xtgeo_grid_to_vtk_explicit_structured_grid( vertex_arr[:, 2] *= -1 print(f"get_vtk_esg_geometry_data() took {t.lap_s():.2f}s") - print(f"{pt_dims=}") - print(f"{vertex_arr.shape=}") - print(f"{vertex_arr.dtype=}") - print(f"{conn_arr.shape=}") - print(f"{conn_arr.dtype=}") + print(f"pt_dims={pt_dims}") + print(f"vertex_arr.shape={vertex_arr.shape}") + print(f"vertex_arr.dtype={vertex_arr.dtype}") + print(f"conn_arr.shape={conn_arr.shape}") + print(f"conn_arr.dtype={conn_arr.dtype}") vtk_esgrid = _create_vtk_esgrid_from_verts_and_conn(pt_dims, vertex_arr, conn_arr) print(f"create vtk_esgrid : {t.lap_s():.2f}s") diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py index 6025c08f8..7f4e09443 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py @@ -32,6 +32,8 @@ LOGGER = logging.getLogger(__name__) +_GRID_VIZ_SERVICE_INSTANCE: Optional["GridVizService"] = None + @dataclass class PropertySpec: @@ -98,12 +100,12 @@ def __init__(self) -> None: @staticmethod def instance() -> "GridVizService": - global GRID_VIZ_SERVICE_INSTANCE - if not GRID_VIZ_SERVICE_INSTANCE: + global _GRID_VIZ_SERVICE_INSTANCE + if not _GRID_VIZ_SERVICE_INSTANCE: LOGGER.debug("Initializing GridVizService instance") - GRID_VIZ_SERVICE_INSTANCE = GridVizService() + _GRID_VIZ_SERVICE_INSTANCE = GridVizService() - return GRID_VIZ_SERVICE_INSTANCE + return _GRID_VIZ_SERVICE_INSTANCE def register_provider(self, provider: EnsembleGridProvider) -> None: provider_id = provider.provider_id() @@ -323,6 +325,3 @@ def _calc_grid_surface(esgrid: vtkExplicitStructuredGrid) -> vtkPolyData: polydata: vtkPolyData = surf_filter.GetOutput() return polydata - - -GRID_VIZ_SERVICE_INSTANCE: Optional[GridVizService] = None From 65db81fc2154793e0625d3456de85c7222b2043d Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Mon, 23 May 2022 08:49:04 +0200 Subject: [PATCH 48/63] Implemented GridVizService.ray_pick() --- .../ensemble_grid_provider/__init__.py | 2 +- .../grid_viz_service.py | 220 ++++++++++++++---- .../_eclipse_grid_viewer/_callbacks.py | 69 +++--- 3 files changed, 217 insertions(+), 74 deletions(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/__init__.py b/webviz_subsurface/_providers/ensemble_grid_provider/__init__.py index 4a0ce7862..3519237a4 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/__init__.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/__init__.py @@ -1,3 +1,3 @@ from .ensemble_grid_provider import EnsembleGridProvider from .ensemble_grid_provider_factory import EnsembleGridProviderFactory -from .grid_viz_service import GridVizService, CellFilter, PropertySpec +from .grid_viz_service import GridVizService, CellFilter, PropertySpec, Ray, PickResult diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py index 7f4e09443..29a61c5e7 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py @@ -11,10 +11,12 @@ from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter - +from vtkmodules.vtkCommonCore import reference from vtkmodules.vtkCommonDataModel import ( vtkExplicitStructuredGrid, vtkPolyData, + vtkCellLocator, + vtkGenericCell, ) from vtkmodules.util.numpy_support import vtk_to_numpy @@ -64,6 +66,23 @@ class PropertyScalars: # max_value: float +@dataclass +class Ray: + origin: List[float] + end: List[float] + # direction: List[float] + + +@dataclass +class PickResult: + cell_index: int + cell_i: int + cell_j: int + cell_k: int + intersection_point: List[float] + cell_property_value: Optional[float] + + class GridWorker: def __init__(self, full_esgrid: vtkExplicitStructuredGrid) -> None: self._full_esgrid = full_esgrid @@ -135,10 +154,8 @@ def get_surface( LOGGER.debug( f"Getting grid surface... " f"(provider_id={provider_id}, real={realization}, " - f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " - f"I=[{cell_filter.i_min},{cell_filter.i_max}] " - f"J=[{cell_filter.j_min},{cell_filter.j_max}] " - f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + f"{_property_spec_dbg_str(property_spec)}, " + f"{_cell_filter_dbg_str(cell_filter)})" ) timer = PerfTimer() @@ -169,27 +186,18 @@ def get_surface( property_scalars: Optional[PropertyScalars] = None if property_spec: - if property_spec.prop_date: - raw_cell_values = provider.get_dynamic_property_values( - property_spec.prop_name, property_spec.prop_date, realization - ) - else: - raw_cell_values = provider.get_static_property_values( - property_spec.prop_name, realization - ) - if raw_cell_values is not None: - mapped_cell_values = raw_cell_values[original_cell_indices_np] - property_scalars = PropertyScalars(value_arr=mapped_cell_values) + raw_cell_vals = _load_property_values(provider, realization, property_spec) + if raw_cell_vals is not None: + mapped_cell_vals = raw_cell_vals[original_cell_indices_np] + property_scalars = PropertyScalars(value_arr=mapped_cell_vals) worker.set_cached_original_cell_indices(cell_filter, original_cell_indices_np) LOGGER.debug( f"Got grid surface in {timer.elapsed_s():.2f}s " f"(provider_id={provider_id}, real={realization}, " - f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " - f"I=[{cell_filter.i_min},{cell_filter.i_max}] " - f"J=[{cell_filter.j_min},{cell_filter.j_max}] " - f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + f"{_property_spec_dbg_str(property_spec)}, " + f"{_cell_filter_dbg_str(cell_filter)})" ) return surface_polys, property_scalars @@ -205,10 +213,8 @@ def get_mapped_property_values( LOGGER.debug( f"Getting property values... " f"(provider_id={provider_id}, real={realization}, " - f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " - f"I=[{cell_filter.i_min},{cell_filter.i_max}] " - f"J=[{cell_filter.j_min},{cell_filter.j_max}] " - f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + f"{_property_spec_dbg_str(property_spec)}, " + f"{_cell_filter_dbg_str(cell_filter)})" ) timer = PerfTimer() @@ -220,16 +226,8 @@ def get_mapped_property_values( if not worker: raise ValueError("Could not get grid worker") - if property_spec.prop_date: - raw_cell_values = provider.get_dynamic_property_values( - property_spec.prop_name, property_spec.prop_date, realization - ) - else: - raw_cell_values = provider.get_static_property_values( - property_spec.prop_name, realization - ) - - if raw_cell_values is None: + raw_cell_vals = _load_property_values(provider, realization, property_spec) + if raw_cell_vals is None: LOGGER.warning( f"No cell values found for " f"prop=({property_spec.prop_name}, {property_spec.prop_name})" @@ -251,29 +249,90 @@ def get_mapped_property_values( cell_filter, original_cell_indices_np ) - mapped_cell_values = raw_cell_values[original_cell_indices_np] + mapped_cell_vals = raw_cell_vals[original_cell_indices_np] LOGGER.debug( f"Got property values in {timer.elapsed_s():.2f}s " f"(provider_id={provider_id}, real={realization}, " - f"prop=({property_spec.prop_name}, {property_spec.prop_date}), " - f"I=[{cell_filter.i_min},{cell_filter.i_max}] " - f"J=[{cell_filter.j_min},{cell_filter.j_max}] " - f"K=[{cell_filter.k_min},{cell_filter.k_max}])" + f"{_property_spec_dbg_str(property_spec)}, " + f"{_cell_filter_dbg_str(cell_filter)})" ) - return PropertyScalars(value_arr=mapped_cell_values) + return PropertyScalars(value_arr=mapped_cell_vals) def ray_pick( self, provider_id: str, realization: int, - ray: List[float], + ray: Ray, property_spec: Optional[PropertySpec], cell_filter: Optional[CellFilter], - ) -> None: - # TODO!!! - pass + ) -> Optional[PickResult]: + + LOGGER.debug( + f"Doing ray pick: " + f"ray.origin={ray.origin}, ray.end={ray.end}, " + f"(provider_id={provider_id}, real={realization}, " + f"{_property_spec_dbg_str(property_spec)}, " + f"{_cell_filter_dbg_str(cell_filter)})" + ) + timer = PerfTimer() + + provider = self._id_to_provider_dict.get(provider_id) + if not provider: + raise ValueError("Could not find provider") + + worker = self._get_or_create_grid_worker(provider_id, realization) + if not worker: + raise ValueError("Could not get grid worker") + + grid = worker.get_full_esgrid() + if cell_filter: + grid = _calc_cropped_grid(grid, cell_filter) + et_crop_s = timer.lap_s() + + cell_id, isect_pt = _raypick_in_grid(grid, ray) + et_pick_s = timer.lap_s() + if cell_id is None: + return None + + original_cell_id = cell_id + if cell_filter: + # If a cell filter is present, assume picking was done against cropped grid + original_cell_id = ( + grid.GetCellData() + .GetAbstractArray("vtkOriginalCellIds") + .GetValue(cell_id) + ) + + i_ref = reference(0) + j_ref = reference(0) + k_ref = reference(0) + grid.ComputeCellStructuredCoords(cell_id, i_ref, j_ref, k_ref, True) + + cell_property_val: Optional[float] = None + if property_spec: + raw_cell_vals = _load_property_values(provider, realization, property_spec) + if raw_cell_vals is not None: + cell_property_val = raw_cell_vals[original_cell_id] + et_props_s = timer.lap_s() + + LOGGER.debug( + f"Did ray pick in {timer.elapsed_s():.2f}s (" + f"crop={et_crop_s:.2f}s, pick={et_pick_s:.2f}s, props={et_props_s:.2f}s, " + f"provider_id={provider_id}, real={realization}, " + f"{_property_spec_dbg_str(property_spec)}, " + f"{_cell_filter_dbg_str(cell_filter)})" + ) + + return PickResult( + cell_index=original_cell_id, + cell_i=i_ref.get(), + cell_j=j_ref.get(), + cell_k=k_ref.get(), + intersection_point=isect_pt, + cell_property_value=cell_property_val, + ) def _get_or_create_grid_worker( self, provider_id: str, realization: int @@ -317,6 +376,46 @@ def _calc_cropped_grid( return cropped_grid +def _raypick_in_grid( + esgrid: vtkExplicitStructuredGrid, ray: Ray +) -> Optional[Tuple[int, List[float]]]: + """Do a ray pick against the specified grid. + Returns None if nothing was hit, otherwise returns the cellId (cell index) of the cell + that was hit and the intersection point + """ + + locator = vtkCellLocator() + locator.SetDataSet(esgrid) + locator.BuildLocator() + + tolerance = 0.0 + t_ref = reference(0.0) + isect_pt = [0.0, 0.0, 0.0] + pcoords = [0.0, 0.0, 0.0] + sub_id_ref = reference(0) + cell_id_ref = reference(0) + cell = vtkGenericCell() + + # From doc for vtkCell it seems that isect_pt will be the actual intersection point + # while pcoords is in parametric coordinates + anyHits = locator.IntersectWithLine( + ray.origin, + ray.end, + tolerance, + t_ref, + isect_pt, + pcoords, + sub_id_ref, + cell_id_ref, + cell, + ) + + if not anyHits: + return None + + return cell_id_ref.get(), isect_pt + + def _calc_grid_surface(esgrid: vtkExplicitStructuredGrid) -> vtkPolyData: surf_filter = vtkExplicitStructuredGridSurfaceFilter() surf_filter.SetInputData(esgrid) @@ -325,3 +424,36 @@ def _calc_grid_surface(esgrid: vtkExplicitStructuredGrid) -> vtkPolyData: polydata: vtkPolyData = surf_filter.GetOutput() return polydata + + +def _load_property_values( + provider: EnsembleGridProvider, realization: int, property_spec: PropertySpec +) -> Optional[np.ndarray]: + if property_spec.prop_date: + prop_values = provider.get_dynamic_property_values( + property_spec.prop_name, property_spec.prop_date, realization + ) + else: + prop_values = provider.get_static_property_values( + property_spec.prop_name, realization + ) + + return prop_values + + +def _property_spec_dbg_str(property_spec: Optional[PropertySpec]) -> str: + if not property_spec: + return "prop=None" + + return f"prop=({property_spec.prop_name}, {property_spec.prop_date})" + + +def _cell_filter_dbg_str(cell_filter: Optional[CellFilter]) -> str: + if not cell_filter: + return "IJK=None" + + return ( + f"I=[{cell_filter.i_min},{cell_filter.i_max}] " + f"J=[{cell_filter.j_min},{cell_filter.j_max}] " + f"K=[{cell_filter.k_min},{cell_filter.k_max}]" + ) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 036fed6b9..eb676ab6a 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -13,6 +13,7 @@ GridVizService, PropertySpec, CellFilter, + Ray, ) from ._layout import PROPERTYTYPE, LayoutElements, GRID_DIRECTION @@ -192,9 +193,10 @@ def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> floa Input(get_uuid(LayoutElements.ENABLE_PICKING), "value"), Input(get_uuid(LayoutElements.PROPERTIES), "value"), Input(get_uuid(LayoutElements.DATES), "value"), + Input(get_uuid(LayoutElements.REALIZATIONS), "value"), + Input(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), Input(get_uuid(LayoutElements.INIT_RESTART), "value"), State(get_uuid(LayoutElements.Z_SCALE), "value"), - Input(get_uuid(LayoutElements.GRID_RANGE_STORE), "data"), State(get_uuid(LayoutElements.VTK_PICK_REPRESENTATION), "actor"), ) # pylint: disable = too-many-locals, too-many-arguments @@ -203,9 +205,10 @@ def _update_click_info( enable_picking: Optional[str], prop: List[str], date: List[int], + realizations: List[int], + grid_range: List[List[int]], proptype: str, zscale: float, - grid_range: List[List[int]], pick_representation_actor: Optional[Dict], ) -> Tuple[str, Dict[str, Any], Dict[str, bool]]: pick_representation_actor = ( @@ -218,47 +221,55 @@ def _update_click_info( return "", {}, pick_representation_actor pick_representation_actor.update({"visibility": True}) - if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: - scalar = datamodel.get_init_values(prop[0]) - else: - scalar = datamodel.get_restart_values(prop[0], date[0]) + client_world_pos = click_data["worldPosition"] + client_ray = click_data["ray"] - cropped_grid = datamodel.esg_accessor.crop(*grid_range) + # Remove z-scaling from client ray + client_world_pos[2] = client_world_pos[2] / zscale + client_ray[0][2] = client_ray[0][2] / zscale + client_ray[1][2] = client_ray[1][2] / zscale - # Getting position and ray below mouse position - coords = click_data["worldPosition"].copy() + ray = Ray(origin=client_ray[0], end=client_ray[1]) + cell_filter = CellFilter( + i_min=grid_range[0][0], + i_max=grid_range[0][1], + j_min=grid_range[1][0], + j_max=grid_range[1][1], + k_min=grid_range[2][0], + k_max=grid_range[2][1], + ) - ray = click_data["ray"] - # Remove z-scaling from points - coords[2] = coords[2] / zscale - ray[0][2] = ray[0][2] / zscale - ray[1][2] = ray[1][2] / zscale + if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: + property_spec = PropertySpec(prop_name=prop[0], prop_date=0) + else: + property_spec = PropertySpec(prop_name=prop[0], prop_date=date[0]) - # Find the cell index and i,j,k of the closest cell the ray intersects - cell_id, ijk = datamodel.esg_accessor.find_closest_cell_to_ray( - cropped_grid, ray + pick_result = grid_viz_service.ray_pick( + provider_id=grid_provider.provider_id(), + realization=realizations[0], + ray=ray, + property_spec=property_spec, + cell_filter=cell_filter, ) - # Get the scalar value of the cell index - scalar_value = scalar[cell_id] if cell_id is not None else np.nan + pick_sphere_pos = pick_result.intersection_point.copy() + pick_sphere_pos[2] *= zscale propname = f"{prop[0]}-{date[0]}" if date else f"{prop[0]}" return ( json.dumps( { - "x": coords[0], - "y": coords[1], - "z": coords[2], - "i": ijk[0], - "j": ijk[1], - "k": ijk[2], - propname: float( - scalar_value, - ), + "x": pick_result.intersection_point[0], + "y": pick_result.intersection_point[1], + "z": pick_result.intersection_point[2], + "i": pick_result.cell_i, + "j": pick_result.cell_j, + "k": pick_result.cell_k, + propname: float(pick_result.cell_property_value), }, indent=2, ), - {"center": click_data["worldPosition"], "radius": 100}, + {"center": pick_sphere_pos, "radius": 100}, pick_representation_actor, ) From cafe42b21b9f422b3f8501989e500486fbeb281c Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Mon, 23 May 2022 12:06:55 +0200 Subject: [PATCH 49/63] Compose correct relative path for grid properties --- .../_providers/ensemble_grid_provider/provider_impl_roff.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py index 9b5176048..a426a07fa 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/provider_impl_roff.py @@ -352,6 +352,5 @@ def _compose_rel_grid_pathstr( if not attribute and not datestr: return str(Path(f"{real}--{name}{extension}")) if not datestr: - return str(Path(f"{real}--{name}--{attribute}--{datestr}{extension}")) - - return str(Path(f"{real}--{name}--{attribute}{extension}")) + return str(Path(f"{real}--{name}--{attribute}{extension}")) + return str(Path(f"{real}--{name}--{attribute}--{datestr}{extension}")) From 39e72667df3ba17be0899aab9109c969f87c569a Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Mon, 23 May 2022 12:17:02 +0200 Subject: [PATCH 50/63] Allow only initial or dynamic properties --- .../plugins/_eclipse_grid_viewer/_layout.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index de94c0dda..a31beb0e7 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -86,7 +86,7 @@ def plugin_main_layout( sidebar( get_uuid=get_uuid, grid_dimensions=grid_dimensions, - realizations=grid_provider.realizations(), + grid_provider=grid_provider, ), vtk_view(get_uuid=get_uuid), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), @@ -103,8 +103,26 @@ def plugin_main_layout( def sidebar( - get_uuid: Callable, grid_dimensions: CellFilter, realizations: List[int] + get_uuid: Callable, grid_dimensions: CellFilter, grid_provider: EnsembleGridProvider ) -> wcc.Frame: + + realizations = grid_provider.realizations() + property_options = [] + property_value = None + + if grid_provider.static_property_names(): + property_options.append( + {"label": PROPERTYTYPE.INIT, "value": PROPERTYTYPE.INIT} + ) + property_value = PROPERTYTYPE.INIT + + if grid_provider.dynamic_property_names(): + property_options.append( + {"label": PROPERTYTYPE.RESTART, "value": PROPERTYTYPE.RESTART} + ) + if property_value is None: + property_value = PROPERTYTYPE.RESTART + return wcc.Frame( style=LayoutStyle.SIDEBAR, children=[ @@ -118,8 +136,8 @@ def sidebar( wcc.RadioItems( label=LayoutTitles.INIT_RESTART, id=get_uuid(LayoutElements.INIT_RESTART), - options=[{"label": prop, "value": prop} for prop in PROPERTYTYPE], - value=PROPERTYTYPE.INIT, + options=property_options, + value=property_value, ), wcc.SelectWithLabel( id=get_uuid(LayoutElements.PROPERTIES), label=LayoutTitles.PROPERTIES From cc6f3809cc2af726c0a5e91e632e8cde295e5931 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 24 May 2022 23:31:16 +0200 Subject: [PATCH 51/63] Only reset camera on realization/scale change --- webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index eb676ab6a..023275b05 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -178,11 +178,10 @@ def _set_representation_property( @callback( Output(get_uuid(LayoutElements.VTK_VIEW), "triggerResetCamera"), - Input(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "polys"), - Input(get_uuid(LayoutElements.VTK_GRID_POLYDATA), "points"), + Input(get_uuid(LayoutElements.REALIZATIONS), "value"), Input(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), ) - def _reset_camera(_polys: np.ndarray, _points: np.ndarray, _actor: dict) -> float: + def _reset_camera(realizations: List[int], _actor: dict) -> float: return time() @callback( From baec9f846e9f5ae79a82a868edf55595f049beeb Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Tue, 24 May 2022 23:31:28 +0200 Subject: [PATCH 52/63] Use RMS style interactions --- .../plugins/_eclipse_grid_viewer/_layout.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index a31beb0e7..8024d2321 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -333,6 +333,37 @@ def vtk_view(get_uuid: Callable) -> webviz_vtk.View: id=get_uuid(LayoutElements.VTK_VIEW), style=LayoutStyle.VTK_VIEW, pickingModes=["click"], + interactorSettings=[ + { + "button": 1, + "action": "Zoom", + "scrollEnabled": True, + }, + { + "button": 3, + "action": "Pan", + }, + { + "button": 2, + "action": "Rotate", + }, + { + "button": 1, + "action": "Pan", + "shift": True, + }, + { + "button": 1, + "action": "Zoom", + "alt": True, + }, + { + "button": 1, + "action": "Roll", + "alt": True, + "shift": True, + }, + ], children=[ webviz_vtk.GeometryRepresentation( id=get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), From 95b6471c06b8277f7008f016c7af5f1e237e9d55 Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Sun, 29 May 2022 14:12:39 +0200 Subject: [PATCH 53/63] Experimental impl of cut_along_polyline() --- .../grid_viz_service.py | 371 +++++++++++++++++- 1 file changed, 355 insertions(+), 16 deletions(-) diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py index 29a61c5e7..f99f96a85 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py @@ -1,36 +1,42 @@ +import dataclasses import logging -from pathlib import Path -from typing import Callable, List, Tuple, Dict, Optional, Any from dataclasses import dataclass -import dataclasses from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Tuple import numpy as np - import xtgeo - -from vtkmodules.vtkFiltersCore import vtkExplicitStructuredGridCrop -from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter -from vtkmodules.vtkCommonCore import reference +from vtkmodules.util.numpy_support import vtk_to_numpy +from vtkmodules.vtkCommonCore import reference, vtkIdList, vtkPoints from vtkmodules.vtkCommonDataModel import ( - vtkExplicitStructuredGrid, - vtkPolyData, + vtkCellArray, vtkCellLocator, + vtkExplicitStructuredGrid, vtkGenericCell, + vtkLine, + vtkPlane, + vtkPolyData, + vtkStaticCellLocator, + vtkUnstructuredGrid, ) - -from vtkmodules.util.numpy_support import vtk_to_numpy - +from vtkmodules.vtkFiltersCore import ( + vtkAppendPolyData, + vtkExplicitStructuredGridCrop, + vtkExplicitStructuredGridToUnstructuredGrid, + vtkPlaneCutter, + vtkPolyDataPlaneClipper, + vtkUnstructuredGridToExplicitStructuredGrid, +) +from vtkmodules.vtkFiltersGeneral import vtkBoxClipDataSet +from vtkmodules.vtkFiltersGeometry import vtkExplicitStructuredGridSurfaceFilter from webviz_subsurface._utils.perf_timer import PerfTimer -from .ensemble_grid_provider import EnsembleGridProvider # Requires updated xtgeo from ._xtgeo_to_vtk_explicit_structured_grid import ( xtgeo_grid_to_vtk_explicit_structured_grid, ) - -from webviz_subsurface._utils.perf_timer import PerfTimer +from .ensemble_grid_provider import EnsembleGridProvider LOGGER = logging.getLogger(__name__) @@ -83,16 +89,20 @@ class PickResult: cell_property_value: Optional[float] +# ============================================================================= class GridWorker: + # ----------------------------------------------------------------------------- def __init__(self, full_esgrid: vtkExplicitStructuredGrid) -> None: self._full_esgrid = full_esgrid self._cached_cell_filter: Optional[CellFilter] = None self._cached_original_cell_indices: Optional[np.ndarray] = None + # ----------------------------------------------------------------------------- def get_full_esgrid(self) -> vtkExplicitStructuredGrid: return self._full_esgrid + # ----------------------------------------------------------------------------- def get_cached_original_cell_indices( self, cell_filter: Optional[CellFilter] ) -> Optional[np.ndarray]: @@ -104,6 +114,7 @@ def get_cached_original_cell_indices( return None + # ----------------------------------------------------------------------------- def set_cached_original_cell_indices( self, cell_filter: Optional[CellFilter], original_cell_indices: np.ndarray ) -> None: @@ -112,11 +123,14 @@ def set_cached_original_cell_indices( self._cached_original_cell_indices = original_cell_indices +# ============================================================================= class GridVizService: + # ----------------------------------------------------------------------------- def __init__(self) -> None: self._id_to_provider_dict: Dict[str, EnsembleGridProvider] = {} self._key_to_worker_dict: Dict[str, GridWorker] = {} + # ----------------------------------------------------------------------------- @staticmethod def instance() -> "GridVizService": global _GRID_VIZ_SERVICE_INSTANCE @@ -126,6 +140,7 @@ def instance() -> "GridVizService": return _GRID_VIZ_SERVICE_INSTANCE + # ----------------------------------------------------------------------------- def register_provider(self, provider: EnsembleGridProvider) -> None: provider_id = provider.provider_id() LOGGER.debug(f"Adding grid provider with id={provider_id}") @@ -143,6 +158,7 @@ def register_provider(self, provider: EnsembleGridProvider) -> None: self._id_to_provider_dict[provider_id] = provider + # ----------------------------------------------------------------------------- def get_surface( self, provider_id: str, @@ -202,6 +218,7 @@ def get_surface( return surface_polys, property_scalars + # ----------------------------------------------------------------------------- def get_mapped_property_values( self, provider_id: str, @@ -260,6 +277,191 @@ def get_mapped_property_values( return PropertyScalars(value_arr=mapped_cell_vals) + # ----------------------------------------------------------------------------- + def cut_along_polyline( + self, + provider_id: str, + realization: int, + polyline_xy: List[float], + property_spec: Optional[PropertySpec], + ) -> Tuple[SurfacePolys, Optional[PropertyScalars]]: + + LOGGER.debug( + f"Cutting along polyline... " + f"(provider_id={provider_id}, real={realization})" + ) + timer = PerfTimer() + + provider = self._id_to_provider_dict.get(provider_id) + if not provider: + raise ValueError("Could not find provider") + + worker = self._get_or_create_grid_worker(provider_id, realization) + if not worker: + raise ValueError("Could not get grid worker") + + esgrid = worker.get_full_esgrid() + + bounds = esgrid.GetBounds() + min_z = bounds[4] + max_z = bounds[5] + + num_points_in_polyline = int(len(polyline_xy) / 2) + + ugrid = _vtk_esg_to_ug(esgrid) + + # !!!!!!!!!!!!!! + # Requires VTK 9.2-ish + # ugrid = _extract_intersected_ugrid(ugrid, polyline_xy, 10.0) + + cutter_alg = vtkPlaneCutter() + cutter_alg.SetInputDataObject(ugrid) + + # cell_locator = vtkStaticCellLocator() + # cell_locator.SetDataSet(esgrid) + # cell_locator.BuildLocator() + + # box_clip_alg = vtkBoxClipDataSet() + # box_clip_alg.SetInputDataObject(ugrid) + + append_alg = vtkAppendPolyData() + cut_surface_polydata_arr = [] + et_setup_s = timer.lap_s() + + et_cut_s = 0.0 + et_clip_s = 0.0 + + for i in range(0, num_points_in_polyline - 1): + x0 = polyline_xy[2 * i] + y0 = polyline_xy[2 * i + 1] + x1 = polyline_xy[2 * (i + 1)] + y1 = polyline_xy[2 * (i + 1) + 1] + fwd_vec = np.array([x1 - x0, y1 - y0, 0.0]) + fwd_vec /= np.linalg.norm(fwd_vec) + right_vec = np.array([fwd_vec[1], -fwd_vec[0], 0]) + + # box_clip_alg.SetBoxClip(x0, x1, y0, y1, min_z, max_z) + # box_clip_alg.Update() + # clipped_ugrid = box_clip_alg.GetOutputDataObject(0) + + # polyline_bounds = _calc_polyline_bounds([x0, y0, x1, y1]) + # polyline_bounds.extend([min_z, max_z]) + # cell_ids = vtkIdList() + # cell_locator.FindCellsWithinBounds(polyline_bounds, cell_ids) + # print(f"{cell_ids.GetNumberOfIds()} {polyline_bounds=}") + + plane = vtkPlane() + plane.SetOrigin([x0, y0, 0]) + plane.SetNormal(right_vec) + + plane_0 = vtkPlane() + plane_0.SetOrigin([x0, y0, 0]) + plane_0.SetNormal(fwd_vec) + + plane_1 = vtkPlane() + plane_1.SetOrigin([x1, y1, 0]) + plane_1.SetNormal(-fwd_vec) + + cutter_alg.SetPlane(plane) + cutter_alg.Update() + + # Apparently we get a vtkPartitionedDataSet back here in VTK 9.1 + # Note that when testing with VTK 9.2-ish it seems we get polydata back instead + cut_surface_dataset_or_polydata = cutter_alg.GetOutput() + # print(f"{type(cut_surface_dataset_or_polydata)=}") + et_cut_s += timer.lap_s() + + if cut_surface_dataset_or_polydata.IsA("vtkPartitionedDataSet"): + cut_surface_dataset = cut_surface_dataset_or_polydata + for i in range(0, cut_surface_dataset.GetNumberOfPartitions()): + part_polydata = cut_surface_dataset.GetPartition(i) + # print(f"{i=} {type(part_polydata)=}") + # print(part_polydata) + + clipper_0 = vtkPolyDataPlaneClipper() + clipper_0.SetInputDataObject(part_polydata) + clipper_0.SetPlane(plane_0) + clipper_0.Update() + clipped_polydata = clipper_0.GetOutputDataObject(0) + + clipper_1 = vtkPolyDataPlaneClipper() + clipper_1.SetInputDataObject(clipped_polydata) + clipper_1.SetPlane(plane_1) + clipper_1.Update() + clipped_polydata = clipper_1.GetOutputDataObject(0) + + # print(f"{i=} {type(clipped_polydata)=}") + # print(clipped_polydata) + + cut_surface_polydata_arr.append(clipped_polydata) + append_alg.AddInputData(clipped_polydata) + + et_clip_s += timer.lap_s() + else: + cut_surface_polydata = cut_surface_dataset_or_polydata + + clipper_0 = vtkPolyDataPlaneClipper() + clipper_0.SetInputDataObject(cut_surface_polydata) + clipper_0.SetPlane(plane_0) + clipper_0.Update() + clipped_polydata = clipper_0.GetOutputDataObject(0) + + clipper_1 = vtkPolyDataPlaneClipper() + clipper_1.SetInputDataObject(clipped_polydata) + clipper_1.SetPlane(plane_1) + clipper_1.Update() + clipped_polydata = clipper_1.GetOutputDataObject(0) + + # print(f"{i=} {type(clipped_polydata)=}") + # print(clipped_polydata) + + cut_surface_polydata_arr.append(clipped_polydata) + append_alg.AddInputData(clipped_polydata) + + et_clip_s += timer.lap_s() + + append_alg.Update() + comb_polydata = append_alg.GetOutput() + et_combine_s = timer.lap_s() + + points_np = vtk_to_numpy(comb_polydata.GetPoints().GetData()).ravel() + polys_np = vtk_to_numpy(comb_polydata.GetPolys().GetData()) + + surface_polys = SurfacePolys(point_arr=points_np, poly_arr=polys_np) + + LOGGER.debug( + f"Cutting along polyline done in {timer.elapsed_s():.2f}s " + f"setup={et_setup_s:.2f}s, cut={et_cut_s:.2f}s, clip={et_clip_s:.2f}s, combine={et_combine_s:.2f}s, " + f"(provider_id={provider_id}, real={realization})" + ) + + return surface_polys, None + + """ + dbg_point_arr = [] + dbg_conn_arr = [] + for i in range(0, num_points_in_polyline): + x = polyline_xy[2 * i] + y = polyline_xy[2 * i + 1] + dbg_point_arr.extend([x, y, min_z]) + dbg_point_arr.extend([x, y, max_z]) + if i > 0: + base = 2 * (i - 1) + dbg_conn_arr.extend([4, base, base + 2, base + 3, base + 1]) + + for i in range(0, int(len(dbg_point_arr) / 3)): + print( + f"{i}: {dbg_point_arr[3*i]}, {dbg_point_arr[3*i + 1]}, {dbg_point_arr[3*i + 2]}" + ) + + point_arr_np = np.array(dbg_point_arr).reshape(-1, 3) + conn_arr_np = np.array(dbg_conn_arr) + surface_polys = SurfacePolys(point_arr=point_arr_np, poly_arr=conn_arr_np) + + return surface_polys, None + """ + + # ----------------------------------------------------------------------------- def ray_pick( self, provider_id: str, @@ -334,6 +536,7 @@ def ray_pick( cell_property_value=cell_property_val, ) + # ----------------------------------------------------------------------------- def _get_or_create_grid_worker( self, provider_id: str, realization: int ) -> Optional[GridWorker]: @@ -356,6 +559,7 @@ def _get_or_create_grid_worker( return worker +# ----------------------------------------------------------------------------- def _calc_cropped_grid( esgrid: vtkExplicitStructuredGrid, cell_filter: CellFilter ) -> vtkExplicitStructuredGrid: @@ -376,6 +580,7 @@ def _calc_cropped_grid( return cropped_grid +# ----------------------------------------------------------------------------- def _raypick_in_grid( esgrid: vtkExplicitStructuredGrid, ray: Ray ) -> Optional[Tuple[int, List[float]]]: @@ -416,6 +621,7 @@ def _raypick_in_grid( return cell_id_ref.get(), isect_pt +# ----------------------------------------------------------------------------- def _calc_grid_surface(esgrid: vtkExplicitStructuredGrid) -> vtkPolyData: surf_filter = vtkExplicitStructuredGridSurfaceFilter() surf_filter.SetInputData(esgrid) @@ -426,6 +632,7 @@ def _calc_grid_surface(esgrid: vtkExplicitStructuredGrid) -> vtkPolyData: return polydata +# ----------------------------------------------------------------------------- def _load_property_values( provider: EnsembleGridProvider, realization: int, property_spec: PropertySpec ) -> Optional[np.ndarray]: @@ -441,6 +648,137 @@ def _load_property_values( return prop_values +# ----------------------------------------------------------------------------- +def _vtk_esg_to_ug(vtk_esgrid: vtkExplicitStructuredGrid) -> vtkUnstructuredGrid: + convertFilter = vtkExplicitStructuredGridToUnstructuredGrid() + convertFilter.SetInputData(vtk_esgrid) + convertFilter.Update() + vtk_ugrid = convertFilter.GetOutput() + + return vtk_ugrid + + +# ----------------------------------------------------------------------------- +def _vtk_ug_to_esg(vtk_ugrid: vtkUnstructuredGrid) -> vtkExplicitStructuredGrid: + convertFilter = vtkUnstructuredGridToExplicitStructuredGrid() + convertFilter.SetInputData(vtk_ugrid) + convertFilter.SetInputArrayToProcess(0, 0, 0, 1, "BLOCK_I") + convertFilter.SetInputArrayToProcess(1, 0, 0, 1, "BLOCK_J") + convertFilter.SetInputArrayToProcess(2, 0, 0, 1, "BLOCK_K") + convertFilter.Update() + vtk_esgrid = convertFilter.GetOutput() + + return vtk_esgrid + + +# ----------------------------------------------------------------------------- +def _calc_polyline_bounds(polyline_xy: List[float]) -> List[float]: + num_points = int(len(polyline_xy) / 2) + if num_points < 1: + return None + + min_x = min(polyline_xy[0::2]) + max_x = max(polyline_xy[0::2]) + min_y = min(polyline_xy[1::2]) + max_y = max(polyline_xy[1::2]) + + return [min_x, max_x, min_y, max_y] + + +# ----------------------------------------------------------------------------- +def _extract_intersected_ugrid( + ugrid: vtkUnstructuredGrid, polyline_xy_in: List[float], max_point_dist: float +) -> vtkUnstructuredGrid: + + # Requires VTK 9.2 + from vtkmodules.vtkFiltersCore import vtkExtractCellsAlongPolyLine + + timer = PerfTimer() + + polyline_xy = _resample_polyline(polyline_xy_in, max_point_dist) + et_resample_s = timer.lap_s() + + num_points_in_polyline = int(len(polyline_xy) / 2) + if num_points_in_polyline < 1: + return ugrid + + bounds = ugrid.GetBounds() + min_z = bounds[4] + max_z = bounds[5] + + points = vtkPoints() + lines = vtkCellArray() + + for i in range(0, num_points_in_polyline): + x = polyline_xy[2 * i] + y = polyline_xy[2 * i + 1] + + points.InsertNextPoint([x, y, min_z]) + points.InsertNextPoint([x, y, max_z]) + + line = vtkLine() + line.GetPointIds().SetId(0, 2 * i) + line.GetPointIds().SetId(1, 2 * i + 1) + + lines.InsertNextCell(line) + + polyData = vtkPolyData() + polyData.SetPoints(points) + polyData.SetLines(lines) + + et_build_s = timer.lap_s() + + extractor = vtkExtractCellsAlongPolyLine() + extractor.SetInputData(0, ugrid) + extractor.SetInputData(1, polyData) + extractor.Update() + + ret_grid = extractor.GetOutput(0) + + et_extract_s = timer.lap_s() + + LOGGER.debug( + f"extraction with {num_points_in_polyline} points took {timer.elapsed_s():.2f}s " + f"(resample={et_resample_s:.2f}s, build={et_build_s:.2f}s, extract={et_extract_s:.2f}s)" + ) + + return ret_grid + + +# ----------------------------------------------------------------------------- +def _resample_polyline(polyline_xy: List[float], max_point_dist: float) -> List[float]: + num_points = int(len(polyline_xy) / 2) + if num_points < 2: + return polyline_xy + + ret_polyline = [] + + prev_x = polyline_xy[0] + prev_y = polyline_xy[1] + ret_polyline.extend([prev_x, prev_y]) + + for i in range(1, num_points): + x = polyline_xy[2 * i] + y = polyline_xy[2 * i + 1] + + fwd = [x - prev_x, y - prev_y] + length = np.linalg.norm(fwd) + if length > max_point_dist: + n = int(length / max_point_dist) + delta_t = 1.0 / (n + 1) + for j in range(0, n): + pt_x = prev_x + fwd[0] * (j + 1) * delta_t + pt_y = prev_y + fwd[1] * (j + 1) * delta_t + ret_polyline.extend([pt_x, pt_y]) + + ret_polyline.extend([x, y]) + prev_x = x + prev_y = y + + return ret_polyline + + +# ----------------------------------------------------------------------------- def _property_spec_dbg_str(property_spec: Optional[PropertySpec]) -> str: if not property_spec: return "prop=None" @@ -448,6 +786,7 @@ def _property_spec_dbg_str(property_spec: Optional[PropertySpec]) -> str: return f"prop=({property_spec.prop_name}, {property_spec.prop_date})" +# ----------------------------------------------------------------------------- def _cell_filter_dbg_str(cell_filter: Optional[CellFilter]) -> str: if not cell_filter: return "IJK=None" From 4b51b1446ef638f9416d91a1e625f5b2d768f7c8 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Tue, 31 May 2022 16:27:55 +0200 Subject: [PATCH 54/63] Added wells --- .../well_provider/_provider_impl_file.py | 41 ++++++++- .../well_provider/_simplify_polyline.py | 27 ++++++ .../_providers/well_provider/well_provider.py | 13 +++ .../_providers/well_provider/well_server.py | 50 +++++++++- .../_eclipse_grid_viewer/_callbacks.py | 92 +++++++++++++++++-- .../plugins/_eclipse_grid_viewer/_layout.py | 60 +++++++++++- .../plugins/_eclipse_grid_viewer/_plugin.py | 37 +++++++- 7 files changed, 301 insertions(+), 19 deletions(-) create mode 100644 webviz_subsurface/_providers/well_provider/_simplify_polyline.py diff --git a/webviz_subsurface/_providers/well_provider/_provider_impl_file.py b/webviz_subsurface/_providers/well_provider/_provider_impl_file.py index f1f006ebe..7d5b62afa 100644 --- a/webviz_subsurface/_providers/well_provider/_provider_impl_file.py +++ b/webviz_subsurface/_providers/well_provider/_provider_impl_file.py @@ -3,11 +3,14 @@ from pathlib import Path from typing import Dict, List, Optional +import numpy as np +import pandas as pd import xtgeo from webviz_subsurface._utils.perf_timer import PerfTimer -from .well_provider import WellPath, WellProvider +from .well_provider import WellPath, WellProvider, WellIntersectionPolyLine +from ._simplify_polyline import rdp LOGGER = logging.getLogger(__name__) @@ -131,3 +134,39 @@ def get_well_xtgeo_obj(self, well_name: str) -> xtgeo.Well: ) return well + + def get_polyline_along_well_path_SIMPLIFIED( + self, well_name: str, tvdmin=None + ) -> np.array: + """Returns a polyline for the well path along with MD for the well.""" + well = self.get_well_xtgeo_obj(well_name).copy() + if tvdmin is not None: + well.dataframe = well.dataframe[well.dataframe["Z_TVDSS"] >= tvdmin] + + xy_arr = np.stack( + ( + np.array(well.dataframe["X_UTME"].values), + np.array(well.dataframe["Y_UTMN"].values), + ), + axis=-1, + ) + + timer = PerfTimer() + if np.all(xy_arr == xy_arr[0]): + xy_start = xy_arr[0] - 500 + xy_end = xy_arr[0] + 500 + + print( + f"Well is vertical. Returning two points extended in xy from trajectory" + ) + return [xy_start, xy_end] + + simplified_xy_arr = rdp(xy_arr, epsilon=1) + + print( + f"Original well polyline has {len(xy_arr)} points. " + f"Simplified well polyline has {len(simplified_xy_arr)} points. ", + f"Time to calculate: {timer.elapsed_ms()}ms", + ) + + return simplified_xy_arr diff --git a/webviz_subsurface/_providers/well_provider/_simplify_polyline.py b/webviz_subsurface/_providers/well_provider/_simplify_polyline.py new file mode 100644 index 000000000..de4d9b6bf --- /dev/null +++ b/webviz_subsurface/_providers/well_provider/_simplify_polyline.py @@ -0,0 +1,27 @@ +import numpy as np + + +def rdp(points, epsilon=5): + # https://towardsdatascience.com/simplify-polylines-with-the-douglas-peucker-algorithm-ac8ed487a4a1 + # get the start and end points + + start = np.tile(np.expand_dims(points[0], axis=0), (points.shape[0], 1)) + end = np.tile(np.expand_dims(points[-1], axis=0), (points.shape[0], 1)) + # find distance from other_points to line formed by start and end + dist_point_to_line = np.abs( + np.cross(end - start, points - start, axis=-1) + ) / np.linalg.norm(end - start, axis=-1) + # get the index of the points with the largest distance + max_idx = np.argmax(dist_point_to_line) + max_value = dist_point_to_line[max_idx] + + result = [] + if max_value > epsilon: + partial_results_left = rdp(points[: max_idx + 1], epsilon) + result += [list(i) for i in partial_results_left if list(i) not in result] + partial_results_right = rdp(points[max_idx:], epsilon) + result += [list(i) for i in partial_results_right if list(i) not in result] + else: + result += [points[0], points[-1]] + + return result diff --git a/webviz_subsurface/_providers/well_provider/well_provider.py b/webviz_subsurface/_providers/well_provider/well_provider.py index 9ddb60bda..67c3816ee 100644 --- a/webviz_subsurface/_providers/well_provider/well_provider.py +++ b/webviz_subsurface/_providers/well_provider/well_provider.py @@ -14,6 +14,12 @@ class WellPath: md_arr: np.ndarray +@dataclass(frozen=True) +class WellIntersectionPolyLine: + x_arr: np.ndarray + y_arr: np.ndarray + + # Class provides data for wells class WellProvider(abc.ABC): @abc.abstractmethod @@ -31,3 +37,10 @@ def get_well_path(self, well_name: str) -> WellPath: @abc.abstractmethod def get_well_xtgeo_obj(self, well_name: str) -> xtgeo.Well: ... + + @abc.abstractmethod + def get_polyline_along_well_path_SIMPLIFIED( + self, well_name: str, tvdmin=None + ) -> WellIntersectionPolyLine: + """Returns a polyline for the well path.""" + ... diff --git a/webviz_subsurface/_providers/well_provider/well_server.py b/webviz_subsurface/_providers/well_provider/well_server.py index 7063855cb..24eb06b27 100644 --- a/webviz_subsurface/_providers/well_provider/well_server.py +++ b/webviz_subsurface/_providers/well_provider/well_server.py @@ -1,10 +1,19 @@ import logging from typing import Dict, List, Optional from urllib.parse import quote +from dataclasses import dataclass import flask import geojson +import numpy as np from dash import Dash +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.vtkCommonDataModel import ( + vtkCellArray, + vtkPolyData, + vtkPolyLine, +) +from vtkmodules.util.numpy_support import vtk_to_numpy from webviz_subsurface._providers.well_provider.well_provider import WellProvider from webviz_subsurface._utils.perf_timer import PerfTimer @@ -16,6 +25,12 @@ _WELL_SERVER_INSTANCE: Optional["WellServer"] = None +@dataclass +class PolyLine: + point_arr: np.ndarray + line_arr: np.ndarray + + class WellServer: def __init__(self, app: Dash) -> None: self._setup_url_rule(app) @@ -31,7 +46,7 @@ def instance(app: Dash) -> "WellServer": return _WELL_SERVER_INSTANCE - def add_provider(self, provider: WellProvider) -> None: + def register_provider(self, provider: WellProvider) -> None: provider_id = provider.provider_id() LOGGER.debug(f"Adding provider with id={provider_id}") @@ -113,3 +128,36 @@ def _handle_wells_request( LOGGER.debug(f"Request handled in: {timer.elapsed_s():.2f}s") return response + + def get_polyline( + self, provider_id: str, well_name: str, tvdmin: float = None + ) -> PolyLine: + provider = self._id_to_provider_dict[provider_id] + well_path = provider.get_well_path(well_name) + xyz_arr = [ + [x, y, z] + for x, y, z in zip(well_path.x_arr, well_path.y_arr, well_path.z_arr) + ] + points = vtkPoints() + for p in xyz_arr: + points.InsertNextPoint(p[0], p[1], -p[2]) + + polyLine = vtkPolyLine() + polyLine.GetPointIds().SetNumberOfIds(len(xyz_arr)) + for i in range(0, len(xyz_arr)): + polyLine.GetPointIds().SetId(i, i) + + # Create a cell array to store the lines in and add the lines to it + cells = vtkCellArray() + cells.InsertNextCell(polyLine) + + # Create a polydata to store everything in + polyData = vtkPolyData() + # Add the points to the dataset + polyData.SetPoints(points) + + # Add the lines to the dataset + polyData.SetLines(cells) + points = vtk_to_numpy(polyData.GetPoints().GetData()) + lines = vtk_to_numpy(polyData.GetLines().GetData()) + return PolyLine(point_arr=points, line_arr=lines) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 023275b05..078fd0733 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -15,6 +15,7 @@ CellFilter, Ray, ) +from webviz_subsurface._providers.well_provider import WellProvider, WellServer from ._layout import PROPERTYTYPE, LayoutElements, GRID_DIRECTION @@ -23,6 +24,8 @@ def plugin_callbacks( get_uuid: Callable, grid_provider: EnsembleGridProvider, grid_viz_service: GridVizService, + well_provider: WellProvider, + well_server: WellServer, ) -> None: @callback( Output(get_uuid(LayoutElements.PROPERTIES), "options"), @@ -122,6 +125,7 @@ def _set_geometry_and_scalar( k_max=grid_range[2][1], ), ) + return ( b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), @@ -149,32 +153,100 @@ def _set_geometry_and_scalar( [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], ) + @callback( + Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_POLYDATA), "points"), + Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_POLYDATA), "polys"), + Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_CELL_DATA), "values"), + Input(get_uuid(LayoutElements.WELL_SELECT), "value"), + Input(get_uuid(LayoutElements.REALIZATIONS), "value"), + Input(get_uuid(LayoutElements.PROPERTIES), "value"), + Input(get_uuid(LayoutElements.DATES), "value"), + State(get_uuid(LayoutElements.INIT_RESTART), "value"), + ) + def set_well_geometries( + well_names: List[str], + realizations: List[int], + prop: List[str], + date: List[int], + proptype: str, + ) -> Tuple[ + List[Dict[str, str]], List[str], List[Dict[str, str]], Optional[List[str]] + ]: + + if not well_names: + return no_update, no_update, no_update + polyline_xy = well_provider.get_polyline_along_well_path_SIMPLIFIED( + well_names[0] + ) + polyline_xy = np.array(polyline_xy).flatten() + if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: + property_spec = PropertySpec(prop_name=prop[0], prop_date=0) + else: + property_spec = PropertySpec(prop_name=prop[0], prop_date=date[0]) + + surface_polys, scalars = grid_viz_service.cut_along_polyline( + provider_id=grid_provider.provider_id(), + realization=realizations[0], + polyline_xy=polyline_xy, + property_spec=property_spec, + ) + + return ( + b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), + b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), + b64_encode_numpy(scalars.value_arr.astype(np.float32)) + if scalars is not None + else no_update, + ) + + @callback( + Output(get_uuid(LayoutElements.VTK_WELL_PATH_POLYDATA), "points"), + Output(get_uuid(LayoutElements.VTK_WELL_PATH_POLYDATA), "lines"), + Input(get_uuid(LayoutElements.WELL_SELECT), "value"), + ) + def set_well_geometries( + well_names: List[str], + ) -> Tuple[ + List[Dict[str, str]], List[str], List[Dict[str, str]], Optional[List[str]] + ]: + + if not well_names: + return no_update, no_update + polyline = well_server.get_polyline( + provider_id=well_provider.provider_id(), well_name=well_names[0] + ) + + return ( + b64_encode_numpy(polyline.point_arr.astype(np.float32)), + b64_encode_numpy(polyline.line_arr.astype(np.float32)), + ) + @callback( Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), + Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_REPRESENTATION), "actor"), + Output(get_uuid(LayoutElements.VTK_WELL_PATH_REPRESENTATION), "actor"), Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "showCubeAxes"), Input(get_uuid(LayoutElements.Z_SCALE), "value"), Input(get_uuid(LayoutElements.SHOW_AXES), "value"), - State(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), ) def _set_representation_actor( - z_scale: int, axes_is_on: List[str], actor: Optional[dict] + z_scale: int, axes_is_on: List[str] ) -> Tuple[dict, bool]: show_axes = bool(z_scale == 1 and axes_is_on) - actor = actor if actor else {} - actor.update({"scale": (1, 1, z_scale)}) - return actor, show_axes + actor = {"scale": (1, 1, z_scale)} + return actor, actor, actor, show_axes @callback( Output(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "property"), + Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_REPRESENTATION), "property"), Input(get_uuid(LayoutElements.SHOW_GRID_LINES), "value"), - State(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "property"), ) def _set_representation_property( - show_grid_lines: List[str], properties: Optional[dict] + show_grid_lines: List[str], ) -> dict: - properties = properties if properties else {} - properties.update({"edgeVisibility": bool(show_grid_lines)}) - return properties + properties = {"edgeVisibility": bool(show_grid_lines)} + + return properties, properties @callback( Output(get_uuid(LayoutElements.VTK_VIEW), "triggerResetCamera"), diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 8024d2321..e39afa595 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -9,7 +9,7 @@ EnsembleGridProvider, CellFilter, ) - +from webviz_subsurface._providers.well_provider import WellProvider # pylint: disable = too-few-public-methods class LayoutElements(str, Enum): @@ -17,11 +17,17 @@ class LayoutElements(str, Enum): INIT_RESTART = "init-restart-select" PROPERTIES = "properties-select" DATES = "dates-select" + WELL_SELECT = "well-select" Z_SCALE = "z-scale" VTK_VIEW = "vtk-view" VTK_GRID_REPRESENTATION = "vtk-grid-representation" VTK_GRID_POLYDATA = "vtk-grid-polydata" VTK_GRID_CELLDATA = "vtk-grid-celldata" + VTK_WELL_INTERSECT_REPRESENTATION = "vtk-well-intersect-representation" + VTK_WELL_INTERSECT_POLYDATA = "vtk-well-intersect-polydata" + VTK_WELL_INTERSECT_CELL_DATA = "vtk-well-intersect-celldata" + VTK_WELL_PATH_REPRESENTATION = "vtk-well-path-representation" + VTK_WELL_PATH_POLYDATA = "vtk-well-path-polydata" STORED_CELL_INDICES_HASH = "stored-cell-indices-hash" SELECTED_CELL = "selected-cell" SHOW_GRID_LINES = "show-grid-lines" @@ -39,6 +45,7 @@ class LayoutTitles(str, Enum): INIT_RESTART = "Init / Restart" PROPERTIES = "Property" DATES = "Date" + WELL_SELECT = "Well" Z_SCALE = "Z-scale" SHOW_GRID_LINES = "Show grid lines" COLORMAP = "Color map" @@ -70,7 +77,7 @@ class LayoutStyle: def plugin_main_layout( - get_uuid: Callable, grid_provider: EnsembleGridProvider + get_uuid: Callable, grid_provider: EnsembleGridProvider, well_names: List[str] ) -> wcc.FlexBox: initial_grid = grid_provider.get_3dgrid(grid_provider.realizations()[0]) grid_dimensions = CellFilter( @@ -87,6 +94,7 @@ def plugin_main_layout( get_uuid=get_uuid, grid_dimensions=grid_dimensions, grid_provider=grid_provider, + well_names=well_names, ), vtk_view(get_uuid=get_uuid), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), @@ -103,7 +111,10 @@ def plugin_main_layout( def sidebar( - get_uuid: Callable, grid_dimensions: CellFilter, grid_provider: EnsembleGridProvider + get_uuid: Callable, + grid_dimensions: CellFilter, + grid_provider: EnsembleGridProvider, + well_names: List[str], ) -> wcc.Frame: realizations = grid_provider.realizations() @@ -140,11 +151,19 @@ def sidebar( value=property_value, ), wcc.SelectWithLabel( - id=get_uuid(LayoutElements.PROPERTIES), label=LayoutTitles.PROPERTIES + id=get_uuid(LayoutElements.PROPERTIES), + label=LayoutTitles.PROPERTIES, ), wcc.SelectWithLabel( id=get_uuid(LayoutElements.DATES), label=LayoutTitles.DATES ), + wcc.SelectWithLabel( + id=get_uuid(LayoutElements.WELL_SELECT), + label=LayoutTitles.WELL_SELECT, + multi=False, + options=[{"value": well, "label": well} for well in well_names], + value=[], + ), wcc.Slider( label=LayoutTitles.Z_SCALE, id=get_uuid(LayoutElements.Z_SCALE), @@ -216,7 +235,7 @@ def sidebar( ) ], ), - html.Pre(id=get_uuid(LayoutElements.SELECTED_CELL)), + html.Pre(id=get_uuid(LayoutElements.SELECTED_CELL), children=[]), ], ) @@ -397,5 +416,36 @@ def vtk_view(get_uuid: Callable) -> webviz_vtk.View: ) ], ), + webviz_vtk.GeometryRepresentation( + id=get_uuid(LayoutElements.VTK_WELL_INTERSECT_REPRESENTATION), + actor={"visibility": True}, + children=[ + webviz_vtk.PolyData( + id=get_uuid(LayoutElements.VTK_WELL_INTERSECT_POLYDATA), + children=[ + webviz_vtk.CellData( + [ + webviz_vtk.DataArray( + id=get_uuid( + LayoutElements.VTK_WELL_INTERSECT_CELL_DATA + ), + registration="setScalars", + name="scalar", + ) + ] + ) + ], + ) + ], + ), + webviz_vtk.GeometryRepresentation( + id=get_uuid(LayoutElements.VTK_WELL_PATH_REPRESENTATION), + actor={"visibility": True}, + children=[ + webviz_vtk.PolyData( + id=get_uuid(LayoutElements.VTK_WELL_PATH_POLYDATA), + ) + ], + ), ], ) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index 86f1d9bb9..42a8e9237 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -8,6 +8,11 @@ GridVizService, CellFilter, ) +from webviz_subsurface._providers.well_provider import ( + WellProvider, + WellProviderFactory, + WellServer, +) from ._callbacks import plugin_callbacks from ._layout import plugin_main_layout @@ -17,7 +22,13 @@ class EclipseGridViewer(WebvizPluginABC): """Eclipse grid viewer""" def __init__( - self, webviz_settings: WebvizSettings, ensembles: List[str], grid_name: str + self, + webviz_settings: WebvizSettings, + app, + ensembles: List[str], + grid_name: str, + well_folder: Path = None, + well_suffix: str = ".rmswell", ) -> None: super().__init__() grid_provider_factory = EnsembleGridProviderFactory.instance() @@ -27,13 +38,35 @@ def __init__( ) self.grid_viz_service = GridVizService.instance() self.grid_viz_service.register_provider(self.grid_provider) + factory = WellProviderFactory.instance() + + if well_folder is not None: + self.well_provider = factory.create_from_well_files( + well_folder=str(well_folder), + well_suffix=".rmswell", + md_logname="MDepth", + ) + + self.well_server = WellServer.instance(app) + self.well_server.register_provider(self.well_provider) + else: + self.well_provider = None + self.well_server = None plugin_callbacks( get_uuid=self.uuid, grid_provider=self.grid_provider, grid_viz_service=self.grid_viz_service, + well_provider=self.well_provider, + well_server=self.well_server, ) @property def layout(self) -> wcc.FlexBox: - return plugin_main_layout(get_uuid=self.uuid, grid_provider=self.grid_provider) + return plugin_main_layout( + get_uuid=self.uuid, + grid_provider=self.grid_provider, + well_names=self.well_provider.well_names() + if self.well_provider is not None + else [], + ) From 5a4825c31c083d6913b4cdfccf6cba8d813dab7e Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Tue, 31 May 2022 19:51:01 +0200 Subject: [PATCH 55/63] Add separate view for intersection --- .../well_provider/_provider_impl_file.py | 2 +- .../_eclipse_grid_viewer/_callbacks.py | 19 +++- .../plugins/_eclipse_grid_viewer/_layout.py | 91 ++++++++++++++++++- 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/webviz_subsurface/_providers/well_provider/_provider_impl_file.py b/webviz_subsurface/_providers/well_provider/_provider_impl_file.py index 7d5b62afa..8c80817de 100644 --- a/webviz_subsurface/_providers/well_provider/_provider_impl_file.py +++ b/webviz_subsurface/_providers/well_provider/_provider_impl_file.py @@ -161,7 +161,7 @@ def get_polyline_along_well_path_SIMPLIFIED( ) return [xy_start, xy_end] - simplified_xy_arr = rdp(xy_arr, epsilon=1) + simplified_xy_arr = rdp(xy_arr) print( f"Original well polyline has {len(xy_arr)} points. " diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index 078fd0733..ee8049b43 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -157,6 +157,9 @@ def _set_geometry_and_scalar( Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_POLYDATA), "points"), Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_POLYDATA), "polys"), Output(get_uuid(LayoutElements.VTK_WELL_INTERSECT_CELL_DATA), "values"), + Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA), "points"), + Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA), "polys"), + Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_CELL_DATA), "values"), Input(get_uuid(LayoutElements.WELL_SELECT), "value"), Input(get_uuid(LayoutElements.REALIZATIONS), "value"), Input(get_uuid(LayoutElements.PROPERTIES), "value"), @@ -174,7 +177,7 @@ def set_well_geometries( ]: if not well_names: - return no_update, no_update, no_update + return no_update, no_update, no_update, no_update, no_update, no_update polyline_xy = well_provider.get_polyline_along_well_path_SIMPLIFIED( well_names[0] ) @@ -197,11 +200,19 @@ def set_well_geometries( b64_encode_numpy(scalars.value_arr.astype(np.float32)) if scalars is not None else no_update, + b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), + b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), + b64_encode_numpy(scalars.value_arr.astype(np.float32)) + if scalars is not None + else no_update, ) @callback( Output(get_uuid(LayoutElements.VTK_WELL_PATH_POLYDATA), "points"), Output(get_uuid(LayoutElements.VTK_WELL_PATH_POLYDATA), "lines"), + Output(get_uuid(LayoutElements.VTK_WELL_PATH_2D_POLYDATA), "points"), + Output(get_uuid(LayoutElements.VTK_WELL_PATH_2D_POLYDATA), "lines"), + Output(get_uuid(LayoutElements.VTK_INTERSECT_VIEW), "triggerResetCamera"), Input(get_uuid(LayoutElements.WELL_SELECT), "value"), ) def set_well_geometries( @@ -211,7 +222,7 @@ def set_well_geometries( ]: if not well_names: - return no_update, no_update + return no_update, no_update, no_update, no_update, no_update polyline = well_server.get_polyline( provider_id=well_provider.provider_id(), well_name=well_names[0] ) @@ -219,6 +230,9 @@ def set_well_geometries( return ( b64_encode_numpy(polyline.point_arr.astype(np.float32)), b64_encode_numpy(polyline.line_arr.astype(np.float32)), + b64_encode_numpy(polyline.point_arr.astype(np.float32)), + b64_encode_numpy(polyline.line_arr.astype(np.float32)), + time(), ) @callback( @@ -254,6 +268,7 @@ def _set_representation_property( Input(get_uuid(LayoutElements.VTK_GRID_REPRESENTATION), "actor"), ) def _reset_camera(realizations: List[int], _actor: dict) -> float: + return time() @callback( diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index e39afa595..79a6ca958 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -20,6 +20,7 @@ class LayoutElements(str, Enum): WELL_SELECT = "well-select" Z_SCALE = "z-scale" VTK_VIEW = "vtk-view" + VTK_INTERSECT_VIEW = "vtk-intersect-view" VTK_GRID_REPRESENTATION = "vtk-grid-representation" VTK_GRID_POLYDATA = "vtk-grid-polydata" VTK_GRID_CELLDATA = "vtk-grid-celldata" @@ -28,6 +29,11 @@ class LayoutElements(str, Enum): VTK_WELL_INTERSECT_CELL_DATA = "vtk-well-intersect-celldata" VTK_WELL_PATH_REPRESENTATION = "vtk-well-path-representation" VTK_WELL_PATH_POLYDATA = "vtk-well-path-polydata" + VTK_WELL_2D_INTERSECT_REPRESENTATION = "vtk-well-2d-intersect-representation" + VTK_WELL_2D_INTERSECT_POLYDATA = "vtk-well-2d-intersect-polydata" + VTK_WELL_2D_INTERSECT_CELL_DATA = "vtk-well-2d-intersect-celldata" + VTK_WELL_PATH_2D_REPRESENTATION = "vtk-well-2d-path-representation" + VTK_WELL_PATH_2D_POLYDATA = "vtk-well-2d-path-polydata" STORED_CELL_INDICES_HASH = "stored-cell-indices-hash" SELECTED_CELL = "selected-cell" SHOW_GRID_LINES = "show-grid-lines" @@ -73,7 +79,7 @@ class PROPERTYTYPE(str, Enum): class LayoutStyle: MAIN_HEIGHT = "87vh" SIDEBAR = {"flex": 1, "height": "87vh"} - VTK_VIEW = {"flex": 5, "height": "87vh"} + VTK_VIEW = {"height": "40vh", "marginBottom": "10px"} def plugin_main_layout( @@ -96,7 +102,13 @@ def plugin_main_layout( grid_provider=grid_provider, well_names=well_names, ), - vtk_view(get_uuid=get_uuid), + html.Div( + style={"flex": "5"}, + children=[ + vtk_3d_view(get_uuid=get_uuid), + vtk_intersect_view(get_uuid=get_uuid), + ], + ), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), dcc.Store( id=get_uuid(LayoutElements.GRID_RANGE_STORE), @@ -347,7 +359,7 @@ def crop_widget( ) -def vtk_view(get_uuid: Callable) -> webviz_vtk.View: +def vtk_3d_view(get_uuid: Callable) -> webviz_vtk.View: return webviz_vtk.View( id=get_uuid(LayoutElements.VTK_VIEW), style=LayoutStyle.VTK_VIEW, @@ -449,3 +461,76 @@ def vtk_view(get_uuid: Callable) -> webviz_vtk.View: ), ], ) + + +def vtk_intersect_view(get_uuid: Callable) -> webviz_vtk.View: + return webviz_vtk.View( + id=get_uuid(LayoutElements.VTK_INTERSECT_VIEW), + style=LayoutStyle.VTK_VIEW, + pickingModes=["click"], + interactorSettings=[ + { + "button": 1, + "action": "Zoom", + "scrollEnabled": True, + }, + { + "button": 3, + "action": "Pan", + }, + { + "button": 2, + "action": "Rotate", + }, + { + "button": 1, + "action": "Pan", + "shift": True, + }, + { + "button": 1, + "action": "Zoom", + "alt": True, + }, + { + "button": 1, + "action": "Roll", + "alt": True, + "shift": True, + }, + ], + children=[ + webviz_vtk.GeometryRepresentation( + id=get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_REPRESENTATION), + actor={"visibility": True}, + property={"edgeVisibility": True}, + children=[ + webviz_vtk.PolyData( + id=get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA), + children=[ + webviz_vtk.CellData( + [ + webviz_vtk.DataArray( + id=get_uuid( + LayoutElements.VTK_WELL_2D_INTERSECT_CELL_DATA + ), + registration="setScalars", + name="scalar", + ) + ] + ) + ], + ) + ], + ), + webviz_vtk.GeometryRepresentation( + id=get_uuid(LayoutElements.VTK_WELL_PATH_2D_REPRESENTATION), + actor={"visibility": True}, + children=[ + webviz_vtk.PolyData( + id=get_uuid(LayoutElements.VTK_WELL_PATH_2D_POLYDATA), + ) + ], + ), + ], + ) From 8004838ee03e30ce77c6a31c69be59d4ea2727db Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Wed, 1 Jun 2022 09:18:27 +0200 Subject: [PATCH 56/63] new grid viewer --- setup.py | 2 +- webviz_subsurface/plugins/_grid_viewer/__init__.py | 0 webviz_subsurface/plugins/_grid_viewer/_callbacks.py | 0 webviz_subsurface/plugins/_grid_viewer/_layout_elements.py | 0 webviz_subsurface/plugins/_grid_viewer/_plugin.py | 0 5 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 webviz_subsurface/plugins/_grid_viewer/__init__.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/_callbacks.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/_layout_elements.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/_plugin.py diff --git a/setup.py b/setup.py index 2d0ccb5ba..9fd7732c1 100644 --- a/setup.py +++ b/setup.py @@ -105,7 +105,7 @@ "scipy>=1.2", "statsmodels>=0.12.1", # indirect dependency through https://plotly.com/python/linear-fits/ "webviz-config>=0.3.8", - "webviz-core-components>=0.5.6", + "webviz-core-components", "webviz-subsurface-components>=0.4.12", "webviz_vtk@git+https://github.com/hanskallekleiv/webviz-vtk", "xtgeo@git+https://github.com/sigurdp/xtgeo/@sigurdp/vtk-esg", diff --git a/webviz_subsurface/plugins/_grid_viewer/__init__.py b/webviz_subsurface/plugins/_grid_viewer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_grid_viewer/_callbacks.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_grid_viewer/_layout_elements.py b/webviz_subsurface/plugins/_grid_viewer/_layout_elements.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_grid_viewer/_plugin.py new file mode 100644 index 000000000..e69de29bb From 59ba281c00b2c3be57cefe09146ac1f341d32682 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Thu, 2 Jun 2022 10:23:01 +0200 Subject: [PATCH 57/63] New plugin using WLF --- setup.py | 3 +- webviz_subsurface/plugins/__init__.py | 1 + .../plugins/_eclipse_grid_viewer/_plugin.py | 1 - .../plugins/_grid_viewer/__init__.py | 1 + .../plugins/_grid_viewer/_layout_elements.py | 30 ++ .../plugins/_grid_viewer/_plugin.py | 57 ++++ .../plugins/_grid_viewer/_types.py | 12 + .../{_callbacks.py => views/__init__.py} | 0 .../_grid_viewer/views/view_3d/__init__.py | 0 .../_grid_viewer/views/view_3d/_view_3d.py | 169 ++++++++++++ .../views/view_3d/settings/__init__.py | 3 + .../views/view_3d/settings/_data_selection.py | 153 +++++++++++ .../views/view_3d/settings/_grid_filter.py | 260 ++++++++++++++++++ .../views/view_3d/settings/_settings.py | 49 ++++ .../view_elements/_vtk_view_3d_element.py | 132 +++++++++ 15 files changed, 869 insertions(+), 2 deletions(-) create mode 100644 webviz_subsurface/plugins/_grid_viewer/_types.py rename webviz_subsurface/plugins/_grid_viewer/{_callbacks.py => views/__init__.py} (100%) create mode 100644 webviz_subsurface/plugins/_grid_viewer/views/view_3d/__init__.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/__init__.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py create mode 100644 webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py diff --git a/setup.py b/setup.py index 9fd7732c1..c42004808 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ "BhpQc = webviz_subsurface.plugins:BhpQc", "DiskUsage = webviz_subsurface.plugins:DiskUsage", "EclipseGridViewer = webviz_subsurface.plugins:EclipseGridViewer", + "GridViewer = webviz_subsurface.plugins:GridViewer", "GroupTree = webviz_subsurface.plugins:GroupTree", "HistoryMatch = webviz_subsurface.plugins:HistoryMatch", "HorizonUncertaintyViewer = webviz_subsurface.plugins:HorizonUncertaintyViewer", @@ -104,7 +105,7 @@ "pyvista>=0.33.3", "scipy>=1.2", "statsmodels>=0.12.1", # indirect dependency through https://plotly.com/python/linear-fits/ - "webviz-config>=0.3.8", + "webviz-config", "webviz-core-components", "webviz-subsurface-components>=0.4.12", "webviz_vtk@git+https://github.com/hanskallekleiv/webviz-vtk", diff --git a/webviz_subsurface/plugins/__init__.py b/webviz_subsurface/plugins/__init__.py index 3fe9b6984..c90cf1dcb 100644 --- a/webviz_subsurface/plugins/__init__.py +++ b/webviz_subsurface/plugins/__init__.py @@ -24,6 +24,7 @@ from ._bhp_qc import BhpQc from ._disk_usage import DiskUsage from ._eclipse_grid_viewer import EclipseGridViewer +from ._grid_viewer import GridViewer from ._group_tree import GroupTree from ._history_match import HistoryMatch from ._horizon_uncertainty_viewer import HorizonUncertaintyViewer diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py index 42a8e9237..0bc1c24d0 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_plugin.py @@ -6,7 +6,6 @@ from webviz_subsurface._providers.ensemble_grid_provider import ( EnsembleGridProviderFactory, GridVizService, - CellFilter, ) from webviz_subsurface._providers.well_provider import ( WellProvider, diff --git a/webviz_subsurface/plugins/_grid_viewer/__init__.py b/webviz_subsurface/plugins/_grid_viewer/__init__.py index e69de29bb..181db1d9d 100644 --- a/webviz_subsurface/plugins/_grid_viewer/__init__.py +++ b/webviz_subsurface/plugins/_grid_viewer/__init__.py @@ -0,0 +1 @@ +from ._plugin import GridViewer diff --git a/webviz_subsurface/plugins/_grid_viewer/_layout_elements.py b/webviz_subsurface/plugins/_grid_viewer/_layout_elements.py index e69de29bb..ae8e20f8d 100644 --- a/webviz_subsurface/plugins/_grid_viewer/_layout_elements.py +++ b/webviz_subsurface/plugins/_grid_viewer/_layout_elements.py @@ -0,0 +1,30 @@ +class ElementIds: + ID = "grid-view" + + class VTKVIEW3D: + ID = "vtk" + VIEW = "vtk-view" + GRID_REPRESENTATION = "grid-representation" + GRID_POLYDATA = "grid-polydata" + GRID_CELLDATA = "grid-celldata" + PICK_REPRESENTATION = "pick-representation" + PICK_SPHERE = "pick-sphere" + + class DataSelectors: + ID = "data-selectors" + REALIZATIONS = "realizations" + STATIC_DYNAMIC = "static-dynamic" + PROPERTIES = "properties" + DATES = "dates" + WELLS = "wells" + + class GridFilter: + ID = "grid-filter" + IJK_CROP_STORE = "ijk-filter-store" + IJK_CROP_WIDGET = "ijk-crop-widget" + + class Settings: + ID = "settings" + ZSCALE = "z-scale" + COLORMAP = "colormap" + SHOW_CUBEAXES = "show-cube-axes" diff --git a/webviz_subsurface/plugins/_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_grid_viewer/_plugin.py index e69de29bb..daa7fe0e4 100644 --- a/webviz_subsurface/plugins/_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_grid_viewer/_plugin.py @@ -0,0 +1,57 @@ +from typing import List, Optional +from pathlib import Path + +from webviz_config import WebvizPluginABC, WebvizSettings + +from webviz_subsurface._providers.ensemble_grid_provider import ( + EnsembleGridProviderFactory, + GridVizService, + EnsembleGridProvider, +) +from webviz_subsurface._providers.well_provider import ( + WellProvider, + WellProviderFactory, + WellServer, +) + +from ._layout_elements import ElementIds +from .views.view_3d._view_3d import View3D + + +class GridViewer(WebvizPluginABC): + def __init__( + self, + app, + webviz_settings, + ensembles: List[str], + grid_name: str, + ): + super().__init__(stretch=True) + + self.ensembles = { + ens_name: webviz_settings.shared_settings["scratch_ensembles"][ens_name] + for ens_name in ensembles + } + + self.add_grid_provider(grid_name=grid_name) + + self.add_store( + ElementIds.GridFilter.IJK_CROP_STORE, + storage_type=WebvizPluginABC.StorageType.SESSION, + ) + print(self._stores) + self.add_view( + View3D( + grid_provider=self.grid_provider, grid_viz_service=self.grid_viz_service + ), + ElementIds.ID, + ) + + def add_grid_provider(self, grid_name: str) -> None: + factory = EnsembleGridProviderFactory.instance() + self.grid_provider: EnsembleGridProvider = factory.create_from_roff_files( + ens_path=list(self.ensembles.values())[0], + grid_name=grid_name, + ) + self.grid_viz_service = GridVizService.instance() + self.grid_viz_service.register_provider(self.grid_provider) diff --git a/webviz_subsurface/plugins/_grid_viewer/_types.py b/webviz_subsurface/plugins/_grid_viewer/_types.py new file mode 100644 index 000000000..2c1cca78e --- /dev/null +++ b/webviz_subsurface/plugins/_grid_viewer/_types.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class PROPERTYTYPE(str, Enum): + STATIC = "Static" + DYNAMIC = "Dynamic" + + +class GRID_DIRECTION(str, Enum): + I = "I" + J = "J" + K = "K" diff --git a/webviz_subsurface/plugins/_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_grid_viewer/views/__init__.py similarity index 100% rename from webviz_subsurface/plugins/_grid_viewer/_callbacks.py rename to webviz_subsurface/plugins/_grid_viewer/views/__init__.py diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/__init__.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py new file mode 100644 index 000000000..ed5c3a63b --- /dev/null +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py @@ -0,0 +1,169 @@ +from typing import List, Tuple, Dict, Optional + +import numpy as np +from webviz_config.webviz_plugin_subclasses import ( + ViewABC, +) + +from dash.development.base_component import Component + +from dash import Input, Output, State, callback, callback_context, no_update, ALL +import webviz_core_components as wcc +from webviz_vtk.utils.vtk import b64_encode_numpy + +from webviz_subsurface._providers.ensemble_grid_provider import ( + EnsembleGridProvider, + GridVizService, + PropertySpec, + CellFilter, + Ray, +) +from webviz_subsurface._utils.perf_timer import PerfTimer +from ..._layout_elements import ElementIds +from ..._types import PROPERTYTYPE +from .settings import DataSettings, GridFilter, Settings +from .view_elements._vtk_view_3d_element import VTKView3D + + +class View3D(ViewABC): + def __init__( + self, grid_provider: EnsembleGridProvider, grid_viz_service: GridVizService + ) -> None: + super().__init__("Grid View") + self.grid_provider = grid_provider + self.grid_viz_service = grid_viz_service + self.vtk_view_3d = VTKView3D() + self.add_view_element(self.vtk_view_3d, ElementIds.VTKVIEW3D.ID), + self.add_settings_group( + DataSettings(grid_provider=grid_provider), ElementIds.DataSelectors.ID + ) + self.add_settings_group( + GridFilter(grid_provider=grid_provider), ElementIds.GridFilter.ID + ) + + def set_callbacks(self) -> None: + @callback( + Output( + self.view_element(ElementIds.VTKVIEW3D.ID) + .component_unique_id(ElementIds.VTKVIEW3D.GRID_POLYDATA) + .to_string(), + "polys", + ), + Output( + self.view_element(ElementIds.VTKVIEW3D.ID) + .component_unique_id(ElementIds.VTKVIEW3D.GRID_POLYDATA) + .to_string(), + "points", + ), + Output( + self.view_element(ElementIds.VTKVIEW3D.ID) + .component_unique_id(ElementIds.VTKVIEW3D.GRID_CELLDATA) + .to_string(), + "values", + ), + Output( + self.view_element(ElementIds.VTKVIEW3D.ID) + .component_unique_id(ElementIds.VTKVIEW3D.GRID_REPRESENTATION) + .to_string(), + "colorDataRange", + ), + Input( + self.settings_group(ElementIds.DataSelectors.ID) + .component_unique_id(ElementIds.DataSelectors.PROPERTIES) + .to_string(), + "value", + ), + Input( + self.settings_group(ElementIds.DataSelectors.ID) + .component_unique_id(ElementIds.DataSelectors.DATES) + .to_string(), + "value", + ), + Input( + self.settings_group(ElementIds.DataSelectors.ID) + .component_unique_id(ElementIds.DataSelectors.REALIZATIONS) + .to_string(), + "value", + ), + Input( + self.get_store_unique_id(ElementIds.GridFilter.IJK_CROP_STORE), "data" + ), + State( + self.settings_group(ElementIds.DataSelectors.ID) + .component_unique_id(ElementIds.DataSelectors.STATIC_DYNAMIC) + .to_string(), + "value", + ), + State( + self.view_element(ElementIds.VTKVIEW3D.ID) + .component_unique_id(ElementIds.VTKVIEW3D.GRID_POLYDATA) + .to_string(), + "polys", + ), + ) + def _set_geometry_and_scalar( + prop: List[str], + date: List[int], + realizations: List[int], + grid_range: List[List[int]], + proptype: str, + current_polys: str, + ) -> Tuple: + + if PROPERTYTYPE(proptype) == PROPERTYTYPE.STATIC: + property_spec = PropertySpec(prop_name=prop[0], prop_date=0) + else: + property_spec = PropertySpec(prop_name=prop[0], prop_date=date[0]) + + triggered = callback_context.triggered[0]["prop_id"] + timer = PerfTimer() + if ( + triggered == "." + or current_polys is None + or self.get_store_unique_id(ElementIds.GridFilter.IJK_CROP_STORE) + in triggered + or self.settings_group(ElementIds.DataSelectors.ID) + .component_unique_id(ElementIds.DataSelectors.REALIZATIONS) + .to_string() + in triggered + ): + surface_polys, scalars = self.grid_viz_service.get_surface( + provider_id=self.grid_provider.provider_id(), + realization=realizations[0], + property_spec=property_spec, + cell_filter=CellFilter( + i_min=grid_range[0][0], + i_max=grid_range[0][1], + j_min=grid_range[1][0], + j_max=grid_range[1][1], + k_min=grid_range[2][0], + k_max=grid_range[2][1], + ), + ) + + return ( + b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), + b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), + b64_encode_numpy(scalars.value_arr.astype(np.float32)), + [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], + ) + else: + scalars = self.grid_viz_service.get_mapped_property_values( + provider_id=self.grid_provider.provider_id(), + realization=realizations[0], + property_spec=property_spec, + cell_filter=CellFilter( + i_min=grid_range[0][0], + i_max=grid_range[0][1], + j_min=grid_range[1][0], + j_max=grid_range[1][1], + k_min=grid_range[2][0], + k_max=grid_range[2][1], + ), + ) + return ( + no_update, + no_update, + b64_encode_numpy(scalars.value_arr.astype(np.float32)), + [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], + ) diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/__init__.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/__init__.py new file mode 100644 index 000000000..00f236001 --- /dev/null +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/__init__.py @@ -0,0 +1,3 @@ +from ._data_selection import DataSettings +from ._grid_filter import GridFilter +from ._settings import Settings diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py new file mode 100644 index 000000000..edc00ef20 --- /dev/null +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py @@ -0,0 +1,153 @@ +from typing import List, Tuple, Dict, Optional + +from dash.development.base_component import Component +from dash import Input, Output, State, callback, no_update +import webviz_core_components as wcc +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC +from webviz_subsurface._providers.ensemble_grid_provider import EnsembleGridProvider + +from webviz_subsurface.plugins._grid_viewer._types import PROPERTYTYPE +from webviz_subsurface.plugins._grid_viewer._layout_elements import ElementIds + + +def list_to_options(values: List) -> List: + return [{"value": val, "label": val} for val in values] + + +class DataSettings(SettingsGroupABC): + def __init__(self, grid_provider: EnsembleGridProvider) -> None: + super().__init__("Data Selection") + self.grid_provider = grid_provider + self.static_dynamic_options = [] + self.static_dynamic_value = None + + if grid_provider.static_property_names(): + self.static_dynamic_options.append( + {"label": PROPERTYTYPE.STATIC, "value": PROPERTYTYPE.STATIC} + ) + self.static_dynamic_value = PROPERTYTYPE.STATIC + + if grid_provider.dynamic_property_names(): + self.static_dynamic_options.append( + {"label": PROPERTYTYPE.DYNAMIC, "value": PROPERTYTYPE.DYNAMIC} + ) + if self.static_dynamic_value is None: + self.static_dynamic_value = PROPERTYTYPE.DYNAMIC + + def layout(self) -> List[Component]: + + return [ + wcc.SelectWithLabel( + label="Realizations", + id=self.register_component_uuid(ElementIds.DataSelectors.REALIZATIONS), + multi=False, + options=list_to_options(self.grid_provider.realizations()), + value=[self.grid_provider.realizations()[0]], + ), + wcc.RadioItems( + label="Static / Dynamic", + id=self.register_component_uuid( + ElementIds.DataSelectors.STATIC_DYNAMIC + ), + options=self.static_dynamic_options, + value=self.static_dynamic_value, + ), + wcc.SelectWithLabel( + label="Property", + id=self.register_component_uuid(ElementIds.DataSelectors.PROPERTIES), + multi=False, + ), + wcc.SelectWithLabel( + label="Date", + id=self.register_component_uuid(ElementIds.DataSelectors.DATES), + multi=False, + ), + ] + + def set_callbacks(self) -> None: + @callback( + Output( + self.component_unique_id( + ElementIds.DataSelectors.PROPERTIES + ).to_string(), + "options", + ), + Output( + self.component_unique_id( + ElementIds.DataSelectors.PROPERTIES + ).to_string(), + "value", + ), + Input( + self.component_unique_id( + ElementIds.DataSelectors.STATIC_DYNAMIC + ).to_string(), + "value", + ), + ) + def _populate_properties( + static_dynamic: str, + ) -> Tuple[ + List[Dict[str, str]], List[str], List[Dict[str, str]], Optional[List[str]] + ]: + if PROPERTYTYPE(static_dynamic) == PROPERTYTYPE.STATIC: + prop_names = self.grid_provider.static_property_names() + + else: + prop_names = self.grid_provider.dynamic_property_names() + + return ( + [{"label": prop, "value": prop} for prop in prop_names], + [prop_names[0]], + ) + + @callback( + Output( + self.component_unique_id(ElementIds.DataSelectors.DATES).to_string(), + "options", + ), + Output( + self.component_unique_id(ElementIds.DataSelectors.DATES).to_string(), + "value", + ), + Input( + self.component_unique_id( + ElementIds.DataSelectors.PROPERTIES + ).to_string(), + "value", + ), + State( + self.component_unique_id( + ElementIds.DataSelectors.STATIC_DYNAMIC + ).to_string(), + "value", + ), + State( + self.component_unique_id(ElementIds.DataSelectors.DATES).to_string(), + "options", + ), + ) + def _populate_dates( + property_name: List[str], + static_dynamic: str, + current_date_options: List, + ) -> Tuple[List[Dict[str, str]], Optional[List[str]]]: + if PROPERTYTYPE(static_dynamic) == PROPERTYTYPE.STATIC: + return [], None + else: + property_name = property_name[0] + dates = self.grid_provider.dates_for_dynamic_property( + property_name=property_name + ) + dates = dates if dates else [] + current_date_options = ( + current_date_options if current_date_options else [] + ) + if set(dates) == set( + [dateopt["value"] for dateopt in current_date_options] + ): + return no_update, no_update + return ( + ([{"label": prop, "value": prop} for prop in dates]), + [dates[0]] if dates else None, + ) diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py new file mode 100644 index 000000000..287929d6b --- /dev/null +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py @@ -0,0 +1,260 @@ +from typing import List, Tuple, Dict, Optional, Any + +from dash.development.base_component import Component +from dash import ( + html, + dcc, + Input, + Output, + callback, + no_update, + ALL, + MATCH, + callback_context, +) +import webviz_core_components as wcc +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC +from webviz_subsurface._providers.ensemble_grid_provider import ( + EnsembleGridProvider, + CellFilter, +) + +from webviz_subsurface.plugins._grid_viewer._types import GRID_DIRECTION +from webviz_subsurface.plugins._grid_viewer._layout_elements import ElementIds + + +def list_to_options(values: List) -> List: + return [{"value": val, "label": val} for val in values] + + +class GridFilter(SettingsGroupABC): + def __init__(self, grid_provider: EnsembleGridProvider) -> None: + super().__init__("Grid IJK Filter") + self.grid_provider = grid_provider + initial_grid = grid_provider.get_3dgrid(grid_provider.realizations()[0]) + self.grid_dimensions = CellFilter( + i_min=0, + j_min=0, + k_min=0, + i_max=initial_grid.dimensions[0] - 1, + j_max=initial_grid.dimensions[1] - 1, + k_max=initial_grid.dimensions[2] - 1, + ) + self.widget_id = self.register_component_uuid( + ElementIds.GridFilter.IJK_CROP_WIDGET + ) + + def layout(self) -> List[Component]: + + return [ + wcc.Selectors( + label="Range filters", + children=[ + crop_widget( + widget_id=self.get_unique_id().to_string(), + min_val=self.grid_dimensions.i_min, + max_val=self.grid_dimensions.i_max, + direction=GRID_DIRECTION.I, + ), + crop_widget( + widget_id=self.get_unique_id().to_string(), + min_val=self.grid_dimensions.j_min, + max_val=self.grid_dimensions.j_max, + direction=GRID_DIRECTION.J, + ), + crop_widget( + widget_id=self.get_unique_id().to_string(), + min_val=self.grid_dimensions.k_min, + max_val=self.grid_dimensions.k_max, + max_width=self.grid_dimensions.k_min, + direction=GRID_DIRECTION.K, + ), + ], + ), + ] + + def set_callbacks(self) -> None: + @callback( + Output( + { + "id": self.get_unique_id().to_string(), + "direction": MATCH, + "component": "input", + "component2": MATCH, + }, + "value", + ), + Output( + { + "id": self.get_unique_id().to_string(), + "direction": MATCH, + "component": "slider", + "component2": MATCH, + }, + "value", + ), + Input( + { + "id": self.get_unique_id().to_string(), + "direction": MATCH, + "component": "input", + "component2": MATCH, + }, + "value", + ), + Input( + { + "id": self.get_unique_id().to_string(), + "direction": MATCH, + "component": "slider", + "component2": MATCH, + }, + "value", + ), + ) + def _synchronize_crop_slider_and_input( + input_val: int, slider_val: int + ) -> Tuple[Any, Any]: + trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0] + if "slider" in trigger_id: + return slider_val, no_update + return no_update, input_val + + @callback( + Output( + self.get_store_unique_id(ElementIds.GridFilter.IJK_CROP_STORE), "data" + ), + Input( + { + "id": self.get_unique_id().to_string(), + "direction": ALL, + "component": "input", + "component2": "start", + }, + "value", + ), + Input( + { + "id": self.get_unique_id().to_string(), + "direction": ALL, + "component": "input", + "component2": "width", + }, + "value", + ), + ) + def _store_grid_range_from_crop_widget( + input_vals: List[int], width_vals: List[int] + ) -> List[List[int]]: + if not input_vals or not width_vals: + return no_update + return [ + [val, val + width - 1] for val, width in zip(input_vals, width_vals) + ] + + +def crop_widget( + widget_id: str, + min_val: int, + max_val: int, + direction: str, + max_width: Optional[int] = None, +) -> html.Div: + max_width = max_width if max_width else max_val + return html.Div( + children=[ + html.Div( + style={ + "display": "grid", + "marginBotton": "0px", + "gridTemplateColumns": f"2fr 1fr 8fr", + }, + children=[ + wcc.Label( + children=f"{direction} Start", + style={ + "fontSize": "0.7em", + "fontWeight": "bold", + "marginRight": "5px", + }, + ), + dcc.Input( + style={"width": "50px", "height": "10px"}, + id={ + "id": widget_id, + "direction": direction, + "component": "input", + "component2": "start", + }, + type="number", + placeholder="Min", + persistence=True, + persistence_type="session", + value=min_val, + min=min_val, + max=max_val, + ), + wcc.Slider( + id={ + "id": widget_id, + "direction": direction, + "component": "slider", + "component2": "start", + }, + min=min_val, + max=max_val, + value=min_val, + step=1, + marks=None, + ), + ], + ), + html.Div( + style={ + "display": "grid", + "marginTop": "0px", + "padding": "0px", + "gridTemplateColumns": f"2fr 1fr 8fr", + }, + children=[ + wcc.Label( + children=f"Width", + style={ + "fontSize": "0.7em", + "textAlign": "right", + "marginRight": "5px", + }, + ), + dcc.Input( + style={"width": "50px", "height": "10px"}, + id={ + "id": widget_id, + "direction": direction, + "component": "input", + "component2": "width", + }, + type="number", + placeholder="Min", + persistence=True, + persistence_type="session", + value=max_width, + min=1, + max=max_val, + ), + wcc.Slider( + id={ + "id": widget_id, + "direction": direction, + "component": "slider", + "component2": "width", + }, + min=1, + max=max_val, + value=max_width, + step=1, + marks=None, + ), + ], + ), + ], + ) diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py new file mode 100644 index 000000000..756c9b941 --- /dev/null +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py @@ -0,0 +1,49 @@ +from typing import List, Tuple, Dict, Optional + +from dash.development.base_component import Component +from dash import Input, Output, State, callback, no_update +import webviz_core_components as wcc +from webviz_config.webviz_plugin_subclasses import SettingsGroupABC +from webviz_subsurface._providers.ensemble_grid_provider import EnsembleGridProvider + +from webviz_subsurface.plugins._grid_viewer._types import PROPERTYTYPE +from webviz_subsurface.plugins._grid_viewer._layout_elements import ElementIds + + +def list_to_options(values: List) -> List: + return [{"value": val, "label": val} for val in values] + + +class Settings(SettingsGroupABC): + def __init__(self) -> None: + super().__init__("Settings") + self.colormaps = ["erdc_rainbow_dark", "Viridis (matplotlib)", "BuRd"] + + def layout(self) -> List[Component]: + + return [ + wcc.Slider( + label="Z Scale", + id=self.register_component_uuid(ElementIds.Settings.ZSCALE), + min=1, + max=10, + value=1, + step=1, + ), + wcc.Selectors( + label="Color map", + children=[ + wcc.Dropdown( + id=self.register_component_uuid(ElementIds.Settings.COLORMAP), + options=list_to_options(self.colormaps), + value=self.colormaps[0], + clearable=False, + ) + ], + ), + wcc.Checklist( + id=self.register_component_uuid(ElementIds.Settings.SHOW_CUBEAXES), + options=["Show bounding box"], + value=["Show bounding box"], + ), + ] diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py new file mode 100644 index 000000000..93cb0f3b2 --- /dev/null +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py @@ -0,0 +1,132 @@ +from typing import List, Tuple + +from dash.development.base_component import Component +from webviz_config.webviz_plugin_subclasses import ViewElementABC + +import webviz_vtk +from dash import callback, Input, Output +from webviz_subsurface.plugins._grid_viewer._layout_elements import ElementIds +from ..settings import Settings + + +class VTKView3D(ViewElementABC): + def __init__(self) -> None: + super().__init__() + self.add_settings_group(Settings(), ElementIds.Settings.ID) + + def inner_layout(self) -> Component: + + return webviz_vtk.View( + id=self.register_component_unique_id(ElementIds.VTKVIEW3D.VIEW), + style={"height": "90vh"}, + pickingModes=["click"], + interactorSettings=[ + { + "button": 1, + "action": "Zoom", + "scrollEnabled": True, + }, + { + "button": 3, + "action": "Pan", + }, + { + "button": 2, + "action": "Rotate", + }, + { + "button": 1, + "action": "Pan", + "shift": True, + }, + { + "button": 1, + "action": "Zoom", + "alt": True, + }, + { + "button": 1, + "action": "Roll", + "alt": True, + "shift": True, + }, + ], + children=[ + webviz_vtk.GeometryRepresentation( + id=self.register_component_unique_id( + ElementIds.VTKVIEW3D.GRID_REPRESENTATION + ), + showCubeAxes=True, + showScalarBar=True, + children=[ + webviz_vtk.PolyData( + id=self.register_component_unique_id( + ElementIds.VTKVIEW3D.GRID_POLYDATA + ), + children=[ + webviz_vtk.CellData( + [ + webviz_vtk.DataArray( + id=self.register_component_unique_id( + ElementIds.VTKVIEW3D.GRID_CELLDATA + ), + registration="setScalars", + name="scalar", + ) + ] + ) + ], + ) + ], + property={"edgeVisibility": True}, + ), + webviz_vtk.GeometryRepresentation( + id=self.register_component_unique_id( + ElementIds.VTKVIEW3D.PICK_REPRESENTATION + ), + actor={"visibility": False}, + children=[ + webviz_vtk.Algorithm( + id=self.register_component_unique_id( + ElementIds.VTKVIEW3D.PICK_SPHERE + ), + vtkClass="vtkSphereSource", + ) + ], + ), + ], + ) + + def set_callbacks(self) -> None: + @callback( + Output( + self.component_unique_id( + ElementIds.VTKVIEW3D.GRID_REPRESENTATION + ).to_string(), + "actor", + ), + Output( + self.component_unique_id( + ElementIds.VTKVIEW3D.GRID_REPRESENTATION + ).to_string(), + "showCubeAxes", + ), + Input( + self.settings_groups()[0] + .component_unique_id(ElementIds.Settings.ZSCALE) + .to_string(), + "value", + ), + Input( + self.settings_groups()[0] + .component_unique_id(ElementIds.Settings.SHOW_CUBEAXES) + .to_string(), + "value", + ), + ) + def _set_representation_actor( + z_scale: int, axes_is_on: List[str] + ) -> Tuple[dict, bool]: + show_axes = bool(z_scale == 1 and axes_is_on) + actor = {"scale": (1, 1, z_scale)} + return actor, show_axes From b7b5e1748f50e7a9353f020197c8092e8e60cd21 Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv <16436291+HansKallekleiv@users.noreply.github.com> Date: Fri, 3 Jun 2022 11:07:15 +0200 Subject: [PATCH 58/63] Added line plot to compare trajectories --- .../well_provider/_provider_impl_file.py | 5 ++- .../_providers/well_provider/well_provider.py | 2 +- .../_eclipse_grid_viewer/_callbacks.py | 43 ++++++++++++++++--- .../plugins/_eclipse_grid_viewer/_layout.py | 20 ++++++++- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/webviz_subsurface/_providers/well_provider/_provider_impl_file.py b/webviz_subsurface/_providers/well_provider/_provider_impl_file.py index 8c80817de..62b61bf8a 100644 --- a/webviz_subsurface/_providers/well_provider/_provider_impl_file.py +++ b/webviz_subsurface/_providers/well_provider/_provider_impl_file.py @@ -136,7 +136,7 @@ def get_well_xtgeo_obj(self, well_name: str) -> xtgeo.Well: return well def get_polyline_along_well_path_SIMPLIFIED( - self, well_name: str, tvdmin=None + self, well_name: str, tvdmin=None, use_rdp=True ) -> np.array: """Returns a polyline for the well path along with MD for the well.""" well = self.get_well_xtgeo_obj(well_name).copy() @@ -160,7 +160,8 @@ def get_polyline_along_well_path_SIMPLIFIED( f"Well is vertical. Returning two points extended in xy from trajectory" ) return [xy_start, xy_end] - + if not use_rdp: + return xy_arr simplified_xy_arr = rdp(xy_arr) print( diff --git a/webviz_subsurface/_providers/well_provider/well_provider.py b/webviz_subsurface/_providers/well_provider/well_provider.py index 67c3816ee..ebd8c6132 100644 --- a/webviz_subsurface/_providers/well_provider/well_provider.py +++ b/webviz_subsurface/_providers/well_provider/well_provider.py @@ -40,7 +40,7 @@ def get_well_xtgeo_obj(self, well_name: str) -> xtgeo.Well: @abc.abstractmethod def get_polyline_along_well_path_SIMPLIFIED( - self, well_name: str, tvdmin=None + self, well_name: str, tvdmin=None, use_rdp: bool = True ) -> WellIntersectionPolyLine: """Returns a polyline for the well path.""" ... diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index ee8049b43..e885412cb 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -160,6 +160,7 @@ def _set_geometry_and_scalar( Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA), "points"), Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA), "polys"), Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_CELL_DATA), "values"), + Output(get_uuid(LayoutElements.LINEGRAPH), "figure"), Input(get_uuid(LayoutElements.WELL_SELECT), "value"), Input(get_uuid(LayoutElements.REALIZATIONS), "value"), Input(get_uuid(LayoutElements.PROPERTIES), "value"), @@ -177,11 +178,42 @@ def set_well_geometries( ]: if not well_names: - return no_update, no_update, no_update, no_update, no_update, no_update - polyline_xy = well_provider.get_polyline_along_well_path_SIMPLIFIED( - well_names[0] + return ( + no_update, + no_update, + no_update, + no_update, + no_update, + no_update, + no_update, + ) + polyline_xy = np.array( + well_provider.get_polyline_along_well_path_SIMPLIFIED(well_names[0]) + ) + polyline_xy_full = np.array( + well_provider.get_polyline_along_well_path_SIMPLIFIED( + well_names[0], use_rdp=False + ) ) - polyline_xy = np.array(polyline_xy).flatten() + + print(polyline_xy[:, 0], polyline_xy[:, 1]) + print(polyline_xy_full) + + def plotly_xy_plot(xy, xy2): + return { + "data": [ + { + "x": xy[:, 0], + "y": xy[:, 1], + "marker": dict( + size=20, + line=dict(color="MediumPurple", width=8), + ), + }, + {"x": xy2[:, 0], "y": xy2[:, 1]}, + ] + } + if PROPERTYTYPE(proptype) == PROPERTYTYPE.INIT: property_spec = PropertySpec(prop_name=prop[0], prop_date=0) else: @@ -190,7 +222,7 @@ def set_well_geometries( surface_polys, scalars = grid_viz_service.cut_along_polyline( provider_id=grid_provider.provider_id(), realization=realizations[0], - polyline_xy=polyline_xy, + polyline_xy=np.array(polyline_xy).flatten(), property_spec=property_spec, ) @@ -205,6 +237,7 @@ def set_well_geometries( b64_encode_numpy(scalars.value_arr.astype(np.float32)) if scalars is not None else no_update, + plotly_xy_plot(polyline_xy, polyline_xy_full), ) @callback( diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index 79a6ca958..afcaf6b8d 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -44,6 +44,7 @@ class LayoutElements(str, Enum): SHOW_AXES = "show-axes" CROP_WIDGET = "crop-widget" GRID_RANGE_STORE = "crop-widget-store" + LINEGRAPH = "line-graph" class LayoutTitles(str, Enum): @@ -106,7 +107,20 @@ def plugin_main_layout( style={"flex": "5"}, children=[ vtk_3d_view(get_uuid=get_uuid), - vtk_intersect_view(get_uuid=get_uuid), + wcc.FlexBox( + children=[ + html.Div( + style={"flex": 1}, + children=vtk_intersect_view(get_uuid=get_uuid), + ), + html.Div( + style={"flex": 1}, + children=dcc.Graph( + id=get_uuid(LayoutElements.LINEGRAPH) + ), + ), + ] + ), ], ), dcc.Store(id=get_uuid(LayoutElements.STORED_CELL_INDICES_HASH)), @@ -534,3 +548,7 @@ def vtk_intersect_view(get_uuid: Callable) -> webviz_vtk.View: ), ], ) + + +def plotly_xy_plot(xy, xy2): + return {"data": [{}]} From 36d6f70cf53c01c713f1fe2211fc0e88bdaa47ac Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Mon, 15 Aug 2022 14:57:59 +0200 Subject: [PATCH 59/63] Sync with WLF. Add reset camera --- setup.py | 2 +- webviz_subsurface/plugins/_grid_viewer/_plugin.py | 3 +-- .../plugins/_grid_viewer/views/view_3d/_view_3d.py | 10 +++++++++- .../views/view_3d/settings/_data_selection.py | 12 ++++++++---- .../views/view_3d/settings/_grid_filter.py | 3 --- .../_grid_viewer/views/view_3d/settings/_settings.py | 8 +++++--- .../view_3d/view_elements/_vtk_view_3d_element.py | 1 + 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index c42004808..24bb5548c 100644 --- a/setup.py +++ b/setup.py @@ -108,7 +108,7 @@ "webviz-config", "webviz-core-components", "webviz-subsurface-components>=0.4.12", - "webviz_vtk@git+https://github.com/hanskallekleiv/webviz-vtk", + "webviz_vtk@git+https://github.com/equinor/webviz-vtk", "xtgeo@git+https://github.com/sigurdp/xtgeo/@sigurdp/vtk-esg", # "xtgeo>=2.18.0a1", ], diff --git a/webviz_subsurface/plugins/_grid_viewer/_plugin.py b/webviz_subsurface/plugins/_grid_viewer/_plugin.py index daa7fe0e4..e241e6a6b 100644 --- a/webviz_subsurface/plugins/_grid_viewer/_plugin.py +++ b/webviz_subsurface/plugins/_grid_viewer/_plugin.py @@ -21,8 +21,7 @@ class GridViewer(WebvizPluginABC): def __init__( self, - app, - webviz_settings, + webviz_settings: WebvizSettings, ensembles: List[str], grid_name: str, ): diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py index ed5c3a63b..a7b27f82a 100644 --- a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/_view_3d.py @@ -33,7 +33,7 @@ def __init__( self.grid_provider = grid_provider self.grid_viz_service = grid_viz_service self.vtk_view_3d = VTKView3D() - self.add_view_element(self.vtk_view_3d, ElementIds.VTKVIEW3D.ID), + self.add_view_element(self.vtk_view_3d, ElementIds.VTKVIEW3D.ID) self.add_settings_group( DataSettings(grid_provider=grid_provider), ElementIds.DataSelectors.ID ) @@ -67,6 +67,12 @@ def set_callbacks(self) -> None: .to_string(), "colorDataRange", ), + Output( + self.view_element(ElementIds.VTKVIEW3D.ID) + .component_unique_id(ElementIds.VTKVIEW3D.VIEW) + .to_string(), + "triggerResetCamera", + ), Input( self.settings_group(ElementIds.DataSelectors.ID) .component_unique_id(ElementIds.DataSelectors.PROPERTIES) @@ -146,6 +152,7 @@ def _set_geometry_and_scalar( b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), b64_encode_numpy(scalars.value_arr.astype(np.float32)), [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], + timer.elapsed_ms(), ) else: scalars = self.grid_viz_service.get_mapped_property_values( @@ -166,4 +173,5 @@ def _set_geometry_and_scalar( no_update, b64_encode_numpy(scalars.value_arr.astype(np.float32)), [np.nanmin(scalars.value_arr), np.nanmax(scalars.value_arr)], + no_update, ) diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py index edc00ef20..03c7bc0ed 100644 --- a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_data_selection.py @@ -39,14 +39,16 @@ def layout(self) -> List[Component]: return [ wcc.SelectWithLabel( label="Realizations", - id=self.register_component_uuid(ElementIds.DataSelectors.REALIZATIONS), + id=self.register_component_unique_id( + ElementIds.DataSelectors.REALIZATIONS + ), multi=False, options=list_to_options(self.grid_provider.realizations()), value=[self.grid_provider.realizations()[0]], ), wcc.RadioItems( label="Static / Dynamic", - id=self.register_component_uuid( + id=self.register_component_unique_id( ElementIds.DataSelectors.STATIC_DYNAMIC ), options=self.static_dynamic_options, @@ -54,12 +56,14 @@ def layout(self) -> List[Component]: ), wcc.SelectWithLabel( label="Property", - id=self.register_component_uuid(ElementIds.DataSelectors.PROPERTIES), + id=self.register_component_unique_id( + ElementIds.DataSelectors.PROPERTIES + ), multi=False, ), wcc.SelectWithLabel( label="Date", - id=self.register_component_uuid(ElementIds.DataSelectors.DATES), + id=self.register_component_unique_id(ElementIds.DataSelectors.DATES), multi=False, ), ] diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py index 287929d6b..5d1ea0b05 100644 --- a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_grid_filter.py @@ -40,9 +40,6 @@ def __init__(self, grid_provider: EnsembleGridProvider) -> None: j_max=initial_grid.dimensions[1] - 1, k_max=initial_grid.dimensions[2] - 1, ) - self.widget_id = self.register_component_uuid( - ElementIds.GridFilter.IJK_CROP_WIDGET - ) def layout(self) -> List[Component]: diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py index 756c9b941..52fb7c89c 100644 --- a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/settings/_settings.py @@ -24,7 +24,7 @@ def layout(self) -> List[Component]: return [ wcc.Slider( label="Z Scale", - id=self.register_component_uuid(ElementIds.Settings.ZSCALE), + id=self.register_component_unique_id(ElementIds.Settings.ZSCALE), min=1, max=10, value=1, @@ -34,7 +34,9 @@ def layout(self) -> List[Component]: label="Color map", children=[ wcc.Dropdown( - id=self.register_component_uuid(ElementIds.Settings.COLORMAP), + id=self.register_component_unique_id( + ElementIds.Settings.COLORMAP + ), options=list_to_options(self.colormaps), value=self.colormaps[0], clearable=False, @@ -42,7 +44,7 @@ def layout(self) -> List[Component]: ], ), wcc.Checklist( - id=self.register_component_uuid(ElementIds.Settings.SHOW_CUBEAXES), + id=self.register_component_unique_id(ElementIds.Settings.SHOW_CUBEAXES), options=["Show bounding box"], value=["Show bounding box"], ), diff --git a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py index 93cb0f3b2..49b0f8834 100644 --- a/webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py +++ b/webviz_subsurface/plugins/_grid_viewer/views/view_3d/view_elements/_vtk_view_3d_element.py @@ -20,6 +20,7 @@ def inner_layout(self) -> Component: id=self.register_component_unique_id(ElementIds.VTKVIEW3D.VIEW), style={"height": "90vh"}, pickingModes=["click"], + autoResetCamera=True, interactorSettings=[ { "button": 1, From 434774121c4a22cca1cad70b85583254dc81954f Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Mon, 15 Aug 2022 16:33:53 +0200 Subject: [PATCH 60/63] Use working webviz-config --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 24bb5548c..082383899 100644 --- a/setup.py +++ b/setup.py @@ -105,7 +105,7 @@ "pyvista>=0.33.3", "scipy>=1.2", "statsmodels>=0.12.1", # indirect dependency through https://plotly.com/python/linear-fits/ - "webviz-config", + "webviz_config@git+https://github.com/equinor/webviz-config@50f0d20f5b6fd6c2d7ee98b17e229e625776ff82", "webviz-core-components", "webviz-subsurface-components>=0.4.12", "webviz_vtk@git+https://github.com/equinor/webviz-vtk", From b517b2da1162d5e729d33592bcb85bceb22d9d1f Mon Sep 17 00:00:00 2001 From: Hans Kallekleiv Date: Tue, 16 Aug 2022 11:54:39 +0200 Subject: [PATCH 61/63] Set initial well --- webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index afcaf6b8d..eea6f2c11 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -188,7 +188,7 @@ def sidebar( label=LayoutTitles.WELL_SELECT, multi=False, options=[{"value": well, "label": well} for well in well_names], - value=[], + value=[well_names[0]], ), wcc.Slider( label=LayoutTitles.Z_SCALE, From 5a3f27fec8432a23ca87b98e1b23782b8af72be6 Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Fri, 19 Aug 2022 15:47:19 +0200 Subject: [PATCH 62/63] Extract and map property scalars in GridVizService.cut_along_polyline() --- setup.py | 1 + .../grid_viz_service.py | 81 +++++++------------ 2 files changed, 30 insertions(+), 52 deletions(-) diff --git a/setup.py b/setup.py index 082383899..bef3292cb 100644 --- a/setup.py +++ b/setup.py @@ -105,6 +105,7 @@ "pyvista>=0.33.3", "scipy>=1.2", "statsmodels>=0.12.1", # indirect dependency through https://plotly.com/python/linear-fits/ + "vtk>=9.2.0rc2", "webviz_config@git+https://github.com/equinor/webviz-config@50f0d20f5b6fd6c2d7ee98b17e229e625776ff82", "webviz-core-components", "webviz-subsurface-components>=0.4.12", diff --git a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py index f99f96a85..a0725c728 100644 --- a/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py +++ b/webviz_subsurface/_providers/ensemble_grid_provider/grid_viz_service.py @@ -24,7 +24,7 @@ vtkExplicitStructuredGridCrop, vtkExplicitStructuredGridToUnstructuredGrid, vtkPlaneCutter, - vtkPolyDataPlaneClipper, + vtkClipPolyData, vtkUnstructuredGridToExplicitStructuredGrid, ) from vtkmodules.vtkFiltersGeneral import vtkBoxClipDataSet @@ -325,7 +325,6 @@ def cut_along_polyline( # box_clip_alg.SetInputDataObject(ugrid) append_alg = vtkAppendPolyData() - cut_surface_polydata_arr = [] et_setup_s = timer.lap_s() et_cut_s = 0.0 @@ -365,60 +364,28 @@ def cut_along_polyline( cutter_alg.SetPlane(plane) cutter_alg.Update() - # Apparently we get a vtkPartitionedDataSet back here in VTK 9.1 - # Note that when testing with VTK 9.2-ish it seems we get polydata back instead - cut_surface_dataset_or_polydata = cutter_alg.GetOutput() - # print(f"{type(cut_surface_dataset_or_polydata)=}") + cut_surface_polydata = cutter_alg.GetOutput() + # print(f"{type(cut_surface_polydata)=}") et_cut_s += timer.lap_s() - if cut_surface_dataset_or_polydata.IsA("vtkPartitionedDataSet"): - cut_surface_dataset = cut_surface_dataset_or_polydata - for i in range(0, cut_surface_dataset.GetNumberOfPartitions()): - part_polydata = cut_surface_dataset.GetPartition(i) - # print(f"{i=} {type(part_polydata)=}") - # print(part_polydata) + # Used vtkPolyDataPlaneClipper earlier, but it seems that it doesn't + # maintain the original cell IDs that we need for the result mapping. + # May want to check up on any performance degradation! + clipper_0 = vtkClipPolyData() + clipper_0.SetInputDataObject(cut_surface_polydata) + clipper_0.SetClipFunction(plane_0) + clipper_0.Update() + clipped_polydata = clipper_0.GetOutput() - clipper_0 = vtkPolyDataPlaneClipper() - clipper_0.SetInputDataObject(part_polydata) - clipper_0.SetPlane(plane_0) - clipper_0.Update() - clipped_polydata = clipper_0.GetOutputDataObject(0) + clipper_1 = vtkClipPolyData() + clipper_1.SetInputDataObject(clipped_polydata) + clipper_1.SetClipFunction(plane_1) + clipper_1.Update() + clipped_polydata = clipper_1.GetOutput() - clipper_1 = vtkPolyDataPlaneClipper() - clipper_1.SetInputDataObject(clipped_polydata) - clipper_1.SetPlane(plane_1) - clipper_1.Update() - clipped_polydata = clipper_1.GetOutputDataObject(0) + append_alg.AddInputData(clipped_polydata) - # print(f"{i=} {type(clipped_polydata)=}") - # print(clipped_polydata) - - cut_surface_polydata_arr.append(clipped_polydata) - append_alg.AddInputData(clipped_polydata) - - et_clip_s += timer.lap_s() - else: - cut_surface_polydata = cut_surface_dataset_or_polydata - - clipper_0 = vtkPolyDataPlaneClipper() - clipper_0.SetInputDataObject(cut_surface_polydata) - clipper_0.SetPlane(plane_0) - clipper_0.Update() - clipped_polydata = clipper_0.GetOutputDataObject(0) - - clipper_1 = vtkPolyDataPlaneClipper() - clipper_1.SetInputDataObject(clipped_polydata) - clipper_1.SetPlane(plane_1) - clipper_1.Update() - clipped_polydata = clipper_1.GetOutputDataObject(0) - - # print(f"{i=} {type(clipped_polydata)=}") - # print(clipped_polydata) - - cut_surface_polydata_arr.append(clipped_polydata) - append_alg.AddInputData(clipped_polydata) - - et_clip_s += timer.lap_s() + et_clip_s += timer.lap_s() append_alg.Update() comb_polydata = append_alg.GetOutput() @@ -429,13 +396,23 @@ def cut_along_polyline( surface_polys = SurfacePolys(point_arr=points_np, poly_arr=polys_np) + property_scalars: Optional[PropertyScalars] = None + if property_spec: + raw_cell_vals = _load_property_values(provider, realization, property_spec) + if raw_cell_vals is not None: + original_cell_indices_np = vtk_to_numpy( + comb_polydata.GetCellData().GetAbstractArray("vtkOriginalCellIds") + ) + mapped_cell_vals = raw_cell_vals[original_cell_indices_np] + property_scalars = PropertyScalars(value_arr=mapped_cell_vals) + LOGGER.debug( f"Cutting along polyline done in {timer.elapsed_s():.2f}s " f"setup={et_setup_s:.2f}s, cut={et_cut_s:.2f}s, clip={et_clip_s:.2f}s, combine={et_combine_s:.2f}s, " f"(provider_id={provider_id}, real={realization})" ) - return surface_polys, None + return surface_polys, property_scalars """ dbg_point_arr = [] From e58c33bcd99feb6d60da9fe01d04f1ce01c4f876 Mon Sep 17 00:00:00 2001 From: Sigurd Pettersen Date: Mon, 22 Aug 2022 14:41:18 +0200 Subject: [PATCH 63/63] Hack to make intersect view appear "flat" and position camera in center of the intersected surface --- .../_eclipse_grid_viewer/_callbacks.py | 63 +++++++++++++++---- .../plugins/_eclipse_grid_viewer/_layout.py | 52 ++++++--------- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py index e885412cb..780926a36 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_callbacks.py @@ -160,6 +160,10 @@ def _set_geometry_and_scalar( Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA), "points"), Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA), "polys"), Output(get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_CELL_DATA), "values"), + Output(get_uuid(LayoutElements.VTK_INTERSECT_VIEW), "cameraPosition"), + Output(get_uuid(LayoutElements.VTK_INTERSECT_VIEW), "cameraFocalPoint"), + Output(get_uuid(LayoutElements.VTK_INTERSECT_VIEW), "cameraViewUp"), + Output(get_uuid(LayoutElements.VTK_INTERSECT_VIEW), "cameraParallelHorScale"), Output(get_uuid(LayoutElements.LINEGRAPH), "figure"), Input(get_uuid(LayoutElements.WELL_SELECT), "value"), Input(get_uuid(LayoutElements.REALIZATIONS), "value"), @@ -186,6 +190,10 @@ def set_well_geometries( no_update, no_update, no_update, + no_update, + no_update, + no_update, + no_update, ) polyline_xy = np.array( well_provider.get_polyline_along_well_path_SIMPLIFIED(well_names[0]) @@ -226,17 +234,31 @@ def plotly_xy_plot(xy, xy2): property_spec=property_spec, ) + approx_plane_normal = _calc_approx_plane_normal_from_polyline_xy(polyline_xy) + + surf_points_3d = np.asarray(surface_polys.point_arr).reshape(-1, 3) + bb_min = np.min(surf_points_3d, axis=0) + bb_max = np.max(surf_points_3d, axis=0) + bb_radius = np.linalg.norm(bb_max - bb_min) / 2 + + center_pt = (bb_max + bb_min) / 2.0 + eye_pt = center_pt + bb_radius * approx_plane_normal + view_up_vec = [0.0, 0.0, 1.0] + + # Make scale slightly larger so we get some space on each side of the viewport + cameraParallelHorScale = bb_radius * 1.05 + return ( - b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), - b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), - b64_encode_numpy(scalars.value_arr.astype(np.float32)) - if scalars is not None - else no_update, - b64_encode_numpy(surface_polys.point_arr.astype(np.float32)), - b64_encode_numpy(surface_polys.poly_arr.astype(np.float32)), - b64_encode_numpy(scalars.value_arr.astype(np.float32)) - if scalars is not None - else no_update, + b64_encode_numpy(surface_polys.point_arr), + b64_encode_numpy(surface_polys.poly_arr), + b64_encode_numpy(scalars.value_arr) if scalars is not None else no_update, + b64_encode_numpy(surface_polys.point_arr), + b64_encode_numpy(surface_polys.poly_arr), + b64_encode_numpy(scalars.value_arr) if scalars is not None else no_update, + eye_pt, + center_pt, + view_up_vec, + cameraParallelHorScale, plotly_xy_plot(polyline_xy, polyline_xy_full), ) @@ -245,7 +267,6 @@ def plotly_xy_plot(xy, xy2): Output(get_uuid(LayoutElements.VTK_WELL_PATH_POLYDATA), "lines"), Output(get_uuid(LayoutElements.VTK_WELL_PATH_2D_POLYDATA), "points"), Output(get_uuid(LayoutElements.VTK_WELL_PATH_2D_POLYDATA), "lines"), - Output(get_uuid(LayoutElements.VTK_INTERSECT_VIEW), "triggerResetCamera"), Input(get_uuid(LayoutElements.WELL_SELECT), "value"), ) def set_well_geometries( @@ -265,7 +286,6 @@ def set_well_geometries( b64_encode_numpy(polyline.line_arr.astype(np.float32)), b64_encode_numpy(polyline.point_arr.astype(np.float32)), b64_encode_numpy(polyline.line_arr.astype(np.float32)), - time(), ) @callback( @@ -472,3 +492,22 @@ def _store_grid_range_from_crop_widget( if not input_vals or not width_vals: return no_update return [[val, val + width - 1] for val, width in zip(input_vals, width_vals)] + + +def _calc_approx_plane_normal_from_polyline_xy(polyline_xy: List[float]) -> List[float]: + polyline_np = np.asarray(polyline_xy).reshape(-1, 2) + num_points_in_polyline = len(polyline_np) + + aggr_right_vec = np.array([0.0, 0.0]) + for i in range(0, num_points_in_polyline - 1): + p0 = polyline_np[i] + p1 = polyline_np[i + 1] + fwd_vec = p1 - p0 + fwd_vec /= np.linalg.norm(fwd_vec) + right_vec = np.array([fwd_vec[1], -fwd_vec[0]]) + aggr_right_vec += right_vec + + avg_right_vec = aggr_right_vec / np.linalg.norm(aggr_right_vec) + approx_plane_normal = np.array([aggr_right_vec[0], aggr_right_vec[1], 0]) + + return approx_plane_normal diff --git a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py index eea6f2c11..2b9a77c2e 100644 --- a/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py +++ b/webviz_subsurface/plugins/_eclipse_grid_viewer/_layout.py @@ -478,46 +478,34 @@ def vtk_3d_view(get_uuid: Callable) -> webviz_vtk.View: def vtk_intersect_view(get_uuid: Callable) -> webviz_vtk.View: + + # fmt: off + interactorSettings = [ + {"button": 1, "action": "Zoom"}, + {"button": 1, "action": "Zoom", "alt": True}, + {"button": 1, "action": "Pan", "shift": True}, + {"button": 3, "action": "Pan"}, + #{"button": 2, "action": "Rotate", "useFocalPointAsCenterOfRotation": True}, + {"dragEnabled": False, "action": "Zoom", "scrollEnabled": True}, + {"dragEnabled": False, "action": "ZoomToMouse", "scrollEnabled": True, "control": True,}, + ] + # fmt: on + return webviz_vtk.View( id=get_uuid(LayoutElements.VTK_INTERSECT_VIEW), style=LayoutStyle.VTK_VIEW, pickingModes=["click"], - interactorSettings=[ - { - "button": 1, - "action": "Zoom", - "scrollEnabled": True, - }, - { - "button": 3, - "action": "Pan", - }, - { - "button": 2, - "action": "Rotate", - }, - { - "button": 1, - "action": "Pan", - "shift": True, - }, - { - "button": 1, - "action": "Zoom", - "alt": True, - }, - { - "button": 1, - "action": "Roll", - "alt": True, - "shift": True, - }, - ], + cameraParallelProjection=True, + autoResetCamera=False, + interactorSettings=interactorSettings, children=[ webviz_vtk.GeometryRepresentation( id=get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_REPRESENTATION), actor={"visibility": True}, - property={"edgeVisibility": True}, + property={ + "edgeVisibility": False, + "lighting": False, + }, children=[ webviz_vtk.PolyData( id=get_uuid(LayoutElements.VTK_WELL_2D_INTERSECT_POLYDATA),