Skip to content

feat: add logic to intersect cell with a box in geos-trame #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
111 changes: 111 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,111 @@
# 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_cell: pv.UnstructuredGrid = None
self._extracted_cell_actor: pv.Actor = None

self._compute_box_as_polydata()
self._compute_intersected_cell()

def append_to_plotter( self, plotter: pv.Plotter ) -> None:
"""Append the box and the intersected cell to the plotter.

The box is represented as a polydata with a low opacity.
"""
self._box_polydata_actor = plotter.add_mesh( self._box_polydata, opacity=0.2 )

if self._extracted_cell is not None:
self._extracted_cell_actor = plotter.add_mesh( self._extracted_cell, show_edges=True )

def reset( self, plotter: pv.Plotter ) -> None:
"""Reset the box viewer by removing the box and the intersected cell from the plotter."""
if self._box_polydata_actor is not None:
plotter.remove_actor( self._box_polydata_actor )

if self._extracted_cell_actor is not None:
plotter.remove_actor( self._extracted_cell_actor )

self._box_polydata = None
self._extracted_cell = None

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_cell = 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
24 changes: 23 additions & 1 deletion 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 Down Expand Up @@ -59,6 +60,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 @@ -138,6 +140,24 @@ 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 ):
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:
self.box_engine.reset( self.plotter )

if not show_obj:
return

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

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

Expand Down Expand Up @@ -215,6 +235,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 +250,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
39 changes: 34 additions & 5 deletions geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ class Well:
well_path: str
polyline: pv.PolyData
tube: pv.PolyData


@dataclass
class WellActor:
"""A WellActor stores the VTK Actor representing a well."""
well_path: str
actor: pv.Actor


Expand All @@ -28,6 +34,7 @@ def __init__( self, size: float, amplification: float ) -> None:
A Well in GEOS could a InternalWell or a Vtkwell.
"""
self._wells: list[ Well ] = []
self._wells_actors: list[ WellActor ] = []

self.size: float = size
self.amplification: float = amplification
Expand All @@ -51,7 +58,8 @@ def add_mesh( self, mesh: pv.PolyData, mesh_path: str ) -> int:
radius = self.size * ( self.STARTING_VALUE / 100 )
tube = mesh.tube( radius=radius, n_sides=50 )

self._wells.append( Well( mesh_path, mesh, tube, pv.Actor() ) )
self._wells.append( Well( mesh_path, mesh, tube ) )
self._wells_actors.append( WellActor( mesh_path, pv.Actor() ) )

return len( self._wells ) - 1

Expand All @@ -78,21 +86,21 @@ def get_tube_size( self ) -> float:

def append_actor( self, perforation_path: str, tube_actor: pv.Actor ) -> None:
"""Append a given actor, typically the Actor returned by the pv.Plotter() when a given mes is added."""
index = self._get_index_from_perforation( perforation_path )
index = self._get_actor_index_from_perforation( perforation_path )
if index == -1:
print( "Cannot found the well to remove from path: ", perforation_path )
return

self._wells[ index ].actor = tube_actor
self._wells_actors[ index ].actor = tube_actor

def get_actor( self, perforation_path: str ) -> pv.Actor | None:
"""Retrieve the polyline linked to a given perforation path."""
index = self._get_index_from_perforation( perforation_path )
index = self._get_actor_index_from_perforation( perforation_path )
if index == -1:
print( "Cannot found the well to remove from path: ", perforation_path )
return None

return self._wells[ index ].actor
return self._wells_actors[ index ].actor

def update( self, value: float ) -> None:
"""Update the radius of the tubes."""
Expand All @@ -108,6 +116,14 @@ def remove( self, perforation_path: str ) -> None:

self._wells.remove( self._wells[ index ] )

def remove_actor( self, perforation_path: str ) -> None:
"""Clear all data stored in this class."""
index = self._get_actor_index_from_perforation( perforation_path )
if index == -1:
print( "Cannot found the well to remove from path: ", perforation_path )

self._wells_actors.remove( self._wells_actors[ index ] )

def _get_index_from_perforation( self, perforation_path: str ) -> int:
"""Retrieve the well associated to a given perforation, otherwise return -1."""
index = -1
Expand All @@ -121,6 +137,19 @@ def _get_index_from_perforation( self, perforation_path: str ) -> int:

return index

def _get_actor_index_from_perforation( self, perforation_path: str ) -> int:
"""Retrieve the well actor associated to a given perforation, otherwise return -1."""
index = -1
if len( self._wells_actors ) == 0:
return index

for i in range( 0, len( self._wells_actors ) ):
if self._wells_actors[ i ].well_path in perforation_path:
index = i
break

return index

def get_number_of_wells( self ) -> int:
"""Get the number of wells in the viewer."""
return len( self._wells )
4 changes: 2 additions & 2 deletions geos-trame/tests/data/geosDeck/geosDeck.xml
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@
<Geometry>
<Box
name="box_1"
xMin="{ 1150, 700, 62 }"
xMax="{ 1250, 800, 137 }"/>
xMin="{ 3509, 4117, -596 }"
xMax="{ 4482, 5041, -500 }"/>
</Geometry>
<!-- End Block to comment to deactivate box definition -->

Expand Down
Loading