Skip to content

Commit b8fb53f

Browse files
lucas-givordpaloma-martinezjafranc
authored
feat: add logic to intersect cell with a box in geos-trame (#99)
* add support for intersected cell from Box * add success alert type for the saving feature * fix: update data added in the plotter * viewer: remove Plotter dependency to the box engine * Update python-package.yml --------- Co-authored-by: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com> Co-authored-by: Jacques Franc <49998870+jafranc@users.noreply.github.com>
1 parent b80dcd7 commit b8fb53f

File tree

10 files changed

+275
-17
lines changed

10 files changed

+275
-17
lines changed

.github/workflows/python-package.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ jobs:
9292
with:
9393
python-version: ${{ matrix.python-version }}
9494
cache: 'pip'
95+
96+
- name: Install OSMesa and GL
97+
run: |
98+
sudo apt-get update
99+
sudo apt-get install -y libosmesa6
100+
sudo apt-get install -y \
101+
libegl1-mesa-dev \
102+
libgles2-mesa-dev \
103+
libgl1-mesa-dev \
104+
libx11-dev \
105+
libxext-dev
106+
95107
- name: Install package
96108
# working-directory: ./${{ matrix.package-name }}
97109
run: |

geos-trame/src/geos/trame/app/components/alertHandler.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@
55

66
from trame.widgets import vuetify3
77

8+
from enum import Enum
9+
10+
11+
class AlertType( str, Enum ):
12+
"""Enum representing the type of VAlert.
13+
14+
For more information, see the uetify documentation:
15+
https://vuetifyjs.com/en/api/VAlert/#props-type
16+
"""
17+
SUCCESS = 'success'
18+
WARNING = 'warning'
19+
ERROR = 'error'
20+
821

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

2740
self.state.alerts = []
2841

29-
self.ctrl.on_add_error.add_task( self.add_error )
30-
self.ctrl.on_add_warning.add_task( self.add_warning )
42+
self.server.controller.on_add_success.add_task( self.add_success )
43+
self.server.controller.on_add_warning.add_task( self.add_warning )
44+
self.server.controller.on_add_error.add_task( self.add_error )
3145

3246
self.generate_alert_ui()
3347

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

78-
if type == "warning":
92+
if type == AlertType.WARNING:
7993
asyncio.get_event_loop().call_later( self.__lifetime_of_alert, self.on_close, alert_id )
8094

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

103+
async def add_success( self, title: str, message: str ) -> None:
104+
"""Add an alert of type 'success'."""
105+
self.add_alert( AlertType.SUCCESS, title, message )
106+
89107
def on_close( self, alert_id: int ) -> None:
90108
"""Remove in the state the alert associated to the given id."""
91109
self.state.alerts = list( filter( lambda i: i[ "id" ] != alert_id, self.state.alerts ) )

geos-trame/src/geos/trame/app/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __init__( self, server: Server, file_name: str ) -> None:
6161
self.ctrl.simput_reload_data = self.simput_widget.reload_data
6262

6363
# Tree
64-
self.tree = DeckTree( self.state.sm_id )
64+
self.tree = DeckTree( self.state.sm_id, self.ctrl )
6565

6666
# Viewers
6767
self.region_viewer = RegionViewer()

geos-trame/src/geos/trame/app/data_types/renderable.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
class Renderable( Enum ):
88
"""Enum class for renderable types and their ids."""
9+
BOX = "Box"
910
VTKMESH = "VTKMesh"
1011
INTERNALMESH = "InternalMesh"
1112
INTERNALWELL = "InternalWell"

geos-trame/src/geos/trame/app/deck/tree.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,28 @@
44
import os
55
from collections import defaultdict
66
from typing import Any
7-
87
import dpath
98
import funcy
109
from pydantic import BaseModel
11-
from trame_simput import get_simput_manager
10+
1211
from xsdata.formats.dataclass.parsers.config import ParserConfig
1312
from xsdata.formats.dataclass.serializers.config import SerializerConfig
1413
from xsdata.utils import text
1514
from xsdata_pydantic.bindings import DictDecoder, XmlContext, XmlSerializer
1615

16+
from trame_server.controller import Controller
17+
from trame_simput import get_simput_manager
18+
1719
from geos.trame.app.deck.file import DeckFile
1820
from geos.trame.app.geosTrameException import GeosTrameException
19-
from geos.trame.app.utils.file_utils import normalize_path, format_xml
2021
from geos.trame.schema_generated.schema_mod import Problem, Included, File, Functions
22+
from geos.trame.app.utils.file_utils import normalize_path, format_xml
2123

2224

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

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

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

3740
def set_input_file( self, input_filename: str ) -> None:
3841
"""Set a new input file.
@@ -172,6 +175,8 @@ def write_files( self ) -> None:
172175
file.write( model_as_xml )
173176
file.close()
174177

178+
self._ctrl.on_add_success( title="File saved", message=f"File {basename} has been saved." )
179+
175180
@staticmethod
176181
def _append_include_file( model: Problem, included_file_path: str ) -> None:
177182
"""Append an Included object which follows this structure according to the documentation.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Lucas Givord - Kitware
4+
import pyvista as pv
5+
6+
from geos.trame.schema_generated.schema_mod import Box
7+
8+
import re
9+
10+
11+
class BoxViewer:
12+
"""A BoxViewer represents a Box and its intersected cell in a mesh.
13+
14+
This mesh is represented in GEOS with a Box.
15+
"""
16+
17+
def __init__( self, mesh: pv.UnstructuredGrid, box: Box ) -> None:
18+
"""Initialize the BoxViewer with a mesh and a box."""
19+
self._mesh: pv.UnstructuredGrid = mesh
20+
21+
self._box: Box = box
22+
self._box_polydata: pv.PolyData = None
23+
self._box_polydata_actor: pv.Actor = None
24+
25+
self._extracted_cells: pv.UnstructuredGrid = None
26+
self._extracted_cells_actor: pv.Actor = None
27+
28+
self._compute_box_as_polydata()
29+
self._compute_intersected_cell()
30+
31+
def get_box_polydata( self ) -> pv.PolyData | None:
32+
"""Get the box polydata."""
33+
return self._box_polydata
34+
35+
def get_box_polydata_actor( self ) -> pv.Actor:
36+
"""Get the actor generated by a pv.Plotter for the box polydata."""
37+
return self._box_polydata_actor
38+
39+
def get_extracted_cells( self ) -> pv.UnstructuredGrid | None:
40+
"""Get the extracted cell polydata."""
41+
return self._extracted_cells
42+
43+
def get_extracted_cells_actor( self ) -> pv.Actor | None:
44+
"""Get the extracted cell polydata actor."""
45+
return self._extracted_cells_actor
46+
47+
def set_box_polydata_actor( self, box_actor: pv.Actor ) -> None:
48+
"""Set the actor generated by a pv.Plotter for the box polydata."""
49+
self._box_polydata_actor = box_actor
50+
51+
def set_extracted_cells_actor( self, extracted_cell: pv.Actor ) -> None:
52+
"""Set the actor generated by a pv.Plotter for the extracted cell."""
53+
self._extracted_cells_actor = extracted_cell
54+
55+
def _compute_box_as_polydata( self ) -> None:
56+
"""Create a polydata reresenting a BBox using pyvista and coordinates from the Geos Box."""
57+
bounding_box: list[ float ] = self._retrieve_bounding_box()
58+
self._box_polydata = pv.Box( bounds=bounding_box )
59+
60+
def _retrieve_bounding_box( self ) -> list[ float ]:
61+
"""This method converts bounding box information from Box into a list of coordinates readable by pyvista.
62+
63+
e.g., this Box:
64+
65+
<Box name="box_1"
66+
xMin="{ 1150, 700, 62 }"
67+
xMax="{ 1250, 800, 137 }"/>
68+
69+
will return [1150, 1250, 700, 800, 62, 137].
70+
"""
71+
# split str and remove brackets
72+
min_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_min )
73+
max_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_max )
74+
75+
min_point = list( map( float, min_point_str ) )
76+
max_point = list( map( float, max_point_str ) )
77+
78+
return [
79+
min_point[ 0 ],
80+
max_point[ 0 ],
81+
min_point[ 1 ],
82+
max_point[ 1 ],
83+
min_point[ 2 ],
84+
max_point[ 2 ],
85+
]
86+
87+
def _compute_intersected_cell( self ) -> None:
88+
"""Extract the cells from the mesh that are inside the box."""
89+
ids = self._mesh.find_cells_within_bounds( self._box_polydata.bounds )
90+
91+
saved_ids: list[ int ] = []
92+
93+
for id in ids:
94+
cell: pv.vtkCell = self._mesh.GetCell( id )
95+
96+
is_inside = self._check_cell_inside_box( cell, self._box_polydata.bounds )
97+
if is_inside:
98+
saved_ids.append( id )
99+
100+
if len( saved_ids ) > 0:
101+
self._extracted_cells = self._mesh.extract_cells( saved_ids )
102+
103+
def _check_cell_inside_box( self, cell: pv.Cell, box_bounds: list[ float ] ) -> bool:
104+
"""Check if the cell is inside the box bounds.
105+
106+
A cell is considered inside the box if his bounds are completely
107+
inside the box bounds.
108+
"""
109+
cell_bounds = cell.GetBounds()
110+
is_inside_in_x = cell_bounds[ 0 ] >= box_bounds[ 0 ] and cell_bounds[ 1 ] <= box_bounds[ 1 ]
111+
is_inside_in_y = cell_bounds[ 2 ] >= box_bounds[ 2 ] and cell_bounds[ 3 ] <= box_bounds[ 3 ]
112+
is_inside_in_z = cell_bounds[ 4 ] >= box_bounds[ 4 ] and cell_bounds[ 5 ] <= box_bounds[ 5 ]
113+
114+
return is_inside_in_x and is_inside_in_y and is_inside_in_z

geos-trame/src/geos/trame/app/ui/viewer/viewer.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
from vtkmodules.vtkRenderingCore import vtkActor
1212

1313
from geos.trame.app.deck.tree import DeckTree
14+
from geos.trame.app.ui.viewer.boxViewer import BoxViewer
1415
from geos.trame.app.ui.viewer.perforationViewer import PerforationViewer
1516
from geos.trame.app.ui.viewer.regionViewer import RegionViewer
1617
from geos.trame.app.ui.viewer.wellViewer import WellViewer
17-
from geos.trame.schema_generated.schema_mod import Vtkmesh, Vtkwell, InternalWell, Perforation
18+
from geos.trame.schema_generated.schema_mod import Box, Vtkmesh, Vtkwell, InternalWell, Perforation
1819

1920
pv.OFF_SCREEN = True
2021

@@ -35,6 +36,7 @@ def __init__(
3536
- Vtkwell,
3637
- Perforation,
3738
- InternalWell
39+
- Box
3840
3941
Everything is handle in the method 'update_viewer()' which is trigger when the
4042
'state.object_state' changed (see DeckTree).
@@ -48,6 +50,7 @@ def __init__(
4850
self._cell_data_array_names: list[ str ] = []
4951
self._source = source
5052
self._pl = pv.Plotter()
53+
self._pl.iren.initialize()
5154
self._mesh_actor: vtkActor | None = None
5255

5356
self.CUT_PLANE = "on_cut_plane_visibility_change"
@@ -59,6 +62,7 @@ def __init__(
5962
self.SELECTED_DATA_ARRAY = "viewer_selected_data_array"
6063
self.state.change( self.SELECTED_DATA_ARRAY )( self._update_actor_array )
6164

65+
self.box_engine: BoxViewer | None = None
6266
self.region_engine = region_viewer
6367
self.well_engine = well_viewer
6468
self._perforations: dict[ str, PerforationViewer ] = {}
@@ -122,7 +126,7 @@ def rendering_menu_extra_items( self ) -> None:
122126
def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) -> None:
123127
"""Add from path the dataset given by the user.
124128
125-
Supported data type is: Vtkwell, Vtkmesh, InternalWell, Perforation.
129+
Supported data type is: Vtkwell, Vtkmesh, InternalWell, Perforation, Box.
126130
127131
object_state : array used to store path to the data and if we want to show it or not.
128132
"""
@@ -138,6 +142,13 @@ def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) ->
138142
if isinstance( active_block, Perforation ):
139143
self._update_perforation( active_block, show_obj, path )
140144

145+
if isinstance( active_block, Box ):
146+
self._update_box( active_block, show_obj )
147+
148+
# when data is added in the pv.Plotter, we need to refresh the scene to update
149+
# the actor to avoid LUT issue.
150+
self.plotter.update()
151+
141152
def _on_clip_visibility_change( self, **kwargs: Any ) -> None:
142153
"""Toggle cut plane visibility for all actors.
143154
@@ -215,6 +226,7 @@ def _update_internalwell( self, path: str, show: bool ) -> None:
215226
"""
216227
if not show:
217228
self.plotter.remove_actor( self.well_engine.get_actor( path ) ) # type: ignore
229+
self.well_engine.remove_actor( path )
218230
return
219231

220232
tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
@@ -229,6 +241,7 @@ def _update_vtkwell( self, path: str, show: bool ) -> None:
229241
"""
230242
if not show:
231243
self.plotter.remove_actor( self.well_engine.get_actor( path ) ) # type: ignore
244+
self.well_engine.remove_actor( path )
232245
return
233246

234247
tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
@@ -331,3 +344,33 @@ def _add_perforation( self, distance_from_head: float, path: str ) -> None:
331344
saved_perforation.add_extracted_cell( cell_actor )
332345

333346
self._perforations[ path ] = saved_perforation
347+
348+
def _update_box( self, active_block: Box, show_obj: bool ) -> None:
349+
"""Generate and display a Box and inner cell(s) from the mesh."""
350+
if self.region_engine.input.number_of_cells == 0 and show_obj:
351+
self.ctrl.on_add_warning(
352+
"Can't display " + active_block.name,
353+
"Please display the mesh before creating a well",
354+
)
355+
return
356+
357+
if self.box_engine is not None:
358+
box_polydata_actor: pv.Actor = self.box_engine.get_box_polydata_actor()
359+
extracted_cell_actor: pv.Actor = self.box_engine.get_extracted_cells_actor()
360+
self.plotter.remove_actor( box_polydata_actor )
361+
self.plotter.remove_actor( extracted_cell_actor )
362+
363+
if not show_obj:
364+
return
365+
366+
box: Box = active_block
367+
self.box_engine = BoxViewer( self.region_engine.input, box )
368+
369+
box_polydata: pv.PolyData = self.box_engine.get_box_polydata()
370+
extracted_cell: pv.UnstructuredGrid = self.box_engine.get_extracted_cells()
371+
372+
if box_polydata is not None and extracted_cell is not None:
373+
_box_polydata_actor = self.plotter.add_mesh( box_polydata, opacity=0.2 )
374+
_extracted_cells_actor = self.plotter.add_mesh( extracted_cell, show_edges=True )
375+
self.box_engine.set_box_polydata_actor( _box_polydata_actor )
376+
self.box_engine.set_extracted_cells_actor( _extracted_cells_actor )

0 commit comments

Comments
 (0)