From ac7cdcde8e83e06cec82232a255617f5d1dcc066 Mon Sep 17 00:00:00 2001 From: Lasse Date: Thu, 30 Oct 2025 00:34:19 +0100 Subject: [PATCH] plot improvements and optimization --- src/fourc_webviewer/fourc_webserver.py | 213 ++++++++++++++++++------- src/fourc_webviewer/pyvista_render.py | 97 ----------- 2 files changed, 158 insertions(+), 152 deletions(-) delete mode 100644 src/fourc_webviewer/pyvista_render.py diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index 2988b3a..e410412 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -14,7 +14,6 @@ from trame.app import get_server from trame.decorators import TrameApp, change, controller -import fourc_webviewer.pyvista_render as pv_render from fourc_webviewer.gui_utils import create_gui from fourc_webviewer.input_file_utils.fourc_yaml_file_visualization import ( function_plot_figure, @@ -32,6 +31,10 @@ FourCGeometry, ) +# Global variable +# factor which scales the spheres used to represent nodal design conditions and result descriptions with respect to the problem length scale +PV_SPHERE_FRAC_SCALE = 1.0 / 45.0 + # always set pyvista to plot off screen with Trame pv.OFF_SCREEN = True @@ -117,8 +120,7 @@ def __init__( self.state.read_in_status = self.state.all_read_in_statuses[ "vtu_conversion_error" ] - - self.update_pyvista_render_objects(init_rendering=True) + self.init_pyvista_render_objects() # create ui create_gui(self.server, self._server_vars["render_window"]) @@ -140,7 +142,7 @@ def init_state_and_server_vars(self): """Initialize state variables (reactive shared state) and server-side only variables, particularly the ones related to the fourc yaml content.""" - + self._actors = {} ### --- self.state VARIABLES FOR INPUT FILE CONTENT --- ### # name of the 4C yaml file self.state.fourc_yaml_name = self._server_vars["fourc_yaml_name"] @@ -228,61 +230,160 @@ def sync_server_vars_from_state(self): self.sync_result_description_section_from_state() self.sync_funct_section_from_state() - def update_pyvista_render_objects(self, init_rendering=False): - """Update/ initialize pyvista view objects (reader, thresholds, global - COS, ...) for the rendered window. The saved vtu file path is hereby - utilized. + def get_problem_length_scale(self, pv_mesh): + """Compute problem length scale from the bounds of the considered + pyvista mesh. Args: - init_rendering (bool): perform initialization tasks? (True: - yes | False: no -> only updating) + pv_mesh (pyvista.UnstructuredGrid): geometry mesh + Returns: + float: maximum coordinate bound difference in 3-dimensions """ - # initialization tasks - if init_rendering: - # initialization: declare render window as a pyvista plotter + # get maximum bound difference as the problem length scale + return max( + pv_mesh.bounds[1] - pv_mesh.bounds[0], + pv_mesh.bounds[3] - pv_mesh.bounds[2], + pv_mesh.bounds[5] - pv_mesh.bounds[4], + ) + + def init_pyvista_render_objects(self): + """Initialize pyvista view objects (reader, thresholds, global COS, + ...) for the rendered window. + + The saved vtu file path is hereby utilized. + """ + if "render_window" not in self._actors: self._server_vars["render_window"] = pv.Plotter() + self._server_vars["render_window"].clear_actors() + + problem_mesh = pv.read(self.state.vtu_path) # get problem mesh - self._server_vars["pv_mesh"] = pv.read(self.state.vtu_path) + self._actors["problem_mesh"] = self._server_vars["render_window"].add_mesh( + problem_mesh, color="bisque", opacity=0.2, render=False + ) # get mesh of the selected material - master_mat_ind = self.determine_master_mat_ind_for_current_selection() - self._server_vars["pv_selected_material_mesh"] = self._server_vars[ - "pv_mesh" - ].threshold( - value=(master_mat_ind - 0.05, master_mat_ind + 0.05), - scalars="element-material", - ) + self._actors["material_meshes"] = {} + for material in self.state.materials_section.keys(): + # get meshes of materials + master_mat_ind = self.determine_master_mat_ind_for_material(material) + self._actors["material_meshes"][material] = self._server_vars[ + "render_window" + ].add_mesh( + problem_mesh.threshold( + value=(master_mat_ind - 0.05, master_mat_ind + 0.05), + scalars="element-material", + ), + color="darkorange", + opacity=0.7, + render=False, + ) - # get nodes of the selected condition geometry + entity - self._server_vars["pv_selected_dc_geometry_entity"] = pv.PointSet( - self._server_vars["pv_mesh"] - ).threshold( - value=1.0, - scalars=f"d{self.state.selected_dc_geometry_type.lower()}{self.state.selected_dc_entity.replace('E', '')}", - preference="point", - ) + all_dc_entities = [ + {"entity": k, "geometry_type": sec_name} + for sec_name, sec in self.state.dc_sections.items() + for k in sec # == sec.keys() + ] + self._actors["dc_geometry_entities"] = {} + # get nodes of the selected condition geometries + entities + for dc_entity in all_dc_entities: + points = problem_mesh.threshold( + value=1.0, + scalars=f"d{dc_entity['geometry_type'].lower()}{dc_entity['entity'].replace('E', '')}", + preference="point", + ).points + + if points.size: + pts = pv.PolyData(points) + r = ( + self.get_problem_length_scale(self._actors["problem_mesh"]) + * PV_SPHERE_FRAC_SCALE + ) - # get coords of node with prescribed result description - self._server_vars["pv_selected_result_description_node_coords"] = ( - self._server_vars["pv_mesh"].points[ - self.state.result_description_section[ - self.state.selected_result_description_id - ]["PARAMETERS"]["NODE"] - - 1, + sphere = pv.Sphere(radius=r, theta_resolution=5, phi_resolution=5) + + glyphs = pts.glyph( + geom=sphere, scale=False, orient=False + ) # in PolyData + self._actors["dc_geometry_entities"][ + (dc_entity["entity"], dc_entity["geometry_type"]) + ] = self._server_vars["render_window"].add_mesh( + glyphs, + color="navy", + opacity=1.0, + render=False, + ) + + self._actors["result_description_nodes"] = {} + all_result_descriptions = self.state.result_description_section.keys() + + for dc in all_result_descriptions: + node_coords = problem_mesh.points[ + self.state.result_description_section[dc]["PARAMETERS"]["NODE"] - 1, :, ] - ) + self._actors["result_description_nodes"][dc] = self._server_vars[ + "render_window" + ].add_mesh( + pv.Sphere( + center=node_coords, + radius=self.get_problem_length_scale(problem_mesh) + * PV_SPHERE_FRAC_SCALE, + ), + color="deepskyblue", + render=False, + ) + self.update_pyvista_render_objects() - # update plotter / rendering - pv_render.update_pv_plotter( - self._server_vars["render_window"], - self._server_vars["pv_mesh"], - self._server_vars["pv_selected_material_mesh"], - self._server_vars["pv_selected_dc_geometry_entity"], - self._server_vars["pv_selected_result_description_node_coords"], - ) + def update_pyvista_render_objects(self): + """Update/ initialize pyvista view objects (reader, thresholds, global + COS, ...) for the rendered window. + + The saved vtu file path is hereby utilized. + """ + legend_items = [] + + for dc in self._actors["result_description_nodes"].values(): + dc.SetVisibility(False) + if ( + self.state.selected_main_section_name == "RESULT DESCRIPTION" + and self.state.selected_result_description_id + and self.state.selected_result_description_id + in self._actors["result_description_nodes"].keys() + ): + self._actors["result_description_nodes"][ + self.state.selected_result_description_id + ].SetVisibility(True) + legend_items.append(("Selected result description", "deepskyblue")) + + for rd in self._actors["dc_geometry_entities"].values(): + rd.SetVisibility(False) + if ( + self.state.selected_main_section_name == "DESIGN CONDITIONS" + and self.state.selected_dc_entity + and self.state.selected_dc_geometry_type + ): + self._actors["dc_geometry_entities"][ + (self.state.selected_dc_entity, self.state.selected_dc_geometry_type) + ].SetVisibility(True) + legend_items.append(("Selected design condition", "navy")) + + for mat in self._actors["material_meshes"].values(): + mat.SetVisibility(False) + if ( + self.state.selected_material + and self.state.selected_main_section_name == "MATERIALS" + ): + self._actors["material_meshes"][self.state.selected_material].SetVisibility( + True + ) + legend_items.append(("Selected material", "orange")) + + self._server_vars["render_window"].remove_legend() + if legend_items: + self._server_vars["render_window"].add_legend(labels=legend_items) def init_general_sections_state_and_server_vars(self): """Get the general sections and cluster them into subsections. For @@ -951,6 +1052,9 @@ def change_selected_main_section_name(self, selected_main_section_name, **kwargs selected_main_section_name ]["subsections"][0] + # update plotter / render objects + self.update_pyvista_render_objects() + @change("selected_material") def change_selected_material(self, selected_material, **kwargs): """Reaction to change of state.selected_material.""" @@ -1148,7 +1252,7 @@ def click_convert_button(self, **kwargs): ] else: # reset view - self.update_pyvista_render_objects() + self.init_pyvista_render_objects() self._server_vars["render_window"].reset_camera() self.ctrl.view_reset_camera() self.ctrl.view_update() @@ -1189,21 +1293,20 @@ def convert_string2num_all_sections(self): self.state.result_description_section ) - def determine_master_mat_ind_for_current_selection(self): - """Determines the real master/source material of the currently selected - material. Accounts for CLONING MATERIAL MAP by going one step further - and checking for the real source material recursively (important in - multi-field problem settings, e.g., in SSTI, the procedure finds the - structural material). + def determine_master_mat_ind_for_material(self, material): + """Determines the real master/source material of a material. Accounts + for CLONING MATERIAL MAP by going one step further and checking for the + real source material recursively (important in multi-field problem + settings, e.g., in SSTI, the procedure finds the structural material). Returns: int: id of the real master material of the currently selected material. """ # get id of the master material - master_mat_id = self.state.materials_section[self.state.selected_material][ - "RELATIONSHIPS" - ]["MASTER MATERIAL"] + master_mat_id = self.state.materials_section[material]["RELATIONSHIPS"][ + "MASTER MATERIAL" + ] # it could now be that the master material is a TARGET material # during cloning material map (and its master might be also a diff --git a/src/fourc_webviewer/pyvista_render.py b/src/fourc_webviewer/pyvista_render.py deleted file mode 100644 index cf7443f..0000000 --- a/src/fourc_webviewer/pyvista_render.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Import modules.""" - -import pyvista as pv -from pyvista.trame.ui import plotter_ui - -# Global variable -# factor which scales the spheres used to represent nodal design conditions and result descriptions with respect to the problem length scale -PV_SPHERE_FRAC_SCALE = 1.0 / 50.0 - - -def update_pv_plotter( - pv_plotter, - mesh, - selected_material_mesh, - selected_dc_geometry_entity, - selected_result_description_node_coords, -): - """Updates the pyvista plotter for the GUI. - - Args: - mesh (pyvista.UnstructuredGrid): problem mesh - selected_material_mesh (pyvista.UnstructuredGrid): mesh - component with - the selected - material. - selected_dc_geometry_entity (pyvista.PointSet): set of points of - the geometric - entity for the - current design - condition selection. - selected_result_description_node_coords (pyvista.pyvista_ndarray): array of - points (nodes) where the selected result description is prescribed. - Returns: - pyvista.Plotter(): plotter object to be integrated in the GUI - """ - - # clear plotter actors - pv_plotter.clear_actors() - - # add mesh to plotter - pv_plotter.add_mesh(mesh, color="bisque", opacity=0.2) - - # add selected material mesh to plotter - pv_plotter.add_mesh( - selected_material_mesh, - color="darkorange", - opacity=0.7, - label="Selected material", - ) - - # add selected design condition mesh to plotter - dc_spheres = pv.MultiBlock() - for i, point in enumerate(selected_dc_geometry_entity.points): - sphere = pv.Sphere( - center=point, radius=get_problem_length_scale(mesh) * PV_SPHERE_FRAC_SCALE - ) - dc_spheres.append(sphere) - pv_plotter.add_mesh( - dc_spheres, - color="navy", - opacity=1.0, - render_points_as_spheres=True, - label="Selected design condition", - ) - - # add selected result description node to plotter - pv_plotter.add_mesh( - pv.Sphere( - center=selected_result_description_node_coords, - radius=get_problem_length_scale(mesh) * PV_SPHERE_FRAC_SCALE, - ), - color="deepskyblue", - label="Selected result description", - ) - - # add plotter legend - pv_plotter.add_legend() - - return pv_plotter - - -def get_problem_length_scale(pv_mesh): - """Compute problem length scale from the bounds of the considered pyvista - mesh. - - Args: - pv_mesh (pyvista.UnstructuredGrid): geometry mesh - Returns: - float: maximum coordinate bound difference in 3-dimensions - """ - - # get maximum bound difference as the problem length scale - return max( - pv_mesh.bounds[1] - pv_mesh.bounds[0], - pv_mesh.bounds[3] - pv_mesh.bounds[2], - pv_mesh.bounds[5] - pv_mesh.bounds[4], - )