Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions geos-trame/src/geos/trame/app/components/alertHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@

from trame.widgets import vuetify3

from enum import Enum


class AlertType( str, Enum ):
"""Enum representing the type of VAlert.

For more information, see the uetify documentation:
https://vuetifyjs.com/en/api/VAlert/#props-type
"""
SUCCESS = 'success'
WARNING = 'warning'
ERROR = 'error'


class AlertHandler( vuetify3.VContainer ):
"""Vuetify component used to display an alert status.
Expand All @@ -26,8 +39,9 @@ def __init__( self ) -> None:

self.state.alerts = []

self.ctrl.on_add_error.add_task( self.add_error )
self.ctrl.on_add_warning.add_task( self.add_warning )
self.server.controller.on_add_success.add_task( self.add_success )
self.server.controller.on_add_warning.add_task( self.add_warning )
self.server.controller.on_add_error.add_task( self.add_error )

self.generate_alert_ui()

Expand Down Expand Up @@ -75,7 +89,7 @@ def add_alert( self, type: str, title: str, message: str ) -> None:
self.state.dirty( "alerts" )
self.state.flush()

if type == "warning":
if type == AlertType.WARNING:
asyncio.get_event_loop().call_later( self.__lifetime_of_alert, self.on_close, alert_id )

async def add_warning( self, title: str, message: str ) -> None:
Expand All @@ -86,6 +100,10 @@ async def add_error( self, title: str, message: str ) -> None:
"""Add an alert of type 'error'."""
self.add_alert( "error", title, message )

async def add_success( self, title: str, message: str ) -> None:
"""Add an alert of type 'success'."""
self.add_alert( AlertType.SUCCESS, title, message )

def on_close( self, alert_id: int ) -> None:
"""Remove in the state the alert associated to the given id."""
self.state.alerts = list( filter( lambda i: i[ "id" ] != alert_id, self.state.alerts ) )
Expand Down
2 changes: 1 addition & 1 deletion geos-trame/src/geos/trame/app/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __init__( self, server: Server, file_name: str ) -> None:
self.ctrl.simput_reload_data = self.simput_widget.reload_data

# Tree
self.tree = DeckTree( self.state.sm_id )
self.tree = DeckTree( self.state.sm_id, self.ctrl )

# Viewers
self.region_viewer = RegionViewer()
Expand Down
1 change: 1 addition & 0 deletions geos-trame/src/geos/trame/app/data_types/renderable.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

class Renderable( Enum ):
"""Enum class for renderable types and their ids."""
BOX = "Box"
VTKMESH = "VTKMesh"
INTERNALMESH = "InternalMesh"
INTERNALWELL = "InternalWell"
Expand Down
13 changes: 9 additions & 4 deletions geos-trame/src/geos/trame/app/deck/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,28 @@
import os
from collections import defaultdict
from typing import Any

import dpath
import funcy
from pydantic import BaseModel
from trame_simput import get_simput_manager

from xsdata.formats.dataclass.parsers.config import ParserConfig
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata.utils import text
from xsdata_pydantic.bindings import DictDecoder, XmlContext, XmlSerializer

from trame_server.controller import Controller
from trame_simput import get_simput_manager

from geos.trame.app.deck.file import DeckFile
from geos.trame.app.geosTrameException import GeosTrameException
from geos.trame.app.utils.file_utils import normalize_path, format_xml
from geos.trame.schema_generated.schema_mod import Problem, Included, File, Functions
from geos.trame.app.utils.file_utils import normalize_path, format_xml


class DeckTree( object ):
"""A tree that represents a deck file along with all the available blocks and parameters."""

def __init__( self, sm_id: str | None = None, **kwargs: Any ) -> None:
def __init__( self, sm_id: str | None = None, ctrl: Controller = None, **kwargs: Any ) -> None:
"""Constructor."""
super( DeckTree, self ).__init__( **kwargs )

Expand All @@ -33,6 +35,7 @@ def __init__( self, sm_id: str | None = None, **kwargs: Any ) -> None:
self.root = None
self.input_has_errors = False
self._sm_id = sm_id
self._ctrl = ctrl

def set_input_file( self, input_filename: str ) -> None:
"""Set a new input file.
Expand Down Expand Up @@ -172,6 +175,8 @@ def write_files( self ) -> None:
file.write( model_as_xml )
file.close()

self._ctrl.on_add_success( title="File saved", message=f"File {basename} has been saved." )

@staticmethod
def _append_include_file( model: Problem, included_file_path: str ) -> None:
"""Append an Included object which follows this structure according to the documentation.
Expand Down
114 changes: 114 additions & 0 deletions geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
# SPDX-FileContributor: Lucas Givord - Kitware
import pyvista as pv

from geos.trame.schema_generated.schema_mod import Box

import re


class BoxViewer:
"""A BoxViewer represents a Box and its intersected cell in a mesh.

This mesh is represented in GEOS with a Box.
"""

def __init__( self, mesh: pv.UnstructuredGrid, box: Box ) -> None:
"""Initialize the BoxViewer with a mesh and a box."""
self._mesh: pv.UnstructuredGrid = mesh

self._box: Box = box
self._box_polydata: pv.PolyData = None
self._box_polydata_actor: pv.Actor = None

self._extracted_cells: pv.UnstructuredGrid = None
self._extracted_cells_actor: pv.Actor = None

self._compute_box_as_polydata()
self._compute_intersected_cell()

def get_box_polydata( self ) -> pv.PolyData | None:
"""Get the box polydata."""
return self._box_polydata

def get_box_polydata_actor( self ) -> pv.Actor:
"""Get the actor generated by a pv.Plotter for the box polydata."""
return self._box_polydata_actor

def get_extracted_cells( self ) -> pv.UnstructuredGrid | None:
"""Get the extracted cell polydata."""
return self._extracted_cells

def get_extracted_cells_actor( self ) -> pv.Actor | None:
"""Get the extracted cell polydata actor."""
return self._extracted_cells_actor

def set_box_polydata_actor( self, box_actor: pv.Actor ) -> None:
"""Set the actor generated by a pv.Plotter for the box polydata."""
self._box_polydata_actor = box_actor

def set_extracted_cells_actor( self, extracted_cell: pv.Actor ) -> None:
"""Set the actor generated by a pv.Plotter for the extracted cell."""
self._extracted_cells_actor = extracted_cell

def _compute_box_as_polydata( self ) -> None:
"""Create a polydata reresenting a BBox using pyvista and coordinates from the Geos Box."""
bounding_box: list[ float ] = self._retrieve_bounding_box()
self._box_polydata = pv.Box( bounds=bounding_box )

def _retrieve_bounding_box( self ) -> list[ float ]:
"""This method converts bounding box information from Box into a list of coordinates readable by pyvista.

e.g., this Box:

<Box name="box_1"
xMin="{ 1150, 700, 62 }"
xMax="{ 1250, 800, 137 }"/>

will return [1150, 1250, 700, 800, 62, 137].
"""
# split str and remove brackets
min_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_min )
max_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_max )

min_point = list( map( float, min_point_str ) )
max_point = list( map( float, max_point_str ) )

return [
min_point[ 0 ],
max_point[ 0 ],
min_point[ 1 ],
max_point[ 1 ],
min_point[ 2 ],
max_point[ 2 ],
]

def _compute_intersected_cell( self ) -> None:
"""Extract the cells from the mesh that are inside the box."""
ids = self._mesh.find_cells_within_bounds( self._box_polydata.bounds )

saved_ids: list[ int ] = []

for id in ids:
cell: pv.vtkCell = self._mesh.GetCell( id )

is_inside = self._check_cell_inside_box( cell, self._box_polydata.bounds )
if is_inside:
saved_ids.append( id )

if len( saved_ids ) > 0:
self._extracted_cells = self._mesh.extract_cells( saved_ids )

def _check_cell_inside_box( self, cell: pv.Cell, box_bounds: list[ float ] ) -> bool:
"""Check if the cell is inside the box bounds.

A cell is considered inside the box if his bounds are completely
inside the box bounds.
"""
cell_bounds = cell.GetBounds()
is_inside_in_x = cell_bounds[ 0 ] >= box_bounds[ 0 ] and cell_bounds[ 1 ] <= box_bounds[ 1 ]
is_inside_in_y = cell_bounds[ 2 ] >= box_bounds[ 2 ] and cell_bounds[ 3 ] <= box_bounds[ 3 ]
is_inside_in_z = cell_bounds[ 4 ] >= box_bounds[ 4 ] and cell_bounds[ 5 ] <= box_bounds[ 5 ]

return is_inside_in_x and is_inside_in_y and is_inside_in_z
48 changes: 45 additions & 3 deletions geos-trame/src/geos/trame/app/ui/viewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
from vtkmodules.vtkRenderingCore import vtkActor

from geos.trame.app.deck.tree import DeckTree
from geos.trame.app.ui.viewer.boxViewer import BoxViewer
from geos.trame.app.ui.viewer.perforationViewer import PerforationViewer
from geos.trame.app.ui.viewer.regionViewer import RegionViewer
from geos.trame.app.ui.viewer.wellViewer import WellViewer
from geos.trame.schema_generated.schema_mod import Vtkmesh, Vtkwell, InternalWell, Perforation
from geos.trame.schema_generated.schema_mod import Box, Vtkmesh, Vtkwell, InternalWell, Perforation

pv.OFF_SCREEN = True

Expand All @@ -35,6 +36,7 @@ def __init__(
- Vtkwell,
- Perforation,
- InternalWell
- Box

Everything is handle in the method 'update_viewer()' which is trigger when the
'state.object_state' changed (see DeckTree).
Expand All @@ -59,6 +61,7 @@ def __init__(
self.SELECTED_DATA_ARRAY = "viewer_selected_data_array"
self.state.change( self.SELECTED_DATA_ARRAY )( self._update_actor_array )

self.box_engine: BoxViewer | None = None
self.region_engine = region_viewer
self.well_engine = well_viewer
self._perforations: dict[ str, PerforationViewer ] = {}
Expand Down Expand Up @@ -122,7 +125,7 @@ def rendering_menu_extra_items( self ) -> None:
def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) -> None:
"""Add from path the dataset given by the user.

Supported data type is: Vtkwell, Vtkmesh, InternalWell, Perforation.
Supported data type is: Vtkwell, Vtkmesh, InternalWell, Perforation, Box.

object_state : array used to store path to the data and if we want to show it or not.
"""
Expand All @@ -138,6 +141,13 @@ def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) ->
if isinstance( active_block, Perforation ):
self._update_perforation( active_block, show_obj, path )

if isinstance( active_block, Box ):
self._update_box( active_block, show_obj )

# when data is added in the pv.Plotter, we need to refresh the scene to update
# the actor to avoid LUT issue.
self.plotter.update()

def _on_clip_visibility_change( self, **kwargs: Any ) -> None:
"""Toggle cut plane visibility for all actors.

Expand Down Expand Up @@ -215,6 +225,7 @@ def _update_internalwell( self, path: str, show: bool ) -> None:
"""
if not show:
self.plotter.remove_actor( self.well_engine.get_actor( path ) ) # type: ignore
self.well_engine.remove_actor( path )
return

tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
Expand All @@ -229,6 +240,7 @@ def _update_vtkwell( self, path: str, show: bool ) -> None:
"""
if not show:
self.plotter.remove_actor( self.well_engine.get_actor( path ) ) # type: ignore
self.well_engine.remove_actor( path )
return

tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
Expand Down Expand Up @@ -328,6 +340,36 @@ def _add_perforation( self, distance_from_head: float, path: str ) -> None:
cell_id = self.region_engine.input.find_closest_cell( point_offsetted )
cell = self.region_engine.input.extract_cells( [ cell_id ] )
cell_actor = self.plotter.add_mesh( cell )
saved_perforation.add_extracted_cell( cell_actor )
saved_perforation.add_extracted_cells( cell_actor )

self._perforations[ path ] = saved_perforation

def _update_box( self, active_block: Box, show_obj: bool ) -> None:
"""Generate and display a Box and inner cell(s) from the mesh."""
if self.region_engine.input.number_of_cells == 0 and show_obj:
self.ctrl.on_add_warning(
"Can't display " + active_block.name,
"Please display the mesh before creating a well",
)
return

if self.box_engine is not None:
box_polydata_actor: pv.Actor = self.box_engine.get_box_polydata_actor()
extracted_cell_actor: pv.Actor = self.box_engine.get_extracted_cells_actor()
self.plotter.remove_actor( box_polydata_actor )
self.plotter.remove_actor( extracted_cell_actor )

if not show_obj:
return

box: Box = active_block
self.box_engine = BoxViewer( self.region_engine.input, box )

box_polydata: pv.PolyData = self.box_engine.get_box_polydata()
extracted_cell: pv.UnstructuredGrid = self.box_engine.get_extracted_cells()

if box_polydata is not None and extracted_cell is not None:
_box_polydata_actor = self.plotter.add_mesh( box_polydata, opacity=0.2 )
_extracted_cells_actor = self.plotter.add_mesh( extracted_cell, show_edges=True )
self.box_engine.set_box_polydata_actor( _box_polydata_actor )
self.box_engine.set_extracted_cells_actor( _extracted_cells_actor )
Loading
Loading