Skip to content

Commit 98cad2d

Browse files
authored
Merge pull request #29 from LaSi5002/feature/plot-improvements
optimize actor handling for performance
2 parents fe3ec0b + d8b9185 commit 98cad2d

File tree

2 files changed

+158
-152
lines changed

2 files changed

+158
-152
lines changed

src/fourc_webviewer/fourc_webserver.py

Lines changed: 158 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from trame.app import get_server
1616
from trame.decorators import TrameApp, change, controller
1717

18-
import fourc_webviewer.pyvista_render as pv_render
1918
from fourc_webviewer.gui_utils import create_gui
2019
from fourc_webviewer.input_file_utils.fourc_yaml_file_visualization import (
2120
function_plot_figure,
@@ -40,6 +39,10 @@
4039
FourCGeometry,
4140
)
4241

42+
# Global variable
43+
# factor which scales the spheres used to represent nodal design conditions and result descriptions with respect to the problem length scale
44+
PV_SPHERE_FRAC_SCALE = 1.0 / 45.0
45+
4346
# always set pyvista to plot off screen with Trame
4447
pv.OFF_SCREEN = True
4548

@@ -128,8 +131,7 @@ def __init__(
128131
self.state.read_in_status = self.state.all_read_in_statuses[
129132
"vtu_conversion_error"
130133
]
131-
132-
self.update_pyvista_render_objects(init_rendering=True)
134+
self.init_pyvista_render_objects()
133135

134136
# create ui
135137
create_gui(self.server, self._server_vars["render_window"])
@@ -151,7 +153,7 @@ def init_state_and_server_vars(self):
151153
"""Initialize state variables (reactive shared state) and server-side
152154
only variables, particularly the ones related to the fourc yaml
153155
content."""
154-
156+
self._actors = {}
155157
### --- self.state VARIABLES FOR INPUT FILE CONTENT --- ###
156158
# name of the 4C yaml file
157159
self.state.fourc_yaml_name = self._server_vars["fourc_yaml_name"]
@@ -242,61 +244,160 @@ def sync_server_vars_from_state(self):
242244
self.sync_result_description_section_from_state()
243245
self.sync_funct_section_from_state()
244246

245-
def update_pyvista_render_objects(self, init_rendering=False):
246-
"""Update/ initialize pyvista view objects (reader, thresholds, global
247-
COS, ...) for the rendered window. The saved vtu file path is hereby
248-
utilized.
247+
def get_problem_length_scale(self, pv_mesh):
248+
"""Compute problem length scale from the bounds of the considered
249+
pyvista mesh.
249250
250251
Args:
251-
init_rendering (bool): perform initialization tasks? (True:
252-
yes | False: no -> only updating)
252+
pv_mesh (pyvista.UnstructuredGrid): geometry mesh
253+
Returns:
254+
float: maximum coordinate bound difference in 3-dimensions
253255
"""
254256

255-
# initialization tasks
256-
if init_rendering:
257-
# initialization: declare render window as a pyvista plotter
257+
# get maximum bound difference as the problem length scale
258+
return max(
259+
pv_mesh.bounds[1] - pv_mesh.bounds[0],
260+
pv_mesh.bounds[3] - pv_mesh.bounds[2],
261+
pv_mesh.bounds[5] - pv_mesh.bounds[4],
262+
)
263+
264+
def init_pyvista_render_objects(self):
265+
"""Initialize pyvista view objects (reader, thresholds, global COS,
266+
...) for the rendered window.
267+
268+
The saved vtu file path is hereby utilized.
269+
"""
270+
if "render_window" not in self._actors:
258271
self._server_vars["render_window"] = pv.Plotter()
259272

273+
self._server_vars["render_window"].clear_actors()
274+
275+
problem_mesh = pv.read(self.state.vtu_path)
260276
# get problem mesh
261-
self._server_vars["pv_mesh"] = pv.read(self.state.vtu_path)
277+
self._actors["problem_mesh"] = self._server_vars["render_window"].add_mesh(
278+
problem_mesh, color="bisque", opacity=0.2, render=False
279+
)
262280

263281
# get mesh of the selected material
264-
master_mat_ind = self.determine_master_mat_ind_for_current_selection()
265-
self._server_vars["pv_selected_material_mesh"] = self._server_vars[
266-
"pv_mesh"
267-
].threshold(
268-
value=(master_mat_ind - 0.05, master_mat_ind + 0.05),
269-
scalars="element-material",
270-
)
282+
self._actors["material_meshes"] = {}
283+
for material in self.state.materials_section.keys():
284+
# get meshes of materials
285+
master_mat_ind = self.determine_master_mat_ind_for_material(material)
286+
self._actors["material_meshes"][material] = self._server_vars[
287+
"render_window"
288+
].add_mesh(
289+
problem_mesh.threshold(
290+
value=(master_mat_ind - 0.05, master_mat_ind + 0.05),
291+
scalars="element-material",
292+
),
293+
color="darkorange",
294+
opacity=0.7,
295+
render=False,
296+
)
271297

272-
# get nodes of the selected condition geometry + entity
273-
self._server_vars["pv_selected_dc_geometry_entity"] = pv.PointSet(
274-
self._server_vars["pv_mesh"]
275-
).threshold(
276-
value=1.0,
277-
scalars=f"d{self.state.selected_dc_geometry_type.lower()}{self.state.selected_dc_entity.replace('E', '')}",
278-
preference="point",
279-
)
298+
all_dc_entities = [
299+
{"entity": k, "geometry_type": sec_name}
300+
for sec_name, sec in self.state.dc_sections.items()
301+
for k in sec # == sec.keys()
302+
]
303+
self._actors["dc_geometry_entities"] = {}
304+
# get nodes of the selected condition geometries + entities
305+
for dc_entity in all_dc_entities:
306+
points = problem_mesh.threshold(
307+
value=1.0,
308+
scalars=f"d{dc_entity['geometry_type'].lower()}{dc_entity['entity'].replace('E', '')}",
309+
preference="point",
310+
).points
311+
312+
if points.size:
313+
pts = pv.PolyData(points)
314+
r = (
315+
self.get_problem_length_scale(self._actors["problem_mesh"])
316+
* PV_SPHERE_FRAC_SCALE
317+
)
280318

281-
# get coords of node with prescribed result description
282-
self._server_vars["pv_selected_result_description_node_coords"] = (
283-
self._server_vars["pv_mesh"].points[
284-
self.state.result_description_section[
285-
self.state.selected_result_description_id
286-
]["PARAMETERS"]["NODE"]
287-
- 1,
319+
sphere = pv.Sphere(radius=r, theta_resolution=5, phi_resolution=5)
320+
321+
glyphs = pts.glyph(
322+
geom=sphere, scale=False, orient=False
323+
) # in PolyData
324+
self._actors["dc_geometry_entities"][
325+
(dc_entity["entity"], dc_entity["geometry_type"])
326+
] = self._server_vars["render_window"].add_mesh(
327+
glyphs,
328+
color="navy",
329+
opacity=1.0,
330+
render=False,
331+
)
332+
333+
self._actors["result_description_nodes"] = {}
334+
all_result_descriptions = self.state.result_description_section.keys()
335+
336+
for dc in all_result_descriptions:
337+
node_coords = problem_mesh.points[
338+
self.state.result_description_section[dc]["PARAMETERS"]["NODE"] - 1,
288339
:,
289340
]
290-
)
341+
self._actors["result_description_nodes"][dc] = self._server_vars[
342+
"render_window"
343+
].add_mesh(
344+
pv.Sphere(
345+
center=node_coords,
346+
radius=self.get_problem_length_scale(problem_mesh)
347+
* PV_SPHERE_FRAC_SCALE,
348+
),
349+
color="deepskyblue",
350+
render=False,
351+
)
352+
self.update_pyvista_render_objects()
291353

292-
# update plotter / rendering
293-
pv_render.update_pv_plotter(
294-
self._server_vars["render_window"],
295-
self._server_vars["pv_mesh"],
296-
self._server_vars["pv_selected_material_mesh"],
297-
self._server_vars["pv_selected_dc_geometry_entity"],
298-
self._server_vars["pv_selected_result_description_node_coords"],
299-
)
354+
def update_pyvista_render_objects(self):
355+
"""Update/ initialize pyvista view objects (reader, thresholds, global
356+
COS, ...) for the rendered window.
357+
358+
The saved vtu file path is hereby utilized.
359+
"""
360+
legend_items = []
361+
362+
for dc in self._actors["result_description_nodes"].values():
363+
dc.SetVisibility(False)
364+
if (
365+
self.state.selected_main_section_name == "RESULT DESCRIPTION"
366+
and self.state.selected_result_description_id
367+
and self.state.selected_result_description_id
368+
in self._actors["result_description_nodes"].keys()
369+
):
370+
self._actors["result_description_nodes"][
371+
self.state.selected_result_description_id
372+
].SetVisibility(True)
373+
legend_items.append(("Selected result description", "deepskyblue"))
374+
375+
for rd in self._actors["dc_geometry_entities"].values():
376+
rd.SetVisibility(False)
377+
if (
378+
self.state.selected_main_section_name == "DESIGN CONDITIONS"
379+
and self.state.selected_dc_entity
380+
and self.state.selected_dc_geometry_type
381+
):
382+
self._actors["dc_geometry_entities"][
383+
(self.state.selected_dc_entity, self.state.selected_dc_geometry_type)
384+
].SetVisibility(True)
385+
legend_items.append(("Selected design condition", "navy"))
386+
387+
for mat in self._actors["material_meshes"].values():
388+
mat.SetVisibility(False)
389+
if (
390+
self.state.selected_material
391+
and self.state.selected_main_section_name == "MATERIALS"
392+
):
393+
self._actors["material_meshes"][self.state.selected_material].SetVisibility(
394+
True
395+
)
396+
legend_items.append(("Selected material", "orange"))
397+
398+
self._server_vars["render_window"].remove_legend()
399+
if legend_items:
400+
self._server_vars["render_window"].add_legend(labels=legend_items)
300401

301402
def init_general_sections_state_and_server_vars(self):
302403
"""Get the general sections and cluster them into subsections. For
@@ -966,6 +1067,9 @@ def change_selected_main_section_name(self, selected_main_section_name, **kwargs
9661067
selected_main_section_name
9671068
]["subsections"][0]
9681069

1070+
# update plotter / render objects
1071+
self.update_pyvista_render_objects()
1072+
9691073
@change("selected_section_name")
9701074
def change_selected_section_name(self, selected_section_name, **kwargs):
9711075
"""Reaction to change of state.selected_section_name."""
@@ -1191,7 +1295,7 @@ def click_convert_button(self, **kwargs):
11911295
]
11921296
else:
11931297
# reset view
1194-
self.update_pyvista_render_objects()
1298+
self.init_pyvista_render_objects()
11951299
self._server_vars["render_window"].reset_camera()
11961300
self.ctrl.view_reset_camera()
11971301
self.ctrl.view_update()
@@ -1259,21 +1363,20 @@ def convert_string2num_all_sections(self):
12591363
self.state.result_description_section
12601364
)
12611365

1262-
def determine_master_mat_ind_for_current_selection(self):
1263-
"""Determines the real master/source material of the currently selected
1264-
material. Accounts for CLONING MATERIAL MAP by going one step further
1265-
and checking for the real source material recursively (important in
1266-
multi-field problem settings, e.g., in SSTI, the procedure finds the
1267-
structural material).
1366+
def determine_master_mat_ind_for_material(self, material):
1367+
"""Determines the real master/source material of a material. Accounts
1368+
for CLONING MATERIAL MAP by going one step further and checking for the
1369+
real source material recursively (important in multi-field problem
1370+
settings, e.g., in SSTI, the procedure finds the structural material).
12681371
12691372
Returns:
12701373
int: id of the real master material of the currently
12711374
selected material.
12721375
"""
12731376
# get id of the master material
1274-
master_mat_id = self.state.materials_section[self.state.selected_material][
1275-
"RELATIONSHIPS"
1276-
]["MASTER MATERIAL"]
1377+
master_mat_id = self.state.materials_section[material]["RELATIONSHIPS"][
1378+
"MASTER MATERIAL"
1379+
]
12771380

12781381
# it could now be that the master material is a TARGET material
12791382
# during cloning material map (and its master might be also a

src/fourc_webviewer/pyvista_render.py

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)