Skip to content

Commit 06f477e

Browse files
authored
feat: visualize well intersection in geos-trame (#83)
* feat(wells): add visualization and interaction with well Improve DeckViewer to be able to show mesh (Region), wells (InternalWell, Vtkmesh) and perforation (Perforation) Update also the gui to be able to hide and control radius property of these datas. * fix(inspector): fix checking of if an item is renderable or not * fix(polyline): make sure to generate a polyline based on connectivity Previously we didn't take in account the connectivty. The geosDeck used in the test has been updated to check this fix. * feat(alert) : add alert system Adding an alert system to notify user about error. It is used in the viewer.py to notify the user about missing dataset mandaotry to read a well or a perforation * fix(IO): support file which doesn't contain an Include tag * test(well): check well intersection feature for InternalWell andVtkmesh * test(well) : reuse singlePhaseFlow dataset To avoid LFS storage limit, test_well_intersection will reuse the mesh from the other deck xml.
1 parent 9a3c489 commit 06f477e

21 files changed

Lines changed: 1206 additions & 339 deletions

geos-trame/src/geos_trame/app/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ def main( server=None, **kwargs ):
3333

3434
app = GeosTrame( server, file_name )
3535
app.server.start( **kwargs )
36+
37+
38+
if __name__ == "__main__":
39+
main()

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from geos_trame.app.ui.inspector import DeckInspector
1414
from geos_trame.app.ui.plotting import DeckPlotting
1515
from geos_trame.app.ui.timeline import TimelineEditor
16-
from geos_trame.app.ui.viewer import DeckViewer
16+
from geos_trame.app.ui.viewer.viewer import DeckViewer
17+
from geos_trame.app.ui.alertHandler import AlertHandler
1718

1819
import sys
1920

@@ -114,6 +115,8 @@ def build_ui( self, *args, **kwargs ):
114115
with VAppLayout( self.server ) as layout:
115116
self.simput_widget.register_layout( layout )
116117

118+
self.alertHandler = AlertHandler()
119+
117120
def on_tab_change( tab_idx ):
118121
pass
119122

@@ -174,8 +177,11 @@ def on_tab_change( tab_idx ):
174177
if self.tree.input_file is not None:
175178
self.deck_ui()
176179
else:
177-
180+
self.ctrl.on_add_error(
181+
"Error",
182+
"The file " + self.state.input_file + " cannot be parsed.",
183+
)
178184
print(
179-
"Cannot build ui as the input file cannot be parse.",
185+
"The file " + self.state.input_file + " cannot be parsed.",
180186
file=sys.stderr,
181187
)

geos-trame/src/geos_trame/app/deck/file.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ def _build_inspect_tree_inner( key, obj, path ) -> dict:
221221
"VTKMesh",
222222
"InternalMesh",
223223
"InternalWell",
224+
"VTKWell",
224225
"Perforation",
225226
]
226227
sub_node[ "drawn" ] = False

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,12 @@ def write_files( self ):
160160
files = self._split( pb )
161161

162162
for filepath, content in files.items():
163-
includeName: str = self.input_file.xml_parser.file_to_relative_path[ filepath ]
164163
model_loaded: BaseModel = self.decode_data( content )
165164
model_with_changes: BaseModel = self._apply_changed_properties( model_loaded )
166-
self._append_include_file( model_with_changes, includeName )
165+
166+
if self.input_file.xml_parser.contains_include_files():
167+
includeName: str = self.input_file.xml_parser.get_relative_path_of_file( filepath )
168+
self._append_include_file( model_with_changes, includeName )
167169

168170
model_as_xml: str = self.to_xml( model_with_changes )
169171

geos-trame/src/geos_trame/app/io/xml_parser.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ def get_simulation_deck( self ) -> ElementTree.Element:
6161
return
6262
return self.simulation_deck
6363

64+
def contains_include_files( self ) -> bool:
65+
"""
66+
Return True if the parsed file contains included file or not.
67+
"""
68+
return len( self.file_to_relative_path ) > 0
69+
70+
def get_relative_path_of_file( self, filename: str ) -> str:
71+
"""
72+
Return the relative path of a given filename.
73+
"""
74+
return self.file_to_relative_path[ filename ]
75+
6476
def _read( self ) -> ElementTree.Element:
6577
"""Reads an xml file (and recursively its included files) into memory
6678
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Lucas Givord - Kitware
4+
import asyncio
5+
6+
from trame.widgets import vuetify3
7+
8+
9+
class AlertHandler( vuetify3.VContainer ):
10+
"""
11+
Vuetify component used to display an alert status.
12+
13+
This alert will be displayed in the bottom right corner of the screen.
14+
It will be displayed until closed by the user or after 10 seconds if it is a success or warning.
15+
"""
16+
17+
def __init__( self ):
18+
super().__init__(
19+
fluid=True,
20+
classes="pa-0 ma-0",
21+
)
22+
23+
self.__max_number_of_status = 5
24+
self.__lifetime_of_alert = 10.0
25+
self._status_id = 0
26+
27+
self.state.alerts = []
28+
29+
self.server.controller.on_add_error.add_task( self.add_error )
30+
self.server.controller.on_add_warning.add_task( self.add_warning )
31+
32+
self.generate_alert_ui()
33+
34+
def generate_alert_ui( self ):
35+
"""
36+
Generate the alert UI.
37+
38+
The alert will be displayed in the bottom right corner of the screen.
39+
40+
Use an abritary z-index value to put the alert on top of the other components.
41+
"""
42+
with self:
43+
with vuetify3.VCol( style="width: 40%; position: fixed; right: 50px; bottom: 50px; z-index: 100;", ):
44+
vuetify3.VAlert(
45+
style="max-height: 20vh; overflow-y: auto",
46+
classes="ma-2",
47+
v_for=( "(status, index) in alerts", ),
48+
key="status",
49+
type=( "status.type", "info" ),
50+
text=( "status.message", "" ),
51+
title=( "status.title", "" ),
52+
closable=True,
53+
click_close=( self.on_close, f"[status.id]" ),
54+
)
55+
56+
def add_alert( self, type: str, title: str, message: str ):
57+
"""
58+
Add a status to the stack with a unique id.
59+
If there are more than 5 alerts displayed, remove the oldest.
60+
A warning will be automatically closed after 10 seconds.
61+
"""
62+
self.state.alerts.append( {
63+
"id": self._status_id,
64+
"type": type,
65+
"title": title,
66+
"message": message,
67+
} )
68+
69+
if len( self.state.alerts ) > self.__max_number_of_status:
70+
self.state.alerts.pop( 0 )
71+
72+
alert_id = self._status_id
73+
self._status_id += 1
74+
self.state.dirty( "alerts" )
75+
self.state.flush()
76+
77+
if type == "warning":
78+
asyncio.get_event_loop().call_later( self.__lifetime_of_alert, self.on_close, alert_id )
79+
80+
async def add_warning( self, title: str, message: str ):
81+
"""
82+
Add an alert of type "warning"
83+
"""
84+
self.add_alert( "warning", title, message )
85+
86+
async def add_error( self, title: str, message: str ):
87+
"""
88+
Add an alert of type "error"
89+
"""
90+
self.add_alert( "error", title, message )
91+
92+
def on_close( self, alert_id ):
93+
"""
94+
Remove in the state the alert associated to the given id.
95+
"""
96+
self.state.alerts = list( filter( lambda i: i[ "id" ] != alert_id, self.state.alerts ) )
97+
self.state.flush()

geos-trame/src/geos_trame/app/ui/inspector.py

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import yaml
88
from pydantic import BaseModel
99
from trame.widgets import vuetify3 as vuetify
10+
from trame.widgets import html
1011
from trame_simput import get_simput_manager
1112
from typing import Any
1213

@@ -15,6 +16,7 @@ class Renderable( Enum ):
1516
VTKMESH = "VTKMesh"
1617
INTERNALMESH = "InternalMesh"
1718
INTERNALWELL = "InternalWell"
19+
VTKWELL = "VTKWell"
1820
PERFORATION = "Perforation"
1921

2022

@@ -100,7 +102,7 @@ def get_node_dict( obj, node_id, path ):
100102
title=node_name,
101103
children=children if len( children ) else [],
102104
hidden_children=[],
103-
is_drawable=node_id in ( k for k in Renderable ),
105+
is_drawable=node_id in ( k.value for k in Renderable ),
104106
drawn=False,
105107
)
106108

@@ -180,28 +182,23 @@ def __init__( self, listen_to_active=True, source=None, **kwargs ):
180182
**{
181183
# style
182184
"hoverable": True,
185+
"max_width": 500,
183186
"rounded": True,
184-
# "dense": True,
185-
# "density": "compact",
186-
# "active_color": "blue",
187187
# activation logic
188-
# "activatable": True,
189-
# "active_strategy": "single-independent",
190-
# "activated": ("active_ids", ),
191-
# "update_activated": "(active_ids) => {active_id = active_ids[0]}",
188+
"activatable": True,
189+
"activated": ( "active_ids", ),
190+
"active_strategy": "single-independent",
191+
"update_activated": ( self.change_current_id, "$event" ),
192192
# selection logic
193-
"selectable": True,
194-
"select_strategy": "single-independent",
195-
"selected": ( "active_ids", ),
196-
"update_selected": "(active_ids) => {active_id = active_ids[0]}",
193+
"selectable": False,
197194
**kwargs,
198195
},
199196
)
200197
self.tree = source
201198
self._source = None
202199
self.listen_to_active = listen_to_active
203200

204-
self.state.obj_path = ""
201+
self.state.object_state = [ "", False ]
205202

206203
# register used types from Problem
207204
self.simput_types = []
@@ -225,18 +222,17 @@ def on_change( topic, ids=None, **kwargs ):
225222

226223
with self:
227224
with vuetify.Template( v_slot_append="{ item }" ):
228-
with vuetify.VBtn(
229-
v_if=( "item.is_drawable" ),
230-
icon=True,
231-
flat=True,
232-
slim=True,
233-
input_value=( "item.drawn" ),
234-
click=( self.to_draw_change, "[item.id]" ),
235-
):
236-
vuetify.VIcon(
237-
"{{ ((item.drawn)) ? 'mdi-eye' : 'mdi-eye-off' }}",
238-
v_if=( "item.is_drawable" ),
239-
)
225+
vuetify.VCheckboxBtn( v_if="item.is_drawable",
226+
focused=True,
227+
dense=True,
228+
hide_details=True,
229+
icon=True,
230+
false_icon="mdi-eye-off",
231+
true_icon="mdi-eye",
232+
update_modelValue=( self.to_draw_change, "[ item, item.id, $event ] " ) )
233+
234+
def to_draw_change( self, item, item_id, drawn ):
235+
self.state.object_state = [ item_id, drawn ]
240236

241237
@property
242238
def source( self ):
@@ -298,13 +294,14 @@ def set_source( self, v ):
298294
debug.set_property( key, getattr( active_block, key ) )
299295
debug.commit()
300296

301-
def to_draw_change( self, path ):
302-
self.state.obj_path = path
303-
304-
# def on_active_change(self, **_):
305-
# if self.listen_to_active:
306-
# print("on_active_change")
307-
# self.set_source_proxy(simple.GetActiveSource())
297+
def change_current_id( self, item_id=None ):
298+
"""
299+
Change the current id of the tree.
300+
This function is called when the user click on the tree.
301+
"""
302+
if item_id is None:
303+
# Silently ignore, it could occurs is the user click on the tree
304+
# and this item is already selected
305+
return
308306

309-
# def on_selection_change(self, node_active, **_):
310-
# print("on_selection_change", node_active)
307+
self.state.active_id = item_id

0 commit comments

Comments
 (0)