From 782c8277a32ae350853d250d407dfb9dcb26af52 Mon Sep 17 00:00:00 2001 From: Lucas Givord Date: Wed, 4 Jun 2025 09:41:51 +0200 Subject: [PATCH 01/17] feat(viewer): add support for intersected cell from Box --- .../geos/trame/app/data_types/renderable.py | 1 + .../src/geos/trame/app/ui/viewer/boxViewer.py | 111 ++++++++++++++++++ .../src/geos/trame/app/ui/viewer/viewer.py | 22 +++- geos-trame/tests/data/geosDeck/geosDeck.xml | 4 +- geos-trame/tests/test_box_intersection.py | 36 ++++++ 5 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py create mode 100644 geos-trame/tests/test_box_intersection.py diff --git a/geos-trame/src/geos/trame/app/data_types/renderable.py b/geos-trame/src/geos/trame/app/data_types/renderable.py index e0312401d..af26a6253 100644 --- a/geos-trame/src/geos/trame/app/data_types/renderable.py +++ b/geos-trame/src/geos/trame/app/data_types/renderable.py @@ -6,6 +6,7 @@ class Renderable( Enum ): """Enum class for renderable types and their ids.""" + BOX = "Box" VTKMESH = "VTKMesh" INTERNALMESH = "InternalMesh" INTERNALWELL = "InternalWell" diff --git a/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py b/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py new file mode 100644 index 000000000..05b59f106 --- /dev/null +++ b/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py @@ -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: + + + + 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 diff --git a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py index 80642046f..e3c06d0a7 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py @@ -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 @@ -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 ] = {} @@ -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. diff --git a/geos-trame/tests/data/geosDeck/geosDeck.xml b/geos-trame/tests/data/geosDeck/geosDeck.xml index a4a7de8ef..efff1d45c 100644 --- a/geos-trame/tests/data/geosDeck/geosDeck.xml +++ b/geos-trame/tests/data/geosDeck/geosDeck.xml @@ -127,8 +127,8 @@ + xMin="{ 3509, 4117, -596 }" + xMax="{ 4482, 5041, -500 }"/> diff --git a/geos-trame/tests/test_box_intersection.py b/geos-trame/tests/test_box_intersection.py new file mode 100644 index 000000000..22b39f00c --- /dev/null +++ b/geos-trame/tests/test_box_intersection.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies. +# SPDX-FileContributor: Kitware +# ruff: noqa +from pathlib import Path + +from trame_server import Server +from trame_server.state import State +from trame_vuetify.ui.vuetify3 import VAppLayout + +from geos.trame.app.core import GeosTrame +from tests.trame_fixtures import trame_state, trame_server_layout + + +def test_box_intersection( trame_server_layout: tuple[ Server, VAppLayout ] ) -> None: + """Test box intersection.""" + root_path = Path( __file__ ).parent.absolute().__str__() + file_name = root_path + "/data/geosDeck/geosDeck.xml" + + app = GeosTrame( trame_server_layout[ 0 ], file_name ) + app.state.ready() + + app.deckInspector.state.object_state = [ "Problem/Mesh/0/VTKMesh/0", True ] + app.deckInspector.state.flush() + + app.deckInspector.state.object_state = [ "Problem/Geometry/0/Box/0", True ] + app.deckInspector.state.flush() + + box = app.deckViewer.box_engine._box + cells = app.deckViewer.box_engine._extracted_cell + + assert box is not None + assert box.x_min == '{ 3509, 4117, -596 }' + assert box.x_max == '{ 4482, 5041, -500 }' + assert cells is not None + assert cells.number_of_cells == 1 From e261d68379c00f2ec26dd0a607d096fa74f3666a Mon Sep 17 00:00:00 2001 From: Lucas Givord Date: Wed, 4 Jun 2025 09:47:31 +0200 Subject: [PATCH 02/17] feat(alert): add success alert type for the saving feature --- .../geos/trame/app/components/alertHandler.py | 24 ++++++++++++++++--- geos-trame/src/geos/trame/app/core.py | 2 +- geos-trame/src/geos/trame/app/deck/tree.py | 13 ++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/geos-trame/src/geos/trame/app/components/alertHandler.py b/geos-trame/src/geos/trame/app/components/alertHandler.py index 230b041fb..ea206d60b 100644 --- a/geos-trame/src/geos/trame/app/components/alertHandler.py +++ b/geos-trame/src/geos/trame/app/components/alertHandler.py @@ -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. @@ -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() @@ -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: @@ -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 ) ) diff --git a/geos-trame/src/geos/trame/app/core.py b/geos-trame/src/geos/trame/app/core.py index 1f66b96ab..0a8f40973 100644 --- a/geos-trame/src/geos/trame/app/core.py +++ b/geos-trame/src/geos/trame/app/core.py @@ -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() diff --git a/geos-trame/src/geos/trame/app/deck/tree.py b/geos-trame/src/geos/trame/app/deck/tree.py index 4979f3bec..b732ddea1 100644 --- a/geos-trame/src/geos/trame/app/deck/tree.py +++ b/geos-trame/src/geos/trame/app/deck/tree.py @@ -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 ) @@ -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. @@ -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. From 6adbb9b138830e8022547b5f7a12a27527a26d4e Mon Sep 17 00:00:00 2001 From: Lucas Givord Date: Tue, 8 Jul 2025 14:11:12 +0200 Subject: [PATCH 03/17] fix: well actor wasn't remove correctly --- .../src/geos/trame/app/ui/viewer/viewer.py | 2 + .../geos/trame/app/ui/viewer/wellViewer.py | 39 ++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py index e3c06d0a7..fea98c0b3 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py @@ -235,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() ) ) @@ -249,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() ) ) diff --git a/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py b/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py index 4196f7b87..237d50ca0 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py @@ -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 @@ -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 @@ -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 @@ -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.""" @@ -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 @@ -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 ) From 41d305472e11e143eeeca89f062a3baf9e2cf0bc Mon Sep 17 00:00:00 2001 From: Lucas Givord Date: Tue, 22 Jul 2025 11:38:06 +0200 Subject: [PATCH 04/17] fix: update data added in the plotter Previously user needs to trigger a render call to fix a bad scalar array/lut when a new data was added in the pyvista.Plotter. We enforce it when we trigger the 'update_viewer' method. --- geos-trame/src/geos/trame/app/ui/viewer/viewer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py index fea98c0b3..be6bdecbc 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py @@ -158,6 +158,9 @@ def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) -> self.box_engine = BoxViewer( self.region_engine.input, box ) self.box_engine.append_to_plotter( self.plotter ) + # make sure that added data in the plot will be rendered with the correct scalar array/lut + self.plotter.update() + def _on_clip_visibility_change( self, **kwargs: Any ) -> None: """Toggle cut plane visibility for all actors. From 79b26e7c525953590715582fc689184e4eb21871 Mon Sep 17 00:00:00 2001 From: Lucas Givord Date: Tue, 22 Jul 2025 15:52:23 +0200 Subject: [PATCH 05/17] viewer: remove Plotter dependency to the box engine --- .../src/geos/trame/app/ui/viewer/boxViewer.py | 39 +++++++------ .../src/geos/trame/app/ui/viewer/viewer.py | 55 ++++++++++++------- geos-trame/tests/test_box_intersection.py | 2 +- 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py b/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py index 05b59f106..71046b83d 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py @@ -22,32 +22,35 @@ def __init__( self, mesh: pv.UnstructuredGrid, box: Box ) -> None: 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._extracted_cells: pv.UnstructuredGrid = None + self._extracted_cells_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. + def get_box_polydata( self ) -> pv.PolyData | None: + """Get the box polydata.""" + return self._box_polydata - The box is represented as a polydata with a low opacity. - """ - self._box_polydata_actor = plotter.add_mesh( self._box_polydata, opacity=0.2 ) + 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 - if self._extracted_cell is not None: - self._extracted_cell_actor = plotter.add_mesh( self._extracted_cell, show_edges=True ) + def get_extracted_cells( self ) -> pv.UnstructuredGrid | None: + """Get the extracted cell polydata.""" + return self._extracted_cells - 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 ) + def get_extracted_cells_actor( self ) -> pv.Actor | None: + """Get the extracted cell polydata actor.""" + return self._extracted_cells_actor - if self._extracted_cell_actor is not None: - plotter.remove_actor( self._extracted_cell_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 - self._box_polydata = None - self._extracted_cell = None + 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.""" @@ -95,7 +98,7 @@ def _compute_intersected_cell( self ) -> None: saved_ids.append( id ) if len( saved_ids ) > 0: - self._extracted_cell = self._mesh.extract_cells( saved_ids ) + 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. diff --git a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py index be6bdecbc..204dc4ac6 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py @@ -36,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). @@ -124,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. """ @@ -141,24 +142,10 @@ def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) -> 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 + self._update_box( active_block, show_obj ) - 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 ) - - # make sure that added data in the plot will be rendered with the correct scalar array/lut + # 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: @@ -353,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 ) diff --git a/geos-trame/tests/test_box_intersection.py b/geos-trame/tests/test_box_intersection.py index 22b39f00c..06db5df41 100644 --- a/geos-trame/tests/test_box_intersection.py +++ b/geos-trame/tests/test_box_intersection.py @@ -27,7 +27,7 @@ def test_box_intersection( trame_server_layout: tuple[ Server, VAppLayout ] ) -> app.deckInspector.state.flush() box = app.deckViewer.box_engine._box - cells = app.deckViewer.box_engine._extracted_cell + cells = app.deckViewer.box_engine._extracted_cells assert box is not None assert box.x_min == '{ 3509, 4117, -596 }' From 1d8c975e1b1b73891ca32cfda0357d6996d9dd8d Mon Sep 17 00:00:00 2001 From: Paloma Martinez <104762252+paloma-martinez@users.noreply.github.com> Date: Thu, 30 Oct 2025 17:44:35 +0100 Subject: [PATCH 06/17] Fix typo --- geos-trame/src/geos/trame/app/ui/viewer/viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py index 204dc4ac6..2536c8d7e 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py @@ -340,7 +340,7 @@ 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_cells( cell_actor ) + saved_perforation.add_extracted_cell( cell_actor ) self._perforations[ path ] = saved_perforation From 8896a8807831d32fcf82f42c17341d3ece8ad471 Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:48:04 +0100 Subject: [PATCH 07/17] Update python-package.yml --- .github/workflows/python-package.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8ae0563e8..f46dc1e0e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -71,6 +71,12 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' + + - name: Install OSMesa + run: | + sudo apt-get update + sudo apt-get install -y libosmesa6-dev libgl1-mesa-glx libglu1-mesa xvfb + - name: Install package # working-directory: ./${{ matrix.package-name }} run: | @@ -94,8 +100,10 @@ jobs: run: | yapf -r --diff ./${{ matrix.package-name }} --style .style.yapf - name: Test with pytest + env: + PYVISTA_OFF_SCREEN: true #working-directory: ./${{ matrix.package-name }} run: # python -m pytest ./${{ matrix.package-name }} --doctest-modules --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html | # wrap pytest to avoid error when no tests in the package - sh -c 'python -m pytest ./${{ matrix.package-name }}; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret' \ No newline at end of file + sh -c 'python -m pytest ./${{ matrix.package-name }}; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret' From 560518da5b22a9b871a18dbb21b28253e2c44e5c Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:03:39 +0100 Subject: [PATCH 08/17] Update python-package.yml --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2cb9428bd..3869e370b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -96,7 +96,7 @@ jobs: - name: Install OSMesa run: | sudo apt-get update - sudo apt-get install -y libosmesa6-dev libgl1-mesa-glx libglu1-mesa xvfb + sudo apt-get install -y libosmesa6 - name: Install package # working-directory: ./${{ matrix.package-name }} From a2a7a638a2ab6303f05dcb38c919052e38009480 Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:14:46 +0100 Subject: [PATCH 09/17] Update python-package.yml --- .github/workflows/python-package.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3869e370b..5835c55c8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -93,10 +93,16 @@ jobs: python-version: ${{ matrix.python-version }} cache: 'pip' - - name: Install OSMesa + - name: Install OSMesa and GL run: | sudo apt-get update - sudo apt-get install -y libosmesa6 + sudo apt-get install -y libosmesa6 + sudo apt-get install -y \ + libegl1-mesa-dev \ + libgles2-mesa-dev \ + libgl1-mesa-dev \ + libx11-dev \ + libxext-dev - name: Install package # working-directory: ./${{ matrix.package-name }} From ab01e6ae098ea406d2763013f348e73cd0f47c98 Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:49:34 +0000 Subject: [PATCH 10/17] fix pyvista init in test --- .github/workflows/python-package.yml | 22 +++++++++---------- .../src/geos/trame/app/ui/viewer/viewer.py | 1 + 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5835c55c8..fc7be3ecd 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -93,16 +93,16 @@ jobs: python-version: ${{ matrix.python-version }} cache: 'pip' - - name: Install OSMesa and GL - run: | - sudo apt-get update - sudo apt-get install -y libosmesa6 - sudo apt-get install -y \ - libegl1-mesa-dev \ - libgles2-mesa-dev \ - libgl1-mesa-dev \ - libx11-dev \ - libxext-dev + # - name: Install OSMesa and GL + # run: | + # sudo apt-get update + # sudo apt-get install -y libosmesa6 + # sudo apt-get install -y \ + # libegl1-mesa-dev \ + # libgles2-mesa-dev \ + # libgl1-mesa-dev \ + # libx11-dev \ + # libxext-dev - name: Install package # working-directory: ./${{ matrix.package-name }} @@ -126,8 +126,6 @@ jobs: run: | yapf -r --diff ./${{ matrix.package-name }} --style .style.yapf - name: Test with pytest - env: - PYVISTA_OFF_SCREEN: true #working-directory: ./${{ matrix.package-name }} run: # python -m pytest ./${{ matrix.package-name }} --doctest-modules --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html | diff --git a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py index 2536c8d7e..252e793e2 100644 --- a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py +++ b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py @@ -50,6 +50,7 @@ def __init__( self._cell_data_array_names: list[ str ] = [] self._source = source self._pl = pv.Plotter() + self._pl.iren.initialize() self._mesh_actor: vtkActor | None = None self.CUT_PLANE = "on_cut_plane_visibility_change" From 348f10064a7dcd08ff3bd012c4f908bf0ccce1d7 Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 3 Nov 2025 08:59:53 +0100 Subject: [PATCH 11/17] debug CI --- .github/workflows/python-package.yml | 52 ++++++++++++++++------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fc7be3ecd..94db536fc 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -57,20 +57,20 @@ jobs: fail-fast: false max-parallel: 3 matrix: - python-version: ["3.10","3.11","3.12"] + python-version: ["3.12"] package-name: - - geos-ats - - geos-utils - - geos-geomechanics - - geos-mesh - - geos-posp - - geos-processing - - geos-timehistory + # - geos-ats + # - geos-utils + # - geos-geomechanics + # - geos-mesh + # - geos-posp + # - geos-processing + # - geos-timehistory - geos-trame - - geos-xml-tools - - geos-xml-viewer - - hdf5-wrapper - - pygeos-tools + # - geos-xml-tools + # - geos-xml-viewer + # - hdf5-wrapper + # - pygeos-tools include: - package-name: geos-geomechanics dependencies: "geos-utils" @@ -93,16 +93,24 @@ jobs: python-version: ${{ matrix.python-version }} cache: 'pip' - # - name: Install OSMesa and GL - # run: | - # sudo apt-get update - # sudo apt-get install -y libosmesa6 - # sudo apt-get install -y \ - # libegl1-mesa-dev \ - # libgles2-mesa-dev \ - # libgl1-mesa-dev \ - # libx11-dev \ - # libxext-dev + - name: Install OSMesa and GL + run: | + sudo apt-get update + sudo apt-get install -y libosmesa6 + sudo apt-get install -y \ + libegl1-mesa-dev \ + libgles2-mesa-dev \ + libgl1-mesa-dev \ + libx11-dev \ + libxext-dev + + - name: ssh + uses: lhotari/action-upterm@v1 + with: +# limits ssh access and adds the ssh public key for the user which triggered the workflow + limit-access-to-actor: true +# limits ssh access and adds the ssh public keys of the listed GitHub users + limit-access-to-users: GitHubLogin - name: Install package # working-directory: ./${{ matrix.package-name }} From 5e7334344a7e805ca95970f16a765569ebcb1d2a Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:19:23 +0100 Subject: [PATCH 12/17] debug CI --- .github/workflows/python-package.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 94db536fc..cd8fbae3f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -104,14 +104,6 @@ jobs: libx11-dev \ libxext-dev - - name: ssh - uses: lhotari/action-upterm@v1 - with: -# limits ssh access and adds the ssh public key for the user which triggered the workflow - limit-access-to-actor: true -# limits ssh access and adds the ssh public keys of the listed GitHub users - limit-access-to-users: GitHubLogin - - name: Install package # working-directory: ./${{ matrix.package-name }} run: | @@ -140,6 +132,14 @@ jobs: # wrap pytest to avoid error when no tests in the package sh -c 'python -m pytest ./${{ matrix.package-name }}; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret' + - name: ssh + uses: lhotari/action-upterm@v1 + with: +# limits ssh access and adds the ssh public key for the user which triggered the workflow + limit-access-to-actor: true +# limits ssh access and adds the ssh public keys of the listed GitHub users + limit-access-to-users: GitHubLogin + # check if GEOS has label for testing GEOS integration check_integration_label: runs-on: ubuntu-latest From 0fed0625504f68112605321d0d7189bb01b24a0f Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:29:56 +0100 Subject: [PATCH 13/17] debug CI --- .github/workflows/python-package.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index cd8fbae3f..0ffe5e660 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -130,7 +130,8 @@ jobs: run: # python -m pytest ./${{ matrix.package-name }} --doctest-modules --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html | # wrap pytest to avoid error when no tests in the package - sh -c 'python -m pytest ./${{ matrix.package-name }}; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret' + # sh -c 'python -m pytest ./${{ matrix.package-name }}; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret + sh -c 'python -m pytest ./${{ matrix.package-name }};' - name: ssh uses: lhotari/action-upterm@v1 From 7efbf7f3c4da63b241a73b2ddda63f284cb9eca7 Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:52:07 +0100 Subject: [PATCH 14/17] debug CI --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0ffe5e660..57eed6775 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -137,9 +137,9 @@ jobs: uses: lhotari/action-upterm@v1 with: # limits ssh access and adds the ssh public key for the user which triggered the workflow - limit-access-to-actor: true + limit-access-to-actor: false # limits ssh access and adds the ssh public keys of the listed GitHub users - limit-access-to-users: GitHubLogin + # limit-access-to-users: GitHubLogin # check if GEOS has label for testing GEOS integration check_integration_label: From fb38c366d638de862ba48169af3db64676069c9b Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:06:24 +0100 Subject: [PATCH 15/17] debug CI --- .github/workflows/python-package.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 57eed6775..bcc237b60 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -130,16 +130,7 @@ jobs: run: # python -m pytest ./${{ matrix.package-name }} --doctest-modules --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html | # wrap pytest to avoid error when no tests in the package - # sh -c 'python -m pytest ./${{ matrix.package-name }}; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret - sh -c 'python -m pytest ./${{ matrix.package-name }};' - - - name: ssh - uses: lhotari/action-upterm@v1 - with: -# limits ssh access and adds the ssh public key for the user which triggered the workflow - limit-access-to-actor: false -# limits ssh access and adds the ssh public keys of the listed GitHub users - # limit-access-to-users: GitHubLogin + sh -c 'python -m pytest ./${{ matrix.package-name }}; ret=$?; [ $ret = 5 ] && exit 0 || exit $ret' # check if GEOS has label for testing GEOS integration check_integration_label: From d45b0b919d13dcc052cb30aab437bcde65c30f5c Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:16:30 +0100 Subject: [PATCH 16/17] clean up CI and restore all tests --- .github/workflows/python-package.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bcc237b60..377f67e8a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -59,18 +59,18 @@ jobs: matrix: python-version: ["3.12"] package-name: - # - geos-ats - # - geos-utils - # - geos-geomechanics - # - geos-mesh - # - geos-posp - # - geos-processing - # - geos-timehistory + - geos-ats + - geos-utils + - geos-geomechanics + - geos-mesh + - geos-posp + - geos-processing + - geos-timehistory - geos-trame - # - geos-xml-tools - # - geos-xml-viewer - # - hdf5-wrapper - # - pygeos-tools + - geos-xml-tools + - geos-xml-viewer + - hdf5-wrapper + - pygeos-tools include: - package-name: geos-geomechanics dependencies: "geos-utils" From da1cf5b90907530b2a98c4b4497ef1c3acdcf591 Mon Sep 17 00:00:00 2001 From: Jacques Franc <49998870+jafranc@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:47:04 +0100 Subject: [PATCH 17/17] restore all python version looks like workflow edition trigger all test-integration. But could be justified --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 377f67e8a..10d5e5130 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -57,7 +57,7 @@ jobs: fail-fast: false max-parallel: 3 matrix: - python-version: ["3.12"] + python-version: ["3.10","3.11","3.12"] package-name: - geos-ats - geos-utils