Skip to content

Commit 5c0fe44

Browse files
feat: add scalar array selection in geos-trame (#103)
* feat: data array selection * tests: add test data * tests: add data_loader test * tests: change random arrays test data to small --------- Co-authored-by: Jean Fechter <[email protected]>
1 parent c9b4b44 commit 5c0fe44

File tree

9 files changed

+497
-49
lines changed

9 files changed

+497
-49
lines changed

geos-trame/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies = [
4141
"trame-gantt==0.1.5",
4242
"xsdata==24.5",
4343
"xsdata-pydantic[lxml]==24.5",
44-
"pyvista==0.44.1",
44+
"pyvista==0.45.2",
4545
"dpath==2.2.0",
4646
"colorcet==3.1.0",
4747
"funcy==2.0",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def __init__( self ) -> None:
2626

2727
self.state.alerts = []
2828

29-
self.server.controller.on_add_error.add_task( self.add_error )
30-
self.server.controller.on_add_warning.add_task( self.add_warning )
29+
self.ctrl.on_add_error.add_task( self.add_error )
30+
self.ctrl.on_add_warning.add_task( self.add_warning )
3131

3232
self.generate_alert_ui()
3333

geos-trame/src/geos/trame/app/io/data_loader.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from geos.trame.app.geosTrameException import GeosTrameException
1212
from geos.trame.app.ui.viewer.regionViewer import RegionViewer
1313
from geos.trame.app.ui.viewer.wellViewer import WellViewer
14-
from geos.trame.app.utils.pv_utils import read_unstructured_grid
14+
from geos.trame.app.utils.pv_utils import read_unstructured_grid, split_vector_arrays
1515
from geos.trame.schema_generated.schema_mod import (
1616
Vtkmesh,
1717
Vtkwell,
@@ -97,6 +97,9 @@ def _update_vtkmesh( self, mesh: Vtkmesh, show: bool ) -> None:
9797

9898
def _read_mesh( self, mesh: Vtkmesh ) -> None:
9999
unstructured_grid = read_unstructured_grid( self.source.get_abs_path( mesh.file ) )
100+
split_vector_arrays( unstructured_grid )
101+
102+
unstructured_grid.set_active_scalars( unstructured_grid.cell_data.keys()[ 0 ] )
100103
self.region_viewer.add_mesh( unstructured_grid )
101104

102105
def _update_vtkwell( self, well: Vtkwell, path: str, show: bool ) -> None:

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ def __init__( self ) -> None:
1313
"""
1414
self.input = pv.UnstructuredGrid()
1515
self.clip = self.input
16-
self.reset()
17-
18-
def __call__( self, normal: tuple[ float ], origin: tuple[ float ] ) -> None:
19-
"""Update clip."""
20-
self.update_clip( normal, origin )
2116

2217
def add_mesh( self, mesh: pv.UnstructuredGrid ) -> None:
2318
"""Set the input to the given mesh."""
@@ -26,7 +21,7 @@ def add_mesh( self, mesh: pv.UnstructuredGrid ) -> None:
2621

2722
def update_clip( self, normal: tuple[ float ], origin: tuple[ float ] ) -> None:
2823
"""Update the current clip with the given normal and origin."""
29-
self.clip.copy_from( self.input.clip( normal=normal, origin=origin, crinkle=True ) ) # type: ignore
24+
self.clip = self.input.clip( normal=normal, origin=origin, crinkle=True ) # type: ignore
3025

3126
def reset( self ) -> None:
3227
"""Reset the input mesh and clip."""

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

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@
1414
from geos.trame.app.ui.viewer.perforationViewer import PerforationViewer
1515
from geos.trame.app.ui.viewer.regionViewer import RegionViewer
1616
from geos.trame.app.ui.viewer.wellViewer import WellViewer
17-
from geos.trame.schema_generated.schema_mod import (
18-
Vtkmesh,
19-
Vtkwell,
20-
Perforation,
21-
InternalWell,
22-
)
17+
from geos.trame.schema_generated.schema_mod import Vtkmesh, Vtkwell, InternalWell, Perforation
2318

2419
pv.OFF_SCREEN = True
2520

@@ -49,13 +44,20 @@ def __init__(
4944
"""
5045
super().__init__( **kwargs )
5146

47+
self._point_data_array_names: list[ str ] = []
48+
self._cell_data_array_names: list[ str ] = []
5249
self._source = source
5350
self._pl = pv.Plotter()
51+
self._mesh_actor: vtkActor | None = None
5452

5553
self.CUT_PLANE = "on_cut_plane_visibility_change"
5654
self.ZAMPLIFICATION = "_z_amplification"
57-
self.server.state[ self.CUT_PLANE ] = True
58-
self.server.state[ self.ZAMPLIFICATION ] = 1
55+
self.state[ self.CUT_PLANE ] = True
56+
self.state[ self.ZAMPLIFICATION ] = 1
57+
58+
self.DATA_ARRAYS = "viewer_data_arrays_items"
59+
self.SELECTED_DATA_ARRAY = "viewer_selected_data_array"
60+
self.state.change( self.SELECTED_DATA_ARRAY )( self._update_actor_array )
5961

6062
self.region_engine = region_viewer
6163
self.well_engine = well_viewer
@@ -68,8 +70,9 @@ def __init__(
6870
view = plotter_ui(
6971
self._pl,
7072
add_menu_items=self.rendering_menu_extra_items,
71-
style="position: absolute;",
7273
)
74+
view.menu.style += "; height: 50px; min-width: 50px;"
75+
view.menu.children[ 0 ].style += "; justify-content: center;"
7376
self.ctrl.view_update = view.update
7477

7578
@property
@@ -88,21 +91,33 @@ def rendering_menu_extra_items( self ) -> None:
8891
For now, adding a button to show/hide all widgets.
8992
"""
9093
self.state.change( self.CUT_PLANE )( self._on_clip_visibility_change )
91-
vuetify.VDivider( vertical=True, classes="mr-1" )
92-
with vuetify.VTooltip( location="bottom" ):
93-
with (
94-
vuetify.Template( v_slot_activator=( "{ props }", ) ),
95-
html.Div( v_bind=( "props", ) ),
96-
):
97-
vuetify.VCheckbox(
98-
v_model=( self.CUT_PLANE, True ),
99-
icon=True,
100-
true_icon="mdi-eye",
101-
false_icon="mdi-eye-off",
102-
dense=True,
103-
hide_details=True,
104-
)
105-
html.Span( "Show/Hide widgets" )
94+
with vuetify.VRow(
95+
classes='pa-0 ma-0 align-center fill-height',
96+
style="flex-wrap: nowrap",
97+
):
98+
vuetify.VDivider( vertical=True, classes="mr-1" )
99+
with vuetify.VTooltip( location="bottom" ):
100+
with (
101+
vuetify.Template( v_slot_activator=( "{ props }", ) ),
102+
html.Div( v_bind=( "props", ) ),
103+
):
104+
vuetify.VCheckbox(
105+
v_model=( self.CUT_PLANE, True ),
106+
icon=True,
107+
true_icon="mdi-eye",
108+
false_icon="mdi-eye-off",
109+
dense=True,
110+
hide_details=True,
111+
)
112+
html.Span( "Show/Hide widgets" )
113+
vuetify.VDivider( vertical=True, classes="mr-1" )
114+
vuetify.VSelect(
115+
hide_details=True,
116+
label="Data Array",
117+
items=( self.DATA_ARRAYS, [] ),
118+
v_model=( self.SELECTED_DATA_ARRAY, None ),
119+
min_width="150px",
120+
)
106121

107122
def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) -> None:
108123
"""Add from path the dataset given by the user.
@@ -205,7 +220,7 @@ def _update_internalwell( self, path: str, show: bool ) -> None:
205220
tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
206221
self.well_engine.append_actor( path, tube_actor )
207222

208-
self.server.controller.view_update()
223+
self.ctrl.view_update()
209224

210225
def _update_vtkwell( self, path: str, show: bool ) -> None:
211226
"""Used to control the visibility of the Vtkwell.
@@ -219,7 +234,30 @@ def _update_vtkwell( self, path: str, show: bool ) -> None:
219234
tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
220235
self.well_engine.append_actor( path, tube_actor )
221236

222-
self.server.controller.view_update()
237+
self.ctrl.view_update()
238+
239+
def _clip_mesh( self, normal: tuple[ float ], origin: tuple[ float ] ) -> None:
240+
"""Plane widget callback to clip the input data."""
241+
if self._mesh_actor is None:
242+
return
243+
self.region_engine.update_clip( normal=normal, origin=origin )
244+
self._mesh_actor.mapper.SetInputData( self.region_engine.clip )
245+
self._update_actor_array()
246+
247+
def _update_actor_array( self, **_: Any ) -> None:
248+
"""Update the actor scalar array."""
249+
array_name = self.state[ self.SELECTED_DATA_ARRAY ]
250+
if array_name is None or self._mesh_actor is None:
251+
return
252+
mapper: pv.DataSetMapper = self._mesh_actor.mapper
253+
254+
mapper.array_name = array_name
255+
mapper.scalar_range = self.region_engine.clip.get_data_range( array_name )
256+
self.region_engine.clip.active_scalars_name = array_name
257+
mapper.scalar_map_mode = "point" if array_name in self._point_data_array_names else "cell"
258+
259+
self.plotter.scalar_bar.title = array_name
260+
self.ctrl.view_update()
223261

224262
def _update_vtkmesh( self, show: bool ) -> None:
225263
"""Used to control the visibility of the Vtkmesh.
@@ -230,21 +268,22 @@ def _update_vtkmesh( self, show: bool ) -> None:
230268
"""
231269
if not show:
232270
self.plotter.clear_plane_widgets()
233-
self.plotter.remove_actor( self._clip_mesh ) # type: ignore
271+
self.plotter.remove_actor( self._mesh_actor ) # type: ignore
272+
self._mesh_actor = None
234273
return
235274

236-
active_scalar = self.region_engine.input.active_scalars_name
237-
self._clip_mesh: vtkActor = self.plotter.add_mesh_clip_plane(
238-
self.region_engine.input,
239-
origin=self.region_engine.input.center,
240-
normal=[ -1, 0, 0 ],
241-
crinkle=True,
242-
show_edges=False,
243-
cmap="glasbey_bw",
244-
scalars=active_scalar,
245-
)
246-
247-
self.server.controller.view_update()
275+
self._point_data_array_names = list( self.region_engine.input.point_data.keys() )
276+
self._cell_data_array_names = list( self.region_engine.input.cell_data.keys() )
277+
self.state[ self.DATA_ARRAYS ] = self._point_data_array_names + self._cell_data_array_names
278+
self.state[ self.SELECTED_DATA_ARRAY ] = self.region_engine.input.active_scalars_name
279+
280+
self._mesh_actor = self.plotter.add_mesh( self.region_engine.input )
281+
self.plotter.add_plane_widget( callback=self._clip_mesh,
282+
normal=[ 1, 0, 0 ],
283+
origin=self.region_engine.input.center,
284+
assign_to_axis=None,
285+
tubing=False,
286+
outline_translation=False )
248287

249288
def _update_perforation( self, perforation: Perforation, show: bool, path: str ) -> None:
250289
"""Generate VTK dataset from a perforation."""

geos-trame/src/geos/trame/app/utils/pv_utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,27 @@
22
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
33
# SPDX-FileContributor: Kitware
44
import pyvista as pv
5+
from vtkmodules.util.numpy_support import vtk_to_numpy, numpy_to_vtk
6+
from vtkmodules.vtkCommonCore import vtkDataArray
57

68

79
def read_unstructured_grid( filename: str ) -> pv.UnstructuredGrid:
810
"""Read an unstructured grid from a .vtu file."""
911
return pv.read( filename ).cast_to_unstructured_grid()
12+
13+
14+
def split_vector_arrays( ug: pv.UnstructuredGrid ) -> None:
15+
"""Create N 1-component arrays from each vector array with N components."""
16+
for data in [ ug.GetPointData(), ug.GetCellData() ]:
17+
for i in range( data.GetNumberOfArrays() ):
18+
array: vtkDataArray = data.GetArray( i )
19+
if array.GetNumberOfComponents() != 1:
20+
np_array = vtk_to_numpy( array )
21+
array_name = array.GetName()
22+
data.RemoveArray( array_name )
23+
for comp in range( array.GetNumberOfComponents() ):
24+
component = np_array[ :, comp ]
25+
new_array_name = f"{array_name}_{comp}"
26+
new_array = numpy_to_vtk( component, deep=True )
27+
new_array.SetName( new_array_name )
28+
data.AddArray( new_array )

0 commit comments

Comments
 (0)